zotero/chrome/content/zotero/contextPane.js

807 lines
24 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2020 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
*/
// TODO: Fix import/require related isues that might be
// related with `require` not reusing the context
var React = require('react');
var ReactDOM = require('react-dom');
var TagsBoxContainer = require('containers/tagsBoxContainer').default;
var NotesList = require('components/itemPane/notesList').default;
var ZoteroContextPane = new function () {
const HTML_NS = 'http://www.w3.org/1999/xhtml';
var _tabCover;
var _contextPane;
var _contextPaneInner;
var _contextPaneSplitter;
var _contextPaneSplitterStacked;
var _itemToggle;
var _notesToggle;
var _panesDeck;
var _itemPaneDeck;
var _notesPaneDeck;
var _itemToolbar;
var _splitButton;
var _itemPaneToggle;
var _notesPaneToggle;
var _toolbar;
var _tabToolbarContainer;
var _itemContexts = [];
var _notesContexts = [];
// Using attribute instead of propery to set 'selectedIndex'
// is more reliable
this.update = _update;
this.getActiveEditor = _getActiveEditor;
this.onLoad = function () {
if (!Zotero) {
return;
}
_tabCover = document.getElementById('zotero-tab-cover');
_itemToggle = document.getElementById('zotero-tb-toggle-item-pane');
_notesToggle = document.getElementById('zotero-tb-toggle-notes-pane');
_contextPane = document.getElementById('zotero-context-pane');
_contextPaneInner = document.getElementById('zotero-context-pane-inner');
_contextPaneSplitter = document.getElementById('zotero-context-splitter');
_contextPaneSplitterStacked = document.getElementById('zotero-context-splitter-stacked');
_itemToolbar = document.getElementById('zotero-item-toolbar');
_splitButton = document.getElementById('zotero-tb-split');
_itemPaneToggle = document.getElementById('zotero-tb-toggle-item-pane');
_notesPaneToggle = document.getElementById('zotero-tb-toggle-notes-pane');
_toolbar = document.getElementById('zotero-toolbar');
_tabToolbarContainer = document.getElementById('zotero-tab-toolbar-container');
_init();
this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'contextPane');
window.addEventListener('resize', _update);
_itemToggle.addEventListener('click', _toggleItemButton);
_notesToggle.addEventListener('click', _toggleNotesButton);
Zotero.Reader.onChangeSidebarWidth = _updatePaneWidth;
Zotero.Reader.onChangeSidebarOpen = _updatePaneWidth;
this._mutationObserver = new MutationObserver(() => {
_updateToolbarWidth();
// Sometimes XUL is late to reflow
setTimeout(_updateToolbarWidth, 100);
});
this._mutationObserver.observe(_tabToolbarContainer, { attributes: true, childList: true, subtree: true });
};
this.onUnload = function () {
_itemToggle.removeEventListener('click', _toggleItemButton);
_notesToggle.removeEventListener('click', _toggleNotesButton);
window.removeEventListener('resize', _update);
Zotero.Notifier.unregisterObserver(this._notifierID);
this._mutationObserver.disconnect();
Zotero.Reader.onChangeSidebarWidth = () => {};
Zotero.Reader.onChangeSidebarOpen = () => {};
_contextPaneInner.innerHTML = '';
_itemContexts = [];
_notesContexts = [];
};
this.notify = Zotero.Promise.coroutine(function* (action, type, ids, extraData) {
if (type == 'item') {
// 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()) {
libraryIDs.push(item.libraryID);
}
else if (action == 'delete') {
libraryIDs.push(extraData[id].libraryID);
}
}
for (let context of _notesContexts) {
if (libraryIDs.includes(context.libraryID)) {
context.update();
}
}
}
else if (type == 'tab') {
if (action == 'add') {
_addItemContext(ids[0], extraData[ids[0]].itemID);
}
else if (action == 'close') {
_removeItemContext(ids[0]);
}
else if (action == 'select') {
if (Zotero_Tabs.selectedIndex == 0) {
_contextPaneSplitter.setAttribute('hidden', true);
_contextPane.setAttribute('collapsed', true);
_toolbar.append(_itemToolbar);
_itemToolbar.classList.remove('tab-mode');
_splitButton.classList.add('hidden');
_tabCover.hidden = true;
}
else {
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
_tabCover.hidden = false;
(async () => {
await reader._initPromise;
_tabCover.hidden = true;
})();
}
_contextPaneSplitter.setAttribute('hidden', false);
_contextPane.setAttribute('collapsed', !(_contextPaneSplitter.getAttribute('state') != 'collapsed'));
_tabToolbarContainer.append(_itemToolbar);
_itemToolbar.classList.add('tab-mode');
_splitButton.classList.remove('hidden');
}
var context = _itemContexts.find(x => x.tabID == ids[0]);
if (context) {
_selectNotesContext(context.libraryID);
}
_selectItemContext(ids[0]);
_update();
}
}
});
function _toggleItemButton() {
_togglePane(0);
}
function _toggleNotesButton() {
_togglePane(1);
}
function _removeNote(id) {
var ps = Components.classes['@mozilla.org/embedcomp/prompt-service;1']
.getService(Components.interfaces.nsIPromptService);
if (ps.confirm(null, '', Zotero.getString('pane.item.notes.delete.confirm'))) {
Zotero.Items.trashTx(id);
}
}
function _getActiveEditor() {
var splitter;
if (Zotero.Prefs.get('layout') == 'stacked') {
splitter = _contextPaneSplitterStacked;
}
else {
splitter = _contextPaneSplitter;
}
if (splitter.getAttribute('state') != 'collapsed') {
if (_panesDeck.selectedIndex == 0) {
let child = _itemPaneDeck.selectedPanel;
if (child) {
var tabPanels = child.querySelector('tabpanels');
if (tabPanels && tabPanels.selectedIndex == 1) {
var notesDeck = child.querySelector('.notes-deck');
if (notesDeck.selectedIndex == 1) {
return child.querySelector('zoteronoteeditor');
}
}
}
}
else {
var node = _notesPaneDeck.selectedPanel;
if (node.selectedIndex == 1) {
return node.querySelector('zoteronoteeditor');
}
}
}
}
function _updateAddToNote() {
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
var editor = _getActiveEditor();
reader.enableAddToNote(!!editor);
}
}
function _updatePaneWidth() {
var stacked = Zotero.Prefs.get('layout') == 'stacked';
var width = Zotero.Reader.getSidebarWidth() + 'px';
if (!Zotero.Reader.getSidebarOpen()) {
width = 0;
}
_contextPane.style.left = stacked ? width : 'unset';
}
function _updateToolbarWidth() {
var stacked = Zotero.Prefs.get('layout') == 'stacked';
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
if ((stacked || _contextPaneSplitter.getAttribute('state') == 'collapsed')) {
reader.setToolbarPlaceholderWidth(_tabToolbarContainer.boxObject.width);
}
else {
reader.setToolbarPlaceholderWidth(0);
}
}
}
function _update() {
if (Zotero_Tabs.selectedIndex == 0) {
return;
}
var splitter;
var stacked = Zotero.Prefs.get('layout') == 'stacked';
if (stacked) {
_contextPaneSplitterStacked.setAttribute('hidden', false);
_contextPaneSplitter.setAttribute('state', 'open');
_contextPaneSplitter.setAttribute('hidden', true);
_contextPane.classList.add('stacked');
_contextPane.classList.remove('standard');
splitter = _contextPaneSplitterStacked;
}
else {
_contextPaneSplitter.setAttribute('hidden', false);
_contextPaneSplitterStacked.setAttribute('hidden', true);
_contextPaneSplitterStacked.setAttribute('state', 'open');
_contextPane.classList.add('standard');
_contextPane.classList.remove('stacked');
splitter = _contextPaneSplitter;
}
var collapsed = splitter.getAttribute('state') == 'collapsed';
var selectedIndex = _panesDeck.selectedIndex;
if (!collapsed && selectedIndex == 0) {
_itemPaneToggle.classList.add('toggled');
}
else {
_itemPaneToggle.classList.remove('toggled');
}
if (!collapsed && selectedIndex == 1) {
_notesPaneToggle.classList.add('toggled');
}
else {
_notesPaneToggle.classList.remove('toggled');
}
if (Zotero_Tabs.selectedIndex > 0) {
var height = 0;
if (Zotero.Prefs.get('layout') == 'stacked'
&& _contextPane.getAttribute('collapsed') != 'true') {
height = _contextPaneInner.boxObject.height;
}
Zotero.Reader.setBottomPlaceholderHeight(height);
}
_updatePaneWidth();
_updateToolbarWidth();
_updateAddToNote();
}
function _togglePane(paneIndex) {
var splitter = Zotero.Prefs.get('layout') == 'stacked'
? _contextPaneSplitterStacked : _contextPaneSplitter;
var isOpen = splitter.getAttribute('state') != 'collapsed';
var hide = false;
var currentPane = _panesDeck.selectedIndex;
if (isOpen && currentPane == paneIndex) {
hide = true;
}
else {
_panesDeck.setAttribute('selectedIndex', paneIndex);
}
splitter.setAttribute('state', hide ? 'collapsed' : 'open');
_update();
}
function _init() {
// vbox
var vbox = document.createElement('vbox');
vbox.setAttribute('flex', '1');
_contextPaneInner.append(vbox);
// Toolbar extension
var toolbarExtension = document.createElement('box');
toolbarExtension.style.height = '32px';
toolbarExtension.id = 'zotero-context-toolbar-extension';
_panesDeck = document.createElement('deck');
_panesDeck.setAttribute('flex', 1);
_panesDeck.setAttribute('selectedIndex', 0);
vbox.append(toolbarExtension, _panesDeck);
// Item pane deck
_itemPaneDeck = document.createElement('deck');
// Notes pane deck
_notesPaneDeck = document.createElement('deck');
_notesPaneDeck.style.backgroundColor = 'white';
_notesPaneDeck.setAttribute('flex', 1);
_notesPaneDeck.className = 'notes-pane-deck';
_panesDeck.append(_itemPaneDeck, _notesPaneDeck);
}
function _addNotesContext(libraryID) {
var list = document.createElement('vbox');
list.setAttribute('flex', 1);
list.className = 'zotero-context-notes-list';
var noteContainer = document.createElement('vbox');
var editor = document.createElement('zoteronoteeditor');
editor.className = 'zotero-context-pane-pinned-note';
editor.setAttribute('flex', 1);
noteContainer.appendChild(editor);
let contextNode = document.createElement('deck');
contextNode.append(list, noteContainer);
_notesPaneDeck.append(contextNode);
contextNode.className = 'context-node';
contextNode.setAttribute('selectedIndex', 0);
editor.returnHandler = () => {
contextNode.setAttribute('selectedIndex', 0);
_updateAddToNote();
};
var head = document.createElement('hbox');
head.style.display = 'flex';
var label = document.createElement('label');
var button = document.createElement('button');
button.setAttribute('label', Zotero.Intl.strings['zotero.toolbar.newNote']);
button.addEventListener('click', () => {
contextNode.setAttribute('selectedIndex', 1);
var item = new Zotero.Item('note');
item.libraryID = libraryID;
// item.parentKey = parentItem.key;
editor.mode = 'edit';
editor.item = item;
editor.parentItem = null;
editor.focus();
_updateAddToNote();
});
var vbox1 = document.createElement('vbox');
vbox1.append(label, button);
var vbox2 = document.createElement('vbox');
vbox2.style.flex = '1';
var input = document.createElement('textbox');
input.setAttribute('type', 'search');
input.setAttribute('timeout', '250');
input.addEventListener('command', () => {
_updateNotesList();
});
vbox2.append(input);
head.append(vbox2, vbox1);
var listBox = document.createElement('vbox');
listBox.style.display = 'flex';
listBox.setAttribute('flex', '1');
var listInner = document.createElementNS(HTML_NS, 'div');
listInner.className = 'notes-list-container';
listBox.append(listInner);
list.append(head, listBox);
var notesListRef = React.createRef();
async function _updateNotesList(reset) {
if (reset) {
input.value = '';
contextNode.setAttribute('selectedIndex', 0);
}
var query = input.value;
await Zotero.Schema.schemaUpdatePromise;
var s = new Zotero.Search();
s.addCondition('libraryID', 'is', libraryID);
s.addCondition('itemType', 'is', 'note');
s.addCondition('noChildren', 'true');
if (query) {
let parts = Zotero.SearchConditions.parseSearchString(query);
for (let part of parts) {
s.addCondition('note', 'contains', part.text);
}
}
var notes = await s.search();
notes = Zotero.Items.get(notes);
notes.sort((a, b) => {
a = a.getField('dateModified');
b = b.getField('dateModified');
return b.localeCompare(a);
});
notesListRef.current.setNotes(notes.map(note => {
var text = note.note;
text = Zotero.Utilities.unescapeHTML(text);
text = text.trim();
text = text.slice(0, 500);
var parts = text.split('\n').map(x => x.trim()).filter(x => x.length);
var title = parts[0] && parts[0].slice(0, Zotero.Notes.MAX_TITLE_LENGTH);
return {
id: note.id,
title: title || Zotero.getString('pane.item.notes.untitled'),
body: parts[1] || '',
date: (new Date(note.dateModified).toLocaleDateString(Zotero.locale))
};
}));
var c = notes.length;
label.value = Zotero.getString('pane.item.notes.count', c, c);
}
ReactDOM.render(
<NotesList
ref={notesListRef}
onClick={(id) => {
_setPinnedNote(libraryID, id);
}}
/>,
listInner,
() => {
_updateNotesList();
}
);
var context = {
libraryID,
node: contextNode,
update: Zotero.Utilities.throttle(_updateNotesList, 1000, { leading: false }),
editor
};
_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);
}
function _isLibraryEditable(libraryID) {
var type = Zotero.Libraries.get(libraryID).libraryType;
if (type == 'group') {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
var group = Zotero.Groups.get(groupID);
return group.editable;
}
return true;
}
function _setPinnedNote(libraryID, itemID) {
var editable = _isLibraryEditable(libraryID);
var context = _getNotesContext(libraryID);
if (context) {
let { editor, node } = context;
node.setAttribute('selectedIndex', 1);
editor.mode = editable ? 'edit' : 'view';
editor.item = Zotero.Items.get(itemID);
editor.parentItem = null;
editor.hideLinksContainer = true;
_updateAddToNote();
}
}
function _appendNoteRows(notes, list, editable, onClick, onDelete) {
for (var i = 0; i < notes.length; i++) {
var note = notes[i];
let id = notes[i].id;
var icon = document.createElement('image');
icon.className = 'zotero-box-icon';
icon.setAttribute('src', `chrome://zotero/skin/treeitem-note${Zotero.hiDPISuffix}.png`);
var label = document.createElement('label');
label.className = 'zotero-box-label';
var title = note.getNoteTitle();
title = title ? title : Zotero.getString('pane.item.notes.untitled');
label.setAttribute('value', title);
label.setAttribute('flex', '1'); //so that the long names will flex smaller
label.setAttribute('crop', 'end');
var box = document.createElement('box');
box.setAttribute('class', 'zotero-clicky');
box.addEventListener('click', () => {
onClick(id);
});
box.appendChild(icon);
box.appendChild(label);
var row = document.createElement('row');
row.appendChild(box);
if (editable) {
var removeButton = document.createElement('label');
removeButton.setAttribute('value', '-');
removeButton.setAttribute('class', 'zotero-clicky zotero-clicky-minus');
removeButton.addEventListener('click', function () {
onDelete(id);
});
row.appendChild(removeButton);
}
list.appendChild(row);
}
}
function _removeItemContext(tabID) {
document.getElementById(tabID + '-context').remove();
_itemContexts = _itemContexts.filter(x => x.tabID != tabID);
}
function _selectItemContext(tabID) {
let selectedIndex = Array.from(_itemPaneDeck.children).findIndex(x => x.id == tabID + '-context');
if (selectedIndex != -1) {
_itemPaneDeck.setAttribute('selectedIndex', selectedIndex);
}
}
function _addItemContext(tabID, itemID) {
var item = Zotero.Items.get(itemID);
if (!item) {
return;
}
var libraryID = item.libraryID;
var editable = _isLibraryEditable(libraryID);
var parentID = item.parentID;
var container = document.createElement('vbox');
container.id = tabID + '-context';
container.className = 'zotero-item-pane-content';
_itemPaneDeck.appendChild(container);
var context = {
tabID,
itemID,
parentID,
libraryID,
update: () => {}
};
_itemContexts.push(context);
if (!parentID) {
var vbox = document.createElement('vbox');
vbox.setAttribute('flex', '1');
vbox.setAttribute('align', 'center');
vbox.setAttribute('pack', 'center');
var description = document.createElement('description');
vbox.append(description);
description.append(Zotero.getString('pane.context.noParent'));
container.append(vbox);
return;
}
var parentItem = Zotero.Items.get(item.parentID);
// tabbox
var tabbox = document.createElement('tabbox');
tabbox.setAttribute('flex', '1');
tabbox.className = 'zotero-view-tabbox';
container.append(tabbox);
// tabs
var tabs = document.createElement('tabs');
tabs.className = 'zotero-editpane-tabs';
// tabpanels
var tabpanels = document.createElement('tabpanels');
tabpanels.setAttribute('flex', '1');
tabpanels.className = 'zotero-view-item';
tabpanels.addEventListener('select', () => {
_updateAddToNote();
});
tabbox.append(tabs, tabpanels);
// Info tab
var tabInfo = document.createElement('tab');
tabInfo.setAttribute('label', Zotero.Intl.strings['zotero.tabs.info.label']);
// Notes tab
var tabNotes = document.createElement('tab');
tabNotes.setAttribute('label', Zotero.Intl.strings['zotero.tabs.notes.label']);
// Tags tab
var tabTags = document.createElement('tab');
tabTags.setAttribute('label', Zotero.Intl.strings['zotero.tabs.tags.label']);
// Related tab
var tabRelated = document.createElement('tab');
tabRelated.setAttribute('label', Zotero.Intl.strings['zotero.tabs.related.label']);
tabs.append(tabInfo, tabNotes, tabTags, tabRelated);
// Info panel
var panelInfo = document.createElement('tabpanel');
panelInfo.setAttribute('flex', '1');
panelInfo.className = 'zotero-editpane-item-box';
var itemBox = document.createElement('zoteroitembox');
itemBox.setAttribute('flex', '1');
panelInfo.append(itemBox);
// Notes panel
var panelNotes = document.createElement('tabpanel');
panelNotes.setAttribute('flex', '1');
panelNotes.setAttribute('orient', 'vertical');
var deck = document.createElement('deck');
deck.className = 'notes-deck';
deck.setAttribute('flex', '1');
panelNotes.append(deck);
var vbox2 = document.createElement('vbox');
var note = document.createElement('zoteronoteeditor');
note.setAttribute('flex', 1);
vbox2.append(note);
var vbox = document.createElement('vbox');
vbox.setAttribute('flex', '1');
vbox.setAttribute('class', 'zotero-box');
vbox.style.overflowY = 'auto';
panelNotes.append(vbox);
var hbox = document.createElement('hbox');
hbox.setAttribute('align', 'center');
var label = document.createElement('label');
var button = document.createElement('button');
button.hidden = !editable;
button.setAttribute('label', Zotero.Intl.strings['zotero.item.add']);
button.addEventListener('click', () => {
deck.setAttribute('selectedIndex', 1);
var item = new Zotero.Item('note');
item.libraryID = parentItem.libraryID;
item.parentID = parentItem.id;
note.returnHandler = () => {
deck.setAttribute('selectedIndex', 0);
_updateAddToNote();
};
note.mode = editable ? 'edit' : 'view';
note.item = item;
note.focus();
_updateAddToNote();
});
hbox.append(label, button);
var grid = document.createElement('grid');
grid.setAttribute('flex', 1);
var columns = document.createElement('columns');
var column = document.createElement('column');
column.setAttribute('flex', 1);
columns.append(column);
var column = document.createElement('column');
columns.append(column);
grid.append(columns);
var rows = document.createElement('rows');
rows.setAttribute('flex', 1);
grid.append(rows);
vbox.append(hbox, grid);
deck.append(vbox, vbox2);
deck.setAttribute('selectedIndex', 0);
// Tags panel
var panelTags = document.createElement('tabpanel');
panelTags.setAttribute('orient', 'vertical');
panelTags.setAttribute('context', 'tags-context-menu');
panelTags.className = 'tags-pane';
panelTags.style.display = 'flex';
var div = document.createElementNS(HTML_NS, 'div');
div.className = 'tags-box-container';
div.style.display = 'flex';
div.style.flexGrow = '1';
panelTags.append(div);
var tagsBoxRef = React.createRef();
ReactDOM.render(
<TagsBoxContainer
key={'tagsBox-' + parentItem.id}
item={parentItem}
editable={editable}
ref={tagsBoxRef}
/>,
div
);
// Related panel
var panelRelated = document.createElement('tabpanel');
var relatedBox = document.createElement('relatedbox');
relatedBox.setAttribute('flex', '1');
relatedBox.className = 'zotero-editpane-related';
panelRelated.addEventListener('click', (event) => {
if (event.originalTarget.closest('.zotero-clicky')) {
Zotero_Tabs.select('zotero-pane');
}
});
panelRelated.append(relatedBox);
tabpanels.append(panelInfo, panelNotes, panelTags, panelRelated);
tabbox.selectedIndex = 0;
itemBox.mode = editable ? 'edit' : 'view';
itemBox.item = parentItem;
relatedBox.mode = editable ? 'edit' : 'view';
relatedBox.item = parentItem;
function _renderNotesPanel() {
rows.innerHTML = '';
var parentItem = Zotero.Items.get(parentID);
if (!parentItem) {
return;
}
var parentNotes = Zotero.Items.get(parentItem.getNotes());
_appendNoteRows(parentNotes, rows, editable, (id) => {
deck.setAttribute('selectedIndex', 1);
note.returnHandler = () => {
deck.setAttribute('selectedIndex', 0);
_updateAddToNote();
};
note.mode = editable ? 'edit' : 'view';
note.item = Zotero.Items.get(id);
note.parentItem = null;
_updateAddToNote();
}, (id) => {
_removeNote(id);
});
var c = parentNotes.length;
label.value = Zotero.getString('pane.item.notes.count', c, c);
}
context.update = Zotero.Utilities.throttle(_renderNotesPanel, 500);
_renderNotesPanel();
}
};
addEventListener('load', function (e) { ZoteroContextPane.onLoad(e); }, false);
addEventListener('unload', function (e) { ZoteroContextPane.onUnload(e); }, false);