From aba82b822d87aae1f134b1e25a32c3c764efd7f8 Mon Sep 17 00:00:00 2001 From: Ramon Mi Date: Wed, 6 May 2026 19:33:05 +0800 Subject: [PATCH 1/3] fix: ensure consistent ordering of rows inserted by plugins Ensure deterministic ordering for rows with the same `position` --- chrome/content/zotero/elements/itemBox.js | 64 +++++++++++-------- .../zotero/xpcom/pluginAPI/itemPaneManager.js | 16 +++++ 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/chrome/content/zotero/elements/itemBox.js b/chrome/content/zotero/elements/itemBox.js index 7016085b19..a80c267163 100644 --- a/chrome/content/zotero/elements/itemBox.js +++ b/chrome/content/zotero/elements/itemBox.js @@ -953,41 +953,51 @@ restoreCustomRowElements() { if (!this._customRowElemCache) return; for (let position of Object.keys(this._customRowElemCache)) { - this._customRowElemCache[position].forEach((rowElem) => { - this.insertCustomRow(rowElem, position); - }); + this.insertCustomRow(this._customRowElemCache[position], position); this._customRowElemCache[position] = []; } } insertCustomRow(rowElem, position = "end") { - switch (position) { - case "start": { - this._infoTable.prepend(rowElem); - break; - } - case "afterCreators": { - // The `_firstRowBeforeCreators` is actually the first row after creator rows - if (this._firstRowBeforeCreators) { - this._infoTable.insertBefore(rowElem, this._firstRowBeforeCreators); - // Update the anchor node for creator rows - this._firstRowBeforeCreators = rowElem; + // Handle both single elements and arrays + let elements = Array.isArray(rowElem) ? rowElem : [rowElem]; + + // Determine insertion order based on position + // Only reverse when there are multiple elements + if (position !== "end" && elements.length > 1) { + // For 'start' and 'afterCreators', reverse to maintain order + elements = elements.reverse(); + } + + for (let elem of elements) { + switch (position) { + case "start": { + this._infoTable.prepend(elem); + break; } - else { - this._infoTable.append(rowElem); + case "afterCreators": { + // The `_firstRowBeforeCreators` is actually the first row after creator rows + if (this._firstRowBeforeCreators) { + this._infoTable.insertBefore(elem, this._firstRowBeforeCreators); + // Update the anchor node for creator rows + this._firstRowBeforeCreators = elem; + } + else { + this._infoTable.append(elem); + } + break; } - break; - } - case "end": - default: { - let dateAddedRow = this._infoTable.querySelector(".meta-label[fieldname=dateAdded]")?.parentElement; - if (dateAddedRow) { - this._infoTable.insertBefore(rowElem, dateAddedRow); + case "end": + default: { + let dateAddedRow = this._infoTable.querySelector(".meta-label[fieldname=dateAdded]")?.parentElement; + if (dateAddedRow) { + this._infoTable.insertBefore(elem, dateAddedRow); + } + else { + this._infoTable.append(elem); + } + break; } - else { - this._infoTable.append(rowElem); - } - break; } } } diff --git a/chrome/content/zotero/xpcom/pluginAPI/itemPaneManager.js b/chrome/content/zotero/xpcom/pluginAPI/itemPaneManager.js index f8890fc799..6a164466f2 100644 --- a/chrome/content/zotero/xpcom/pluginAPI/itemPaneManager.js +++ b/chrome/content/zotero/xpcom/pluginAPI/itemPaneManager.js @@ -437,6 +437,22 @@ } this._refresh([rowID]); } + + get data() { + let options = this.options; + // Ensure deterministic ordering for rows with the same `position`. + // Sort `rowID` so insertion (prepend/insertBefore) produces a + // stable visual order regardless of registration timing. + if (Array.isArray(options) && options.length > 1) { + options = options.sort((a, b) => + String(a.rowID ?? "").localeCompare(String(b.rowID ?? "")) + ); + } + return { + updateID: this.updateID, + options, + }; + } } From 6164f3554d550918c0d0227b5af9dd303207c55f Mon Sep 17 00:00:00 2001 From: Ramon Mi Date: Wed, 6 May 2026 21:27:28 +0800 Subject: [PATCH 2/3] resolve order inversion issue in `restoreCustomRowElements` function --- chrome/content/zotero/elements/itemBox.js | 68 +++++++++++------------ 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/chrome/content/zotero/elements/itemBox.js b/chrome/content/zotero/elements/itemBox.js index a80c267163..b8bf8cf17f 100644 --- a/chrome/content/zotero/elements/itemBox.js +++ b/chrome/content/zotero/elements/itemBox.js @@ -953,51 +953,45 @@ restoreCustomRowElements() { if (!this._customRowElemCache) return; for (let position of Object.keys(this._customRowElemCache)) { - this.insertCustomRow(this._customRowElemCache[position], position); + this._customRowElemCache[position].forEach((rowElem) => { + this.insertCustomRow(rowElem, position); + }); this._customRowElemCache[position] = []; } } insertCustomRow(rowElem, position = "end") { - // Handle both single elements and arrays - let elements = Array.isArray(rowElem) ? rowElem : [rowElem]; - - // Determine insertion order based on position - // Only reverse when there are multiple elements - if (position !== "end" && elements.length > 1) { - // For 'start' and 'afterCreators', reverse to maintain order - elements = elements.reverse(); - } - - for (let elem of elements) { - switch (position) { - case "start": { - this._infoTable.prepend(elem); - break; + switch (position) { + case "start": { + let itemTypeRow = this._infoTable.querySelector(".meta-label[fieldname=itemType]")?.parentElement; + if (itemTypeRow) { + this._infoTable.insertBefore(rowElem, itemTypeRow); } - case "afterCreators": { - // The `_firstRowBeforeCreators` is actually the first row after creator rows - if (this._firstRowBeforeCreators) { - this._infoTable.insertBefore(elem, this._firstRowBeforeCreators); - // Update the anchor node for creator rows - this._firstRowBeforeCreators = elem; - } - else { - this._infoTable.append(elem); - } - break; + else { + this._infoTable.append(rowElem); } - case "end": - default: { - let dateAddedRow = this._infoTable.querySelector(".meta-label[fieldname=dateAdded]")?.parentElement; - if (dateAddedRow) { - this._infoTable.insertBefore(elem, dateAddedRow); - } - else { - this._infoTable.append(elem); - } - break; + break; + } + case "afterCreators": { + // The `_firstRowBeforeCreators` is actually the first row after creator rows + if (this._firstRowBeforeCreators) { + this._infoTable.insertBefore(rowElem, this._firstRowBeforeCreators); } + else { + this._infoTable.append(rowElem); + } + break; + } + case "end": + default: { + let dateAddedRow = this._infoTable.querySelector(".meta-label[fieldname=dateAdded]")?.parentElement; + if (dateAddedRow) { + this._infoTable.insertBefore(rowElem, dateAddedRow); + } + else { + this._infoTable.append(rowElem); + } + break; } } } From e0b12a04a711bc08597478d73c9fd61ee68c9a84 Mon Sep 17 00:00:00 2001 From: Ramon Mi Date: Thu, 7 May 2026 13:46:41 +0800 Subject: [PATCH 3/3] modify comments for sorting rule --- chrome/content/zotero/xpcom/pluginAPI/itemPaneManager.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chrome/content/zotero/xpcom/pluginAPI/itemPaneManager.js b/chrome/content/zotero/xpcom/pluginAPI/itemPaneManager.js index 6a164466f2..df26e6169e 100644 --- a/chrome/content/zotero/xpcom/pluginAPI/itemPaneManager.js +++ b/chrome/content/zotero/xpcom/pluginAPI/itemPaneManager.js @@ -440,9 +440,8 @@ get data() { let options = this.options; - // Ensure deterministic ordering for rows with the same `position`. - // Sort `rowID` so insertion (prepend/insertBefore) produces a - // stable visual order regardless of registration timing. + // Since `rowID` is mapped to a namespaced key including `pluginID`, + // sorting by it produces a stable visual order grouped by plugin, if (Array.isArray(options) && options.length > 1) { options = options.sort((a, b) => String(a.rowID ?? "").localeCompare(String(b.rowID ?? ""))