CEify contextPane

This commit is contained in:
windingwind 2024-01-31 14:37:52 +08:00 committed by Dan Stillman
parent b2ad17d604
commit b34859d882
16 changed files with 1010 additions and 811 deletions

View file

@ -32,6 +32,7 @@
"ZoteroPane_Local": false,
"ZoteroPane": false,
"Zotero_Tabs": false,
"ZoteroContextPane": false,
"IOUtils": false,
"NetUtil": false,
"FileUtils": false,

View file

@ -23,30 +23,45 @@
***** END LICENSE BLOCK *****
*/
var ZoteroContextPane = new function () {
var _tabCover;
var _contextPane;
var _contextPaneInner;
var _contextPaneSplitter;
var _contextPaneSplitterStacked;
var _sidenav;
var _panesDeck;
var _itemPaneDeck;
var _notesPaneDeck;
var _itemContexts = [];
var _notesContexts = [];
var _globalDeckIndex = [];
var _preventGlobalDeckChange = false;
let ZoteroContextPane = new function () {
let _tabCover;
let _contextPane;
let _contextPaneInner;
let _contextPaneSplitter;
let _contextPaneSplitterStacked;
let _librarySidenav;
let _readerSidenav;
// Using attribute instead of property to set 'selectedIndex'
// is more reliable
this.update = _update;
this.getActiveEditor = _getActiveEditor;
this.focus = _focus;
this.togglePane = _togglePane;
this.getActiveEditor = () => {
return _contextPaneInner._getActiveEditor();
};
this.focus = () => {
return _contextPaneInner._focus();
};
this.getSidenav = () => {
return Zotero_Tabs.selectedType == "library"
? _librarySidenav
: _readerSidenav;
};
this.getSplitter = () => {
return _isStacked()
? _contextPaneSplitterStacked
: _contextPaneSplitter;
};
this.showTabCover = (isShow) => {
_tabCover.classList.toggle('hidden', !isShow);
};
this.updateAddToNote = _updateAddToNote;
this.init = function () {
if (!Zotero) {
@ -55,261 +70,40 @@ var ZoteroContextPane = new function () {
_tabCover = document.getElementById('zotero-tab-cover');
_contextPane = document.getElementById('zotero-context-pane');
// <context-pane> CE
_contextPaneInner = document.getElementById('zotero-context-pane-inner');
_contextPaneSplitter = document.getElementById('zotero-context-splitter');
_contextPaneSplitterStacked = document.getElementById('zotero-context-splitter-stacked');
_sidenav = document.getElementById('zotero-context-pane-sidenav');
_librarySidenav = document.querySelector("#zotero-view-item-sidenav");
_readerSidenav = document.getElementById('zotero-context-pane-sidenav');
_panesDeck = document.createXULElement('deck');
_panesDeck.setAttribute('flex', 1);
_panesDeck.setAttribute('selectedIndex', 0);
_panesDeck.classList = "zotero-context-panes-deck";
_contextPaneInner.sidenav = _readerSidenav;
_contextPaneInner.append(_panesDeck);
// Item pane deck
_itemPaneDeck = document.createXULElement('deck');
// Notes pane deck
_notesPaneDeck = document.createXULElement('deck');
_notesPaneDeck.setAttribute('flex', 1);
_notesPaneDeck.className = 'notes-pane-deck';
_panesDeck.append(_itemPaneDeck, _notesPaneDeck);
_sidenav.contextNotesPane = _notesPaneDeck;
this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'contextPane');
window.addEventListener('resize', _update);
Zotero.Reader.onChangeSidebarWidth = _updatePaneWidth;
Zotero.Reader.onToggleSidebar = _updatePaneWidth;
_contextPaneInner.addEventListener("keypress", ZoteroPane.itemPane._itemDetails.handleKeypress);
};
this.destroy = function () {
window.removeEventListener('resize', _update);
_contextPaneInner.removeEventListener("keypress", ZoteroPane.itemPane._itemDetails.handleKeypress);
Zotero.Notifier.unregisterObserver(this._notifierID);
Zotero.Reader.onChangeSidebarWidth = () => {};
Zotero.Reader.onToggleSidebar = () => {};
_contextPaneInner.innerHTML = '';
_itemContexts = [];
_notesContexts = [];
};
this.notify = function (action, type, ids, extraData) {
if (type == 'item') {
// Update, remove or re-create item panes
for (let context of _itemContexts.slice()) {
let item = Zotero.Items.get(context.itemID);
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() || item.isRegularItem())) {
libraryIDs.push(item.libraryID);
}
else if (action == 'delete') {
libraryIDs.push(extraData[id].libraryID);
}
}
for (let context of _notesContexts) {
if (libraryIDs.includes(context.libraryID)) {
context.affectedIDs = new Set([...context.affectedIDs, ...ids]);
context.update();
}
}
}
else if (type == 'tab') {
if (action == 'add') {
_addItemContext(ids[0], extraData[ids[0]].itemID);
}
else if (action == 'close') {
_removeItemContext(ids[0]);
if (Zotero_Tabs.deck.children.length == 1) {
_notesContexts.forEach(x => x.notesList.expanded = false);
}
// Close tab specific notes if tab id no longer exists, but
// do that only when unloaded tab is reloaded
setTimeout(() => {
var contextNodes = Array.from(_notesPaneDeck.children);
for (let contextNode of contextNodes) {
var nodes = Array.from(contextNode.querySelector('.zotero-context-pane-tab-notes-deck').children);
for (let node of nodes) {
var tabID = node.getAttribute('data-tab-id');
if (!document.getElementById(tabID)) {
node.remove();
}
}
}
// For unknown reason fx102, unlike 60, sometimes doesn't automatically update selected index
_selectItemContext(Zotero_Tabs.selectedID);
});
}
else if (action == 'select') {
// It seems that changing `hidden` or `collapsed` values might
// be related with significant slow down when there are too many
// DOM nodes (i.e. 10k notes)
if (Zotero_Tabs.selectedType == 'library') {
_contextPaneSplitter.setAttribute('hidden', true);
_contextPane.setAttribute('collapsed', true);
_tabCover.classList.add('hidden');
_sidenav.hidden = true;
}
else if (Zotero_Tabs.selectedType == 'reader') {
if (_panesDeck.selectedIndex == 1
&& _notesPaneDeck.selectedPanel.selectedIndex != 2
&& !_preventGlobalDeckChange) {
let libraryID = _notesPaneDeck.selectedPanel.getAttribute('data-library-id');
_globalDeckIndex[libraryID] = _notesPaneDeck.selectedPanel.selectedIndex;
}
_preventGlobalDeckChange = false;
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
_tabCover.classList.remove('hidden');
(async () => {
await reader._initPromise;
_tabCover.classList.add('hidden');
// Focus reader pages view if context pane note editor is not selected
if (Zotero_Tabs.selectedID == reader.tabID
&& !Zotero_Tabs.isTabsMenuVisible()
&& (!document.activeElement
|| !document.activeElement.closest('.context-node iframe[id="editor-view"]'))) {
if (!Zotero_Tabs.focusOptions?.keepTabFocused) {
// Do not move focus to the reader during keyboard navigation
reader.focus();
}
}
var attachment = await Zotero.Items.getAsync(reader.itemID);
if (attachment) {
_selectNotesContext(attachment.libraryID);
var notesContext = _getNotesContext(attachment.libraryID);
notesContext.updateFromCache();
}
let tabNotesDeck = _notesPaneDeck.selectedPanel.querySelector('.zotero-context-pane-tab-notes-deck');
let selectedIndex = Array.from(tabNotesDeck.children).findIndex(x => x.getAttribute('data-tab-id') == ids[0]);
if (selectedIndex != -1) {
tabNotesDeck.setAttribute('selectedIndex', selectedIndex);
_notesPaneDeck.selectedPanel.setAttribute('selectedIndex', 2);
}
else {
let libraryID = _notesPaneDeck.selectedPanel.getAttribute('data-library-id');
_notesPaneDeck.selectedPanel.setAttribute('selectedIndex', _globalDeckIndex[libraryID] || 0);
}
})();
}
_contextPaneSplitter.setAttribute('hidden', false);
_contextPane.setAttribute('collapsed', !(_contextPaneSplitter.getAttribute('state') != 'collapsed'));
// It seems that on heavy load (i.e. syncing) the line below doesn't set the correct value,
// therefore we repeat the same operation at the end of JS message queue
setTimeout(() => {
_contextPane.setAttribute('collapsed', !(_contextPaneSplitter.getAttribute('state') != 'collapsed'));
});
_sidenav.hidden = false;
}
_selectItemContext(ids[0]);
_update();
}
}
};
function _toggleItemButton() {
_togglePane(0);
}
function _toggleNotesButton() {
_togglePane(1);
}
function _getActiveEditor() {
var splitter;
if (Zotero.Prefs.get('layout') == 'stacked') {
splitter = _contextPaneSplitterStacked;
}
else {
splitter = _contextPaneSplitter;
}
if (splitter.getAttribute('state') != 'collapsed') {
if (_panesDeck.selectedIndex == 1) {
var libraryContext = _notesPaneDeck.selectedPanel;
// Global note
if (libraryContext.selectedIndex == 1) {
return libraryContext.querySelector('note-editor');
}
// Tab specific child note
else if (libraryContext.selectedIndex == 2) {
return libraryContext.querySelector('.zotero-context-pane-tab-notes-deck').selectedPanel.querySelector('note-editor');
}
}
}
return null;
}
function _focus() {
var splitter;
let node;
if (Zotero.Prefs.get('layout') == 'stacked') {
splitter = _contextPaneSplitterStacked;
}
else {
splitter = _contextPaneSplitter;
}
if (splitter.getAttribute('state') != 'collapsed') {
if (_panesDeck.selectedIndex == 0) {
// Focus the title in the header
var header = _itemPaneDeck.selectedPanel.querySelector("pane-header editable-text");
header.focus();
return true;
}
else {
node = _notesPaneDeck.selectedPanel;
if (node.selectedIndex == 0) {
node.querySelector('search-textbox').focus();
return true;
}
else {
node.querySelector('note-editor').focusFirst();
return true;
}
}
}
return false;
}
function _updateAddToNote() {
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
var editor = _getActiveEditor();
var libraryReadOnly = editor && editor.item && _isLibraryReadOnly(editor.item.libraryID);
var noteReadOnly = editor && editor.item
let editor = ZoteroContextPane.getActiveEditor();
let libraryReadOnly = editor && editor.item && _isLibraryReadOnly(editor.item.libraryID);
let noteReadOnly = editor && editor.item
&& (editor.item.deleted || editor.item.parentItem && editor.item.parentItem.deleted);
reader.enableAddToNote(!!editor && !libraryReadOnly && !noteReadOnly);
}
}
function _updatePaneWidth() {
var stacked = Zotero.Prefs.get('layout') == 'stacked';
var width = Zotero.Reader.getSidebarWidth() + 'px';
let stacked = _isStacked();
let width = Zotero.Reader.getSidebarWidth() + 'px';
if (!Zotero.Reader.getSidebarOpen()) {
width = 0;
}
@ -327,13 +121,15 @@ var ZoteroContextPane = new function () {
}
}
function _isStacked() {
return Zotero.Prefs.get('layout') == 'stacked';
}
function _update() {
if (Zotero_Tabs.selectedIndex == 0) {
return;
}
var stacked = Zotero.Prefs.get('layout') == 'stacked';
if (stacked) {
if (_isStacked()) {
_contextPaneSplitterStacked.setAttribute('hidden', false);
_contextPaneSplitter.setAttribute('state', 'open');
_contextPaneSplitter.setAttribute('hidden', true);
@ -361,514 +157,32 @@ var ZoteroContextPane = new function () {
}
if (Zotero_Tabs.selectedIndex > 0) {
var height = null;
if (Zotero.Prefs.get('layout') == 'stacked') {
height = 0;
if (_contextPane.getAttribute('collapsed') != 'true') {
height = _contextPaneInner.getBoundingClientRect().height;
}
let height = 0;
if (_isStacked()
&& _contextPane.getAttribute('collapsed') != 'true') {
height = _contextPaneInner.getBoundingClientRect().height;
}
Zotero.Reader.setBottomPlaceholderHeight(height);
}
_updatePaneWidth();
_updateAddToNote();
_sidenav.container?.render();
}
function _togglePane() {
var splitter = Zotero.Prefs.get('layout') == 'stacked'
? _contextPaneSplitterStacked
: _contextPaneSplitter;
var open = true;
if (splitter.getAttribute('state') != 'collapsed') {
open = false;
}
splitter.setAttribute('state', open ? 'open' : 'collapsed');
_update();
}
function _getCurrentAttachment() {
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
return Zotero.Items.get(reader.itemID);
}
return null;
}
function _addNotesContext(libraryID) {
let readOnly = _isLibraryReadOnly(libraryID);
var list = document.createXULElement('vbox');
list.setAttribute('flex', 1);
list.className = 'zotero-context-notes-list';
let noteContainer = document.createXULElement('vbox');
noteContainer.classList.add('zotero-context-note-container');
let title = document.createXULElement('vbox');
title.className = 'zotero-context-pane-editor-parent-line';
let divider = document.createElement("div");
divider.classList.add("divider");
let editor = new (customElements.get('note-editor'));
editor.className = 'zotero-context-pane-pinned-note';
editor.setAttribute('flex', 1);
noteContainer.append(title, divider, editor);
let tabNotesContainer = document.createXULElement('vbox');
tabNotesContainer.classList.add('zotero-context-note-container');
title = document.createXULElement('vbox');
title.className = 'zotero-context-pane-editor-parent-line';
divider = document.createElement("div");
divider.classList.add("divider");
let tabNotesDeck = document.createXULElement('deck');
tabNotesDeck.className = 'zotero-context-pane-tab-notes-deck';
tabNotesDeck.setAttribute('flex', 1);
tabNotesContainer.append(title, divider, tabNotesDeck);
let contextNode = document.createXULElement('deck');
contextNode.append(list, noteContainer, tabNotesContainer);
_notesPaneDeck.append(contextNode);
contextNode.className = 'context-node';
contextNode.setAttribute('data-library-id', libraryID);
contextNode.setAttribute('selectedIndex', 0);
var head = document.createXULElement('hbox');
head.style.display = 'flex';
async function _createNoteFromAnnotations(child) {
var attachment = _getCurrentAttachment();
if (!attachment) {
return;
}
var annotations = attachment.getAnnotations().filter(x => x.annotationType != 'ink');
if (!annotations.length) {
return;
}
var note = await Zotero.EditorInstance.createNoteFromAnnotations(
annotations,
{
parentID: child && attachment.parentID
}
);
_updateAddToNote();
input.value = '';
_updateNotesList();
_setPinnedNote(note);
}
function _createNote(child) {
contextNode.setAttribute('selectedIndex', 1);
var item = new Zotero.Item('note');
item.libraryID = libraryID;
if (child) {
var attachment = _getCurrentAttachment();
if (!attachment) {
return;
}
item.parentID = attachment.parentID;
}
_setPinnedNote(item);
_updateAddToNote();
input.value = '';
_updateNotesList();
}
var vbox = document.createXULElement('vbox');
vbox.style.flex = '1';
var input = document.createXULElement('search-textbox');
input.setAttribute('data-l10n-id', 'context-notes-search');
input.setAttribute('data-l10n-attrs', 'placeholder');
input.style.margin = '6px 8px 7px 8px';
input.setAttribute('type', 'search');
input.setAttribute('timeout', '250');
input.addEventListener('command', () => {
notesList.expanded = false;
_updateNotesList();
});
vbox.append(input);
head.append(vbox);
var listBox = document.createXULElement('vbox');
listBox.style.display = 'flex';
listBox.style.minWidth = '0';
listBox.setAttribute('flex', '1');
var listInner = document.createElement('div');
listInner.className = 'notes-list-container';
// Otherwise it can be focused with tab
listInner.tabIndex = -1;
listBox.append(listInner);
list.append(head, listBox);
var notesList = document.createXULElement('context-notes-list');
notesList.addEventListener('note-click', (event) => {
let { id } = event.detail;
let item = Zotero.Items.get(id);
if (item) {
_setPinnedNote(item);
}
});
notesList.addEventListener('note-contextmenu', (event) => {
let { id, screenX, screenY } = event.detail;
let item = Zotero.Items.get(id);
if (item) {
document.getElementById('context-pane-list-move-to-trash').setAttribute('disabled', readOnly);
var popup = document.getElementById('context-pane-list-popup');
let handleCommand = event => _handleListPopupClick(id, event);
popup.addEventListener('popupshowing', () => {
popup.addEventListener('command', handleCommand, { once: true });
popup.addEventListener('popuphiding', () => {
popup.removeEventListener('command', handleCommand);
}, { once: true });
}, { once: true });
popup.openPopupAtScreen(screenX, screenY, true);
}
});
notesList.addEventListener('add-child', (event) => {
document.getElementById('context-pane-add-child-note').setAttribute('disabled', readOnly);
document.getElementById('context-pane-add-child-note-from-annotations').setAttribute('disabled', readOnly);
var popup = document.getElementById('context-pane-add-child-note-button-popup');
popup.onclick = _handleAddChildNotePopupClick;
popup.openPopup(event.detail.button, 'after_end');
});
notesList.addEventListener('add-standalone', (event) => {
document.getElementById('context-pane-add-standalone-note').setAttribute('disabled', readOnly);
document.getElementById('context-pane-add-standalone-note-from-annotations').setAttribute('disabled', readOnly);
var popup = document.getElementById('context-pane-add-standalone-note-button-popup');
popup.onclick = _handleAddStandaloneNotePopupClick;
popup.openPopup(event.detail.button, 'after_end');
});
function _isVisible() {
let splitter = Zotero.Prefs.get('layout') == 'stacked'
? _contextPaneSplitterStacked
: _contextPaneSplitter;
return Zotero_Tabs.selectedID != 'zotero-pane'
&& _panesDeck.selectedIndex == 1
&& context.node.selectedIndex == 0
&& splitter.getAttribute('state') != 'collapsed';
}
async function _updateNotesList(useCached) {
var query = input.value;
var notes;
// Calls itself and debounces until notes list becomes
// visible, and then updates
if (!useCached && !_isVisible()) {
context.update();
return;
}
if (useCached && context.cachedNotes.length) {
notes = context.cachedNotes;
}
else {
await Zotero.Schema.schemaUpdatePromise;
var s = new Zotero.Search();
s.addCondition('libraryID', 'is', libraryID);
s.addCondition('itemType', 'is', 'note');
if (query) {
let parts = Zotero.SearchConditions.parseSearchString(query);
for (let part of parts) {
s.addCondition('note', 'contains', part.text);
}
}
notes = await s.search();
notes = Zotero.Items.get(notes);
if (Zotero.Prefs.get('sortNotesChronologically.reader')) {
notes.sort((a, b) => {
a = a.dateModified;
b = b.dateModified;
return (a > b ? -1 : (a < b ? 1 : 0));
});
}
else {
let collation = Zotero.getLocaleCollation();
notes.sort((a, b) => {
let aTitle = Zotero.Items.getSortTitle(a.getNoteTitle());
let bTitle = Zotero.Items.getSortTitle(b.getNoteTitle());
return collation.compareString(1, aTitle, bTitle);
});
}
let cachedNotesIndex = new Map();
for (let cachedNote of context.cachedNotes) {
cachedNotesIndex.set(cachedNote.id, cachedNote);
}
notes = notes.map((note) => {
var parentItem = note.parentItem;
// If neither note nor parent item is affected try to return the cached note
if (!context.affectedIDs.has(note.id)
&& (!parentItem || !context.affectedIDs.has(parentItem.id))) {
let cachedNote = cachedNotesIndex.get(note.id);
if (cachedNote) {
return cachedNote;
}
}
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);
var date = Zotero.Date.sqlToDate(note.dateModified, true);
date = Zotero.Date.toFriendlyDate(date);
return {
id: note.id,
title: title || Zotero.getString('pane.item.notes.untitled'),
body: parts[1] || '',
date,
parentID: note.parentID,
parentItemType: parentItem && parentItem.itemType,
parentTitle: parentItem && parentItem.getDisplayTitle()
};
});
context.cachedNotes = notes;
context.affectedIDs = new Set();
}
var attachment = _getCurrentAttachment();
var parentID = attachment && attachment.parentID;
notesList.hasParent = !!parentID;
notesList.notes = notes.map(note => ({
...note,
isCurrentChild: parentID && note.parentID == parentID
}));
}
var context = {
libraryID,
node: contextNode,
editor,
notesList,
cachedNotes: [],
affectedIDs: new Set(),
update: Zotero.Utilities.throttle(_updateNotesList, 1000, { leading: false }),
updateFromCache: () => _updateNotesList(true)
};
function _handleListPopupClick(id, event) {
switch (event.originalTarget.id) {
case 'context-pane-list-show-in-library':
ZoteroPane_Local.selectItem(id);
Zotero_Tabs.select('zotero-pane');
break;
case 'context-pane-list-edit-in-window':
ZoteroPane_Local.openNoteWindow(id);
break;
case 'context-pane-list-move-to-trash':
if (!readOnly) {
Zotero.Items.trashTx(id);
context.cachedNotes = context.cachedNotes.filter(x => x.id != id);
_updateNotesList(true);
}
break;
default:
}
}
function _handleAddChildNotePopupClick(event) {
if (readOnly) {
return;
}
switch (event.originalTarget.id) {
case 'context-pane-add-child-note':
_createNote(true);
break;
case 'context-pane-add-child-note-from-annotations':
_createNoteFromAnnotations(true);
break;
default:
}
}
function _handleAddStandaloneNotePopupClick(event) {
if (readOnly) {
return;
}
switch (event.originalTarget.id) {
case 'context-pane-add-standalone-note':
_createNote();
break;
case 'context-pane-add-standalone-note-from-annotations':
_createNoteFromAnnotations();
break;
default:
}
}
listInner.append(notesList);
_updateNotesList();
_notesContexts.push(context);
return context;
}
function _getNotesContext(libraryID) {
var context = _notesContexts.find(x => x.libraryID == libraryID);
if (!context) {
context = _addNotesContext(libraryID);
}
return context;
}
function _selectNotesContext(libraryID) {
let context = _getNotesContext(libraryID);
_notesPaneDeck.setAttribute('selectedIndex', Array.from(_notesPaneDeck.children).findIndex(x => x == context.node));
}
function _removeNotesContext(libraryID) {
var context = _notesContexts.find(x => x.libraryID == libraryID);
context.node.remove();
_notesContexts = _notesContexts.filter(x => x.libraryID != libraryID);
_readerSidenav.container?.render();
}
function _isLibraryReadOnly(libraryID) {
return !Zotero.Libraries.get(libraryID).editable;
}
function _setPinnedNote(item) {
var readOnly = _isLibraryReadOnly(item.libraryID);
var context = _getNotesContext(item.libraryID);
if (context) {
var { editor, node } = context;
let isChild = false;
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
let attachment = Zotero.Items.get(reader.itemID);
if (attachment.parentItemID == item.parentItemID) {
isChild = true;
}
}
var tabNotesDeck = _notesPaneDeck.selectedPanel.querySelector('.zotero-context-pane-tab-notes-deck');
var parentTitleContainer;
let vbox;
if (isChild) {
vbox = document.createXULElement('vbox');
vbox.setAttribute('data-tab-id', Zotero_Tabs.selectedID);
vbox.style.display = 'flex';
editor = new (customElements.get('note-editor'));
editor.style.display = 'flex';
editor.style.width = '100%';
vbox.append(editor);
tabNotesDeck.append(vbox);
editor.mode = readOnly ? 'view' : 'edit';
editor.item = item;
editor.parentItem = null;
_notesPaneDeck.selectedPanel.setAttribute('selectedIndex', 2);
tabNotesDeck.setAttribute('selectedIndex', tabNotesDeck.children.length - 1);
parentTitleContainer = _notesPaneDeck.selectedPanel.children[2].querySelector('.zotero-context-pane-editor-parent-line');
}
else {
node.setAttribute('selectedIndex', 1);
editor.mode = readOnly ? 'view' : 'edit';
editor.item = item;
editor.parentItem = null;
parentTitleContainer = node.querySelector('.zotero-context-pane-editor-parent-line');
}
editor.focus();
let parentItem = item.parentItem;
let container = document.createElement('div');
container.classList.add("parent-title-container");
let returnBtn = document.createXULElement("toolbarbutton");
returnBtn.classList.add("zotero-tb-note-return");
returnBtn.addEventListener("command", () => {
// Immediately save note content before vbox with note-editor iframe is destroyed below
editor.saveSync();
_panesDeck.setAttribute('selectedIndex', 1);
_notesPaneDeck.selectedPanel.setAttribute('selectedIndex', 0);
vbox?.remove();
_updateAddToNote();
_preventGlobalDeckChange = true;
});
let title = document.createElement('div');
title.className = 'parent-title';
title.textContent = parentItem?.getDisplayTitle() || '';
container.append(returnBtn, title);
parentTitleContainer.replaceChildren(container);
_updateAddToNote();
function _togglePane() {
var splitter = ZoteroContextPane.getSplitter();
var open = true;
if (splitter.getAttribute('state') != 'collapsed') {
open = false;
}
}
function _removeItemContext(tabID) {
document.getElementById(tabID + '-context').remove();
_itemContexts = _itemContexts.filter(x => x.tabID != tabID);
}
function _selectItemContext(tabID) {
let previousPinnedPane = _sidenav.container?.pinnedPane || "";
let selectedPanel = Array.from(_itemPaneDeck.children).find(x => x.id == tabID + '-context');
if (selectedPanel) {
_itemPaneDeck.selectedPanel = selectedPanel;
selectedPanel.sidenav = _sidenav;
if (previousPinnedPane) selectedPanel.pinnedPane = previousPinnedPane;
}
}
async function _addItemContext(tabID, itemID) {
var { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID);
var library = Zotero.Libraries.get(libraryID);
await library.waitForDataLoad('item');
var item = Zotero.Items.get(itemID);
if (!item) {
return;
}
libraryID = item.libraryID;
var readOnly = _isLibraryReadOnly(libraryID);
var parentID = item.parentID;
var context = {
tabID,
itemID,
parentID,
libraryID,
update: () => {}
};
_itemContexts.push(context);
let previousPinnedPane = _sidenav.container?.pinnedPane || "";
let targetItem = parentID ? Zotero.Items.get(parentID) : item;
let itemDetails = document.createXULElement('item-details');
itemDetails.id = tabID + '-context';
itemDetails.className = 'zotero-item-pane-content';
_itemPaneDeck.appendChild(itemDetails);
itemDetails.mode = readOnly ? "view" : null;
itemDetails.item = targetItem;
itemDetails.sidenav = _sidenav;
if (previousPinnedPane) itemDetails.pinnedPane = previousPinnedPane;
_selectItemContext(tabID);
await itemDetails.render();
splitter.setAttribute('state', open ? 'open' : 'collapsed');
_update();
}
};

View file

@ -37,6 +37,7 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemPaneSe
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentBox.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentPreview.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentPreviewBox.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/contextPane.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/duplicatesMergePane.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/guidancePanel.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemBox.js', this);
@ -65,6 +66,7 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachment
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/annotationRow.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/contextNotesList.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/noteRow.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/notesContext.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/librariesCollectionsBox.js', this);
{

View file

@ -93,7 +93,10 @@ import { getCSSItemTypeIcon } from 'components/icons';
_handleAnnotationClick = () => {
// TODO: jump to annotations pane
let pane = this._getSidenav()?.container.querySelector(`:scope > [data-pane="attachment-annotations"]`);
let pane;
if (ZoteroContextPane) {
pane = ZoteroContextPane.getSidenav()?.container.querySelector(`:scope > [data-pane="attachment-annotations"]`);
}
if (pane) {
pane._section.open = true;
}
@ -105,14 +108,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
}
};
_getSidenav() {
// TODO: update this after unifying item pane & context pane
return document.querySelector(
Zotero_Tabs.selectedType === 'library'
? "#zotero-view-item-sidenav"
: "#zotero-context-pane-sidenav");
}
render() {
if (!this.initialized) return;

View file

@ -90,12 +90,10 @@
}
get usePreview() {
if (this.tabType == "reader") return false;
return this.hasAttribute('data-use-preview');
}
set usePreview(val) {
if (this.tabType == "reader") return;
this.toggleAttribute('data-use-preview', val);
this.updatePreview();
}
@ -103,7 +101,6 @@
init() {
this.initCollapsibleSection();
this._section.addEventListener('add', this._handleAdd);
// this._section.addEventListener('togglePreview', this._handleTogglePreview);
this._attachments = this.querySelector('.attachments-container');
@ -182,8 +179,6 @@
if (!this._item) return;
if (!force && this._isAlreadyRendered()) return;
this.usePreview = Zotero.Prefs.get('showAttachmentPreview');
await this._updateAttachmentIDs();
let itemAttachments = Zotero.Items.get(this._attachmentIDs);
@ -193,6 +188,7 @@
this.addRow(attachment);
}
this.updateCount();
this.usePreview = Zotero.Prefs.get('showAttachmentPreview');
}
updateCount() {
@ -204,11 +200,25 @@
if (!this.usePreview || !this._section.open) {
return;
}
let attachment = await this._item.getBestAttachment();
let attachment = await this._getPreviewAttachment();
if (!attachment) {
this.toggleAttribute('data-use-preview', false);
return;
}
this._preview.item = attachment;
await this._preview.render();
}
async _getPreviewAttachment() {
let attachment = await this._item.getBestAttachment();
if (this.tabType === "reader"
&& Zotero_Tabs._getTab(Zotero_Tabs.selectedID)?.tab?.data?.itemID == attachment.id) {
// In the reader, only show the preview when viewing a secondary attachment
return null;
}
return attachment;
}
_handleAdd = (event) => {
this._section.open = true;
ZoteroPane.updateAddAttachmentMenu(this._addPopup);
@ -227,8 +237,8 @@
}
};
_handleContextMenu = () => {
if (this.tabType == "reader") return;
_handleContextMenu = async () => {
if (!await this._getPreviewAttachment()) return;
let contextMenu = this._section._contextMenu;
let menu = document.createXULElement("menuitem");
menu.classList.add('menuitem-iconic', 'zotero-menuitem-toggle-preview');

View file

@ -401,12 +401,9 @@
if (document.documentElement.getAttribute('windowtype') !== 'navigator:browser') {
return null;
}
if (typeof Zotero_Tabs == "undefined") return null;
if (!ZoteroContextPane) return null;
// TODO: update this after unifying item pane & context pane
return document.querySelector(
Zotero_Tabs.selectedType === 'library'
? "#zotero-view-item-sidenav"
: "#zotero-context-pane-sidenav");
return ZoteroContextPane.getSidenav();
}
render() {

View file

@ -0,0 +1,334 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2024 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.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 *****
*/
{
class ContextPane extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<deck id="zotero-context-pane-deck" flex="1" selectedIndex="0">
<deck id="zotero-context-pane-item-deck"></deck>
<deck id="zotero-context-pane-notes-deck" class="notes-pane-deck" flex="1"></deck>
</deck>
`);
get sidenav() {
return this._sidenav;
}
set sidenav(sidenav) {
this._sidenav = sidenav;
// TODO: decouple sidenav and contextPane
sidenav.contextNotesPane = this._notesPaneDeck;
}
get viewType() {
return ["item", "notes"][this._panesDeck.getAttribute('selectedIndex')];
}
set viewType(viewType) {
let viewTypeMap = {
item: "0",
notes: "1",
};
if (!(viewType in viewTypeMap)) {
throw new Error(`ContextPane.viewType must be one of ["item", "notes"], but got ${viewType}`);
}
this._panesDeck.setAttribute("selectedIndex", viewTypeMap[viewType]);
}
init() {
this._panesDeck = this.querySelector('#zotero-context-pane-deck');
// Item pane deck
this._itemPaneDeck = this.querySelector('#zotero-context-pane-item-deck');
// Notes pane deck
this._notesPaneDeck = this.querySelector('#zotero-context-pane-notes-deck');
this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'contextPane');
}
destroy() {
Zotero.Notifier.unregisterObserver(this._notifierID);
}
notify(action, type, ids, extraData) {
if (type == 'item') {
this._handleItemUpdate(action, type, ids, extraData);
return;
}
if (type == 'tab' && action == 'add') {
this._handleTabAdd(action, type, ids, extraData);
return;
}
if (type == 'tab' && action == 'close') {
this._handleTabClose(action, type, ids, extraData);
return;
}
if (type == 'tab' && action == 'select') {
this._handleTabSelect(action, type, ids, extraData);
}
}
_handleItemUpdate(action, type, ids, extraData) {
// Update, remove or re-create item panes
for (let itemDetails of Array.from(this._itemPaneDeck.children)) {
let item = itemDetails.item;
let tabID = itemDetails.dataset.tabId;
if (!item) {
this._removeItemContext(tabID);
}
else if (item.parentID != itemDetails.parentID) {
this._removeItemContext(tabID);
this._addItemContext(tabID, item.itemID);
}
}
// Update notes lists for affected libraries
let libraryIDs = [];
for (let id of ids) {
let item = Zotero.Items.get(id);
if (item && (item.isNote() || item.isRegularItem())) {
libraryIDs.push(item.libraryID);
}
else if (action == 'delete') {
libraryIDs.push(extraData[id].libraryID);
}
}
for (let context of Array.from(this._notesPaneDeck.children)) {
if (libraryIDs.includes(context.libraryID)) {
context.affectedIDs = new Set([...context.affectedIDs, ...ids]);
context.update();
}
}
}
_handleTabAdd(action, type, ids, extraData) {
let data = extraData[ids[0]];
this._addItemContext(ids[0], data.itemID, data.type);
}
_handleTabClose(action, type, ids) {
this._removeItemContext(ids[0]);
if (Zotero_Tabs.deck.children.length == 1) {
Array.from(this._notesPaneDeck.children).forEach(x => x.notesList.expanded = false);
}
// Close tab specific notes if tab id no longer exists, but
// do that only when unloaded tab is reloaded
setTimeout(() => {
let contextNodes = Array.from(this._notesPaneDeck.children);
for (let contextNode of contextNodes) {
let nodes = Array.from(contextNode.querySelector('.zotero-context-pane-tab-notes-deck').children);
for (let node of nodes) {
let tabID = node.getAttribute('data-tab-id');
if (!document.getElementById(tabID)) {
node.remove();
}
}
}
// For unknown reason fx102, unlike 60, sometimes doesn't automatically update selected index
this._selectItemContext(Zotero_Tabs.selectedID);
});
}
_handleTabSelect(action, type, ids) {
// TEMP: move these variables to ZoteroContextPane
let _contextPaneSplitter = document.getElementById('zotero-context-splitter');
let _contextPane = document.getElementById('zotero-context-pane');
// It seems that changing `hidden` or `collapsed` values might
// be related with significant slow down when there are too many
// DOM nodes (i.e. 10k notes)
if (Zotero_Tabs.selectedType == 'library') {
_contextPaneSplitter.setAttribute('hidden', true);
_contextPane.setAttribute('collapsed', true);
ZoteroContextPane.showTabCover(false);
this._sidenav.hidden = true;
}
else if (Zotero_Tabs.selectedType == 'reader') {
let currentNoteContext = this._getCurrentNotesContext();
currentNoteContext?._cacheViewType();
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
this._handleReaderReady(reader);
_contextPaneSplitter.setAttribute('hidden', false);
_contextPane.setAttribute('collapsed', !(_contextPaneSplitter.getAttribute('state') != 'collapsed'));
// It seems that on heavy load (i.e. syncing) the line below doesn't set the correct value,
// therefore we repeat the same operation at the end of JS message queue
setTimeout(() => {
_contextPane.setAttribute('collapsed', !(_contextPaneSplitter.getAttribute('state') != 'collapsed'));
});
this._sidenav.hidden = false;
}
this._selectItemContext(ids[0]);
ZoteroContextPane.update();
}
async _handleReaderReady(reader) {
if (!reader) {
return;
}
ZoteroContextPane.showTabCover(true);
await reader._initPromise;
ZoteroContextPane.showTabCover(false);
// Focus reader pages view if context pane note editor is not selected
if (Zotero_Tabs.selectedID == reader.tabID
&& !Zotero_Tabs.isTabsMenuVisible()
&& (!document.activeElement
|| !document.activeElement.closest('.context-node iframe[id="editor-view"]'))) {
if (!Zotero_Tabs.focusOptions?.keepTabFocused) {
// Do not move focus to the reader during keyboard navigation
reader.focus();
}
}
let attachment = await Zotero.Items.getAsync(reader.itemID);
if (attachment) {
this._selectNotesContext(attachment.libraryID);
let notesContext = this._getNotesContext(attachment.libraryID);
notesContext.updateFromCache();
}
let currentNoteContext = this._getCurrentNotesContext();
let tabNotesDeck = currentNoteContext.querySelector('.zotero-context-pane-tab-notes-deck');
let selectedIndex = Array.from(tabNotesDeck.children).findIndex(x => x.getAttribute('data-tab-id') == reader.tabID);
if (selectedIndex != -1) {
tabNotesDeck.setAttribute('selectedIndex', selectedIndex);
currentNoteContext.viewType = "childNote";
}
else {
currentNoteContext._restoreViewType();
}
}
_getCurrentNotesContext() {
return this._notesPaneDeck.selectedPanel;
}
_getNotesContext(libraryID) {
let context = Array.from(this._notesPaneDeck.children).find(x => x.libraryID == libraryID);
if (!context) {
context = this._addNotesContext(libraryID);
}
return context;
}
_addNotesContext(libraryID) {
let context = new (customElements.get("notes-context"));
this._notesPaneDeck.append(context);
context.libraryID = libraryID;
return context;
}
_selectNotesContext(libraryID) {
let context = this._getNotesContext(libraryID);
this._notesPaneDeck.selectedPanel = context;
}
_removeNotesContext(libraryID) {
let context = Array.from(this._notesPaneDeck.children).find(x => x.libraryID == libraryID);
context?.remove();
}
_getActiveEditor() {
let currentContext = this._getCurrentNotesContext();
return currentContext?._getCurrentEditor();
}
_getItemContext(tabID) {
return this._itemPaneDeck.querySelector(`[data-tab-id="${tabID}"]`);
}
_removeItemContext(tabID) {
this._itemPaneDeck.querySelector(`[data-tab-id="${tabID}"]`).remove();
}
_selectItemContext(tabID) {
let previousPinnedPane = this._sidenav.container?.pinnedPane || "";
let selectedPanel = this._getItemContext(tabID);
if (selectedPanel) {
this._itemPaneDeck.selectedPanel = selectedPanel;
selectedPanel.sidenav = this._sidenav;
if (previousPinnedPane) selectedPanel.pinnedPane = previousPinnedPane;
}
}
async _addItemContext(tabID, itemID, tabType = "") {
let { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID);
let library = Zotero.Libraries.get(libraryID);
await library.waitForDataLoad('item');
let item = Zotero.Items.get(itemID);
if (!item) {
return;
}
libraryID = item.libraryID;
let readOnly = !Zotero.Libraries.get(libraryID).editable;
let parentID = item.parentID;
let previousPinnedPane = this._sidenav.container?.pinnedPane || "";
let targetItem = parentID ? Zotero.Items.get(parentID) : item;
let itemDetails = document.createXULElement('item-details');
itemDetails.id = tabID + '-context';
itemDetails.dataset.tabId = tabID;
itemDetails.className = 'zotero-item-pane-content';
this._itemPaneDeck.appendChild(itemDetails);
itemDetails.mode = readOnly ? "view" : null;
itemDetails.item = targetItem;
// Manually cache parentID
itemDetails.parentID = parentID;
itemDetails.sidenav = this._sidenav;
if (previousPinnedPane) itemDetails.pinnedPane = previousPinnedPane;
// `unloaded` tabs are never selected and shouldn't be rendered on creation.
// Use `includes` here for forward compatibility.
if (!tabType.includes("unloaded")) {
this._selectItemContext(tabID);
}
}
_focus() {
let splitter = ZoteroContextPane.getSplitter();
let node;
if (splitter.getAttribute('state') != 'collapsed') {
if (this.viewType == "item") {
node = this._itemPaneDeck.selectedPanel;
node.focus();
return true;
}
else {
this._getCurrentNotesContext()?.focus();
}
}
return false;
}
}
customElements.define("context-pane", ContextPane);
}

View file

@ -60,9 +60,9 @@
<notes-box id="zotero-editpane-notes" class="zotero-editpane-notes" data-pane="notes"/>
<attachment-box id="zotero-attachment-box" flex="1" data-pane="attachment-info" data-use-preview="true" hidden="true"/>
<attachment-box id="zotero-attachment-box" data-pane="attachment-info" data-use-preview="true" hidden="true"/>
<attachment-annotations-box id="zotero-editpane-attachment-annotations" flex="1" data-pane="attachment-annotations" hidden="true"/>
<attachment-annotations-box id="zotero-editpane-attachment-annotations" data-pane="attachment-annotations" hidden="true"/>
<libraries-collections-box id="zotero-editpane-libraries-collections" class="zotero-editpane-libraries-collections" data-pane="libraries-collections"/>
@ -82,6 +82,17 @@
this._item = item;
}
/*
* For contextPane update
*/
get parentID() {
return this._cachedParentID;
}
set parentID(parentID) {
this._cachedParentID = parentID;
}
get mode() {
return this._mode;
}
@ -145,6 +156,8 @@
set sidenav(sidenav) {
this._sidenav = sidenav;
sidenav.container = this;
// Manually update once and further changes will be synced automatically to sidenav
this.forceUpdateSideNav();
}
static get observedAttributes() {
@ -366,6 +379,10 @@
return true;
}
forceUpdateSideNav() {
this.getPanes().forEach(elem => this._sidenav.updatePaneStatus(elem.dataset.pane));
}
async scrollToPane(paneID, behavior = 'smooth') {
let pane = this.getPane(paneID);
if (!pane) return null;
@ -374,7 +391,7 @@
// If the itemPane is collapsed, just remember which pane needs to be scrolled to
// when itemPane is expanded.
if (this._collapsed || this.getAttribute("no-render")) {
if (this._collapsed) {
return null;
}
@ -487,15 +504,6 @@
isLibraryTab ? 'zotero-view-item-sidenav' : 'zotero-context-pane-sidenav'
);
// Shift-tab from title when reader is opened focuses the last button in tabs toolbar
if (event.target.closest(".title") && event.key == "Tab"
&& event.shiftKey && Zotero_Tabs.selectedType == "reader") {
let focusable = [...document.querySelectorAll("#zotero-tabs-toolbar toolbarbutton:not([disabled]):not([hidden])")];
let btn = focusable[focusable.length - 1];
btn.focus();
stopEvent();
return;
}
// Tab from the scrollable area focuses the pinned pane if it exists
if (event.target.classList.contains("zotero-view-item") && event.key == "Tab" && !event.shiftKey && sidenav.pinnedPane) {
let pane = sidenav.getPane(sidenav.pinnedPane);

View file

@ -92,27 +92,7 @@
* @param {"message" | "item" | "note" | "duplicates"} type view type
*/
set viewType(type) {
switch (type) {
case "message": {
this._deck.selectedIndex = 0;
break;
}
case "item": {
this._deck.selectedIndex = 1;
break;
}
case "note": {
this._deck.selectedIndex = 2;
break;
}
case "duplicates": {
this._deck.selectedIndex = 3;
break;
}
}
// If item pane is no selected, do not render
this._itemDetails.toggleAttribute("no-render", type == "item");
this._itemDetails.sidenav.toggleDefaultStatus(type != "item");
this.setAttribute("view-type", type);
}
render() {
@ -163,6 +143,11 @@
this._itemDetails.mode = this.editable ? null : "view";
this._itemDetails.item = item;
if (this.hasAttribute("collapsed")) {
return true;
}
this._itemDetails.render();
if (item.isFeedItem) {
@ -466,13 +451,27 @@
}
static get observedAttributes() {
return ['collapsed'];
return ['collapsed', 'width', 'height', 'view-type'];
}
attributeChangedCallback(name) {
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "collapsed": {
this.handleResize();
break;
}
case "width": {
this.style.width = `${newValue}px`;
break;
}
case "height": {
this.style.height = `${newValue}px`;
break;
}
case "view-type": {
if (newValue !== oldValue) {
this._handleViewTypeChange(newValue);
}
}
}
}
@ -497,6 +496,33 @@
}
}
}
_handleViewTypeChange(type) {
let previousViewType = this.viewType;
switch (type) {
case "message": {
this._deck.selectedIndex = 0;
break;
}
case "item": {
this._deck.selectedIndex = 1;
break;
}
case "note": {
this._deck.selectedIndex = 2;
break;
}
case "duplicates": {
this._deck.selectedIndex = 3;
break;
}
}
let isViewingItem = type == "item";
if (previousViewType != "item" && isViewingItem) {
this._itemDetails.forceUpdateSideNav();
}
this._itemDetails.sidenav.toggleDefaultStatus(!isViewingItem);
}
}
customElements.define("item-pane", ItemPane);
}

View file

@ -256,8 +256,7 @@
}
toolbarbutton.setAttribute('aria-selected', !contextNotesPaneVisible && pane == pinnedPane);
toolbarbutton.parentElement.hidden = !this.container.getPane(pane);
// No need to set `hidden` here, since it's updated by ItemDetails#_handlePaneStatus
// Set .pinned on the container, for pin styling
toolbarbutton.parentElement.classList.toggle('pinned', pane == pinnedPane);
}

View file

@ -0,0 +1,491 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2024 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.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 *****
*/
{
class NotesContext extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<deck class="context-node" flex="1" selectedIndex="0">
<vbox class="zotero-context-notes-list" flex="1">
<hbox style="display: flex;">
<vbox style="flex: 1;">
<search-textbox data-l10n-id="context-notes-search" data-l10n-attrs="placeholder"
style="margin: 6px 8px 7px 8px;"
type="search" timeout="250">
</search-textbox>
</vbox>
</hbox>
<vbox style="display: flex; min-width: 0;" flex="1">
<html:div class="notes-list-container" tabindex="-1">
<context-notes-list></context-notes-list>
</html:div>
</vbox>
</vbox>
<vbox class="zotero-context-note-container context-note-standalone">
<vbox class="zotero-context-pane-editor-parent-line"></vbox>
<html:div class="divider"></html:div>
<note-editor class="zotero-context-pane-pinned-note" flex="1"></note-editor>
</vbox>
<vbox class="zotero-context-note-container context-note-child">
<vbox class="zotero-context-pane-editor-parent-line"></vbox>
<html:div class="divider"></html:div>
<deck class="zotero-context-pane-tab-notes-deck" flex="1"></deck>
</vbox>
</deck>
`);
get editable() {
return this._editable;
}
set editable(editable) {
this._editable = editable;
}
get libraryID() {
return Number(this.node.dataset.libraryId);
}
set libraryID(libraryID) {
this.node.dataset.libraryId = libraryID;
this.editable = Zotero.Libraries.get(libraryID).editable;
}
get selectedIndex() {
return this.node.selectedIndex;
}
set selectedIndex(selectedIndex) {
this.setAttribute("selectedIndex", selectedIndex);
this.node.selectedIndex = selectedIndex;
}
get selectedPanel() {
return this.node.selectedPanel;
}
set selectedPanel(selectedPanel) {
this.node.selectedPanel = selectedPanel;
}
get viewType() {
return ["notesList", "standaloneNote", "childNote"][this.node.selectedIndex];
}
set viewType(viewType) {
let viewTypeMap = {
notesList: "0",
standaloneNote: "1",
childNote: "2",
};
if (!(viewType in viewTypeMap)) {
throw new Error(`NotesContext.viewType must be one of ["notesList", "standaloneNote", "childNote"], but got ${viewType}`);
}
this.node.setAttribute("selectedIndex", viewTypeMap[viewType]);
}
static get observedAttributes() {
return ['selectedIndex'];
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'selectedIndex':
this.node.selectedIndex = newValue;
break;
}
}
update = Zotero.Utilities.throttle(this._updateNotesList, 1000, { leading: false });
cachedNotes = [];
affectedIDs = new Set();
updateFromCache = () => this._updateNotesList(true);
init() {
this.node = this.querySelector(".context-node");
this.editor = this.querySelector(".zotero-context-pane-pinned-note");
this.notesList = this.querySelector("context-notes-list");
this.input = this.querySelector("search-textbox");
this.input.addEventListener('command', () => {
this.notesList.expanded = false;
this._updateNotesList();
});
this._preventViewTypeCache = false;
this._cachedViewType = "";
this._initNotesList();
}
focus() {
if (this.viewType == "notesList") {
this.input.focus();
return true;
}
else {
this._getCurrentEditor().focusFirst();
return true;
}
}
_initNotesList() {
this.notesList.addEventListener('note-click', (event) => {
let { id } = event.detail;
let item = Zotero.Items.get(id);
if (item) {
this._setPinnedNote(item);
}
});
this.notesList.addEventListener('note-contextmenu', (event) => {
let { id, screenX, screenY } = event.detail;
let item = Zotero.Items.get(id);
if (item) {
document.getElementById('context-pane-list-move-to-trash').setAttribute('disabled', !this.editable);
let popup = document.getElementById('context-pane-list-popup');
let handleCommand = event => this._handleListPopupClick(id, event);
popup.addEventListener('popupshowing', () => {
popup.addEventListener('command', handleCommand, { once: true });
popup.addEventListener('popuphiding', () => {
popup.removeEventListener('command', handleCommand);
}, { once: true });
}, { once: true });
popup.openPopupAtScreen(screenX, screenY, true);
}
});
this.notesList.addEventListener('add-child', (event) => {
document.getElementById('context-pane-add-child-note').setAttribute('disabled', !this.editable);
document.getElementById('context-pane-add-child-note-from-annotations').setAttribute('disabled', !this.editable);
let popup = document.getElementById('context-pane-add-child-note-button-popup');
popup.onclick = this._handleAddChildNotePopupClick;
popup.openPopup(event.detail.button, 'after_end');
});
this.notesList.addEventListener('add-standalone', (event) => {
document.getElementById('context-pane-add-standalone-note').setAttribute('disabled', !this.editable);
document.getElementById('context-pane-add-standalone-note-from-annotations').setAttribute('disabled', !this.editable);
let popup = document.getElementById('context-pane-add-standalone-note-button-popup');
popup.onclick = this._handleAddStandaloneNotePopupClick;
popup.openPopup(event.detail.button, 'after_end');
});
}
async _createNoteFromAnnotations(child) {
let attachment = this._getCurrentAttachment();
if (!attachment) {
return;
}
let annotations = attachment.getAnnotations().filter(x => x.annotationType != 'ink');
if (!annotations.length) {
return;
}
let note = await Zotero.EditorInstance.createNoteFromAnnotations(
annotations,
{
parentID: child && attachment.parentID
}
);
ZoteroContextPane.updateAddToNote();
this.input.value = '';
this._updateNotesList();
this._setPinnedNote(note);
}
_createNote(child) {
this.viewType = "standaloneNote";
let item = new Zotero.Item('note');
item.libraryID = this.libraryID;
if (child) {
let attachment = this._getCurrentAttachment();
if (!attachment) {
return;
}
item.parentID = attachment.parentID;
}
this._setPinnedNote(item);
ZoteroContextPane.updateAddToNote();
this.input.value = '';
this._updateNotesList();
}
_isNotesListVisible() {
let splitter = ZoteroContextPane.getSplitter();
return Zotero_Tabs.selectedID != 'zotero-pane'
&& ZoteroContextPane.viewType == "notes"
&& this.viewType == "notesList"
&& splitter.getAttribute('state') != 'collapsed';
}
_getCurrentEditor() {
let splitter = ZoteroContextPane.getSplitter();
if (splitter.getAttribute('state') == 'collapsed' || ZoteroContextPane.viewType != "notes") return null;
return this.node.selectedPanel.querySelector('note-editor');
}
_getCurrentAttachment() {
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
return Zotero.Items.get(reader.itemID);
}
return null;
}
_setPinnedNote(item) {
let { editor, node } = this;
let isChild = false;
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
let attachment = Zotero.Items.get(reader.itemID);
if (attachment.parentItemID == item.parentItemID) {
isChild = true;
}
}
let tabNotesDeck = this.querySelector('.zotero-context-pane-tab-notes-deck');
let parentTitleContainer;
let vbox;
if (isChild) {
vbox = document.createXULElement('vbox');
vbox.setAttribute('data-tab-id', Zotero_Tabs.selectedID);
vbox.style.display = 'flex';
editor = new (customElements.get('note-editor'));
editor.style.display = 'flex';
editor.style.width = '100%';
vbox.append(editor);
tabNotesDeck.append(vbox);
editor.mode = this.editable ? 'edit' : 'view';
editor.item = item;
editor.parentItem = null;
this.viewType = "childNote";
tabNotesDeck.setAttribute('selectedIndex', tabNotesDeck.children.length - 1);
parentTitleContainer = this.querySelector('.context-note-child > .zotero-context-pane-editor-parent-line');
}
else {
this.viewType = "standaloneNote";
editor.mode = this.editable ? 'edit' : 'view';
editor.item = item;
editor.parentItem = null;
parentTitleContainer = node.querySelector('.context-note-standalone > .zotero-context-pane-editor-parent-line');
}
editor.focus();
let parentItem = item.parentItem;
let container = document.createElement('div');
container.classList.add("parent-title-container");
let returnBtn = document.createXULElement("toolbarbutton");
returnBtn.classList.add("zotero-tb-note-return");
returnBtn.addEventListener("command", () => {
// Immediately save note content before vbox with note-editor iframe is destroyed below
editor.saveSync();
ZoteroContextPane.viewType = "notes";
this.viewType = "notesList";
vbox?.remove();
ZoteroContextPane.updateAddToNote();
this._preventViewTypeCache = true;
});
let title = document.createElement('div');
title.className = 'parent-title';
title.textContent = parentItem?.getDisplayTitle() || '';
container.append(returnBtn, title);
parentTitleContainer.replaceChildren(container);
ZoteroContextPane.updateAddToNote();
}
async _updateNotesList(useCached) {
let query = this.input.value;
let notes;
// Calls itself and debounces until notes list becomes
// visible, and then updates
if (!useCached && !this._isNotesListVisible()) {
this.update();
return;
}
if (useCached && this.cachedNotes.length) {
notes = this.cachedNotes;
}
else {
await Zotero.Schema.schemaUpdatePromise;
let s = new Zotero.Search();
s.addCondition('libraryID', 'is', this.libraryID);
s.addCondition('itemType', 'is', 'note');
if (query) {
let parts = Zotero.SearchConditions.parseSearchString(query);
for (let part of parts) {
s.addCondition('note', 'contains', part.text);
}
}
notes = await s.search();
notes = Zotero.Items.get(notes);
if (Zotero.Prefs.get('sortNotesChronologically.reader')) {
notes.sort((a, b) => {
a = a.dateModified;
b = b.dateModified;
return (a > b ? -1 : (a < b ? 1 : 0));
});
}
else {
let collation = Zotero.getLocaleCollation();
notes.sort((a, b) => {
let aTitle = Zotero.Items.getSortTitle(a.getNoteTitle());
let bTitle = Zotero.Items.getSortTitle(b.getNoteTitle());
return collation.compareString(1, aTitle, bTitle);
});
}
let cachedNotesIndex = new Map();
for (let cachedNote of this.cachedNotes) {
cachedNotesIndex.set(cachedNote.id, cachedNote);
}
notes = notes.map((note) => {
let parentItem = note.parentItem;
// If neither note nor parent item is affected try to return the cached note
if (!this.affectedIDs.has(note.id)
&& (!parentItem || !this.affectedIDs.has(parentItem.id))) {
let cachedNote = cachedNotesIndex.get(note.id);
if (cachedNote) {
return cachedNote;
}
}
let text = note.note;
text = Zotero.Utilities.unescapeHTML(text);
text = text.trim();
text = text.slice(0, 500);
let parts = text.split('\n').map(x => x.trim()).filter(x => x.length);
let title = parts[0] && parts[0].slice(0, Zotero.Notes.MAX_TITLE_LENGTH);
let date = Zotero.Date.sqlToDate(note.dateModified, true);
date = Zotero.Date.toFriendlyDate(date);
return {
id: note.id,
title: title || Zotero.getString('pane.item.notes.untitled'),
body: parts[1] || '',
date,
parentID: note.parentID,
parentItemType: parentItem && parentItem.itemType,
parentTitle: parentItem && parentItem.getDisplayTitle()
};
});
this.cachedNotes = notes;
this.affectedIDs = new Set();
}
let attachment = this._getCurrentAttachment();
let parentID = attachment && attachment.parentID;
this.notesList.hasParent = !!parentID;
this.notesList.notes = notes.map(note => ({
...note,
isCurrentChild: parentID && note.parentID == parentID
}));
}
_cacheViewType() {
if (ZoteroContextPane.viewType == "notes"
&& this.viewType != "childNote" && !this._preventViewTypeCache) {
this._cachedViewType = this.viewType;
}
this._preventViewTypeCache = false;
}
_restoreViewType() {
if (!this._cachedViewType) return;
this.viewType = this._cachedViewType;
this._cachedViewType = "";
}
_handleListPopupClick(id, event) {
switch (event.originalTarget.id) {
case 'context-pane-list-show-in-library':
ZoteroPane_Local.selectItem(id);
Zotero_Tabs.select('zotero-pane');
break;
case 'context-pane-list-edit-in-window':
ZoteroPane_Local.openNoteWindow(id);
break;
case 'context-pane-list-move-to-trash':
if (this.editable) {
Zotero.Items.trashTx(id);
this.cachedNotes = this.cachedNotes.filter(x => x.id != id);
this._updateNotesList(true);
}
break;
default:
}
}
_handleAddChildNotePopupClick = (event) => {
if (!this.editable) {
return;
}
switch (event.originalTarget.id) {
case 'context-pane-add-child-note':
this._createNote(true);
break;
case 'context-pane-add-child-note-from-annotations':
this._createNoteFromAnnotations(true);
break;
default:
}
};
_handleAddStandaloneNotePopupClick = (event) => {
if (!this.editable) {
return;
}
switch (event.originalTarget.id) {
case 'context-pane-add-standalone-note':
this._createNote();
break;
case 'context-pane-add-standalone-note-from-annotations':
this._createNoteFromAnnotations();
break;
default:
}
};
}
customElements.define("notes-context", NotesContext);
}

View file

@ -257,7 +257,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]: data }, true);
Zotero.Notifier.trigger('add', 'tab', [id], { [id]: Object.assign({}, data, { type }) }, true);
if (select) {
let previousID = this._selectedID;
this.select(id);

View file

@ -913,6 +913,8 @@ class EditorInstance {
}
_getSpellChecker() {
// Fix cannot access dead object error
if (Components.utils.isDeadWrapper(this._iframeWindow)) return null;
if (!this._spellChecker) {
let editingSession = this._iframeWindow.docShell.editingSession;
this._spellChecker = new InlineSpellChecker(

View file

@ -1216,7 +1216,7 @@
>
<grippy/>
</splitter>
<vbox id="zotero-context-pane-inner" zotero-persist="height"/>
<context-pane id="zotero-context-pane-inner" flex="1" zotero-persist="height"></context-pane>
</vbox>
<item-pane-sidenav id="zotero-context-pane-sidenav" class="zotero-view-item-sidenav" hidden="true"/>
</box>

View file

@ -24,7 +24,8 @@ attachments-box {
}
}
&:not([data-use-preview]) {
&:not([data-use-preview]),
& > collapsible-section[empty] {
attachment-preview {
visibility: hidden;
display: none;

View file

@ -1,7 +1,26 @@
item-pane {
&[collapsed="true"] {
min-width: $width-sidenav;
min-height: $width-sidenav;
max-width: $width-sidenav;
&[view-type=note] {
min-width: 0;
width: 0px !important;
}
visibility: inherit;
&.stacked {
max-width: unset;
max-height: $width-sidenav;
&[view-type=note] {
min-height: 0;
height: 0px !important;
}
}
#zotero-item-pane-content {
visibility: collapse;
}