您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
本登録時にタイトルを送信し収集。収集されたデータからワード候補を表示。
// ==UserScript== // @name タイトル入力補助 // @namespace http://tampermonkey.net/ // @version 1.10 // @description 本登録時にタイトルを送信し収集。収集されたデータからワード候補を表示。 // @license MIT // @match *://plus-nao.com/forests/*/mainedit/* // @match *://plus-nao.com/forests/*/registered_mainedit/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @run-at document-start // ==/UserScript== (async function () { 'use strict'; const url = 'https://raw.githubusercontent.com/NEL227/my-data-repo/main/data/NGwords.txt'; const dbName = 'ngWordsDB'; const storeName = 'ngWordsStore'; const keyName = 'ngWords'; let ngWords = []; const db = await openDatabase(); try { const cachedData = await getFromDB(db, storeName, keyName); const oneDayInMillis = 24 * 60 * 60 * 1000; const now = new Date(); if (cachedData && now - new Date(cachedData.timestamp) <= oneDayInMillis) { ngWords = cachedData.words; } } catch (error) {} initMainScript(ngWords); try { const response = await fetch(url); if (response.ok) { const text = await response.text(); const newWords = text.split('\n').map(word => word.trim()).filter(word => word); if (JSON.stringify(newWords) !== JSON.stringify(ngWords)) { ngWords = newWords; await saveToDB(db, storeName, { id: keyName, words: ngWords, timestamp: new Date() }); } } } catch (error) {} function openDatabase() { return new Promise((resolve) => { const request = indexedDB.open(dbName, 1); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName, { keyPath: 'id' }); } }; }); } function getFromDB(db, store, key) { return new Promise((resolve, reject) => { const transaction = db.transaction([store], 'readonly'); const objectStore = transaction.objectStore(store); const request = objectStore.get(key); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(); }); } function saveToDB(db, store, data) { return new Promise((resolve, reject) => { const transaction = db.transaction([store], 'readwrite'); const objectStore = transaction.objectStore(store); const request = objectStore.put(data); request.onsuccess = () => resolve(); request.onerror = () => reject(); }); } function initMainScript(ngWords) { (function() { 'use strict'; const jsonURL = 'https://raw.githubusercontent.com/NEL227/my-data-repo/main/data/sorted_data.json'; GM_addStyle(` #popup { position: fixed; top: 1%; left: 0.5%; width: 400px; height: 800px; max-width: 100%; max-height: 98%; background: white; border: 1px solid black; padding: 10px; padding-left: 15px; box-shadow: 0 0 10px rgba(0,0,0,0.5); z-index: 10000; overflow-y: auto; display: none; border-radius: 5px; box-sizing: border-box; } #popup-header { display: flex; justify-content: center; align-items: center; font-weight: bold; font-size: 16px; height: 20px; position: sticky; top: -11px; background-color: white; z-index: 10; padding: 10px; border-bottom: 1px solid #ddd; } #popup-content { height: auto; overflow-y: visible; box-sizing: border-box; } #popup-close { cursor: pointer; background: transparent; color: black; border: none; font-size: 24px; padding: 10px; position: absolute; top: -11px; left: 1px; line-height: 1; border-radius: 5px; position: sticky; z-index: 11; } #popup-content ul { padding: 0; list-style: none; display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; margin: 0; } #popup-content ul li { padding: 3px; padding-right: 5px; font-size: 14px; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center; } .add-word-button { background-color: #ffffff; color: #4CAF50; border: 1px solid #4CAF50; padding: 3px; cursor: pointer; font-size: 12px; margin-left: 5px; border-radius: 6px; transition: background-color 0.2s ease, transform 0.2s ease; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; position: relative; } .add-word-button::before { content: '📑'; font-size: 14px; display: block; position: relative; top: -1px; left: 1px; } .add-word-button::after { content: ''; position: absolute; top: -5px; left: -5px; width: 34px; height: 34px; z-index: 0; } .add-word-button:hover { background-color: #4CAF50; color: #ffffff; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .add-word-button:active { background-color: #388E3C; transform: translateY(0); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } #show-subwords-button { background-color: #4CAF50; color: #fff; border: none; padding: 3px; border-radius: 5px; cursor: pointer; font-size: 12px; margin-top: 5px; display: block; width: 100px; text-align: center; transition: background-color 0.2s ease, transform 0.2s ease; } #show-subwords-button:hover:not(.disabled) { background-color: #388E3C; } #show-subwords-button:active:not(.disabled) { transform: translateY(2px); } #show-subwords-button.disabled { background-color: #ccc; cursor: default; } #show-subwords-button.active { background-color: #4CAF50; color: #ffffff; cursor: default; } #settings-button { background-color: #ffffff; color: #4CAF50; border: 1px solid #4CAF50; padding: 3px; cursor: pointer; font-size: 12px; border-radius: 6px; transition: background-color 0.2s ease, transform 0.2s ease; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-left: 5px; margin-top: 5.5px; } #settings-button::before { content: '⚙️'; font-size: 14px; display: block; position: relative; top: -0.5px; } #settings-button:hover { background-color: #4CAF50; color: #ffffff; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } #settings-button:active { background-color: #388E3C; transform: translateY(0); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } #settings-popup { position: fixed; top: 20%; left: 50%; transform: translateX(-50%); width: 300px; background: white; border: 1px solid black; padding: 10px; padding-left: 30px; box-shadow: 0 0 10px rgba(0,0,0,0.5); z-index: 10001; display: none; border-radius: 5px; box-sizing: border-box; } #settings-popup label { display: block; margin-bottom: 5px; } #settings-popup input, #settings-popup button { box-sizing: border-box; width: calc(100% - 20px); padding: 5px; margin-bottom: 10px; display: block; } #settings-popup button { background-color: #4CAF50; color: #fff; border: none; border-radius: 5px; cursor: pointer; font-size: 12px; transition: background-color 0.2s ease, transform 0.2s ease; display: block; } #settings-popup button:hover { background-color: #388E3C; } #settings-popup button:active { transform: translateY(2px); } td[colspan="3"]:has(input[name="data[TbMainproduct][daihyo_syohin_name]"]) { position: relative; padding-top: 30px; } .button-container { display: flex; align-items: center; gap: 5px; margin-top: 5px; position: absolute; left: 0; bottom: 0; transform: scale(0.9); z-index: 999; } #show-subwords-button.disabled.active { background-color: #388E3C; } `); const popup = document.createElement('div'); popup.id = 'popup'; popup.className = 'suggest-popup'; popup.innerHTML = ` <button id="popup-close">×</button> <div id="popup-header"></div> <div id="popup-content"><ul></ul></div> `; document.body.appendChild(popup); const settingsButton = document.createElement('button'); settingsButton.id = 'settings-button'; settingsButton.title = '設定'; settingsButton.className = 'add-word-button'; const settingsPopup = document.createElement('div'); settingsPopup.id = 'settings-popup'; settingsPopup.innerHTML = ` <label for="popup-width">横幅 (px):</label> <input type="number" id="popup-width" value="400" step="10" /> <label for="popup-height">高さ (px):</label> <input type="number" id="popup-height" value="800" step="10" /> <button id="apply-settings">適用</button> `; document.body.appendChild(settingsPopup); function fetchJSON(callback) { const cacheLifetime = 24 * 60 * 60 * 1000; getFromIndexedDB() .then(cachedData => { const now = new Date().getTime(); if (cachedData && (now - cachedData.timestamp < cacheLifetime)) { callback(cachedData.data); } else { fetch(jsonURL, { method: 'GET', cache: 'no-cache' }) .then(response => response.json()) .then(data => { saveToIndexedDB(data) .catch(error => console.error('データの保存中にエラーが発生しました:', error)); callback(data); }) .catch(error => console.error('JSONデータの取得中にエラーが発生しました:', error)); } }) .catch(error => console.error('IndexedDBからのデータ取得中にエラーが発生しました:', error)); } function handleData(data) { const inputField = document.querySelector('input[name="data[TbMainproduct][daihyo_syohin_name]"]'); const inputField2A = document.querySelector('input[name="data[TbMainproduct][daihyo_syohin_name]"]'); const inputField2B = document.querySelector('[contenteditable="true"]'); let activeInputField2 = inputField2A || inputField2B; const button = document.getElementById('show-subwords-button'); const setActiveInputField2 = (field) => { activeInputField2 = field; }; [inputField2A, inputField2B].forEach(field => { if (field) { field.addEventListener('focus', () => setActiveInputField2(field)); } }); if (inputField) { let inputValue = activeInputField2.textContent?.trim() || activeInputField2.value.trim(); let words = inputValue.split(/\s+/); let mainWord = ''; if (words.length > 0) { mainWord = words[0]; if (mainWord.endsWith('用') && words.length > 1) { let secondWord = words[1]; if (!secondWord.endsWith('用')) { mainWord = mainWord + secondWord.replace(/\s+/g, ''); } } } if (data[mainWord]) { const popupHeader = document.getElementById('popup-header'); if (popupHeader) { popupHeader.textContent = `「${mainWord}」`; } const popupContent = document.getElementById('popup-content').querySelector('ul'); const updateSubwords = (currentInputValue) => { const inputWords = currentInputValue.split(/\s+/); const subwords = Object.entries(data[mainWord]) .filter(([subword]) => !ngWords.includes(subword)) .sort(([, aCount], [, bCount]) => bCount - aCount) .map(([subword]) => { const existsInInput = inputWords.includes(subword); return ` <li style="color: ${existsInInput ? 'green' : 'black'};"> ${subword} <button class="add-word-button" data-word="${subword}"></button> </li> `; }) .join(''); popupContent.innerHTML = subwords; document.querySelectorAll('.add-word-button').forEach(button => { button.addEventListener('click', (event) => { const word = event.target.getAttribute('data-word'); const text = activeInputField2.textContent || activeInputField2.value || ''; const selection = window.getSelection(); const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; let start = 0, end = 0; if (range && activeInputField2.isContentEditable) { start = range.startOffset; end = range.endOffset; } else if (activeInputField2.selectionStart !== undefined) { start = activeInputField2.selectionStart; end = activeInputField2.selectionEnd; } const before = text.slice(0, start) || ''; const after = text.slice(end) || ''; const needsSpaceBefore = (before && before.length > 0 && before[before.length - 1] !== ' ') || false; const needsSpaceAfter = (after && after.length > 0 && after[0] !== ' ') || false; const newValue = before + (needsSpaceBefore ? ' ' : '') + word + (needsSpaceAfter ? ' ' : '') + after; if (activeInputField2.isContentEditable) { activeInputField2.textContent = newValue; const newRange = document.createRange(); newRange.setStart(activeInputField2.firstChild, start + word.length + (needsSpaceBefore ? 1 : 0)); newRange.setEnd(activeInputField2.firstChild, start + word.length + (needsSpaceBefore ? 1 : 0)); selection.removeAllRanges(); selection.addRange(newRange); } else { activeInputField2.value = newValue; activeInputField2.setSelectionRange(start + word.length + (needsSpaceBefore ? 1 : 0), start + word.length + (needsSpaceBefore ? 1 : 0)); } activeInputField2.focus(); updateSubwords(activeInputField2.textContent?.trim() || activeInputField2.value.trim()); }); }); }; updateSubwords(inputValue); activeInputField2.addEventListener('input', () => { updateSubwords(activeInputField2.textContent?.trim() || activeInputField2.value.trim()); }); const popup = document.getElementById('popup'); if (popup) { popup.style.display = 'block'; if (button) { button.textContent = '表示中'; button.classList.add('disabled', 'active'); button.disabled = true; } } } else { if (button) { button.textContent = '登録なし'; button.classList.add('disabled'); button.classList.remove('active'); button.disabled = true; } } } } function initDB() { return new Promise((resolve, reject) => { const request = indexedDB.open('jsonCacheDB', 1); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains('jsonData')) { db.createObjectStore('jsonData', { keyPath: 'id' }); } }; request.onsuccess = (event) => { resolve(event.target.result); }; request.onerror = (event) => { reject('IndexedDBの初期化に失敗しました'); }; }); } function saveToIndexedDB(data) { return initDB().then(db => { return new Promise((resolve, reject) => { const transaction = db.transaction(['jsonData'], 'readwrite'); const store = transaction.objectStore('jsonData'); const cacheData = { id: 'jsonData', timestamp: new Date().getTime(), data: data }; store.put(cacheData); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject('データの保存に失敗しました'); }); }); } function getFromIndexedDB() { return initDB().then(db => { return new Promise((resolve, reject) => { const transaction = db.transaction(['jsonData'], 'readonly'); const store = transaction.objectStore('jsonData'); const request = store.get('jsonData'); request.onsuccess = (event) => { resolve(event.target.result); }; request.onerror = () => reject('データの取得に失敗しました'); }); }); } function adjustButtonContainerStyle() { const url = window.location.href; const buttonContainer = document.querySelector('.button-container'); if (buttonContainer) { if (url.includes('registered_mainedit')) { buttonContainer.style.bottom = '31.5px'; buttonContainer.style.left = `1px`; } else { buttonContainer.style.bottom = '51px'; buttonContainer.style.left = `1px`; } } } function addShowSubwordsButton() { const tdElement = document.querySelector('td[colspan="3"][scope="row"]'); const inputField = document.querySelector('input[name="data[TbMainproduct][daihyo_syohin_name]"]'); if (tdElement && inputField) { const buttonContainer = document.createElement('div'); buttonContainer.className = 'button-container'; const showSubwordsButton = document.createElement('button'); showSubwordsButton.id = 'show-subwords-button'; showSubwordsButton.textContent = 'ワード候補'; const settingsButton = document.createElement('button'); settingsButton.id = 'settings-button'; settingsButton.title = '設定'; settingsButton.className = 'settings-button'; buttonContainer.appendChild(showSubwordsButton); buttonContainer.appendChild(settingsButton); tdElement.appendChild(buttonContainer); adjustButtonContainerStyle(); showSubwordsButton.addEventListener('click', (event) => { if (event.isTrusted) { if (!showSubwordsButton.classList.contains('disabled')) { event.preventDefault(); event.stopPropagation(); fetchJSON(data => handleData(data)); } } }); settingsButton.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); toggleSettingsPopup(); }); } } function toggleSettingsPopup() { const settingsPopup = document.getElementById('settings-popup'); if (settingsPopup) { settingsPopup.style.display = settingsPopup.style.display === 'block' ? 'none' : 'block'; } } function closePopup() { const popup = document.getElementById('popup'); const showSubwordsButton = document.getElementById('show-subwords-button'); if (popup) { popup.style.display = 'none'; if (showSubwordsButton) { showSubwordsButton.textContent = 'ワード候補'; showSubwordsButton.classList.remove('disabled', 'active'); showSubwordsButton.disabled = false; } } } function applySettings() { const width = document.getElementById('popup-width').value || 400; const height = document.getElementById('popup-height').value || 800; const popup = document.getElementById('popup'); if (popup) { popup.style.width = `${width}px`; popup.style.height = `${height}px`; } localStorage.setItem('popupWidth', width); localStorage.setItem('popupHeight', height); } function closeSettingsOnClickOutside(event) { const settings = document.getElementById('settings-popup'); const settingsButton = document.getElementById('settings-button'); if (settings && !settings.contains(event.target) && event.target !== settingsButton) { settings.style.display = 'none'; } } document.addEventListener('keydown', function(event) { if (event.key === 'Escape') { closePopup(); } }); document.addEventListener('click', function(event) { if (event.target.id === 'popup-close') { closePopup(); } else if (event.target.id === 'apply-settings') { applySettings(); } }); settingsButton.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); toggleSettingsPopup(); }); document.addEventListener('click', closeSettingsOnClickOutside); const inputField = document.querySelector('input[name="data[TbMainproduct][daihyo_syohin_name]"]'); if (inputField) { inputField.addEventListener('input', () => { const button = document.getElementById('show-subwords-button'); const popup = document.getElementById('popup'); if (button && button.textContent === '登録なし') { button.textContent = 'ワード候補'; button.classList.remove('disabled'); button.disabled = false; } if (popup && popup.style.display === 'block') { button.textContent = '表示中(更新)'; button.classList.remove('disabled'); button.classList.add('active'); button.disabled = false; } }); } const observer = new MutationObserver((mutationsList, observer) => { mutationsList.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.matches('.title-popup[contenteditable="true"]')) { const checkButtonExistence = () => { const button = document.getElementById('show-subwords-button'); if (button) { const popup = document.getElementById('popup'); if (button && button.textContent === '登録なし') { button.textContent = 'ワード候補'; button.classList.remove('disabled'); button.disabled = false; } if (popup && popup.style.display === 'block') { button.textContent = '表示中(更新)'; button.classList.remove('disabled'); button.classList.add('active'); button.disabled = false; } node.addEventListener('input', () => { if (button && button.textContent === '登録なし') { button.textContent = 'ワード候補'; button.classList.remove('disabled'); button.disabled = false; } if (popup && popup.style.display === 'block') { button.textContent = '表示中(更新)'; button.classList.remove('disabled'); button.classList.add('active'); button.disabled = false; } }); observer.disconnect(); } else { setTimeout(checkButtonExistence, 100); } }; checkButtonExistence(); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('load', () => { addShowSubwordsButton(); const savedWidth = localStorage.getItem('popupWidth'); const savedHeight = localStorage.getItem('popupHeight'); if (savedWidth && savedHeight) { const popup = document.getElementById('popup'); if (popup) { popup.style.width = `${savedWidth}px`; popup.style.height = `${savedHeight}px`; document.getElementById('popup-width').value = savedWidth; document.getElementById('popup-height').value = savedHeight; } } fetchJSON(data => { }); }); //送信機能 const API_URL = 'https://my-data-repo.vercel.app/api/github-proxy'; const INPUT_SELECTOR = '#TbMainproductDaihyoSyohinName'; const BUTTON_SELECTOR = '#saveAndSkuStock'; function getFileShaAndContent(callback) { GM_xmlhttpRequest({ method: "GET", url: `${API_URL}`, onload: function(response) { if (response.status === 200) { const data = JSON.parse(response.responseText); const sha = data.sha; const existingContent = data.content; callback(sha, existingContent); } else { console.error("ファイルの取得に失敗しました:", response.responseText); callback(null, null); } }, onerror: function(error) { console.error("エラーが発生しました:", error); callback(null, null); } }); } function uploadData(retryCount = 0) { const inputElement = document.querySelector(INPUT_SELECTOR); if (inputElement) { const newData = inputElement.value; getFileShaAndContent(function(sha, existingContent) { if (sha !== null) { const updatedContent = existingContent + "\n" + newData; GM_xmlhttpRequest({ method: "PUT", url: API_URL, headers: { "Content-Type": "application/json", }, data: JSON.stringify({ sha: sha, newData: updatedContent }), onload: function(response) { if (response.status === 200) { console.log("データ送信成功"); } else if (response.status === 422 && retryCount < 3) { console.warn("競合確認...リトライ中"); setTimeout(() => uploadData(retryCount + 1), 1000); } else { console.error("データ送信失敗:", response.responseText); } }, onerror: function(error) { console.error("Error:", error); if (retryCount < 3) { setTimeout(() => uploadData(retryCount + 1), 1000); } } }); } }); } } function setupButtonListener() { const buttonElement = document.querySelector(BUTTON_SELECTOR); if (buttonElement) { buttonElement.addEventListener('click', uploadData); } } setupButtonListener(); })(); } })();