updated list items ui

This commit is contained in:
2026-02-04 23:45:25 +03:00
parent 0f9babbb76
commit 90b9ea5134
6 changed files with 555 additions and 87 deletions

View File

@@ -64,15 +64,16 @@ export class ListManagerController {
const listsContainer = document.getElementById('listsContainer');
listsContainer?.addEventListener('click', (e) => this.handleListClick(e));
listsContainer?.addEventListener('change', (e) => this.handleListCheckboxChange(e));
listsContainer?.addEventListener('dragstart', (e) => this.handleDragStart(e));
listsContainer?.addEventListener('dragover', (e) => this.handleDragOver(e));
listsContainer?.addEventListener('drop', (e) => this.handleDrop(e));
listsContainer?.addEventListener('dragend', () => this.clearDragState());
const wordList = document.getElementById('wordList');
wordList?.addEventListener('click', (e) => this.handleWordListClick(e));
wordList?.addEventListener('change', (e) => this.handleWordListChange(e));
wordList?.addEventListener('keydown', (e) => this.handleWordListKeydown(e));
wordList?.addEventListener('blur', (e) => this.handleWordListBlur(e), true);
}
private setupStorageSync(): void {
@@ -313,29 +314,28 @@ export class ListManagerController {
const listItem = target.closest('.list-item') as HTMLElement | null;
if (!listItem) return;
if (target.tagName === 'INPUT' || target.tagName === 'BUTTON') return;
const index = Number(listItem.dataset.index);
if (Number.isNaN(index)) return;
const mouseEvent = event as MouseEvent;
// Ctrl/Cmd + click for multi-select
if (mouseEvent.ctrlKey || mouseEvent.metaKey) {
if (this.selectedLists.has(index)) {
this.selectedLists.delete(index);
} else {
this.selectedLists.add(index);
}
this.renderLists();
return;
}
// Regular click - set as current and clear multi-selection
this.currentListIndex = index;
this.selectedLists.clear();
this.selectedWords.clear();
this.render();
}
private handleListCheckboxChange(event: Event): void {
const target = event.target as HTMLInputElement;
if (!target.classList.contains('list-checkbox')) return;
const index = Number(target.dataset.index);
if (Number.isNaN(index)) return;
if (target.checked) {
this.selectedLists.add(index);
} else {
this.selectedLists.delete(index);
}
}
private handleDragStart(event: DragEvent): void {
const target = (event.target as HTMLElement).closest('.list-item') as HTMLElement | null;
if (!target) return;
@@ -384,23 +384,84 @@ export class ListManagerController {
document.querySelectorAll('.list-item.drag-over').forEach(item => item.classList.remove('drag-over'));
}
private handleWordListChange(event: Event): void {
const target = event.target as HTMLInputElement;
private handleWordListClick(event: Event): void {
const target = event.target as HTMLElement;
const list = this.lists[this.currentListIndex];
if (!list) return;
if (target.classList.contains('word-checkbox') && target.dataset.index != null) {
const editBtn = target.closest('.edit-word-btn') as HTMLElement | null;
if (editBtn) {
event.stopPropagation();
const index = Number(editBtn.dataset.index);
if (Number.isNaN(index)) return;
this.startEditingWord(index);
return;
}
// Handle toggle button click
if (target.classList.contains('toggle-btn')) {
event.stopPropagation();
const index = Number(target.dataset.index);
if (target.checked) {
this.selectedWords.add(index);
} else {
if (Number.isNaN(index)) return;
const word = list.words[index];
if (word) {
word.active = !word.active;
this.save();
}
return;
}
// Don't select if clicking on color inputs or edit input
if (target.tagName === 'INPUT') {
if ((target as HTMLInputElement).type === 'color') {
event.stopPropagation();
return;
}
if (target.classList.contains('word-edit-input')) {
event.stopPropagation();
return;
}
}
// Don't select if clicking inside word-actions area (except on the word item itself)
if (target.closest('.word-actions') && !target.classList.contains('word-item')) {
return;
}
// Handle word item selection
const wordItem = target.closest('.word-item') as HTMLElement | null;
if (!wordItem) return;
const index = Number(wordItem.dataset.index);
if (Number.isNaN(index)) return;
const mouseEvent = event as MouseEvent;
// Ctrl/Cmd + click for multi-select
if (mouseEvent.ctrlKey || mouseEvent.metaKey) {
if (this.selectedWords.has(index)) {
this.selectedWords.delete(index);
} else {
this.selectedWords.add(index);
}
this.renderWords();
return;
}
const editIndex = Number(target.dataset.bgEdit ?? target.dataset.fgEdit ?? target.dataset.activeEdit ?? -1);
// Regular click - toggle selection
if (this.selectedWords.has(index)) {
this.selectedWords.delete(index);
} else {
this.selectedWords.add(index);
}
this.renderWords();
}
private handleWordListChange(event: Event): void {
const target = event.target as HTMLInputElement;
const list = this.lists[this.currentListIndex];
if (!list) return;
const editIndex = Number(target.dataset.bgEdit ?? target.dataset.fgEdit ?? -1);
if (Number.isNaN(editIndex) || editIndex < 0) return;
const word = list.words[editIndex];
@@ -408,14 +469,28 @@ export class ListManagerController {
if (target.dataset.bgEdit != null) word.background = target.value;
if (target.dataset.fgEdit != null) word.foreground = target.value;
if (target.dataset.activeEdit != null) word.active = target.checked;
this.save();
}
private handleWordListKeydown(event: KeyboardEvent): void {
if (event.key !== 'Enter') return;
private startEditingWord(index: number): void {
const wordItem = document.querySelector(`.word-item[data-index="${index}"]`);
if (!wordItem) return;
const textSpan = wordItem.querySelector('.word-text') as HTMLElement;
const input = wordItem.querySelector('.word-edit-input') as HTMLInputElement;
if (!textSpan || !input) return;
textSpan.classList.add('editing');
input.classList.add('active');
input.focus();
input.select();
}
private handleWordListBlur(event: Event): void {
const target = event.target as HTMLInputElement;
if (!target.classList.contains('word-edit-input')) return;
const list = this.lists[this.currentListIndex];
if (!list) return;
@@ -425,8 +500,26 @@ export class ListManagerController {
const word = list.words[index];
if (!word) return;
word.wordStr = target.value;
this.save();
const newValue = target.value.trim();
if (newValue && newValue !== word.wordStr) {
word.wordStr = newValue;
this.save();
} else {
this.renderWords();
}
}
private handleWordListKeydown(event: KeyboardEvent): void {
const target = event.target as HTMLInputElement;
if (!target.classList.contains('word-edit-input')) return;
if (event.key === 'Enter') {
event.preventDefault();
target.blur();
} else if (event.key === 'Escape') {
event.preventDefault();
this.renderWords();
}
}
private getSelectedListIndices(): number[] {
@@ -459,10 +552,10 @@ export class ListManagerController {
const item = document.createElement('div');
item.className = 'list-item';
if (index === this.currentListIndex) item.classList.add('active');
if (this.selectedLists.has(index)) item.classList.add('selected');
item.draggable = true;
item.dataset.index = index.toString();
item.innerHTML = `
<input type="checkbox" class="list-checkbox" data-index="${index}" ${this.selectedLists.has(index) ? 'checked' : ''}>
<div class="list-meta">
<div class="list-name">${DOMUtils.escapeHtml(list.name)}</div>
<div class="list-stats">${total} words • ${activeCount} active</div>
@@ -519,13 +612,19 @@ export class ListManagerController {
wordList.innerHTML = entries.map(entry => {
const word = entry.word;
const index = entry.index;
const isSelected = this.selectedWords.has(index);
return `
<div class="word-item ${word.active ? '' : 'disabled'}">
<input type="checkbox" class="word-checkbox" data-index="${index}" ${this.selectedWords.has(index) ? 'checked' : ''}>
<input type="text" value="${DOMUtils.escapeHtml(word.wordStr)}" data-word-edit="${index}">
<input type="color" value="${word.background || list.background}" data-bg-edit="${index}">
<input type="color" value="${word.foreground || list.foreground}" data-fg-edit="${index}">
<input type="checkbox" data-active-edit="${index}" ${word.active ? 'checked' : ''} title="Active">
<div class="word-item ${word.active ? '' : 'disabled'} ${isSelected ? 'selected' : ''}" data-index="${index}">
<span class="word-text">${DOMUtils.escapeHtml(word.wordStr)}</span>
<input type="text" class="word-edit-input" value="${DOMUtils.escapeHtml(word.wordStr)}" data-word-edit="${index}">
<div class="word-actions">
<button class="icon-btn edit-word-btn" data-index="${index}" title="Edit word">
<i class="fa-solid fa-pen"></i>
</button>
<input type="color" value="${word.background || list.background}" data-bg-edit="${index}" title="Background color">
<input type="color" value="${word.foreground || list.foreground}" data-fg-edit="${index}" title="Foreground color">
<button class="toggle-btn ${word.active ? 'active' : ''}" data-index="${index}" title="Toggle active"></button>
</div>
</div>
`;
}).join('');

View File

@@ -201,17 +201,79 @@ export class PopupController {
}
private setupWordListEvents(wordList: HTMLDivElement): void {
wordList.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
if (target.type === 'checkbox' && target.dataset.index != null) {
const index = +target.dataset.index;
if (target.checked) {
this.selectedCheckboxes.add(index);
} else {
wordList.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const list = this.lists[this.currentListIndex];
if (!list) return;
// Handle edit button click
const editBtn = target.closest('.icon-btn.edit-word-btn') as HTMLElement | null;
if (editBtn) {
e.stopPropagation();
const index = Number(editBtn.dataset.index);
if (!Number.isNaN(index)) {
this.startEditingWord(index);
}
return;
}
// Handle toggle button click
if (target.classList.contains('toggle-btn')) {
e.stopPropagation();
const index = Number(target.dataset.index);
if (!Number.isNaN(index)) {
const word = list.words[index];
if (word) {
word.active = !word.active;
this.save();
}
}
return;
}
// Don't select if clicking on color inputs or edit input
if (target.tagName === 'INPUT') {
if ((target as HTMLInputElement).type === 'color') {
e.stopPropagation();
return;
}
if (target.classList.contains('word-edit-input')) {
e.stopPropagation();
return;
}
}
// Don't select if clicking inside word-actions area
if (target.closest('.word-actions') && !target.classList.contains('word-item')) {
return;
}
// Handle word item selection
const wordItem = target.closest('.word-item') as HTMLElement | null;
if (!wordItem) return;
const index = Number(wordItem.dataset.index);
if (Number.isNaN(index)) return;
const mouseEvent = e as MouseEvent;
// Ctrl/Cmd + click for multi-select
if (mouseEvent.ctrlKey || mouseEvent.metaKey) {
if (this.selectedCheckboxes.has(index)) {
this.selectedCheckboxes.delete(index);
} else {
this.selectedCheckboxes.add(index);
}
this.renderWords();
return;
}
// Regular click - toggle selection
if (this.selectedCheckboxes.has(index)) {
this.selectedCheckboxes.delete(index);
} else {
this.selectedCheckboxes.add(index);
}
this.renderWords();
});
wordList.addEventListener('change', (e) => {
@@ -227,19 +289,40 @@ export class PopupController {
});
wordList.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const target = e.target as HTMLInputElement;
const index = +(target.dataset.wordEdit ?? -1);
if (index === -1) return;
const target = e.target as HTMLInputElement;
if (!target.classList.contains('word-edit-input')) return;
const word = this.lists[this.currentListIndex].words[index];
if (target.dataset.wordEdit != null) {
word.wordStr = target.value;
this.save();
}
if (e.key === 'Enter') {
e.preventDefault();
target.blur();
} else if (e.key === 'Escape') {
e.preventDefault();
this.renderWords();
}
});
wordList.addEventListener('blur', (e) => {
const target = e.target as HTMLInputElement;
if (!target.classList.contains('word-edit-input')) return;
const list = this.lists[this.currentListIndex];
if (!list) return;
const index = Number(target.dataset.wordEdit ?? -1);
if (Number.isNaN(index) || index < 0) return;
const word = list.words[index];
if (!word) return;
const newValue = target.value.trim();
if (newValue && newValue !== word.wordStr) {
word.wordStr = newValue;
this.save();
} else {
this.renderWords();
}
}, true);
let scrolling = false;
wordList.addEventListener('scroll', () => {
if (scrolling) return;
@@ -251,6 +334,20 @@ export class PopupController {
});
}
private startEditingWord(index: number): void {
const wordItem = document.querySelector(`.word-item[data-index="${index}"]`);
if (!wordItem) return;
const textSpan = wordItem.querySelector('.word-text') as HTMLElement;
const input = wordItem.querySelector('.word-edit-input') as HTMLInputElement;
if (!textSpan || !input) return;
textSpan.classList.add('editing');
input.classList.add('active');
input.focus();
input.select();
}
private setupWordSelection(): void {
document.getElementById('selectAllBtn')?.addEventListener('click', () => {
const list = this.lists[this.currentListIndex];
@@ -670,7 +767,7 @@ export class PopupController {
}
const itemHeight = 32;
const itemSpacing = 2;
const itemSpacing = 4;
const totalItemHeight = itemHeight + itemSpacing;
const containerHeight = wordList.clientHeight || 250;
const scrollTop = wordList.scrollTop;
@@ -682,7 +779,7 @@ export class PopupController {
const spacer = document.createElement('div');
spacer.style.position = 'relative';
spacer.style.height = `${filteredWords.length * totalItemHeight}px`;
spacer.style.height = `${filteredWords.length * totalItemHeight - itemSpacing}px`;
spacer.style.width = '100%';
for (let i = startIndex; i < endIndex; i++) {
@@ -705,21 +802,31 @@ export class PopupController {
private createWordItem(word: HighlightWord, realIndex: number, displayIndex: number, itemHeight: number): HTMLDivElement {
const container = document.createElement('div');
container.className = 'word-item';
if (this.selectedCheckboxes.has(realIndex)) {
container.classList.add('selected');
}
if (word.active === false) {
container.classList.add('disabled');
}
container.style.cssText = `
position: absolute;
top: ${displayIndex * (itemHeight + 2)}px;
top: ${displayIndex * (itemHeight + 4)}px;
`;
container.dataset.index = realIndex.toString();
const list = this.lists[this.currentListIndex];
container.innerHTML = `
<input type="checkbox" class="word-checkbox" data-index="${realIndex}" ${this.selectedCheckboxes.has(realIndex) ? 'checked' : ''} title="${chrome.i18n.getMessage('select_title') || 'Select'}">
<input type="text" value="${DOMUtils.escapeHtml(word.wordStr)}" data-word-edit="${realIndex}" placeholder="${chrome.i18n.getMessage('word_placeholder') || 'Word or phrase'}">
<input type="color" value="${word.background || list.background}" data-bg-edit="${realIndex}" title="${chrome.i18n.getMessage('background_color_title') || 'Background color'}">
<input type="color" value="${word.foreground || list.foreground}" data-fg-edit="${realIndex}" title="${chrome.i18n.getMessage('text_color_title') || 'Text color'}">
<span class="word-text">${DOMUtils.escapeHtml(word.wordStr)}</span>
<input type="text" class="word-edit-input" value="${DOMUtils.escapeHtml(word.wordStr)}" data-word-edit="${realIndex}">
<div class="word-actions">
<button class="icon-btn edit-word-btn" data-index="${realIndex}" title="${chrome.i18n.getMessage('edit') || 'Edit'}">
<i class="fa-solid fa-pen"></i>
</button>
<input type="color" value="${word.background || list.background}" data-bg-edit="${realIndex}" title="${chrome.i18n.getMessage('background_color_title') || 'Background color'}">
<input type="color" value="${word.foreground || list.foreground}" data-fg-edit="${realIndex}" title="${chrome.i18n.getMessage('text_color_title') || 'Text color'}">
<button class="toggle-btn ${word.active !== false ? 'active' : ''}" data-index="${realIndex}" title="${chrome.i18n.getMessage('toggle_active') || 'Toggle active'}"></button>
</div>
`;
return container;