Adds a Citation Explorer dialog for document integration (#3468)

This commit is contained in:
Adomas Venčkauskas 2023-07-11 15:02:41 +03:00 committed by Dan Stillman
parent 8ddb6bfd17
commit a3654652b2
35 changed files with 2537 additions and 233 deletions

View file

@ -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;

View file

@ -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);
}
});
}

View file

@ -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);

View file

@ -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 {
<span
key={columnName + '-label'}
className={`label ${column.dataKey}`}
title={column.iconLabel ? columnName : ""}>
{...(column.iconLabel ? { title: columnName } : {})}
>
{label}
</span>
{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('&', '&amp;');
}
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) {

View file

@ -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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
{
class BubbleInput extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div xmlns:html="http://www.w3.org/1999/xhtml" flex="1" spellcheck="false" class="bubble-input body" role="application">
</html:div>
`);
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", '<span id="zotero-drag"/>');
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 <br> 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);
}

View file

@ -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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/

View file

@ -0,0 +1,77 @@
<?xml version="1.0"?>
<!--
***** 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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
-->
<!DOCTYPE html SYSTEM "chrome://zotero/locale/zotero.dtd">
<html
id="citation-dialog"
class="vbox flex"
persist="screenX screenY width height sizemode"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
drawintitlebar="true"
resizable="false">
<head>
<title>&zotero.integration.quickFormatDialog.title;</title>
<link rel="localization" href="zotero.ftl"/>
<link rel="stylesheet" href="chrome://global/skin/global.css" />
<link rel="stylesheet" href="chrome://zotero/skin/zotero.css" />
<link rel="stylesheet" href="chrome://zotero-platform/content/zotero.css" />
<script src="../include.js"/>
<script src="../customElements.js"/>
<script src="citationDialog.js"/>
</head>
<body class="vbox flex">
<div id="search-area" class="layout">
<div id="search-row" class="hbox">
<div id="z-icon"></div>
<xul:bubble-input class="flex" placeholder="Type to search, or add selected and open items"/>
<xul:button id="mode-button" class="icon icon-css icon-citation-dialog-library"/>
<xul:button id="settings-button" class="icon icon-css icon-citation-dialog-library"/>
</div>
<div id="notification" style="display: none;"></div>
</div>
<div id="list-layout" class="layout">
<div id="list-selected-items"></div>
<div id="list-open-items"></div>
<div id="list-cited-items"></div>
<div id="list-found-items"></div>
</div>
<div id="library-layout" class="layout" style="display: none;">
<div id="library-other-items" class="hbox">
<div id="library-selected-items"></div>
<div id="library-open-items"></div>
<div id="library-cited-items"></div>
</div>
<div id="library-trees">
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,510 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2023 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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
const React = require('react');
const ReactDOM = require('react-dom');
const diff = require('diff');
const VirtualizedTable = require('components/virtualized-table');
const { getCSSIcon, IconAttachSmall } = require('components/icons');
const ItemTree = require('zotero/itemTree');
const { getColumnDefinitionsByDataKey } = require('zotero/itemTreeColumns');
const { makeRowRenderer } = VirtualizedTable;
let io, citations, items, uncitedItems, citationList, itemList;
let citationRows = [];
let itemRows = [];
let uncitedItemRows = [];
let _addToTarget;
let disableCitationActivate;
let selectedTab = 0;
const citationColumns = [
{
dataKey: 'isLinked',
label: 'Is Linked',
iconLabel: <IconAttachSmall/>,
width: 26,
staticWidth: true,
fixedWidth: true,
renderer: (index, data, column) => {
let icon = getCSSIcon('IconCross');
if (data) {
icon = getCSSIcon('IconTick');
}
icon.className += ` cell ${column.className}`;
return icon;
}
},
{
dataKey: 'title',
label: "Citation",
type: 'html'
},
];
let itemColumns = getColumnDefinitionsByDataKey(['title', 'firstCreator', 'date']);
itemColumns.push({
dataKey: 'isLinked',
label: 'Is Linked',
iconLabel: <IconAttachSmall/>,
width: 26,
staticWidth: true,
fixedWidth: true,
renderer: (index, data, column) => {
let icon = getCSSIcon('IconCross');
if (data) {
icon = getCSSIcon('IconTick');
}
icon.className += ` cell ${column.className}`;
return icon;
}
});
itemColumns[1].sortDirection = 1;
window.ZoteroDocumentCitations = {
init: async function () {
this._highlightedCitations = new Set();
this._filteredCitations = new Set();
this._filteredItems = new Set();
document.querySelector('#button-show-in-document').addEventListener('click', this.onCitationActivate.bind(this));
document.querySelector('#button-edit-citation').addEventListener('click', this.onCitationEdit.bind(this));
document.querySelector('#button-show-in-zotero').addEventListener('click', this.onItemActivate.bind(this));
document.querySelector('#button-relink-item').addEventListener('click', this.onItemRelink.bind(this));
let lastTranslationTarget = Zotero.Prefs.get('documentCitations.lastAddToTarget');
if (lastTranslationTarget) {
let id = parseInt(lastTranslationTarget.substr(1));
if (lastTranslationTarget[0] == "L") {
_addToTarget = Zotero.Libraries.get(id);
}
else if (lastTranslationTarget[0] == "C") {
_addToTarget = Zotero.Collections.get(id);
}
}
if (!_addToTarget) {
_addToTarget = Zotero.Libraries.userLibrary;
}
this.setAddToButton();
io = window.arguments[0].wrappedJSObject;
citations = Object.values(io.citations);
items = io.items;
uncitedItems = io.uncitedItems;
await this._initMappings();
await this.refreshCitationList();
await this.refreshItemList();
},
refreshCitationList: async function () {
this._renderedCitationRows = citationRows.filter(row => !this._filteredCitations.has(row.ref.citationID));
this._renderedCitationRows.forEach((row) => {
row.highlighted = this._highlightedCitations.has(row.ref.citationID);
});
// init VirtualizedTable
if (!citationList) {
await new Promise((resolve) => {
ReactDOM.createRoot(document.querySelector('#citation-list')).render(<VirtualizedTable
id="citation-list"
ref={(ref) => {
citationList = ref;
resolve();
}}
multiSelect={true}
getRowCount={() => this._renderedCitationRows.length}
showHeader={true}
staticColumns={true}
columns={citationColumns}
renderItem={makeRowRenderer(index => this._renderedCitationRows[index])}
onActivate={this.onCitationActivate.bind(this)}
onSelectionChange={this.onCitationSelectionChange.bind(this)}
getRowString={index => this._renderedCitationRows[index].title}
/>);
});
}
citationList.invalidate();
},
refreshItemList: async function () {
let rows = selectedTab === 0 ? itemRows : uncitedItemRows;
rows.forEach((item) => {
item.isLinked = !item.cslItemID;
});
let filteredItems = rows.filter(item => !this._filteredItems.has(item.id));
if (!itemList) {
let domElem = document.querySelector('#item-list');
itemList = await ItemTree.init(domElem, {
id: "document-collections",
regularOnly: true,
columns: itemColumns,
shouldListenForNotifications: false,
onSelectionChange: this.onItemSelectionChange.bind(this),
onActivate: this.onItemActivate.bind(this),
emptyMessage: Zotero.getString('pane.items.loading')
});
await itemList.waitForLoad();
}
await itemList.changeCollectionTreeRow({
getItems: async () => filteredItems,
isSearch: () => true,
isSearchMode: () => true,
});
},
onCitationFilter: async function () {
let searchString = this._normalizeSearch(document.querySelector('#citation-search').value);
let citationStrings = await Promise.all(citations.map(citation => citation.field.getText()));
this._filteredCitations = new Set();
citationStrings.forEach((str, index) => {
if (!this._normalizeSearch(str).includes(searchString)) {
this._filteredCitations.add(citations[index].citationID);
}
});
await this.refreshCitationList();
},
onItemFilter: async function () {
let searchString = this._normalizeSearch(document.querySelector('#item-search').value);
let itemStrings = itemRows.map((item) => {
return [item.getField('title'), item.getField('firstCreator'), item.getField('date')].join(' ');
});
this._filteredItems = new Set();
itemStrings.forEach((str, index) => {
if (!this._normalizeSearch(str).includes(searchString)) {
this._filteredItems.add(itemRows[index].id);
}
});
await this.refreshItemList();
},
onSelectTab: async function (selectedIndex) {
if (selectedIndex === selectedTab) return;
selectedTab = selectedIndex;
if (selectedTab) {
this._highlightedCitations = new Set();
document.querySelector('#button-show-in-zotero').hidden = true;
document.querySelector('#button-relink-item').hidden = false;
document.querySelector('#button-addTo-library').style.display = 'none';
}
await this.refreshCitationList();
await this.refreshItemList();
},
_initMappings: async function () {
itemRows = items.map((item) => {
let citedIn = [];
return new Proxy(item, {
get(target, prop) {
if (prop == 'id' && !target.id) {
return target.cslItemID;
}
if (prop == 'citedIn') {
return citedIn;
}
return Reflect.get(...arguments);
}
});
});
uncitedItemRows = uncitedItems.map((item) => {
return new Proxy(item, {
get(target, prop) {
if (prop == 'citedIn') {
return [];
}
return Reflect.get(...arguments);
}
});
});
citationRows = await Promise.all(citations
.map(async (citation, citationIndex) => {
let isLinked = true;
let citedItems = [];
// check if all citation items are linked
for (let citationItem of citation.citationItems) {
itemRows.forEach((itemRow, itemIndex) => {
if ([itemRow.id, itemRow.cslItemID].includes(citationItem.id)) {
citedItems.push(itemIndex);
itemRow.citedIn.push(citationIndex);
}
});
if (typeof citationItem.id != 'number') {
isLinked = false;
break;
}
}
let title = await citation.field.getText();
if (citation.properties.plainCitation != title) {
let d = diff(citation.properties.plainCitation, title);
title = d.map(([type, text]) => {
if (type == 0) return text;
if (type == -1) return `<span style="color: red; text-decoration: line-through">${text}</span>`;
if (type == 1) return `<span style="color: green">${text}</span>`;
}).join('');
}
return {
title: title,
isLinked: isLinked,
citedItems,
highlighted: this._highlightedCitations.has(citation.citationID),
ref: citation
};
}));
},
/**
* Select citation in text
* @returns {Promise<void>}
* @private
*/
onCitationActivate: async function () {
if (disableCitationActivate) return;
const citation = citations[citationList.selection.focused];
try {
await io.selectCitation(citation);
const isCitationActivated = await io.cursorInCitation(citation);
if (isCitationActivated) {
await io.activateDocument();
return;
}
}
catch (e) { }
// An error got thrown or wrong citation got activated, which means that some citations got deleted
// and now the citation explorer dialog is not showing correct citations and citation
// activation is not going to work right.
var ps = Services.prompt;
var title = Zotero.getString('general.warning');
var message = Zotero.getString('integration.citationExplorer.citationsModified', [Zotero.appName]);
ps.alert(window, title, message);
disableCitationActivate = true;
document.querySelector('#button-show-in-document').disabled = true;
document.querySelector('#button-edit-citation').disabled = true;
},
/**
* Highlight items that are cited in citation
* @returns {Promise<void>}
* @private
*/
onCitationSelectionChange: async function () {
let highlightedItems = [];
for (let index of citationList.selection.selected) {
for (let item of citations[index].citationItems) {
if (item.cslItemID) {
highlightedItems.push(item.cslItemID);
}
else {
highlightedItems.push(item.id);
}
}
}
itemList.setHighlightedRows(highlightedItems);
},
onCitationEdit: async function () {
let citation = citations[citationList.selection.focused];
io.openCitationDialog = citation._field;
window.close();
},
/**
* Highlight citations that contain item
* @returns {Promise<void>}
* @private
*/
onItemSelectionChange: async function () {
if (selectedTab === 1) return;
this._highlightedCitations = new Set();
for (let selectedItemIndex of itemList.selection.selected) {
for (let citationIndex of itemRows[selectedItemIndex].citedIn) {
this._highlightedCitations.add(citations[citationIndex].citationID);
}
}
const item = itemList.getRow(itemList.selection.focused).ref;
const isUnlinked = typeof item.id != 'number';
const isMultiple = itemList.selection.selected.size > 1;
document.querySelector('#button-show-in-zotero').hidden = isMultiple || isUnlinked;
document.querySelector('#button-relink-item').hidden = isMultiple || !isUnlinked;
document.querySelector('#button-addTo-library').style.display = (isMultiple || !isUnlinked) ? 'none' : 'inherit';
await this.refreshCitationList();
},
onItemActivate: async function () {
if (itemList.selection.selected.size > 1) return;
const item = itemList.getRow(itemList.selection.focused).ref;
if (typeof item.id != 'number') {
this.onItemRelink();
}
else {
await Zotero.Utilities.Internal.showInLibrary(item.id);
}
},
onItemRelink: async function () {
let io = { dataIn: null, dataOut: null, multiSelect: false, deferred: Zotero.Promise.defer() };
window.openDialog('chrome://zotero/content/selectItemsDialog.xhtml', '',
'chrome,dialog=no,centerscreen,resizable=yes', io);
await io.deferred.promise;
if (!io.dataOut || !io.dataOut.length) {
return;
}
let items = await Zotero.Items.getAsync(io.dataOut);
if (!items.length) {
return;
}
let treeRow = itemList.getRow(itemList.selection.focused);
const oldItemID = treeRow.id;
const itemIdx = itemRows.findIndex(row => row.id === treeRow.id);
this._linkItem(items[0], oldItemID, itemIdx);
await this._initMappings();
await this.refreshCitationList();
await this.refreshItemList();
},
async addToLibraryAndLink() {
var collectionID = _addToTarget.objectType == 'collection' ? _addToTarget.id : undefined;
for (let index of itemList.selection.selected) {
let treeRow = itemList.getRow(index);
const oldItemID = treeRow.id;
const itemIdx = itemRows.findIndex(row => row.id === treeRow.id);
// Save item
let item = treeRow.ref.clone(_addToTarget.libraryID);
if (collectionID) {
item.addToCollection(collectionID);
}
await item.saveTx();
this._linkItem(item, oldItemID, itemIdx);
}
await this._initMappings();
await this.refreshCitationList();
await this.refreshItemList();
},
_linkItem(item, oldItemID, itemIdx) {
// For all citations where the item is cited
for (let citationIndex of itemRows[itemIdx].citedIn) {
let citation = citations[citationIndex];
let citationItemIdx = citation.citationItems.findIndex(i => i.id == oldItemID);
let citationItem = citation.citationItems[citationItemIdx];
// Update the citation with the new item
citationItem.id = item.id;
citationItem.uris = Zotero.Integration.currentSession.uriMap.getURIsForItemID(citationItem.id);
// Mark citation for an update with citeproc and write changes to doc
io.updateIndex(citationIndex);
}
items[itemIdx] = item;
},
buildAddToLibraryContextMenu(event) {
var menu = document.querySelector('#item-addTo-menu');
// Don't trigger rebuilding on nested popupmenu open/close
if (event.target != menu) {
return;
}
// Clear previous items
while (menu.firstChild) {
menu.removeChild(menu.firstChild);
}
let target = Zotero.Prefs.get('documentCitations.lastAddToTarget');
if (!target) {
target = "L" + Zotero.Libraries.userLibraryID;
}
var libraries = Zotero.Libraries.getAll();
for (let library of libraries) {
if (!library.editable || library.libraryType == 'publications') {
continue;
}
Zotero.Utilities.Internal.createMenuForTarget(
library,
menu,
target,
function(event, libraryOrCollection) {
if (event.target.tagName == 'menu') {
Zotero.Promise.coroutine(function* () {
// Simulate menuitem flash on OS X
if (Zotero.isMac) {
event.target.setAttribute('_moz-menuactive', false);
yield Zotero.Promise.delay(50);
event.target.setAttribute('_moz-menuactive', true);
yield Zotero.Promise.delay(50);
event.target.setAttribute('_moz-menuactive', false);
yield Zotero.Promise.delay(50);
event.target.setAttribute('_moz-menuactive', true);
}
menu.hidePopup();
ZoteroDocumentCitations.setAddToTarget(libraryOrCollection);
event.stopPropagation();
})();
}
else {
ZoteroDocumentCitations.setAddToTarget(libraryOrCollection);
event.stopPropagation();
}
}
);
}
},
setAddToTarget(translationTarget) {
_addToTarget = translationTarget;
Zotero.Prefs.set('documentCitations.lastAddToTarget', translationTarget.treeViewID);
this.setAddToButton();
},
setAddToButton() {
var label = Zotero.getString('pane.item.addTo', _addToTarget.name);
var elem = document.querySelector('#button-addTo-library');
elem.label = label;
elem.title = label;
elem.image = _addToTarget.treeViewImage;
},
/**
* @param {String} s
* @return {String}
*/
_normalizeSearch(s) {
return Zotero.Utilities.removeDiacritics(
Zotero.Utilities.trimInternal(s).toLowerCase(),
true);
},
};
window.addEventListener('DOMContentLoaded', function () {
ZoteroDocumentCitations.init();
});

View file

@ -0,0 +1,101 @@
<?xml version="1.0"?>
<!--
***** BEGIN LICENSE BLOCK *****
Copyright © 2023 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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
-->
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css" type="text/css"?>
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css"?>
<?xml-stylesheet href="chrome://zotero/skin/integration.css" type="text/css"?>
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
<xul:window
id="zotero-citation-explorer-dialog"
orient="vertical"
title="Citation Explorer"
width="750" height="450"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
persist="screenX screenY width height"
resizable="true">
<xul:dialog
buttons="accept" buttonpack="end">
<script>
var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
Services.scriptloader.loadSubScript("chrome://zotero/content/include.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/customElements.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/integration/citationExplorer.js", this);
</script>
<div class="vbox flex xul-border-div">
<div class="hbox flex">
<div class="vbox flex">
<div id="citations-label">Citations</div>
<div class="vbox flex panel" style="border-right: none">
<xul:search-textbox id="citation-search" class="search-box" timeout="250" oncommand="ZoteroDocumentCitations.onCitationFilter()" dir="reverse"/>
<div id="citation-list-container" class="virtualized-table-container">
<div id="citation-list"></div>
</div>
<div class="hbox button-container">
<input type="button" id="button-show-in-document" value="Show in Document"/>
<input type="button" id="button-edit-citation" value="Edit Citation"/>
</div>
</div>
</div>
<div class="splitter"></div>
<div class="vbox flex">
<xul:tabbox onselect="ZoteroDocumentCitations.onSelectTab(this.selectedIndex)">
<xul:tabs>
<xul:tab id="tab-cited-items" label="Cited Items"/>
<xul:tab id="tab-uncited-items" label="Uncited Items"/>
</xul:tabs>
<!--We are making our own tabpanel UI, but the onselect event above won't fire without these-->
<xul:tabpanels hidden="true"><xul:tabpanel/><xul:tabpanel/></xul:tabpanels>
</xul:tabbox>
<div id="items-panel" class="vbox flex panel">
<xul:search-textbox id="item-search" class="search-box" timeout="250" oncommand="ZoteroDocumentCitations.onItemFilter()" dir="reverse"/>
<div id="item-list-container" class="virtualized-table-container">
<div id="item-list"></div>
</div>
<div class="hbox button-container">
<input type="button" id="button-show-in-zotero" value="Show in Zotero"/>
<input type="button" id="button-relink-item" value="Relink Item" hidden="true"/>
<button is="split-menu-button" id="button-addTo-library"
onclick="ZoteroDocumentCitations.addToLibraryAndLink()"
popup="item-addTo-menu" style="display: none"/>
<xul:popupset>
<xul:menupopup id="item-addTo-menu" onpopupshowing="ZoteroDocumentCitations.buildAddToLibraryContextMenu(event);"/>
</xul:popupset>
</div>
</div>
</div>
</div>
</div>
</xul:dialog>
</xul:window>

View file

@ -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);
}
/**

View file

@ -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);

View file

@ -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: <Icons.IconAttachSmall />,
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"]
},
{

View file

@ -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');
}
}

View file

@ -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);
}

View file

@ -1700,6 +1700,7 @@ Zotero.Server.Connector.Ping.prototype = {
prefs: {
automaticSnapshots: Zotero.Prefs.get('automaticSnapshots'),
googleDocsAddNoteEnabled: true,
googleDocsCitationExplorerEnabled: true,
translatorsHash,
sortedTranslatorHash
}

View file

@ -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();

View file

@ -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;

View file

@ -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()) {

View file

@ -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"

View file

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.75 -3.05176e-05C15.4404 -3.05176e-05 16 0.559614 16 1.24997V9.67134C15.6534 9.24296 15.2297 8.87949 14.75 8.60202V6.99997L7 6.99997V14.75H8.60199C8.87946 15.2296 9.24292 15.6533 9.67129 16H1.25C0.559646 16 0 15.4403 0 14.75V1.24997C0 0.559614 0.559646 -3.05176e-05 1.25 -3.05176e-05H14.75ZM1.25 14.75H5.75V6.99997L1.25 6.99997L1.25 14.75ZM14.75 5.74997V1.24997L1.25 1.24997L1.25 5.74997H14.75ZM7 4.12497H3V2.87497H7V4.12497ZM18 17.1161L17.1161 18L14.4933 15.3773C13.9277 15.7699 13.2407 16 12.5 16C10.567 16 8.99998 14.433 8.99998 12.5C8.99998 10.567 10.567 8.99999 12.5 8.99999C14.433 8.99999 16 10.567 16 12.5C16 13.2407 15.7699 13.9277 15.3772 14.4934L18 17.1161ZM14.75 12.5C14.75 13.7426 13.7426 14.75 12.5 14.75C11.2573 14.75 10.25 13.7426 10.25 12.5C10.25 11.2573 11.2573 10.25 12.5 10.25C13.7426 10.25 14.75 11.2573 14.75 12.5Z" fill="black" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.75 1.25V5.75H1.25V1.25H14.75ZM16 1.25C16 0.559644 15.4404 0 14.75 0H1.25C0.559644 0 0 0.559644 0 1.25V5.75C0 6.44036 0.559644 7 1.25 7H14.75C15.4404 7 16 6.44036 16 5.75V1.25ZM7 4.125H3V2.875H7V4.125ZM8.60201 14.75C8.87947 15.2297 9.24293 15.6534 9.67131 16H2V14.75H8.60201ZM8.06222 11.75C8.0213 11.9939 8 12.2445 8 12.5C8 12.669 8.00932 12.8358 8.02746 13H2V11.75H8.06222ZM8.75779 10C9.08901 9.50518 9.51578 9.07972 10.0117 8.75H2V10H8.75779ZM18 17.1161L17.1162 18L14.4933 15.3773C13.9277 15.7699 13.2407 16 12.5 16C10.567 16 9 14.433 9 12.5C9 10.567 10.567 9.00002 12.5 9.00002C14.433 9.00002 16 10.567 16 12.5C16 13.2408 15.7699 13.9278 15.3772 14.4934L18 17.1161ZM14.75 12.5C14.75 13.7427 13.7426 14.75 12.5 14.75C11.2574 14.75 10.25 13.7427 10.25 12.5C10.25 11.2574 11.2574 10.25 12.5 10.25C13.7426 10.25 14.75 11.2574 14.75 12.5Z" fill="black" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 2.75C11.4142 2.75 11.75 2.41421 11.75 2C11.75 1.58579 11.4142 1.25 11 1.25C10.5858 1.25 10.25 1.58579 10.25 2C10.25 2.41421 10.5858 2.75 11 2.75ZM12.9004 2.625C12.638 3.42348 11.8863 4 11 4C10.1137 4 9.36204 3.42348 9.0996 2.625H0V1.375H9.0996C9.36204 0.576517 10.1137 0 11 0C11.8863 0 12.638 0.576517 12.9004 1.375L16 1.375V2.625L12.9004 2.625ZM5 8.75C5.41421 8.75 5.75 8.41421 5.75 8C5.75 7.58579 5.41421 7.25 5 7.25C4.58579 7.25 4.25 7.58579 4.25 8C4.25 8.41421 4.58579 8.75 5 8.75ZM5 10C5.8863 10 6.63796 9.42348 6.90041 8.625L16 8.625V7.375L6.90041 7.375C6.63796 6.57652 5.8863 6 5 6C4.1137 6 3.36204 6.57652 3.09959 7.375H0V8.625H3.09959C3.36204 9.42348 4.1137 10 5 10ZM11.75 14C11.75 14.4142 11.4142 14.75 11 14.75C10.5858 14.75 10.25 14.4142 10.25 14C10.25 13.5858 10.5858 13.25 11 13.25C11.4142 13.25 11.75 13.5858 11.75 14ZM12.9004 14.625C12.638 15.4235 11.8863 16 11 16C10.1137 16 9.36204 15.4235 9.0996 14.625H0V13.375H9.0996C9.36204 12.5765 10.1137 12 11 12C11.8863 12 12.638 12.5765 12.9004 13.375H16V14.625H12.9004Z" fill="black" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

774
resource/diff.js Normal file
View file

@ -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.<string>} 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.<string>} 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: A<ins>BA</ins>C -> <ins>AB</ins>AC
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;

View file

@ -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";

View file

@ -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
}

View file

@ -0,0 +1,7 @@
#citation-dialog {
min-width: 800px;
.layout {
display: contents;
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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 {

View file

@ -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 {

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -11,5 +11,6 @@
@import "mac/titleBar";
@import "mac/components/menupopup";
@import "mac/components/menulist";
@import "mac/citationExplorer";
// Elements

View file

@ -9,6 +9,7 @@
@import "win/createParent";
@import "win/tabBar";
@import "win/titleBar";
@import "win/citationExplorer";
// Elements