// ==UserScript==
// @name 在庫表の改良
// @namespace http://tampermonkey.net/
// @version 1.28
// @description 入力中Enterで下に移動。コピペ時の改行に対応。各行をナンバリング。文字数チェック。重複コードチェック。
// @license MIT
// @match *://plus-nao.com/forests/*/mainedit/*
// @match *://plus-nao.com/forests/*/registered_mainedit/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
function countByteLength(str) {
let length = 0;
for (let char of str) {
length += (char.match(/[^\x00-\xff]/)) ? 2 : 1;
}
return length;
}
function highlightInputIfExceedsMaxLength(input, maxLength) {
if (!input) return;
const isOverLimit = countByteLength(input.value) > maxLength;
if (isOverLimit) {
input.classList.add('error-maxlength');
} else {
input.classList.remove('error-maxlength');
}
}
function attachEventListeners(input, maxLength) {
if (!input) return;
input.addEventListener('input', () => {
highlightInputIfExceedsMaxLength(input, maxLength);
updateButtonState();
});
input.addEventListener('paste', () => {
highlightInputIfExceedsMaxLength(input, maxLength);
updateButtonState();
});
}
function highlightDuplicateCodes() {
const stockSettingTable = document.getElementById('stockSettingTable');
if (!stockSettingTable) return;
const codeInputsFirstColumn = stockSettingTable.querySelectorAll('tr td:nth-child(3) input[type="text"]');
const valuesFirstColumn = {};
const duplicatesFirstColumn = new Set();
const codeInputsSecondColumn = stockSettingTable.querySelectorAll('tr td:nth-child(6) input[type="text"]');
const valuesSecondColumn = {};
const duplicatesSecondColumn = new Set();
codeInputsFirstColumn.forEach(input => input.classList.remove('error-duplicate'));
codeInputsFirstColumn.forEach(input => {
const value = input.value.trim();
if (value) {
if (valuesFirstColumn[value]) {
duplicatesFirstColumn.add(value);
} else {
valuesFirstColumn[value] = true;
}
}
});
codeInputsFirstColumn.forEach(input => {
if (duplicatesFirstColumn.has(input.value.trim())) {
input.classList.add('error-duplicate');
}
});
codeInputsSecondColumn.forEach(input => input.classList.remove('error-duplicate'));
codeInputsSecondColumn.forEach(input => {
const value = input.value.trim();
if (value) {
if (valuesSecondColumn[value]) {
duplicatesSecondColumn.add(value);
} else {
valuesSecondColumn[value] = true;
}
}
});
codeInputsSecondColumn.forEach(input => {
if (duplicatesSecondColumn.has(input.value.trim())) {
input.classList.add('error-duplicate');
}
});
}
function initHighlighting() {
const maxLength = 32;
const verticalAxisInput = document.getElementById('TbMainproduct縦軸項目名');
const horizontalAxisInput = document.getElementById('TbMainproduct横軸項目名');
const inputs = document.querySelectorAll('.hontoroku tr td:nth-child(3) input[type="text"], .hontoroku tr td:nth-child(6) input[type="text"]');
inputs.forEach(input => {
input.addEventListener('input', () => {
highlightDuplicateCodes();
});
});
attachEventListeners(verticalAxisInput, maxLength);
attachEventListeners(horizontalAxisInput, maxLength);
highlightInputIfExceedsMaxLength(verticalAxisInput, maxLength);
highlightInputIfExceedsMaxLength(horizontalAxisInput, maxLength);
highlightDuplicateCodes();
}
const style = document.createElement('style');
style.textContent = `
.error-maxlength {
border: 2px solid red !important;
}
.error-duplicate {
border: 2px solid #ffa600 !important;
}
`;
document.head.appendChild(style);
function attachEventListenersForStockSettingTable(input, maxLength, columnType) {
if (!input) return;
const performDuplicateCheck = () => {
if (columnType === 'first') {
highlightDuplicateCodes();
} else if (columnType === 'second') {
highlightDuplicateCodes();
}
};
input.addEventListener('input', () => {
highlightInputIfExceedsMaxLength(input, maxLength);
performDuplicateCheck();
updateButtonState();
});
input.addEventListener('focus', () => {
highlightInputIfExceedsMaxLength(input, maxLength);
performDuplicateCheck();
updateButtonState();
});
input.addEventListener('blur', () => {
highlightInputIfExceedsMaxLength(input, maxLength);
performDuplicateCheck();
updateButtonState();
});
input.addEventListener('change', () => {
highlightInputIfExceedsMaxLength(input, maxLength);
performDuplicateCheck();
updateButtonState();
});
input.addEventListener('paste', () => {
highlightInputIfExceedsMaxLength(input, maxLength);
performDuplicateCheck();
updateButtonState();
});
}
document.addEventListener('DOMContentLoaded', initHighlighting);
function highlightInputsInStockSettingTable() {
const rows = document.querySelectorAll('#stockSettingTable table.hontoroku tr');
const maxLength = 32;
rows.forEach((row, index) => {
if (index > 0 && index <= 20) {
const secondColInput = row.querySelector('td:nth-child(2) input');
const fifthColInput = row.querySelector('td:nth-child(5) input');
attachEventListenersForStockSettingTable(secondColInput, maxLength, 'first');
attachEventListenersForStockSettingTable(fifthColInput, maxLength, 'second');
highlightInputIfExceedsMaxLength(secondColInput, maxLength);
highlightInputIfExceedsMaxLength(fifthColInput, maxLength);
}
});
}
const getByteLength = (str) => {
let byteLength = 0;
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i);
byteLength += (charCode > 0x7F) ? 2 : 1;
}
return byteLength;
};
const highlightInput = (input, headerByteLength) => {
const inputByteLength = getByteLength(input.value);
const isOverLimit = (headerByteLength + inputByteLength > 19);
input.style.border = isOverLimit ? '2px solid red' : '';
return isOverLimit;
};
const highlightInputsBasedOnByteLength = (headerByteLength) => {
const stockSettingTable = document.getElementById('stockSettingTable');
if (!stockSettingTable) return;
const rows = stockSettingTable.querySelectorAll('tr');
let hasRedBorder = false;
rows.forEach(row => {
const cells = row.querySelectorAll('td');
if (cells.length >= 6) {
const thirdColInput = cells[1].querySelector('input');
const sixthColInput = cells[4].querySelector('input');
if (thirdColInput) {
hasRedBorder = highlightInput(thirdColInput, headerByteLength) || hasRedBorder;
}
if (sixthColInput) {
hasRedBorder = highlightInput(sixthColInput, headerByteLength) || hasRedBorder;
}
}
});
return hasRedBorder;
};
const updateButtonState = () => {
const verticalAxisInput = document.getElementById('TbMainproduct縦軸項目名');
const horizontalAxisInput = document.getElementById('TbMainproduct横軸項目名');
const thirdColInputs = document.querySelectorAll('#stockSettingTable table.hontoroku tr td:nth-child(3) input');
const sixthColInputs = document.querySelectorAll('#stockSettingTable table.hontoroku tr td:nth-child(6) input');
const maxLength = 32;
const headerTextElement = document.querySelector('h2');
const headerTextMatch = headerTextElement ? headerTextElement.textContent.match(/\[(.*?)\]/) : null;
const headerByteLength = headerTextMatch ? getByteLength(headerTextMatch[1]) : 0;
let hasRedBorder = false;
let buttonMessage = '';
const inputsToCheck1 = [verticalAxisInput, horizontalAxisInput];
inputsToCheck1.forEach(input => {
if (input && countByteLength(input.value) > maxLength) {
hasRedBorder = true;
buttonMessage = "項目名を全角16(半角32)文字以内にしてください";
}
});
if (!hasRedBorder) {
const inputsToCheck2 = [...thirdColInputs, ...sixthColInputs];
inputsToCheck2.forEach(input => {
if (input && (headerByteLength + getByteLength(input.value) > 19)) {
hasRedBorder = true;
buttonMessage = "商品コード+SKUを20文字以内にしてください";
}
});
}
const saveButton = document.getElementById('saveAndSkuStock');
if (saveButton && saveButton.value !== "送料を選択してください") {
saveButton.disabled = hasRedBorder;
saveButton.style.cursor = hasRedBorder ? 'not-allowed' : '';
saveButton.value = hasRedBorder ? buttonMessage : '保存してSKU在庫の設定';
}
const registeredSaveButton = document.getElementById('registeredSaveAndSkuStock');
if (registeredSaveButton && registeredSaveButton.value !== "送料を選択してください") {
registeredSaveButton.disabled = hasRedBorder;
registeredSaveButton.style.cursor = hasRedBorder ? 'not-allowed' : '';
registeredSaveButton.value = hasRedBorder ? buttonMessage : '保存してSKU在庫の設定';
}
};
const stockSettingTable = document.getElementById('stockSettingTable');
if (stockSettingTable) {
stockSettingTable.addEventListener('focusout', () => {
setTimeout(() => {
const headerTextElement = document.querySelector('h2');
const headerTextMatch = headerTextElement ? headerTextElement.textContent.match(/\[(.*?)\]/) : null;
const headerByteLength = headerTextMatch ? getByteLength(headerTextMatch[1]) : 0;
highlightInputsBasedOnByteLength(headerByteLength);
updateButtonState();
}, 10);
});
}
const observer = new MutationObserver(() => {
initHighlighting();
highlightInputsInStockSettingTable();
highlightDuplicateCodes();
const headerTextElement = document.querySelector('h2');
const headerTextMatch = headerTextElement ? headerTextElement.textContent.match(/\[(.*?)\]/) : null;
const headerByteLength = headerTextMatch ? getByteLength(headerTextMatch[1]) : 0;
highlightInputsBasedOnByteLength(headerByteLength);
updateButtonState();
});
observer.observe(document.body, { childList: true, subtree: true });
initHighlighting();
highlightInputsInStockSettingTable();
const headerTextElement = document.querySelector('h2');
const headerTextMatch = headerTextElement ? headerTextElement.textContent.match(/\[(.*?)\]/) : null;
const headerByteLength = headerTextMatch ? getByteLength(headerTextMatch[1]) : 0;
highlightInputsBasedOnByteLength(headerByteLength);
updateButtonState();
const divs = document.querySelectorAll('div');
for (const div of divs) {
if (div.textContent.includes("この商品は在庫表の設定変更ができません")) {
return;
}
}
const columns = {
many: {
inputIndex: 1,
codeOffset: 1
},
few: {
inputIndex: 4,
codeOffset: 1
}
};
let startIndex, endIndex;
const url = window.location.href;
if (url.includes("/forests/TbMainproducts/mainedit/") || url.includes("/forests/tb_mainproducts/mainedit/")) {
startIndex = 29;
endIndex = 50;
} else if (url.includes("/forests/TbMainproducts/registered_mainedit/") || url.includes("/forests/tb_mainproducts/registered_mainedit/")) {
startIndex = 50;
endIndex = 71;
} else {
return;
}
function getInputs(column) {
return Array.from(document.querySelectorAll(`table.hontoroku tr td:nth-child(${column.inputIndex}) input[type="text"]:not([readonly])`));
}
function handleEnterKey(inputs) {
return function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const currentIndex = inputs.indexOf(this);
const nextInput = inputs[currentIndex + 1];
if (nextInput) {
nextInput.focus();
}
const event = new Event('change', { bubbles: true });
inputs[currentIndex].dispatchEvent(event);
}
};
}
function handlePaste(inputs) {
return function(e) {
e.preventDefault();
const pasteData = (e.clipboardData || window.clipboardData).getData('text');
const lines = pasteData.split('\n').filter(line => line.trim() !== '');
let currentIndex = inputs.indexOf(this);
if (lines.length === 0) {
return;
}
if (lines.length > 1) {
setTimeout(() => {
lines.forEach((line, i) => {
if (currentIndex + i < inputs.length) {
const currentInput = inputs[currentIndex + i];
currentInput.value = line;
currentInput.dispatchEvent(new Event('change', { bubbles: true }));
}
});
const lastIndex = Math.min(currentIndex + lines.length - 1, inputs.length - 1);
inputs[lastIndex].focus();
}, 0);
} else {
const currentInput = inputs[currentIndex];
const currentText = currentInput.value;
const selectionStart = currentInput.selectionStart;
const selectionEnd = currentInput.selectionEnd;
const newText = currentText.substring(0, selectionStart) + lines[0] + currentText.substring(selectionEnd);
currentInput.value = newText;
const newCursorPosition = selectionStart + lines[0].length;
currentInput.setSelectionRange(newCursorPosition, newCursorPosition);
currentInput.dispatchEvent(new Event('change', { bubbles: true }));
}
if (lines.length === 1 && pasteData.endsWith('\n')) {
return;
}
inputs[Math.min(currentIndex + lines.length - 1, inputs.length - 1)].focus();
};
}
function addEventListenersToInputs(inputs) {
inputs.forEach(input => {
input.addEventListener('keydown', handleEnterKey(inputs));
input.addEventListener('paste', handlePaste(inputs));
});
}
function addRowNumbers(startIndex, endIndex) {
const tableRows = document.querySelectorAll('table.hontoroku tbody tr');
tableRows.forEach((row, index) => {
const th = document.createElement('th');
th.scope = 'row';
th.style.textAlign = 'center';
if (index >= startIndex && index <= endIndex) {
if (index === startIndex) {
th.innerText = '';
} else if (index <= endIndex - 1) {
th.innerText = index - startIndex;
} else {
th.innerText = '';
}
} else {
th.style.display = 'none';
}
row.insertAdjacentElement('afterbegin', th);
});
}
function focusFirstInput() {
const firstInput = document.querySelector('table.hontoroku tr td:nth-child(2) input[type="text"]');
if (firstInput) {
firstInput.focus();
}
}
function addEnterKeyListener() {
const verticalInput = document.getElementById('TbMainproduct縦軸項目名');
if (verticalInput) {
verticalInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
focusFirstInput();
}
});
}
}
addEnterKeyListener();
Object.values(columns).forEach(column => {
const inputs = getInputs(column);
addEventListenersToInputs(inputs);
});
addRowNumbers(startIndex, endIndex);
})();