From 59b1d75b98eecd2e910a18a8f77b81b44091dd93 Mon Sep 17 00:00:00 2001 From: Abe Jellinek Date: Fri, 10 May 2024 08:23:26 -0400 Subject: [PATCH] Item pane header customization (#3791) --- chrome/content/zotero/bibliography.js | 2 +- chrome/content/zotero/elements/itemBox.js | 27 +- chrome/content/zotero/elements/paneHeader.js | 343 ++++++++++++++++-- .../zotero/preferences/preferences.xhtml | 1 + .../zotero/preferences/preferences_general.js | 78 ++++ .../preferences/preferences_general.xhtml | 33 ++ chrome/content/zotero/xpcom/style.js | 1 - chrome/locale/en-US/zotero/preferences.ftl | 5 + chrome/locale/en-US/zotero/zotero.ftl | 15 + .../default/zotero/16/universal/merge.svg | 2 +- defaults/preferences/zotero.js | 3 + scss/elements/_duplicatesMergePane.scss | 39 +- scss/elements/_editableText.scss | 17 +- scss/elements/_itemBox.scss | 10 +- scss/elements/_paneHeader.scss | 64 ++-- scss/preferences/_general.scss | 4 + test/tests/itemPaneTest.js | 132 +++++++ 17 files changed, 664 insertions(+), 112 deletions(-) diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js index b9d629ce28..334063d28b 100644 --- a/chrome/content/zotero/bibliography.js +++ b/chrome/content/zotero/bibliography.js @@ -79,7 +79,7 @@ var Zotero_File_Interface_Bibliography = new function() { } // See note in style.js - if (!Zotero.Styles.initialized) { + if (!Zotero.Styles.initialized()) { // Initialize styles yield Zotero.Styles.init(); } diff --git a/chrome/content/zotero/elements/itemBox.js b/chrome/content/zotero/elements/itemBox.js index 5223afff36..bad3ac7549 100644 --- a/chrome/content/zotero/elements/itemBox.js +++ b/chrome/content/zotero/elements/itemBox.js @@ -439,14 +439,7 @@ } get _ignoreFields() { - let value = ['title', 'abstractNote'] - .flatMap(field => [ - field, - ...(Zotero.ItemFields.getTypeFieldsFromBase(field, true) || []) - ]); - // Cache the result - Object.defineProperty(this, '_ignoreFields', { value }); - return value; + return ['abstractNote']; } get _linkMenu() { @@ -696,7 +689,7 @@ var button = document.createXULElement("toolbarbutton"); button.className = 'zotero-field-version-button zotero-clicky-merge'; button.setAttribute('type', 'menu'); - button.setAttribute('wantdropmarker', true); + button.setAttribute('data-l10n-id', 'itembox-button-merge'); var popup = button.appendChild(document.createXULElement("menupopup")); @@ -760,8 +753,13 @@ // Creator rows - // Place, in order of preference, after type or at beginning - let field = this._infoTable.querySelector('[fieldname="itemType"]'); + // Place, in order of preference, after title, after type, + // or at beginning + var titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title'); + var field = this._infoTable.querySelector(`[fieldname="${Zotero.ItemFields.getName(titleFieldID)}"]`); + if (!field) { + field = this._infoTable.querySelector('[fieldName="itemType"]'); + } if (field) { this._beforeRow = field.parentNode.nextSibling; } @@ -2069,6 +2067,13 @@ var fieldName = label.getAttribute('fieldname'); this._modifyField(fieldName, newValue); + if (Zotero.ItemFields.isFieldOfBase(fieldName, 'title')) { + let shortTitleVal = this.item.getField('shortTitle'); + if (newValue.toLowerCase().startsWith(shortTitleVal.toLowerCase())) { + this._modifyField('shortTitle', newValue.substring(0, shortTitleVal.length)); + } + } + if (this.saveOnEdit) { // If a field is open, blur it, which will trigger a save and cause // the saveTx() to be a no-op diff --git a/chrome/content/zotero/elements/paneHeader.js b/chrome/content/zotero/elements/paneHeader.js index 25807d2f3a..a2ff4895b8 100644 --- a/chrome/content/zotero/elements/paneHeader.js +++ b/chrome/content/zotero/elements/paneHeader.js @@ -26,30 +26,31 @@ "use strict"; { + let htmlDoc = document.implementation.createHTMLDocument(); + class PaneHeader extends ItemPaneSectionElementBase { content = MozXULElement.parseXULToFragment(` - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + `, ['chrome://zotero/locale/zotero.dtd']); - showInFeeds = true; - _item = null; _titleFieldID = null; @@ -65,9 +66,51 @@ init() { this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'paneHeader'); + this._prefsObserverIDs = [ + Zotero.Prefs.registerObserver('itemPaneHeader', () => { + // TEMP?: _forceRenderAll() doesn't do anything if the section is hidden, so un-hide first + this.hidden = false; + this._forceRenderAll(); + }), + Zotero.Prefs.registerObserver('itemPaneHeader.bibEntry.style', () => this._forceRenderAll()), + Zotero.Prefs.registerObserver('itemPaneHeader.bibEntry.locale', () => this._forceRenderAll()), + ]; - this.titleField = this.querySelector('.title editable-text'); - this.menuButton = this.querySelector('.menu-button'); + this.title = this.querySelector('.title'); + this.titleField = this.title.querySelector('editable-text'); + this.creatorYear = this.querySelector('.creator-year'); + this.bibEntry = this.querySelector('.bib-entry'); + this.bibEntry.attachShadow({ mode: 'open' }); + + // Context menu for non-editable information (creator/year and bib entry) + this.secondaryPopup = this.querySelector('.secondary-popup'); + this.secondaryPopup.firstElementChild.addEventListener('command', () => this._handleSecondaryCopy()); + + this.creatorYear.addEventListener('contextmenu', (event) => { + if (this.item) { + this.secondaryPopup.openPopupAtScreen(event.screenX + 1, event.screenY + 1, true); + } + }); + + this.bibEntryContent = document.createElement('div'); + this.bibEntryContent.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + this.bibEntryContent.addEventListener('click', (event) => { + event.preventDefault(); + if (event.target.matches('a[href]')) { + Zotero.launchURL(event.target.href); + } + }); + this.bibEntryContent.addEventListener('contextmenu', (event) => { + if (this._item && Zotero.Styles.initialized()) { + this.secondaryPopup.openPopupAtScreen(event.screenX + 1, event.screenY + 1, true); + } + }); + this.bibEntry.shadowRoot.append(this.bibEntryContent); + + this.viewAsPopup = this.querySelector('.view-as-popup'); + this.viewAsPopup.addEventListener('popupshowing', () => this._buildViewAsMenu(this.viewAsPopup)); + + this._bibEntryCache = new LRUCache(); this.titleField.addEventListener('change', () => this.save()); this.titleField.ariaLabel = Zotero.getString('itemFields.title'); @@ -81,6 +124,17 @@ this._setTransformedValue(newValue); }, }); + + menupopup.append(document.createXULElement('menuseparator')); + + let viewAsMenu = document.createXULElement('menu'); + viewAsMenu.setAttribute('data-l10n-id', 'item-pane-header-view-as'); + viewAsMenu.setAttribute('type', 'menu'); + let viewAsPopup = document.createXULElement('menupopup'); + this._buildViewAsMenu(viewAsPopup); + viewAsMenu.append(viewAsPopup); + menupopup.append(viewAsMenu); + this.ownerDocument.querySelector('popupset').append(menupopup); menupopup.addEventListener('popuphidden', () => menupopup.remove()); menupopup.openPopupAtScreen(event.screenX + 1, event.screenY + 1, true); @@ -89,9 +143,18 @@ destroy() { Zotero.Notifier.unregisterObserver(this._notifierID); + for (let id of this._prefsObserverIDs) { + Zotero.Prefs.unregisterObserver(id); + } } notify(action, type, ids) { + if (action == 'modify' || action == 'delete') { + for (let id of ids) { + this._bibEntryCache.delete(id); + } + } + if (action == 'modify' && this.item && ids.includes(this.item.id)) { this._forceRenderAll(); } @@ -102,15 +165,15 @@ this._item.setField(this._titleFieldID, newValue); let shortTitleVal = this._item.getField('shortTitle'); if (newValue.toLowerCase().startsWith(shortTitleVal.toLowerCase())) { - this._item.setField('shortTitle', newValue.substr(0, shortTitleVal.length)); + this._item.setField('shortTitle', newValue.substring(0, shortTitleVal.length)); } await this._item.saveTx(); } async save() { - if (this.item) { - this.item.setField(this._titleFieldID, this.titleField.value); - await this.item.saveTx(); + if (this._item) { + this._item.setField(this._titleFieldID, this.titleField.value); + await this._item.saveTx(); } this._forceRenderAll(); } @@ -121,29 +184,189 @@ await this.save(); } } - + render() { - if (!this.item) { + if (!this._item) { return; } if (this._isAlreadyRendered()) return; - this._titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title'); + let headerMode = Zotero.Prefs.get('itemPaneHeader'); + if (this._item.isAttachment()) { + headerMode = 'title'; + } - let title = this.item.getField(this._titleFieldID); - // If focused, update the value that will be restored on Escape; - // otherwise, update the displayed value - if (this.titleField.focused) { - this.titleField.initialValue = title; + if (headerMode === 'none') { + this.hidden = true; + return; } - else { - this.titleField.value = title; + + this.hidden = false; + this.title.hidden = true; + this.creatorYear.hidden = true; + this.bibEntry.hidden = true; + + if (headerMode === 'bibEntry') { + if (!Zotero.Styles.initialized()) { + this.bibEntryContent.textContent = Zotero.getString('general.loading'); + this.bibEntry.classList.add('loading'); + this.bibEntry.hidden = false; + Zotero.Styles.init().then(() => this._forceRenderAll()); + return; + } + + if (this._renderBibEntry()) { + this.bibEntry.hidden = false; + return; + } + + // Fall back to Title/Creator/Year if style is not found + headerMode = 'titleCreatorYear'; } - this.titleField.readOnly = !this.editable; - if (this._titleFieldID) { - this.titleField.placeholder = Zotero.ItemFields.getLocalizedString(this._titleFieldID); + + if (headerMode === 'title' || headerMode === 'titleCreatorYear') { + this._titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this._item.itemTypeID, 'title'); + + let title = this.item.getField(this._titleFieldID); + // If focused, update the value that will be restored on Escape; + // otherwise, update the displayed value + if (this.titleField.focused) { + this.titleField.initialValue = title; + } + else { + this.titleField.value = title; + } + this.titleField.readOnly = !this.editable; + if (this._titleFieldID) { + this.titleField.placeholder = Zotero.ItemFields.getLocalizedString(this._titleFieldID); + } + this.title.hidden = false; } - this.menuButton.hidden = !this.item.isRegularItem() && !this.item.isAttachment(); + + if (headerMode === 'titleCreatorYear') { + let firstCreator = this._item.getField('firstCreator'); + let year = this._item.getField('year'); + let creatorYearString = ''; + if (firstCreator) { + creatorYearString += firstCreator; + } + if (year) { + creatorYearString += ` (${year})`; + } + + if (creatorYearString) { + this.creatorYear.textContent = creatorYearString; + this.creatorYear.hidden = false; + } + else { + this.creatorYear.hidden = true; + } + } + + // Make title field padding tighter if creator/year is visible below it + this.titleField.toggleAttribute('tight', + headerMode === 'titleCreatorYear' && !this.creatorYear.hidden); + } + + _renderBibEntry() { + let style = Zotero.Styles.get(Zotero.Prefs.get('itemPaneHeader.bibEntry.style')); + if (!style) { + Zotero.warn('Style not found: ' + Zotero.Prefs.get('itemPaneHeader.bibEntry.style')); + return false; + } + let locale = Zotero.Prefs.get('itemPaneHeader.bibEntry.locale'); + + // Create engine if not cached (first run with this style) + if (this._cslEngineStyleID !== style.styleID || this._cslEngineLocale !== locale) { + this._cslEngine = style.getCiteProc(locale, 'html'); + this._cslEngineStyleID = style.styleID; + this._cslEngineLocale = locale; + this._bibEntryCache.clear(); + } + + // Create bib entry if not cached (first run on this item or item data has changed) + if (!this._bibEntryCache.has(this._item.id)) { + // Force refresh items - without this, entries won't change when item data changes + this._cslEngine.updateItems([]); + this._bibEntryCache.set(this._item.id, + Zotero.Cite.makeFormattedBibliographyOrCitationList(this._cslEngine, + [this._item], 'html', false)); + } + + htmlDoc.body.innerHTML = this._bibEntryCache.get(this._item.id); + // Remove .loading (added above if styles weren't yet initialized) + this.bibEntry.classList.remove('loading'); + // Remove existing children and *then* append new ones to avoid "scripts are blocked internally" + // error in log + this.bibEntryContent.replaceChildren(); + this.bibEntryContent.append(...htmlDoc.body.childNodes); + + let body = this.bibEntryContent.querySelector('.csl-bib-body'); + if (!body) { + Zotero.debug('No .csl-bib-body found in bib entry'); + return false; + } + + // Remove any custom indentation/line height set by the style + body.style.marginLeft = body.style.marginRight = ''; + body.style.textIndent = ''; + body.style.lineHeight = ''; + + if (style.categories === 'numeric') { + // Remove number from entry if present + let number = body.querySelector('.csl-entry > .csl-left-margin:first-child'); + if (number) { + let followingContent = number.nextElementSibling; + if (followingContent?.classList.contains('csl-right-inline')) { + followingContent.classList.remove('csl-right-inline'); + followingContent.style = ''; + } + number.remove(); + } + } + + return true; + } + + _handleSecondaryCopy() { + let selectedMode = Zotero.Prefs.get('itemPaneHeader'); + if (selectedMode === 'titleCreatorYear') { + Zotero.Utilities.Internal.copyTextToClipboard(this.creatorYear.textContent); + } + else if (selectedMode === 'bibEntry') { + Zotero_File_Interface.copyItemsToClipboard( + [this._item], + Zotero.Prefs.get('itemPaneHeader.bibEntry.style'), + Zotero.Prefs.get('itemPaneHeader.bibEntry.locale'), + false, + false + ); + } + } + + _buildViewAsMenu(menupopup) { + menupopup.replaceChildren(); + + let selectedMode = Zotero.Prefs.get('itemPaneHeader'); + for (let headerMode of ['title', 'titleCreatorYear', 'bibEntry']) { + let menuitem = document.createXULElement('menuitem'); + menuitem.setAttribute('data-l10n-id', 'item-pane-header-' + headerMode); + menuitem.setAttribute('type', 'radio'); + menuitem.setAttribute('checked', headerMode === selectedMode); + menuitem.addEventListener('command', () => { + Zotero.Prefs.set('itemPaneHeader', headerMode); + }); + menupopup.append(menuitem); + } + + menupopup.append(document.createXULElement('menuseparator')); + + let moreOptionsMenuitem = document.createXULElement('menuitem'); + moreOptionsMenuitem.setAttribute('data-l10n-id', 'item-pane-header-more-options'); + moreOptionsMenuitem.addEventListener('command', () => { + Zotero.Utilities.Internal.openPreferences('zotero-prefpane-general'); + }); + menupopup.append(moreOptionsMenuitem); } renderCustomHead(callback) { @@ -159,4 +382,46 @@ } } customElements.define("pane-header", PaneHeader); + + /** + * Simple LRU cache that stores bibliography entries for the 100 most recently viewed items. + */ + class LRUCache { + static CACHE_SIZE = 100; + + _map = new Map(); + + clear() { + this._map.clear(); + } + + has(key) { + return this._map.has(key); + } + + get(key) { + if (!this._map.has(key)) { + return undefined; + } + let value = this._map.get(key); + // Maps are sorted by insertion order, so delete and add back at the end + this._map.delete(key); + this._map.set(key, value); + return value; + } + + set(key, value) { + this._map.delete(key); + // Delete the first (= inserted earliest) elements until we're under CACHE_SIZE + while (this._map.size >= this.constructor.CACHE_SIZE) { + this._map.delete(this._map.keys().next().value); + } + this._map.set(key, value); + return this; + } + + delete(key) { + return this._map.delete(key); + } + } } diff --git a/chrome/content/zotero/preferences/preferences.xhtml b/chrome/content/zotero/preferences/preferences.xhtml index fd87d09535..7c7c1fb2c1 100644 --- a/chrome/content/zotero/preferences/preferences.xhtml +++ b/chrome/content/zotero/preferences/preferences.xhtml @@ -54,6 +54,7 @@ + diff --git a/chrome/content/zotero/preferences/preferences_general.js b/chrome/content/zotero/preferences/preferences_general.js index a817b114c4..6e6e451c7b 100644 --- a/chrome/content/zotero/preferences/preferences_general.js +++ b/chrome/content/zotero/preferences/preferences_general.js @@ -53,6 +53,7 @@ Zotero_Preferences.General = { } this.refreshLocale(); + this._initItemPaneHeaderUI(); this.updateAutoRenameFilesUI(); this._updateFileHandlerUI(); this._initEbookFontFamilyMenu(); @@ -136,6 +137,83 @@ Zotero_Preferences.General = { Zotero.Utilities.Internal.quitZotero(true); } }, + + _initItemPaneHeaderUI() { + let pane = document.querySelector('#zotero-prefpane-general'); + let headerMenu = document.querySelector('#item-pane-header-menulist'); + let styleMenu = document.querySelector('#item-pane-header-style-menu'); + + this._updateItemPaneHeaderStyleUI(); + pane.addEventListener('showing', () => this._updateItemPaneHeaderStyleUI()); + + // menulists stop responding to clicks if we replace their items while + // they're closing. Yield back to the event loop before updating to + // avoid this. + let updateUI = () => { + setTimeout(() => { + this._updateItemPaneHeaderStyleUI(); + }); + }; + headerMenu.addEventListener('command', updateUI); + styleMenu.addEventListener('command', updateUI); + }, + + _updateItemPaneHeaderStyleUI: Zotero.Utilities.Internal.serial(async function () { + let optionsContainer = document.querySelector('#item-pane-header-bib-entry-options'); + let styleMenu = document.querySelector('#item-pane-header-style-menu'); + let localeMenu = document.querySelector('#item-pane-header-locale-menu'); + + optionsContainer.hidden = Zotero.Prefs.get('itemPaneHeader') !== 'bibEntry'; + if (optionsContainer.hidden) { + return; + } + + if (!Zotero.Styles.initialized()) { + let menus = [styleMenu, localeMenu]; + for (let menu of menus) { + menu.selectedItem = null; + menu.setAttribute('label', Zotero.getString('general.loading')); + menu.disabled = true; + } + await Zotero.Styles.init(); + for (let menu of menus) { + menu.disabled = false; + } + } + + let currentStyle = Zotero.Styles.get(styleMenu.value); + let currentLocale = Zotero.Prefs.get('itemPaneHeader.bibEntry.locale'); + + styleMenu.menupopup.replaceChildren(); + for (let style of Zotero.Styles.getVisible()) { + let menuitem = document.createXULElement('menuitem'); + menuitem.label = style.title; + menuitem.value = style.styleID; + styleMenu.menupopup.append(menuitem); + } + + if (currentStyle) { + if (currentStyle.styleID !== styleMenu.value) { + // Style has been renamed + styleMenu.value = currentStyle.styleID; + } + + if (!localeMenu.menupopup.childElementCount) { + Zotero.Styles.populateLocaleList(localeMenu); + } + Zotero.Styles.updateLocaleList(localeMenu, currentStyle, currentLocale); + } + else { + // Style is unknown/removed - show placeholder + let shortName = styleMenu.value.replace('http://www.zotero.org/styles/', ''); + let missingLabel = await document.l10n.formatValue( + 'preferences-item-pane-header-missing-style', + { shortName } + ); + styleMenu.selectedItem = null; + styleMenu.setAttribute('label', missingLabel); + } + }), updateAutoRenameFilesUI: function () { setTimeout(() => { diff --git a/chrome/content/zotero/preferences/preferences_general.xhtml b/chrome/content/zotero/preferences/preferences_general.xhtml index 2698f907f8..78f6e5e5f8 100644 --- a/chrome/content/zotero/preferences/preferences_general.xhtml +++ b/chrome/content/zotero/preferences/preferences_general.xhtml @@ -44,6 +44,39 @@ + + + + + + + + + + + diff --git a/chrome/content/zotero/xpcom/style.js b/chrome/content/zotero/xpcom/style.js index 95b057efc6..278086892f 100644 --- a/chrome/content/zotero/xpcom/style.js +++ b/chrome/content/zotero/xpcom/style.js @@ -507,7 +507,6 @@ Zotero.Styles = new function() { * Populate menulist with locales * * @param {xul:menulist} menulist - * @return {Promise} */ this.populateLocaleList = function (menulist) { if (!_initialized) { diff --git a/chrome/locale/en-US/zotero/preferences.ftl b/chrome/locale/en-US/zotero/preferences.ftl index 514e331ce9..da95aa5309 100644 --- a/chrome/locale/en-US/zotero/preferences.ftl +++ b/chrome/locale/en-US/zotero/preferences.ftl @@ -33,6 +33,11 @@ preferences-color-scheme-light = preferences-color-scheme-dark = .label = Dark +preferences-item-pane-header = Item Pane Header: +preferences-item-pane-header-style = Header Citation Style: +preferences-item-pane-header-locale = Header Language: +preferences-item-pane-header-missing-style = Missing style: <{ $shortName }> + preferences-advanced-language-and-region-title = Language and Region preferences-advanced-enable-bidi-ui = .label = Enable bidirectional text editing utilities diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl index ea2f29d153..d45419d662 100644 --- a/chrome/locale/en-US/zotero/zotero.ftl +++ b/chrome/locale/en-US/zotero/zotero.ftl @@ -114,6 +114,8 @@ item-button-view-online = itembox-button-options = .tooltiptext = Open Context Menu +itembox-button-merge = + .aria-label = Select Version reader-use-dark-mode-for-content = .label = Use Dark Mode for Content @@ -494,3 +496,16 @@ quicksearch-input = .aria-label = Quick Search .placeholder = { $placeholder } .aria-description = { $placeholder } + +item-pane-header-view-as = + .label = View As +item-pane-header-none = + .label = None +item-pane-header-title = + .label = Title +item-pane-header-titleCreatorYear = + .label = Title, Creator, Year +item-pane-header-bibEntry = + .label = Bibliography Entry +item-pane-header-more-options = + .label = More Options diff --git a/chrome/skin/default/zotero/16/universal/merge.svg b/chrome/skin/default/zotero/16/universal/merge.svg index 359026ac0c..039f06f388 100644 --- a/chrome/skin/default/zotero/16/universal/merge.svg +++ b/chrome/skin/default/zotero/16/universal/merge.svg @@ -1,3 +1,3 @@ - + diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js index eb2c6d4b10..398ad00348 100644 --- a/defaults/preferences/zotero.js +++ b/defaults/preferences/zotero.js @@ -74,6 +74,9 @@ pref("extensions.zotero.sortCreatorAsString", false); pref("extensions.zotero.uiDensity", "comfortable"); +pref("extensions.zotero.itemPaneHeader", "title"); +pref("extensions.zotero.itemPaneHeader.bibEntry.style", "http://www.zotero.org/styles/apa"); +pref("extensions.zotero.itemPaneHeader.bibEntry.locale", ""); //Tag Selector pref("extensions.zotero.tagSelector.showAutomatic", true); diff --git a/scss/elements/_duplicatesMergePane.scss b/scss/elements/_duplicatesMergePane.scss index 7f2bc53eaf..9788bac92b 100644 --- a/scss/elements/_duplicatesMergePane.scss +++ b/scss/elements/_duplicatesMergePane.scss @@ -1,20 +1,37 @@ duplicates-merge-pane { - display: flex; - flex-direction: column; - - groupbox { - margin: 8px 0 0 0; - min-height: 0; + &, #zotero-duplicates-merge-version-select, #zotero-duplicates-merge-item-box-container { + display: flex; + flex-direction: column; + gap: 8px; } - #zotero-duplicates-merge-button - { - font-size: 13px; + padding-top: 9px; + + > groupbox { + // Override default margin/padding that breaks our styles here + margin: 0; + padding: 0; + padding-inline: 8px; + min-height: 0; + + > :is(description, label, button) { + margin: 0; + } + } + + #zotero-duplicates-merge-field-select { + margin-bottom: 9px; } #zotero-duplicates-merge-item-box-container { - overflow-y: auto; - padding: 0 8px; + flex: 1; + padding-inline: 8px; + overflow-y: scroll; + border-top: var(--material-border-quinary); + + collapsible-section > .head { + display: none; + } } /* Show duplicates date list item as selected even when not focused diff --git a/scss/elements/_editableText.scss b/scss/elements/_editableText.scss index f6e52331ad..fddc68cc90 100644 --- a/scss/elements/_editableText.scss +++ b/scss/elements/_editableText.scss @@ -1,11 +1,17 @@ @include comfortable { --editable-text-padding-inline: 4px; --editable-text-padding-block: 4px; + + --editable-text-tight-padding-inline: 4px; + --editable-text-tight-padding-block: 2px; } @include compact { --editable-text-padding-inline: 4px; --editable-text-padding-block: 1px; + + --editable-text-tight-padding-inline: 3px; + --editable-text-tight-padding-block: 1px; } editable-text { @@ -14,15 +20,8 @@ editable-text { --max-visible-lines: 1; &[tight] { - @include comfortable { - --editable-text-padding-inline: 4px; - --editable-text-padding-block: 2px; - } - - @include compact { - --editable-text-padding-inline: 3px; - --editable-text-padding-block: 1px; - } + --editable-text-padding-inline: var(--editable-text-tight-padding-inline); + --editable-text-padding-block: var(--editable-text-tight-padding-block); } // Fun auto-sizing approach from CSSTricks: diff --git a/scss/elements/_itemBox.scss b/scss/elements/_itemBox.scss index 7e33f2d7b2..2c4855ebe9 100644 --- a/scss/elements/_itemBox.scss +++ b/scss/elements/_itemBox.scss @@ -14,6 +14,10 @@ item-box { #info-table { @include meta-table; + + .meta-row .zotero-field-version-button { + padding: 3px; + } } .creator-type-label, #more-creators-label { @@ -169,12 +173,6 @@ item-box { align-self: center; } - /* Merge pane in duplicates view */ - .zotero-field-version-button { - margin: 0; - padding: 0; - } - /* * Retraction box */ diff --git a/scss/elements/_paneHeader.scss b/scss/elements/_paneHeader.scss index b4c5046ae1..b823b11dd4 100644 --- a/scss/elements/_paneHeader.scss +++ b/scss/elements/_paneHeader.scss @@ -1,46 +1,44 @@ pane-header { - display: flex; + &:not([hidden]) { + display: flex; + } flex-direction: column; - align-items: flex-start; - padding: 6px 8px; - gap: 6px; + align-items: stretch; + padding: 8px; border-bottom: 1px solid var(--fill-quinary); - .head { - display: flex; - align-self: stretch; - gap: 4px; + max-height: 25%; + overflow-y: auto; + scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-background); + scrollbar-gutter: stable; + + .title { + margin-top: calc(0px - var(--editable-text-padding-block)); + flex: 1 1 0; + font-weight: 600; + line-height: 1.333; - .title { - align-self: center; - margin-top: calc(0px - var(--editable-text-padding-block)); - padding: 2px 0px 1px 0px; - flex: 1 1 0; - font-weight: 600; - line-height: 1.333; - - editable-text { - flex: 1; - } - } - - .menu-button { - align-self: start; - } - - .menu-button toolbarbutton { - @include svgicon-menu("go-to", "universal", "20"); + editable-text { + flex: 1; } } - .menu-button { - align-self: start; + .creator-year { + color: var(--fill-secondary); } - .menu-button toolbarbutton { - @include svgicon-menu("go-to", "universal", "20"); - --width-focus-border: 2px; - @include focus-ring; + .bib-entry { + line-height: 1.5; + + &.loading { + color: var(--fill-secondary); + } + } + + .creator-year, .bib-entry { + // Set padding to match editable-text in tight mode, plus 1px for border + padding-inline: calc(var(--editable-text-tight-padding-inline) + 1px); + overflow-wrap: anywhere; } .custom-head { diff --git a/scss/preferences/_general.scss b/scss/preferences/_general.scss index e8c69a70dc..db4e45a88f 100644 --- a/scss/preferences/_general.scss +++ b/scss/preferences/_general.scss @@ -18,6 +18,10 @@ height: 16px; } +#item-pane-header-locale-menu { + min-width: 12em; +} + @media (-moz-platform: windows) { button, menulist, radio, checkbox, input { margin-block: 4px; diff --git a/test/tests/itemPaneTest.js b/test/tests/itemPaneTest.js index 2fab81c051..523462a597 100644 --- a/test/tests/itemPaneTest.js +++ b/test/tests/itemPaneTest.js @@ -9,6 +9,138 @@ describe("Item pane", function () { after(function () { win.close(); }); + + describe("Item pane header", function () { + let itemData = { + itemType: 'book', + title: 'Birds - A Primer of Ornithology (Teach Yourself Books)', + creators: [{ + creatorType: 'author', + lastName: 'Hyde', + firstName: 'George E.' + }] + }; + + before(async function () { + await Zotero.Styles.init(); + }); + + after(function () { + Zotero.Prefs.clear('itemPaneHeader'); + Zotero.Prefs.clear('itemPaneHeader.bibEntry.style'); + Zotero.Prefs.clear('itemPaneHeader.bibEntry.locale'); + }); + + it("should be hidden when set to None mode", async function () { + Zotero.Prefs.set('itemPaneHeader', 'none'); + await createDataObject('item', itemData); + assert.isTrue(doc.querySelector('pane-header').hidden); + }); + + it("should show title when set to Title mode", async function () { + Zotero.Prefs.set('itemPaneHeader', 'title'); + let item = await createDataObject('item', itemData); + + assert.isFalse(doc.querySelector('pane-header .title').hidden); + assert.isTrue(doc.querySelector('pane-header .creator-year').hidden); + assert.isTrue(doc.querySelector('pane-header .bib-entry').hidden); + + assert.equal(doc.querySelector('pane-header .title editable-text').value, item.getField('title')); + }); + + it("should show title/creator/year when set to Title/Creator/Year mode", async function () { + Zotero.Prefs.set('itemPaneHeader', 'titleCreatorYear'); + let item = await createDataObject('item', itemData); + item.setField('date', '1962-05-01'); + await item.saveTx(); + + assert.isTrue(doc.querySelector('pane-header .bib-entry').hidden); + assert.isFalse(doc.querySelector('pane-header .title').hidden); + assert.isFalse(doc.querySelector('pane-header .creator-year').hidden); + + assert.equal(doc.querySelector('pane-header .title editable-text').value, item.getField('title')); + let creatorYearText = doc.querySelector('pane-header .creator-year').textContent; + assert.include(creatorYearText, 'Hyde'); + assert.include(creatorYearText, '1962'); + }); + + it("should show bib entry when set to Bibliography Entry mode", async function () { + Zotero.Prefs.set('itemPaneHeader', 'bibEntry'); + Zotero.Prefs.set('itemPaneHeader.bibEntry.style', 'http://www.zotero.org/styles/apa'); + await createDataObject('item', itemData); + + assert.isFalse(doc.querySelector('pane-header .bib-entry').hidden); + assert.isTrue(doc.querySelector('pane-header .title').hidden); + assert.isTrue(doc.querySelector('pane-header .creator-year').hidden); + + let bibEntry = doc.querySelector('pane-header .bib-entry').shadowRoot.firstElementChild.textContent; + assert.equal(bibEntry.trim(), 'Hyde, G. E. (n.d.). Birds—A Primer of Ornithology (Teach Yourself Books).'); + }); + + it("should update bib entry on item change when set to Bibliography Entry mode", async function () { + Zotero.Prefs.set('itemPaneHeader', 'bibEntry'); + Zotero.Prefs.set('itemPaneHeader.bibEntry.style', 'http://www.zotero.org/styles/apa'); + let item = await createDataObject('item', itemData); + + let bibEntryElem = doc.querySelector('pane-header .bib-entry').shadowRoot.firstElementChild; + + assert.equal(bibEntryElem.textContent.trim(), 'Hyde, G. E. (n.d.). Birds—A Primer of Ornithology (Teach Yourself Books).'); + + item.setField('date', '1962-05-01'); + await item.saveTx(); + assert.equal(bibEntryElem.textContent.trim(), 'Hyde, G. E. (1962). Birds—A Primer of Ornithology (Teach Yourself Books).'); + + item.setCreators([ + { + creatorType: 'author', + lastName: 'Smith', + firstName: 'John' + } + ]); + await item.saveTx(); + assert.equal(bibEntryElem.textContent.trim(), 'Smith, J. (1962). Birds—A Primer of Ornithology (Teach Yourself Books).'); + + item.setField('title', 'Birds'); + await item.saveTx(); + assert.equal(bibEntryElem.textContent.trim(), 'Smith, J. (1962). Birds.'); + }); + + it("should update bib entry on style change when set to Bibliography Entry mode", async function () { + Zotero.Prefs.set('itemPaneHeader', 'bibEntry'); + Zotero.Prefs.set('itemPaneHeader.bibEntry.style', 'http://www.zotero.org/styles/apa'); + await createDataObject('item', itemData); + + let bibEntryElem = doc.querySelector('pane-header .bib-entry').shadowRoot.firstElementChild; + + assert.equal(bibEntryElem.textContent.trim(), 'Hyde, G. E. (n.d.). Birds—A Primer of Ornithology (Teach Yourself Books).'); + + Zotero.Prefs.set('itemPaneHeader.bibEntry.style', 'http://www.zotero.org/styles/chicago-author-date'); + assert.equal(bibEntryElem.textContent.trim(), 'Hyde, George E. n.d. Birds - A Primer of Ornithology (Teach Yourself Books).'); + }); + + it("should update bib entry on locale change when set to Bibliography Entry mode", async function () { + Zotero.Prefs.set('itemPaneHeader', 'bibEntry'); + Zotero.Prefs.set('itemPaneHeader.bibEntry.style', 'http://www.zotero.org/styles/apa'); + await createDataObject('item', itemData); + + let bibEntryElem = doc.querySelector('pane-header .bib-entry').shadowRoot.firstElementChild; + + assert.equal(bibEntryElem.textContent.trim(), 'Hyde, G. E. (n.d.). Birds—A Primer of Ornithology (Teach Yourself Books).'); + + Zotero.Prefs.set('itemPaneHeader.bibEntry.locale', 'de-DE'); + assert.equal(bibEntryElem.textContent.trim(), 'Hyde, G. E. (o. J.). Birds—A Primer of Ornithology (Teach Yourself Books).'); + }); + + it("should fall back to Title/Creator/Year when citation style is missing", async function () { + Zotero.Prefs.set('itemPaneHeader', 'bibEntry'); + Zotero.Prefs.set('itemPaneHeader.bibEntry.style', 'http://www.zotero.org/styles/an-id-that-does-not-match-any-citation-style'); + await createDataObject('item', itemData); + + assert.isTrue(doc.querySelector('pane-header .bib-entry').hidden); + assert.isFalse(doc.querySelector('pane-header .title').hidden); + assert.isFalse(doc.querySelector('pane-header .creator-year').hidden); + }); + }); describe("Info pane", function () { it("should refresh on item update", function* () {