mirror of
https://github.com/obsqrbtz/goose-highlighter.git
synced 2026-04-08 20:19:06 +03:00
feat: handle operations on selected items
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,5 +64,8 @@
|
|||||||
},
|
},
|
||||||
"confirm_delete_words": {
|
"confirm_delete_words": {
|
||||||
"message": "Вы уверены, что хотите удалить выбранные слова?"
|
"message": "Вы уверены, что хотите удалить выбранные слова?"
|
||||||
|
},
|
||||||
|
"deselect_all": {
|
||||||
|
"message": "Отменить выбор"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"] {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
129
popup/popup.js
129
popup/popup.js
@@ -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();
|
||||||
Reference in New Issue
Block a user