feat: handle operations on selected items

This commit is contained in:
2025-06-16 19:38:24 +03:00
parent 49a29e1379
commit 01a71fcc82
5 changed files with 179 additions and 51 deletions

View File

@@ -64,5 +64,8 @@
}, },
"confirm_delete_words": { "confirm_delete_words": {
"message": "Are you sure you want to delete the selected words?" "message": "Are you sure you want to delete the selected words?"
},
"deselect_all": {
"message": "Deselect All"
} }
} }

View File

@@ -64,5 +64,8 @@
}, },
"confirm_delete_words": { "confirm_delete_words": {
"message": "Вы уверены, что хотите удалить выбранные слова?" "message": "Вы уверены, что хотите удалить выбранные слова?"
},
"deselect_all": {
"message": "Отменить выбор"
} }
} }

View File

@@ -22,16 +22,22 @@
body { body {
font-family: 'Segoe UI', sans-serif; font-family: 'Segoe UI', sans-serif;
width: 360px; width: 400px;
margin: 0; margin: 0;
padding: 0; padding: 0;
background: var(--bg-color); background: var(--bg-color);
color: var(--text-color); color: var(--text-color);
transition: background 0.3s ease, color 0.3s ease; transition: background 0.3s ease, color 0.3s ease;
max-height: 600px;
overflow: hidden;
} }
.container { .container {
padding: 16px; padding: 12px;
height: 100%;
display: flex;
flex-direction: column;
gap: 8px;
} }
h1 { h1 {
@@ -42,7 +48,15 @@ h1 {
} }
.section { .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"], input[type="text"],
@@ -50,9 +64,9 @@ textarea,
select { select {
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
margin-top: 6px; margin-top: 4px;
margin-bottom: 10px; margin-bottom: 6px;
padding: 8px; padding: 6px;
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--input-border); border: 1px solid var(--input-border);
background-color: var(--input-bg); background-color: var(--input-bg);
@@ -61,7 +75,7 @@ select {
} }
textarea { textarea {
height: 70px; height: 60px;
resize: vertical; resize: vertical;
background-color: var(--input-bg) !important; background-color: var(--input-bg) !important;
color: var(--text-color) !important; color: var(--text-color) !important;
@@ -102,9 +116,9 @@ input[type="checkbox"]:checked::after {
} }
button { button {
margin: 4px 2px; margin: 2px;
padding: 8px 12px; padding: 6px 10px;
font-size: 0.9em; font-size: 0.85em;
border: none; border: none;
border-radius: 6px; border-radius: 6px;
background-color: var(--button-bg); background-color: var(--button-bg);
@@ -117,36 +131,56 @@ button:hover {
background-color: var(--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 { #wordList>div {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 6px; margin-bottom: 4px;
gap: 6px; gap: 4px;
background: var(--wordlist-bg); background: var(--wordlist-bg);
padding: 6px; padding: 4px;
border-radius: 6px; border-radius: 6px;
will-change: transform;
flex-wrap: nowrap;
position: absolute;
left: 4px;
right: 4px;
} }
#wordList input[type="text"] { #wordList input[type="text"] {
flex: 2; flex: 1;
min-width: 0;
background-color: var(--input-bg) !important; background-color: var(--input-bg) !important;
color: var(--text-color) !important; color: var(--text-color) !important;
border: 1px solid var(--input-border) !important; border: 1px solid var(--input-border) !important;
} }
#wordList input[type="color"] { #wordList input[type="color"] {
width: 28px; width: 24px;
height: 28px; height: 24px;
padding: 0; padding: 0;
border-radius: 4px; border-radius: 4px;
flex-shrink: 0;
} }
label { #wordList input[type="checkbox"] {
display: flex; flex-shrink: 0;
align-items: center; }
gap: 8px;
margin-bottom: 6px; #wordList label {
font-size: 0.95em; flex-shrink: 0;
white-space: nowrap;
margin: 0;
} }
input[type="file"] { input[type="file"] {

View File

@@ -39,14 +39,17 @@
<button id="addWordsBtn" data-i18n="apply_paste">Add Words</button> <button id="addWordsBtn" data-i18n="apply_paste">Add Words</button>
</div> </div>
<div class="section"> <div class="word-list-section">
<button id="selectAllBtn" data-i18n="select_all">Select All</button> <div class="section">
<button id="deleteSelectedBtn" data-i18n="delete_selected">Delete Selected</button> <button id="selectAllBtn" data-i18n="select_all">Select All</button>
<button id="disableSelectedBtn" data-i18n="disable_selected">Disable Selected</button> <button id="deselectAllBtn" data-i18n="deselect_all">Deselect All</button>
<button id="enableSelectedBtn" data-i18n="enable_selected">Enable Selected</button> <button id="deleteSelectedBtn" data-i18n="delete_selected">Delete Selected</button>
</div> <button id="disableSelectedBtn" data-i18n="disable_selected">Disable Selected</button>
<button id="enableSelectedBtn" data-i18n="enable_selected">Enable Selected</button>
</div>
<div id="wordList"></div> <div id="wordList"></div>
</div>
<div class="section"> <div class="section">
<button id="importBtn" data-i18n="import_list">Import JSON</button> <button id="importBtn" data-i18n="import_list">Import JSON</button>

View File

@@ -9,11 +9,18 @@ const importInput = document.getElementById("importInput");
let lists = []; let lists = [];
let currentListIndex = 0; 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() { async function save() {
await chrome.storage.local.set({ lists }); await debouncedSave();
renderLists();
renderWords();
} }
async function load() { async function load() {
@@ -49,14 +56,34 @@ function updateListForm() {
function renderWords() { function renderWords() {
const list = lists[currentListIndex]; 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"); 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"); const cbSelect = document.createElement("input");
cbSelect.type = "checkbox"; cbSelect.type = "checkbox";
cbSelect.dataset.index = i; cbSelect.dataset.index = i;
if (selectedCheckboxes.has(i)) {
cbSelect.checked = true;
}
const inputWord = document.createElement("input"); const inputWord = document.createElement("input");
inputWord.type = "text"; inputWord.type = "text";
@@ -96,11 +123,49 @@ function renderWords() {
inputWord.style.color = fg; inputWord.style.color = fg;
inputWord.style.border = `1px solid ${border}`; 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 = () => { listSelect.onchange = () => {
selectedCheckboxes.clear();
currentListIndex = +listSelect.value; currentListIndex = +listSelect.value;
renderWords(); renderWords();
updateListForm(); updateListForm();
@@ -140,47 +205,62 @@ document.getElementById("addWordsBtn").onclick = () => {
save(); save();
}; };
document.getElementById("selectAllBtn").onclick = () => {
wordList.querySelectorAll("input[type=checkbox]").forEach(cb => cb.checked = true);
};
document.getElementById("deleteSelectedBtn").onclick = () => { document.getElementById("deleteSelectedBtn").onclick = () => {
if (confirm(chrome.i18n.getMessage("confirm_delete_words"))) { if (confirm(chrome.i18n.getMessage("confirm_delete_words"))) {
const list = lists[currentListIndex]; 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)); lists[currentListIndex].words = list.words.filter((_, i) => !toDelete.includes(i));
selectedCheckboxes.clear();
save(); save();
renderWords();
} }
}; };
document.getElementById("disableSelectedBtn").onclick = () => { document.getElementById("disableSelectedBtn").onclick = () => {
const list = lists[currentListIndex]; 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(); save();
renderWords();
}; };
document.getElementById("enableSelectedBtn").onclick = () => { document.getElementById("enableSelectedBtn").onclick = () => {
const list = lists[currentListIndex]; 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(); save();
renderWords();
}; };
wordList.addEventListener("input", e => { wordList.addEventListener("input", e => {
const index = e.target.dataset.wordEdit ?? e.target.dataset.bgEdit ?? e.target.dataset.fgEdit; 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 (index == null) return;
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; const word = lists[currentListIndex].words[index];
save(); 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 => { wordList.addEventListener("change", e => {
if (e.target.dataset.activeEdit != null) { if (e.target.type === "checkbox") {
lists[currentListIndex].words[e.target.dataset.activeEdit].active = e.target.checked; if (e.target.dataset.index != null) { // Word selection checkbox
save(); 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"); const exportBtn = document.getElementById("exportBtn");
exportBtn.onclick = () => { exportBtn.onclick = () => {
const blob = new Blob([JSON.stringify(lists, null, 2)], { type: "application/json" }); 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(); load();