CEify contextPane
This commit is contained in:
parent
b2ad17d604
commit
b34859d882
16 changed files with 1010 additions and 811 deletions
|
@ -32,6 +32,7 @@
|
|||
"ZoteroPane_Local": false,
|
||||
"ZoteroPane": false,
|
||||
"Zotero_Tabs": false,
|
||||
"ZoteroContextPane": false,
|
||||
"IOUtils": false,
|
||||
"NetUtil": false,
|
||||
"FileUtils": false,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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() {
|
||||
|
|
334
chrome/content/zotero/elements/contextPane.js
Normal file
334
chrome/content/zotero/elements/contextPane.js
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
491
chrome/content/zotero/elements/notesContext.js
Normal file
491
chrome/content/zotero/elements/notesContext.js
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -24,7 +24,8 @@ attachments-box {
|
|||
}
|
||||
}
|
||||
|
||||
&:not([data-use-preview]) {
|
||||
&:not([data-use-preview]),
|
||||
& > collapsible-section[empty] {
|
||||
attachment-preview {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue