New official API for custom columns in the item tree

633 views
Skip to first unread message

XY Wong

unread,
Jul 25, 2023, 10:51:43 AM7/25/23
to zotero-dev
Hi everyone!

I am glad to announce that the new official API for custom columns in the item tree is ready.

Zotero 7 beta 23 adds an API for creating custom columns in the item tree. The item tree is widely used in Zotero for displaying a table of items (e.g., the items list in the main library view and the search results in the Advanced Search window).

If you were previously using monkey-patching to add custom columns in Zotero 6, please switch to using the official API in Zotero 7.


I started working on the plugin ecosystem as one of the Zotero team since May 2023. This API is our first step to make developing plugins more easy. Please let us know if you encounter any problems with this API. We will continuously bring more APIs in the future.

Best,
Xiangyu

Emiliano Heyns

unread,
Jul 26, 2023, 2:53:55 PM7/26/23
to zotero-dev
That's a great mechanism - could we have such automatic removal for document elements, notifier listeners and idle listeners also? 

XY Wong

unread,
Jul 26, 2023, 10:51:54 PM7/26/23
to zotero-dev
@Emiliano Heyns

Thanks. I'm afraid we won't support automatic removal for elements. You may need to implement yourself, or use third-party lib (e.g. plugin-toolkit, but do not rely on them too much).

Other listeners do not support auto-removal yet, and even if we want them to, it might not be now. I have many other new APIs to work on before enhancing the existing ones.

Emiliano Heyns

unread,
Jul 27, 2023, 5:11:53 AM7/27/23
to zotero-dev
That's OK, I have my own mechanisms, but this autoremoval made one of them superfluous, might as well see if others could go too :) 

iseexuhs

unread,
Aug 2, 2023, 10:13:29 PM8/2/23
to zotero-dev
Hello, I tried this new API.

I can succeed to add a new custom column by using the API, but it seems the added column fails to be removed by using the command "await Zotero.ItemTreeManager.unregisterColumns(registeredDataKey);", and it always returns false, but not true as the Zotero source code tells.

Please help check it. Thanks a lot.

XY Wong

unread,
Aug 2, 2023, 10:16:14 PM8/2/23
to zotero-dev
We need the source code that can reproduce your problem.

iseexuhs

unread,
Aug 2, 2023, 10:19:55 PM8/2/23
to zotero-dev

  const registeredDataKey = await Zotero.ItemTreeManager.registerColumns({
      dataKey: 'translateTitle',
      label: '标题翻译',
      pluginID: 'make-...@zotero.org',
      dataProvider: (item, dataKey) => {
          return item.getField('shortTitle');
      },
  });
 
  await Zotero.ItemTreeManager.unregisterColumns('translateTitle');

XY Wong

unread,
Aug 2, 2023, 10:23:33 PM8/2/23
to zotero-dev
Nop. This is not how the API should be called: Use await Zotero.ItemTreeManager.unregisterColumns(registeredDataKey);
as you mentioned.
在2023年8月3日星期四 UTC+8 10:13:29<iseexuhs> 写道:

XY Wong

unread,
Aug 2, 2023, 10:25:51 PM8/2/23
to zotero-dev
And, you do not need to call it manually to unregister the columns, if you have pluginID set correctly.
Zotero will remove all the plugin's columns when the plugin is disabled.

iseexuhs

unread,
Aug 2, 2023, 10:31:20 PM8/2/23
to zotero-dev
I checked the source code, If run above codes,  registeredDataKey in the "const registeredDataKey.. " should get true or false.

So, 'await Zotero.ItemTreeManager.unregisterColumns(registeredDataKey) ' is not reasonable. 

Also, the source code also tells to use ''await Zotero.ItemTreeManager.unregisterColumns('translateTitle');"

It is very confusing.

在2023年8月3日星期四 UTC+8 10:23:33<XY Wong> 写道:

iseexuhs

unread,
Aug 2, 2023, 10:35:34 PM8/2/23
to zotero-dev
My purpose it to unregister the added column instantly, I mean without restart.

For now, I can't not seen anything happen by running the ''unregisterColumns" API. The column can not be removed instantly.

XY Wong

unread,
Aug 2, 2023, 10:45:15 PM8/2/23
to zotero-dev

1. It's reasonable. We will prefix the dataKey, which is `translateTitle` in your case, to avoid conflicts between plugins. The actually registered `dataKey` is something like `make-it-red-zotero-org-translateTitle` in your case, which is the one you get from the return of `registerColumns`.

2. registerColumns returns a string, or an array of strings, on success and returns false on fail.

3. It is a common behavior to unregister something with the return of the register call, e.g. addEventListener.

XY Wong

unread,
Aug 2, 2023, 10:50:46 PM8/2/23
to zotero-dev
*addEventListener -> Zotero.Notifier.registerObserver

iseexuhs

unread,
Aug 2, 2023, 11:00:45 PM8/2/23
to zotero-dev
Ok, it succeeds. As you say, the  code annotation 'Zotero.ItemTreeManager.unregisterColumns('rtitle');' in the Zotero source code should be fixed to avoid confusion.

Thanks a lot.
在2023年8月3日星期四 UTC+8 10:45:15<XY Wong> 写道:

XY Wong

unread,
Sep 5, 2023, 10:24:18 PM9/5/23
to zotero-dev
We now offer support for custom cell rendering in the latest Zotero 7 beta, with an example available in the source code accessible at https://github.com/zotero/zotero/blob/e1def9971c18999b0e0bb127fa3f2cb1ccd59d5f/chrome/content/zotero/xpcom/itemTreeManager.js#L91-L101.

Emiliano Heyns

unread,
Sep 6, 2023, 1:56:20 AM9/6/23
to zotero-dev
But this is optional, right? If I just provide dataProvider and not renderCell it still gets displayed?

XY Wong

unread,
Sep 6, 2023, 2:03:25 AM9/6/23
to zotero-dev
Yes of course. Displaying a citation key or some plain-text content does not need that.

Emiliano Heyns

unread,
Sep 8, 2023, 11:10:19 AM9/8/23
to zotero-dev
Will this API also work for the item viewer in the PDF display tab?
Message has been deleted

XY Wong

unread,
Sep 8, 2023, 9:45:09 PM9/8/23
to zotero-dev
Theoretically, it works for every item tree. If you find it not working with any of them, let me know and I'll do a fix.

Emiliano Heyns

unread,
Aug 10, 2024, 9:11:02 AM8/10/24
to zotero-dev
Must dataProvider return a string, or can it be an object? I want to add the item id to the cell in rendercell so that I can refresh the cell when the data in it changes, but for that I'd want to return something like { itemID: item.id, data: item.getField('citationKey') } so that I can use both in constructing the cell in renderCell.

On Wednesday, September 6, 2023 at 4:24:18 AM UTC+2 XY Wong wrote:

XY Wong

unread,
Aug 10, 2024, 11:58:10 AM8/10/24
to zotero-dev
The dataProvidor is mainly for the default cell rendering function, which requires a string. Using a custom renderCell, you could get the data from your source instead of the dataProvider.

Emiliano Heyns

unread,
Aug 10, 2024, 12:03:21 PM8/10/24
to zotero-dev
Is the item available in renderCell? I looked in the source and I only saw a data argument, and the sample code seemed to assume that was a string 

--
You received this message because you are subscribed to a topic in the Google Groups "zotero-dev" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/zotero-dev/4jqa8QIk6DM/unsubscribe.
To unsubscribe from this group and all its topics, send an email to zotero-dev+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/zotero-dev/bacd6014-8e6c-4b61-847d-e3b76890f938n%40googlegroups.com.

XY Wong

unread,
Aug 10, 2024, 12:21:25 PM8/10/24
to zotero-dev
No, the dataProvider is also used by sorting, which uses Zotero.getLocaleCollation().compareString, which requires a string. The behavior of comparing non-string values is unpredictable.

A possible workaround is to return something like `${stringForSort}-${itemID}` and split to get the itemID in the renderCell.

Emiliano Heyns

unread,
Oct 24, 2024, 4:53:27 PM10/24/24
to zotero-dev
Where does `document` come from in this sample?

On Wednesday, September 6, 2023 at 4:24:18 AM UTC+2 XY Wong wrote:

XY Wong

unread,
Oct 30, 2024, 7:02:59 AM10/30/24
to zotero-dev
In 7.0.9, the renderCell is passed a new argument “document”:

renderCell(index, data, column, isFirstColumn, document)

Thanks for the feedback.

Emiliano Heyns

unread,
Nov 5, 2024, 6:43:35 AM11/5/24
to zotero-dev
Can I use CSS in the rendered cells?

Emiliano Heyns

unread,
Nov 5, 2024, 7:53:19 AM11/5/24
to zotero-dev
I'm getting

document.createElement is not a function

On Wednesday, October 30, 2024 at 12:02:59 PM UTC+1 XY Wong wrote:

Emiliano Heyns

unread,
Nov 5, 2024, 8:06:37 AM11/5/24
to zotero-dev
I had the argument order wrong. I've copied in the sample code:

renderCell: (index, data, column, isFirstColumn, doc) => {
        const cell = doc.createElement('span');
        cell.className = `cell ${column.className}`;
        cell.textContent = data;
        cell.style.color = 'red';
        return cell;
      },

and I'm getting

TypeError: Node.appendChild: Argument 1 is not an object.
    _renderItem chrome://zotero/content/itemTree.js:2989
    _renderItem chrome://zotero/content/components/virtualized-table.js:1148
    rerenderItem chrome://zotero/content/components/windowed-list.js:104
    invalidate chrome://zotero/content/components/windowed-list.js:123
    invalidate chrome://zotero/content/components/virtualized-table.js:1310
    refreshAndMaintainSelection chrome://zotero/content/itemTree.js:1113

XY Wong

unread,
Nov 5, 2024, 8:08:03 AM11/5/24
to zoter...@googlegroups.com
Thanks for the report, I will check that

--
You received this message because you are subscribed to a topic in the Google Groups "zotero-dev" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/zotero-dev/4jqa8QIk6DM/unsubscribe.
To unsubscribe from this group and all its topics, send an email to zotero-dev+...@googlegroups.com.

XY Wong

unread,
Nov 5, 2024, 9:57:25 AM11/5/24
to zotero-dev
It would be helpful if you would have the full call to register the custom column. Where are you calling this registration from, i.e. from the bootstrap.js or its sub-scripts (the plugin sandbox), or somewhere else?

Emiliano Heyns

unread,
Nov 5, 2024, 10:39:59 AM11/5/24
to zotero-dev
    await Zotero.ItemTreeManager.registerColumns({
      dataKey: 'pubpeer',
      label: 'PubPeer',
      pluginID: 'pub...@pubpeer.com',
      dataProvider: (item, _dataKey) => {
        const feedback = this.feedbackFor(item)
        // https://groups.google.com/g/zotero-dev/c/4jqa8QIk6DM/m/s86FPjYzAgAJ
        return `${feedback.total_comments || ''}\t${item.id}`
      },

      renderCell: (index, data, column, isFirstColumn, doc) => {
        const cell = doc.createElement('span');
        cell.className = `cell ${column.className}`;
        cell.textContent = data;
        cell.style.color = 'red';
        return cell;
      },
      /*
      renderCell: (index, data, column, isFirstColumn, doc, xdata) => {

        const cell = doc.createElement('span')
        cell.className = `pubpeer cell ${column.className}`;
        let icon
        if (data) {
          if (PubPeer.ready.isPending()) {
            icon = states.icon.loading
          }
          else {
            const [ total, itemID ] = data.split('\t')
            cell.textContent = total

            const item = Zotero.Items.get(parseInt(itemID))
            const feedback = this.feedbackFor(item)
            const state = feedback.users.map(user => Zotero.PubPeer.users[user])
            if (state.includes('priority')) {
              icon = states.icon.highlighted
            }
            else if (state.includes('neutral')) {
              icon = states.icon.neutral
            }
            else {
              icon = states.icon.muted
            }
          }
        }
        if (icon) {
          cell.style.paddingLeft = '20px'
          cell.style.backgroundImage = `url(${rootURI}content/skin/${icon})`
          cell.style.backgroundSize = '10px 10px'
          cell.style.backgroundRepeat = 'no-repeat'
          cell.style.backgroundPosition = 'left center'
        }
        return cell
      },
      */
    })

this is being called from a script loaded by

Services.scriptloader.loadSubScript(rootURI + 'pubpeer.js', { rootURI, Zotero })

from bootstrap.js

XY Wong

unread,
Nov 5, 2024, 12:11:37 PM11/5/24
to zotero-dev
I cannot reproduce. Is it possible to share a link to the xpi file?

Emiliano Heyns

unread,
Nov 5, 2024, 4:55:47 PM11/5/24
to zotero-dev
Much appreciated! Fortunately this plugin is fairly simple

https://0x0.st/XD-j.box.xpi

XY Wong

unread,
Nov 6, 2024, 5:14:00 AM11/6/24
to zotero-dev
The `renderCell` is not correct in this xpi:


diff --git a/pubpeer.js b/pubpeer.js
index dcadb08..2307d83 100644
--- a/pubpeer.js
+++ b/pubpeer.js
@@ -2006,7 +2006,7 @@ ${text}`);
           const feedback = this.feedbackFor(item);
           return `${feedback.total_comments || ""}     ${item.id}`;
         },
-        renderCell: (_index, document, data, column) => {
+        renderCell: (_index, data, column, isFirstColumn, document) => {
           const cell = document.createElement("span");

           cell.className = `pubpeer cell ${column.className}`;
           let icon;

Emiliano Heyns

unread,
Nov 6, 2024, 5:37:35 AM11/6/24
to zotero-dev
Sorry about that -- don't know how I uploaded the old version, the right version is at https://0x0.st/XDKZ.xpi

XY Wong

unread,
Nov 6, 2024, 5:45:46 AM11/6/24
to zotero-dev
This XPI works for me without any error, and it's the same if I replace the renderCell with the one from example. I still cannot reproduce.

Are you sure you are on the latest beta of Zotero?

Emiliano Heyns

unread,
Nov 6, 2024, 6:12:57 AM11/6/24
to zotero-dev
🤦

I am not on the beta, but I also hadn't updated to 7.0.9

Thanks so much,
Emiliano

XY Wong

unread,
Nov 15, 2024, 7:40:04 AM11/15/24
to zotero-dev

Important Update in 7.0.10-beta.2:

The following changes have been made to the API:

  • Renamed Functions:

    • Zotero.ItemTreeManager.registerColumns → Zotero.ItemTreeManager.registerColumn
    • Zotero.ItemTreeManager.unregisterColumns → Zotero.ItemTreeManager.unregisterColumn
  • Key Change:
    The updated functions no longer accept an array of options. Instead, they now require a single option object as an argument.

Example:

<code language="javascript">
// Old API (accepts an array of options, or a single option object)
Zotero.ItemTreeManager.registerColumns([opt1, opt2]);
Zotero.ItemTreeManager.registerColumns(opt);
// New API (only accepts a single option object)
Zotero.ItemTreeManager.registerColumn(opt);
</code>

The same adjustment applies to the unregisterColumn function.

Note:
The old functions are deprecated but remain available for compatibility. While your existing code will continue to work for now, we strongly recommend updating your plugin to use the new functions.

Reply all
Reply to author
Forward
0 new messages