mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-30 16:42:15 -04:00
fix(ui): route transient dropdown menus through escMenuStack to stop listener leaks (#4684)
The app's ad-hoc dropdown/context menus each wire their own document-level outside-click listener, but that listener only removes itself on an *outside* click. Every other dismissal path -- clicking a menu item (which calls el.remove() directly), a Cancel button, Escape, or the "close the previously-open menu" reopen sweep -- tears the node down without unregistering the listener, orphaning it on `document`. The stranded listener then lingers and can break the next menu interaction: the recurring "the button stops working until I refresh the page" class of bug (e.g. delete an email, then the kebab/more button is dead on the other rows). Route all 16 of these menus through the existing escMenuStack helper (bindMenuDismiss / dismissOrRemove), exactly as documentLibrary.js _showLibDropdown, cookbookRunning.js, and research/panel.js already do: a single idempotent close() owns the teardown and is released on every dismissal path, reopen sweeps use dismissOrRemove() instead of a bare .remove(), and Escape flows through the central LIFO esc-stack arbiter. Net -49 lines. Menus migrated: cookbook _showDepMenu; document export menu and _openDocAiReplyChoice; emailInbox _showEmailMenu; emailLibrary _showReaderMoreMenu / _showCardMenu / _showBulkActionsMenu; gallery _showGalleryBulkMenu; notes _pickCustomDate / _openNoteCornerMenu; settings (3 unified-integrations dropdowns); skills _openSkillMenu; tasks _showTaskDropdown; compare _toggleExportMenu. Per-menu semantics preserved (anchor-as-inside tests, the tasks 250ms ghost-click guard, emailLibrary's reader-more-active anchor class and the bulk-Cancel select-mode reset, settings' reused-vs-recreated lifecycles). Six menus with custom lifecycles (notes _openReminderMenu, sessions long-press, document markdown-toolbar, emojiPicker, compare model selector) are intentionally left for a follow-up -- each needs individual review. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+6
-6
@@ -7,6 +7,7 @@
|
||||
|
||||
import uiModule from './ui.js';
|
||||
import * as spinnerModule from './spinner.js';
|
||||
import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
|
||||
const API = window.location.origin;
|
||||
let skills = [];
|
||||
@@ -391,14 +392,14 @@ function _svg(paths, { fill = 'none', size = 13 } = {}) {
|
||||
// Kebab dropdown for a collapsed skill card — same actions + icons as the
|
||||
// expanded footer (Publish/Unpublish · Edit · Delete).
|
||||
function _openSkillMenu(btn, card, sk, name, isPublished) {
|
||||
document.querySelectorAll('.skill-kebab-menu').forEach(m => m.remove());
|
||||
document.querySelectorAll('.skill-kebab-menu').forEach(dismissOrRemove);
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'skill-kebab-menu';
|
||||
const mk = (paths, label, opts, onClick) => {
|
||||
const item = document.createElement('button');
|
||||
item.className = 'skill-kebab-item' + (opts && opts.danger ? ' danger' : '');
|
||||
item.innerHTML = _svg(paths, opts) + `<span>${label}</span>`;
|
||||
item.addEventListener('click', (e) => { e.stopPropagation(); menu.remove(); onClick(); });
|
||||
item.addEventListener('click', (e) => { e.stopPropagation(); close(); onClick(); });
|
||||
menu.appendChild(item);
|
||||
};
|
||||
if (isPublished) mk(_ICON.unpublish, 'Unpublish', {}, () => _setSkillStatus(name, 'draft'));
|
||||
@@ -410,7 +411,7 @@ function _openSkillMenu(btn, card, sk, name, isPublished) {
|
||||
selItem.innerHTML = '<svg class="memory-select-btn-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3" fill="currentColor" stroke="none"/></svg><span>Select</span>';
|
||||
selItem.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
menu.remove();
|
||||
close();
|
||||
if (!_selectMode) _enterSelectMode();
|
||||
_selectedNames.add(name);
|
||||
renderSkillsList();
|
||||
@@ -432,7 +433,7 @@ function _openSkillMenu(btn, card, sk, name, isPublished) {
|
||||
const cancelItem = document.createElement('button');
|
||||
cancelItem.className = 'skill-kebab-item dropdown-cancel-mobile';
|
||||
cancelItem.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg><span>Cancel</span>';
|
||||
cancelItem.addEventListener('click', (e) => { e.stopPropagation(); menu.remove(); });
|
||||
cancelItem.addEventListener('click', (e) => { e.stopPropagation(); close(); });
|
||||
menu.appendChild(cancelItem);
|
||||
|
||||
document.body.appendChild(menu);
|
||||
@@ -453,8 +454,7 @@ function _openSkillMenu(btn, card, sk, name, isPublished) {
|
||||
menu.style.maxHeight = Math.max(80, window.innerHeight - 12 - mr2.top) + 'px';
|
||||
menu.style.overflowY = 'auto';
|
||||
}
|
||||
const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('click', close, true); } };
|
||||
setTimeout(() => document.addEventListener('click', close, true), 0);
|
||||
const close = bindMenuDismiss(menu, () => { menu.remove(); }, (ev) => !menu.contains(ev.target));
|
||||
}
|
||||
|
||||
// Cards for the agent's built-in tool capabilities (from
|
||||
|
||||
Reference in New Issue
Block a user