From 01a71fcc827766b2899ec04d4462bb538728d059 Mon Sep 17 00:00:00 2001 From: Daniel Dada Date: Mon, 16 Jun 2025 19:38:24 +0300 Subject: [PATCH] feat: handle operations on selected items --- _locales/en/messages.json | 3 + _locales/ru/messages.json | 3 + popup/popup.css | 78 ++++++++++++++++------- popup/popup.html | 17 ++--- popup/popup.js | 129 +++++++++++++++++++++++++++++++------- 5 files changed, 179 insertions(+), 51 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9cf17c5..309004e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -64,5 +64,8 @@ }, "confirm_delete_words": { "message": "Are you sure you want to delete the selected words?" + }, + "deselect_all": { + "message": "Deselect All" } } \ No newline at end of file diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 5dd5c48..aeab38c 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -64,5 +64,8 @@ }, "confirm_delete_words": { "message": "Вы уверены, что хотите удалить выбранные слова?" + }, + "deselect_all": { + "message": "Отменить выбор" } } \ No newline at end of file diff --git a/popup/popup.css b/popup/popup.css index 747ebcf..0a37de7 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -22,16 +22,22 @@ body { font-family: 'Segoe UI', sans-serif; - width: 360px; + width: 400px; margin: 0; padding: 0; background: var(--bg-color); color: var(--text-color); transition: background 0.3s ease, color 0.3s ease; + max-height: 600px; + overflow: hidden; } .container { - padding: 16px; + padding: 12px; + height: 100%; + display: flex; + flex-direction: column; + gap: 8px; } h1 { @@ -42,7 +48,15 @@ h1 { } .section { - margin-bottom: 16px; + margin-bottom: 0; +} + +.word-list-section { + flex: 1; + min-height: 200px; + display: flex; + flex-direction: column; + gap: 8px; } input[type="text"], @@ -50,9 +64,9 @@ textarea, select { width: 100%; box-sizing: border-box; - margin-top: 6px; - margin-bottom: 10px; - padding: 8px; + margin-top: 4px; + margin-bottom: 6px; + padding: 6px; border-radius: 6px; border: 1px solid var(--input-border); background-color: var(--input-bg); @@ -61,7 +75,7 @@ select { } textarea { - height: 70px; + height: 60px; resize: vertical; background-color: var(--input-bg) !important; color: var(--text-color) !important; @@ -102,9 +116,9 @@ input[type="checkbox"]:checked::after { } button { - margin: 4px 2px; - padding: 8px 12px; - font-size: 0.9em; + margin: 2px; + padding: 6px 10px; + font-size: 0.85em; border: none; border-radius: 6px; background-color: var(--button-bg); @@ -117,36 +131,56 @@ button:hover { background-color: var(--button-hover); } +#wordList { + position: relative; + overflow-y: auto; + overflow-x: hidden; + flex: 1; + background: var(--wordlist-bg); + border-radius: 6px; + padding: 4px; + min-height: 200px; +} + #wordList>div { display: flex; align-items: center; - margin-bottom: 6px; - gap: 6px; + margin-bottom: 4px; + gap: 4px; background: var(--wordlist-bg); - padding: 6px; + padding: 4px; border-radius: 6px; + will-change: transform; + flex-wrap: nowrap; + position: absolute; + left: 4px; + right: 4px; } #wordList input[type="text"] { - flex: 2; + flex: 1; + min-width: 0; background-color: var(--input-bg) !important; color: var(--text-color) !important; border: 1px solid var(--input-border) !important; } #wordList input[type="color"] { - width: 28px; - height: 28px; + width: 24px; + height: 24px; padding: 0; border-radius: 4px; + flex-shrink: 0; } -label { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 6px; - font-size: 0.95em; +#wordList input[type="checkbox"] { + flex-shrink: 0; +} + +#wordList label { + flex-shrink: 0; + white-space: nowrap; + margin: 0; } input[type="file"] { diff --git a/popup/popup.html b/popup/popup.html index fa7dc95..b5a449e 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -39,14 +39,17 @@ -
- - - - -
+
+
+ + + + + +
-
+
+
diff --git a/popup/popup.js b/popup/popup.js index 0f231d8..5082ce8 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -9,11 +9,18 @@ const importInput = document.getElementById("importInput"); let lists = []; let currentListIndex = 0; +let saveTimeout; +let selectedCheckboxes = new Set(); + +async function debouncedSave() { + clearTimeout(saveTimeout); + saveTimeout = setTimeout(async () => { + await chrome.storage.local.set({ lists }); + }, 500); +} async function save() { - await chrome.storage.local.set({ lists }); - renderLists(); - renderWords(); + await debouncedSave(); } async function load() { @@ -49,14 +56,34 @@ function updateListForm() { function renderWords() { const list = lists[currentListIndex]; - wordList.innerHTML = ""; // Clear first + const fragment = document.createDocumentFragment(); - list.words.forEach((w, i) => { + const itemHeight = 32; + const containerHeight = wordList.clientHeight; + const scrollTop = wordList.scrollTop; + const startIndex = Math.floor(scrollTop / itemHeight); + const endIndex = Math.min( + startIndex + Math.ceil(containerHeight / itemHeight) + 2, + list.words.length + ); + + wordList.style.height = `${list.words.length * itemHeight}px`; + + for (let i = startIndex; i < endIndex; i++) { + const w = list.words[i]; const container = document.createElement("div"); + container.style.height = `${itemHeight}px`; + container.style.position = 'absolute'; + container.style.top = `${i * itemHeight}px`; + container.style.width = '100%'; + container.style.boxSizing = 'border-box'; const cbSelect = document.createElement("input"); cbSelect.type = "checkbox"; cbSelect.dataset.index = i; + if (selectedCheckboxes.has(i)) { + cbSelect.checked = true; + } const inputWord = document.createElement("input"); inputWord.type = "text"; @@ -96,11 +123,49 @@ function renderWords() { inputWord.style.color = fg; inputWord.style.border = `1px solid ${border}`; - wordList.appendChild(container); - }); + fragment.appendChild(container); + } + + wordList.innerHTML = ''; + wordList.appendChild(fragment); } +document.getElementById("selectAllBtn").onclick = () => { + const list = lists[currentListIndex]; + list.words.forEach((_, index) => { + selectedCheckboxes.add(index); + }); + renderWords(); +}; + +wordList.addEventListener("change", e => { + if (e.target.type === "checkbox") { + if (e.target.dataset.index != null) { + if (e.target.checked) { + selectedCheckboxes.add(+e.target.dataset.index); + } else { + selectedCheckboxes.delete(+e.target.dataset.index); + } + } else if (e.target.dataset.activeEdit != null) { + lists[currentListIndex].words[e.target.dataset.activeEdit].active = e.target.checked; + debouncedSave(); + } + } +}); + +let scrollTimeout; +wordList.addEventListener('scroll', () => { + if (scrollTimeout) { + return; + } + scrollTimeout = setTimeout(() => { + requestAnimationFrame(renderWords); + scrollTimeout = null; + }, 16); // ~60fps +}); + listSelect.onchange = () => { + selectedCheckboxes.clear(); currentListIndex = +listSelect.value; renderWords(); updateListForm(); @@ -140,47 +205,62 @@ document.getElementById("addWordsBtn").onclick = () => { save(); }; -document.getElementById("selectAllBtn").onclick = () => { - wordList.querySelectorAll("input[type=checkbox]").forEach(cb => cb.checked = true); -}; - document.getElementById("deleteSelectedBtn").onclick = () => { if (confirm(chrome.i18n.getMessage("confirm_delete_words"))) { const list = lists[currentListIndex]; - const toDelete = [...wordList.querySelectorAll("input[type=checkbox]:checked")].map(cb => +cb.dataset.index); + const toDelete = Array.from(selectedCheckboxes); lists[currentListIndex].words = list.words.filter((_, i) => !toDelete.includes(i)); + selectedCheckboxes.clear(); save(); + renderWords(); } }; document.getElementById("disableSelectedBtn").onclick = () => { const list = lists[currentListIndex]; - wordList.querySelectorAll("input[type=checkbox]:checked").forEach(cb => list.words[+cb.dataset.index].active = false); + selectedCheckboxes.forEach(index => { + list.words[index].active = false; + }); save(); + renderWords(); }; document.getElementById("enableSelectedBtn").onclick = () => { const list = lists[currentListIndex]; - wordList.querySelectorAll("input[type=checkbox]:checked").forEach(cb => list.words[+cb.dataset.index].active = true); + selectedCheckboxes.forEach(index => { + list.words[index].active = true; + }); save(); + renderWords(); }; wordList.addEventListener("input", e => { const index = e.target.dataset.wordEdit ?? e.target.dataset.bgEdit ?? e.target.dataset.fgEdit; - if (e.target.dataset.wordEdit != null) lists[currentListIndex].words[index].wordStr = e.target.value; - if (e.target.dataset.bgEdit != null) lists[currentListIndex].words[index].background = e.target.value; - if (e.target.dataset.fgEdit != null) lists[currentListIndex].words[index].foreground = e.target.value; - save(); + if (index == null) return; + + const word = lists[currentListIndex].words[index]; + if (e.target.dataset.wordEdit != null) word.wordStr = e.target.value; + if (e.target.dataset.bgEdit != null) word.background = e.target.value; + if (e.target.dataset.fgEdit != null) word.foreground = e.target.value; + + debouncedSave(); }); wordList.addEventListener("change", e => { - if (e.target.dataset.activeEdit != null) { - lists[currentListIndex].words[e.target.dataset.activeEdit].active = e.target.checked; - save(); + if (e.target.type === "checkbox") { + if (e.target.dataset.index != null) { // Word selection checkbox + if (e.target.checked) { + selectedCheckboxes.add(+e.target.dataset.index); + } else { + selectedCheckboxes.delete(+e.target.dataset.index); + } + } else if (e.target.dataset.activeEdit != null) { // Active checkbox + lists[currentListIndex].words[e.target.dataset.activeEdit].active = e.target.checked; + debouncedSave(); + } } }); - const exportBtn = document.getElementById("exportBtn"); exportBtn.onclick = () => { const blob = new Blob([JSON.stringify(lists, null, 2)], { type: "application/json" }); @@ -250,4 +330,9 @@ toggle.addEventListener('change', () => { } }); +document.getElementById("deselectAllBtn").onclick = () => { + selectedCheckboxes.clear(); + renderWords(); +}; + load(); \ No newline at end of file