タイトルの改良

入力されている全文をポップアップ表示し、ポップアップ内で編集可能。不要なスペースと重複ワードの検出・削除。全角スペースを半角に。文字数カウンターを追加。ブラウザタブタイトルにコードを追加。

// ==UserScript==
// @name         タイトルの改良
// @namespace    http://tampermonkey.net/
// @version      1.31
// @description  入力されている全文をポップアップ表示し、ポップアップ内で編集可能。不要なスペースと重複ワードの検出・削除。全角スペースを半角に。文字数カウンターを追加。ブラウザタブタイトルにコードを追加。
// @license      MIT
// @match        *://plus-nao.com/forests/*/mainedit/*
// @match        *://plus-nao.com/forests/*/registered_mainedit/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    document.title += "/" + window.location.href.split('/').pop();

    const MAX_LENGTH = 255;
    let isEditingPopup = false;

    const style = document.createElement('style');
    style.textContent = `
        .cursor-warning {
            color: red;
        }
    `;

    document.head.appendChild(style);

    window.addEventListener('load', function() {
        const targetTd = document.querySelector('td[colspan="3"]:has(input[name="data[TbMainproduct][daihyo_syohin_name]"])');
        if (targetTd) {
            const computedStyle = window.getComputedStyle(targetTd);
            const paddingTop = computedStyle.paddingTop;

            if (paddingTop === '7px') {
                targetTd.style.position = 'relative';
                targetTd.style.paddingTop = '30px';
            }
        }

        const inputFieldId = 'TbMainproductDaihyoSyohinName';
        const inputField = document.getElementById(inputFieldId);
        if (!inputField) return;

        const wrapperDiv = document.createElement('div');
        wrapperDiv.style.position = 'relative';
        inputField.parentNode.insertBefore(wrapperDiv, inputField);
        wrapperDiv.appendChild(inputField);
        inputField.style.width = 'calc(100% - 60px)';

        const popupStyle = `
            position: absolute;
            background-color: white;
            border: 2px solid #ccc;
            border-radius: 5px;
            padding: 4px 10px;
            z-index: 1000;
            display: none;
            overflow: auto;
            white-space: pre-wrap;
            word-wrap: break-word;
            box-sizing: border-box;
            width: calc(100% - 60px);
        `;

        const popup = document.createElement('div');
        popup.className = 'title-popup';
        popup.style.cssText = popupStyle;
        popup.contentEditable = true;
        wrapperDiv.appendChild(popup);

        function syncPopupToInput() {
            const updatedText = popup.textContent.replace(/\n/g, ' ');
            inputField.value = updatedText;
            updateButtonVisibility();
            updateCursorPosition(true);
        }

        function updatePopup() {
            if (inputField === document.activeElement && inputField.value.trim() !== '') {
                popup.textContent = inputField.value;
                popup.style.display = 'block';
                updatePopupText();
            } else {
                popup.style.display = 'none';
            }
        }

        function updatePopupText() {
            const text = inputField.value;
            popup.textContent = text;
        }

        const popupElement = document.querySelector('#popup');

        const observer = new MutationObserver(function(mutationsList) {
            for (const mutation of mutationsList) {
                if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
                    if (popupElement.style.display === 'none') {
                        popup.style.display = 'none';
                    }
                }
            }
        });

        if (popupElement) {
            observer.observe(popupElement, { attributes: true });
        }

        document.addEventListener('click', function(event) {
            if (popup.style.display === 'block' &&
                !popup.contains(event.target) &&
                !inputField.contains(event.target) &&
                !event.target.closest('.suggest-popup')) {
                const selection = window.getSelection();
                if (selection.rangeCount > 0) {
                    const range = selection.getRangeAt(0);
                    const startContainer = range.startContainer;
                    const endContainer = range.endContainer;

                    if (wrapperDiv.contains(startContainer) || wrapperDiv.contains(endContainer)) {
                        return;
                    }
                }

                popup.style.display = 'none';
                inputField.blur();
                updateButtonVisibility();
                updateCursorPosition(false);
            }
        });

        const textObserver = new MutationObserver(() => {
            const updatedText = popup.textContent.replace(/\n/g, ' ');
            inputField.value = updatedText;
            updateButtonVisibility();
            updateCursorPosition(true);
        });

        if (popup) {
            textObserver.observe(popup, { childList: true, subtree: true, characterData: true });
        }

        popup.addEventListener('mouseup', () => {
            updateCursorPosition(true);
        });

        popup.addEventListener('focus', function() {
            isEditingPopup = true;
            updateCursorPosition(true);
        });

        popup.addEventListener('blur', function() {
            if (popup.textContent.length > MAX_LENGTH) {
                alert(`入力可能な文字数を超えています。256字以降は切り捨てられます。`);
            }

            const updatedText = popup.textContent.replace(/\n/g, ' ');
            popup.textContent = updatedText;
            inputField.value = updatedText;
            validatePopupInput();
            isEditingPopup = false;
            updateCursorPosition(false);
        });

        popup.addEventListener('keydown', function (event) {
            const selection = window.getSelection();
            const range = selection.getRangeAt(0);
            const cursorOffset = range.startOffset;

            if (event.key === 'Enter') {
                const beforeCursor = popup.textContent.slice(0, cursorOffset);
                const afterCursor = popup.textContent.slice(cursorOffset);

                popup.textContent = beforeCursor + ' ' + afterCursor;

                const newRange = document.createRange();
                const firstChild = popup.firstChild;

                if (firstChild && firstChild.nodeType === 3) {
                    const newCursorPosition = beforeCursor.length + 1;
                    newRange.setStart(firstChild, newCursorPosition);
                    newRange.collapse(true);

                    selection.removeAllRanges();
                    selection.addRange(newRange);

                    setTimeout(() => {
                        updateCursorPosition(document.activeElement === popup, newCursorPosition);
                    }, 0);
                }

                inputField.value = popup.textContent;

                event.preventDefault();
            } else {
                setTimeout(() => {
                    updateCursorPosition(document.activeElement === popup);
                }, 0);
            }
        });

        popup.addEventListener('paste', function (event) {
            event.preventDefault();

            const clipboardData = event.clipboardData || window.clipboardData;
            const pasteText = clipboardData.getData('text/plain');

            const selection = window.getSelection();
            const range = selection.getRangeAt(0);
            const cursorOffset = range.startOffset;

            range.deleteContents();

            const beforeCursor = popup.textContent.slice(0, cursorOffset);
            const afterCursor = popup.textContent.slice(cursorOffset);

            const updatedPasteText = pasteText.replace(/\n/g, ' ');

            popup.textContent = beforeCursor + updatedPasteText + afterCursor;

            const newCursorPosition = beforeCursor.length + updatedPasteText.length;

            const newRange = document.createRange();
            newRange.setStart(popup.firstChild, newCursorPosition);
            newRange.collapse(true);

            selection.removeAllRanges();
            selection.addRange(newRange);

            inputField.value = popup.textContent;

            updateButtonVisibility();
            updateCursorPosition(true);
        });

        popup.addEventListener('input', () => {
            const selection = window.getSelection();
            const range = selection.getRangeAt(0);
            const startOffset = range.startOffset;
            const startNode = range.startContainer;

            const text = popup.textContent;

            const updatedText = text.replace(/\n/g, ' ');
            inputField.value = updatedText;

            updateButtonVisibility();
            updateCursorPosition(true);

            const newRange = document.createRange();
            newRange.setStart(startNode, Math.min(startOffset, text.length));
            newRange.collapse(true);
            selection.removeAllRanges();
            selection.addRange(newRange);
        });

        let lastCursorPosition = null;

        const suggestPopupElements = document.querySelectorAll('.suggest-popup');

        suggestPopupElements.forEach(suggestPopup => {
            suggestPopup.addEventListener('mousedown', (e) => {
                if (!isEditingPopup) return;
                e.preventDefault();

                const selection = window.getSelection();
                if (selection.rangeCount > 0) {
                    const range = selection.getRangeAt(0);
                    lastCursorPosition = {
                        node: range.startContainer,
                        offset: range.startOffset,
                    };
                }
            });

            suggestPopup.addEventListener('click', (e) => {
                if (!isEditingPopup) return;
                if (e.target.classList.contains('add-word-button')) return;

                if (lastCursorPosition) {
                    const selection = window.getSelection();
                    const newRange = document.createRange();
                    newRange.setStart(lastCursorPosition.node, lastCursorPosition.offset);
                    newRange.collapse(true);

                    selection.removeAllRanges();
                    selection.addRange(newRange);

                    updateCursorPosition(true);
                }
            });
        });

        function updateInputField() {
            inputField.value = popup.textContent;
            updateButtonVisibility();
            updateCursorPosition(true);
        }

        function updateInputFieldCursorFromPopup() {
            const selection = window.getSelection();
            const range = selection.getRangeAt(0);
            const offset = range.startOffset;
            const text = popup.textContent;
            const cursorPos = getCharacterOffsetFromPopup(text, offset);
            inputField.setSelectionRange(cursorPos, cursorPos);
            inputField.focus();
            updateCursorPosition(true);
        }

        function getCharacterOffsetFromPopup(text, offset) {
            return offset;
        }

        const spaceFixButton = createButton('スペース修正');
        const removeDuplicatesButton = createButton('重複削除');
        const cursorPosition = createCursorPosition();
        wrapperDiv.appendChild(cursorPosition);

        addHighlightStyles();

        if (isRegisteredEditPage()) {
            const buttonContainer = document.createElement('div');
            buttonContainer.style.position = 'absolute';
            buttonContainer.style.top = '-29px';
            buttonContainer.style.right = '55px';
            buttonContainer.style.display = 'flex';
            buttonContainer.style.flexDirection = 'row';
            buttonContainer.style.gap = '10px';
            buttonContainer.style.zIndex = '1000';

            inputField.parentNode.style.position = 'relative';
            inputField.parentNode.appendChild(buttonContainer);

            setButtonStyles(removeDuplicatesButton, {
                backgroundColor: 'transparent',
                color: '#000000',
                border: '1px solid #ccc',
                padding: '0px 7px',
                marginLeft: '5px',
                marginTop: '4px',
                borderRadius: '5px',
                cursor: 'pointer',
                fontSize: '12px',
                visibility: 'hidden',
                transition: 'background-color 0.3s, transform 0.1s',
            });

            setButtonStyles(spaceFixButton, {
                backgroundColor: 'transparent',
                color: '#000000',
                border: '1px solid #ccc',
                padding: '0px 7px',
                marginLeft: '5px',
                marginTop: '4px',
                borderRadius: '5px',
                cursor: 'pointer',
                fontSize: '12px',
                visibility: 'hidden',
                transition: 'background-color 0.3s, transform 0.1s',
            });

            buttonContainer.appendChild(spaceFixButton);
            buttonContainer.appendChild(removeDuplicatesButton);

            updateButtonVisibility();

        } else if (isMainEditPage()) {
            const buttonContainer = document.createElement('div');
            buttonContainer.style.position = 'absolute';
            buttonContainer.style.top = '-29px';
            buttonContainer.style.right = '55px';
            buttonContainer.style.display = 'flex';
            buttonContainer.style.flexDirection = 'row';
            buttonContainer.style.gap = '10px';
            buttonContainer.style.zIndex = '1000';

            inputField.parentNode.style.position = 'relative';
            inputField.parentNode.appendChild(buttonContainer);

            setButtonStyles(removeDuplicatesButton, {
                backgroundColor: 'transparent',
                color: '#000000',
                border: '1px solid #ccc',
                padding: '0px 7px',
                marginLeft: '5px',
                marginTop: '4px',
                borderRadius: '5px',
                cursor: 'pointer',
                fontSize: '12px',
                visibility: 'hidden',
                transition: 'background-color 0.3s, transform 0.1s',
            });

            setButtonStyles(spaceFixButton, {
                backgroundColor: 'transparent',
                color: '#000000',
                border: '1px solid #ccc',
                padding: '0px 7px',
                marginLeft: '5px',
                marginTop: '4px',
                borderRadius: '5px',
                cursor: 'pointer',
                fontSize: '12px',
                visibility: 'hidden',
                transition: 'background-color 0.3s, transform 0.1s',
            });

            buttonContainer.appendChild(spaceFixButton);
            buttonContainer.appendChild(removeDuplicatesButton);

            updateButtonVisibility();
        }

        inputField.addEventListener('blur', function() {
            updateCursorPosition(false);
        });
        inputField.addEventListener('focus', function() {
            updatePopup();
            updateCursorPosition(true);
        });
        inputField.addEventListener('input', function() {
            updatePopup();
        });

        inputField.addEventListener('keyup', function() {
            updateCursorPosition(true);
        });
        inputField.addEventListener('click', function() {
            updateCursorPosition(true);
        });

        spaceFixButton.addEventListener('click', function(event) {
            event.preventDefault();
            event.stopPropagation();
            handleSpaceFixClick(inputField, spaceFixButton);
            addClickFeedback(spaceFixButton);
        });

        removeDuplicatesButton.addEventListener('click', function(event) {
            event.preventDefault();
            event.stopPropagation();
            handleRemoveDuplicatesClick(inputField, removeDuplicatesButton);
            addClickFeedback(removeDuplicatesButton);
        });

        removeDuplicatesButton.addEventListener('mouseover', function() {
            const duplicates = getDuplicateWords(inputField.value);
            if (duplicates.length > 0) {
                removeDuplicatesButton.title = `重複ワード: ${duplicates.join(', ')}`;
            } else {
                removeDuplicatesButton.title = '';
            }
        });

        function addClickFeedback(button) {
            button.style.transform = 'scale(0.9)';
            setTimeout(() => {
                button.style.transform = 'scale(1)';
            }, 100);
        }

        function attachContainerToElement(container, selector) {
            const targetButton = document.querySelector(selector);
            if (targetButton) {
                const wrapper = document.createElement('div');
                wrapper.style.display = 'inline-flex';
                wrapper.style.alignItems = 'flex-end';

                targetButton.parentNode.insertBefore(wrapper, targetButton.nextSibling);
                wrapper.appendChild(targetButton);
                wrapper.appendChild(container);
            }
        }

        function createButton(textContent) {
            const btn = document.createElement('button');
            btn.textContent = textContent;
            return btn;
        }

        function createCursorPosition() {
            const span = document.createElement('span');
            span.style.marginLeft = '3px';
            span.style.fontSize = '11px';
            span.style.verticalAlign = 'middle';
            return span;
        }

        function addHighlightStyles() {
            const style = document.createElement('style');
            style.innerHTML = `
                .highlight {
                    border: 2px solid #ff0000;
                    background-color: #fff5f5;
                }
            `;
            document.head.appendChild(style);
        }

        function isRegisteredEditPage() {
            return window.location.href.includes('/forests/TbMainproducts/registered_mainedit/') ||
                window.location.href.includes('/forests/tb_mainproducts/registered_mainedit/');
        }

        function isMainEditPage() {
            return window.location.href.includes('/forests/TbMainproducts/mainedit/') ||
                window.location.href.includes('/forests/tb_mainproducts/mainedit/');
        }

        function setButtonStyles(button, styles) {
            Object.assign(button.style, styles);

            button.addEventListener('mouseover', function() {
                button.style.backgroundColor = '#f0f0f0';
            });
            button.addEventListener('mouseout', function() {
                button.style.backgroundColor = 'transparent';
            });

            button.addEventListener('mousedown', function() {
                button.style.transform = 'scale(0.95)';
            });
            button.addEventListener('mouseup', function() {
                button.style.transform = 'scale(1)';
            });
        }

        function attachButtonToElement(button, selector, callback) {
            const targetButton = document.querySelector(selector);
            if (targetButton) {
                targetButton.parentNode.insertBefore(button, targetButton.nextSibling);
                callback();
            }
        }

        function attachButtonToElementInTd(button, tagName, includeText1, includeText2, callback) {
            const parentTd = inputField.closest('td');
            if (!parentTd) {
                return;
            }

            const targetElements = parentTd.getElementsByTagName(tagName);
            for (let i = 0; i < targetElements.length; i++) {
                if (targetElements[i].innerHTML.includes(includeText1) && targetElements[i].innerHTML.includes(includeText2)) {
                    targetElements[i].appendChild(button);
                    callback();
                    return;
                }
            }
        }

        function updateCursorPosition(focused, customPosition = null) {
            let position;
            let totalLength;

            if (focused && inputField === document.activeElement) {
                position = inputField.selectionStart;
                totalLength = inputField.value.length;
            } else if (focused && popup === document.activeElement) {
                const selection = window.getSelection();
                position = customPosition !== null ? customPosition : selection.anchorOffset;
                totalLength = popup.textContent.length;
            } else {
                position = 0;
                totalLength = inputField.value.length;
            }

            cursorPosition.textContent = focused ? `${position}/${totalLength}` : `${totalLength}`;

            if (totalLength > MAX_LENGTH) {
                cursorPosition.classList.add('cursor-warning');
            } else {
                cursorPosition.classList.remove('cursor-warning');
            }
        }

        function validatePopupInput() {
            let currentText = popup.textContent;
            currentText = currentText.replace(/\n/g, ' ');
            if (currentText.length > MAX_LENGTH) {
                currentText = currentText.substring(0, MAX_LENGTH);
                popup.textContent = currentText;
                inputField.value = currentText;
            }
        }

        function updateButtonVisibility() {
            if (isEditingPopup) {
                return;
            }

            const value = inputField.value;
            const hasSpaceIssues = value.match(/\s{2,}| |^[\s ]+|[\s ]+$/);
            const hasDuplicates = hasDuplicateWords(value);

            if (hasSpaceIssues) {
                spaceFixButton.style.visibility = 'visible';
                inputField.classList.add('highlight');
            } else {
                spaceFixButton.style.visibility = 'hidden';
            }

            if (hasDuplicates) {
                removeDuplicatesButton.style.visibility = 'visible';
                inputField.classList.add('highlight');
            } else {
                removeDuplicatesButton.style.visibility = 'hidden';
                if (!hasSpaceIssues) {
                    inputField.classList.remove('highlight');
                }
            }

            updateCursorPosition(document.activeElement === inputField);
        }

        function handleSpaceFixClick(inputField, button) {
            const trimmedValue = inputField.value.trim();
            let processedValue = trimmedValue.replace(/\s{2,}/g, ' ');
            processedValue = processedValue.replace(/ /g, ' ');
            inputField.value = processedValue;
            button.style.visibility = 'hidden';
            updateButtonVisibility();
            updatePopupContent();
            updateCursorPosition(document.activeElement === inputField);
        }

        function updatePopupContent() {
            const text = inputField.value;
            popup.textContent = text;
        }

        function handleRemoveDuplicatesClick(inputField, button) {
            const value = inputField.value;
            const words = value.split(/\s+/);
            const uniqueWords = [...new Set(words)];
            const processedValue = uniqueWords.join(' ');
            if (value !== processedValue) {
                inputField.value = processedValue;
                inputField.classList.add('highlight');
                updatePopupContent();
            } else {
                inputField.classList.remove('highlight');
            }
            button.style.visibility = 'hidden';
            updateButtonVisibility();
            updateCursorPosition(document.activeElement === inputField);
        }

        function hasDuplicateWords(value) {
            const words = value.split(/\s+/).filter(word => word.trim() !== '');
            const uniqueWords = new Set(words);
            return uniqueWords.size < words.length;
        }

        function getDuplicateWords(value) {
            const words = value.split(/\s+/).filter(word => word.trim() !== '');
            const wordCount = {};
            const duplicates = [];

            words.forEach(word => {
                wordCount[word] = (wordCount[word] || 0) + 1;
            });

            for (const word in wordCount) {
                if (wordCount[word] > 1) {
                    duplicates.push(word);
                }
            }

            return duplicates;
        }
    });
})();