mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
fix(split-pane): stop leaking document listeners on repeated split-picker toggles
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d0a887d98a
commit
33b2605815
@@ -191,7 +191,7 @@
|
||||
</button>
|
||||
<button class="btn-icon-header btn-file-viewer" onclick="app.toggleFileBrowserButton()" title="File Viewer" aria-label="Open file viewer" aria-expanded="false"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></button>
|
||||
<button class="btn-icon-header btn-multimonitor btn-multimonitor--hidden" onclick="app.launchMultiMonitor()" title="Open Codeman across all displays" aria-label="Open Codeman across all displays"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="13" height="9" rx="1.5"/><rect x="11" y="9" width="11" height="8" rx="1.5"/></svg></button>
|
||||
<button class="btn-icon-header btn-split btn-split--hidden" onclick="app.openSplitPicker()" title="Split: open a second session beside this one" aria-label="Split: open a second session beside this one"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg></button>
|
||||
<button class="btn-icon-header btn-split btn-split--hidden" onclick="app.openSplitPicker(event)" title="Split: open a second session beside this one" aria-label="Split: open a second session beside this one"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg></button>
|
||||
<button class="btn-icon-header btn-ultracode-agents btn-ultracode-agents--hidden" onclick="app.toggleUltracodeAgentsPanel()" title="Ultracode / Workflow agents" aria-label="Open ultracode workflow agents"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="12" r="2.5"/><path d="M8.2 7.2 15.6 11M8.2 16.8 15.6 13"/></svg></button>
|
||||
<div class="header-plan-usage header-plan-usage--hidden" id="planUsageChip" title="Claude and Codex plan usage limits">—</div>
|
||||
<button class="btn-icon-header btn-notifications" onclick="app.toggleNotifications()" title="Notifications" aria-label="Toggle notifications" style="display:none;">
|
||||
|
||||
@@ -124,7 +124,14 @@
|
||||
})(window);
|
||||
|
||||
Object.assign(CodemanApp.prototype, {
|
||||
openSplitPicker() {
|
||||
openSplitPicker(event) {
|
||||
// Mirrors toggleRunModeMenu (session-ui.js): stopPropagation on the
|
||||
// OPENING click so it never reaches the outside-click listener this
|
||||
// same call is about to register — without it, a click landing on the
|
||||
// button's own inner <svg> (matched by neither `menu.contains()` nor
|
||||
// the old exact-node check below) bubbled straight through to
|
||||
// `document` and self-closed the menu it just opened.
|
||||
event?.stopPropagation();
|
||||
if (this._splitPane) {
|
||||
this.closeSplitPane();
|
||||
return;
|
||||
@@ -134,8 +141,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
this.sessionOrder,
|
||||
this.activeSessionId
|
||||
);
|
||||
const existing = document.getElementById('splitPickerMenu');
|
||||
if (existing) existing.remove();
|
||||
// Route a pre-existing menu through the SAME dismiss path used
|
||||
// everywhere else, instead of a raw `.remove()`: a genuinely still-open
|
||||
// menu has live document listeners (see below), and a raw removal left
|
||||
// them attached forever — only the single-slot field below got
|
||||
// overwritten, so every prior pair but the last was orphaned on
|
||||
// `document` with no way to ever find and remove it again.
|
||||
this._dismissSplitPicker();
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.id = 'splitPickerMenu';
|
||||
@@ -162,14 +174,32 @@ Object.assign(CodemanApp.prototype, {
|
||||
// Dismiss on outside click or Escape — same one-shot listener pattern as
|
||||
// session-ui.js's other transient popovers (toggleCaseSettings(),
|
||||
// toggleRunModeMenu()). Deferred by a tick so the click that OPENED the
|
||||
// menu (still bubbling) doesn't immediately close it. Picking an item
|
||||
// menu (still bubbling) doesn't immediately close it — reinforced by
|
||||
// the button's own stopPropagation() above, which is what actually
|
||||
// stops that same click reaching `document` at all. Picking an item
|
||||
// (above) calls the SAME dismiss method, so these listeners never
|
||||
// outlive the menu either way.
|
||||
//
|
||||
// Self-removing by identity: each handler removes ITSELF (and its
|
||||
// sibling) the moment it fires, rather than leaning solely on the
|
||||
// `this._splitPickerDismissHandlers` field. That field is still kept in
|
||||
// sync (so `_dismissSplitPicker()` called from elsewhere — the picker
|
||||
// item's onclick above, or a still-open menu at the top of this method
|
||||
// — can find and remove the CURRENT pair), but no path here can ever
|
||||
// again leave a pair attached to `document` with nothing referencing it.
|
||||
const closeOnOutsideClick = (e) => {
|
||||
if (!menu.contains(e.target) && e.target !== splitBtn) this._dismissSplitPicker();
|
||||
if (menu.contains(e.target) || e.target.closest('.btn-split')) return;
|
||||
document.removeEventListener('click', closeOnOutsideClick);
|
||||
document.removeEventListener('keydown', closeOnEscape);
|
||||
this._splitPickerDismissHandlers = null;
|
||||
menu.remove();
|
||||
};
|
||||
const closeOnEscape = (e) => {
|
||||
if (e.key === 'Escape') this._dismissSplitPicker();
|
||||
if (e.key !== 'Escape') return;
|
||||
document.removeEventListener('click', closeOnOutsideClick);
|
||||
document.removeEventListener('keydown', closeOnEscape);
|
||||
this._splitPickerDismissHandlers = null;
|
||||
menu.remove();
|
||||
};
|
||||
this._splitPickerDismissHandlers = { closeOnOutsideClick, closeOnEscape };
|
||||
setTimeout(() => document.addEventListener('click', closeOnOutsideClick), 0);
|
||||
|
||||
Reference in New Issue
Block a user