diff --git a/_locales/de/messages.json b/_locales/de/messages.json index a190aed..0e1e55a 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -185,6 +185,18 @@ "copy": { "message": "Kopieren" }, + "move_to_list": { + "message": "Zur Liste verschieben" + }, + "copy_to_list": { + "message": "In Liste kopieren" + }, + "no_other_lists": { + "message": "Keine anderen Listen" + }, + "word_actions": { + "message": "In eine andere Liste verschieben oder kopieren" + }, "merge_lists_min_two": { "message": "Wählen Sie mindestens zwei Listen zum Zusammenführen aus." }, diff --git a/_locales/en/messages.json b/_locales/en/messages.json index fd0ea0f..0ac2023 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "Copy" }, + "move_to_list": { + "message": "Move to list" + }, + "copy_to_list": { + "message": "Copy to list" + }, + "no_other_lists": { + "message": "No other lists" + }, + "word_actions": { + "message": "Move or copy to another list" + }, "merge_lists_min_two": { "message": "Select at least two lists to merge." }, diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 967f591..ab94400 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "Copiar" }, + "move_to_list": { + "message": "Mover a lista" + }, + "copy_to_list": { + "message": "Copiar a lista" + }, + "no_other_lists": { + "message": "No hay otras listas" + }, + "word_actions": { + "message": "Mover o copiar a otra lista" + }, "merge_lists_min_two": { "message": "Seleccione al menos dos listas para combinar." }, diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 44712d2..6ce3962 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "Copier" }, + "move_to_list": { + "message": "Déplacer vers la liste" + }, + "copy_to_list": { + "message": "Copier vers la liste" + }, + "no_other_lists": { + "message": "Aucune autre liste" + }, + "word_actions": { + "message": "Déplacer ou copier vers une autre liste" + }, "merge_lists_min_two": { "message": "Sélectionnez au moins deux listes à fusionner." }, diff --git a/_locales/hi/messages.json b/_locales/hi/messages.json index c91b039..fda9a74 100644 --- a/_locales/hi/messages.json +++ b/_locales/hi/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "कॉपी करें" }, + "move_to_list": { + "message": "सूची में ले जाएं" + }, + "copy_to_list": { + "message": "सूची में कॉपी करें" + }, + "no_other_lists": { + "message": "कोई अन्य सूची नहीं" + }, + "word_actions": { + "message": "किसी अन्य सूची में ले जाएं या कॉपी करें" + }, "merge_lists_min_two": { "message": "मर्ज करने के लिए कम से कम दो सूचियाँ चुनें।" }, diff --git a/_locales/it/messages.json b/_locales/it/messages.json index ca0cc09..5ca5578 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "Copia" }, + "move_to_list": { + "message": "Sposta in elenco" + }, + "copy_to_list": { + "message": "Copia in elenco" + }, + "no_other_lists": { + "message": "Nessun altro elenco" + }, + "word_actions": { + "message": "Sposta o copia in un altro elenco" + }, "merge_lists_min_two": { "message": "Seleziona almeno due elenchi da unire." }, diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index ef27a05..32d8c6d 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "コピー" }, + "move_to_list": { + "message": "リストに移動" + }, + "copy_to_list": { + "message": "リストにコピー" + }, + "no_other_lists": { + "message": "他のリストはありません" + }, + "word_actions": { + "message": "別のリストに移動またはコピー" + }, "merge_lists_min_two": { "message": "統合するには少なくとも2つのリストを選択してください。" }, diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index fc7baab..9887972 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "복사" }, + "move_to_list": { + "message": "목록으로 이동" + }, + "copy_to_list": { + "message": "목록에 복사" + }, + "no_other_lists": { + "message": "다른 목록 없음" + }, + "word_actions": { + "message": "다른 목록으로 이동 또는 복사" + }, "merge_lists_min_two": { "message": "병합하려면 최소 두 개의 리스트를 선택하세요." }, diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index 6e3d2bf..19563fd 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "Kopiëren" }, + "move_to_list": { + "message": "Verplaats naar lijst" + }, + "copy_to_list": { + "message": "Kopieer naar lijst" + }, + "no_other_lists": { + "message": "Geen andere lijsten" + }, + "word_actions": { + "message": "Verplaats of kopieer naar een andere lijst" + }, "merge_lists_min_two": { "message": "Selecteer minimaal twee lijsten om samen te voegen." }, diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 6832e0d..c6f308f 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "Kopiuj" }, + "move_to_list": { + "message": "Przenieś do listy" + }, + "copy_to_list": { + "message": "Kopiuj do listy" + }, + "no_other_lists": { + "message": "Brak innych list" + }, + "word_actions": { + "message": "Przenieś lub skopiuj do innej listy" + }, "merge_lists_min_two": { "message": "Wybierz co najmniej dwie listy do scalenia." }, diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index e9579fa..336295d 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "Copiar" }, + "move_to_list": { + "message": "Mover para lista" + }, + "copy_to_list": { + "message": "Copiar para lista" + }, + "no_other_lists": { + "message": "Nenhuma outra lista" + }, + "word_actions": { + "message": "Mover ou copiar para outra lista" + }, "merge_lists_min_two": { "message": "Selecione pelo menos duas listas para mesclar." }, diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 2c479cf..228525b 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "Копировать" }, + "move_to_list": { + "message": "Переместить в список" + }, + "copy_to_list": { + "message": "Копировать в список" + }, + "no_other_lists": { + "message": "Нет других списков" + }, + "word_actions": { + "message": "Переместить или скопировать в другой список" + }, "merge_lists_min_two": { "message": "Выберите как минимум два списка для объединения." }, diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 72c832a..d7f4594 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "Kopyala" }, + "move_to_list": { + "message": "Listeye taşı" + }, + "copy_to_list": { + "message": "Listeye kopyala" + }, + "no_other_lists": { + "message": "Başka liste yok" + }, + "word_actions": { + "message": "Başka bir listeye taşı veya kopyala" + }, "merge_lists_min_two": { "message": "Birleştirmek için en az iki liste seçin." }, diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 9a9e329..38f36a9 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -191,6 +191,18 @@ "copy": { "message": "复制" }, + "move_to_list": { + "message": "移动到列表" + }, + "copy_to_list": { + "message": "复制到列表" + }, + "no_other_lists": { + "message": "没有其他列表" + }, + "word_actions": { + "message": "移动或复制到另一个列表" + }, "merge_lists_min_two": { "message": "选择至少两个列表进行合并。" }, diff --git a/popup/popup.css b/popup/popup.css index d961caa..618718f 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -645,6 +645,60 @@ body { text-align: center; } +/* Word item 3-dot menu dropdown */ +.word-item-menu-dropdown { + display: none; + position: fixed; + min-width: 160px; + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: 8px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.15); + z-index: 200; + overflow: hidden; +} + +.word-item-menu-dropdown.open { + display: block; +} + +.word-item-menu-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 12px; + border: none; + background: none; + font-size: 13px; + color: var(--text-color); + cursor: pointer; + text-align: left; + transition: background 0.15s; +} + +.word-item-menu-item:hover:not(.disabled) { + background: var(--section-bg); +} + +.word-item-menu-item.disabled { + opacity: 0.6; + cursor: default; +} + +.word-item-menu-item i { + font-size: 12px; + opacity: 0.8; + flex-shrink: 0; +} + +.word-item-menu-item .list-color-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + /* Empty State */ .empty-state { flex: 1; diff --git a/popup/popup.html b/popup/popup.html index fc9a576..cb974ee 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -196,6 +196,9 @@
+ + +

Click to select • Ctrl/Cmd+Click for multi-select

diff --git a/src/popup/PopupController.ts b/src/popup/PopupController.ts index ac2c9ab..c207911 100644 --- a/src/popup/PopupController.ts +++ b/src/popup/PopupController.ts @@ -16,6 +16,9 @@ export class PopupController { private activeTab = 'lists'; private pageHighlights: Array<{ word: string; count: number; background: string; foreground: string }> = []; private highlightIndices = new Map(); + private wordMenuOpenForIndex: number | null = null; + private wordMenuCopyOnly = false; + private wordMenuCloseListener: (() => void) | null = null; async initialize(): Promise { await this.loadData(); @@ -278,6 +281,17 @@ export class PopupController { const list = this.lists[this.currentListIndex]; if (!list) return; + // Handle 3-dot menu button click + const menuBtn = target.closest('.word-item-menu-btn') as HTMLElement | null; + if (menuBtn) { + e.stopPropagation(); + const index = Number(menuBtn.dataset.index); + if (!Number.isNaN(index)) { + this.openWordItemMenu(index, menuBtn); + } + return; + } + // Handle edit button click const editBtn = target.closest('.word-item-icon-btn.edit-word-btn') as HTMLElement | null; if (editBtn) { @@ -428,6 +442,155 @@ export class PopupController { input.select(); } + private openWordItemMenu(wordIndex: number, buttonEl: HTMLElement): void { + const dropdown = document.getElementById('wordItemMenuDropdown'); + if (!dropdown) return; + + this.closeWordItemMenu(); + + const rect = buttonEl.getBoundingClientRect(); + const padding = 8; + dropdown.style.left = `${rect.left}px`; + dropdown.style.top = `${rect.bottom + 4}px`; + dropdown.style.right = ''; + + const moveLabel = chrome.i18n.getMessage('move_to_list') || 'Move to list'; + const copyLabel = chrome.i18n.getMessage('copy_to_list') || 'Copy to list'; + + dropdown.innerHTML = ` + + + `; + + dropdown.querySelectorAll('.word-item-menu-item').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + const action = (item as HTMLElement).dataset.action; + if (action === 'move') { + this.showWordMenuListPicker(wordIndex, false); + } else if (action === 'copy') { + this.showWordMenuListPicker(wordIndex, true); + } + }); + }); + + this.wordMenuOpenForIndex = wordIndex; + dropdown.classList.add('open'); + dropdown.setAttribute('aria-hidden', 'false'); + + requestAnimationFrame(() => { + const dr = dropdown.getBoundingClientRect(); + const vw = window.innerWidth; + const vh = window.innerHeight; + if (dr.right > vw - padding) { + dropdown.style.left = `${vw - dr.width - padding}px`; + } + if (dr.left < padding) { + dropdown.style.left = `${padding}px`; + } + if (dr.bottom > vh - padding) { + dropdown.style.top = `${vh - dr.height - padding}px`; + } + if (dr.top < padding) { + dropdown.style.top = `${padding}px`; + } + }); + + const closeHandler = (e: MouseEvent) => { + const target = e.target as Node; + if (dropdown.contains(target) || buttonEl.contains(target)) return; + this.closeWordItemMenu(); + document.removeEventListener('click', closeHandler); + this.wordMenuCloseListener = null; + }; + this.wordMenuCloseListener = () => { + document.removeEventListener('click', closeHandler); + this.wordMenuCloseListener = null; + }; + setTimeout(() => document.addEventListener('click', closeHandler), 0); + } + + private showWordMenuListPicker(wordIndex: number, copyOnly: boolean): void { + const dropdown = document.getElementById('wordItemMenuDropdown'); + if (!dropdown || this.wordMenuOpenForIndex === null) return; + + this.wordMenuCopyOnly = copyOnly; + const currentList = this.lists[this.currentListIndex]; + const otherLists = this.lists + .map((list, index) => ({ list, index })) + .filter(({ index }) => index !== this.currentListIndex); + + if (otherLists.length === 0) { + const noOtherLabel = chrome.i18n.getMessage('no_other_lists') || 'No other lists'; + dropdown.innerHTML = ` +
+ ${DOMUtils.escapeHtml(noOtherLabel)} +
+ `; + return; + } + + dropdown.innerHTML = otherLists.map(({ list, index }) => ` + + `).join(''); + + dropdown.querySelectorAll('.word-item-menu-item[data-target-index]').forEach(item => { + item.addEventListener('click', (e) => { + e.stopPropagation(); + const targetIndex = Number((item as HTMLElement).dataset.targetIndex); + if (Number.isNaN(targetIndex)) return; + if (this.wordMenuCopyOnly) { + this.copyWordToOtherList(wordIndex, targetIndex); + } else { + this.moveWordToOtherList(wordIndex, targetIndex); + } + this.closeWordItemMenu(); + this.save(); + this.renderWords(); + this.renderLists(); + }); + }); + } + + private closeWordItemMenu(): void { + const dropdown = document.getElementById('wordItemMenuDropdown'); + if (dropdown) { + dropdown.classList.remove('open'); + dropdown.setAttribute('aria-hidden', 'true'); + dropdown.innerHTML = ''; + } + this.wordMenuOpenForIndex = null; + if (this.wordMenuCloseListener) { + this.wordMenuCloseListener(); + } + } + + private moveWordToOtherList(wordIndex: number, targetListIndex: number): void { + const list = this.lists[this.currentListIndex]; + const targetList = this.lists[targetListIndex]; + const word = list.words[wordIndex]; + if (!word || !targetList) return; + targetList.words.push({ ...word }); + list.words.splice(wordIndex, 1); + } + + private copyWordToOtherList(wordIndex: number, targetListIndex: number): void { + const list = this.lists[this.currentListIndex]; + const targetList = this.lists[targetListIndex]; + const word = list.words[wordIndex]; + if (!word || !targetList) return; + targetList.words.push({ ...word }); + } + private setupWordSelection(): void { document.getElementById('selectAllBtn')?.addEventListener('click', () => { const list = this.lists[this.currentListIndex]; @@ -890,6 +1053,7 @@ export class PopupController { } private renderWords(): void { + this.closeWordItemMenu(); const list = this.lists[this.currentListIndex]; const wordList = document.getElementById('wordList') as HTMLDivElement; @@ -922,6 +1086,7 @@ export class PopupController { const list = this.lists[this.currentListIndex]; const bgColor = word.background || list.background; const fgColor = word.foreground || list.foreground; + const menuTitle = chrome.i18n.getMessage('word_actions') || 'Actions'; return `
@@ -940,6 +1105,9 @@ export class PopupController { +
`;