From 22a1b33a429ffb7042752977ad8ae5d5f937953f Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Mon, 31 Aug 2026 15:11:15 -0400 Subject: [PATCH] Remove library tree event listeners on teardown Listeners added to the item and collection trees' event bindings survived unregister() and held ZoteroPane and the views for as long as the tree objects did. makeClassEventDispatcher() now provides clearEventListeners(), which the trees call on unregister, so callers don't need to remove their own listeners. The collection tree focus listener, a plain DOM listener, is removed in destroy(). --- chrome/content/zotero/collectionTree.jsx | 1 + chrome/content/zotero/itemTree.jsx | 1 + chrome/content/zotero/xpcom/utilities_internal.js | 8 ++++++++ chrome/content/zotero/zoteroPane.js | 12 +++++++++++- 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/chrome/content/zotero/collectionTree.jsx b/chrome/content/zotero/collectionTree.jsx index 9dc6b2c114..21de5676ad 100644 --- a/chrome/content/zotero/collectionTree.jsx +++ b/chrome/content/zotero/collectionTree.jsx @@ -1367,6 +1367,7 @@ var CollectionTree = class CollectionTree extends LibraryTree { unregister() { this._uninitialized = true; Zotero.Notifier.unregisterObserver(this._unregisterID); + this.clearEventListeners(); } //////////////////////////////////////////////////////////////////////////////// diff --git a/chrome/content/zotero/itemTree.jsx b/chrome/content/zotero/itemTree.jsx index 192626fa43..748372ace1 100644 --- a/chrome/content/zotero/itemTree.jsx +++ b/chrome/content/zotero/itemTree.jsx @@ -1145,6 +1145,7 @@ var ItemTree = class ItemTree extends LibraryTree { for (let id of this._prefsObserverIDs) { Zotero.Prefs.unregisterObserver(id); } + this.clearEventListeners(); this._writeColumnPrefsToFile(true); } diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js index d6fb48e231..5eda717f7f 100644 --- a/chrome/content/zotero/xpcom/utilities_internal.js +++ b/chrome/content/zotero/xpcom/utilities_internal.js @@ -2374,6 +2374,14 @@ Zotero.Utilities.Internal = { } ev.listeners.delete(listener); }; + + /** + * Remove all listeners from all events, so that an object being torn down doesn't + * keep its listeners' closures alive + */ + cls.prototype.clearEventListeners = function () { + this._events = null; + }; cls.prototype._waitForEvent = async function (event) { return new Zotero.Promise((resolve) => { diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index bb47aed176..0d7ab0b0ef 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -38,6 +38,8 @@ var ZoteroPane = new function () { this._listeners = {}; this.__defineGetter__('loaded', function () { return _loaded; }); var _lastSelectedItems = []; + // [element, listener] for the collection tree focus listener added in initCollectionsTree() + var _collectionsTreeFocusListener = null; var lastFocusedElement = null; this.lastKeyPress = null; @@ -779,6 +781,11 @@ var ZoteroPane = new function () { this.serializePersist(); + if (_collectionsTreeFocusListener) { + let [elem, listener] = _collectionsTreeFocusListener; + elem.removeEventListener("focus", listener); + _collectionsTreeFocusListener = null; + } if(this.collectionsView) this.collectionsView.unregister(); if(this.itemsView) this.itemsView.unregister(); if (_syncRemindersObserverID) { @@ -1651,7 +1658,10 @@ var ZoteroPane = new function () { dragAndDrop: true, multiSelect: true }); - collectionsTree.firstChild.addEventListener("focus", ZoteroPane.collectionsView.recordCollectionTreeFocus); + let treeElem = collectionsTree.firstChild; + let focusListener = ZoteroPane.collectionsView.recordCollectionTreeFocus; + treeElem.addEventListener("focus", focusListener); + _collectionsTreeFocusListener = [treeElem, focusListener]; } catch (e) { Zotero.logError(e);