From 6b531093c9f6bcc5cc0564378bd5bbbee539c993 Mon Sep 17 00:00:00 2001 From: liyuankui Date: Fri, 15 May 2026 12:07:49 +0800 Subject: [PATCH] fix(remark): cancel empty annotation on outside click; replace clear-all two-click with undo toast - Bug 1: clicking outside popup without interacting now silently removes the annotation instead of leaving an empty entry; annotation is kept only if user explicitly chose a color or typed a note - Bug 3: clear-all no longer uses a confusing two-click confirmation; single click clears immediately with a 5-second undo toast (consistent with single-item delete behaviour) - i18n: add remark_all_cleared key, remove unused remark_confirm_clear Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/_locales/en/messages.json | 8 ++-- src/ui/remark-mode.ts | 73 ++++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 11a8c17b..f2d39ee7 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -391,6 +391,10 @@ "message": "Cancel & remove", "description": "Cancel button in the annotation popup" }, + "remark_all_cleared": { + "message": "All cleared", + "description": "Undo toast label shown after clearing all remarks" + }, "remark_clear_all": { "message": "Clear all remarks", "description": "Tooltip for clearing all remarks" @@ -411,10 +415,6 @@ "message": "Suggestion", "description": "Label for the yellow annotation color" }, - "remark_confirm_clear": { - "message": "Click again to confirm", - "description": "Tooltip shown before confirming clear-all remarks" - }, "remark_copied": { "message": "Copied!", "description": "Feedback shown after remarks are successfully copied" diff --git a/src/ui/remark-mode.ts b/src/ui/remark-mode.ts index 9dbddf56..85f14d66 100644 --- a/src/ui/remark-mode.ts +++ b/src/ui/remark-mode.ts @@ -436,9 +436,11 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll popupEl.style.left = `${left}px`; // Wire color buttons — change existing annotation's color + let interacted = false; const colorBtns = popupEl.querySelectorAll('.remark-color-btn'); colorBtns.forEach(btn => { btn.addEventListener('click', () => { + interacted = true; colorBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); ann.color = btn.dataset.color as RemarkColor; @@ -477,11 +479,20 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll setTimeout(() => noteInput?.focus(), 50); - // Click outside → save note and close (annotation persists) + // Click outside → cancel (remove annotation) if user never interacted; otherwise save const outsideHandler = (e: MouseEvent) => { if (popupEl && !popupEl.contains(e.target as Node)) { - if (noteInput) { - ann.note = noteInput.value.trim(); + const note = noteInput?.value.trim() ?? ''; + if (!interacted && note === '') { + // No interaction — silently discard the annotation + annotations = annotations.filter(a => a.id !== annId); + renderHighlights(); + renderSidebarContent(); + notifyCount(); + void saveAnnotations(); + window.getSelection()?.removeAllRanges(); + } else if (noteInput) { + ann.note = note; renderSidebarContent(); void saveAnnotations(); } @@ -660,31 +671,55 @@ export function createRemarkMode(options: RemarkModeOptions): RemarkModeControll } }); - // Wire clear-all button (two-click confirmation) + // Wire clear-all button — immediate clear with 5s undo (consistent with single-item delete) const clearBtn = el.querySelector('.remark-sidebar-clear'); if (clearBtn) { - let confirmPending = false; clearBtn.addEventListener('click', () => { if (annotations.length === 0) return; - if (!confirmPending) { - confirmPending = true; - clearBtn.textContent = '⚠️'; - clearBtn.title = t('remark_confirm_clear', 'Click again to confirm'); - clearBtn.classList.add('remark-confirm'); - setTimeout(() => { - confirmPending = false; - clearBtn.textContent = '🗑️'; - clearBtn.title = t('remark_clear_all', 'Clear all remarks'); - clearBtn.classList.remove('remark-confirm'); - }, 3000); - return; - } - confirmPending = false; + const UNDO_SECONDS = 5; + const savedAnnotations = [...annotations]; annotations = []; renderHighlights(); renderSidebarContent(); notifyCount(); void saveAnnotations(); + + const list = el.querySelector('.remark-sidebar-list'); + if (!list) return; + let remaining = UNDO_SECONDS; + const undo = document.createElement('div'); + undo.className = 'remark-undo-row'; + undo.setAttribute('role', 'status'); + undo.setAttribute('aria-live', 'polite'); + undo.innerHTML = `${t('remark_all_cleared', 'All cleared')}
${remaining}s
`; + list.prepend(undo); + + const countdownEl = undo.querySelector('.remark-undo-countdown')!; + let committed = false; + const tick = setInterval(() => { + remaining--; + if (remaining > 0) countdownEl.textContent = `${remaining}s`; + else clearInterval(tick); + }, 1000); + const commit = (): void => { + if (committed) return; + committed = true; + clearInterval(tick); + undo.remove(); + }; + undo.querySelector('.remark-undo-btn')?.addEventListener('click', () => { + if (committed) return; + committed = true; + clearInterval(tick); + clearTimeout(timer); + undo.remove(); + annotations = savedAnnotations; + renderHighlights(); + renderSidebarContent(); + notifyCount(); + void saveAnnotations(); + }); + const timer = setTimeout(commit, UNDO_SECONDS * 1000); }); }