From ca4ced1e9f84edb4263b53722527c640a6bdd775 Mon Sep 17 00:00:00 2001
From: Dan Stillman <dstillman@zotero.org>
Date: Thu, 25 Aug 2022 05:15:25 -0400
Subject: [PATCH] Add Zotero.Item::topLevelItem and
 Zotero.Items.getTopLevel(items)

---
 chrome/content/zotero/xpcom/data/item.js  | 11 ++++++++++-
 chrome/content/zotero/xpcom/data/items.js | 11 +++++++++++
 test/tests/itemTest.js                    | 20 ++++++++++++++++++++
 3 files changed, 41 insertions(+), 1 deletion(-)

diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
index 8327625210..5eb9467335 100644
--- a/chrome/content/zotero/xpcom/data/item.js
+++ b/chrome/content/zotero/xpcom/data/item.js
@@ -153,7 +153,16 @@ Zotero.defineProperty(Zotero.Item.prototype, 'parentItemKey', {
 Zotero.defineProperty(Zotero.Item.prototype, 'parentItem', {
 	get: function() { return Zotero.Items.get(this.parentID) || undefined; },
 });
-
+Zotero.defineProperty(Zotero.Item.prototype, 'topLevelItem', {
+	get: function () {
+		var item = this; // eslint-disable-line consistent-this
+		var parentItem;
+		while ((parentItem = item.parentItem)) {
+			item = parentItem;
+		}
+		return item;
+	}
+});
 
 Zotero.defineProperty(Zotero.Item.prototype, 'firstCreator', {
 	get: function() { return this._firstCreator; }
diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js
index bf10c7b7f9..9344de6ef9 100644
--- a/chrome/content/zotero/xpcom/data/items.js
+++ b/chrome/content/zotero/xpcom/data/items.js
@@ -1735,6 +1735,17 @@ Zotero.Items = function() {
 	};
 	
 	
+	/**
+	 * Get the top-level items of all passed items
+	 *
+	 * @param {Zotero.Item[]} items
+	 * @return {Zotero.Item[]}
+	 */
+	this.getTopLevel = function (items) {
+		return [...new Set(items.map(item => item.topLevelItem))];
+	};
+	
+	
 	/**
 	 * Returns an array of items with children of selected parents removed
 	 *
diff --git a/test/tests/itemTest.js b/test/tests/itemTest.js
index b2f594e757..de0edd3d2b 100644
--- a/test/tests/itemTest.js
+++ b/test/tests/itemTest.js
@@ -483,6 +483,26 @@ describe("Zotero.Item", function () {
 		});
 	});
 	
+	describe("#topLevelItem", function () {
+		it("should return self for top-level item", async function () {
+			var item = await createDataObject('item');
+			assert.equal(item, item.topLevelItem);
+		});
+		
+		it("should return parent item for note", async function () {
+			var item = await createDataObject('item');
+			var note = await createDataObject('item', { itemType: 'note', parentItemID: item.id });
+			assert.equal(item, note.topLevelItem);
+		});
+		
+		it("should return top-level item for annotation", async function () {
+			var item = await createDataObject('item');
+			var attachment = await importPDFAttachment(item);
+			var annotation = await createAnnotation('highlight', attachment);
+			assert.equal(item, annotation.topLevelItem);
+		});
+	});
+	
 	describe("#getCreators()", function () {
 		it("should update after creators are removed", function* () {
 			var item = createUnsavedDataObject('item');