diff --git a/chrome/content/zotero-platform/unix/overlay.css b/chrome/content/zotero-platform/unix/overlay.css index 49bc6bd38e..b3a2e95af7 100644 --- a/chrome/content/zotero-platform/unix/overlay.css +++ b/chrome/content/zotero-platform/unix/overlay.css @@ -10,6 +10,11 @@ tab { border-right: 1px solid hsla(0, 0%, 0%, 0.2); border-left: 1px solid hsla(0, 0%, 0%, 0.2); } + +tab:where([visuallyselected="true"]) { + background: Background; +} + /* Fixes misc Ubuntu 16.10 rendering issue fixes. */ #zotero-prefs menulist dropmarker{ width: 30px; diff --git a/chrome/content/zotero/advancedSearch.js b/chrome/content/zotero/advancedSearch.js index b3719a9351..89e6e10ec4 100644 --- a/chrome/content/zotero/advancedSearch.js +++ b/chrome/content/zotero/advancedSearch.js @@ -69,9 +69,7 @@ var ZoteroAdvancedSearch = new function() { columns, }); - // A minimal implementation of Zotero.CollectionTreeRow - var collectionTreeRow = { - view: {}, + this.itemsView.changeCollectionTreeRow({ ref: _searchBox.search, visibilityGroup: 'default', isSearchMode: () => true, @@ -85,8 +83,9 @@ var ZoteroAdvancedSearch = new function() { isFeeds: () => false, isFeedsOrFeed: () => false, isShare: () => false, - isTrash: () => false - }; + isTrash: () => false, + isSearch: () => true + }); this.itemsView.changeCollectionTreeRow(collectionTreeRow); // Focus the first field in the window @@ -101,12 +100,11 @@ var ZoteroAdvancedSearch = new function() { _searchBox.updateSearch(); _searchBox.active = true; - // A minimal implementation of Zotero.CollectionTreeRow - var collectionTreeRow = { - view: {}, + return this.itemsView.changeCollectionTreeRow({ ref: _searchBox.search, visibilityGroup: 'default', isSearchMode: () => true, + isSearch: () => true, getItems: async function () { await Zotero.Libraries.get(_libraryID).waitForDataLoad('item'); @@ -114,20 +112,8 @@ var ZoteroAdvancedSearch = new function() { search.libraryID = _libraryID; var ids = await search.search(); return Zotero.Items.get(ids); - }, - isLibrary: () => false, - isCollection: () => false, - isSearch: () => true, - isPublications: () => false, - isDuplicates: () => false, - isFeed: () => false, - isFeeds: () => false, - isFeedsOrFeed: () => false, - isShare: () => false, - isTrash: () => false - }; - - return this.itemsView.changeCollectionTreeRow(collectionTreeRow); + } + }); } diff --git a/chrome/content/zotero/collectionTree.jsx b/chrome/content/zotero/collectionTree.jsx index 70ab141024..197557cbee 100644 --- a/chrome/content/zotero/collectionTree.jsx +++ b/chrome/content/zotero/collectionTree.jsx @@ -710,6 +710,7 @@ var CollectionTree = class CollectionTree extends LibraryTree { await this.waitForLoad(); + // Check if items from multiple libraries were specified // Check if items from multiple libraries were specified if (items.length > 1 && new Set(items.map(item => item.libraryID)).size > 1) { Zotero.debug("Can't select items in multiple libraries", 2); diff --git a/chrome/content/zotero/components/virtualized-table.jsx b/chrome/content/zotero/components/virtualized-table.jsx index b4931be286..88b74e9423 100644 --- a/chrome/content/zotero/components/virtualized-table.jsx +++ b/chrome/content/zotero/components/virtualized-table.jsx @@ -394,7 +394,7 @@ class VirtualizedTable extends React.Component { getRowCount: PropTypes.func.isRequired, - renderItem: PropTypes.func, + renderItem: PropTypes.func.isRequired, // Row height specified as lines of text per row. Defaults to 1 linesPerRow: PropTypes.number, // Do not adjust for Zotero-defined font scaling @@ -672,7 +672,7 @@ class VirtualizedTable extends React.Component { for (let i = this.selection.focused + 1, checked = 0; checked < rowCount; i++, checked++) { i %= rowCount; let rowString = this.props.getRowString(i); - if (rowString.toLowerCase().indexOf(char) == 0) { + if (rowString && rowString.toLowerCase().indexOf(char) == 0) { if (i != this.selection.focused) { this.scrollToRow(i); this.onSelection(i); @@ -854,8 +854,10 @@ class VirtualizedTable extends React.Component { offset += resizingRect.width; } const widthSum = aRect.width + bRect.width; - const aSpacingOffset = (aColumn.minWidth ? aColumn.minWidth : COLUMN_MIN_WIDTH) + (aColumn.noPadding ? 0 : COLUMN_PADDING); - const bSpacingOffset = (bColumn.minWidth ? bColumn.minWidth : COLUMN_MIN_WIDTH) + (bColumn.noPadding ? 0 : COLUMN_PADDING); + const aColumnPadding = aColumn.iconLabel ? 0 : COLUMN_PADDING; + const bColumnPadding = bColumn.iconLabel ? 0 : COLUMN_PADDING; + const aSpacingOffset = (aColumn.minWidth ? aColumn.minWidth : COLUMN_MIN_WIDTH) + aColumnPadding; + const bSpacingOffset = (bColumn.minWidth ? bColumn.minWidth : COLUMN_MIN_WIDTH) + bColumnPadding; const aColumnWidth = Math.min(widthSum - bSpacingOffset, Math.max(aSpacingOffset, event.clientX - (RESIZER_WIDTH / 2) - offset)); const bColumnWidth = widthSum - aColumnWidth; let onResizeData = {}; @@ -1003,7 +1005,7 @@ class VirtualizedTable extends React.Component { } _handleColumnDragStart = (index, event) => { - if (event.button !== 0) return false; + if (event.button !== 0 || this.props.staticColumns) return false; // Remember for sorting this._headerMouseDownIndex = index; this.setState({ draggingColumn: index }); @@ -1223,7 +1225,8 @@ class VirtualizedTable extends React.Component { + {...(column.iconLabel ? { title: columnName } : {})} + > {label} {sortIndicator} @@ -1630,6 +1633,7 @@ var Columns = class { } const column = this._columns.find(column => column.dataKey == dataKey); const styleIndex = this._columnStyleMap[dataKey]; + const columnPadding = column.iconLabel ? 0 : COLUMN_PADDING; if (storePrefs && !column.fixedWidth) { column.width = width; prefs[dataKey] = this._getColumnPrefsToPersist(column); @@ -1642,7 +1646,7 @@ var Columns = class { this._stylesheet.sheet.cssRules[styleIndex].style.setProperty('max-width', `${width}px`, 'important'); this._stylesheet.sheet.cssRules[styleIndex].style.setProperty('min-width', `${width}px`, 'important'); } else { - width = (width - COLUMN_PADDING); + width = (width - columnPadding); Zotero.debug(`Columns ${dataKey} width ${width}`); this._stylesheet.sheet.cssRules[styleIndex].style.setProperty('flex-basis', `${width}px`); } @@ -1740,10 +1744,18 @@ var Columns = class { }; function renderCell(index, data, column, dir = null) { - column = column || { columnName: "" }; + column = column || { dataKey: "" }; + if (column.renderer) { + return column.renderer(index, data, column, dir); + } let span = document.createElement('span'); span.className = `cell ${column.className}`; - span.textContent = data; + if (column.type == 'html') { + span.innerHTML = data.replaceAll('&', '&'); + } + else { + span.textContent = data; + } if (dir) span.dir = dir; return span; } @@ -1777,6 +1789,7 @@ function makeRowRenderer(getRowData) { div.classList.toggle('selected', selection.isSelected(index)); div.classList.toggle('focused', selection.focused == index); const rowData = getRowData(index); + div.classList.toggle('highlighted', !!rowData.highlighted); let ariaLabel = ""; if (columns.length) { diff --git a/chrome/content/zotero/elements/bubbleInput.js b/chrome/content/zotero/elements/bubbleInput.js new file mode 100644 index 0000000000..ed3b4aecc6 --- /dev/null +++ b/chrome/content/zotero/elements/bubbleInput.js @@ -0,0 +1,462 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2024 Corporation for Digital Scholarship + Vienna, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +"use strict"; + +{ + class BubbleInput extends XULElementBase { + content = MozXULElement.parseXULToFragment(` + + + `); + + init() { + this._body = this.querySelector('.bubble-input.body'); + this._body.addEventListener('keydown', this._onBodyKeydown.bind(this), { capture: true }); + this._body.addEventListener('click', this._onBodyClick.bind(this)); + this._body.addEventListener('dragenter', event => event.preventDefault()); + this._body.addEventListener('dragover', (event) => { + event.preventDefault(); + if (event.target === this._body) { + const { lastBubble: lastBeforeDrop } = this._getLastBubbleBeforePoint(event.clientX, event.clientY); + const lastBubble = this._body.querySelector('.bubble:last-of-type'); + if (lastBubble !== lastBeforeDrop) return; + // Only drop to final position if dragging after all bubbles, not in-between + this._dragOver?.classList.remove('drop-after', 'drop-before'); + this._dragOver = event.target; + lastBubble.classList.add('drop-after'); + } + }); + this._body.addEventListener('dragleave', (event) => { + this._dragOver?.classList.remove('drop-after', 'drop-before'); + this._dragOver = null; + }); + this._body.addEventListener('drop', (event) => { + event.preventDefault(); + event.stopPropagation(); + if (!this._dragBubble || !this._dragOver && this._dragBubble != this._dragOver) return; + + this._dragBubble.remove(); + if (this._dragOver === this._body) { + this._body.querySelector('.bubble:last-of-type').after(this._dragBubble); + } + else { + this._dragOver.before(this._dragBubble); + } + this._dragOver?.classList.remove('drop-after', 'drop-before'); + + // // Find old position in list + // var oldPosition = this._getBubbleIndex(this._dragBubble); + // + // // Move bubble + // var range = document.createRange(); + // // Prevent dragging out of qfe + // if (event.target === qfe) { + // range.setStartAfter(qfe.childNodes[qfe.childNodes.length-1]); + // } + // else { + // range.setStartAfter(event.target); + // } + // dragging.parentNode.removeChild(dragging); + // var bubble = _insertBubble(JSON.parse(dragging.dataset.citationItem), range); + // this._dragBubble = null; + // + // // If moved out of order, turn off "Keep Sources Sorted" + // if(io.sortable && keepSorted && keepSorted.hasAttribute("checked") && oldPosition !== -1 && + // oldPosition != _getBubbleIndex(bubble)) { + // keepSorted.removeAttribute("checked"); + // } + // + // yield _previewAndSort(); + // _moveCursorToEnd(); + }); + this._appendInput(); + + this._dragBubble = null; + this._dragOver = null; + } + + _onBodyClick(event) { + if (event.target !== this._body) { + return; + } + let clickX = event.clientX; + let clickY = event.clientY; + let { lastBubble, startOfTheLine } = this._getLastBubbleBeforePoint(clickX, clickY); + // If click happened right before another input, focus that input + // instead of adding another one. There may be a br node on the way, so we have to check + // more than just the next node. + if (lastBubble) { + let nextNode = lastBubble.nextElementSibling; + while (nextNode && !nextNode.classList.contains("bubble")) { + if (this._isInput(nextNode)) { + nextNode.focus(); + return; + } + nextNode = nextNode.nextElementSibling; + } + } + let newInput = this._createInputElem(); + if (lastBubble !== null) { + lastBubble.after(newInput); + if (startOfTheLine) { + let lineBreak = document.createElement("br"); + lastBubble.after(lineBreak); + } + } + else { + this._body.prepend(newInput); + } + newInput.focus(); + } + + _onBodyKeydown(event) { + const focused = document.activeElement; + if (this._isInput(focused) && event.key == "Enter") { + this.convertInputToBubble(); + } + else if (["ArrowLeft", "ArrowRight"].includes(event.key) && !event.shiftKey) { + // On arrow left from the beginning of the input, move to previous bubble + if (event.key === "ArrowLeft" && (!this._isInput(focused) || focused.selectionStart === 0)) { + this._moveFocusBack(focused); + event.preventDefault(); + } + // On arrow right from the end of the input, move to next bubble + else if (event.key === "ArrowRight" && (!this._isInput(focused) || focused.selectionStart === focused.value.length)) { + this._moveFocusForward(focused); + event.preventDefault(); + } + } + else if (this._isInput(focused) && ["Backspace", "Delete"].includes(event.key) + && (focused.selectionStart + focused.selectionEnd) === 0) { + event.preventDefault(); + // Backspace/Delete from the beginning of an input will delete the previous bubble. + // If there are two inputs next to each other as a result, they are merged + if (this.previousElementSibling) { + this.previousElementSibling.remove(); + this._combineNeighboringInputs(); + } + } + } + + convertInputToBubble(text) { + const input = this.getLastInput(); + text = text || input.value; + const bubble = this._createBubble(text); + input.before(bubble); + input.value = ""; + return bubble; + } + + _createBubble(str) { + let bubble = document.createElement("div"); + bubble.setAttribute("draggable", "true"); + bubble.setAttribute("role", "button"); + bubble.setAttribute("tabindex", "0"); + bubble.setAttribute("aria-describedby", "bubble-description"); + bubble.setAttribute("aria-haspopup", true); + bubble.className = "bubble"; + // VoiceOver works better without it + if (!Zotero.isMac) { + bubble.setAttribute("aria-label", str); + } + // bubble.addEventListener("click", _onBubbleClick); + bubble.addEventListener("dragstart", (event) => { + this._dragBubble = event.currentTarget; + event.dataTransfer.setData("text/plain", ''); + event.stopPropagation(); + }); + bubble.addEventListener("dragover", (event) => { + this._dragOver?.classList.remove('drop-after', 'drop-before'); + this._dragOver = bubble; + bubble.classList.add('drop-before'); + }); + // bubble.addEventListener("dragend", onBubbleDragEnd); + // bubble.addEventListener("keypress", onBubblePress); + // bubble.addEventListener("mousedown", (_) => { + // _bubbleMouseDown = true; + // }); + // bubble.addEventListener("mouseup", (_) => { + // _bubbleMouseDown = false; + // }); + // bubble.dataset.citationItem = JSON.stringify(citationItem); + let text = document.createElement("span"); + text.textContent = str; + text.className = "text"; + bubble.append(text); + + let cross = document.createElement("div"); + cross.className = "cross"; + cross.addEventListener("click", () => { + bubble.remove(); + }); + cross.addEventListener("keydown", (e) => { + if (e.target === cross && e.key === "Enter") { + bubble.remove(); + } + }); + bubble.append(cross); + + return bubble; + } + + _isInput(node) { + if (!node) return false; + return node.tagName === "input"; + } + + // Determine if the input is empty + _isInputEmpty(input) { + if (!input) { + return true; + } + return input.value.length == 0; + } + + getLastInput() { + return this.getCurrentInput() || this._lastFocusedInput; + } + + getCurrentInput() { + if (this._isInput(document.activeElement)) { + return document.activeElement; + } + return false; + } + + // Create input in the end of the editor and focus it + _appendInput() { + let newInput = this._createInputElem(); + this._body.appendChild(newInput); + setTimeout(() => newInput.focus()); + return newInput; + } + + isEmpty() { + return this._body.childElementCount == 1 && this._isInput(this._body.firstChild); + } + + // If this input field was counted as previously focused, + // it will be cleared. Call before removing the field + _clearLastFocused(input) { + if (input == this._lastFocusedInput) { + this._lastFocusedInput = null; + } + } + + _getContentWidth(input) { + let span = document.createElement("span"); + span.classList = "input"; + span.innerText = input.value; + this._body.appendChild(span); + let spanWidth = span.getBoundingClientRect().width; + span.remove(); + return spanWidth + 2; + } + + _createInputElem() { + let input = document.createElement('input'); + input.setAttribute("aria-describedby", "input-description"); + input.addEventListener("input", (_) => { + // _resetSearchTimer(); + // Expand/shrink the input field to match the width of content + let width = this._getContentWidth(input); + input.style.width = width + 'px'; + }); + // input.addEventListener("keypress", onInputPress); + input.addEventListener("keypress", e => this._onInputKeypress(input, e)); + // input.addEventListener("paste", _onPaste, false); + input.addEventListener("focus", (_) => { + // // If the input used for the last search run is refocused, + // // just make sure the reference panel is opened if it has items. + // if (this._lastFocusedInput == input && referenceBox.childElementCount > 0) { + // _openReferencePanel(); + // return; + // } + // // Otherwise, run the search if the input is non-empty. + // if (!isInputEmpty(input)) { + // _resetSearchTimer(); + // } + // else { + // _updateItemList({ citedItems: [] }); + // } + this._lastFocusedInput = input; + }); + // // Delete empty input on blur unless it's the last input + input.addEventListener("blur", (_) => { + // Timeout to know where the focus landed after + setTimeout(() => { + const inputFocused = this._isInput(document.activeElement); + if (this._isInputEmpty(input) && inputFocused) { + // // Resizing window right before drag-drop reordering starts, will interrupt the + // // drag event. To avoid it, hide the input immediately and delete it after delay. + // if (_bubbleMouseDown) { + // input.style.display = "none"; + // setTimeout(() => { + // input.remove(); + // }, 500); + // clearLastFocused(input); + // } + // else + if (document.activeElement !== input && !this.isEmpty()) { + // If no dragging, delete it if focus has moved elsewhere. + // If focus remained, the entire dialog lost focus, so do nothing + // If this is the last, non-removable, input - do not remove it as well. + input.remove(); + this._clearLastFocused(input); + } + } + }); + // If there was a br added before input so that it doesn't appear on the previous line, + // remove it + if (input.previousElementSibling?.tagName == "br") { + input.previousElementSibling.remove(); + } + }); + return input; + } + + _onInputKeypress(input, event) { + if (event.target === input && event.key == "Enter") { + this.convertInputToBubble(); + } + else if (["ArrowLeft", "ArrowRight"].includes(event.key) && !event.shiftKey) { + // On arrow left from the beginning of the input, move to previous bubble + if (event.key === "ArrowLeft" && input.selectionStart === 0) { + this._moveFocusBack(input); + event.preventDefault(); + } + // On arrow right from the end of the input, move to next bubble + else if (event.key === "ArrowRight" && input.selectionStart === input.value.length) { + this._moveFocusForward(input); + event.preventDefault(); + } + } + else if (["Backspace", "Delete"].includes(event.key) + && (input.selectionStart + input.selectionEnd) === 0) { + event.preventDefault(); + // Backspace/Delete from the beginning of an input will delete the previous bubble. + // If there are two inputs next to each other as a result, they are merged + if (this.previousElementSibling) { + this.previousElementSibling.remove(); + this._combineNeighboringInputs(); + } + } + } + + _moveFocusForward(node) { + if (node.nextElementSibling?.focus) { + node.nextElementSibling.focus(); + return true; + } + return false; + } + + _moveFocusBack(node) { + // Skip line break if it's before the node + if (node.previousElementSibling?.tagName == "br") { + node = node.previousElementSibling; + } + if (node.previousElementSibling?.focus) { + node.previousElementSibling.focus(); + return true; + } + return false; + } + + // If a bubble is removed between two inputs we need to combine them + _combineNeighboringInputs() { + let node = this._body.firstChild; + while (node && node.nextElementSibling) { + if (this._isInput(node) + && this._isInput(node.nextElementSibling)) { + let secondInputValue = node.nextElementSibling.value; + node.value += ` ${secondInputValue}`; + node.dispatchEvent(new Event('input', { bubbles: true })); + // Make sure focus is not lost when two inputs are combined + if (node.nextElementSibling == document.activeElement) { + node.focus(); + } + node.nextElementSibling.remove(); + } + node = node.nextElementSibling; + } + } + + _getBubbleIndex(bubble) { + return this.body.querySelectorAll('.bubble').indexOf(bubble); + } + + /** + * Find the last bubble (lastBubble) before a given coordinate and indicate if there are no bubbles + * to the left of the x-coordinate (startOfTheLine). If there is no last bubble, null is returned. + * startOfTheLine indicates if a
should be added so that a new input placed after lastBubble + * does not land on the previous line. + * Outputs for a sample of coordinates (with #3 having startOfTheLine=true): + * NULL #1 #2 #3 + * ↓ ↓ ↓ ↓ + * [ bubble_1 bubble_2 bubble_3 + * bubble_4, bubble_5 ] + * ↑ ↑ ↑ ↑ + * #3 #4 #5 #5 + * @param {Int} x - X coordinate + * @param {Int} y - Y coordinate + * @returns {lastBubble: Node, startOfTheLine: Bool} + */ + _getLastBubbleBeforePoint(x, y) { + let bubbles = this._body.querySelectorAll('.bubble'); + let lastBubble = null; + let startOfTheLine = false; + for (let i = 0; i < bubbles.length; i++) { + let rect = bubbles[i].getBoundingClientRect(); + // If within the vertical range of a bubble + if (y >= rect.top && y <= rect.bottom) { + // If the click is to the right of a bubble, it becomes a candidate + if (x > rect.right) { + lastBubble = i; + } + // Otherwise, stop and return the last bubble we saw if any + else { + if (i == 0) { + lastBubble = null; + } + else { + // Indicate there is no bubble before this one + startOfTheLine = lastBubble === null; + lastBubble = Math.max(i - 1, 0); + } + break; + } + } + } + if (lastBubble !== null) { + lastBubble = bubbles[lastBubble]; + } + return { lastBubble: lastBubble, startOfTheLine: startOfTheLine }; + } + } + + customElements.define('bubble-input', BubbleInput); +} \ No newline at end of file diff --git a/chrome/content/zotero/integration/citationDialog.js b/chrome/content/zotero/integration/citationDialog.js new file mode 100644 index 0000000000..d0c58e6e92 --- /dev/null +++ b/chrome/content/zotero/integration/citationDialog.js @@ -0,0 +1,24 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2024 Corporation for Digital Scholarship + Vienna, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ diff --git a/chrome/content/zotero/integration/citationDialog.xhtml b/chrome/content/zotero/integration/citationDialog.xhtml new file mode 100644 index 0000000000..29cfd20366 --- /dev/null +++ b/chrome/content/zotero/integration/citationDialog.xhtml @@ -0,0 +1,77 @@ + + + + + + + + &zotero.integration.quickFormatDialog.title; + + + + + +
+
+
+
Citations
+
+ +
+
+
+
+ + +
+
+
+
+
+ + + + + + + + +
+ +
+
+
+
+ + +
+
+
+
+
+ + diff --git a/chrome/content/zotero/integration/quickFormat.js b/chrome/content/zotero/integration/quickFormat.js index 88a4a83faf..7ad3e09b2f 100644 --- a/chrome/content/zotero/integration/quickFormat.js +++ b/chrome/content/zotero/integration/quickFormat.js @@ -2336,23 +2336,7 @@ var Zotero_QuickFormat = new function () { this.showInLibrary = async function (itemID) { let citationItem = JSON.parse(panelRefersToBubble?.dataset.citationItem || "{}"); var id = itemID || citationItem.id; - var pane = Zotero.getActiveZoteroPane(); - // Open main window if it's not open (Mac) - if (!pane) { - let win = Zotero.openMainWindow(); - await new Zotero.Promise((resolve) => { - let onOpen = function () { - win.removeEventListener('load', onOpen); - resolve(); - }; - win.addEventListener('load', onOpen); - }); - pane = win.ZoteroPane; - } - pane.selectItem(id); - - // Pull window to foreground - Zotero.Utilities.Internal.activate(pane.document.defaultView); + Zotero.Utilities.Internal.showInLibrary(id); } /** diff --git a/chrome/content/zotero/itemTree.jsx b/chrome/content/zotero/itemTree.jsx index a4f3bd7d5e..28e2350275 100644 --- a/chrome/content/zotero/itemTree.jsx +++ b/chrome/content/zotero/itemTree.jsx @@ -53,6 +53,23 @@ const CHILD_INDENT = 16; const COLORED_TAGS_RE = new RegExp("^(?:Numpad|Digit)([0-" + Zotero.Tags.MAX_COLORED_TAGS + "]{1})$"); const COLUMN_PREFS_FILEPATH = OS.Path.join(Zotero.Profile.dir, "treePrefs.json"); const ATTACHMENT_STATE_LOAD_DELAY = 150; //ms +const STUB_COLLECTION_TREE_ROW = { + view: {}, + ref: {}, + visibilityGroup: "", + isSearchMode: () => false, + getItems: async () => [], + isLibrary: () => false, + isCollection: () => false, + isSearch: () => false, + isPublications: () => false, + isDuplicates: () => false, + isFeed: () => false, + isFeeds: () => false, + isFeedsOrFeed: () => false, + isShare: () => false, + isTrash: () => false +}; var ItemTree = class ItemTree extends LibraryTree { static async init(domEl, opts={}) { @@ -83,6 +100,9 @@ var ItemTree = class ItemTree extends LibraryTree { dragAndDrop: false, persistColumns: false, columnPicker: false, + regularOnly: false, + multiSelect: true, + shouldListenForNotifications: true, columns: COLUMNS, onContextMenu: noop, onActivate: noop, @@ -96,6 +116,9 @@ var ItemTree = class ItemTree extends LibraryTree { dragAndDrop: PropTypes.bool, persistColumns: PropTypes.bool, columnPicker: PropTypes.bool, + regularOnly: PropTypes.bool, + multiSelect: PropTypes.bool, + shouldListenForNotifications: PropTypes.bool, columns: PropTypes.array, onSelectionChange: PropTypes.func, onContextMenu: PropTypes.func, @@ -117,18 +140,21 @@ var ItemTree = class ItemTree extends LibraryTree { this._introText = null; this._rowCache = {}; + this._highlightedRows = new Set(); this._modificationLock = Zotero.Promise.resolve(); this._refreshPromise = Zotero.Promise.resolve(); this._dropRow = null; - this._unregisterID = Zotero.Notifier.registerObserver( - this, - ['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem', 'search', 'itemtree', 'collection'], - 'itemTreeView', - 50 - ); + if (props.shouldListenForNotifications) { + this._unregisterID = Zotero.Notifier.registerObserver( + this, + ['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem', 'search', 'itemtree', 'collection'], + 'itemTreeView', + 50 + ); + } this._itemsPaneMessage = null; @@ -245,7 +271,7 @@ var ItemTree = class ItemTree extends LibraryTree { // TEMP: Hide annotations newSearchItems = newSearchItems.filter(item => !item.isAnnotation()); // Remove notes and attachments if necessary - if (this.regularOnly) { + if (this.props.regularOnly) { newSearchItems = newSearchItems.filter((item) => { return item instanceof Zotero.Collection || item instanceof Zotero.Search @@ -257,12 +283,11 @@ var ItemTree = class ItemTree extends LibraryTree { let itemsToAdd = newSearchItems.filter(item => this._rowMap[item.treeViewID] === undefined); // Find the parents of search matches let newSearchParentIDs = new Set( - this.regularOnly + this.props.regularOnly ? [] : newSearchItems.filter(item => !!item.parentItemID).map(item => item.parentItemID) ); this._searchParentIDs = newSearchParentIDs; - newSearchItems = new Set(newSearchItems); var newCellTextCache = {}; var newSearchMode = this.collectionTreeRow.isSearchMode(); @@ -286,7 +311,7 @@ var ItemTree = class ItemTree extends LibraryTree { } let isSearchParent = newSearchParentIDs.has(row.ref.treeViewID); // If not showing children or no children match the search, close - if (this.regularOnly || !isSearchParent) { + if (this.props.regularOnly || !isSearchParent) { row.isOpen = false; skipChildren = true; } @@ -294,7 +319,7 @@ var ItemTree = class ItemTree extends LibraryTree { skipChildren = false; } // Skip items that don't match the search and don't have children that do - if (!newSearchItems.has(row.ref) && !isSearchParent) { + if (!newSearchItemIDs.has(row.ref.id) && !isSearchParent) { continue; } } @@ -907,13 +932,10 @@ var ItemTree = class ItemTree extends LibraryTree { // view (e.g., as triggered by itemsView.runListeners('select') in ZoteroPane::itemSelected()) // before returning. This guarantees that changes are reflected in the middle and right-hand panes // before returning from the save transaction. - // - // If no onselect handler is set on the tree element, as is the case in the Advanced Search window, - // the select listeners never get called, so don't wait. - if (reselect && this.props.onSelectionChange) { + if (reselect) { var selectPromise = this.waitForSelect(); + // Triggers reselect on the item tree and fires a select event this.selection.selectEventsSuppressed = false; - Zotero.debug("Yielding for select promise"); // TEMP return selectPromise; } else { @@ -1104,6 +1126,16 @@ var ItemTree = class ItemTree extends LibraryTree { } async changeCollectionTreeRow(collectionTreeRow) { + // When used outside the Zotero Pane, "collectionTreeRow" is not an actual + // tree row being passed in, but rather a simple object that defines necessary properties. + // So we need to supplement other CollectionTreeRow properties so that itemTree code + // does not complain. + // Obviously this is not ideal and would be best refactored so that collection + // tree row dependencies are specified separately + // and there's a separate method to refresh the items. + if (collectionTreeRow.constructor.name == "Object") { + collectionTreeRow = Object.assign({}, STUB_COLLECTION_TREE_ROW, collectionTreeRow); + } if (this._locked) return; if (!collectionTreeRow) { this.tree = null; @@ -1113,7 +1145,10 @@ var ItemTree = class ItemTree extends LibraryTree { Zotero.debug(`itemTree.changeCollectionTreeRow(): ${collectionTreeRow.id}`); this._itemTreeLoadingDeferred = Zotero.Promise.defer(); this.setItemsPaneMessage(Zotero.getString('pane.items.loading')); - let newId = "item-tree-" + this.props.id + "-" + collectionTreeRow.visibilityGroup; + let newId = "item-tree-" + this.props.id; + if (collectionTreeRow.visibilityGroup) { + newId += "-" + collectionTreeRow.visibilityGroup; + } if (this.id != newId && this.props.persistColumns) { await this._writeColumnPrefsToFile(true); this.id = newId; @@ -1491,7 +1526,10 @@ var ItemTree = class ItemTree extends LibraryTree { fieldB = creatorSortCache[bItemID]; var prop = sortCreatorAsString ? 'firstCreator' : 'sortCreator'; var sortStringA = itemA[prop]; + // Unsaved items like those embedded in documents + if (!sortStringA) sortStringA = itemA.getField('firstCreator'); var sortStringB = itemB[prop]; + if (!sortStringB) sortStringB = itemB.getField('firstCreator'); if (fieldA === undefined) { let firstCreator = Zotero.Items.getSortTitle(sortStringA); fieldA = firstCreator; @@ -1633,6 +1671,14 @@ var ItemTree = class ItemTree extends LibraryTree { this.ensureRowIsVisible(indices[0] + maxBuffer); } + async setHighlightedRows(ids) { + if (!Array.isArray(ids)) { + return; + } + this._highlightedRows = new Set(ids); + this.tree.invalidate(); + } + toggleOpenState = async (index, skipRowMapRefresh=false) => { // Shouldn't happen but does if an item is dragged over a closed // container until it opens and then released, since the container @@ -1800,7 +1846,11 @@ var ItemTree = class ItemTree extends LibraryTree { } getRowString(index) { - return this.getCellText(index, this.getSortField()) + let row = this.getRow(index); + if (row.ref.isFeedItem) { + return this.getCellText(index, 'title'); + } + return this.getCellText(index, this.getSortField()); } async deleteSelection(force) { @@ -2014,7 +2064,7 @@ var ItemTree = class ItemTree extends LibraryTree { }; isContainerEmpty = (index) => { - if (this.regularOnly) { + if (this.props.regularOnly) { return true; } @@ -3056,6 +3106,7 @@ var ItemTree = class ItemTree extends LibraryTree { const rowData = this._getRowData(index); div.classList.toggle('context-row', !!rowData.contextRow); div.classList.toggle('unread', !!rowData.unread); + div.classList.toggle('highlighted', this._highlightedRows.has(rowData.id)); if (this._dropRow == index) { let span; if (Zotero.DragDrop.currentOrientation != 0) { @@ -3204,6 +3255,7 @@ var ItemTree = class ItemTree extends LibraryTree { } let row = { + id: itemID, // Not a collection or search in the trash isItem: treeRow.ref instanceof Zotero.Item }; @@ -3406,7 +3458,7 @@ var ItemTree = class ItemTree extends LibraryTree { let hasDefaultIn = columns.some(column => 'defaultIn' in column); for (let column of columns) { if (this.props.persistColumns) { - if (column.disabledIn && column.disabledIn.includes(visibilityGroup)) continue;; + if (column.disabledIn && column.disabledIn.includes(visibilityGroup)) continue; const columnSettings = columnsSettings[column.dataKey]; if (!columnSettings && this.id === 'main') { column = this._setLegacyColumnSettings(column); @@ -3414,10 +3466,6 @@ var ItemTree = class ItemTree extends LibraryTree { // Also includes a `hidden` pref and overrides the above if available column = Object.assign({}, column, columnSettings || {}); - - if (column.sortDirection) { - this._sortedColumn = column; - } // If column does not have an "ordinal" field it means it // is newly added if (!("ordinal" in column)) { @@ -3429,13 +3477,16 @@ var ItemTree = class ItemTree extends LibraryTree { } // Initial hidden value if (!("hidden" in column)) { - if (hasDefaultIn) { + if (hasDefaultIn && visibilityGroup) { column.hidden = !(column.defaultIn && column.defaultIn.includes(visibilityGroup)); } else { column.hidden = false; } } + if (column.sortDirection) { + this._sortedColumn = column; + } this._columns.push(column); } @@ -3919,7 +3970,10 @@ var ItemTreeRow = function(ref, level, isOpen) ItemTreeRow.prototype.getField = function(field, unformatted) { - if (!Zotero.ItemTreeManager.isCustomColumn(field)) { + if (this.ref.hasOwnProperty(field) && this.ref[field] != null){ + return this.ref[field]; + } + else if (!Zotero.ItemTreeManager.isCustomColumn(field)) { return this.ref.getField(field, unformatted, true); } return Zotero.ItemTreeManager.getCustomCellData(this.ref, field); diff --git a/chrome/content/zotero/itemTreeColumns.jsx b/chrome/content/zotero/itemTreeColumns.jsx index bf30f1b033..20c185eece 100644 --- a/chrome/content/zotero/itemTreeColumns.jsx +++ b/chrome/content/zotero/itemTreeColumns.jsx @@ -40,7 +40,6 @@ const Icons = require('components/icons'); * @property {string} [width] - A column width instead of flex ratio. See above. * @property {boolean} [fixedWidth] - Default: false. Set to true to disable column resizing * @property {boolean} [staticWidth] - Default: false. Set to true to prevent columns from changing width when the width of the tree increases or decreases - * @property {boolean} [noPadding] - Set to true for columns with padding disabled in stylesheet * @property {number} [minWidth] - Override the default [20px] column min-width for resizing * @property {React.Component} [iconLabel] - Set an Icon label instead of a text-based one * @property {string} [iconPath] - Set an Icon path, overrides {iconLabel} @@ -338,7 +337,6 @@ const COLUMNS = [ iconLabel: , fixedWidth: true, width: "32", - noPadding: true, zoteroPersist: ["hidden", "sortDirection"] }, { @@ -350,7 +348,6 @@ const COLUMNS = [ width: "26", minWidth: 26, staticWidth: true, - noPadding: true, zoteroPersist: ["width", "hidden", "sortDirection"] }, { diff --git a/chrome/content/zotero/libraryTree.js b/chrome/content/zotero/libraryTree.js index a60cbb6538..a749c0dc81 100644 --- a/chrome/content/zotero/libraryTree.js +++ b/chrome/content/zotero/libraryTree.js @@ -194,9 +194,12 @@ var LibraryTree = class LibraryTree extends React.Component { this._rowMap = rowMap; } - _onSelectionChange = () => { - if (!this._uninitialized) { - this.props.onSelectionChange && this.props.onSelectionChange(this.selection); + _onSelectionChange = async () => { + if (!this._uninitialized && this.props.onSelectionChange) { + try { + await Zotero.Promise.resolve(this.props.onSelectionChange(this.selection)); + } catch (e) {} + this.runListeners('select'); } } diff --git a/chrome/content/zotero/selectItemsDialog.js b/chrome/content/zotero/selectItemsDialog.js index ce72d66476..b2f2069f3b 100644 --- a/chrome/content/zotero/selectItemsDialog.js +++ b/chrome/content/zotero/selectItemsDialog.js @@ -69,12 +69,8 @@ var doLoad = async function () { Zotero_Bibliography_Dialog.treeItemSelected(); } else if (isAddEditItemsDialog) { - onItemSelected(); Zotero_Citation_Dialog.treeItemSelected(); } - else { - onItemSelected(); - } }, onActivate: () => { document.querySelector('dialog').acceptDialog(); @@ -83,15 +79,15 @@ var doLoad = async function () { dragAndDrop: false, persistColumns: true, columnPicker: true, - emptyMessage: Zotero.getString('pane.items.loading'), - multiSelect: !io.singleSelection + multiSelect: io.multiSelect, + emptyMessage: Zotero.getString('pane.items.loading') }); itemsView.setItemsPaneMessage(Zotero.getString('pane.items.loading')); const filterLibraryIDs = false || io.filterLibraryIDs; const hideSources = io.hideCollections || ['duplicates', 'trash', 'feeds']; collectionsView = await CollectionTree.init(document.getElementById('zotero-collections-tree'), { - onSelectionChange: Zotero.Utilities.debounce(() => onCollectionSelected(), 100), + onSelectionChange: () => onCollectionSelected(), filterLibraryIDs, hideSources }); @@ -161,8 +157,6 @@ var onCollectionSelected = async function () { await itemsView.changeCollectionTreeRow(collectionTreeRow); itemsView.clearItemsPaneMessage(); - - collectionsView.runListeners('select'); }; function onSearch() @@ -174,25 +168,6 @@ function onSearch() } } -function onItemSelected() -{ - itemsView.runListeners('select'); - if (io.onlyRegularItems) { - // Disable "accept" button if a top-level item isn't selected - let selected = itemsView.getSelectedItems(); - let disableAccept = (selected && !selected.every(item => item.isRegularItem())); - // TEMP: Disable the button directly only as long as we move the button box in doLoad(). - // Then, we should set buttondisabledaccept attribute on the dialog - if (disableAccept) { - document.querySelector("dialog button[dlgtype='accept']").setAttribute("disabled", true); - } - else { - // Remove disabled attribute since the stylesheet looks at disabled attribute - document.querySelector("dialog button[dlgtype='accept']").removeAttribute("disabled"); - } - } -} - function doAccept() { io.dataOut = itemsView.getSelectedItems(true); } diff --git a/chrome/content/zotero/xpcom/connector/server_connector.js b/chrome/content/zotero/xpcom/connector/server_connector.js index 68d7a968f5..b3d5f7db29 100644 --- a/chrome/content/zotero/xpcom/connector/server_connector.js +++ b/chrome/content/zotero/xpcom/connector/server_connector.js @@ -1700,6 +1700,7 @@ Zotero.Server.Connector.Ping.prototype = { prefs: { automaticSnapshots: Zotero.Prefs.get('automaticSnapshots'), googleDocsAddNoteEnabled: true, + googleDocsCitationExplorerEnabled: true, translatorsHash, sortedTranslatorHash } diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 995514f7fd..64d80fb569 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -432,7 +432,7 @@ Zotero.Integration = new function() { } } - Zotero.Utilities.Internal.activate(); + Zotero.Utilities.Internal.activate(Zotero.Integration.currentWindow); let ps = Services.prompt; if (e instanceof Zotero.Exception.Alert) { ps.alert(null, Zotero.getString('integration.error.title'), displayError); @@ -569,7 +569,7 @@ Zotero.Integration = new function() { // and display wrong field types in doc preferences. if (!session || session.agent != agent) { session = new Zotero.Integration.Session(doc, app); - session.reload = true; + session.rebuildCiteprocState = true; } session.agent = agent; session._doc = doc; @@ -581,6 +581,7 @@ Zotero.Integration = new function() { session._deleteFields = {}; session._bibliographyFields = []; session._shouldMerge = false; + session._transactionUpToDate = false; if (dataString == EXPORTED_DOCUMENT_MARKER) { Zotero.Integration.currentSession = session; @@ -708,7 +709,7 @@ Zotero.Integration.Interface.prototype.addCitation = async function () { let citations = await this._session.cite(null); if (this._session.data.prefs.delayCitationUpdates) { for (let citation of citations) { - await this._session.writeDelayedCitation(citation._field, citation); + await this._session.writeDelayedCitation(citation.field, citation); } } else { @@ -741,7 +742,7 @@ Zotero.Integration.Interface.prototype.addEditCitation = async function (docFiel let citations = await this._session.cite(docField); if (this._session.data.prefs.delayCitationUpdates) { for (let citation of citations) { - await this._session.writeDelayedCitation(citation._field, citation); + await this._session.writeDelayedCitation(citation.field, citation); } } else { return this._session.updateDocument(FORCE_CITATIONS_FALSE, false, false); @@ -763,7 +764,7 @@ Zotero.Integration.Interface.prototype.addNote = async function () { let citations = await this._session.cite(null, true); if (this._session.data.prefs.delayCitationUpdates) { for (let citation of citations) { - await this._session.writeDelayedCitation(citation._field, citation); + await this._session.writeDelayedCitation(citation.field, citation); } } else { @@ -789,7 +790,7 @@ Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutin yield field.clearCode(); if(this._session.data.prefs.delayCitationUpdates) { // Refreshes citeproc state before proceeding - this._session.reload = true; + this._session.rebuildCiteprocState = true; citationsMode = FORCE_CITATIONS_REGENERATE; } yield this._session.updateFromDocument(citationsMode); @@ -822,7 +823,7 @@ Zotero.Integration.Interface.prototype.editBibliography = Zotero.Promise.corouti var citationsMode = FORCE_CITATIONS_FALSE; if(this._session.data.prefs.delayCitationUpdates) { // Refreshes citeproc state before proceeding - this._session.reload = true; + this._session.rebuildCiteprocState = true; citationsMode = FORCE_CITATIONS_REGENERATE; } yield this._session.updateFromDocument(citationsMode); @@ -861,14 +862,31 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro var citationsMode = FORCE_CITATIONS_FALSE; if(this._session.data.prefs.delayCitationUpdates) { // Refreshes citeproc state before proceeding - this._session.reload = true; + this._session.rebuildCiteprocState = true; citationsMode = FORCE_CITATIONS_REGENERATE; } yield this._session.updateFromDocument(citationsMode); - if (!newBibliography) yield this._session.editBibliography(bibliography); + if (!newBibliography) { + yield this._session.editBibliography(bibliography); + } yield this._session.updateDocument(citationsMode, true, false); }); +Zotero.Integration.Interface.prototype.citationExplorer = async function () { + await this._session.init(true, false); + + var citationsMode = FORCE_CITATIONS_FALSE; + if(this._session.data.prefs.delayCitationUpdates) { + // Refreshes citeproc state before proceeding + this._session.rebuildCiteprocState = true; + citationsMode = FORCE_CITATIONS_REGENERATE; + } + await this._session.updateFromDocument(citationsMode); + await this._session.openCitationExplorer(); + return this._session.updateDocument(citationsMode, true, false); +} + + /** * Updates the citation data for all citations and bibliography entries. * @return {Promise} @@ -877,7 +895,7 @@ Zotero.Integration.Interface.prototype.refresh = async function() { await this._session.init(true, false); this._session._shouldMerge = true; - this._session.reload = this._session.reload || this._session.data.prefs.delayCitationUpdates; + this._session.rebuildCiteprocState = this._session.rebuildCiteprocState || this._session.data.prefs.delayCitationUpdates; await this._session.updateFromDocument(FORCE_CITATIONS_REGENERATE); await this._session.updateDocument(FORCE_CITATIONS_REGENERATE, true, false); } @@ -1003,7 +1021,17 @@ Zotero.Integration.Session = function(doc, app) { this.secondaryFieldType = app.secondaryFieldType; this.outputFormat = app.outputFormat || 'rtf'; this._dontActivateDocument = false; + + // Set to true upon updateFromDocument() + // Changes to false after delayed citation insert + // updateFromDocument() not called for most operations in delayed update mode this._sessionUpToDate = false; + // Set to true upon updateFromDocument() + // Changes to false on new transaction + this._transactionUpToDate = false; + // Controls whether adjacent fields should be merged + // Generally only enabled for Interface.refresh() calls + this._shouldMerge = false; this._app = app; this._fields = null; @@ -1103,7 +1131,10 @@ Zotero.Integration.Session.prototype.getFields = new function() { /** * Updates Zotero.Integration.Session citations from the session document */ -Zotero.Integration.Session.prototype.updateFromDocument = Zotero.Promise.coroutine(function* (forceCitations) { +Zotero.Integration.Session.prototype.updateFromDocument = Zotero.Promise.coroutine(function* (forceUpdateAllCitations) { + if (this._transactionUpToDate) { + return; + } yield this.getFields(); this.resetRequest(this._doc); @@ -1114,11 +1145,11 @@ Zotero.Integration.Session.prototype.updateFromDocument = Zotero.Promise.corouti var timer = new Zotero.Integration.Timer(); timer.start(); this.progressBar.start(); - if (forceCitations) { - this.regenAll = true; + if (forceUpdateAllCitations) { + this.forceUpdateAllCitations = true; // See Session.restoreProcessorState() for a comment if (!Zotero.Prefs.get('cite.useCiteprocRs')) { - this.reload = true; + this.rebuildCiteprocState = true; } } yield this._processFields(); @@ -1129,18 +1160,18 @@ Zotero.Integration.Session.prototype.updateFromDocument = Zotero.Promise.corouti Zotero.debug('Retracted item handling failed', 2); Zotero.logError(e); } - this.regenAll = false; + this.forceUpdateAllCitations = false; var updateTime = timer.stop(); this.progressBar.finishSegment(); Zotero.debug("Integration: Updated session data for " + this._fields.length + " fields in " + updateTime + "; " + this._fields.length/updateTime + " fields/second"); - if (this.reload) { + if (this.rebuildCiteprocState) { this.restoreProcessorState(); - delete this.reload; + this.rebuildCiteprocState = false; } - this._sessionUpToDate = true; + this._sessionUpToDate = this._transactionUpToDate = true; }); /** @@ -1158,6 +1189,7 @@ Zotero.Integration.Session.prototype._processFields = async function () { var noteIndex = await field.getNoteIndex(), data = await field.unserialize(), citation = new Zotero.Integration.Citation(field, data, noteIndex); + citation.fieldIndex = i; if (this._shouldMerge && typeof field.isAdjacentToNextField === 'function' && await field.isAdjacentToNextField()) { adjacentCitations.push(citation); @@ -1307,7 +1339,7 @@ Zotero.Integration.Session.prototype._updateDocument = async function(forceCitat var citation = this.citationsByIndex[i]; if (citation) { - let citationField = citation._field; + let citationField = citation.field; var isRich = false; if (!citation.properties.dontUpdate) { @@ -1476,9 +1508,12 @@ Zotero.Integration.Session.prototype.cite = async function (field, addNote=false // Preparing data to pass into CitationEditInterface var fieldIndexPromise, citationsByItemIDPromise; - if (!this.data.prefs.delayCitationUpdates - || !Object.keys(this.citationsByItemID).length - || this._sessionUpToDate) { + const citationDataLoadedFromDocument = Object.keys(this.citationsByItemID).length; + if (this.data.prefs.delayCitationUpdates && citationDataLoadedFromDocument) { + fieldIndexPromise = Zotero.Promise.resolve(-1); + citationsByItemIDPromise = Zotero.Promise.resolve(this.citationsByItemID); + } + else { fieldIndexPromise = this.getFields().then(async function (fields) { for (var i = 0, n = fields.length; i < n; i++) { if (await fields[i].equals(field._field)) { @@ -1490,13 +1525,8 @@ Zotero.Integration.Session.prototype.cite = async function (field, addNote=false } return -1; }); - citationsByItemIDPromise = this.updateFromDocument(FORCE_CITATIONS_FALSE).then(function() { - return this.citationsByItemID; - }.bind(this)); - } - else { - fieldIndexPromise = Zotero.Promise.resolve(-1); - citationsByItemIDPromise = Zotero.Promise.resolve(this.citationsByItemID); + citationsByItemIDPromise = + this.updateFromDocument(FORCE_CITATIONS_FALSE).then(() => this.citationsByItemID); } var previewFn = async function (citation) { @@ -1579,9 +1609,9 @@ Zotero.Integration.Session.prototype.cite = async function (field, addNote=false } for (let citation of citations) { if (fields) { - citation._field = new Zotero.Integration.CitationField(fields[citation._fieldIndex]); + citation.field = new Zotero.Integration.CitationField(fields[citation.fieldIndex]); } - await this.addCitation(citation._fieldIndex, await citation._field.getNoteIndex(), citation); + await this.addCitation(citation.fieldIndex, await citation.field.getNoteIndex(), citation); } return citations; }; @@ -1677,7 +1707,7 @@ Zotero.Integration.Session.prototype._insertNoteIntoDocument = async function (f let insertedCitations = await Promise.all(fields.map(async (field, index) => { let citation = new Zotero.Integration.Citation(new Zotero.Integration.CitationField(field, 'TEMP'), citations[index]); - citation._fieldIndex = fieldIndex + fields.length - 1 - index; + citation.fieldIndex = fieldIndex + fields.length - 1 - index; return citation; })); return insertedCitations; @@ -1687,8 +1717,8 @@ Zotero.Integration.Session.prototype._insertItemsIntoDocument = async function ( if (!field) { field = new Zotero.Integration.CitationField(await this.addField(true, fieldIndex)); } - citation._field = field; - citation._fieldIndex = fieldIndex; + citation.field = field; + citation.fieldIndex = fieldIndex; return citation; }; @@ -1807,8 +1837,11 @@ Zotero.Integration.Session.prototype.resetRequest = function(doc) { // Citations that are not new to the session but where the item metadata // has changed will be marked in updateIndices this.updateIndices = {}; - // Citations that require updating in the document will be marked in - // processIndices + // Forces all citations to be added to updateIndices, which runs them through + // citeproc and updates in document if necessary + this.forceUpdateAllCitations = false; + // Reloads citeproc with all citation data. + this.rebuildCiteprocState = false; this.processIndices = {}; this.citationsByItemID = {}; @@ -1896,7 +1929,7 @@ Zotero.Integration.Session.prototype.setData = async function (data, resetStyle) this.style = getStyle.getCiteProc(data.style.locale, this.outputFormat, data.prefs.automaticJournalAbbreviations); this.styleClass = getStyle.class; // We're changing the citeproc instance, so we'll have to reinsert all citations into the registry - this.reload = true; + this.rebuildCiteprocState = true; this.styleID = data.style.styleID; } catch (e) { Zotero.logError(e); @@ -1976,9 +2009,8 @@ Zotero.Integration.Session.prototype.setDocPrefs = async function (showImportExp || oldData.prefs.fieldType != data.prefs.fieldType || (!data.prefs.delayCitationUpdates && oldData.prefs.delayCitationUpdates != data.prefs.delayCitationUpdates) || oldData.prefs.automaticJournalAbbreviations != data.prefs.automaticJournalAbbreviations) { - // This will cause us to regenerate all citations - this.regenAll = true; - this.reload = true; + this.forceUpdateAllCitations = true; + this.rebuildCiteprocState = true; } return oldData || null; @@ -2128,7 +2160,7 @@ Zotero.Integration.Session.prototype.addCitation = async function (index, noteIn if (!this.oldCitations.has(citation.citationID)) { this.newIndices[index] = true; } - if (this.regenAll && !this.newIndices[index]) { + if (this.forceUpdateAllCitations && !this.newIndices[index]) { this.updateIndices[index] = true; } Zotero.debug("Integration: Adding citationID "+citation.citationID); @@ -2332,8 +2364,9 @@ Zotero.Integration.Session.prototype.writeDelayedCitation = Zotero.Promise.corou }); -Zotero.Integration.Session.prototype.getItems = function() { - return Zotero.Cite.getItem(Object.keys(this.citationsByItemID)); +Zotero.Integration.Session.prototype.getItems = function(itemIDs) { + itemIDs = itemIDs || Object.keys(this.citationsByItemID); + return Zotero.Cite.getItem(itemIDs); } Zotero.Integration.Session.prototype.handleRetractedItems = async function () { @@ -2403,6 +2436,44 @@ Zotero.Integration.Session.prototype.promptForRetraction = function (citedItem, return checkbox.value; } +/** + * Opens the citation explorer + */ +Zotero.Integration.Session.prototype.openCitationExplorer = async function () { + if (!Object.keys(this.citationsByIndex).length) { + throw new Error('Integration.Session.openCitationExplorer: called without loaded citations'); + } + + let io = { + citations: this.citationsByIndex, + uncitedItems: this.bibliography ? Array.from(await this.getItems(Array.from(this.bibliography.uncitedItemIDs))) : [], + items: await this.getItems(), + activateDocument: async () => this._doc.activate(), + selectCitation: async citation => citation.field.select(), + cursorInCitation: async (citation) => { + const field = await this._doc.cursorInField(this.data.prefs['fieldType']); + if (!field) return false; + const citationField = await Zotero.Integration.Field.loadExisting(field); + const data = await citationField.unserialize(); + return data.citationID === citation.citationID; + }, + updateIndex: index => this.updateIndices[index] = true + }; + + await Zotero.Integration.displayDialog('chrome://zotero/content/integration/citationExplorer.xhtml', 'resizable', io); + + if (io.openCitationDialog) { + let citations = await this.cite(io.openCitationDialog); + if (this.data.prefs.delayCitationUpdates) { + for (let citation of citations) { + await this.writeDelayedCitation(citation.field, citation); + } + } else { + return this.updateDocument(FORCE_CITATIONS_FALSE, false, false); + } + } +}; + /** * Edits integration bibliography @@ -3088,7 +3159,15 @@ Zotero.Integration.Citation = class { this.properties = data.properties; this.properties.noteIndex = noteIndex; - this._field = citationField; + this.field = citationField; + } + + /** + * @deprecated + */ + get _field() { + Zotero.debug('Citation._field is deprecated. Use Citation.field'); + return this.field; } /** @@ -3225,7 +3304,7 @@ Zotero.Integration.Citation = class { var msg = Zotero.getString("integration.missingItem.multiple", (idx).toString()); } msg += '\n\n'+Zotero.getString('integration.missingItem.description'); - await this._field.select(); + await this.field.select(); await Zotero.Integration.currentDoc.activate(); var result = await Zotero.Integration.currentSession.displayAlert(msg, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO_CANCEL); @@ -3253,9 +3332,9 @@ Zotero.Integration.Citation = class { // Check for modified field text or dontUpdate flag if (this.properties.dontUpdate || (this.properties.plainCitation - && await this._field.getText() !== this.properties.plainCitation)) { + && await this.field.getText() !== this.properties.plainCitation)) { await Zotero.Integration.currentDoc.activate(); - var fieldText = await this._field.getText(); + var fieldText = await this.field.getText(); Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n" + "citaion.properties.dontUpdate: " + this.properties.dontUpdate + "\n" + "Original: " + this.properties.plainCitation + "\n" @@ -3336,7 +3415,7 @@ Zotero.Integration.Citation = class { Zotero.Integration.Bibliography = class { constructor(bibliographyField, data) { - this._field = bibliographyField; + this.field = bibliographyField; this.data = data; this.uncitedItemIDs = new Set(); diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js index 9cd5432b51..03248f8db8 100644 --- a/chrome/content/zotero/xpcom/utilities_internal.js +++ b/chrome/content/zotero/xpcom/utilities_internal.js @@ -1737,6 +1737,28 @@ Zotero.Utilities.Internal = { }, + showInLibrary: async function (itemID) { + var pane = Zotero.getActiveZoteroPane(); + // Open main window if it's not open (Mac) + if (!pane) { + let win = Zotero.openMainWindow(); + await new Zotero.Promise((resolve) => { + let onOpen = function () { + win.removeEventListener('load', onOpen); + resolve(); + }; + win.addEventListener('load', onOpen); + }); + pane = win.ZoteroPane; + } + pane.selectItem(itemID); + + // Pull window to foreground + Zotero.Utilities.Internal.activate(pane.document.defaultView); + pane.document.ownerGlobal.focus(); + }, + + filterStack: function (stack) { return stack.split(/\n/) .filter(line => !line.includes('resource://zotero/bluebird')) @@ -2534,7 +2556,19 @@ Zotero.Utilities.Internal.activate = new function () { * Bring a window to the foreground by interfacing directly with X11 */ function _X11BringToForeground(win, intervalID) { - var windowTitle = win.getInterface(Ci.nsIWebNavigation).title; + try { + var windowTitle = win.getInterface(Ci.nsIWebNavigation).title; + if (!windowTitle) { + windowTitle = win.document.title + } + if (!windowTitle) { + throw new Error(`Could not find window title for ${win.location.href}`); + } + } catch (e) { + Zotero.debug(`Could not find window title for ${win.location.href}`, 1); + Zotero.logError(e); + win.clearInterval(intervalID); + } var x11Window = _X11FindWindow(_x11RootWindow, windowTitle); if (!x11Window) return; diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 9e00bc7c2f..733b415af3 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -1780,66 +1780,62 @@ var ZoteroPane = new function() this.onCollectionSelected = Zotero.serial(async function () { - try { - var collectionTreeRow = this.getCollectionTreeRow(); - if (!collectionTreeRow) { - Zotero.debug('ZoteroPane.onCollectionSelected: No selected collection found'); - return; - } - - if (this.itemsView && this.itemsView.collectionTreeRow && this.itemsView.collectionTreeRow.id == collectionTreeRow.id) { - Zotero.debug("ZoteroPane.onCollectionSelected: Collection selection hasn't changed"); - - // Update enabled actions, in case editability has changed - this._updateEnabledActionsForRow(collectionTreeRow); - return; - } - - // Rename tab - Zotero_Tabs.rename('zotero-pane', collectionTreeRow.getName()); - - let type = Zotero.Libraries.get(collectionTreeRow.ref.libraryID).libraryType; - - // Clear quick search and tag selector when switching views - document.getElementById('zotero-tb-search-textbox').value = ""; - if (ZoteroPane.tagSelector) { - ZoteroPane.tagSelector.clearTagSelection(); - } - - collectionTreeRow.setSearch(''); - if (ZoteroPane.tagSelector) { - collectionTreeRow.setTags(ZoteroPane.tagSelector.getTagSelection()); - } - - this._updateEnabledActionsForRow(collectionTreeRow); - - // If item data not yet loaded for library, load it now. - // Other data types are loaded at startup - if (collectionTreeRow.isFeeds()) { - var feedsToLoad = Zotero.Feeds.getAll().filter(feed => !feed.getDataLoaded('item')); - if (feedsToLoad.length) { - Zotero.debug("Waiting for items to load for feeds " + feedsToLoad.map(feed => feed.libraryID)); - ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading')); - for (let feed of feedsToLoad) { - await feed.waitForDataLoad('item'); - } - } - } - else { - var library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID); - if (!library.getDataLoaded('item')) { - Zotero.debug("Waiting for items to load for library " + library.libraryID); - ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading')); - await library.waitForDataLoad('item'); - } - } - - this.itemsView.changeCollectionTreeRow(collectionTreeRow); - - Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id); - } finally { - this.collectionsView.runListeners('select'); + var collectionTreeRow = this.getCollectionTreeRow(); + if (!collectionTreeRow) { + Zotero.debug('ZoteroPane.onCollectionSelected: No selected collection found'); + return; } + + if (this.itemsView && this.itemsView.collectionTreeRow && this.itemsView.collectionTreeRow.id == collectionTreeRow.id) { + Zotero.debug("ZoteroPane.onCollectionSelected: Collection selection hasn't changed"); + + // Update enabled actions, in case editability has changed + this._updateEnabledActionsForRow(collectionTreeRow); + return; + } + + // Rename tab + Zotero_Tabs.rename('zotero-pane', collectionTreeRow.getName()); + + let type = Zotero.Libraries.get(collectionTreeRow.ref.libraryID).libraryType; + + // Clear quick search and tag selector when switching views + document.getElementById('zotero-tb-search-textbox').value = ""; + if (ZoteroPane.tagSelector) { + ZoteroPane.tagSelector.clearTagSelection(); + } + + collectionTreeRow.setSearch(''); + if (ZoteroPane.tagSelector) { + collectionTreeRow.setTags(ZoteroPane.tagSelector.getTagSelection()); + } + + this._updateEnabledActionsForRow(collectionTreeRow); + + // If item data not yet loaded for library, load it now. + // Other data types are loaded at startup + if (collectionTreeRow.isFeeds()) { + var feedsToLoad = Zotero.Feeds.getAll().filter(feed => !feed.getDataLoaded('item')); + if (feedsToLoad.length) { + Zotero.debug("Waiting for items to load for feeds " + feedsToLoad.map(feed => feed.libraryID)); + ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading')); + for (let feed of feedsToLoad) { + await feed.waitForDataLoad('item'); + } + } + } + else { + var library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID); + if (!library.getDataLoaded('item')) { + Zotero.debug("Waiting for items to load for library " + library.libraryID); + ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading')); + await library.waitForDataLoad('item'); + } + } + + this.itemsView.changeCollectionTreeRow(collectionTreeRow); + + Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id); }); @@ -1951,15 +1947,12 @@ var ZoteroPane = new function() return this.itemPane.render(); }.bind(this))() - .catch(function (e) { + .catch((e) => { Zotero.logError(e); Zotero.crash(); throw e; - }.bind(this)) - .finally(function () { - return this.itemsView.runListeners('select'); - }.bind(this)); - }; + }); + } this.updateAddAttachmentMenu = function (popup) { if (!this.canEdit()) { diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index 8a34dbb7c1..a5e44cc602 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -974,6 +974,7 @@ integration.upgradeTemplate = The %S plugin for %S is outdated. Reinstall the pl integration.mendeleyImport.title = Missing Mendeley Data integration.mendeleyImport.description = %1$S detected that the document you are citing with contains Mendeley citations. %1$S will be able to manage these citations if you import your Mendeley database. integration.mendeleyImport.openImporter = Open Mendeley Importer... +integration.citationExplorer.citationsModified = Citations in your document have been modified since Citation Explorer has been opened and %S will not be able to activate them until you reopen this window. styles.install.title = Install Style styles.install.unexpectedError = An unexpected error occurred while installing "%1$S" diff --git a/chrome/skin/default/zotero/16/universal/citation-dialog-library.svg b/chrome/skin/default/zotero/16/universal/citation-dialog-library.svg new file mode 100644 index 0000000000..9c05383f0e --- /dev/null +++ b/chrome/skin/default/zotero/16/universal/citation-dialog-library.svg @@ -0,0 +1,3 @@ + + + diff --git a/chrome/skin/default/zotero/16/universal/citation-dialog-list.svg b/chrome/skin/default/zotero/16/universal/citation-dialog-list.svg new file mode 100644 index 0000000000..8b1cd61fb9 --- /dev/null +++ b/chrome/skin/default/zotero/16/universal/citation-dialog-list.svg @@ -0,0 +1,3 @@ + + + diff --git a/chrome/skin/default/zotero/16/universal/citation-dialog-settings.svg b/chrome/skin/default/zotero/16/universal/citation-dialog-settings.svg new file mode 100644 index 0000000000..c3fdf8253d --- /dev/null +++ b/chrome/skin/default/zotero/16/universal/citation-dialog-settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/resource/diff.js b/resource/diff.js new file mode 100644 index 0000000000..187d17689b --- /dev/null +++ b/resource/diff.js @@ -0,0 +1,774 @@ +/** + * This library modifies the diff-patch-match library by Neil Fraser + * by removing the patch and match functionality and certain advanced + * options in the diff function. The original license is as follows: + * + * === + * + * Diff Match and Patch + * + * Copyright 2006 Google Inc. + * http://code.google.com/p/google-diff-match-patch/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * The data structure representing a diff is an array of tuples: + * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] + * which means: delete 'Hello', add 'Goodbye' and keep ' world.' + */ +var DIFF_DELETE = -1; +var DIFF_INSERT = 1; +var DIFF_EQUAL = 0; + + +/** + * Find the differences between two texts. Simplifies the problem by stripping + * any common prefix or suffix off the texts before diffing. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {Int|Object} [cursor_pos] Edit position in text1 or object with more info + * @return {Array} Array of diff tuples. + */ +function diff_main(text1, text2, cursor_pos, _fix_unicode) { + // Check for equality + if (text1 === text2) { + if (text1) { + return [[DIFF_EQUAL, text1]]; + } + return []; + } + + if (cursor_pos != null) { + var editdiff = find_cursor_edit_diff(text1, text2, cursor_pos); + if (editdiff) { + return editdiff; + } + } + + // Trim off common prefix (speedup). + var commonlength = diff_commonPrefix(text1, text2); + var commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = diff_commonSuffix(text1, text2); + var commonsuffix = text1.substring(text1.length - commonlength); + text1 = text1.substring(0, text1.length - commonlength); + text2 = text2.substring(0, text2.length - commonlength); + + // Compute the diff on the middle block. + var diffs = diff_compute_(text1, text2); + + // Restore the prefix and suffix. + if (commonprefix) { + diffs.unshift([DIFF_EQUAL, commonprefix]); + } + if (commonsuffix) { + diffs.push([DIFF_EQUAL, commonsuffix]); + } + diff_cleanupMerge(diffs, _fix_unicode); + return diffs; +}; + + +/** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @return {Array} Array of diff tuples. + */ +function diff_compute_(text1, text2) { + var diffs; + + if (!text1) { + // Just add some text (speedup). + return [[DIFF_INSERT, text2]]; + } + + if (!text2) { + // Just delete some text (speedup). + return [[DIFF_DELETE, text1]]; + } + + var longtext = text1.length > text2.length ? text1 : text2; + var shorttext = text1.length > text2.length ? text2 : text1; + var i = longtext.indexOf(shorttext); + if (i !== -1) { + // Shorter text is inside the longer text (speedup). + diffs = [ + [DIFF_INSERT, longtext.substring(0, i)], + [DIFF_EQUAL, shorttext], + [DIFF_INSERT, longtext.substring(i + shorttext.length)] + ]; + // Swap insertions for deletions if diff is reversed. + if (text1.length > text2.length) { + diffs[0][0] = diffs[2][0] = DIFF_DELETE; + } + return diffs; + } + + if (shorttext.length === 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; + } + + // Check to see if the problem can be split in two. + var hm = diff_halfMatch_(text1, text2); + if (hm) { + // A half-match was found, sort out the return data. + var text1_a = hm[0]; + var text1_b = hm[1]; + var text2_a = hm[2]; + var text2_b = hm[3]; + var mid_common = hm[4]; + // Send both pairs off for separate processing. + var diffs_a = diff_main(text1_a, text2_a); + var diffs_b = diff_main(text1_b, text2_b); + // Merge the results. + return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b); + } + + return diff_bisect_(text1, text2); +}; + + +/** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @return {Array} Array of diff tuples. + * @private + */ +function diff_bisect_(text1, text2) { + // Cache the text lengths to prevent multiple calls. + var text1_length = text1.length; + var text2_length = text2.length; + var max_d = Math.ceil((text1_length + text2_length) / 2); + var v_offset = max_d; + var v_length = 2 * max_d; + var v1 = new Array(v_length); + var v2 = new Array(v_length); + // Setting all elements to -1 is faster in Chrome & Firefox than mixing + // integers and undefined. + for (var x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + var delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will collide + // with the reverse path. + var front = (delta % 2 !== 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + var k1start = 0; + var k1end = 0; + var k2start = 0; + var k2end = 0; + for (var d = 0; d < max_d; d++) { + // Walk the front path one step. + for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + var k1_offset = v_offset + k1; + var x1; + if (k1 === -d || (k1 !== d && v1[k1_offset - 1] < v1[k1_offset + 1])) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + var y1 = x1 - k1; + while ( + x1 < text1_length && y1 < text2_length && + text1.charAt(x1) === text2.charAt(y1) + ) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + var k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] !== -1) { + // Mirror x2 onto top-left coordinate system. + var x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit_(text1, text2, x1, y1); + } + } + } + } + + // Walk the reverse path one step. + for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + var k2_offset = v_offset + k2; + var x2; + if (k2 === -d || (k2 !== d && v2[k2_offset - 1] < v2[k2_offset + 1])) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + var y2 = x2 - k2; + while ( + x2 < text1_length && y2 < text2_length && + text1.charAt(text1_length - x2 - 1) === text2.charAt(text2_length - y2 - 1) + ) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + var k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] !== -1) { + var x1 = v1[k1_offset]; + var y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit_(text1, text2, x1, y1); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; +}; + + +/** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @return {Array} Array of diff tuples. + */ +function diff_bisectSplit_(text1, text2, x, y) { + var text1a = text1.substring(0, x); + var text2a = text2.substring(0, y); + var text1b = text1.substring(x); + var text2b = text2.substring(y); + + // Compute both diffs serially. + var diffs = diff_main(text1a, text2a); + var diffsb = diff_main(text1b, text2b); + + return diffs.concat(diffsb); +}; + + +/** + * Determine the common prefix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the start of each + * string. + */ +function diff_commonPrefix(text1, text2) { + // Quick check for common null cases. + if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + var pointermin = 0; + var pointermax = Math.min(text1.length, text2.length); + var pointermid = pointermax; + var pointerstart = 0; + while (pointermin < pointermid) { + if ( + text1.substring(pointerstart, pointermid) == + text2.substring(pointerstart, pointermid) + ) { + pointermin = pointermid; + pointerstart = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + + if (is_surrogate_pair_start(text1.charCodeAt(pointermid - 1))) { + pointermid--; + } + + return pointermid; +}; + + +/** + * Determine the common suffix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of each string. + */ +function diff_commonSuffix(text1, text2) { + // Quick check for common null cases. + if (!text1 || !text2 || text1.slice(-1) !== text2.slice(-1)) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + var pointermin = 0; + var pointermax = Math.min(text1.length, text2.length); + var pointermid = pointermax; + var pointerend = 0; + while (pointermin < pointermid) { + if ( + text1.substring(text1.length - pointermid, text1.length - pointerend) == + text2.substring(text2.length - pointermid, text2.length - pointerend) + ) { + pointermin = pointermid; + pointerend = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + + if (is_surrogate_pair_end(text1.charCodeAt(text1.length - pointermid))) { + pointermid--; + } + + return pointermid; +}; + + +/** + * Do the two texts share a substring which is at least half the length of the + * longer text? + * This speedup can produce non-minimal diffs. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.} Five element Array, containing the prefix of + * text1, the suffix of text1, the prefix of text2, the suffix of + * text2 and the common middle. Or null if there was no match. + */ +function diff_halfMatch_(text1, text2) { + var longtext = text1.length > text2.length ? text1 : text2; + var shorttext = text1.length > text2.length ? text2 : text1; + if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { + return null; // Pointless. + } + + /** + * Does a substring of shorttext exist within longtext such that the substring + * is at least half the length of longtext? + * Closure, but does not reference any external variables. + * @param {string} longtext Longer string. + * @param {string} shorttext Shorter string. + * @param {number} i Start index of quarter length substring within longtext. + * @return {Array.} Five element Array, containing the prefix of + * longtext, the suffix of longtext, the prefix of shorttext, the suffix + * of shorttext and the common middle. Or null if there was no match. + * @private + */ + function diff_halfMatchI_(longtext, shorttext, i) { + // Start with a 1/4 length substring at position i as a seed. + var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); + var j = -1; + var best_common = ''; + var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; + while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { + var prefixLength = diff_commonPrefix( + longtext.substring(i), shorttext.substring(j)); + var suffixLength = diff_commonSuffix( + longtext.substring(0, i), shorttext.substring(0, j)); + if (best_common.length < suffixLength + prefixLength) { + best_common = shorttext.substring( + j - suffixLength, j) + shorttext.substring(j, j + prefixLength); + best_longtext_a = longtext.substring(0, i - suffixLength); + best_longtext_b = longtext.substring(i + prefixLength); + best_shorttext_a = shorttext.substring(0, j - suffixLength); + best_shorttext_b = shorttext.substring(j + prefixLength); + } + } + if (best_common.length * 2 >= longtext.length) { + return [ + best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common + ]; + } else { + return null; + } + } + + // First check if the second quarter is the seed for a half-match. + var hm1 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 4)); + // Check again based on the third quarter. + var hm2 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 2)); + var hm; + if (!hm1 && !hm2) { + return null; + } else if (!hm2) { + hm = hm1; + } else if (!hm1) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length > hm2[4].length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + var text1_a, text1_b, text2_a, text2_b; + if (text1.length > text2.length) { + text1_a = hm[0]; + text1_b = hm[1]; + text2_a = hm[2]; + text2_b = hm[3]; + } else { + text2_a = hm[0]; + text2_b = hm[1]; + text1_a = hm[2]; + text1_b = hm[3]; + } + var mid_common = hm[4]; + return [text1_a, text1_b, text2_a, text2_b, mid_common]; +}; + + +/** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param {Array} diffs Array of diff tuples. + * @param {boolean} fix_unicode Whether to normalize to a unicode-correct diff + */ +function diff_cleanupMerge(diffs, fix_unicode) { + diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end. + var pointer = 0; + var count_delete = 0; + var count_insert = 0; + var text_delete = ''; + var text_insert = ''; + var commonlength; + while (pointer < diffs.length) { + if (pointer < diffs.length - 1 && !diffs[pointer][1]) { + diffs.splice(pointer, 1); + continue; + } + switch (diffs[pointer][0]) { + case DIFF_INSERT: + + count_insert++; + text_insert += diffs[pointer][1]; + pointer++; + break; + case DIFF_DELETE: + count_delete++; + text_delete += diffs[pointer][1]; + pointer++; + break; + case DIFF_EQUAL: + var previous_equality = pointer - count_insert - count_delete - 1; + if (fix_unicode) { + // prevent splitting of unicode surrogate pairs. when fix_unicode is true, + // we assume that the old and new text in the diff are complete and correct + // unicode-encoded JS strings, but the tuple boundaries may fall between + // surrogate pairs. we fix this by shaving off stray surrogates from the end + // of the previous equality and the beginning of this equality. this may create + // empty equalities or a common prefix or suffix. for example, if AB and AC are + // emojis, `[[0, 'A'], [-1, 'BA'], [0, 'C']]` would turn into deleting 'ABAC' and + // inserting 'AC', and then the common suffix 'AC' will be eliminated. in this + // particular case, both equalities go away, we absorb any previous inequalities, + // and we keep scanning for the next equality before rewriting the tuples. + if (previous_equality >= 0 && ends_with_pair_start(diffs[previous_equality][1])) { + var stray = diffs[previous_equality][1].slice(-1); + diffs[previous_equality][1] = diffs[previous_equality][1].slice(0, -1); + text_delete = stray + text_delete; + text_insert = stray + text_insert; + if (!diffs[previous_equality][1]) { + // emptied out previous equality, so delete it and include previous delete/insert + diffs.splice(previous_equality, 1); + pointer--; + var k = previous_equality - 1; + if (diffs[k] && diffs[k][0] === DIFF_INSERT) { + count_insert++; + text_insert = diffs[k][1] + text_insert; + k--; + } + if (diffs[k] && diffs[k][0] === DIFF_DELETE) { + count_delete++; + text_delete = diffs[k][1] + text_delete; + k--; + } + previous_equality = k; + } + } + if (starts_with_pair_end(diffs[pointer][1])) { + var stray = diffs[pointer][1].charAt(0); + diffs[pointer][1] = diffs[pointer][1].slice(1); + text_delete += stray; + text_insert += stray; + } + } + if (pointer < diffs.length - 1 && !diffs[pointer][1]) { + // for empty equality not at end, wait for next equality + diffs.splice(pointer, 1); + break; + } + if (text_delete.length > 0 || text_insert.length > 0) { + // note that diff_commonPrefix and diff_commonSuffix are unicode-aware + if (text_delete.length > 0 && text_insert.length > 0) { + // Factor out any common prefixes. + commonlength = diff_commonPrefix(text_insert, text_delete); + if (commonlength !== 0) { + if (previous_equality >= 0) { + diffs[previous_equality][1] += text_insert.substring(0, commonlength); + } else { + diffs.splice(0, 0, [DIFF_EQUAL, text_insert.substring(0, commonlength)]); + pointer++; + } + text_insert = text_insert.substring(commonlength); + text_delete = text_delete.substring(commonlength); + } + // Factor out any common suffixes. + commonlength = diff_commonSuffix(text_insert, text_delete); + if (commonlength !== 0) { + diffs[pointer][1] = + text_insert.substring(text_insert.length - commonlength) + diffs[pointer][1]; + text_insert = text_insert.substring(0, text_insert.length - commonlength); + text_delete = text_delete.substring(0, text_delete.length - commonlength); + } + } + // Delete the offending records and add the merged ones. + var n = count_insert + count_delete; + if (text_delete.length === 0 && text_insert.length === 0) { + diffs.splice(pointer - n, n); + pointer = pointer - n; + } else if (text_delete.length === 0) { + diffs.splice(pointer - n, n, [DIFF_INSERT, text_insert]); + pointer = pointer - n + 1; + } else if (text_insert.length === 0) { + diffs.splice(pointer - n, n, [DIFF_DELETE, text_delete]); + pointer = pointer - n + 1; + } else { + diffs.splice(pointer - n, n, [DIFF_DELETE, text_delete], [DIFF_INSERT, text_insert]); + pointer = pointer - n + 2; + } + } + if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1][1] += diffs[pointer][1]; + diffs.splice(pointer, 1); + } else { + pointer++; + } + count_insert = 0; + count_delete = 0; + text_delete = ''; + text_insert = ''; + break; + } + } + if (diffs[diffs.length - 1][1] === '') { + diffs.pop(); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + var changes = false; + pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] === DIFF_EQUAL && + diffs[pointer + 1][0] === DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + if (diffs[pointer][1].substring(diffs[pointer][1].length - + diffs[pointer - 1][1].length) === diffs[pointer - 1][1]) { + // Shift the edit over the previous equality. + diffs[pointer][1] = diffs[pointer - 1][1] + + diffs[pointer][1].substring(0, diffs[pointer][1].length - + diffs[pointer - 1][1].length); + diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; + diffs.splice(pointer - 1, 1); + changes = true; + } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == + diffs[pointer + 1][1]) { + // Shift the edit over the next equality. + diffs[pointer - 1][1] += diffs[pointer + 1][1]; + diffs[pointer][1] = + diffs[pointer][1].substring(diffs[pointer + 1][1].length) + + diffs[pointer + 1][1]; + diffs.splice(pointer + 1, 1); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + diff_cleanupMerge(diffs, fix_unicode); + } +}; + +function is_surrogate_pair_start(charCode) { + return charCode >= 0xD800 && charCode <= 0xDBFF; +} + +function is_surrogate_pair_end(charCode) { + return charCode >= 0xDC00 && charCode <= 0xDFFF; +} + +function starts_with_pair_end(str) { + return is_surrogate_pair_end(str.charCodeAt(0)); +} + +function ends_with_pair_start(str) { + return is_surrogate_pair_start(str.charCodeAt(str.length - 1)); +} + +function remove_empty_tuples(tuples) { + var ret = []; + for (var i = 0; i < tuples.length; i++) { + if (tuples[i][1].length > 0) { + ret.push(tuples[i]); + } + } + return ret; +} + +function make_edit_splice(before, oldMiddle, newMiddle, after) { + if (ends_with_pair_start(before) || starts_with_pair_end(after)) { + return null; + } + return remove_empty_tuples([ + [DIFF_EQUAL, before], + [DIFF_DELETE, oldMiddle], + [DIFF_INSERT, newMiddle], + [DIFF_EQUAL, after] + ]); +} + +function find_cursor_edit_diff(oldText, newText, cursor_pos) { + // note: this runs after equality check has ruled out exact equality + var oldRange = typeof cursor_pos === 'number' ? + { index: cursor_pos, length: 0 } : cursor_pos.oldRange; + var newRange = typeof cursor_pos === 'number' ? + null : cursor_pos.newRange; + // take into account the old and new selection to generate the best diff + // possible for a text edit. for example, a text change from "xxx" to "xx" + // could be a delete or forwards-delete of any one of the x's, or the + // result of selecting two of the x's and typing "x". + var oldLength = oldText.length; + var newLength = newText.length; + if (oldRange.length === 0 && (newRange === null || newRange.length === 0)) { + // see if we have an insert or delete before or after cursor + var oldCursor = oldRange.index; + var oldBefore = oldText.slice(0, oldCursor); + var oldAfter = oldText.slice(oldCursor); + var maybeNewCursor = newRange ? newRange.index : null; + editBefore: { + // is this an insert or delete right before oldCursor? + var newCursor = oldCursor + newLength - oldLength; + if (maybeNewCursor !== null && maybeNewCursor !== newCursor) { + break editBefore; + } + if (newCursor < 0 || newCursor > newLength) { + break editBefore; + } + var newBefore = newText.slice(0, newCursor); + var newAfter = newText.slice(newCursor); + if (newAfter !== oldAfter) { + break editBefore; + } + var prefixLength = Math.min(oldCursor, newCursor); + var oldPrefix = oldBefore.slice(0, prefixLength); + var newPrefix = newBefore.slice(0, prefixLength); + if (oldPrefix !== newPrefix) { + break editBefore; + } + var oldMiddle = oldBefore.slice(prefixLength); + var newMiddle = newBefore.slice(prefixLength); + return make_edit_splice(oldPrefix, oldMiddle, newMiddle, oldAfter); + } + editAfter: { + // is this an insert or delete right after oldCursor? + if (maybeNewCursor !== null && maybeNewCursor !== oldCursor) { + break editAfter; + } + var cursor = oldCursor; + var newBefore = newText.slice(0, cursor); + var newAfter = newText.slice(cursor); + if (newBefore !== oldBefore) { + break editAfter; + } + var suffixLength = Math.min(oldLength - cursor, newLength - cursor); + var oldSuffix = oldAfter.slice(oldAfter.length - suffixLength); + var newSuffix = newAfter.slice(newAfter.length - suffixLength); + if (oldSuffix !== newSuffix) { + break editAfter; + } + var oldMiddle = oldAfter.slice(0, oldAfter.length - suffixLength); + var newMiddle = newAfter.slice(0, newAfter.length - suffixLength); + return make_edit_splice(oldBefore, oldMiddle, newMiddle, oldSuffix); + } + } + if (oldRange.length > 0 && newRange && newRange.length === 0) { + replaceRange: { + // see if diff could be a splice of the old selection range + var oldPrefix = oldText.slice(0, oldRange.index); + var oldSuffix = oldText.slice(oldRange.index + oldRange.length); + var prefixLength = oldPrefix.length; + var suffixLength = oldSuffix.length; + if (newLength < prefixLength + suffixLength) { + break replaceRange; + } + var newPrefix = newText.slice(0, prefixLength); + var newSuffix = newText.slice(newLength - suffixLength); + if (oldPrefix !== newPrefix || oldSuffix !== newSuffix) { + break replaceRange; + } + var oldMiddle = oldText.slice(prefixLength, oldLength - suffixLength); + var newMiddle = newText.slice(prefixLength, newLength - suffixLength); + return make_edit_splice(oldPrefix, oldMiddle, newMiddle, oldSuffix); + } + } + + return null; +} + +function diff(text1, text2, cursor_pos) { + // only pass fix_unicode=true at the top level, not when diff_main is + // recursively invoked + return diff_main(text1, text2, cursor_pos, true); +} + +diff.INSERT = DIFF_INSERT; +diff.DELETE = DIFF_DELETE; +diff.EQUAL = DIFF_EQUAL; + +module.exports = diff; \ No newline at end of file diff --git a/scss/_zotero.scss b/scss/_zotero.scss index fbae7be4f4..f63195588f 100644 --- a/scss/_zotero.scss +++ b/scss/_zotero.scss @@ -3,6 +3,7 @@ @import "abstracts/variables"; @import "abstracts/functions"; +@import "abstracts/layout"; @import "abstracts/mixins"; @import "abstracts/placeholders"; @import "abstracts/utilities"; @@ -25,6 +26,8 @@ @import "components/autosuggest"; @import "components/banners"; @import "components/button"; +@import "components/citationExplorer"; +@import "components/citationDialog"; @import "components/clicky"; @import "components/contextPane"; @import "components/collection-tree"; @@ -73,6 +76,7 @@ @import "elements/attachmentBox"; @import "elements/attachmentPreview"; @import "elements/attachmentPreviewBox"; +@import "elements/bubbleInput"; @import "elements/colorPicker"; @import "elements/guidancePanel"; @import "elements/infoBox"; diff --git a/scss/abstracts/_layout.scss b/scss/abstracts/_layout.scss new file mode 100644 index 0000000000..6b660fa239 --- /dev/null +++ b/scss/abstracts/_layout.scss @@ -0,0 +1,18 @@ +.hbox { + display: flex; + flex-direction: row; + + .separator { + margin: 0 3px; + border-right: 2px solid #ddd; + } +} + +.vbox { + display: flex; + flex-direction: column; +} + +.flex { + flex: 1 +} diff --git a/scss/components/_citationDialog.scss b/scss/components/_citationDialog.scss new file mode 100644 index 0000000000..019241183e --- /dev/null +++ b/scss/components/_citationDialog.scss @@ -0,0 +1,7 @@ +#citation-dialog { + min-width: 800px; + + .layout { + display: contents; + } +} \ No newline at end of file diff --git a/scss/components/_citationExplorer.scss b/scss/components/_citationExplorer.scss new file mode 100644 index 0000000000..7615565799 --- /dev/null +++ b/scss/components/_citationExplorer.scss @@ -0,0 +1,29 @@ +#zotero-citation-explorer-dialog { + min-height: 500px; + + dialog { + max-height: 100vh; + } + + .virtualized-table-container { + height: 100%; + flex: 1; + } + + .xul-border-div { + -moz-box-flex: 1; + } + + #citations-label { + margin: 5px 0 4px; + } + + .panel { + border: solid 1px ThreeDShadow; + padding: 5px; + } + + .search-box { + margin: 2px -1px; + } +} diff --git a/scss/components/_collection-tree.scss b/scss/components/_collection-tree.scss index 1c0f415a98..4ffc9556a9 100644 --- a/scss/components/_collection-tree.scss +++ b/scss/components/_collection-tree.scss @@ -60,6 +60,11 @@ $icons: ( padding: 0 8px 8px; scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-background); } + + // Highlight takes priority over selection + .row.highlighted.selected { + background: #FFFF99; + } .cell.primary { display: flex; diff --git a/scss/components/_item-tree.scss b/scss/components/_item-tree.scss index b0c9fb18d4..02c455d873 100644 --- a/scss/components/_item-tree.scss +++ b/scss/components/_item-tree.scss @@ -164,7 +164,12 @@ } } } - + + .cell.hasAttachment { + height: 100%; + // Don't show ellipsis + text-overflow: unset; + } .cell.primary { .retracted { @@ -172,10 +177,6 @@ margin-inline-start: 3px; } } - - .cell.no-padding { - padding: 0; - } .cell.hasAttachment { text-overflow: unset; @@ -183,11 +184,8 @@ display: flex; justify-content: center; - .cell-text { - text-overflow: unset; - align-items: center; - display: flex; - justify-content: center; + .icon-treeitemattachmentpdf { + background-size: 10px } .icon-missing-file { diff --git a/scss/components/_virtualized-table.scss b/scss/components/_virtualized-table.scss index 390490a351..3e9e9f120d 100644 --- a/scss/components/_virtualized-table.scss +++ b/scss/components/_virtualized-table.scss @@ -211,6 +211,21 @@ min-width: 0px; } } + +.virtualized-table.multi-select:focus { + .row.focused { + border: 1px dotted highlight; + z-index: 10000; + + > *:first-child { + margin-inline-start: -1px; + } + + > *:last-child { + margin-inline-end: -1px; + } + } +} .virtualized-table-header { display: flex; @@ -232,6 +247,14 @@ &.static-columns { pointer-events: none; + .cell { + &:hover { + background: inherit; + } + } + [title] { + pointer-events: auto; + } } &::after { @@ -293,13 +316,15 @@ &.cell-icon { > .label { margin-inline-start: 0; + display: flex; + justify-content: center; } .icon-css { fill: var(--fill-secondary); } - justify-content: center; + padding: 0; } .sort-indicator { diff --git a/scss/elements/_bubbleInput.scss b/scss/elements/_bubbleInput.scss new file mode 100644 index 0000000000..0e2fb18b7e --- /dev/null +++ b/scss/elements/_bubbleInput.scss @@ -0,0 +1,106 @@ +bubble-input { + min-width: 200px; + + .body { + cursor: text; + display: flex; + flex-flow: row wrap; + align-items: center; + //margin: 0; + font: -moz-field; + outline: none; + //line-height: 2em; + //width: 700px; /* initial editor width - adjusted after dom load */ + min-height: 28px; + font-size: 13px; + background: white; + color: black; + width: 100%; + padding-inline: 6px; + + span { + white-space: pre; + } + } + + input { + outline: none; + width: 2px; + border: none !important; + box-sizing: border-box; + height: 15px; + /* Keep the background and text color unchanged in dark mode */ + background-color: transparent !important; + color: black; + //background-image: none !important; + padding: 0; + margin-inline: -1px; + } + + .bubble { + --margin-horizontal: 2px; + border-radius: 8px; + background-color: #dee7f8; + border-style: solid; + border-width: 1px; + border-color: #a8c0ec; + padding: 0 4px; + margin: 0 var(--margin-horizontal); + display: flex; + flex-direction: row; + align-items: center; + white-space: nowrap; + line-height: normal; + color: #000; + -moz-user-select: none; + cursor: pointer; + height: 16px; + max-width: -moz-available; + position: relative; + + &:hover { + background-color: #bbcef1; + border-color: #6d95e0; + + .cross { + display: block; + } + + .text { + margin-right: -16px; + mask-image: linear-gradient(to left, transparent 12px, var(--fill-primary) 24px); + overflow: hidden; + text-overflow: ellipsis; + } + } + + &.drop-before::before, &.drop-after::after { + content: ""; + display: block; + margin-left: -1px; + height: 14px; + border-left: 1px solid var(--fill-primary); + position: absolute; + } + &.drop-before::before { + left: calc(-1 * var(--margin-horizontal)); + } + &.drop-after::after { + right: calc(-2 * var(--margin-horizontal)); + } + + &[selected="true"] { + border-radius: 8px !important; + background-color: #598bec; + color: #fff; + } + + .cross { + @include svgicon("x-8", "universal", "16"); + display: none; + width: 16px; + height: 18px; + align-self: start; + } + } +} \ No newline at end of file diff --git a/scss/mac/_citationExplorer.scss b/scss/mac/_citationExplorer.scss new file mode 100644 index 0000000000..5b499344ce --- /dev/null +++ b/scss/mac/_citationExplorer.scss @@ -0,0 +1,14 @@ +#zotero-citation-explorer-dialog { + tab[visuallyselected="true"]:not(:-moz-window-inactive) { + color: initial !important; + } + + #items-panel { + margin-top: 1.5em; + } + + #citations-label { + margin: 0.5em 0 0.38em; + font-size: 1.2em; + } +} \ No newline at end of file diff --git a/scss/win/_citationExplorer.scss b/scss/win/_citationExplorer.scss new file mode 100644 index 0000000000..d3ed61a654 --- /dev/null +++ b/scss/win/_citationExplorer.scss @@ -0,0 +1,13 @@ +#zotero-citation-explorer-dialog { + #citations-label { + margin: 3px 0 2px; + } + + tab:focus-visible > .tab-middle { + outline: none; + } + + input[type="button"], button { + margin-top: 7px; + } +} \ No newline at end of file diff --git a/scss/zotero-mac.scss b/scss/zotero-mac.scss index c25eeb1417..149b5b5698 100644 --- a/scss/zotero-mac.scss +++ b/scss/zotero-mac.scss @@ -11,5 +11,6 @@ @import "mac/titleBar"; @import "mac/components/menupopup"; @import "mac/components/menulist"; +@import "mac/citationExplorer"; // Elements diff --git a/scss/zotero-win.scss b/scss/zotero-win.scss index 43b62ff20e..17448c86bf 100644 --- a/scss/zotero-win.scss +++ b/scss/zotero-win.scss @@ -9,6 +9,7 @@ @import "win/createParent"; @import "win/tabBar"; @import "win/titleBar"; +@import "win/citationExplorer"; // Elements