From 527c30b8c106e52affc41fb198d0714bafbed3b3 Mon Sep 17 00:00:00 2001 From: Abe Jellinek Date: Sat, 23 Dec 2023 22:26:30 -0800 Subject: [PATCH] Initial implementation of Libraries and Collections box --- chrome/content/zotero/customElements.js | 1 + .../zotero/elements/itemPaneSidenav.js | 5 + .../elements/librariesCollectionsBox.js | 256 ++++++++++++++++++ chrome/content/zotero/itemPane.js | 5 +- chrome/content/zotero/zoteroPane.js | 6 +- chrome/content/zotero/zoteroPane.xhtml | 2 + chrome/locale/en-US/zotero/zotero.ftl | 5 + chrome/locale/en-US/zotero/zotero.properties | 1 + .../itempane/16/libraries-collections.svg | 10 + .../itempane/20/libraries-collections.svg | 10 + scss/_zotero.scss | 1 + scss/abstracts/_mixins.scss | 8 +- scss/abstracts/_variables.scss | 3 +- scss/components/_virtualized-table.scss | 2 +- scss/elements/_attachmentRow.scss | 3 +- scss/elements/_collapsibleSection.scss | 1 - scss/elements/_librariesCollectionsBox.scss | 52 ++++ scss/elements/_notesBox.scss | 2 +- scss/elements/_tagsBox.scss | 1 - scss/themes/_dark.scss | 3 +- scss/themes/_light.scss | 1 + 21 files changed, 363 insertions(+), 15 deletions(-) create mode 100644 chrome/content/zotero/elements/librariesCollectionsBox.js create mode 100644 chrome/skin/default/zotero/itempane/16/libraries-collections.svg create mode 100644 chrome/skin/default/zotero/itempane/20/libraries-collections.svg create mode 100644 scss/elements/_librariesCollectionsBox.scss diff --git a/chrome/content/zotero/customElements.js b/chrome/content/zotero/customElements.js index 350d7c022d..f1eeb00547 100644 --- a/chrome/content/zotero/customElements.js +++ b/chrome/content/zotero/customElements.js @@ -55,6 +55,7 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachment Services.scriptloader.loadSubScript('chrome://zotero/content/elements/annotationRow.js', this); Services.scriptloader.loadSubScript('chrome://zotero/content/elements/contextNotesList.js', this); Services.scriptloader.loadSubScript('chrome://zotero/content/elements/noteRow.js', this); +Services.scriptloader.loadSubScript('chrome://zotero/content/elements/librariesCollectionsBox.js', this); { // Fix missing property bug that breaks arrow key navigation between s diff --git a/chrome/content/zotero/elements/itemPaneSidenav.js b/chrome/content/zotero/elements/itemPaneSidenav.js index 2bf8d62245..b158a8a914 100644 --- a/chrome/content/zotero/elements/itemPaneSidenav.js +++ b/chrome/content/zotero/elements/itemPaneSidenav.js @@ -53,6 +53,11 @@ data-l10n-id="sidenav-notes" data-pane="notes"/> + + + . + + ***** END LICENSE BLOCK ***** +*/ + +"use strict"; + +import { getCSSIcon } from 'components/icons'; + +{ + class LibrariesCollectionsBox extends XULElementBase { + content = MozXULElement.parseXULToFragment(` + + + + + + + + + + + `, ['chrome://zotero/locale/zotero.dtd']); + + _item = null; + + _linkedItems = []; + + _mode = null; + + get item() { + return this._item; + } + + set item(item) { + this._item = item; + // Getting linked items is an async process, so start by rendering without them + this._linkedItems = []; + this.render(); + + this._updateLinkedItems(); + } + + get mode() { + return this._mode; + } + + set mode(mode) { + this._mode = mode; + this.render(); + } + + init() { + this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'librariesCollectionsBox'); + this._body = this.querySelector('.body'); + this._section = this.querySelector('collapsible-section'); + this._section.addEventListener('add', (event) => { + this.querySelector('.add-popup').openPopupAtScreen( + event.detail.button.screenX, + event.detail.button.screenY, + true + ); + this._section.open = true; + }); + this.render(); + } + + destroy() { + Zotero.Notifier.unregisterObserver(this._notifierID); + } + + notify(action, type, ids) { + if (action == 'modify' + && this._item + && (ids.includes(this._item.id) || this._linkedItems.some(item => ids.includes(item.id)))) { + this.render(); + } + } + + _buildRow(obj, level, contextItem) { + let isContext = obj instanceof Zotero.Collection && !contextItem.inCollection(obj.id); + + let row = document.createElement('div'); + row.classList.add('row'); + row.dataset.id = obj.treeViewID; + row.dataset.level = level; + row.style.setProperty('--level', level); + row.classList.toggle('context', isContext); + + let box = document.createElement('div'); + box.classList.add('box'); + + let iconName; + if (obj instanceof Zotero.Group) { + iconName = 'library-group'; + } + else if (obj instanceof Zotero.Library) { + iconName = 'library'; + } + else { + iconName = 'collection'; + } + let icon = getCSSIcon(iconName); + box.append(icon); + + let text = document.createElement('span'); + text.classList.add('label'); + text.textContent = obj.name; + box.append(text); + + row.append(box); + + if (this._mode == 'edit' && obj instanceof Zotero.Collection && !isContext) { + let remove = document.createXULElement('toolbarbutton'); + remove.className = 'zotero-clicky zotero-clicky-minus'; + remove.addEventListener('command', () => { + if (Services.prompt.confirm( + window, + Zotero.getString('pane.items.remove.title'), + Zotero.getString('pane.items.removeFromOther', [obj.name]) + )) { + contextItem.removeFromCollection(obj.id); + contextItem.saveTx(); + } + }); + row.append(remove); + } + + let isCurrent = ZoteroPane.collectionsView.selectedTreeRow?.id == obj.treeViewID; + box.classList.toggle('current', isCurrent); + + // Disable clicky if this is a context row or we're already in the library/collection it points to + let disableClicky = isContext || isCurrent; + box.toggleAttribute('disabled', disableClicky); + if (!disableClicky) { + box.addEventListener('click', async () => { + await ZoteroPane.collectionsView.selectByID(obj.treeViewID); + await ZoteroPane.selectItem(contextItem.id); + }); + } + + return row; + } + + _findRow(obj) { + return this._body.querySelector(`.row[data-id="${obj.treeViewID}"]`); + } + + _getChildren(row = null) { + let rows = Array.from(this._body.querySelectorAll('.row')); + let startIndex = row ? rows.indexOf(row) + 1 : 0; + let level = row ? parseInt(row.dataset.level) + 1 : 0; + let children = []; + for (let i = startIndex; i < rows.length; i++) { + let childLevel = parseInt(rows[i].dataset.level); + if (childLevel == level) { + children.push(rows[i]); + } + else if (childLevel < level) { + break; + } + } + return children; + } + + _addObject(obj, contextItem) { + let existingRow = this._findRow(obj); + if (existingRow) { + return existingRow; + } + + let parent = obj instanceof Zotero.Library + ? null + : (obj.parentID ? Zotero.Collections.get(obj.parentID) : Zotero.Libraries.get(obj.libraryID)); + let parentRow = parent && this._findRow(parent); + if (parent && !parentRow) { + parentRow = this._addObject(parent, contextItem); + } + + let row = this._buildRow(obj, parentRow ? parseInt(parentRow.dataset.level) + 1 : 0, contextItem); + let siblings = this._getChildren(parentRow); + let added = false; + for (let sibling of siblings) { + if (Zotero.localeCompare(sibling.querySelector('.label').textContent, obj.name) > 0) { + sibling.before(row); + added = true; + break; + } + } + if (!added) { + if (siblings.length) { + let lastSibling = siblings[siblings.length - 1]; + let childrenOfLastSibling = this._getChildren(lastSibling); + if (childrenOfLastSibling.length) { + childrenOfLastSibling[childrenOfLastSibling.length - 1].after(row); + } + else { + lastSibling.after(row); + } + } + else if (parentRow) { + parentRow.after(row); + } + else { + this._body.append(row); + } + } + return row; + } + + async _updateLinkedItems() { + this._linkedItems = (await Promise.all(Zotero.Libraries.getAll() + .filter(lib => lib.libraryID !== this._item.libraryID) + .map(lib => this._item.getLinkedItem(lib.libraryID, true)))) + .filter(Boolean); + this.render(); + } + + render() { + if (!this._item) { + return; + } + + this._body.replaceChildren(); + for (let item of [this._item, ...this._linkedItems]) { + this._addObject(Zotero.Libraries.get(item.libraryID), item); + for (let collection of Zotero.Collections.get(item.getCollections())) { + this._addObject(collection, item); + } + } + + this._section.showAdd = this._mode == 'edit'; + } + } + customElements.define("libraries-collections-box", LibrariesCollectionsBox); +} diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js index 3877094657..63cd5a7da5 100644 --- a/chrome/content/zotero/itemPane.js +++ b/chrome/content/zotero/itemPane.js @@ -25,7 +25,7 @@ var ZoteroItemPane = new function() { var _container; - var _header, _sidenav, _scrollParent, _itemBox, _abstractBox, _attachmentsBox, _tagsBox, _notesBox, _relatedBox, _boxes; + var _header, _sidenav, _scrollParent, _itemBox, _abstractBox, _attachmentsBox, _tagsBox, _notesBox, _librariesCollectionsBox, _relatedBox, _boxes; var _deck; var _lastItem; var _selectedNoteID; @@ -45,8 +45,9 @@ var ZoteroItemPane = new function() { _notesBox = document.getElementById('zotero-editpane-notes'); _attachmentsBox = document.getElementById('zotero-editpane-attachments'); _tagsBox = document.getElementById('zotero-editpane-tags'); + _librariesCollectionsBox = document.getElementById('zotero-editpane-libraries-collections'); _relatedBox = document.getElementById('zotero-editpane-related'); - _boxes = [_itemBox, _abstractBox, _notesBox, _attachmentsBox, _tagsBox, _relatedBox]; + _boxes = [_itemBox, _abstractBox, _notesBox, _attachmentsBox, _librariesCollectionsBox, _tagsBox, _relatedBox]; _deck = document.getElementById('zotero-item-pane-content'); diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index c6e56ad6d2..91c51fae80 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -3953,10 +3953,10 @@ var ZoteroPane = new function() this.buildAddToCollectionMenu = function (event) { - if (event.target.id !== 'zotero-add-to-collection-popup') return; + if (event.target !== event.currentTarget) return; - let popup = document.getElementById('zotero-add-to-collection-popup'); - let separator = document.getElementById('zotero-add-to-collection-separator'); + let popup = event.target; + let separator = popup.querySelector('menuseparator'); while (popup.childElementCount > 2) { popup.removeChild(popup.lastElementChild); } diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml index 2e966e6494..995014cf8b 100644 --- a/chrome/content/zotero/zoteroPane.xhtml +++ b/chrome/content/zotero/zoteroPane.xhtml @@ -1128,6 +1128,8 @@ + + diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl index 060a872fdb..e3a6b1f5e1 100644 --- a/chrome/locale/en-US/zotero/zotero.ftl +++ b/chrome/locale/en-US/zotero/zotero.ftl @@ -235,6 +235,7 @@ pane-info = Info pane-abstract = Abstract pane-attachments = Attachments pane-notes = Notes +pane-libraries-collections = Libraries and Collections pane-tags = Tags pane-related = Related @@ -252,6 +253,8 @@ section-notes = [one] { $count } Note *[other] { $count } Notes } +section-libraries-collections = + .label = { pane-libraries-collections } section-tags = .label = { $count -> [one] { $count } Tag @@ -268,6 +271,8 @@ sidenav-attachments = .tooltiptext = { pane-attachments } sidenav-notes = .tooltiptext = { pane-notes } +sidenav-libraries-collections = + .tooltiptext = { pane-libraries-collections } sidenav-tags = .tooltiptext = { pane-tags } sidenav-related = diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index 87191a850e..ad08fef613 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -339,6 +339,7 @@ pane.items.delete.multiple = Are you sure you want to delete the selected items pane.items.remove.title = Remove from Collection pane.items.remove = Are you sure you want to remove the selected item from this collection? pane.items.remove.multiple = Are you sure you want to remove the selected items from this collection? +pane.items.removeFromOther = Are you sure you want to remove the selected item from “%S”? pane.items.removeFromPublications.title = Remove from My Publications pane.items.removeFromPublications = Are you sure you want to remove the selected item from My Publications? pane.items.removeFromPublications.multiple = Are you sure you want to remove the selected items from My Publications? diff --git a/chrome/skin/default/zotero/itempane/16/libraries-collections.svg b/chrome/skin/default/zotero/itempane/16/libraries-collections.svg new file mode 100644 index 0000000000..e5c9b39b12 --- /dev/null +++ b/chrome/skin/default/zotero/itempane/16/libraries-collections.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/chrome/skin/default/zotero/itempane/20/libraries-collections.svg b/chrome/skin/default/zotero/itempane/20/libraries-collections.svg new file mode 100644 index 0000000000..a9e776413a --- /dev/null +++ b/chrome/skin/default/zotero/itempane/20/libraries-collections.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/scss/_zotero.scss b/scss/_zotero.scss index edec95a9c6..7ed7390cb6 100644 --- a/scss/_zotero.scss +++ b/scss/_zotero.scss @@ -76,3 +76,4 @@ @import "elements/attachmentRow"; @import "elements/annotationRow"; @import "elements/noteRow"; +@import "elements/librariesCollectionsBox"; diff --git a/scss/abstracts/_mixins.scss b/scss/abstracts/_mixins.scss index a5999d8fbe..8b86537b7b 100644 --- a/scss/abstracts/_mixins.scss +++ b/scss/abstracts/_mixins.scss @@ -86,11 +86,15 @@ gap: 4px; padding-inline-start: 4px; overflow: hidden; + border-radius: 5px; - &:hover { - border-radius: 5px; + &:not([disabled]):hover { background-color: var(--fill-quinary); } + + &:not([disabled]):active { + background-color: var(--fill-quarternary); + } .label { display: -webkit-box; diff --git a/scss/abstracts/_variables.scss b/scss/abstracts/_variables.scss index bf85e18939..20c37a16ae 100644 --- a/scss/abstracts/_variables.scss +++ b/scss/abstracts/_variables.scss @@ -82,9 +82,10 @@ $z-index-loading-cover: 60; // -------------------------------------------------- $item-pane-sections: ( "info": var(--accent-blue), - "abstract": var(--accent-teal), + "abstract": var(--accent-azure), "attachments": var(--accent-green), "notes": var(--accent-yellow), + "libraries-collections": var(--accent-teal), "tags": var(--accent-orange), "related": var(--accent-wood), ); diff --git a/scss/components/_virtualized-table.scss b/scss/components/_virtualized-table.scss index 617b2b2b8a..8fb1e05a12 100644 --- a/scss/components/_virtualized-table.scss +++ b/scss/components/_virtualized-table.scss @@ -148,7 +148,7 @@ } &.context-row { - color: gray; + color: var(--fill-secondary); } .spacer-twisty { diff --git a/scss/elements/_attachmentRow.scss b/scss/elements/_attachmentRow.scss index 3b736145e7..8d14cefe6b 100644 --- a/scss/elements/_attachmentRow.scss +++ b/scss/elements/_attachmentRow.scss @@ -44,8 +44,7 @@ attachment-row { } &.context > .head .label { - // TODO This color is used in virtualized-table - probably want to change to something theme-defined - color: gray; + color: var(--fill-secondary); } & > .body { diff --git a/scss/elements/_collapsibleSection.scss b/scss/elements/_collapsibleSection.scss index 40b956d8a8..653e3a9035 100644 --- a/scss/elements/_collapsibleSection.scss +++ b/scss/elements/_collapsibleSection.scss @@ -101,6 +101,5 @@ collapsible-section { &.disable-transitions * { transition: none !important; - color: red; } } diff --git a/scss/elements/_librariesCollectionsBox.scss b/scss/elements/_librariesCollectionsBox.scss new file mode 100644 index 0000000000..0f6657d00d --- /dev/null +++ b/scss/elements/_librariesCollectionsBox.scss @@ -0,0 +1,52 @@ +libraries-collections-box { + display: flex; + flex-direction: column; + + .body { + display: flex; + flex-direction: column; + margin-inline-start: 16px; + + .row { + display: flex; + align-items: flex-start; + gap: 4px; + margin-inline-start: calc(16px * var(--level, 0)); + + @include comfortable { + padding-block: 2px; + } + + &.context { + color: var(--fill-secondary); + + .box .icon { + opacity: 0.5; + } + } + + .box { + @include clicky-item; + flex: 1; + + &.current { + font-weight: 590; + } + + .icon { + width: 16px; + height: 16px; + } + } + + toolbarbutton { + margin-inline-start: auto; + visibility: hidden; + } + + &:is(:hover, :focus-within) toolbarbutton { + visibility: visible; + } + } + } +} diff --git a/scss/elements/_notesBox.scss b/scss/elements/_notesBox.scss index 9b02a51f1e..611d3828e5 100644 --- a/scss/elements/_notesBox.scss +++ b/scss/elements/_notesBox.scss @@ -7,7 +7,7 @@ notes-box, related-box { notes-box .body, related-box .body { display: flex; flex-direction: column; - padding-inline-start: 16px - 2px; + padding-inline-start: 16px; .row { display: flex; diff --git a/scss/elements/_tagsBox.scss b/scss/elements/_tagsBox.scss index 14fe050820..4d09ed1c1d 100644 --- a/scss/elements/_tagsBox.scss +++ b/scss/elements/_tagsBox.scss @@ -19,7 +19,6 @@ tags-box .body { grid-template-columns: 12px 1fr 20px; align-items: center; column-gap: 4px; - padding-block: 1px; // Shift-Enter &.multiline { diff --git a/scss/themes/_dark.scss b/scss/themes/_dark.scss index a4e81357e6..4631440e78 100644 --- a/scss/themes/_dark.scss +++ b/scss/themes/_dark.scss @@ -2,6 +2,7 @@ @use "sass:map"; $-colors: ( + accent-azure: #66adffd9, accent-blue: #4072e5, accent-blue10: #4072e54d, accent-blue30: #4072e573, @@ -87,4 +88,4 @@ $-colors: ( --material-border-quinary: 1px solid var(--fill-quinary); --material-border-quarternary: 1px solid var(--fill-quarternary); } -} \ No newline at end of file +} diff --git a/scss/themes/_light.scss b/scss/themes/_light.scss index 66d4935ffd..7a2a120edb 100644 --- a/scss/themes/_light.scss +++ b/scss/themes/_light.scss @@ -2,6 +2,7 @@ @use "sass:map"; $-colors: ( + accent-azure: #66adff, accent-blue: #4072e5, accent-blue10: #4072e51a, accent-blue30: #4072e54d,