added move-to dropdown to word items

This commit is contained in:
2026-02-06 13:06:24 +03:00
parent 9294593975
commit 01135ff92e
17 changed files with 393 additions and 0 deletions

View File

@@ -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."
},

View File

@@ -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."
},

View File

@@ -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."
},

View File

@@ -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."
},

View File

@@ -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": "मर्ज करने के लिए कम से कम दो सूचियाँ चुनें।"
},

View File

@@ -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."
},

View File

@@ -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つのリストを選択してください。"
},

View File

@@ -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": "병합하려면 최소 두 개의 리스트를 선택하세요."
},

View File

@@ -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."
},

View File

@@ -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."
},

View File

@@ -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."
},

View File

@@ -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": "Выберите как минимум два списка для объединения."
},

View File

@@ -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."
},

View File

@@ -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": "选择至少两个列表进行合并。"
},

View File

@@ -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;

View File

@@ -196,6 +196,9 @@
<!-- Word List -->
<div id="wordList" class="word-list-container"></div>
<!-- Word item 3-dot menu dropdown (positioned by JS) -->
<div id="wordItemMenuDropdown" class="word-item-menu-dropdown" role="menu" aria-hidden="true"></div>
<!-- Help text -->
<p class="word-list-hint" data-i18n="multi_select_hint">Click to select • Ctrl/Cmd+Click for multi-select</p>
</div>

View File

@@ -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<string, number>();
private wordMenuOpenForIndex: number | null = null;
private wordMenuCopyOnly = false;
private wordMenuCloseListener: (() => void) | null = null;
async initialize(): Promise<void> {
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 = `
<button type="button" class="word-item-menu-item" data-action="move">
<i class="fa-solid fa-arrow-right"></i>
<span>${DOMUtils.escapeHtml(moveLabel)}</span>
</button>
<button type="button" class="word-item-menu-item" data-action="copy">
<i class="fa-solid fa-copy"></i>
<span>${DOMUtils.escapeHtml(copyLabel)}</span>
</button>
`;
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 = `
<div class="word-item-menu-item disabled">
<span>${DOMUtils.escapeHtml(noOtherLabel)}</span>
</div>
`;
return;
}
dropdown.innerHTML = otherLists.map(({ list, index }) => `
<button type="button" class="word-item-menu-item" data-target-index="${index}">
<span class="list-color-indicator" style="background-color: ${DOMUtils.escapeHtml(list.background)}"></span>
<span>${DOMUtils.escapeHtml(list.name)}</span>
</button>
`).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 `
<div class="word-item ${isSelected ? 'selected' : ''}" data-index="${realIndex}">
@@ -940,6 +1105,9 @@ export class PopupController {
<i class="fa-solid fa-eye-slash eye-disabled"></i>
</span>
</label>
<button type="button" class="word-item-icon-btn word-item-menu-btn" data-index="${realIndex}" title="${DOMUtils.escapeHtml(menuTitle)}" aria-label="${DOMUtils.escapeHtml(menuTitle)}">
<i class="fa-solid fa-ellipsis-v"></i>
</button>
</div>
</div>
`;