325 lines
10 KiB
JavaScript
325 lines
10 KiB
JavaScript
/*
|
|
***** 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 mode() {
|
|
return ["item", "notes"][this._panesDeck.getAttribute('selectedIndex')];
|
|
}
|
|
|
|
set mode(mode) {
|
|
let modeMap = {
|
|
item: "0",
|
|
notes: "1",
|
|
};
|
|
if (!(mode in modeMap)) {
|
|
throw new Error(`ContextPane.mode must be one of ["item", "notes"], but got ${mode}`);
|
|
}
|
|
this._panesDeck.selectedIndex = modeMap[mode];
|
|
}
|
|
|
|
get activeEditor() {
|
|
let currentContext = this._getCurrentNotesContext();
|
|
return currentContext?._getCurrentEditor();
|
|
}
|
|
|
|
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 = ZoteroContextPane.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.showLoadingMessage(false);
|
|
this._sidenav.hidden = true;
|
|
}
|
|
else if (Zotero_Tabs.selectedType == 'reader') {
|
|
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.showLoadingMessage(true);
|
|
await reader._initPromise;
|
|
ZoteroContextPane.showLoadingMessage(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.updateNotesListFromCache();
|
|
}
|
|
|
|
let currentNoteContext = this._getCurrentNotesContext();
|
|
currentNoteContext.switchToTab(reader.tabID);
|
|
}
|
|
|
|
_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();
|
|
}
|
|
|
|
_getItemContext(tabID) {
|
|
return this._itemPaneDeck.querySelector(`[data-tab-id="${tabID}"]`);
|
|
}
|
|
|
|
_removeItemContext(tabID) {
|
|
this._itemPaneDeck.querySelector(`[data-tab-id="${tabID}"]`)?.remove();
|
|
}
|
|
|
|
_selectItemContext(tabID) {
|
|
let previousContainer = this._sidenav.container;
|
|
let selectedPanel = this._getItemContext(tabID);
|
|
if (selectedPanel) {
|
|
this._itemPaneDeck.selectedPanel = selectedPanel;
|
|
selectedPanel.sidenav = this._sidenav;
|
|
// Inherits previous pinned states
|
|
if (previousContainer) selectedPanel.pinnedPane = previousContainer.pinnedPane;
|
|
selectedPanel.render();
|
|
}
|
|
}
|
|
|
|
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 editable = 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.editable = editable;
|
|
itemDetails.tabType = "reader";
|
|
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);
|
|
}
|
|
}
|
|
|
|
handleFocus() {
|
|
let splitter = ZoteroContextPane.splitter;
|
|
|
|
if (splitter.getAttribute('state') != 'collapsed') {
|
|
if (this.mode == "item") {
|
|
let header = this._itemPaneDeck.selectedPanel.querySelector("pane-header editable-text");
|
|
header.focus();
|
|
return true;
|
|
}
|
|
else {
|
|
this._getCurrentNotesContext()?.focus();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
customElements.define("context-pane", ContextPane);
|
|
}
|