menumanager: context menu for library/group

116 views
Skip to first unread message

Emiliano Heyns

unread,
Feb 26, 2026, 8:10:52 AMFeb 26
to zotero-dev
Does the menu manager have a target for a library/group? I see targets for collections and searches but not for the library/group.

XY Wong

unread,
Mar 2, 2026, 5:38:55 AMMar 2
to zotero-dev
You may want to use the `collectionTreeRow` from `getContext` [1] to decide if it's a library or group.


[1] https://github.com/zotero/zotero/blob/59056389077b3fc153d37263c422333bb3d04304/chrome/content/zotero/zoteroPane.js#L4208-L4214

Emiliano Heyns

unread,
Mar 2, 2026, 7:23:42 AMMar 2
to zotero-dev
How would I distinguish between an item being right-clicked and a library being right-clicked? In both cases collectionTreeRow would be populated right?

XY Wong

unread,
Mar 2, 2026, 7:50:43 AMMar 2
to zotero-dev
The item context menu and collection context menu are different targets ("main/library/item" and "main/library/collection").

Emiliano Heyns

unread,
Mar 2, 2026, 8:47:02 AMMar 2
to zotero-dev
Ah, and in the case of the library or group being selected, no collection is selected.

XY Wong

unread,
Mar 2, 2026, 8:59:38 AMMar 2
to zotero-dev
Please use `collectionTreeRow.type`.

Actually, you could register a menu with logging to console in its onShowing and inspect the properties of the context object passed to the callback.

Emiliano Heyns

unread,
Mar 2, 2026, 4:01:44 PMMar 2
to zotero-dev
Will do.

Is it possible to have a variable number of entries under a submenu?

Xiangyu Wang

unread,
Mar 2, 2026, 4:04:38 PMMar 2
to zoter...@googlegroups.com
The (sub)menus need to be registered first and then dynamically set to hidden/visible in the onShowing.

--
You received this message because you are subscribed to the Google Groups "zotero-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to zotero-dev+...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/zotero-dev/e658678d-7508-4002-a02e-0e859a2b1294n%40googlegroups.com.

Emiliano Heyns

unread,
Mar 2, 2026, 4:20:31 PMMar 2
to zotero-dev
Alright, so I need to guess a sensible number. Not a major problem.

Emiliano Heyns

unread,
Mar 7, 2026, 10:08:59 AMMar 7
to zotero-dev
I thought I would use this to put up to 10 items in the submenu, but I can see the submenu onShowing is being called, but the submenu ones aren't. Have I overlooked something?

function selectedAutoExports(mode) {
  const selected = mode === 'collection'
    ? Zotero.getActiveZoteroPane().getSelectedCollection(true)
    : Zotero.getActiveZoteroPane().getSelectedLibraryID()
    return AutoExport.db.all(_ => _.type === mode && _.id === selected)
}
Zotero.MenuManager.registerMenu({
  menuID: `${pluginID}-menu-collection`,
  pluginID,
  target: 'main/library/collection',
  menus: [
    {
      menuType: 'submenu',
      l10nID: 'better-bibtex',
      icon: 'chrome://zotero-better-bibtex/content/skin/bibtex-menu.svg',
      menus: [
        {
          menuType: 'submenu',
          l10nID: 'better-bibtex_preferences_auto-export',
          onShowing: (_event, context) => {
            const type = context.collectionTreeRow.type
            context.setVisible(selectedAutoExports(type).length !== 0)
            Zotero.debug(`submenu ${type}: ${selectedAutoExports(type).length}`)
          },
          menus: Array.from({ length: 10 }).map((_, i) => ({
            menuType: 'menuitem',
            onShowing: (event, context) => {
              const type = context.collectionTreeRow.type
              const aes = selectedAutoExports(type)
              Zotero.debug(`menuitem ${i} ${type}: ${typeof aes[i] !== 'undefined'}`)
              context.setVisible(typeof aes[i] !== 'undefined')
              context.menuElem.setAttribute('label', aes[i]?.path || '[path not set]')
            },
            onCommand: (_event, _context) => {
              const ae = selectedAutoExports('collection')[i]
              if (ae) Zotero.BetterBibTeX.AutoExport.run(ae.path)
            },
          })),
        ],
      },
    ],
  }
)

XY Wong

unread,
Mar 7, 2026, 10:25:32 AMMar 7
to zotero-dev
The submenus' onShowing event is only get triggered when its popup is showing, not when the top level popup is showing.

The following code works correctly for me:

```js
Zotero.MenuManager.registerMenu({
    menuID: "1",
    pluginID: "yo...@plugin.id",

    target: "main/library/collection",
    menus: [
      {
        menuType: "submenu",
        l10nID: "menu-print",
        menus: [
          {
            menuType: "submenu",
            onShowing: (_event, context) => {
              Zotero.warn(new Error(`Showing submenu parent`));
            },
            l10nID: "menu-print",

            menus: Array.from({ length: 10 }).map((_, i) => ({
              menuType: "menuitem",
              onShowing: (event, context) => {
                Zotero.warn(new Error(`Showing menu item ${i}`));

              },
            })),
          },
        ],
      },
    ],
  });
```

Btw, it would be nice if you would provide the minimal code for reproduce that can run fully on its own, instead of ones with function calls/vars that are not available in the code snippets.

Emiliano Heyns

unread,
Mar 7, 2026, 11:06:18 AMMar 7
to zotero-dev
Totally fair -- it should have been

        const aes = [ { path: 'path 1' }, { path: 'path 2' } ]

        Zotero.MenuManager.registerMenu({
          menuID: '1',
          pluginID: 'yo...@plugin.id',
          target: 'main/library/collection',
          menus: [{
            menuType: 'submenu',
            onShowing: (_event, context) => {
              context.menuElem.setAttribute('label', 'level 1')
              Zotero.warn(new Error('Showing submenu level 1'))

            },
            menus: [{
              menuType: 'submenu',
              onShowing: (_event, context) => {
                context.menuElem.setAttribute('label', 'level 2')
                context.setVisible(aes.length > 0)
                Zotero.warn(new Error('Showing submenu level 2'))

              },
              menus: Array.from({ length: 10 }).map((_, i) => ({
                menuType: 'menuitem',
                onShowing: (_event, context) => {
                  // @ts-expect-error TS2339
                  context.menuElem.setAttribute('label', aes[i]?.path || `${i}`)
                  // @ts-expect-error TS2339
                  context.setVisible(aes.length > i)

                  Zotero.warn(new Error(`Showing menu item ${i}`))
                },
              })),
            }],
          }],
        })

and that does work, so I have some debugging to do.

Emiliano Heyns

unread,
Mar 7, 2026, 11:35:34 AMMar 7
to zotero-dev
I am really sorry, but I am still not seeing it in the actual code. I think I have stripped this down to have no dependencies outside the snippet, to me this looks equivalent to the standalone snippet I posted above this:

        function selectedAutoExports(_mode) {
          return [ { path: '/x/y' }, { path: '/foo/bar' } ]

        }
        Zotero.MenuManager.registerMenu({
          menuID: `${pluginID}-menu-collection`,
          pluginID,
          target: 'main/library/collection',
          menus: [
            {
              menuType: 'submenu',
              l10nID: 'better-bibtex',
              icon: 'chrome://zotero-better-bibtex/content/skin/bibtex-menu.svg',
              menus: [
                {
                  menuType: 'submenu',
                  l10nID: 'better-bibtex_preferences_auto-export',
                  onShowing: (_event, context) => {
                    Zotero.warn(new Error(`submenu showing: ${selectedAutoExports('any').length !== 0}`))

                    const type = context.collectionTreeRow.type
                    context.setVisible(selectedAutoExports(type).length !== 0)
                  },
                  menus: Array.from({ length: 10 }).map((_, i) => ({
                    menuType: 'menuitem',
                    onShowing: (event: Event, context) => {
                      Zotero.warn(new Error(`menuitem ${i} showing?`))

                      const type = context.collectionTreeRow.type
                      const aes = selectedAutoExports(type)
                      context.setVisible(aes.length > i)
                      Zotero.warn(new Error(`menuitem ${i} showing: ${aes.length > i}`))

                      context.menuElem.setAttribute('label', aes[i]?.path || '[path not set]')
                    },
                    /*onCommand: (_event: Event, _context) => {

                      const ae = selectedAutoExports('collection')[i]
                      if (ae) Zotero.BetterBibTeX.AutoExport.run(ae.path)
                    }*/,
                  }))/* as MenuItem[] */,
                },
                // { menuType: 'menuitem', l10nID: 'better-bibtex_zotero-pane_show_collection-key', onCommand: (_event, _context) => showPullExportURLs('collection') },
                // { menuType: 'menuitem', l10nID: 'better-bibtex_aux-scanner', onCommand: (_event, _context) => void Zotero.BetterBibTeX.scanAUX('collection') },
                // { menuType: 'menuitem', l10nID: 'better-bibtex_report-errors', onCommand: (_event, _context) => void Zotero.BetterBibTeX.ErrorReport.open('collection') },
              ],
            },
          ],
        })

and I don't see 

menuitem ${i} showing?

where I would have expected it to. I do see 

`submenu showing: true`

Emiliano Heyns

unread,
Mar 8, 2026, 6:43:03 AMMar 8
to zotero-dev
I saw a mention on the forums that maybe the missling l10nid plays a role. How do I set l10nargs in the onShowing? Using  context.menuElem.setAttribute?

XY Wong

unread,
Mar 8, 2026, 6:45:26 AMMar 8
to zotero-dev
Use [1] `context.setL10nArgs`. It is always recommended to use l10n instead of directly setting attributes like lable.

Message has been deleted
Message has been deleted

Emiliano Heyns

unread,
Mar 8, 2026, 7:42:53 AMMar 8
to zotero-dev
I now have this

function selectedAutoExports(type) {
          const selected = type === 'collection'
            ? Zotero.getActiveZoteroPane().getSelectedCollection(true)
            : Zotero.getActiveZoteroPane().getSelectedLibraryID()
          // return AutoExport.db.all(_ => _.type === type && _.id === selected)
          return Array.from({ length: 3 }).map((_, i) => ({ path: `/path/${i}`, type, id: selected }))

        }
        Zotero.MenuManager.registerMenu({
          menuID: '1',
          pluginID: 'yo...@plugin.id',
          target: 'main/library/collection',
          menus: [{
            menuType: 'submenu',
            l10nID: 'better-bibtex',
            onShowing: (_event, _context) => {
              Zotero.debug('Showing submenu level 1')

            },
            menus: [{
              menuType: 'submenu',
              l10nID: 'better-bibtex_preferences_auto-export',
              onShowing: (_event, context) => {
                const type = context.collectionTreeRow.type
                const aes = selectedAutoExports(type)
                context.setVisible(aes.length > 0)
                Zotero.debug('Showing submenu level 2')

              },
              menus: Array.from({ length: 10 }).map((_, i) => ({
                menuType: 'menuitem',
                l10nID: 'better-bibtex_auto-export_path',
                onShowing: (_event, context) => {
                  Zotero.debug(`Showing menu item ${i}`)

                  const type = context.collectionTreeRow.type
                  const aes = selectedAutoExports(type)
                  // @ts-expect-error TS2339
                  context.setL10nArgs({ path: aes[i]?.path || '' })

                  // @ts-expect-error TS2339
                  context.setVisible(aes.length > i)
                },
              })),
            }],
          }],
        })

with

better-bibtex =
  .label = Better BibTeX
better-bibtex_preferences_auto-export = Automatic export
  .label = { better-bibtex_preferences_auto-export }
better-bibtex_auto-export_path = { $path }
  .label = { better-bibtex_auto-export_path }

I think this is fully self-contained? But the menu items are not showing, and I don't see `Showing menu item ${i}` in my logs

XY Wong

unread,
Mar 8, 2026, 7:56:56 AMMar 8
to zotero-dev
```

better-bibtex_preferences_auto-export = Automatic export
  .label = { better-bibtex_preferences_auto-export }
better-bibtex_auto-export_path = { $path }
  .label = { better-bibtex_auto-export_path }
```

Same as the situation we discussed in another thread, that your l10n string is replacing the whole innerHTML of the popup and thus there is no sub menus at all.

Remove the `Automatic export` and `{ $path }`.

Northword

unread,
Mar 8, 2026, 8:19:16 AMMar 8
to zotero-dev
```ts
type ItemMenu = _ZoteroTypes.MenuManager.MenuData<_ZoteroTypes.MenuManager.LibraryMenuContext>;

      Zotero.MenuManager.registerMenu({
          menuID: '00000000000002',
          pluginID: 'y...@plugin.id',
          target: 'main/library/collection',
          menus: [{
            menuType: 'submenu',
            l10nID: 'better-bibtex',
            onShowing: (_event, _context) => {
              Zotero.debug('Showing submenu level 1')
            },
            menus: [{
              menuType: 'submenu',
              l10nID: 'better-bibtex_preferences_auto-export',
              onShowing: (_event, context) => {
                Zotero.debug('Showing submenu level 2')

              },
              menus: Array.from({ length: 10 }).map((_, i): ItemMenu => ({
                menuType: 'menuitem',
                l10nID: 'better-bibtex_auto-export_path',
                l10nArgs: { path: i },
                onShowing: (_event, context) => {
                  Zotero.debug(`Showing menu item ${i}`)
                },
              })),
            }],
          }],
        })
```

```ftl
better-bibtex =
  .label = Better BibTeX
better-bibtex_preferences_auto-export =
  .label = Automatic export
better-bibtex_auto-export_path =
  .label = { $path }
```


PixPin_2026-03-08_20-15-04.png

Emiliano Heyns

unread,
Mar 8, 2026, 9:01:42 AMMar 8
to zotero-dev
Oh. I am using that construct to prevent duplicate strings piling up in the FTL. So I should probably never have both the toplevel string and attributes in one entry? I thought that it would just prioritize the .label if it was present.

XY Wong

unread,
Mar 8, 2026, 11:17:35 AM (14 days ago) Mar 8
to zotero-dev
You can use both, just keep in mind what they actually do. There are cases like setting the innerHTML of a div element with a `title` attribute that would need both.

And better not to reuse one key for multiple elements unless they are meant to be exactly the same. To prevent duplication, you can use FTL variables for the same string; but not the same key for multiple elements.

Emiliano Heyns

unread,
Mar 9, 2026, 4:29:15 AM (13 days ago) Mar 9
to zotero-dev
They are meant to be exactly the same

I am closer now; with this code

function selectedAutoExports(type: 'collection' | 'library') {

          const selected = type === 'collection'
            ? Zotero.getActiveZoteroPane().getSelectedCollection(true)
            : Zotero.getActiveZoteroPane().getSelectedLibraryID()
          // return AutoExport.db.all(_ => _.type === type && _.id === selected)
          return Array.from({ length: 3 }).map((_, i) => ({ path: `/path/${i}`, type, id: selected }))
        }
        Zotero.MenuManager.registerMenu({
          menuID: `${pluginID}-menu-collection`,
          pluginID,
          target: 'main/library/collection',
          menus: [
            {
              menuType: 'submenu',
              l10nID: 'better-bibtex',
              icon: 'chrome://zotero-better-bibtex/content/skin/bibtex-menu.svg',
              menus: [
                {
                  menuType: 'submenu',
                  l10nID: 'better-bibtex_collection-menu_auto-export',

                  onShowing: (_event, context) => {
                    const type = context.collectionTreeRow.type
                    const aes = selectedAutoExports(type)
                    context.setVisible(aes.length > 0)
                  },
                  menus: Array.from({ length: 10 }).map((_, i) => ({
                    menuType: 'menuitem',
                    l10nID: 'better-bibtex_collection-menu_auto-export_path',
                    onShowing: (event: Event, context: any) => {

                      const type = context.collectionTreeRow.type
                      const aes = selectedAutoExports(type)
                      context.setVisible(aes.length > i)
                      Zotero.debug(`menuitem ${i} = ${JSON.stringify(aes[i] || {})}`)
                      context.setL10nArgs(aes[i] || {})
                    },
                    onCommand: (_event: Event, context) => {
                      const type = context.collectionTreeRow.type
                      const ae = selectedAutoExports(type)[i]
                      if (ae) Zotero.BetterBibTeX.AutoExport.run(ae.path)
                    },
                  })) as MenuItem[],

                },
                // { menuType: 'menuitem', l10nID: 'better-bibtex_zotero-pane_show_collection-key', onCommand: (_event, _context) => showPullExportURLs('collection') },
                // { menuType: 'menuitem', l10nID: 'better-bibtex_aux-scanner', onCommand: (_event, _context) => void Zotero.BetterBibTeX.scanAUX('collection') },
                // { menuType: 'menuitem', l10nID: 'better-bibtex_report-errors', onCommand: (_event, _context) => void Zotero.BetterBibTeX.ErrorReport.open('collection') },
              ],
            },
          ],
        })

and this ftl


better-bibtex =
  .label = Better BibTeX
better-bibtex_collection-menu_auto-export =
  .label = Automatic export
better-bibtex_collection-menu_auto-export_path =
  .label = { $path }

the menu options do all show, but the menu items all show "{ $path }"

with the log showing

zotero(3)(+0000000): menuitem 0 = {"path":"/path/0","type":"library","id":1}

zotero(3)(+0000000): menuitem 1 = {"path":"/path/1","type":"library","id":1}

zotero(3)(+0000001): menuitem 2 = {"path":"/path/2","type":"library","id":1}

zotero(3)(+0000000): menuitem 3 = {}

zotero(3)(+0000000): menuitem 4 = {}

zotero(3)(+0000001): menuitem 5 = {}

zotero(3)(+0000000): menuitem 6 = {}

zotero(3)(+0000000): menuitem 7 = {}

zotero(3)(+0000000): menuitem 8 = {}

zotero(3)(+0000001): menuitem 9 = {}

Message has been deleted

Emiliano Heyns

unread,
Mar 9, 2026, 11:26:56 AM (13 days ago) Mar 9
to zotero-dev
On the other hand, setting the 'label' attribute works, so I could just do that instead

Emiliano Heyns

unread,
Mar 9, 2026, 11:32:00 AM (13 days ago) Mar 9
to zotero-dev
BTW I specify an icon on the first submenu, but in my tests it appears in all things under it, yet *not* on the submenu itself.

XY Wong

unread,
Mar 9, 2026, 11:32:48 AM (13 days ago) Mar 9
to zotero-dev
`context.setL10nArgs(JSON.stringify(aes[i] || {}));`

It only accepts a json format string. The type definition lib might need to be corrected.

XY Wong

unread,
Mar 9, 2026, 11:39:55 AM (13 days ago) Mar 9
to zotero-dev
The icon (should) actually be visible on the top level submenu, though because of platform-specific rules on MacOS it is hidden. In the design of the app we don't allow any icons on the context menu.

It is a bug that the icon appears in its child elements. Before the fix, you can use an invalid icon path for the child elements as a temporary workaround. Thanks for the report.

Emiliano Heyns

unread,
Mar 9, 2026, 11:52:21 AM (13 days ago) Mar 9
to zotero-dev

XY Wong

unread,
Mar 9, 2026, 12:01:42 PM (13 days ago) Mar 9
to zotero-dev
That's the initial value of `l10nArgs`, which takes an object because it's already part of an object passed for configuration, while the function `setL10nArgs` accepts only a string. It was designed like this because for function calls we prioritize passing primitives instead of objects whenever possible. We probably should allow initial `l10nArgs` as string, though. That's a good point.
Reply all
Reply to author
Forward
0 new messages