Greasy Fork is available in English.

在庫表の改良

入力中Enterで下に移動。コピペ時の改行に対応。各行をナンバリング。文字数チェック。

// ==UserScript==
// @name         在庫表の改良
// @namespace    http://tampermonkey.net/
// @version      1.25
// @description  入力中Enterで下に移動。コピペ時の改行に対応。各行をナンバリング。文字数チェック。
// @license      MIT
// @match        *plus-nao.com/forests/TbMainproducts/mainedit/*
// @match        *plus-nao.com/forests/tb_mainproducts/mainedit/*
// @match        *plus-nao.com/forests/TbMainproducts/registered_mainedit/*
// @match        *plus-nao.com/forests/tb_mainproducts/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;
        input.style.border = countByteLength(input.value) > maxLength ? '2px solid red' : '';
    }

    function attachEventListeners(input, maxLength) {
        if (!input) return;
        input.addEventListener('input', () => {
            highlightInputIfExceedsMaxLength(input, maxLength);
            updateButtonState();
        });
        input.addEventListener('paste', () => setTimeout(() => {
            highlightInputIfExceedsMaxLength(input, maxLength);
            updateButtonState();
        }, 0));
    }

    function initHighlighting() {
        const maxLength = 32;
        const verticalAxisInput = document.getElementById('TbMainproduct縦軸項目名');
        const horizontalAxisInput = document.getElementById('TbMainproduct横軸項目名');

        attachEventListeners(verticalAxisInput, maxLength);
        attachEventListeners(horizontalAxisInput, maxLength);

        highlightInputIfExceedsMaxLength(verticalAxisInput, maxLength);
        highlightInputIfExceedsMaxLength(horizontalAxisInput, maxLength);
    }

    function attachEventListenersForStockSettingTable(input, maxLength) {
        if (!input) return;
        input.addEventListener('input', () => {
            highlightInputIfExceedsMaxLength(input, maxLength);
            updateButtonState();
        });
        input.addEventListener('focus', () => {
            highlightInputIfExceedsMaxLength(input, maxLength);
            updateButtonState();
        });
        input.addEventListener('blur', () => {
            highlightInputIfExceedsMaxLength(input, maxLength);
            updateButtonState();
        });
        input.addEventListener('change', () => {
            highlightInputIfExceedsMaxLength(input, maxLength);
            updateButtonState();
        });
        input.addEventListener('paste', () => setTimeout(() => {
            highlightInputIfExceedsMaxLength(input, maxLength);
            updateButtonState();
        }, 0));
    }

    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);
                attachEventListenersForStockSettingTable(fifthColInput, maxLength);
                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 secondColInputs = document.querySelectorAll('#stockSettingTable table.hontoroku tr td:nth-child(2) input');
        const fifthColInputs = document.querySelectorAll('#stockSettingTable table.hontoroku tr td:nth-child(5) input');
        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();
        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) {
                lines.forEach((line, i) => {
                    if (currentIndex + i < inputs.length) {
                        const currentInput = inputs[currentIndex + i];
                        currentInput.value = line;

                        const event = new Event('change', { bubbles: true });
                        currentInput.dispatchEvent(event);
                    }
                });
            } 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);

                const event = new Event('change', { bubbles: true });
                currentInput.dispatchEvent(event);
            }

            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);
        });
    }

    Object.values(columns).forEach(column => {
        const inputs = getInputs(column);
        addEventListenersToInputs(inputs);
    });

    addRowNumbers(startIndex, endIndex);

})();