Improve the new UI:

- Revert `notesList` to standalone notes mode only
- Fix splitter styling on Windows
- Fix `contextPane` performance fixes
- Add a customizable throttle function to `Zotero.Utilities`
- Fix reader tab selection issues
- Improve error handling in the new editor
- Update pdf-reader and zotero-note-editor submodules
- Fix contextPane child notes list
- Properly notify editor about new images
- Fix note title and body snippet extraction
- Persist right-hand pane state when switching tab
- Hopefully fix two panes visible at the same time
This commit is contained in:
Martynas Bagdonas 2020-12-22 15:25:13 +02:00 committed by Dan Stillman
parent c6a196b999
commit 51f760fe1a
12 changed files with 211 additions and 141 deletions

View file

@ -61,7 +61,9 @@
#zotero-collections-splitter:not([state=collapsed]),
#zotero-items-splitter:not([state=collapsed]),
#zotero-tags-splitter:not([state=collapsed]) {
#zotero-tags-splitter:not([state=collapsed]),
#zotero-context-splitter:not([state=collapsed]),
#zotero-context-splitter-stacked:not([state=collapsed]) {
border: 0;
background-color: transparent;
position: relative;
@ -70,9 +72,15 @@
z-index: 1;
}
#zotero-context-splitter:not([state=collapsed]),
#zotero-context-splitter-stacked:not([state=collapsed]) {
z-index: 0;
}
#zotero-collections-splitter:not([state=collapsed]),
#zotero-items-splitter:not([state=collapsed]):not([orient=vertical]),
#zotero-tags-splitter:not([state=collapsed]) {
#zotero-tags-splitter:not([state=collapsed]),
#zotero-context-splitter:not([state=collapsed]) {
border-inline-end: 1px solid var(--theme-border-color);
min-width: 0;
width: 3px;
@ -80,7 +88,8 @@
}
#zotero-tags-splitter:not([state=collapsed]),
#zotero-items-splitter:not([state=collapsed])[orient=vertical] {
#zotero-items-splitter:not([state=collapsed])[orient=vertical],
#zotero-context-splitter-stacked:not([state=collapsed]) {
border-block-end: 1px solid var(--theme-border-color);
min-height: 0;
height: 3px;
@ -89,13 +98,16 @@
#zotero-collections-splitter > grippy,
#zotero-items-splitter > grippy,
#zotero-tags-splitter > grippy {
#zotero-tags-splitter > grippy,
#zotero-context-splitter > grippy {
border: 0;
}
#zotero-collections-splitter:not([state=collapsed]) > grippy,
#zotero-items-splitter:not([state=collapsed]) > grippy,
#zotero-tags-splitter:not([state=collapsed]) > grippy {
#zotero-tags-splitter:not([state=collapsed]) > grippy,
#zotero-context-splitter:not([state=collapsed]) > grippy,
#zotero-context-splitter-stacked:not([state=collapsed]) > grippy {
display: none;
}

View file

@ -109,18 +109,8 @@
}
this.notify = async (event, type, ids, extraData) => {
// Update citations
let uris = [];
let items = await Zotero.Items.getAsync(ids);
for (let item of items) {
let uri = Zotero.URI.getItemURI(item);
if (uri) {
uris.push(uri)
}
}
if (this._editorInstance) {
await this._editorInstance.updateCitationsForURIs(uris);
await this._editorInstance.notify(ids);
}
if (!this.item) return;
@ -252,15 +242,14 @@
<property name="linksOnTop">
<setter>
<![CDATA[
return;
if (val) {
var container = this._id('links-container');
var parent = container.parentNode;
var sib = container.nextSibling;
while (parent.firstChild !== container) {
parent.insertBefore(parent.removeChild(parent.firstChild), sib);
}
}
// if (val) {
// var container = this._id('links-container');
// var parent = container.parentNode;
// var sib = container.nextSibling;
// while (parent.firstChild !== container) {
// parent.insertBefore(parent.removeChild(parent.firstChild), sib);
// }
// }
]]>
</setter>
</property>

View file

@ -25,18 +25,14 @@
import React, { forwardRef, useImperativeHandle, useState } from 'react';
const NoteRow = ({ parentTitle, parentImageSrc, title, body, date, onClick }) => {
const NoteRow = ({ title, body, date, onClick }) => {
return (
<div className="note-row" onClick={onClick}>
<div className="inner">
{parentTitle !== null && <div className="first-line">
<div className="icon"><img src={parentImageSrc}/></div>
<div className="title">{parentTitle}</div>
</div>}
<div className="second-line">
<div className="first-line">
<div className="title">{title}</div>
</div>
<div className="third-line">
<div className="second-line">
<div className="date">{date}</div>
<div className="body">{body}</div>
</div>

View file

@ -82,43 +82,66 @@ var ZoteroContextPane = new function () {
_init();
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'contextPane');
this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'contextPane');
window.addEventListener('resize', _update);
_itemToggle.addEventListener('click', () => {
_togglePane(0);
});
_notesToggle.addEventListener('click', () => {
_togglePane(1);
});
_itemToggle.addEventListener('click', _toggleItemButton);
_notesToggle.addEventListener('click', _toggleNotesButton);
Zotero.Reader.onChangeSidebarWidth = _updatePaneWidth;
Zotero.Reader.onChangeSidebarOpen = _updatePaneWidth;
const observer = new MutationObserver(() => {
this._mutationObserver = new MutationObserver(() => {
_updateToolbarWidth();
// Sometimes XUL is late to reflow
setTimeout(_updateToolbarWidth, 100);
});
observer.observe(_tabToolbarContainer, { attributes: true, childList: true, subtree: true });
this._mutationObserver.observe(_tabToolbarContainer, { attributes: true, childList: true, subtree: true });
};
this.onUnload = function () {
Zotero.Notifier.unregisterObserver(this._unregisterID);
_itemToggle.removeEventListener('click', _toggleItemButton);
_notesToggle.removeEventListener('click', _toggleNotesButton);
window.removeEventListener('resize', _update);
Zotero.Notifier.unregisterObserver(this._notifierID);
this._mutationObserver.disconnect();
Zotero.Reader.onChangeSidebarWidth = () => {};
Zotero.Reader.onChangeSidebarOpen = () => {};
_contextPaneInner.innerHTML = '';
_itemContexts = [];
_notesContexts = [];
};
this.notify = Zotero.Promise.coroutine(function* (action, type, ids, extraData) {
if (type == 'item') {
// TODO: Filter library and seriously thing about the performance of this part
for (let context of _itemContexts) {
// Update, remove or re-create item panes
for (let context of _itemContexts.slice()) {
let item = Zotero.Items.get(context.itemID);
if (item && item.parentID != context.parentID) {
if (!item) {
_removeItemContext(context.tabID);
}
else if (item.parentID != context.parentID) {
_removeItemContext(context.tabID);
_addItemContext(context.tabID, context.itemID);
}
else {
context.update();
}
}
// Update notes lists for affected libraries
let libraryIDs = [];
for (let id of ids) {
let item = Zotero.Items.get(id);
if (item && item.isNote()) {
libraryIDs.push(item.libraryID);
}
else if (action == 'delete') {
libraryIDs.push(extraData[id].libraryID);
}
}
for (let context of _notesContexts) {
context.updateNotesList();
if (libraryIDs.includes(context.libraryID)) {
context.update();
}
}
}
else if (type == 'tab') {
@ -157,12 +180,20 @@ var ZoteroContextPane = new function () {
if (context) {
_selectNotesContext(context.libraryID);
}
_selectItemContext(ids[0], extraData[ids[0]].type);
_selectItemContext(ids[0]);
_update();
}
}
});
function _toggleItemButton() {
_togglePane(0);
}
function _toggleNotesButton() {
_togglePane(1);
}
function _removeNote(id) {
var ps = Components.classes['@mozilla.org/embedcomp/prompt-service;1']
.getService(Components.interfaces.nsIPromptService);
@ -183,6 +214,7 @@ var ZoteroContextPane = new function () {
if (splitter.getAttribute('state') != 'collapsed') {
if (_panesDeck.selectedIndex == 0) {
let child = _itemPaneDeck.selectedPanel;
if (child) {
var tabPanels = child.querySelector('tabpanels');
if (tabPanels && tabPanels.selectedIndex == 1) {
var notesDeck = child.querySelector('.notes-deck');
@ -191,6 +223,7 @@ var ZoteroContextPane = new function () {
}
}
}
}
else {
var node = _notesPaneDeck.selectedPanel;
if (node.selectedIndex == 1) {
@ -286,20 +319,12 @@ var ZoteroContextPane = new function () {
}
function _togglePane(paneIndex) {
var splitter;
var stacked = Zotero.Prefs.get('layout') == 'stacked';
if (stacked) {
splitter = _contextPaneSplitterStacked;
}
else {
splitter = _contextPaneSplitter;
}
var splitter = Zotero.Prefs.get('layout') == 'stacked'
? _contextPaneSplitterStacked : _contextPaneSplitter;
var isOpen = splitter.getAttribute('state') != 'collapsed';
var hide = false;
var currentPane = _panesDeck.selectedIndex;
if (isOpen && currentPane == paneIndex) {
hide = true;
}
@ -307,9 +332,6 @@ var ZoteroContextPane = new function () {
_panesDeck.setAttribute('selectedIndex', paneIndex);
}
var context = _itemContexts.find(x => x.tabID == Zotero_Tabs.selectedID);
context.selectedIndex = paneIndex;
splitter.setAttribute('state', hide ? 'collapsed' : 'open');
_update();
}
@ -371,7 +393,7 @@ var ZoteroContextPane = new function () {
var label = document.createElement('label');
var button = document.createElement('button');
button.setAttribute('label', Zotero.Intl.strings['zotero.item.add']);
button.setAttribute('label', Zotero.Intl.strings['zotero.toolbar.newNote']);
button.addEventListener('click', () => {
contextNode.setAttribute('selectedIndex', 1);
var item = new Zotero.Item('note');
@ -394,7 +416,7 @@ var ZoteroContextPane = new function () {
input.setAttribute('type', 'search');
input.setAttribute('timeout', '250');
input.addEventListener('command', () => {
updateNotesList();
_updateNotesList();
});
vbox2.append(input);
@ -411,20 +433,20 @@ var ZoteroContextPane = new function () {
var notesListRef = React.createRef();
var updateNotesList = async (reset) => {
async function _updateNotesList(reset) {
if (reset) {
input.value = '';
contextNode.setAttribute('selectedIndex', 0);
}
var text = input.value;
var query = input.value;
await Zotero.Schema.schemaUpdatePromise;
var s = new Zotero.Search();
s.addCondition('libraryID', 'is', libraryID);
s.addCondition('itemType', 'is', 'note');
s.addCondition('noChildren', 'false');
if (text) {
s.addCondition('note', 'contains', text, true);
s.addCondition('noChildren', 'true');
if (query) {
s.addCondition('note', 'contains', query, true);
}
var notes = await s.search();
notes = Zotero.Items.get(notes);
@ -435,20 +457,15 @@ var ZoteroContextPane = new function () {
});
notesListRef.current.setNotes(notes.map(note => {
var text2 = note.note.slice(0, 500);
text2 = text2.trim();
// TODO: Fix a potential performance issuse
text2 = Zotero.Utilities.unescapeHTML(text2);
var parts = text2.split('\n').map(x => x.trim()).filter(x => x.length);
var parent = null;
if (note.parentID) {
parent = Zotero.Items.get(note.parentID);
}
var text = note.note;
text = Zotero.Utilities.unescapeHTML(text);
text = text.trim();
text = text.slice(0, 500);
var parts = text.split('\n').map(x => x.trim()).filter(x => x.length);
var title = parts[0] && parts[0].slice(0, Zotero.Notes.MAX_TITLE_LENGTH);
return {
id: note.id,
parentTitle: parent && parent.getDisplayTitle(),
parentImageSrc: parent && parent.getImageSrc(),
title: parts[0] || Zotero.getString('pane.item.notes.untitled'),
title: title || Zotero.getString('pane.item.notes.untitled'),
body: parts[1] || '',
date: (new Date(note.dateModified).toLocaleDateString(Zotero.locale))
};
@ -468,14 +485,14 @@ var ZoteroContextPane = new function () {
/>,
listInner,
() => {
updateNotesList();
_updateNotesList();
}
);
var context = {
libraryID,
node: contextNode,
updateNotesList,
update: Zotero.Utilities.throttle(_updateNotesList, 1000, { leading: false }),
editor
};
@ -503,7 +520,7 @@ var ZoteroContextPane = new function () {
_notesContexts = _notesContexts.filter(x => x.libraryID != libraryID);
}
function _libraryEditable(libraryID) {
function _isLibraryEditable(libraryID) {
var type = Zotero.Libraries.get(libraryID).libraryType;
if (type == 'group') {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
@ -514,7 +531,7 @@ var ZoteroContextPane = new function () {
}
function _setPinnedNote(libraryID, itemID) {
var editable = _libraryEditable(libraryID);
var editable = _isLibraryEditable(libraryID);
var context = _getNotesContext(libraryID);
if (context) {
let { editor, node } = context;
@ -530,7 +547,7 @@ var ZoteroContextPane = new function () {
function _appendNoteRows(notes, list, editable, onClick, onDelete) {
for (var i = 0; i < notes.length; i++) {
var note = notes[i];
var id = notes[i].id;
let id = notes[i].id;
var icon = document.createElement('image');
icon.className = 'zotero-box-icon';
@ -552,6 +569,8 @@ var ZoteroContextPane = new function () {
box.appendChild(icon);
box.appendChild(label);
var row = document.createElement('row');
row.appendChild(box);
if (editable) {
var removeButton = document.createElement('label');
removeButton.setAttribute('value', '-');
@ -559,11 +578,6 @@ var ZoteroContextPane = new function () {
removeButton.addEventListener('click', function () {
onDelete(id);
});
}
var row = document.createElement('row');
row.appendChild(box);
if (editable) {
row.appendChild(removeButton);
}
@ -576,34 +590,32 @@ var ZoteroContextPane = new function () {
_itemContexts = _itemContexts.filter(x => x.tabID != tabID);
}
function _selectItemContext(tabID, type) {
function _selectItemContext(tabID) {
let selectedIndex = Array.from(_itemPaneDeck.children).findIndex(x => x.id == tabID + '-context');
if (selectedIndex != -1) {
_itemPaneDeck.setAttribute('selectedIndex', selectedIndex);
var context = _itemContexts.find(x => x.tabID == tabID);
if (context && Zotero_Tabs.selectedIndex > 0) {
_panesDeck.setAttribute('selectedIndex', context.selectedIndex);
}
}
}
function _addItemContext(tabID, itemID) {
var item = Zotero.Items.get(itemID);
if (!item) {
return;
}
var libraryID = item.libraryID;
var editable = _isLibraryEditable(libraryID);
var parentID = item.parentID;
var container = document.createElement('vbox');
container.id = tabID + '-context';
container.className = 'zotero-item-pane-content';
_itemPaneDeck.appendChild(container);
var item = Zotero.Items.get(itemID);
var libraryID = item.libraryID;
var editable = _libraryEditable(libraryID);
var parentID = item.parentID;
var context = {
tabID,
itemID,
parentID,
libraryID,
selectedIndex: 0,
update: () => {}
};
_itemContexts.push(context);
@ -619,7 +631,6 @@ var ZoteroContextPane = new function () {
container.append(vbox);
return;
}
var parentItem = Zotero.Items.get(item.parentID);
// tabbox
@ -685,7 +696,6 @@ var ZoteroContextPane = new function () {
hbox.setAttribute('align', 'center');
var label = document.createElement('label');
var button = document.createElement('button');
// TODO: Should not depend on the current ZoteroPane state
button.hidden = !editable;
button.setAttribute('label', Zotero.Intl.strings['zotero.item.add']);
button.addEventListener('click', () => {
@ -763,6 +773,10 @@ var ZoteroContextPane = new function () {
function _renderNotesPanel() {
rows.innerHTML = '';
var parentItem = Zotero.Items.get(parentID);
if (!parentItem) {
return;
}
var parentNotes = Zotero.Items.get(parentItem.getNotes());
_appendNoteRows(parentNotes, rows, editable, (id) => {
deck.setAttribute('selectedIndex', 1);
@ -782,7 +796,7 @@ var ZoteroContextPane = new function () {
label.value = Zotero.getString(str, [c]);
}
context.update = _renderNotesPanel;
context.update = Zotero.Utilities.throttle(_renderNotesPanel, 500);
_renderNotesPanel();
}
};

View file

@ -113,7 +113,7 @@ var Zotero_Tabs = new function () {
index = index || this._tabs.length;
this._tabs.splice(index, 0, tab);
this._update();
Zotero.Notifier.trigger('add', 'tab', [id], { [id]: notifierData });
Zotero.Notifier.trigger('add', 'tab', [id], { [id]: notifierData }, true);
if (select) {
this.select(id);
}
@ -157,7 +157,7 @@ var Zotero_Tabs = new function () {
if (tab.onClose) {
tab.onClose();
}
Zotero.Notifier.trigger('close', 'tab', [tab.id]);
Zotero.Notifier.trigger('close', 'tab', [tab.id], true);
this._update();
};
@ -199,7 +199,7 @@ var Zotero_Tabs = new function () {
this._selectedID = id;
this.deck.selectedIndex = Array.from(this.deck.children).findIndex(x => x.id == id);
this._update();
Zotero.Notifier.trigger('select', 'tab', [tab.id], { [tab.id]: { type: tab.type } });
Zotero.Notifier.trigger('select', 'tab', [tab.id], { [tab.id]: { type: tab.type } }, true);
};
/**

View file

@ -101,12 +101,29 @@ class EditorInstance {
async updateCitationsForURIs(uris) {
let subscriptions = this._subscriptions
.filter(s => s.data.citation && s.data.citation.citationItems
.some(citationItem => uris.some(uri => citationItem.uris.includes(uri))));
.some(citationItem => citationItem.uris && uris.some(uri => citationItem.uris.includes(uri))));
for (let subscription of subscriptions) {
await this._feedSubscription(subscription);
}
}
async notify(ids) {
let items = await Zotero.Items.getAsync(ids);
// Update attachments
let keys = items.map(item => item.key);
this._subscriptions
.filter(s => keys.includes(s.data.attachmentKey))
.forEach(s => this._feedSubscription(s));
// Update citations
let uris = items.map(x => Zotero.URI.getItemURI(x)).filter(x => x);
this._subscriptions
.filter(s => s.data.citation && s.data.citation.citationItems
.some(citationItem => citationItem.uris && uris.some(uri => citationItem.uris.includes(uri))))
.forEach(s => this._feedSubscription(s));
}
saveSync() {
if (!this._readOnly && !this._disableSaving && this._iframeWindow) {
let noteData = this._iframeWindow.wrappedJSObject.getDataSync();
@ -600,6 +617,9 @@ class EditorInstance {
async _getFormattedCitationParts(citation) {
let formattedItems = [];
for (let citationItem of citation.citationItems) {
if (!Array.isArray(citationItem.uris)) {
continue;
}
let item = await this._getItemFromURIs(citationItem.uris);
if (!item && citationItem.itemData) {
item = new Zotero.Item();

View file

@ -755,6 +755,10 @@ class Reader {
}
if (reader) {
if (reader instanceof ReaderTab) {
reader._window.Zotero_Tabs.select(reader.tabID);
}
if (location) {
reader.navigate(location);
}
@ -806,9 +810,6 @@ class Reader {
if (reader instanceof ReaderWindow) {
reader._window.focus();
}
else {
reader._window.Zotero_Tabs.select(reader.tabID);
}
}
async triggerAnnotationsImportCheck(itemID) {

View file

@ -49,6 +49,62 @@ Zotero.Utilities = {
};
},
/**
* Creates and returns a new, throttled version of the
* passed function, that, when invoked repeatedly,
* will only actually call the original function at most
* once per every wait milliseconds
*
* By default, throttle will execute the function as soon
* as you call it for the first time, and, if you call it
* again any number of times during the wait period, as soon
* as that period is over. If you'd like to disable the
* leading-edge call, pass {leading: false}, and if you'd
* like to disable the execution on the trailing-edge,
* pass {trailing: false}. See
* https://underscorejs.org/#throttle
* https://github.com/jashkenas/underscore/blob/master/underscore.js
* (c) 2009-2018 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
* Underscore may be freely distributed under the MIT license.
*
* @param {Function} func Function to throttle
* @param {Integer} wait Wait period in milliseconds
* @param {Boolean} [options.leading] Call at the beginning of the wait period
* @param {Boolean} [options.trailing] Call at the end of the wait period
*/
throttle: function (func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function () {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function () {
var now = Date.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
}
else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
},
/**
* Fixes author name capitalization.
* Currently for all uppercase names only

View file

@ -3,7 +3,7 @@
}
#zotero-context-pane {
min-width: 300px;
min-width: 360px;
}
#zotero-context-pane.stacked {
@ -49,7 +49,7 @@
right: 0;
height: 32px;
-moz-appearance: none;
background: linear-gradient(to top, #aeaeae 0, #aeaeae 1px, #d3d3d3 1px, #e3e3e3 100%);
background: linear-gradient(to top, #a9a9a9 0, #a9a9a9 1px, #f6f6f6 1px, #f6f6f6 100%);
}
#zotero-tab-toolbar-container #zotero-tb-locate,
@ -63,7 +63,7 @@
opacity: 0.99;
width: 100%;
margin-inline-start: -5px;
background: linear-gradient(to top, #aeaeae 0, #aeaeae 1px, #d3d3d3 1px, #e3e3e3 100%);
background: linear-gradient(to top, #a9a9a9 0, #a9a9a9 1px, #f6f6f6 1px, #f6f6f6 100%);
}
.zotero-context-notes-list {

@ -1 +1 @@
Subproject commit be1fa9a77fb9090a1aad9a9cd40b8ebb22ee4281
Subproject commit 7595b7d83720941812a531b9a547deac626b24bc

View file

@ -31,24 +31,6 @@
.first-line {
display: flex;
.icon {
display: flex;
align-items: center;
}
.title {
flex-grow: 1;
width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 8px;
}
}
.second-line {
display: flex;
.title {
flex-grow: 1;
width: 0;
@ -59,7 +41,7 @@
}
}
.third-line {
.second-line {
display: flex;
.date {

@ -1 +1 @@
Subproject commit a271598218fa8f52cd6d571d5c1b28e913803409
Subproject commit db2bfc6cbb724dbb1a22bafc324357a1bd61c3b1