CEify itemPane
This commit is contained in:
parent
827bcd704d
commit
b2ad17d604
45 changed files with 2081 additions and 1637 deletions
|
@ -23,6 +23,7 @@
|
|||
"XRegExp": false,
|
||||
"XULElement": false,
|
||||
"XULElementBase": false,
|
||||
"ItemPaneSectionElementBase": false,
|
||||
"Cu": false,
|
||||
"ChromeWorker": false,
|
||||
"Localization": false,
|
||||
|
@ -31,7 +32,6 @@
|
|||
"ZoteroPane_Local": false,
|
||||
"ZoteroPane": false,
|
||||
"Zotero_Tabs": false,
|
||||
"ZoteroItemPane": false,
|
||||
"IOUtils": false,
|
||||
"NetUtil": false,
|
||||
"FileUtils": false,
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
#zotero-feed-item-toggleRead-button {
|
||||
margin: 5px 0 3px 6px;
|
||||
}
|
||||
|
||||
#zotero-feed-item-addTo-button {
|
||||
margin: 5px 6px 3px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* Show duplicates date list item as selected even when not focused
|
||||
(default behavior on other platforms) */
|
||||
#zotero-duplicates-merge-original-date:not(:focus) > richlistitem[selected="true"] {
|
||||
background-color: -moz-cellhighlight;
|
||||
color: -moz-cellhighlighttext;
|
||||
}
|
|
@ -17,11 +17,6 @@ tree {
|
|||
padding-left: 0.05em;
|
||||
}
|
||||
|
||||
#zotero-item-pane-groupbox {
|
||||
-moz-appearance: none !important;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.zotero-editpane-item-box > scrollbox, .zotero-view-item > tabpanel > vbox,
|
||||
#zotero-editpane-tags > scrollbox, .zotero-editpane-related {
|
||||
padding-top: 5px;
|
||||
|
|
|
@ -82,12 +82,12 @@ var ZoteroContextPane = new function () {
|
|||
window.addEventListener('resize', _update);
|
||||
Zotero.Reader.onChangeSidebarWidth = _updatePaneWidth;
|
||||
Zotero.Reader.onToggleSidebar = _updatePaneWidth;
|
||||
_contextPaneInner.addEventListener("keypress", ZoteroItemPane.handleKeypress);
|
||||
_contextPaneInner.addEventListener("keypress", ZoteroPane.itemPane._itemDetails.handleKeypress);
|
||||
};
|
||||
|
||||
this.destroy = function () {
|
||||
window.removeEventListener('resize', _update);
|
||||
_contextPaneInner.removeEventListener("keypress", ZoteroItemPane.handleKeypress);
|
||||
_contextPaneInner.removeEventListener("keypress", ZoteroPane.itemPane._itemDetails.handleKeypress);
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
Zotero.Reader.onChangeSidebarWidth = () => {};
|
||||
Zotero.Reader.onToggleSidebar = () => {};
|
||||
|
@ -227,10 +227,6 @@ var ZoteroContextPane = new function () {
|
|||
|
||||
_selectItemContext(ids[0]);
|
||||
_update();
|
||||
// When a loaded tab is selected, scroll to the pinned pane, if any
|
||||
if (_sidenav.pinnedPane) {
|
||||
_sidenav.scrollToPane(_sidenav.pinnedPane, 'instant');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -265,10 +261,12 @@ var ZoteroContextPane = new function () {
|
|||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _focus() {
|
||||
var splitter;
|
||||
let node;
|
||||
if (Zotero.Prefs.get('layout') == 'stacked') {
|
||||
splitter = _contextPaneSplitterStacked;
|
||||
}
|
||||
|
@ -284,7 +282,7 @@ var ZoteroContextPane = new function () {
|
|||
return true;
|
||||
}
|
||||
else {
|
||||
var node = _notesPaneDeck.selectedPanel;
|
||||
node = _notesPaneDeck.selectedPanel;
|
||||
if (node.selectedIndex == 0) {
|
||||
node.querySelector('search-textbox').focus();
|
||||
return true;
|
||||
|
@ -375,12 +373,13 @@ var ZoteroContextPane = new function () {
|
|||
|
||||
_updatePaneWidth();
|
||||
_updateAddToNote();
|
||||
_sidenav.showPendingPane();
|
||||
_sidenav.container?.render();
|
||||
}
|
||||
|
||||
function _togglePane() {
|
||||
var splitter = Zotero.Prefs.get('layout') == 'stacked'
|
||||
? _contextPaneSplitterStacked : _contextPaneSplitter;
|
||||
? _contextPaneSplitterStacked
|
||||
: _contextPaneSplitter;
|
||||
|
||||
var open = true;
|
||||
if (splitter.getAttribute('state') != 'collapsed') {
|
||||
|
@ -396,6 +395,7 @@ var ZoteroContextPane = new function () {
|
|||
if (reader) {
|
||||
return Zotero.Items.get(reader.itemID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _addNotesContext(libraryID) {
|
||||
|
@ -522,7 +522,7 @@ var ZoteroContextPane = new function () {
|
|||
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);
|
||||
let handleCommand = event => _handleListPopupClick(id, event);
|
||||
popup.addEventListener('popupshowing', () => {
|
||||
popup.addEventListener('command', handleCommand, { once: true });
|
||||
popup.addEventListener('popuphiding', () => {
|
||||
|
@ -549,7 +549,8 @@ var ZoteroContextPane = new function () {
|
|||
|
||||
function _isVisible() {
|
||||
let splitter = Zotero.Prefs.get('layout') == 'stacked'
|
||||
? _contextPaneSplitterStacked : _contextPaneSplitter;
|
||||
? _contextPaneSplitterStacked
|
||||
: _contextPaneSplitter;
|
||||
|
||||
return Zotero_Tabs.selectedID != 'zotero-pane'
|
||||
&& _panesDeck.selectedIndex == 1
|
||||
|
@ -604,7 +605,7 @@ var ZoteroContextPane = new function () {
|
|||
for (let cachedNote of context.cachedNotes) {
|
||||
cachedNotesIndex.set(cachedNote.id, cachedNote);
|
||||
}
|
||||
notes = notes.map(note => {
|
||||
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)
|
||||
|
@ -760,8 +761,9 @@ var ZoteroContextPane = new function () {
|
|||
|
||||
var tabNotesDeck = _notesPaneDeck.selectedPanel.querySelector('.zotero-context-pane-tab-notes-deck');
|
||||
var parentTitleContainer;
|
||||
let vbox;
|
||||
if (isChild) {
|
||||
var vbox = document.createXULElement('vbox');
|
||||
vbox = document.createXULElement('vbox');
|
||||
vbox.setAttribute('data-tab-id', Zotero_Tabs.selectedID);
|
||||
vbox.style.display = 'flex';
|
||||
|
||||
|
@ -821,24 +823,16 @@ var ZoteroContextPane = new function () {
|
|||
}
|
||||
|
||||
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;
|
||||
let div = selectedPanel.querySelector('.zotero-view-item');
|
||||
// _addItemContext() awaits, so the div may not have been created yet. We'll set _sidenav.container
|
||||
// below even if we don't set it here.
|
||||
if (div) {
|
||||
_sidenav.container = div;
|
||||
}
|
||||
selectedPanel.sidenav = _sidenav;
|
||||
if (previousPinnedPane) selectedPanel.pinnedPane = previousPinnedPane;
|
||||
}
|
||||
}
|
||||
|
||||
async function _addItemContext(tabID, itemID) {
|
||||
var container = document.createXULElement('vbox');
|
||||
container.id = tabID + '-context';
|
||||
container.className = 'zotero-item-pane-content';
|
||||
_itemPaneDeck.appendChild(container);
|
||||
|
||||
var { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID);
|
||||
var library = Zotero.Libraries.get(libraryID);
|
||||
await library.waitForDataLoad('item');
|
||||
|
@ -860,85 +854,21 @@ var ZoteroContextPane = new function () {
|
|||
};
|
||||
_itemContexts.push(context);
|
||||
|
||||
let previousPinnedPane = _sidenav.container?.pinnedPane || "";
|
||||
|
||||
let targetItem = parentID ? Zotero.Items.get(parentID) : item;
|
||||
|
||||
// Dynamically create item pane tabs and panels as in zoteroPane.xhtml.
|
||||
// Keep the code below in sync with zoteroPane.xhtml
|
||||
let itemDetails = document.createXULElement('item-details');
|
||||
itemDetails.id = tabID + '-context';
|
||||
itemDetails.className = 'zotero-item-pane-content';
|
||||
_itemPaneDeck.appendChild(itemDetails);
|
||||
|
||||
// hbox
|
||||
var hbox = document.createXULElement('hbox');
|
||||
hbox.setAttribute('flex', '1');
|
||||
hbox.className = 'zotero-view-item-container';
|
||||
container.append(hbox);
|
||||
itemDetails.mode = readOnly ? "view" : null;
|
||||
itemDetails.item = targetItem;
|
||||
itemDetails.sidenav = _sidenav;
|
||||
if (previousPinnedPane) itemDetails.pinnedPane = previousPinnedPane;
|
||||
|
||||
// main
|
||||
var main = document.createElement('div');
|
||||
main.className = 'zotero-view-item-main';
|
||||
hbox.append(main);
|
||||
|
||||
// pane-header
|
||||
var paneHeader = document.createXULElement('pane-header');
|
||||
main.append(paneHeader);
|
||||
|
||||
// div
|
||||
var div = document.createElement('div');
|
||||
div.className = 'zotero-view-item';
|
||||
div.setAttribute("tabindex", "0");
|
||||
main.append(div);
|
||||
|
||||
// Info
|
||||
var itemBox = new (customElements.get('item-box'));
|
||||
itemBox.setAttribute('data-pane', 'info');
|
||||
div.append(itemBox);
|
||||
|
||||
// Abstract
|
||||
var abstractBox = new (customElements.get('abstract-box'));
|
||||
abstractBox.className = 'zotero-editpane-abstract';
|
||||
abstractBox.setAttribute('data-pane', 'abstract');
|
||||
div.append(abstractBox);
|
||||
|
||||
// Attachment info
|
||||
var attachmentBox = new (customElements.get('attachment-box'));
|
||||
attachmentBox.className = 'zotero-editpane-attachment';
|
||||
attachmentBox.setAttribute('data-pane', 'attachment-info');
|
||||
div.append(attachmentBox);
|
||||
|
||||
// Tags
|
||||
var tagsBox = new (customElements.get('tags-box'));
|
||||
tagsBox.className = 'zotero-editpane-tags';
|
||||
tagsBox.setAttribute('data-pane', 'tags');
|
||||
div.append(tagsBox);
|
||||
|
||||
// Related
|
||||
var relatedBox = new (customElements.get('related-box'));
|
||||
relatedBox.className = 'zotero-editpane-related';
|
||||
relatedBox.setAttribute('data-pane', 'related');
|
||||
div.append(relatedBox);
|
||||
|
||||
paneHeader.mode = readOnly ? 'view' : 'edit';
|
||||
paneHeader.item = targetItem;
|
||||
|
||||
itemBox.mode = readOnly ? 'view' : 'edit';
|
||||
itemBox.item = targetItem;
|
||||
|
||||
abstractBox.mode = readOnly ? 'view' : 'edit';
|
||||
abstractBox.item = targetItem;
|
||||
|
||||
attachmentBox.mode = readOnly ? 'view' : 'edit';
|
||||
attachmentBox.item = targetItem;
|
||||
|
||||
tagsBox.mode = readOnly ? 'view' : 'edit';
|
||||
tagsBox.item = targetItem;
|
||||
|
||||
relatedBox.mode = readOnly ? 'view' : 'edit';
|
||||
relatedBox.item = targetItem;
|
||||
|
||||
if (_itemPaneDeck.selectedPanel === container) {
|
||||
_sidenav.container = div;
|
||||
}
|
||||
// When a tab is loaded, scroll to the pinned pane, if any
|
||||
if (_sidenav.pinnedPane) {
|
||||
_sidenav.scrollToPane(_sidenav.pinnedPane, 'instant');
|
||||
}
|
||||
_selectItemContext(tabID);
|
||||
await itemDetails.render();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -31,13 +31,18 @@ Services.scriptloader.loadSubScript("resource://zotero/require.js", this);
|
|||
|
||||
Services.scriptloader.loadSubScript("chrome://global/content/customElements.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/base.js", this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemPaneSection.js', this);
|
||||
|
||||
// Load our custom elements
|
||||
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/duplicatesMergePane.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/guidancePanel.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemBox.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemDetails.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemPane.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemMessagePane.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/mergeGroup.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/menulistItemTypes.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/noteEditor.js', this);
|
||||
|
|
|
@ -1,157 +0,0 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2009 Center for History and New Media
|
||||
George Mason University, Fairfax, Virginia, USA
|
||||
http://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 *****
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var Zotero_Duplicates_Pane = new function () {
|
||||
var _masterItem;
|
||||
var _items = [];
|
||||
var _otherItems = [];
|
||||
var _ignoreFields = ['dateAdded', 'dateModified', 'accessDate'];
|
||||
|
||||
this.setItems = function (items, displayNumItemsOnTypeError) {
|
||||
var itemTypeID, oldestItem, otherItems = [];
|
||||
for (let item of items) {
|
||||
// Find the oldest item
|
||||
if (!oldestItem) {
|
||||
oldestItem = item;
|
||||
}
|
||||
else if (item.dateAdded < oldestItem.dateAdded) {
|
||||
otherItems.push(oldestItem);
|
||||
oldestItem = item;
|
||||
}
|
||||
else {
|
||||
otherItems.push(item);
|
||||
}
|
||||
|
||||
if (!item.isRegularItem() || ['annotation', 'attachment', 'note'].includes(item.itemType)) {
|
||||
var msg = Zotero.getString('pane.item.duplicates.onlyTopLevel');
|
||||
ZoteroPane_Local.setItemPaneMessage(msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure all items are of the same type
|
||||
if (itemTypeID) {
|
||||
if (itemTypeID != item.itemTypeID) {
|
||||
if (displayNumItemsOnTypeError) {
|
||||
var msg = Zotero.getString('pane.item.selected.multiple', items.length);
|
||||
}
|
||||
else {
|
||||
var msg = Zotero.getString('pane.item.duplicates.onlySameItemType');
|
||||
}
|
||||
ZoteroPane_Local.setItemPaneMessage(msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
itemTypeID = item.itemTypeID;
|
||||
}
|
||||
}
|
||||
|
||||
_items = items;
|
||||
|
||||
_items.sort(function (a, b) {
|
||||
return a.dateAdded > b.dateAdded ? 1 : a.dateAdded == b.dateAdded ? 0 : -1;
|
||||
});
|
||||
|
||||
//
|
||||
// Update the UI
|
||||
//
|
||||
|
||||
var button = document.getElementById('zotero-duplicates-merge-button');
|
||||
var versionSelect = document.getElementById('zotero-duplicates-merge-version-select');
|
||||
var itembox = document.getElementById('zotero-duplicates-merge-item-box');
|
||||
var fieldSelect = document.getElementById('zotero-duplicates-merge-field-select');
|
||||
|
||||
var alternatives = oldestItem.multiDiff(otherItems, _ignoreFields);
|
||||
if (alternatives) {
|
||||
// Populate menulist with Date Added values from all items
|
||||
var dateList = document.getElementById('zotero-duplicates-merge-original-date');
|
||||
dateList.innerHTML = '';
|
||||
|
||||
var numRows = 0;
|
||||
for (let item of items) {
|
||||
var date = Zotero.Date.sqlToDate(item.dateAdded, true);
|
||||
dateList.appendItem(date.toLocaleString());
|
||||
numRows++;
|
||||
}
|
||||
|
||||
dateList.setAttribute('rows', numRows);
|
||||
|
||||
// If we set this inline, the selection doesn't take on the first
|
||||
// selection after unhiding versionSelect (when clicking
|
||||
// from a set with no differences) -- tested in Fx5.0.1
|
||||
setTimeout(function () {
|
||||
dateList.selectedIndex = 0;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
button.label = Zotero.getString('pane.item.duplicates.mergeItems', (otherItems.length + 1));
|
||||
versionSelect.hidden = fieldSelect.hidden = !alternatives;
|
||||
itembox.hiddenFields = alternatives ? [] : ['dateAdded', 'dateModified'];
|
||||
|
||||
this.setMaster(0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
this.setMaster = function (pos) {
|
||||
var itembox = document.getElementById('zotero-duplicates-merge-item-box');
|
||||
itembox.mode = 'fieldmerge';
|
||||
|
||||
_otherItems = _items.concat();
|
||||
var item = _otherItems.splice(pos, 1)[0];
|
||||
|
||||
// Add master item's values to the beginning of each set of
|
||||
// alternative values so that they're still available if the item box
|
||||
// modifies the item
|
||||
var alternatives = item.multiDiff(_otherItems, _ignoreFields);
|
||||
if (alternatives) {
|
||||
let itemValues = item.toJSON();
|
||||
for (let i in alternatives) {
|
||||
alternatives[i].unshift(itemValues[i] !== undefined ? itemValues[i] : '');
|
||||
}
|
||||
itembox.fieldAlternatives = alternatives;
|
||||
}
|
||||
|
||||
_masterItem = item;
|
||||
itembox.item = item.clone();
|
||||
}
|
||||
|
||||
|
||||
this.merge = Zotero.Promise.coroutine(function* () {
|
||||
var itembox = document.getElementById('zotero-duplicates-merge-item-box');
|
||||
Zotero.CollectionTreeCache.clear();
|
||||
// Update master item with any field alternatives from the item box
|
||||
var json = _masterItem.toJSON();
|
||||
// Exclude certain properties that are empty in the cloned object, so we don't clobber them
|
||||
const { relations, collections, tags, ...keep } = itembox.item.toJSON();
|
||||
Object.assign(json, keep);
|
||||
|
||||
_masterItem.fromJSON(json);
|
||||
Zotero.Items.merge(_masterItem, _otherItems);
|
||||
});
|
||||
}
|
|
@ -26,7 +26,7 @@
|
|||
"use strict";
|
||||
|
||||
{
|
||||
class AbstractBox extends XULElementBase {
|
||||
class AbstractBox extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-abstract" data-pane="abstract">
|
||||
<html:div class="body">
|
||||
|
@ -50,7 +50,6 @@
|
|||
this._item = item;
|
||||
if (item?.isRegularItem()) {
|
||||
this.hidden = false;
|
||||
this.render();
|
||||
}
|
||||
else {
|
||||
this.hidden = true;
|
||||
|
@ -67,13 +66,12 @@
|
|||
}
|
||||
this.blurOpenField();
|
||||
this._mode = mode;
|
||||
this.render();
|
||||
}
|
||||
|
||||
init() {
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'abstractBox');
|
||||
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this.initCollapsibleSection();
|
||||
|
||||
this._abstractField = this.querySelector('editable-text');
|
||||
this._abstractField.addEventListener('change', () => this.save());
|
||||
|
@ -88,7 +86,7 @@
|
|||
|
||||
notify(action, type, ids) {
|
||||
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
|
||||
this.render();
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,7 +95,7 @@
|
|||
this.item.setField('abstractNote', this._abstractField.value);
|
||||
await this.item.saveTx();
|
||||
}
|
||||
this.render();
|
||||
this.render(true);
|
||||
}
|
||||
|
||||
async blurOpenField() {
|
||||
|
@ -107,10 +105,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.item) {
|
||||
return;
|
||||
}
|
||||
render(force = false) {
|
||||
if (!this.item) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
let abstract = this.item.getField('abstractNote');
|
||||
this._section.summary = abstract;
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
*/
|
||||
|
||||
{
|
||||
class AttachmentAnnotationsBox extends XULElementBase {
|
||||
class AttachmentAnnotationsBox extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-attachments-annotations" data-pane="attachment-annotations">
|
||||
<html:div class="body">
|
||||
|
@ -32,41 +32,41 @@
|
|||
</collapsible-section>
|
||||
`);
|
||||
|
||||
get tabType() {
|
||||
return this._tabType;
|
||||
}
|
||||
|
||||
set tabType(tabType) {
|
||||
this._tabType = tabType;
|
||||
this._updateHidden();
|
||||
}
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(item) {
|
||||
this._item = item;
|
||||
if (item?.isFileAttachment()) {
|
||||
this.hidden = false;
|
||||
this.render();
|
||||
}
|
||||
else {
|
||||
this.hidden = true;
|
||||
}
|
||||
this._updateHidden();
|
||||
}
|
||||
|
||||
init() {
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this._section.addEventListener("toggle", this._handleSectionOpen);
|
||||
this.initCollapsibleSection();
|
||||
|
||||
this._body = this.querySelector('.body');
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._section.removeEventListener("toggle", this._handleSectionOpen);
|
||||
}
|
||||
destroy() {}
|
||||
|
||||
notify(action, type, ids) {
|
||||
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
|
||||
this.render();
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
render(force = false) {
|
||||
if (!this.initialized || !this.item?.isFileAttachment()) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
let annotations = this.item.getAnnotations();
|
||||
this._section.setCount(annotations.length);
|
||||
|
@ -91,12 +91,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
_handleSectionOpen = (event) => {
|
||||
if (event.target !== this._section || !this._section.open) {
|
||||
return;
|
||||
_updateHidden() {
|
||||
this.hidden = !this.item?.isFileAttachment() || this.tabType == "reader";
|
||||
}
|
||||
this.render();
|
||||
};
|
||||
}
|
||||
customElements.define("attachment-annotations-box", AttachmentAnnotationsBox);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
|
||||
{
|
||||
class AttachmentBox extends XULElementBase {
|
||||
class AttachmentBox extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-attachment-info" data-pane="attachment-info">
|
||||
<html:div class="body">
|
||||
|
@ -181,6 +181,15 @@
|
|||
this.toggleAttribute('data-use-preview', val);
|
||||
}
|
||||
|
||||
get tabType() {
|
||||
return this._tabType;
|
||||
}
|
||||
|
||||
set tabType(tabType) {
|
||||
this._tabType = tabType;
|
||||
if (tabType == "reader") this.usePreview = false;
|
||||
}
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
@ -192,15 +201,16 @@
|
|||
if (val.isAttachment()) {
|
||||
this._item = val;
|
||||
this.hidden = false;
|
||||
this.render();
|
||||
this._preview.disableResize = false;
|
||||
}
|
||||
else {
|
||||
this.hidden = true;
|
||||
this._preview.disableResize = true;
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this.initCollapsibleSection();
|
||||
|
||||
this._id('url').addEventListener('contextmenu', (event) => {
|
||||
this._id('url-menu').openPopupAtScreen(event.screenX, event.screenY, true);
|
||||
|
@ -230,12 +240,6 @@
|
|||
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentbox');
|
||||
|
||||
this._section.addEventListener("toggle", (ev) => {
|
||||
if (ev.target.open && this.usePreview) {
|
||||
this._preview.render();
|
||||
}
|
||||
});
|
||||
|
||||
// Work around the reindex toolbarbutton not wanting to properly receive focus on tab.
|
||||
// Make <image> focusable. On focus of the image, bounce the focus to the toolbarbutton.
|
||||
// Temporarily remove tabindex from the <image> so that the focus can move past the
|
||||
|
@ -282,15 +286,17 @@
|
|||
continue;
|
||||
}
|
||||
|
||||
this.render();
|
||||
this.render(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async render() {
|
||||
if (this._isRendering) {
|
||||
return;
|
||||
}
|
||||
async render(force = false) {
|
||||
if (!this.item) return;
|
||||
if (this._isRendering) return;
|
||||
if (!this._section.open) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
Zotero.debug('Refreshing attachment box');
|
||||
this._isRendering = true;
|
||||
// Cancel editing filename when refreshing
|
||||
|
@ -298,6 +304,7 @@
|
|||
|
||||
if (this.usePreview) {
|
||||
this._preview.item = this.item;
|
||||
this._preview.render();
|
||||
}
|
||||
|
||||
let fileNameRow = this._id('fileNameRow');
|
||||
|
@ -521,7 +528,7 @@
|
|||
}
|
||||
// Don't allow empty filename
|
||||
if (!newFilename) {
|
||||
this.render();
|
||||
this.render(true);
|
||||
return;
|
||||
}
|
||||
let newExt = getExtension(newFilename);
|
||||
|
@ -577,7 +584,7 @@
|
|||
Zotero.getString('pane.item.attachments.fileNotFound.text1')
|
||||
);
|
||||
}
|
||||
this.render();
|
||||
this.render(true);
|
||||
}
|
||||
|
||||
initAttachmentNoteEditor() {
|
||||
|
|
|
@ -24,8 +24,7 @@
|
|||
*/
|
||||
|
||||
{
|
||||
// eslint-disable-next-line no-undef
|
||||
class AttachmentPreview extends XULElementBase {
|
||||
class AttachmentPreview extends ItemPaneSectionElementBase {
|
||||
static fileTypeMap = {
|
||||
// TODO: support video and audio
|
||||
// 'video/mp4': 'video',
|
||||
|
@ -49,7 +48,7 @@
|
|||
this._isDiscarding = false;
|
||||
this._failedCount = 0;
|
||||
|
||||
this._intersectionOb = new IntersectionObserver(this._handleIntersection.bind(this));
|
||||
// this._intersectionOb = new IntersectionObserver(this._handleIntersection.bind(this));
|
||||
this._resizeOb = new ResizeObserver(this._handleResize.bind(this));
|
||||
}
|
||||
|
||||
|
@ -97,14 +96,6 @@
|
|||
|
||||
set item(val) {
|
||||
this._item = (val instanceof Zotero.Item && val.isFileAttachment()) ? val : null;
|
||||
if (this.isVisible) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
setItemAndRender(item) {
|
||||
this._item = item;
|
||||
this.render();
|
||||
}
|
||||
|
||||
get previewType() {
|
||||
|
@ -140,7 +131,16 @@
|
|||
}
|
||||
|
||||
get hasPreview() {
|
||||
return this.getAttribute("data-preview-status") === "success";
|
||||
return this.dataset.previewStatus === "success";
|
||||
}
|
||||
|
||||
get disableResize() {
|
||||
return this.dataset.disableResize !== "false";
|
||||
}
|
||||
|
||||
set disableResize(val) {
|
||||
this.dataset.disableResize = val ? "true" : "false";
|
||||
this._handleResize();
|
||||
}
|
||||
|
||||
setPreviewStatus(val) {
|
||||
|
@ -151,32 +151,9 @@
|
|||
this.setAttribute("data-preview-status", val);
|
||||
}
|
||||
|
||||
get isVisible() {
|
||||
const rect = this.getBoundingClientRect();
|
||||
// Sample per 20 px
|
||||
const samplePeriod = 20;
|
||||
let x = rect.left + rect.width / 2;
|
||||
let yStart = rect.top;
|
||||
let yEnd = rect.bottom;
|
||||
let elAtPos;
|
||||
// Check visibility from top/bottom to center
|
||||
for (let dy = 1; dy < Math.floor((yEnd - yStart) / 2); dy += samplePeriod) {
|
||||
elAtPos = document.elementFromPoint(x, yStart + dy);
|
||||
if (this.contains(elAtPos)) {
|
||||
return true;
|
||||
}
|
||||
elAtPos = document.elementFromPoint(x, yEnd - dy);
|
||||
if (this.contains(elAtPos)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setPreviewStatus("loading");
|
||||
this._dragImageContainer = this.querySelector(".drag-container");
|
||||
this._intersectionOb.observe(this);
|
||||
this._resizeOb.observe(this);
|
||||
this.addEventListener("dblclick", (event) => {
|
||||
this.openAttachment(event);
|
||||
|
@ -193,7 +170,6 @@
|
|||
|
||||
destroy() {
|
||||
this._reader?.uninit();
|
||||
this._intersectionOb.disconnect();
|
||||
this._resizeOb.disconnect();
|
||||
this.removeEventListener("DOMContentLoaded", this._handleReaderLoad);
|
||||
this.removeEventListener("mouseenter", this.updateGoto);
|
||||
|
@ -401,41 +377,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
async _handleIntersection(entries) {
|
||||
const DISCARD_TIMEOUT = 60000;
|
||||
let needsRefresh = false;
|
||||
let needsDiscard = false;
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
needsRefresh = true;
|
||||
}
|
||||
else {
|
||||
needsDiscard = true;
|
||||
}
|
||||
});
|
||||
if (needsRefresh) {
|
||||
let sidenav = this._getSidenav();
|
||||
// Sidenav is in smooth scrolling mode
|
||||
if (sidenav?._disableScrollHandler) {
|
||||
// Wait for scroll to finish
|
||||
await sidenav._waitForScroll();
|
||||
// If the preview is not visible, do not render
|
||||
if (!this.isVisible) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Try to render the preview when the preview enters viewport
|
||||
this.render();
|
||||
}
|
||||
else if (!this._isDiscardPlanned && needsDiscard) {
|
||||
this._isDiscardPlanned = true;
|
||||
setTimeout(() => {
|
||||
this.discard();
|
||||
}, DISCARD_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
_handleResize() {
|
||||
if (this.disableResize) return;
|
||||
this.style.setProperty("--preview-width", `${this.clientWidth}px`);
|
||||
}
|
||||
|
||||
|
@ -475,14 +418,6 @@
|
|||
this.style.setProperty("--width-height-ratio", scaleRatio);
|
||||
}
|
||||
|
||||
_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");
|
||||
}
|
||||
|
||||
_id(id) {
|
||||
return this.querySelector(`#${id}`);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
|
||||
{
|
||||
class AttachmentPreviewBox extends XULElementBase {
|
||||
class AttachmentPreviewBox extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-attachment-preview" data-pane="attachment-preview">
|
||||
<html:div class="body">
|
||||
|
@ -64,19 +64,14 @@
|
|||
}
|
||||
|
||||
init() {
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this.initCollapsibleSection();
|
||||
this._preview = this.querySelector("#attachment-preview");
|
||||
|
||||
this._section.addEventListener("toggle", (ev) => {
|
||||
if (ev.target.open && this.usePreview) {
|
||||
this._preview.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
|
||||
async render() {
|
||||
if (!this._section.open) return;
|
||||
let bestAttachment = await this.item.getBestAttachment();
|
||||
if (bestAttachment) {
|
||||
this._preview.item = bestAttachment;
|
||||
|
|
|
@ -93,7 +93,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
|
||||
_handleAnnotationClick = () => {
|
||||
// TODO: jump to annotations pane
|
||||
// ZoteroItemPane.setNextPane("attachment-annotations");
|
||||
let pane = this._getSidenav()?.container.querySelector(`:scope > [data-pane="attachment-annotations"]`);
|
||||
if (pane) {
|
||||
pane._section.open = true;
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"use strict";
|
||||
|
||||
{
|
||||
class AttachmentsBox extends XULElementBase {
|
||||
class AttachmentsBox extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-attachments" data-pane="attachments" extra-buttons="add">
|
||||
<html:div class="body">
|
||||
|
@ -52,22 +52,14 @@
|
|||
}
|
||||
|
||||
set item(item) {
|
||||
let isRegularItem = item?.isRegularItem();
|
||||
this.hidden = !isRegularItem;
|
||||
if (!isRegularItem || this._item === item) {
|
||||
if (this._item === item) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._item = item;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
set mode(mode) {
|
||||
this._mode = mode;
|
||||
let hidden = !item?.isRegularItem() || item?.isFeedItem;
|
||||
this.hidden = hidden;
|
||||
this._preview.disableResize = !!hidden;
|
||||
}
|
||||
|
||||
get inTrash() {
|
||||
|
@ -88,17 +80,28 @@
|
|||
this.updateCount();
|
||||
}
|
||||
|
||||
get tabType() {
|
||||
return this._tabType;
|
||||
}
|
||||
|
||||
set tabType(tabType) {
|
||||
this._tabType = tabType;
|
||||
this._updateHidden();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
init() {
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this.initCollapsibleSection();
|
||||
this._section.addEventListener('add', this._handleAdd);
|
||||
// this._section.addEventListener('togglePreview', this._handleTogglePreview);
|
||||
|
||||
|
@ -112,16 +115,11 @@
|
|||
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentsBox');
|
||||
|
||||
this._section.addEventListener("toggle", (ev) => {
|
||||
if (ev.target.open) {
|
||||
this._preview.render();
|
||||
}
|
||||
});
|
||||
|
||||
this._section._contextMenu.addEventListener('popupshowing', this._handleContextMenu, { once: true });
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._section.removeEventListener('add', this._handleAdd);
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
|
||||
|
@ -180,8 +178,9 @@
|
|||
return row;
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
async render(force = false) {
|
||||
if (!this._item) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
this.usePreview = Zotero.Prefs.get('showAttachmentPreview');
|
||||
|
||||
|
@ -202,15 +201,12 @@
|
|||
}
|
||||
|
||||
async updatePreview() {
|
||||
if (!this.usePreview) {
|
||||
if (!this.usePreview || !this._section.open) {
|
||||
return;
|
||||
}
|
||||
let attachment = await this._item.getBestAttachment();
|
||||
if (!this._preview.hasPreview) {
|
||||
this._preview.setItemAndRender(attachment);
|
||||
return;
|
||||
}
|
||||
this._preview.item = attachment;
|
||||
await this._preview.render();
|
||||
}
|
||||
|
||||
_handleAdd = (event) => {
|
||||
|
@ -232,6 +228,7 @@
|
|||
};
|
||||
|
||||
_handleContextMenu = () => {
|
||||
if (this.tabType == "reader") return;
|
||||
let contextMenu = this._section._contextMenu;
|
||||
let menu = document.createXULElement("menuitem");
|
||||
menu.classList.add('menuitem-iconic', 'zotero-menuitem-toggle-preview');
|
||||
|
@ -264,6 +261,10 @@
|
|||
}
|
||||
this._attachmentIDs = sortedAttachmentIDs;
|
||||
}
|
||||
|
||||
_updateHidden() {
|
||||
this.hidden = !this._item?.isRegularItem();
|
||||
}
|
||||
}
|
||||
customElements.define("attachments-box", AttachmentsBox);
|
||||
}
|
||||
|
|
|
@ -212,7 +212,7 @@
|
|||
pinSection.setAttribute('data-l10n-id', 'pin-section');
|
||||
pinSection.addEventListener('command', () => {
|
||||
let sidenav = this._getSidenav();
|
||||
sidenav.scrollToPane(this.dataset.pane, 'smooth');
|
||||
sidenav.container.scrollToPane(this.dataset.pane, 'smooth');
|
||||
sidenav.pinnedPane = this.dataset.pane;
|
||||
});
|
||||
contextMenu.append(pinSection);
|
||||
|
@ -401,6 +401,7 @@
|
|||
if (document.documentElement.getAttribute('windowtype') !== 'navigator:browser') {
|
||||
return null;
|
||||
}
|
||||
if (typeof Zotero_Tabs == "undefined") return null;
|
||||
// TODO: update this after unifying item pane & context pane
|
||||
return document.querySelector(
|
||||
Zotero_Tabs.selectedType === 'library'
|
||||
|
|
186
chrome/content/zotero/elements/duplicatesMergePane.js
Normal file
186
chrome/content/zotero/elements/duplicatesMergePane.js
Normal file
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
***** 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 DuplicatesMergePane extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<groupbox>
|
||||
<button id="zotero-duplicates-merge-button" />
|
||||
</groupbox>
|
||||
|
||||
<groupbox id="zotero-duplicates-merge-version-select">
|
||||
<description>&zotero.duplicatesMerge.versionSelect;</description>
|
||||
<hbox>
|
||||
<richlistbox id="zotero-duplicates-merge-original-date" rows="0"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
|
||||
<groupbox flex="1">
|
||||
<description id="zotero-duplicates-merge-field-select">&zotero.duplicatesMerge.fieldSelect;</description>
|
||||
<vbox id="zotero-duplicates-merge-item-box-container" flex="1">
|
||||
<item-box id="zotero-duplicates-merge-item-box" flex="1"/>
|
||||
</vbox>
|
||||
</groupbox>
|
||||
`, ['chrome://zotero/locale/zotero.dtd']);
|
||||
|
||||
init() {
|
||||
this._masterItem = null;
|
||||
this._items = [];
|
||||
this._otherItems = [];
|
||||
this._ignoreFields = ['dateAdded', 'dateModified', 'accessDate'];
|
||||
|
||||
this.querySelector("#zotero-duplicates-merge-button").addEventListener(
|
||||
"command", () => this.merge());
|
||||
this.querySelector("#zotero-duplicates-merge-original-date").addEventListener(
|
||||
"select", event => this.setMaster(event.target.selectedIndex));
|
||||
}
|
||||
|
||||
setItems(items, displayNumItemsOnTypeError) {
|
||||
let itemTypeID, oldestItem, otherItems = [];
|
||||
for (let item of items) {
|
||||
// Find the oldest item
|
||||
if (!oldestItem) {
|
||||
oldestItem = item;
|
||||
}
|
||||
else if (item.dateAdded < oldestItem.dateAdded) {
|
||||
otherItems.push(oldestItem);
|
||||
oldestItem = item;
|
||||
}
|
||||
else {
|
||||
otherItems.push(item);
|
||||
}
|
||||
|
||||
if (!item.isRegularItem() || ['annotation', 'attachment', 'note'].includes(item.itemType)) {
|
||||
let msg = Zotero.getString('pane.item.duplicates.onlyTopLevel');
|
||||
ZoteroPane.itemPane.setItemPaneMessage(msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure all items are of the same type
|
||||
if (itemTypeID) {
|
||||
if (itemTypeID != item.itemTypeID) {
|
||||
let msg;
|
||||
if (displayNumItemsOnTypeError) {
|
||||
msg = Zotero.getString('pane.item.selected.multiple', items.length);
|
||||
}
|
||||
else {
|
||||
msg = Zotero.getString('pane.item.duplicates.onlySameItemType');
|
||||
}
|
||||
ZoteroPane.itemPane.setItemPaneMessage(msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
itemTypeID = item.itemTypeID;
|
||||
}
|
||||
}
|
||||
|
||||
this._items = items;
|
||||
|
||||
this._items.sort(function (a, b) {
|
||||
return a.dateAdded > b.dateAdded ? 1 : a.dateAdded == b.dateAdded ? 0 : -1;
|
||||
});
|
||||
|
||||
//
|
||||
// Update the UI
|
||||
//
|
||||
|
||||
let button = document.getElementById('zotero-duplicates-merge-button');
|
||||
let versionSelect = document.getElementById('zotero-duplicates-merge-version-select');
|
||||
let itembox = document.getElementById('zotero-duplicates-merge-item-box');
|
||||
let fieldSelect = document.getElementById('zotero-duplicates-merge-field-select');
|
||||
|
||||
let alternatives = oldestItem.multiDiff(otherItems, this._ignoreFields);
|
||||
if (alternatives) {
|
||||
// Populate menulist with Date Added values from all items
|
||||
let dateList = document.getElementById('zotero-duplicates-merge-original-date');
|
||||
dateList.innerHTML = '';
|
||||
|
||||
let numRows = 0;
|
||||
for (let item of items) {
|
||||
let date = Zotero.Date.sqlToDate(item.dateAdded, true);
|
||||
dateList.appendItem(date.toLocaleString());
|
||||
numRows++;
|
||||
}
|
||||
|
||||
dateList.setAttribute('rows', numRows);
|
||||
|
||||
// If we set this inline, the selection doesn't take on the first
|
||||
// selection after unhiding versionSelect (when clicking
|
||||
// from a set with no differences) -- tested in Fx5.0.1
|
||||
setTimeout(function () {
|
||||
dateList.selectedIndex = 0;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
button.label = Zotero.getString('pane.item.duplicates.mergeItems', (otherItems.length + 1));
|
||||
versionSelect.hidden = fieldSelect.hidden = !alternatives;
|
||||
itembox.hiddenFields = alternatives ? [] : ['dateAdded', 'dateModified'];
|
||||
|
||||
this.setMaster(0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setMaster(pos) {
|
||||
let itembox = document.getElementById('zotero-duplicates-merge-item-box');
|
||||
itembox.mode = 'fieldmerge';
|
||||
|
||||
this._otherItems = this._items.concat();
|
||||
let item = this._otherItems.splice(pos, 1)[0];
|
||||
|
||||
// Add master item's values to the beginning of each set of
|
||||
// alternative values so that they're still available if the item box
|
||||
// modifies the item
|
||||
let alternatives = item.multiDiff(this._otherItems, this._ignoreFields);
|
||||
if (alternatives) {
|
||||
let itemValues = item.toJSON();
|
||||
for (let i in alternatives) {
|
||||
alternatives[i].unshift(itemValues[i] !== undefined ? itemValues[i] : '');
|
||||
}
|
||||
itembox.fieldAlternatives = alternatives;
|
||||
}
|
||||
|
||||
this._masterItem = item;
|
||||
itembox.item = item.clone();
|
||||
// The item.id is null which equals to _lastRenderItemID, so we need to force render it
|
||||
itembox.render(true);
|
||||
}
|
||||
|
||||
async merge() {
|
||||
let itembox = document.getElementById('zotero-duplicates-merge-item-box');
|
||||
Zotero.CollectionTreeCache.clear();
|
||||
// Update master item with any field alternatives from the item box
|
||||
let json = this._masterItem.toJSON();
|
||||
// Exclude certain properties that are empty in the cloned object, so we don't clobber them
|
||||
const { relations: _r, collections: _c, tags: _t, ...keep } = itembox.item.toJSON();
|
||||
Object.assign(json, keep);
|
||||
|
||||
this._masterItem.fromJSON(json);
|
||||
Zotero.Items.merge(this._masterItem, this._otherItems);
|
||||
}
|
||||
}
|
||||
customElements.define("duplicates-merge-pane", DuplicatesMergePane);
|
||||
}
|
|
@ -26,7 +26,7 @@
|
|||
"use strict";
|
||||
|
||||
{
|
||||
class ItemBox extends XULElement {
|
||||
class ItemBox extends ItemPaneSectionElementBase {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
@ -56,8 +56,10 @@
|
|||
this._draggedCreator = false;
|
||||
this._ztabindex = 0;
|
||||
this._selectField = null;
|
||||
}
|
||||
|
||||
this.content = MozXULElement.parseXULToFragment(`
|
||||
get content() {
|
||||
return MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-info" data-pane="info" style="width:100%">
|
||||
<html:div class="body">
|
||||
<div id="item-box" xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
@ -102,12 +104,8 @@
|
|||
`, ['chrome://zotero/locale/zotero.dtd']);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._destroyed = false;
|
||||
window.addEventListener("unload", this.destroy);
|
||||
|
||||
this.appendChild(document.importNode(this.content, true));
|
||||
|
||||
init() {
|
||||
this.initCollapsibleSection();
|
||||
this._creatorTypeMenu.addEventListener('command', async (event) => {
|
||||
var typeBox = document.popupNode;
|
||||
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
||||
|
@ -129,7 +127,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (event) => {
|
||||
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (_event) => {
|
||||
var row = document.popupNode.closest('.meta-row');
|
||||
var typeBox = row.querySelector('.creator-type-label').parentNode;
|
||||
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
||||
|
@ -185,7 +183,6 @@
|
|||
break;
|
||||
}
|
||||
this.moveCreator(index, dir);
|
||||
return;
|
||||
}
|
||||
else if (event.explicitOriginalTarget.id == "creator-transform-switch") {
|
||||
// Switch creator field mode action
|
||||
|
@ -193,7 +190,6 @@
|
|||
var lastName = creatorNameBox.firstChild;
|
||||
let fieldMode = parseInt(lastName.getAttribute("fieldMode"));
|
||||
this.switchCreatorMode(row, fieldMode == 1 ? 0 : 1, false, true, index);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -235,7 +231,7 @@
|
|||
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'itemBox');
|
||||
Zotero.Prefs.registerObserver('fontSize', () => {
|
||||
this.refresh();
|
||||
this.render(true);
|
||||
});
|
||||
|
||||
this.style.setProperty('--comma-character',
|
||||
|
@ -243,24 +239,9 @@
|
|||
}
|
||||
|
||||
destroy() {
|
||||
if (this._destroyed) {
|
||||
return;
|
||||
}
|
||||
window.removeEventListener("unload", this.destroy);
|
||||
this._destroyed = true;
|
||||
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// Empty the DOM. We will rebuild if reconnected.
|
||||
while (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Public properties
|
||||
//
|
||||
|
@ -340,7 +321,6 @@
|
|||
this._item = val;
|
||||
this._lastTabIndex = null;
|
||||
this.scrollToTop();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
// .ref is an alias for .item
|
||||
|
@ -484,18 +464,24 @@
|
|||
if (document.activeElement == this.itemTypeMenu) {
|
||||
this._selectField = "item-type-menu";
|
||||
}
|
||||
this.refresh();
|
||||
this.render(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
render(force = false) {
|
||||
Zotero.debug('Refreshing item box');
|
||||
|
||||
if (!this.item) {
|
||||
Zotero.debug('No item to refresh', 2);
|
||||
return;
|
||||
}
|
||||
if (!this._section.open) return;
|
||||
|
||||
// Always update retraction status
|
||||
this.updateRetracted();
|
||||
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
this.updateRetracted();
|
||||
|
||||
|
@ -672,14 +658,14 @@
|
|||
optionsButton.setAttribute('data-l10n-id', "itembox-button-options");
|
||||
// eslint-disable-next-line no-loop-func
|
||||
let triggerPopup = (e) => {
|
||||
let menupopup = ZoteroItemPane.buildFieldTransformMenu({
|
||||
let menupopup = ZoteroPane.buildFieldTransformMenu({
|
||||
target: valueElement,
|
||||
onTransform: (newValue) => {
|
||||
this._setFieldTransformedValue(valueElement, newValue);
|
||||
}
|
||||
});
|
||||
this.querySelector('popupset').append(menupopup);
|
||||
menupopup.addEventListener('popuphidden', (e) => {
|
||||
menupopup.addEventListener('popuphidden', () => {
|
||||
menupopup.remove();
|
||||
optionsButton.style.visibility = '';
|
||||
});
|
||||
|
@ -722,7 +708,7 @@
|
|||
menuitem.getAttribute('fieldname'),
|
||||
menuitem.getAttribute('originalValue')
|
||||
);
|
||||
this.refresh();
|
||||
this.render(true);
|
||||
});
|
||||
popup.appendChild(menuitem);
|
||||
}
|
||||
|
@ -1155,7 +1141,7 @@
|
|||
// If the row is still hidden, no 'drop' event happened, meaning creator rows
|
||||
// were not reordered. To make sure everything is in correct order, just refresh.
|
||||
if (row.classList.contains("drag-hidden-creator")) {
|
||||
this.refresh();
|
||||
this.render(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1200,12 +1186,12 @@
|
|||
rowData.setAttribute("ztabindex", ++this._ztabindex);
|
||||
rowData.addEventListener('click', () => {
|
||||
this._displayAllCreators = true;
|
||||
this.refresh();
|
||||
this.render(true);
|
||||
});
|
||||
rowData.addEventListener('keypress', (e) => {
|
||||
if (["Enter", ' '].includes(e.key)) {
|
||||
this._displayAllCreators = true;
|
||||
this.refresh();
|
||||
this.render(true);
|
||||
}
|
||||
});
|
||||
rowData.textContent = Zotero.getString('general.numMore', num);
|
||||
|
@ -1421,7 +1407,7 @@
|
|||
await this.item.saveTx();
|
||||
}
|
||||
else {
|
||||
this.refresh();
|
||||
this.render(true);
|
||||
}
|
||||
|
||||
functionsToRun.forEach(f => f.bind(this)());
|
||||
|
@ -1512,7 +1498,7 @@
|
|||
valueElement.setAttribute('tight', true);
|
||||
|
||||
valueElement.addEventListener("focus", e => this.updateLastFocused(e));
|
||||
valueElement.addEventListener("keypress", (e) => this.handleKeyPress(e));
|
||||
valueElement.addEventListener("keypress", e => this.handleKeyPress(e));
|
||||
switch (fieldName) {
|
||||
case 'itemType':
|
||||
valueElement.setAttribute('itemTypeID', valueText);
|
||||
|
|
575
chrome/content/zotero/elements/itemDetails.js
Normal file
575
chrome/content/zotero/elements/itemDetails.js
Normal file
|
@ -0,0 +1,575 @@
|
|||
/*
|
||||
***** 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 *****
|
||||
*/
|
||||
|
||||
{
|
||||
const AsyncFunction = (async () => {}).constructor;
|
||||
|
||||
const waitFrame = async () => {
|
||||
return waitNoLongerThan(new Promise((resolve) => {
|
||||
requestAnimationFrame(resolve);
|
||||
}), 30);
|
||||
};
|
||||
|
||||
const waitFrames = async (n) => {
|
||||
for (let i = 0; i < n; i++) {
|
||||
await waitFrame();
|
||||
}
|
||||
};
|
||||
|
||||
const waitNoLongerThan = async (promise, ms = 1000) => {
|
||||
return Promise.race([
|
||||
promise,
|
||||
Zotero.Promise.delay(ms)
|
||||
]);
|
||||
};
|
||||
|
||||
class ItemDetails extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<hbox id="zotero-view-item-container" class="zotero-view-item-container" flex="1">
|
||||
<html:div class="zotero-view-item-main">
|
||||
<pane-header id="zotero-item-pane-header" />
|
||||
|
||||
<html:div id="zotero-view-item" class="zotero-view-item" tabindex="0">
|
||||
<item-box id="zotero-editpane-item-box" data-pane="info"/>
|
||||
|
||||
<abstract-box id="zotero-editpane-abstract" class="zotero-editpane-abstract" data-pane="abstract"/>
|
||||
|
||||
<attachments-box id="zotero-editpane-attachments" data-pane="attachments"/>
|
||||
|
||||
<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-annotations-box id="zotero-editpane-attachment-annotations" flex="1" data-pane="attachment-annotations" hidden="true"/>
|
||||
|
||||
<libraries-collections-box id="zotero-editpane-libraries-collections" class="zotero-editpane-libraries-collections" data-pane="libraries-collections"/>
|
||||
|
||||
<tags-box id="zotero-editpane-tags" class="zotero-editpane-tags" data-pane="tags"/>
|
||||
|
||||
<related-box id="zotero-editpane-related" class="zotero-editpane-related" data-pane="related"/>
|
||||
</html:div>
|
||||
</html:div>
|
||||
</hbox>
|
||||
`);
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(item) {
|
||||
this._item = item;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
set mode(mode) {
|
||||
this._mode = mode;
|
||||
}
|
||||
|
||||
get pinnedPane() {
|
||||
return this.getAttribute('pinnedPane');
|
||||
}
|
||||
|
||||
set pinnedPane(val) {
|
||||
if (!val || !this.getPane(val)) {
|
||||
val = '';
|
||||
}
|
||||
this.setAttribute('pinnedPane', val);
|
||||
if (val) {
|
||||
this._pinnedPaneMinScrollHeight = this._getMinScrollHeightForPane(this.getPane(val));
|
||||
}
|
||||
this.sidenav.updatePaneStatus(val);
|
||||
}
|
||||
|
||||
get _minScrollHeight() {
|
||||
return parseFloat(this._paneParent.style.getPropertyValue('--min-scroll-height') || 0);
|
||||
}
|
||||
|
||||
set _minScrollHeight(val) {
|
||||
this._paneParent.style.setProperty('--min-scroll-height', val + 'px');
|
||||
}
|
||||
|
||||
get _collapsed() {
|
||||
let collapsible = this.closest('splitter:not([hidden="true"]) + *');
|
||||
if (!collapsible) return false;
|
||||
return collapsible.getAttribute('collapsed') === 'true';
|
||||
}
|
||||
|
||||
set _collapsed(val) {
|
||||
let collapsible = this.closest('splitter:not([hidden="true"]) + *');
|
||||
if (!collapsible) return;
|
||||
let splitter = collapsible.previousElementSibling;
|
||||
if (val) {
|
||||
collapsible.setAttribute('collapsed', 'true');
|
||||
collapsible.removeAttribute("width");
|
||||
collapsible.removeAttribute("height");
|
||||
splitter.setAttribute('state', 'collapsed');
|
||||
splitter.setAttribute('substate', 'after');
|
||||
}
|
||||
else {
|
||||
collapsible.removeAttribute('collapsed');
|
||||
splitter.setAttribute('state', '');
|
||||
splitter.setAttribute('substate', 'after');
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
get sidenav() {
|
||||
return this._sidenav;
|
||||
}
|
||||
|
||||
set sidenav(sidenav) {
|
||||
this._sidenav = sidenav;
|
||||
sidenav.container = this;
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['pinnedPane'];
|
||||
}
|
||||
|
||||
init() {
|
||||
this._container = this.querySelector('#zotero-view-item-container');
|
||||
this._header = this.querySelector('#zotero-item-pane-header');
|
||||
this._paneParent = this.querySelector('#zotero-view-item');
|
||||
|
||||
this._container.addEventListener("keypress", this._handleKeypress);
|
||||
this._paneParent.addEventListener('scroll', this._handleContainerScroll);
|
||||
|
||||
this._paneHiddenOb = new MutationObserver(this._handlePaneStatus);
|
||||
this._paneHiddenOb.observe(this._paneParent, {
|
||||
attributes: true,
|
||||
attributeFilter: ["hidden"],
|
||||
subtree: true,
|
||||
});
|
||||
this._initIntersectionObserver();
|
||||
|
||||
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'ItemDetails');
|
||||
|
||||
this._disableScrollHandler = false;
|
||||
this._pinnedPaneMinScrollHeight = 0;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._container.removeEventListener("keypress", this._handleKeypress);
|
||||
this._paneParent.removeEventListener('scroll', this._handleContainerScroll);
|
||||
|
||||
this._paneHiddenOb.disconnect();
|
||||
this._intersectionOb.disconnect();
|
||||
|
||||
Zotero.Notifier.unregisterObserver(this._unregisterID);
|
||||
}
|
||||
|
||||
async render() {
|
||||
if (!this.initialized || !this.item) {
|
||||
return;
|
||||
}
|
||||
let item = this.item;
|
||||
Zotero.debug('Viewing item');
|
||||
this._isRendering = true;
|
||||
|
||||
let panes = this.getPanes();
|
||||
let pendingBoxes = [];
|
||||
let inTrash = ZoteroPane.collectionsView.selectedTreeRow && ZoteroPane.collectionsView.selectedTreeRow.isTrash();
|
||||
let tabType = Zotero_Tabs.selectedType;
|
||||
for (let box of [this._header, ...panes]) {
|
||||
if (!box.showInFeeds && item.isFeedItem) {
|
||||
box.style.display = 'none';
|
||||
box.hidden = true;
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
box.style.display = '';
|
||||
box.hidden = false;
|
||||
}
|
||||
|
||||
if (this.mode) {
|
||||
box.mode = this.mode;
|
||||
|
||||
if (box.mode == 'view') {
|
||||
box.hideEmptyFields = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
box.mode = 'edit';
|
||||
}
|
||||
|
||||
box.item = item;
|
||||
box.inTrash = inTrash;
|
||||
box.tabType = tabType;
|
||||
// Render sync boxes immediately
|
||||
if (!box.hidden && box.render) {
|
||||
if (box.render instanceof AsyncFunction) {
|
||||
pendingBoxes.push(box);
|
||||
}
|
||||
else {
|
||||
box.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pinnedPaneElem = this.getPane(this.pinnedPane);
|
||||
let pinnedIndex = panes.indexOf(pinnedPaneElem);
|
||||
|
||||
this._paneParent.style.paddingBottom = '';
|
||||
if (pinnedPaneElem) {
|
||||
let paneID = pinnedPaneElem.dataset.pane;
|
||||
this.scrollToPane(paneID, 'instant');
|
||||
this.pinnedPane = paneID;
|
||||
}
|
||||
else {
|
||||
this._paneParent.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
// Only render visible panes
|
||||
for (let box of pendingBoxes) {
|
||||
if (pinnedIndex > -1 && panes.indexOf(box) < pinnedIndex) {
|
||||
continue;
|
||||
}
|
||||
if (!this.isPaneVisible(box.dataset.pane)) {
|
||||
continue;
|
||||
}
|
||||
await waitNoLongerThan(box.render(), 500);
|
||||
}
|
||||
// After all panes finish first rendering, try secondary rendering
|
||||
for (let box of panes) {
|
||||
if (!box.secondaryRender) {
|
||||
continue;
|
||||
}
|
||||
if (pinnedIndex > -1 && panes.indexOf(box) < pinnedIndex) {
|
||||
continue;
|
||||
}
|
||||
if (this.isPaneVisible(box.dataset.pane)) {
|
||||
continue;
|
||||
}
|
||||
await waitNoLongerThan(box.secondaryRender(), 500);
|
||||
}
|
||||
if (this.item.id == item.id) {
|
||||
this._isRendering = false;
|
||||
}
|
||||
}
|
||||
|
||||
renderCustomSections() {
|
||||
let lastUpdate = Zotero.ItemPaneManager.getUpdateTime();
|
||||
if (this._lastUpdateCustomSection == lastUpdate) return;
|
||||
this._lastUpdateCustomSection = lastUpdate;
|
||||
|
||||
let targetPanes = Zotero.ItemPaneManager.getCustomSections();
|
||||
let currentPaneElements = this.getCustomPanes();
|
||||
// Remove
|
||||
for (let elem of currentPaneElements) {
|
||||
let elemPaneID = elem.dataset.pane;
|
||||
if (targetPanes.find(pane => pane.paneID == elemPaneID)) continue;
|
||||
this._intersectionOb.unobserve(elem);
|
||||
elem.remove();
|
||||
this.sidenav.removePane(elemPaneID);
|
||||
}
|
||||
// Create
|
||||
let currentPaneIDs = currentPaneElements.map(elem => elem.dataset.pane);
|
||||
for (let section of targetPanes) {
|
||||
let { paneID, head, sidenav, fragment,
|
||||
onInit, onDestroy, onDataChange, onRender, onSecondaryRender, onToggle,
|
||||
sectionButtons } = section;
|
||||
if (currentPaneIDs.includes(paneID)) continue;
|
||||
let elem = new (customElements.get("item-pane-custom-section"));
|
||||
elem.dataset.sidenavOptions = JSON.stringify(sidenav || {});
|
||||
elem.paneID = paneID;
|
||||
elem.fragment = fragment;
|
||||
elem.registerSectionIcon({ icon: head.icon, darkIcon: head.darkIcon });
|
||||
elem.registerHook({ type: "init", callback: onInit });
|
||||
elem.registerHook({ type: "destroy", callback: onDestroy });
|
||||
elem.registerHook({ type: "dataChange", callback: onDataChange });
|
||||
elem.registerHook({ type: "render", callback: onRender });
|
||||
elem.registerHook({ type: "secondaryRender", callback: onSecondaryRender });
|
||||
elem.registerHook({ type: "toggle", callback: onToggle });
|
||||
if (sectionButtons) {
|
||||
for (let buttonOptions of sectionButtons) {
|
||||
elem.registerSectionButton(buttonOptions);
|
||||
}
|
||||
}
|
||||
this._paneParent.append(elem);
|
||||
elem.setL10nID(head.l10nID);
|
||||
elem.setL10nArgs(head.l10nArgs);
|
||||
this._intersectionOb.observe(elem);
|
||||
this.sidenav.addPane(paneID);
|
||||
}
|
||||
}
|
||||
|
||||
renderCustomHead(callback) {
|
||||
this._header.renderCustomHead(callback);
|
||||
}
|
||||
|
||||
notify = async (action, _type, _ids, _extraData) => {
|
||||
if (action == 'refresh' && this.item) {
|
||||
await this.render();
|
||||
}
|
||||
};
|
||||
|
||||
getPane(id) {
|
||||
return this._paneParent.querySelector(`:scope > [data-pane="${CSS.escape(id)}"]:not([hidden])`);
|
||||
}
|
||||
|
||||
getPanes() {
|
||||
return Array.from(this._paneParent.querySelectorAll(':scope > [data-pane]'));
|
||||
}
|
||||
|
||||
getEnabledPanes() {
|
||||
return Array.from(this._paneParent.querySelectorAll(':scope > [data-pane]:not([hidden])'));
|
||||
}
|
||||
|
||||
getVisiblePanes() {
|
||||
let panes = this.getPanes();
|
||||
let visiblePanes = [];
|
||||
for (let paneElem of panes) {
|
||||
if (this.isPaneVisible(paneElem.dataset.pane)) {
|
||||
visiblePanes.push(paneElem);
|
||||
}
|
||||
else if (visiblePanes.length > 0) {
|
||||
// Early stop at first invisible pane after some visible panes
|
||||
break;
|
||||
}
|
||||
}
|
||||
return visiblePanes;
|
||||
}
|
||||
|
||||
isPaneVisible(paneID) {
|
||||
let paneElem = this.getPane(paneID);
|
||||
if (!paneElem) return false;
|
||||
let paneRect = paneElem.getBoundingClientRect();
|
||||
let containerRect = this._paneParent.getBoundingClientRect();
|
||||
if (paneRect.top >= containerRect.bottom || paneRect.bottom <= containerRect.top) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async scrollToPane(paneID, behavior = 'smooth') {
|
||||
let pane = this.getPane(paneID);
|
||||
if (!pane) return null;
|
||||
|
||||
let scrollPromise;
|
||||
|
||||
// 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")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The pane should always be at the very top
|
||||
// If there isn't enough stuff below it for it to be at the top, we add padding
|
||||
// We use a ::before pseudo-element for this so that we don't need to add another level to the DOM
|
||||
this._makeSpaceForPane(pane);
|
||||
if (behavior == 'smooth') {
|
||||
this._disableScrollHandler = true;
|
||||
scrollPromise = this._waitForScroll();
|
||||
scrollPromise.then(() => this._disableScrollHandler = false);
|
||||
}
|
||||
pane.scrollIntoView({ block: 'start', behavior });
|
||||
pane.focus();
|
||||
return scrollPromise;
|
||||
}
|
||||
|
||||
_makeSpaceForPane(pane) {
|
||||
let oldMinScrollHeight = this._minScrollHeight;
|
||||
let newMinScrollHeight = this._getMinScrollHeightForPane(pane);
|
||||
if (newMinScrollHeight > oldMinScrollHeight) {
|
||||
this._minScrollHeight = newMinScrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
_getMinScrollHeightForPane(pane) {
|
||||
let paneRect = pane.getBoundingClientRect();
|
||||
let containerRect = this._paneParent.getBoundingClientRect();
|
||||
// No offsetTop property for XUL elements
|
||||
let offsetTop = paneRect.top - containerRect.top + this._paneParent.scrollTop;
|
||||
return offsetTop + containerRect.height;
|
||||
}
|
||||
|
||||
async _waitForScroll() {
|
||||
let scrollPromise = Zotero.Promise.defer();
|
||||
let lastScrollTop = this._paneParent.scrollTop;
|
||||
const checkScrollStart = () => {
|
||||
// If the scrollTop is not changed, wait for scroll to happen
|
||||
if (lastScrollTop === this._paneParent.scrollTop) {
|
||||
requestAnimationFrame(checkScrollStart);
|
||||
}
|
||||
// Wait for scroll to end
|
||||
else {
|
||||
requestAnimationFrame(checkScrollEnd);
|
||||
}
|
||||
};
|
||||
const checkScrollEnd = async () => {
|
||||
// Wait for 3 frames to make sure not further scrolls
|
||||
await waitFrames(3);
|
||||
if (lastScrollTop === this._paneParent.scrollTop) {
|
||||
scrollPromise.resolve();
|
||||
}
|
||||
else {
|
||||
lastScrollTop = this._paneParent.scrollTop;
|
||||
requestAnimationFrame(checkScrollEnd);
|
||||
}
|
||||
};
|
||||
checkScrollStart();
|
||||
// Abort after 3 seconds, which should be enough
|
||||
return Promise.race([
|
||||
scrollPromise.promise,
|
||||
Zotero.Promise.delay(3000)
|
||||
]);
|
||||
}
|
||||
|
||||
async blurOpenField() {
|
||||
let panes = [this._header, ...this.getPanes()];
|
||||
for (let pane of panes) {
|
||||
if (pane.blurOpenField && pane.contains(document.activeElement)) {
|
||||
await pane.blurOpenField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._paneParent.focus();
|
||||
}
|
||||
|
||||
_initIntersectionObserver() {
|
||||
if (this._intersectionOb) {
|
||||
this._intersectionOb.disconnect();
|
||||
}
|
||||
this._intersectionOb = new IntersectionObserver(this._handleIntersection);
|
||||
this.getPanes().forEach(elem => this._intersectionOb.observe(elem));
|
||||
}
|
||||
|
||||
_handleContainerScroll = () => {
|
||||
// Don't scroll hidden pane
|
||||
if (this.hidden || this._disableScrollHandler) return;
|
||||
|
||||
let minHeight = this._minScrollHeight;
|
||||
if (minHeight) {
|
||||
let newMinScrollHeight = this._paneParent.scrollTop + this._paneParent.clientHeight;
|
||||
// Ignore overscroll (which generates scroll events on Windows 11, unlike on macOS)
|
||||
// and don't shrink below the pinned pane's min scroll height
|
||||
if (newMinScrollHeight > this._paneParent.scrollHeight
|
||||
|| this.getPane(this.pinnedPane) && newMinScrollHeight < this._pinnedPaneMinScrollHeight) {
|
||||
return;
|
||||
}
|
||||
this._minScrollHeight = newMinScrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
// Keyboard navigation within the itemPane. Also handles contextPane keyboard nav
|
||||
_handleKeypress = (event) => {
|
||||
let stopEvent = () => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
let isLibraryTab = Zotero_Tabs.selectedIndex == 0;
|
||||
let sidenav = document.getElementById(
|
||||
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);
|
||||
pane.firstChild._head.focus();
|
||||
stopEvent();
|
||||
return;
|
||||
}
|
||||
// Tab tavigation between entries and buttons within library, related and notes boxes
|
||||
if (event.key == "Tab" && event.target.closest(".box")) {
|
||||
let next = null;
|
||||
if (event.key == "Tab" && !event.shiftKey) {
|
||||
next = event.target.nextElementSibling;
|
||||
}
|
||||
if (event.key == "Tab" && event.shiftKey) {
|
||||
next = event.target.parentNode.previousElementSibling?.lastChild;
|
||||
}
|
||||
// Force the element to be visible before focusing
|
||||
if (next) {
|
||||
next.style.visibility = "visible";
|
||||
next.focus();
|
||||
next.style.removeProperty("visibility");
|
||||
stopEvent();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_handlePaneStatus = (muts) => {
|
||||
for (let mut of muts) {
|
||||
let paneID = mut.target.dataset.pane;
|
||||
if (paneID) {
|
||||
this.sidenav.updatePaneStatus(paneID);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_handleIntersection = async (entries) => {
|
||||
if (this._isRendering) return;
|
||||
let needsRefresh = [];
|
||||
let needsDiscard = [];
|
||||
entries.forEach((entry) => {
|
||||
let targetPaneElem = entry.target;
|
||||
if (entry.isIntersecting && targetPaneElem.render) {
|
||||
needsRefresh.push(targetPaneElem);
|
||||
}
|
||||
else if (targetPaneElem.discard) {
|
||||
needsDiscard.push(targetPaneElem);
|
||||
}
|
||||
});
|
||||
let needsCheckVisibility = false;
|
||||
// Sidenav is in smooth scrolling mode
|
||||
if (this._disableScrollHandler) {
|
||||
// Wait for scroll to finish
|
||||
await this._waitForScroll();
|
||||
needsCheckVisibility = true;
|
||||
}
|
||||
if (needsRefresh.length > 0) {
|
||||
needsRefresh.forEach(async (paneElem) => {
|
||||
if (needsCheckVisibility && !this.isPaneVisible(paneElem.dataset.pane)) {
|
||||
return;
|
||||
}
|
||||
await paneElem.render();
|
||||
if (paneElem.secondaryRender) await paneElem.secondaryRender();
|
||||
});
|
||||
}
|
||||
if (needsDiscard.length > 0) {
|
||||
needsDiscard.forEach((paneElem) => {
|
||||
if (needsCheckVisibility && this.isPaneVisible(paneElem.dataset.pane)) {
|
||||
return;
|
||||
}
|
||||
paneElem.discard();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
customElements.define("item-details", ItemDetails);
|
||||
}
|
70
chrome/content/zotero/elements/itemMessagePane.js
Normal file
70
chrome/content/zotero/elements/itemMessagePane.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
***** 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 ItemMessagePane extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<html:div class="custom-head empty"></html:div>
|
||||
<groupbox id="zotero-item-pane-groupbox" pack="center" align="center">
|
||||
<vbox id="zotero-item-pane-message-box"/>
|
||||
</groupbox>
|
||||
`);
|
||||
|
||||
init() {
|
||||
this._messageBox = this.querySelector('#zotero-item-pane-message-box');
|
||||
}
|
||||
|
||||
render(content) {
|
||||
this._messageBox.textContent = '';
|
||||
if (typeof content == 'string') {
|
||||
let contentParts = content.split("\n\n");
|
||||
for (let part of contentParts) {
|
||||
let desc = document.createXULElement('description');
|
||||
desc.appendChild(document.createTextNode(part));
|
||||
this._messageBox.appendChild(desc);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this._messageBox.appendChild(content);
|
||||
}
|
||||
}
|
||||
|
||||
renderCustomHead(callback) {
|
||||
let customHead = this.querySelector(".custom-head");
|
||||
customHead.replaceChildren();
|
||||
let append = (...args) => {
|
||||
customHead.append(...args);
|
||||
};
|
||||
if (callback) callback({
|
||||
doc: document,
|
||||
append: (...args) => {
|
||||
append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("item-message-pane", ItemMessagePane);
|
||||
}
|
502
chrome/content/zotero/elements/itemPane.js
Normal file
502
chrome/content/zotero/elements/itemPane.js
Normal file
|
@ -0,0 +1,502 @@
|
|||
/*
|
||||
***** 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 ItemPane extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<deck id="zotero-item-pane-content" class="zotero-item-pane-content" selectedIndex="0" flex="1" zotero-persist="width height" height="300">
|
||||
<item-message-pane id="zotero-item-message" />
|
||||
|
||||
<item-details id="zotero-item-details" />
|
||||
|
||||
<note-editor id="zotero-note-editor" flex="1" notitle="1"
|
||||
previousfocus="zotero-items-tree" />
|
||||
|
||||
<duplicates-merge-pane id="zotero-duplicates-merge-pane" />
|
||||
</deck>
|
||||
<item-pane-sidenav id="zotero-view-item-sidenav" class="zotero-view-item-sidenav"/>
|
||||
`);
|
||||
|
||||
init() {
|
||||
this._itemDetails = this.querySelector("#zotero-item-details");
|
||||
this._noteEditor = this.querySelector("#zotero-note-editor");
|
||||
this._duplicatesPane = this.querySelector("#zotero-duplicates-merge-pane");
|
||||
this._messagePane = this.querySelector("#zotero-item-message");
|
||||
this._sidenav = this.querySelector("#zotero-view-item-sidenav");
|
||||
this._deck = this.querySelector("#zotero-item-pane-content");
|
||||
|
||||
this._itemDetails.sidenav = this._sidenav;
|
||||
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item']);
|
||||
|
||||
this._translationTarget = null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
set data(data) {
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
get viewMode() {
|
||||
return this._viewMode;
|
||||
}
|
||||
|
||||
set viewMode(mode) {
|
||||
this._viewMode = mode;
|
||||
}
|
||||
|
||||
get editable() {
|
||||
return this._editable;
|
||||
}
|
||||
|
||||
set editable(editable) {
|
||||
this._editable = editable;
|
||||
}
|
||||
|
||||
get viewType() {
|
||||
return ["message", "item", "note", "duplicates"][this._deck.selectedIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set view type
|
||||
* @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");
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.data) return false;
|
||||
let hideSidenav = false;
|
||||
let renderStatus = false;
|
||||
// Single item selected
|
||||
if (this.data.length == 1) {
|
||||
let item = this.data[0];
|
||||
|
||||
if (item.isNote()) {
|
||||
hideSidenav = true;
|
||||
renderStatus = this.renderNoteEditor(item);
|
||||
}
|
||||
else {
|
||||
renderStatus = this.renderItemPane(item);
|
||||
}
|
||||
}
|
||||
// Zero or multiple items selected
|
||||
else {
|
||||
renderStatus = this.renderMessage();
|
||||
}
|
||||
this._sidenav.hidden = hideSidenav;
|
||||
return renderStatus;
|
||||
}
|
||||
|
||||
notify(action, type) {
|
||||
if (type == 'item' && action == 'modify') {
|
||||
if (this.viewMode.isFeedsOrFeed) {
|
||||
this.updateReadLabel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderNoteEditor(item) {
|
||||
this.viewType = "note";
|
||||
|
||||
let noteEditor = document.getElementById('zotero-note-editor');
|
||||
noteEditor.mode = this.editable ? 'edit' : 'view';
|
||||
noteEditor.viewMode = 'library';
|
||||
noteEditor.parent = null;
|
||||
noteEditor.item = item;
|
||||
return true;
|
||||
}
|
||||
|
||||
renderItemPane(item) {
|
||||
this.viewType = "item";
|
||||
|
||||
this._itemDetails.mode = this.editable ? null : "view";
|
||||
this._itemDetails.item = item;
|
||||
this._itemDetails.render();
|
||||
|
||||
if (item.isFeedItem) {
|
||||
let lastTranslationTarget = Zotero.Prefs.get('feeds.lastTranslationTarget');
|
||||
if (lastTranslationTarget) {
|
||||
let id = parseInt(lastTranslationTarget.substr(1));
|
||||
if (lastTranslationTarget[0] == "L") {
|
||||
this._translationTarget = Zotero.Libraries.get(id);
|
||||
}
|
||||
else if (lastTranslationTarget[0] == "C") {
|
||||
this._translationTarget = Zotero.Collections.get(id);
|
||||
}
|
||||
}
|
||||
if (!this._translationTarget) {
|
||||
this._translationTarget = Zotero.Libraries.userLibrary;
|
||||
}
|
||||
this.setTranslateButton();
|
||||
// Too slow for now
|
||||
// if (!item.isTranslated) {
|
||||
// item.translate();
|
||||
// }
|
||||
ZoteroPane.startItemReadTimeout(item.id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
let msg;
|
||||
|
||||
let count = this.data.length;
|
||||
|
||||
// Display duplicates merge interface in item pane
|
||||
if (this.viewMode.isDuplicates) {
|
||||
if (!this.editable) {
|
||||
if (count) {
|
||||
msg = Zotero.getString('pane.item.duplicates.writeAccessRequired');
|
||||
}
|
||||
else {
|
||||
msg = Zotero.getString('pane.item.selected.zero');
|
||||
}
|
||||
this.setItemPaneMessage(msg);
|
||||
}
|
||||
else if (count) {
|
||||
this.viewType = "duplicates";
|
||||
|
||||
// On a Select All of more than a few items, display a row
|
||||
// count instead of the usual item type mismatch error
|
||||
let displayNumItemsOnTypeError = count > 5 && count == this.viewMode.rowCount;
|
||||
|
||||
// Initialize the merge pane with the selected items
|
||||
this._duplicatesPane.setItems(this.data, displayNumItemsOnTypeError);
|
||||
}
|
||||
else {
|
||||
msg = Zotero.getString('pane.item.duplicates.selectToMerge');
|
||||
this.setItemPaneMessage(msg);
|
||||
}
|
||||
}
|
||||
// Display label in the middle of the item pane
|
||||
else {
|
||||
if (count) {
|
||||
msg = Zotero.getString('pane.item.selected.multiple', count);
|
||||
}
|
||||
else {
|
||||
let rowCount = this.viewMode.rowCount;
|
||||
let str = 'pane.item.unselected.';
|
||||
switch (rowCount) {
|
||||
case 0:
|
||||
str += 'zero';
|
||||
break;
|
||||
case 1:
|
||||
str += 'singular';
|
||||
break;
|
||||
default:
|
||||
str += 'plural';
|
||||
break;
|
||||
}
|
||||
msg = Zotero.getString(str, [rowCount]);
|
||||
}
|
||||
|
||||
this.setItemPaneMessage(msg);
|
||||
// Return false for itemTreeTest#shouldn't select a modified item
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
setItemPaneMessage(msg) {
|
||||
this.viewType = "message";
|
||||
this._messagePane.render(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display buttons at top of item pane depending on context
|
||||
*/
|
||||
updateItemPaneButtons() {
|
||||
let container;
|
||||
if (!this.data.length) {
|
||||
return;
|
||||
}
|
||||
else if (this.data.length > 1) {
|
||||
container = this._messagePane;
|
||||
}
|
||||
else if (this.data[0].isNote()) {
|
||||
container = this._noteEditor;
|
||||
}
|
||||
else {
|
||||
container = this._itemDetails;
|
||||
}
|
||||
|
||||
// My Publications buttons
|
||||
var isPublications = this.viewMode.isPublications;
|
||||
// Show in My Publications view if selected items are all notes or non-linked-file attachments
|
||||
var showMyPublicationsButtons = isPublications
|
||||
&& this.data.every((item) => {
|
||||
return item.isNote()
|
||||
|| (item.isAttachment()
|
||||
&& item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE);
|
||||
});
|
||||
|
||||
if (showMyPublicationsButtons) {
|
||||
container.renderCustomHead(this.renderPublicationsHead.bind(this));
|
||||
return;
|
||||
}
|
||||
|
||||
// Trash button
|
||||
let nonDeletedItemsSelected = this.data.some(item => !item.deleted);
|
||||
if (this.viewMode.isTrash && !nonDeletedItemsSelected) {
|
||||
container.renderCustomHead(this.renderTrashHead.bind(this));
|
||||
return;
|
||||
}
|
||||
|
||||
// Feed buttons
|
||||
if (this.viewMode.isFeedsOrFeed) {
|
||||
container.renderCustomHead(this.renderFeedHead.bind(this));
|
||||
this.updateReadLabel();
|
||||
return;
|
||||
}
|
||||
|
||||
container.renderCustomHead();
|
||||
}
|
||||
|
||||
renderPublicationsHead(data) {
|
||||
let { doc, append } = data;
|
||||
let button = doc.createXULElement("button");
|
||||
button.id = 'zotero-item-pane-my-publications-button';
|
||||
|
||||
let hiddenItemsSelected = this.data.some(item => !item.inPublications);
|
||||
let str, onclick;
|
||||
if (hiddenItemsSelected) {
|
||||
str = 'showInMyPublications';
|
||||
onclick = () => Zotero.Items.addToPublications(this.data);
|
||||
}
|
||||
else {
|
||||
str = 'hideFromMyPublications';
|
||||
onclick = () => Zotero.Items.removeFromPublications(this.data);
|
||||
}
|
||||
button.label = Zotero.getString('pane.item.' + str);
|
||||
button.onclick = onclick;
|
||||
append(button);
|
||||
}
|
||||
|
||||
renderTrashHead(data) {
|
||||
let { doc, append } = data;
|
||||
let restoreButton = doc.createXULElement("button");
|
||||
restoreButton.id = "zotero-item-restore-button";
|
||||
restoreButton.dataset.l10nId = "menu-restoreToLibrary";
|
||||
restoreButton.addEventListener("command", () => {
|
||||
ZoteroPane.restoreSelectedItems();
|
||||
});
|
||||
|
||||
let deleteButton = doc.createXULElement("button");
|
||||
deleteButton.id = "zotero-item-delete-button";
|
||||
deleteButton.dataset.l10nId = "menu-deletePermanently";
|
||||
deleteButton.addEventListener("command", () => {
|
||||
ZoteroPane.deleteSelectedItems();
|
||||
});
|
||||
|
||||
append(restoreButton, deleteButton);
|
||||
}
|
||||
|
||||
renderFeedHead(data) {
|
||||
let { doc, append } = data;
|
||||
|
||||
let toggleReadButton = doc.createXULElement("button");
|
||||
toggleReadButton.id = "zotero-feed-item-toggleRead-button";
|
||||
toggleReadButton.addEventListener("command", () => {
|
||||
ZoteroPane.toggleSelectedItemsRead();
|
||||
});
|
||||
|
||||
let addToButton = new (customElements.get('split-menu-button'));
|
||||
addToButton.id = "zotero-feed-item-addTo-button";
|
||||
addToButton.setAttribute("popup", "zotero-item-addTo-menu");
|
||||
addToButton.addEventListener("command", () => this.translateSelectedItems());
|
||||
|
||||
append(toggleReadButton, addToButton);
|
||||
|
||||
this.setTranslateButton();
|
||||
}
|
||||
|
||||
updateReadLabel() {
|
||||
var items = this.data;
|
||||
var isUnread = false;
|
||||
for (let item of items) {
|
||||
if (!item.isRead) {
|
||||
isUnread = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setReadLabel(!isUnread);
|
||||
}
|
||||
|
||||
setReadLabel(isRead) {
|
||||
var elem = document.getElementById('zotero-feed-item-toggleRead-button');
|
||||
var label = Zotero.getString('pane.item.' + (isRead ? 'markAsUnread' : 'markAsRead'));
|
||||
elem.label = label;
|
||||
|
||||
var key = Zotero.Keys.getKeyForCommand('toggleRead');
|
||||
var tooltip = label + (Zotero.rtl ? ' \u202B' : ' ') + '(' + key + ')';
|
||||
elem.title = tooltip;
|
||||
}
|
||||
|
||||
async translateSelectedItems() {
|
||||
var collectionID = this._translationTarget.objectType == 'collection' ? this._translationTarget.id : undefined;
|
||||
var items = this.data;
|
||||
for (let item of items) {
|
||||
await item.translate(this._translationTarget.libraryID, collectionID);
|
||||
}
|
||||
}
|
||||
|
||||
buildTranslateSelectContextMenu(event) {
|
||||
var menu = document.getElementById('zotero-item-addTo-menu');
|
||||
// Don't trigger rebuilding on nested popupmenu open/close
|
||||
if (event.target != menu) {
|
||||
return;
|
||||
}
|
||||
// Clear previous items
|
||||
while (menu.firstChild) {
|
||||
menu.removeChild(menu.firstChild);
|
||||
}
|
||||
|
||||
let target = Zotero.Prefs.get('feeds.lastTranslationTarget');
|
||||
if (!target) {
|
||||
target = "L" + Zotero.Libraries.userLibraryID;
|
||||
}
|
||||
|
||||
var libraries = Zotero.Libraries.getAll();
|
||||
for (let library of libraries) {
|
||||
if (!library.editable || library.libraryType == 'publications') {
|
||||
continue;
|
||||
}
|
||||
Zotero.Utilities.Internal.createMenuForTarget(
|
||||
library,
|
||||
menu,
|
||||
target,
|
||||
async (event, libraryOrCollection) => {
|
||||
if (event.target.tagName == 'menu') {
|
||||
// Simulate menuitem flash on OS X
|
||||
if (Zotero.isMac) {
|
||||
event.target.setAttribute('_moz-menuactive', false);
|
||||
await Zotero.Promise.delay(50);
|
||||
event.target.setAttribute('_moz-menuactive', true);
|
||||
await Zotero.Promise.delay(50);
|
||||
event.target.setAttribute('_moz-menuactive', false);
|
||||
await Zotero.Promise.delay(50);
|
||||
event.target.setAttribute('_moz-menuactive', true);
|
||||
}
|
||||
menu.hidePopup();
|
||||
|
||||
this.setTranslationTarget(libraryOrCollection);
|
||||
event.stopPropagation();
|
||||
}
|
||||
else {
|
||||
this.setTranslationTarget(libraryOrCollection);
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setTranslateButton() {
|
||||
if (!this._translationTarget) return;
|
||||
var label = Zotero.getString('pane.item.addTo', this._translationTarget.name);
|
||||
var elem = document.getElementById('zotero-feed-item-addTo-button');
|
||||
elem.label = label;
|
||||
|
||||
var key = Zotero.Keys.getKeyForCommand('saveToZotero');
|
||||
|
||||
var tooltip = label
|
||||
+ (Zotero.rtl ? ' \u202B' : ' ') + '('
|
||||
+ (Zotero.isMac ? '⇧⌘' : Zotero.getString('general.keys.ctrlShift'))
|
||||
+ key + ')';
|
||||
elem.title = tooltip;
|
||||
elem.image = this._translationTarget.treeViewImage;
|
||||
}
|
||||
|
||||
setTranslationTarget(translationTarget) {
|
||||
this._translationTarget = translationTarget;
|
||||
Zotero.Prefs.set('feeds.lastTranslationTarget', translationTarget.treeViewID);
|
||||
this.setTranslateButton();
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['collapsed'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name) {
|
||||
switch (name) {
|
||||
case "collapsed": {
|
||||
this.handleResize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
if (this.getAttribute("collapsed")) {
|
||||
this.removeAttribute("width");
|
||||
this.removeAttribute("height");
|
||||
}
|
||||
else {
|
||||
// Must have width or height to auto-resize when changing sidenav visibility
|
||||
// Keep in sync with $min-width-item-pane and min-height + sidebar size
|
||||
let minWidth = 337;
|
||||
let minHeight = 205;
|
||||
let width = this.getAttribute("width");
|
||||
let height = this.getAttribute("height");
|
||||
if (!width || Number(width) < minWidth) this.setAttribute("width", String(minWidth));
|
||||
if (!height || Number(height) < minHeight) this.setAttribute("height", String(minHeight));
|
||||
// Render item pane after open
|
||||
if ((!width || !height) && this.viewType == "item") {
|
||||
this._itemDetails.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("item-pane", ItemPane);
|
||||
}
|
78
chrome/content/zotero/elements/itemPaneSection.js
Normal file
78
chrome/content/zotero/elements/itemPaneSection.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
***** 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 ItemPaneSectionElementBase extends XULElementBase {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.render) {
|
||||
Zotero.warn("Pane section must have method render().");
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._section) {
|
||||
this._section.removeEventListener("toggle", this._handleSectionToggle);
|
||||
this._section = null;
|
||||
}
|
||||
}
|
||||
|
||||
initCollapsibleSection() {
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
if (this._section) {
|
||||
this._section.addEventListener("toggle", this._handleSectionToggle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} if false, data change will not be saved
|
||||
*/
|
||||
_handleDataChange(_type, _value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_handleSectionToggle = async (event) => {
|
||||
if (event.target !== this._section || !this._section.open) {
|
||||
return;
|
||||
}
|
||||
if (this.render) await this.render(true);
|
||||
if (this.secondaryRender) await this.secondaryRender(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {"primary" | "secondary"} [type]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isAlreadyRendered(type = "primary") {
|
||||
let key = `_${type}RenderItemID`;
|
||||
let cachedFlag = this[key];
|
||||
if (cachedFlag && this.item?.id == cachedFlag) {
|
||||
return true;
|
||||
}
|
||||
this._lastRenderItemID = this.item.id;
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -120,17 +120,13 @@
|
|||
|
||||
_disableScrollHandler = false;
|
||||
|
||||
_pendingPane = null;
|
||||
|
||||
get container() {
|
||||
return this._container;
|
||||
}
|
||||
|
||||
set container(val) {
|
||||
if (this._container == val) return;
|
||||
this._container?.removeEventListener('scroll', this._handleContainerScroll);
|
||||
this._container = val;
|
||||
this._container.addEventListener('scroll', this._handleContainerScroll);
|
||||
this.render(true);
|
||||
}
|
||||
|
||||
|
@ -145,25 +141,21 @@
|
|||
}
|
||||
|
||||
get pinnedPane() {
|
||||
return this.getAttribute('pinnedPane');
|
||||
return this.container?.pinnedPane;
|
||||
}
|
||||
|
||||
set pinnedPane(val) {
|
||||
if (!val || !this.getPane(val)) {
|
||||
val = '';
|
||||
}
|
||||
this.setAttribute('pinnedPane', val);
|
||||
if (val) {
|
||||
this._pinnedPaneMinScrollHeight = this._getMinScrollHeightForPane(this.getPane(val));
|
||||
}
|
||||
if (!this.container) return;
|
||||
this.container.pinnedPane = val;
|
||||
}
|
||||
|
||||
get _minScrollHeight() {
|
||||
return parseFloat(this._container.style.getPropertyValue('--min-scroll-height') || 0);
|
||||
get _collapsed() {
|
||||
return this.container?._collapsed;
|
||||
}
|
||||
|
||||
set _minScrollHeight(val) {
|
||||
this._container.style.setProperty('--min-scroll-height', val + 'px');
|
||||
set _collapsed(val) {
|
||||
if (!this.container) return;
|
||||
this.container._collapsed = val;
|
||||
}
|
||||
|
||||
get _contextNotesPaneVisible() {
|
||||
|
@ -192,212 +184,17 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
get _collapsed() {
|
||||
let collapsible = this.container.closest('splitter:not([hidden="true"]) + *');
|
||||
if (!collapsible) return false;
|
||||
return collapsible.getAttribute('collapsed') === 'true';
|
||||
}
|
||||
|
||||
set _collapsed(val) {
|
||||
let collapsible = this.container.closest('splitter:not([hidden="true"]) + *');
|
||||
if (!collapsible) return;
|
||||
let splitter = collapsible.previousElementSibling;
|
||||
if (val) {
|
||||
collapsible.setAttribute('collapsed', 'true');
|
||||
splitter.setAttribute('state', 'collapsed');
|
||||
splitter.setAttribute('substate', 'after');
|
||||
}
|
||||
else {
|
||||
collapsible.removeAttribute('collapsed');
|
||||
splitter.setAttribute('state', '');
|
||||
splitter.setAttribute('substate', 'after');
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
this.render();
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['pinnedPane'];
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
scrollToPane(id, behavior = 'smooth') {
|
||||
// If the itemPane is collapsed, just remember which pane needs to be scrolled to
|
||||
// when itemPane is expanded.
|
||||
if (this._collapsed) {
|
||||
this._pendingPane = id;
|
||||
return;
|
||||
}
|
||||
if (this._contextNotesPane && this._contextNotesPaneVisible) {
|
||||
this._contextNotesPaneVisible = false;
|
||||
behavior = 'instant';
|
||||
}
|
||||
|
||||
let pane = this.getPane(id);
|
||||
if (!pane) return;
|
||||
|
||||
// The pane should always be at the very top
|
||||
// If there isn't enough stuff below it for it to be at the top, we add padding
|
||||
// We use a ::before pseudo-element for this so that we don't need to add another level to the DOM
|
||||
this._makeSpaceForPane(pane);
|
||||
if (behavior == 'smooth') {
|
||||
this._disableScrollHandler = true;
|
||||
this._waitForScroll().then(() => this._disableScrollHandler = false);
|
||||
}
|
||||
pane.scrollIntoView({ block: 'start', behavior });
|
||||
pane.focus();
|
||||
}
|
||||
|
||||
_makeSpaceForPane(pane) {
|
||||
let oldMinScrollHeight = this._minScrollHeight;
|
||||
let newMinScrollHeight = this._getMinScrollHeightForPane(pane);
|
||||
if (newMinScrollHeight > oldMinScrollHeight) {
|
||||
this._minScrollHeight = newMinScrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
_getMinScrollHeightForPane(pane) {
|
||||
let paneRect = pane.getBoundingClientRect();
|
||||
let containerRect = this._container.getBoundingClientRect();
|
||||
// No offsetTop property for XUL elements
|
||||
let offsetTop = paneRect.top - containerRect.top + this._container.scrollTop;
|
||||
return offsetTop + containerRect.height;
|
||||
}
|
||||
|
||||
_handleContainerScroll = () => {
|
||||
// Don't scroll hidden pane
|
||||
if (this.hidden || this._disableScrollHandler) return;
|
||||
let minHeight = this._minScrollHeight;
|
||||
if (minHeight) {
|
||||
let newMinScrollHeight = this._container.scrollTop + this._container.clientHeight;
|
||||
// Ignore overscroll (which generates scroll events on Windows 11, unlike on macOS)
|
||||
// and don't shrink below the pinned pane's min scroll height
|
||||
if (newMinScrollHeight > this._container.scrollHeight
|
||||
|| this.pinnedPane && newMinScrollHeight < this._pinnedPaneMinScrollHeight) {
|
||||
return;
|
||||
}
|
||||
this._minScrollHeight = newMinScrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
async _waitForScroll() {
|
||||
let scrollPromise = Zotero.Promise.defer();
|
||||
let lastScrollTop = this._container.scrollTop;
|
||||
const waitFrame = async () => {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(resolve);
|
||||
});
|
||||
};
|
||||
const waitFrames = async (n) => {
|
||||
for (let i = 0; i < n; i++) {
|
||||
await waitFrame();
|
||||
}
|
||||
};
|
||||
const checkScrollStart = () => {
|
||||
// If the scrollTop is not changed, wait for scroll to happen
|
||||
if (lastScrollTop === this._container.scrollTop) {
|
||||
requestAnimationFrame(checkScrollStart);
|
||||
}
|
||||
// Wait for scroll to end
|
||||
else {
|
||||
requestAnimationFrame(checkScrollEnd);
|
||||
}
|
||||
};
|
||||
const checkScrollEnd = async () => {
|
||||
// Wait for 3 frames to make sure not further scrolls
|
||||
await waitFrames(3);
|
||||
if (lastScrollTop === this._container.scrollTop) {
|
||||
scrollPromise.resolve();
|
||||
}
|
||||
else {
|
||||
lastScrollTop = this._container.scrollTop;
|
||||
requestAnimationFrame(checkScrollEnd);
|
||||
}
|
||||
};
|
||||
checkScrollStart();
|
||||
// Abort after 3 seconds, which should be enough
|
||||
return Promise.race([
|
||||
scrollPromise.promise,
|
||||
Zotero.Promise.delay(3000)
|
||||
]);
|
||||
}
|
||||
|
||||
getPanes() {
|
||||
return Array.from(this.container.querySelectorAll(':scope > [data-pane]:not([hidden])'));
|
||||
}
|
||||
|
||||
getPane(id) {
|
||||
return this.container.querySelector(`:scope > [data-pane="${CSS.escape(id)}"]:not([hidden])`);
|
||||
}
|
||||
|
||||
isPanePinnable(id) {
|
||||
return id !== 'info' && id !== 'context-all-notes' && id !== 'context-item-notes';
|
||||
}
|
||||
|
||||
showPendingPane() {
|
||||
if (!this._pendingPane || this._collapsed) return;
|
||||
this.scrollToPane(this._pendingPane, 'instant');
|
||||
this._pendingPane = null;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.container) {
|
||||
this.container = document.getElementById('zotero-view-item');
|
||||
}
|
||||
|
||||
for (let toolbarbutton of this.querySelectorAll('toolbarbutton')) {
|
||||
let pane = toolbarbutton.dataset.pane;
|
||||
|
||||
if (pane === 'context-notes') {
|
||||
toolbarbutton.addEventListener('click', (event) => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
if (event.detail == 2) {
|
||||
this.pinnedPane = null;
|
||||
}
|
||||
this._contextNotesPaneVisible = true;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
else if (pane === 'toggle-collapse') {
|
||||
toolbarbutton.addEventListener('click', (event) => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
this._collapsed = !this._collapsed;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let pinnable = this.isPanePinnable(pane);
|
||||
toolbarbutton.parentElement.classList.toggle('pinnable', pinnable);
|
||||
|
||||
toolbarbutton.addEventListener('click', (event) => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let scrollType = this._collapsed ? 'instant' : 'smooth';
|
||||
this._collapsed = false;
|
||||
switch (event.detail) {
|
||||
case 1:
|
||||
this.scrollToPane(pane, scrollType);
|
||||
break;
|
||||
case 2:
|
||||
if (this.pinnedPane == pane || !pinnable) {
|
||||
this.pinnedPane = null;
|
||||
}
|
||||
else {
|
||||
this.pinnedPane = pane;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (pinnable) {
|
||||
toolbarbutton.addEventListener('contextmenu', (event) => {
|
||||
this._contextMenuTarget = pane;
|
||||
|
@ -409,24 +206,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
this.addEventListener('click', this.handleButtonClick);
|
||||
|
||||
this.querySelector('.zotero-menuitem-pin').addEventListener('command', () => {
|
||||
this.scrollToPane(this._contextMenuTarget, 'smooth');
|
||||
this.container.scrollToPane(this._contextMenuTarget, 'smooth');
|
||||
this.pinnedPane = this._contextMenuTarget;
|
||||
});
|
||||
this.querySelector('.zotero-menuitem-unpin').addEventListener('command', () => {
|
||||
this.pinnedPane = null;
|
||||
});
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
render(force = false) {
|
||||
// TEMP: only render sidenav when pane is visible
|
||||
if (!force && this.container.id === "zotero-view-item"
|
||||
&& document.querySelector("#zotero-item-pane-content").selectedIndex !== "1"
|
||||
) {
|
||||
return;
|
||||
destroy() {
|
||||
this.removeEventListener('click', this.handleButtonClick);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.container) return;
|
||||
let contextNotesPaneVisible = this._contextNotesPaneVisible;
|
||||
let pinnedPane = this.pinnedPane;
|
||||
for (let toolbarbutton of this.querySelectorAll('toolbarbutton')) {
|
||||
|
@ -460,7 +256,7 @@
|
|||
}
|
||||
|
||||
toolbarbutton.setAttribute('aria-selected', !contextNotesPaneVisible && pane == pinnedPane);
|
||||
toolbarbutton.parentElement.hidden = !this.getPane(pane);
|
||||
toolbarbutton.parentElement.hidden = !this.container.getPane(pane);
|
||||
|
||||
// Set .pinned on the container, for pin styling
|
||||
toolbarbutton.parentElement.classList.toggle('pinned', pane == pinnedPane);
|
||||
|
@ -470,6 +266,96 @@
|
|||
this.querySelector('.highlight-notes-inactive').classList.toggle('highlight',
|
||||
this._contextNotesPane && !contextNotesPaneVisible);
|
||||
}
|
||||
|
||||
updatePaneStatus(paneID) {
|
||||
if (!paneID) {
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
let toolbarbutton = this.querySelector(`toolbarbutton[data-pane=${paneID}]`);
|
||||
if (!toolbarbutton) return;
|
||||
toolbarbutton.parentElement.hidden = !this.container.getPane(paneID);
|
||||
if (this.pinnedPane) {
|
||||
if (paneID == this.pinnedPane && !toolbarbutton.parentElement.classList.contains("pinned")) {
|
||||
this.querySelector(".pin-wrapper.pinned")?.classList.remove("pinned");
|
||||
toolbarbutton.parentElement.classList.add('pinned');
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.querySelector(".pin-wrapper.pinned")?.classList.remove("pinned");
|
||||
}
|
||||
}
|
||||
|
||||
toggleDefaultStatus(isDefault) {
|
||||
this._defaultStatus = isDefault;
|
||||
this.renderDefaultStatus();
|
||||
}
|
||||
|
||||
renderDefaultStatus() {
|
||||
if (this._defaultStatus) {
|
||||
this.querySelectorAll('toolbarbutton').forEach((elem) => {
|
||||
elem.disabled = true;
|
||||
elem.parentElement.hidden = !(
|
||||
["info", "abstract", "attachments", "notes", "libraries-collections", "tags", "related"]
|
||||
.includes(elem.dataset.pane));
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.querySelectorAll('toolbarbutton').forEach((elem) => {
|
||||
elem.disabled = false;
|
||||
});
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
handleButtonClick = (event) => {
|
||||
let toolbarbutton = event.target;
|
||||
let pane = toolbarbutton.dataset.pane;
|
||||
if (!pane) return;
|
||||
switch (pane) {
|
||||
case "context-notes":
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
if (event.detail == 2) {
|
||||
this.pinnedPane = null;
|
||||
}
|
||||
this._contextNotesPaneVisible = true;
|
||||
break;
|
||||
case "toggle-collapse":
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
this._collapsed = !this._collapsed;
|
||||
break;
|
||||
default: {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
let pinnable = this.isPanePinnable(pane);
|
||||
let scrollType = this._collapsed ? 'instant' : 'smooth';
|
||||
if (this._collapsed) this._collapsed = false;
|
||||
switch (event.detail) {
|
||||
case 1:
|
||||
if (this._contextNotesPane && this._contextNotesPaneVisible) {
|
||||
this._contextNotesPaneVisible = false;
|
||||
scrollType = 'instant';
|
||||
}
|
||||
this.container.scrollToPane(pane, scrollType);
|
||||
break;
|
||||
case 2:
|
||||
if (this.pinnedPane == pane || !pinnable) {
|
||||
this.pinnedPane = null;
|
||||
}
|
||||
else {
|
||||
this.pinnedPane = pane;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.render();
|
||||
};
|
||||
}
|
||||
customElements.define("item-pane-sidenav", ItemPaneSidenav);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
import { getCSSIcon } from 'components/icons';
|
||||
|
||||
{
|
||||
class LibrariesCollectionsBox extends XULElementBase {
|
||||
class LibrariesCollectionsBox extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-libraries-collections" data-pane="libraries-collections" extra-buttons="add">
|
||||
<html:div class="body"/>
|
||||
|
@ -61,11 +61,7 @@ import { getCSSIcon } from 'components/icons';
|
|||
return;
|
||||
}
|
||||
this._item = item;
|
||||
// Getting linked items is an async process, so start by rendering without them
|
||||
this._linkedItems = [];
|
||||
this.render();
|
||||
|
||||
this._updateLinkedItems();
|
||||
}
|
||||
|
||||
get mode() {
|
||||
|
@ -75,33 +71,25 @@ import { getCSSIcon } from 'components/icons';
|
|||
set mode(mode) {
|
||||
this._mode = mode;
|
||||
this.setAttribute('mode', mode);
|
||||
this.render();
|
||||
}
|
||||
|
||||
init() {
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'librariesCollectionsBox');
|
||||
this._body = this.querySelector('.body');
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this._section.addEventListener('add', (event) => {
|
||||
this.querySelector('.add-popup').openPopupAtScreen(
|
||||
event.detail.button.screenX,
|
||||
event.detail.button.screenY,
|
||||
true
|
||||
);
|
||||
this._section.open = true;
|
||||
});
|
||||
this.render();
|
||||
this.initCollapsibleSection();
|
||||
this._section.addEventListener('add', this._handleAdd);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
this._section.removeEventListener('add', this._handleAdd);
|
||||
}
|
||||
|
||||
notify(action, type, ids) {
|
||||
if (action == 'modify'
|
||||
&& this._item
|
||||
&& (ids.includes(this._item.id) || this._linkedItems.some(item => ids.includes(item.id)))) {
|
||||
this.render();
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,27 +226,53 @@ import { getCSSIcon } from 'components/icons';
|
|||
return row;
|
||||
}
|
||||
|
||||
async _updateLinkedItems() {
|
||||
render(force = false) {
|
||||
if (!this._item) return;
|
||||
if (!this._section.open) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
this._body.replaceChildren();
|
||||
|
||||
for (let item of [this._item]) {
|
||||
this._addObject(Zotero.Libraries.get(item.libraryID), item);
|
||||
for (let collection of Zotero.Collections.get(item.getCollections())) {
|
||||
this._addObject(collection, item);
|
||||
}
|
||||
}
|
||||
if (force) {
|
||||
this.secondaryRender();
|
||||
}
|
||||
}
|
||||
|
||||
async secondaryRender() {
|
||||
if (!this._item) {
|
||||
return;
|
||||
}
|
||||
// Skip if already rendered
|
||||
if (this._linkedItems.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._linkedItems = (await Promise.all(Zotero.Libraries.getAll()
|
||||
.filter(lib => lib.libraryID !== this._item.libraryID)
|
||||
.map(lib => this._item.getLinkedItem(lib.libraryID, true))))
|
||||
.filter(Boolean);
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this._item) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._body.replaceChildren();
|
||||
for (let item of [this._item, ...this._linkedItems]) {
|
||||
for (let item of this._linkedItems) {
|
||||
this._addObject(Zotero.Libraries.get(item.libraryID), item);
|
||||
for (let collection of Zotero.Collections.get(item.getCollections())) {
|
||||
this._addObject(collection, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleAdd = (event) => {
|
||||
this.querySelector('.add-popup').openPopupAtScreen(
|
||||
event.detail.button.screenX,
|
||||
event.detail.button.screenY,
|
||||
true
|
||||
);
|
||||
this._section.open = true;
|
||||
};
|
||||
}
|
||||
customElements.define("libraries-collections-box", LibrariesCollectionsBox);
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
this._destroyed = false;
|
||||
|
||||
this.content = MozXULElement.parseXULToFragment(`
|
||||
<html:div class="custom-head empty"></html:div>
|
||||
<box flex="1" tooltip="html-tooltip" style="display: flex; flex-grow: 1" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<div id="note-editor" style="display: flex;flex-direction: column;flex-grow: 1;" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<iframe id="editor-view" style="border: 0;width: 100%;flex-grow: 1;" src="resource://zotero/note-editor/editor.html" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" type="content"/>
|
||||
|
@ -321,6 +322,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
renderCustomHead(callback) {
|
||||
let customHead = this.querySelector(".custom-head");
|
||||
customHead.replaceChildren();
|
||||
let append = (...args) => {
|
||||
customHead.append(...args);
|
||||
};
|
||||
if (callback) callback({
|
||||
doc: document,
|
||||
append: (...args) => {
|
||||
append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_id(id) {
|
||||
return this.querySelector(`#${id}`);
|
||||
}
|
||||
|
@ -394,6 +409,8 @@
|
|||
}
|
||||
|
||||
refresh() {
|
||||
this._id('related').render();
|
||||
this._id('tags').render();
|
||||
}
|
||||
|
||||
_id(id) {
|
||||
|
|
|
@ -28,29 +28,24 @@
|
|||
import { getCSSItemTypeIcon } from 'components/icons';
|
||||
|
||||
{
|
||||
class NotesBox extends XULElementBase {
|
||||
class NotesBox extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-notes" data-pane="notes" extra-buttons="add">
|
||||
<html:div class="body"/>
|
||||
</collapsible-section>
|
||||
`);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
init() {
|
||||
this._mode = 'view';
|
||||
this._item = null;
|
||||
this._noteIDs = [];
|
||||
}
|
||||
|
||||
init() {
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this.initCollapsibleSection();
|
||||
this._section.addEventListener('add', this._handleAdd);
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'notesBox');
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._section = null;
|
||||
this._section.removeEventListener('add', this._handleAdd);
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
|
||||
|
@ -86,19 +81,19 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
return;
|
||||
}
|
||||
this._item = val;
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
notify(event, type, ids, extraData) {
|
||||
notify(event, type, ids, _extraData) {
|
||||
if (['modify', 'delete'].includes(event) && ids.some(id => this._noteIDs.includes(id))) {
|
||||
this._refresh();
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
_refresh() {
|
||||
render(force = false) {
|
||||
if (!this._item) {
|
||||
return;
|
||||
}
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
this._noteIDs = this._item.getNotes();
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"use strict";
|
||||
|
||||
{
|
||||
class PaneHeader extends XULElementBase {
|
||||
class PaneHeader extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<html:div class="head">
|
||||
<html:div class="title">
|
||||
|
@ -44,6 +44,8 @@
|
|||
</toolbarbutton>
|
||||
</html:div>
|
||||
</html:div>
|
||||
<html:div class="custom-head">
|
||||
</html:div>
|
||||
`, ['chrome://zotero/locale/zotero.dtd']);
|
||||
|
||||
showInFeeds = true;
|
||||
|
@ -61,7 +63,6 @@
|
|||
set item(item) {
|
||||
this.blurOpenField();
|
||||
this._item = item;
|
||||
this.render();
|
||||
}
|
||||
|
||||
get mode() {
|
||||
|
@ -70,7 +71,6 @@
|
|||
|
||||
set mode(mode) {
|
||||
this._mode = mode;
|
||||
this.render();
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -85,7 +85,7 @@
|
|||
if (!this._item) return;
|
||||
|
||||
event.preventDefault();
|
||||
let menupopup = ZoteroItemPane.buildFieldTransformMenu({
|
||||
let menupopup = ZoteroPane.buildFieldTransformMenu({
|
||||
target: this.titleField,
|
||||
onTransform: (newValue) => {
|
||||
this._setTransformedValue(newValue);
|
||||
|
@ -95,8 +95,6 @@
|
|||
menupopup.addEventListener('popuphidden', () => menupopup.remove());
|
||||
menupopup.openPopupAtScreen(event.screenX + 1, event.screenY + 1, true);
|
||||
});
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
@ -105,7 +103,7 @@
|
|||
|
||||
notify(action, type, ids) {
|
||||
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
|
||||
this.render();
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,7 +122,7 @@
|
|||
this.item.setField(this._titleFieldID, this.titleField.value);
|
||||
await this.item.saveTx();
|
||||
}
|
||||
this.render();
|
||||
this.render(true);
|
||||
}
|
||||
|
||||
async blurOpenField() {
|
||||
|
@ -134,10 +132,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
render(force = false) {
|
||||
if (!this.item) {
|
||||
return;
|
||||
}
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
this._titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title');
|
||||
|
||||
|
@ -156,6 +155,20 @@
|
|||
}
|
||||
this.menuButton.hidden = !this.item.isRegularItem() && !this.item.isAttachment();
|
||||
}
|
||||
|
||||
renderCustomHead(callback) {
|
||||
let customHead = this.querySelector(".custom-head");
|
||||
customHead.replaceChildren();
|
||||
let append = (...args) => {
|
||||
customHead.append(...args);
|
||||
};
|
||||
if (callback) callback({
|
||||
doc: document,
|
||||
append: (...args) => {
|
||||
append(...Components.utils.cloneInto(args, window, { wrapReflectors: true, cloneFunctions: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("pane-header", PaneHeader);
|
||||
}
|
||||
|
|
|
@ -28,29 +28,24 @@
|
|||
import { getCSSItemTypeIcon } from 'components/icons';
|
||||
|
||||
{
|
||||
class RelatedBox extends XULElementBase {
|
||||
class RelatedBox extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-related" data-pane="related" extra-buttons="add">
|
||||
<html:div class="body"/>
|
||||
</collapsible-section>
|
||||
`);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
init() {
|
||||
this._mode = 'view';
|
||||
this._item = null;
|
||||
}
|
||||
|
||||
init() {
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'relatedbox');
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this.initCollapsibleSection();
|
||||
this._section.addEventListener('add', this.add);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._section.removeEventListener('add', this.add);
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
this._section = null;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
|
@ -78,7 +73,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
|
||||
set item(val) {
|
||||
this._item = val;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
notify(event, type, ids, _extraData) {
|
||||
|
@ -86,7 +80,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
|
||||
// Refresh if this item has been modified
|
||||
if (event == 'modify' && ids.includes(this._item.id)) {
|
||||
this.refresh();
|
||||
this.render(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -96,14 +90,17 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
let relatedItemIDs = new Set(this._item.relatedItems.map(key => Zotero.Items.getIDFromLibraryAndKey(libraryID, key)));
|
||||
for (let id of ids) {
|
||||
if (relatedItemIDs.has(id)) {
|
||||
this.refresh();
|
||||
this.render(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
render(force = false) {
|
||||
if (!this.item) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
let body = this.querySelector('.body');
|
||||
body.replaceChildren();
|
||||
|
||||
|
|
|
@ -25,14 +25,17 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
{
|
||||
/**
|
||||
* A split menubutton with a clickable left side and a dropmarker that opens a menu.
|
||||
*/
|
||||
{
|
||||
class SplitMenuButton extends HTMLButtonElement {
|
||||
_image = null;
|
||||
|
||||
_label = null;
|
||||
|
||||
_commandListenerCache = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
@ -54,21 +57,43 @@
|
|||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.append(this.constructor.contentFragment);
|
||||
this.append(this.contentFragment);
|
||||
}
|
||||
|
||||
// Prevent DOM-attached mouse handlers from running in the dropmarker area
|
||||
for (const eventType of ['mousedown', 'mouseup', 'click']) {
|
||||
const handler = this.getAttribute('on' + eventType);
|
||||
if (!handler) {
|
||||
continue;
|
||||
disconnectedCallback() {
|
||||
while (this._commandListenerCache.length) {
|
||||
let cache = this._commandListenerCache.pop();
|
||||
super.removeEventListener("click", cache.actual);
|
||||
}
|
||||
this['on' + eventType] = null;
|
||||
this.addEventListener(eventType, (event) => {
|
||||
}
|
||||
|
||||
addEventListener(type, listener, options) {
|
||||
if (type == "command") {
|
||||
let newListener = (event) => {
|
||||
if (!this._isEventInDropmarkerBox(event)) {
|
||||
eval(handler).bind(this);
|
||||
listener(event);
|
||||
}
|
||||
};
|
||||
this._commandListenerCache.push({
|
||||
original: listener,
|
||||
actual: newListener
|
||||
});
|
||||
super.addEventListener("click", newListener, options);
|
||||
return;
|
||||
}
|
||||
super.addEventListener(type, listener, options);
|
||||
}
|
||||
|
||||
removeEventListener(type, listener, options) {
|
||||
if (type == "command") {
|
||||
let cacheIndex = this._commandListenerCache.findIndex(cache => cache.original == listener);
|
||||
if (cacheIndex != -1) {
|
||||
let cache = this._commandListenerCache.splice(cacheIndex, 1)[0];
|
||||
super.removeEventListener("click", cache.actual, options);
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.removeEventListener(type, listener, options);
|
||||
}
|
||||
|
||||
get image() {
|
||||
|
@ -87,7 +112,7 @@
|
|||
this.querySelector('[anonid="button-text"]').textContent = value;
|
||||
}
|
||||
|
||||
static get contentFragment() {
|
||||
get contentFragment() {
|
||||
// Zotero.hiDPI[Suffix] may not have been initialized yet, so calculate it ourselves
|
||||
let hiDPISuffix = window.devicePixelRatio > 1 ? '@2x' : '';
|
||||
let frag = document.importNode(
|
||||
|
@ -109,7 +134,7 @@
|
|||
|
||||
_isEventInDropmarkerBox(event) {
|
||||
let rect = this.querySelector('[anonid="dropmarker-box"]').getBoundingClientRect();
|
||||
return !Zotero.rtl && event.clientX >= rect.left || Zotero.rtl && event.clientX <= rect.right
|
||||
return !Zotero.rtl && event.clientX >= rect.left || Zotero.rtl && event.clientX <= rect.right;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,20 +26,8 @@
|
|||
"use strict";
|
||||
|
||||
{
|
||||
class TagsBox extends XULElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.count = 0;
|
||||
this.clickHandler = null;
|
||||
|
||||
this._tabDirection = null;
|
||||
this._tagColors = [];
|
||||
this._notifierID = null;
|
||||
this._mode = 'view';
|
||||
this._item = null;
|
||||
|
||||
this.content = MozXULElement.parseXULToFragment(`
|
||||
class TagsBox extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-tags" data-pane="tags" extra-buttons="add">
|
||||
<html:div class="body">
|
||||
<html:div id="rows" class="tags-box-list"/>
|
||||
|
@ -52,16 +40,18 @@
|
|||
</html:div>
|
||||
</collapsible-section>
|
||||
`, ['chrome://zotero/locale/zotero.dtd']);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._destroyed = false;
|
||||
window.addEventListener("unload", this.destroy);
|
||||
init() {
|
||||
this.count = 0;
|
||||
this.clickHandler = null;
|
||||
|
||||
let content = document.importNode(this.content, true);
|
||||
this.append(content);
|
||||
this._tabDirection = null;
|
||||
this._tagColors = [];
|
||||
this._notifierID = null;
|
||||
this._mode = 'view';
|
||||
this._item = null;
|
||||
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this.initCollapsibleSection();
|
||||
this._section.addEventListener('add', this._handleAddButtonClick);
|
||||
this.addEventListener('click', (event) => {
|
||||
if (event.target === this) {
|
||||
|
@ -100,21 +90,10 @@
|
|||
}
|
||||
|
||||
destroy() {
|
||||
if (this._destroyed) {
|
||||
return;
|
||||
}
|
||||
window.removeEventListener("unload", this.destroy);
|
||||
this._destroyed = true;
|
||||
|
||||
this._section = null;
|
||||
this._section.removeEventListener('add', this._handleAddButtonClick);
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.replaceChildren();
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
@ -151,12 +130,11 @@
|
|||
return;
|
||||
}
|
||||
this._item = val;
|
||||
this.reload();
|
||||
}
|
||||
|
||||
notify(event, type, ids, extraData) {
|
||||
if (type == 'setting' && ids.some(val => val.split("/")[1] == 'tagColors') && this.item) {
|
||||
this.reload();
|
||||
this.render(true);
|
||||
}
|
||||
else if (type == 'item-tag') {
|
||||
let itemID, _tagID;
|
||||
|
@ -186,11 +164,14 @@
|
|||
this.updateCount();
|
||||
}
|
||||
else if (type == 'tag' && event == 'modify') {
|
||||
this.reload();
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
|
||||
reload() {
|
||||
render(force = false) {
|
||||
if (!this.item) return;
|
||||
if (!force && this._isAlreadyRendered()) return;
|
||||
|
||||
Zotero.debug('Reloading tags box');
|
||||
|
||||
// Cancel field focusing while we're updating
|
||||
|
@ -316,7 +297,7 @@
|
|||
await item.saveTx();
|
||||
}
|
||||
catch (e) {
|
||||
this.reload();
|
||||
this.render(true);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -490,7 +471,7 @@
|
|||
await this.item.saveTx();
|
||||
}
|
||||
catch (e) {
|
||||
this.reload();
|
||||
this.render(true);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -507,7 +488,7 @@
|
|||
await this.item.saveTx();
|
||||
}
|
||||
catch (e) {
|
||||
this.reload();
|
||||
this.render(true);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -530,7 +511,7 @@
|
|||
|
||||
tags.forEach(tag => this.item.addTag(tag));
|
||||
await this.item.saveTx();
|
||||
this.reload();
|
||||
this.render(true);
|
||||
}
|
||||
// Single tag at end
|
||||
else {
|
||||
|
@ -548,7 +529,7 @@
|
|||
await this.item.saveTx();
|
||||
}
|
||||
catch (e) {
|
||||
this.reload();
|
||||
this.render(true);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,352 +0,0 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2009 Center for History and New Media
|
||||
George Mason University, Fairfax, Virginia, USA
|
||||
http://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 *****
|
||||
*/
|
||||
|
||||
var ZoteroItemPane = new function() {
|
||||
var _container;
|
||||
var _header, _sidenav, _scrollParent, _itemBox, _abstractBox, _attachmentsBox, _attachmentInfoBox, _attachmentAnnotationsBox, _tagsBox, _notesBox, _librariesCollectionsBox, _relatedBox, _boxes;
|
||||
var _deck;
|
||||
var _lastItem;
|
||||
var _selectedNoteID;
|
||||
var _translationTarget;
|
||||
|
||||
this.onLoad = function () {
|
||||
if (!Zotero) {
|
||||
return;
|
||||
}
|
||||
|
||||
_container = document.getElementById('zotero-view-item-container');
|
||||
_header = document.getElementById('zotero-item-pane-header');
|
||||
_sidenav = document.getElementById('zotero-view-item-sidenav');
|
||||
_scrollParent = document.getElementById('zotero-view-item');
|
||||
_itemBox = document.getElementById('zotero-editpane-item-box');
|
||||
_abstractBox = document.getElementById('zotero-editpane-abstract');
|
||||
_notesBox = document.getElementById('zotero-editpane-notes');
|
||||
_attachmentsBox = document.getElementById('zotero-editpane-attachments');
|
||||
_attachmentInfoBox = document.getElementById('zotero-attachment-box');
|
||||
_attachmentAnnotationsBox = document.getElementById('zotero-editpane-attachment-annotations');
|
||||
_tagsBox = document.getElementById('zotero-editpane-tags');
|
||||
_librariesCollectionsBox = document.getElementById('zotero-editpane-libraries-collections');
|
||||
_relatedBox = document.getElementById('zotero-editpane-related');
|
||||
_boxes = [_itemBox, _abstractBox, _notesBox, _attachmentsBox, _attachmentInfoBox, _attachmentAnnotationsBox, _librariesCollectionsBox, _tagsBox, _relatedBox];
|
||||
|
||||
_deck = document.getElementById('zotero-item-pane-content');
|
||||
|
||||
this._unregisterID = Zotero.Notifier.registerObserver(this, ['item'], 'itemPane');
|
||||
|
||||
_container.addEventListener("keypress", this.handleKeypress);
|
||||
};
|
||||
|
||||
|
||||
this.onUnload = function () {
|
||||
Zotero.Notifier.unregisterObserver(this._unregisterID);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Load a top-level item
|
||||
*/
|
||||
this.viewItem = Zotero.Promise.coroutine(function* (item, mode, pinnedPane) {
|
||||
Zotero.debug('Viewing item');
|
||||
|
||||
_notesBox.parentItem = item;
|
||||
|
||||
let isSameItem = _lastItem?.id === item.id;
|
||||
|
||||
_lastItem = item;
|
||||
|
||||
_container.classList.toggle('feed-item', !!item.isFeedItem);
|
||||
if (item.isFeedItem) {
|
||||
let lastTranslationTarget = Zotero.Prefs.get('feeds.lastTranslationTarget');
|
||||
if (lastTranslationTarget) {
|
||||
let id = parseInt(lastTranslationTarget.substr(1));
|
||||
if (lastTranslationTarget[0] == "L") {
|
||||
_translationTarget = Zotero.Libraries.get(id);
|
||||
}
|
||||
else if (lastTranslationTarget[0] == "C") {
|
||||
_translationTarget = Zotero.Collections.get(id);
|
||||
}
|
||||
}
|
||||
if (!_translationTarget) {
|
||||
_translationTarget = Zotero.Libraries.userLibrary;
|
||||
}
|
||||
this.setTranslateButton();
|
||||
}
|
||||
|
||||
let inTrash = ZoteroPane.collectionsView.selectedTreeRow && ZoteroPane.collectionsView.selectedTreeRow.isTrash();
|
||||
for (let box of [_header, ..._boxes]) {
|
||||
if (!box.showInFeeds && item.isFeedItem) {
|
||||
box.style.display = 'none';
|
||||
box.hidden = true;
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
box.style.display = '';
|
||||
box.hidden = false;
|
||||
}
|
||||
|
||||
if (mode) {
|
||||
box.mode = mode;
|
||||
|
||||
if (box.mode == 'view') {
|
||||
box.hideEmptyFields = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
box.mode = 'edit';
|
||||
}
|
||||
|
||||
box.item = item;
|
||||
box.inTrash = inTrash;
|
||||
}
|
||||
|
||||
if (!isSameItem) {
|
||||
if (pinnedPane && !_sidenav.getPane(pinnedPane)) {
|
||||
pinnedPane = "";
|
||||
}
|
||||
|
||||
_scrollParent.style.paddingBottom = '';
|
||||
if (pinnedPane) {
|
||||
_sidenav.scrollToPane(pinnedPane, 'instant');
|
||||
_sidenav.pinnedPane = pinnedPane;
|
||||
}
|
||||
else if (pinnedPane !== false) {
|
||||
_sidenav.scrollToPane(_sidenav.getPanes()[0]?.getAttribute('data-pane'), 'instant');
|
||||
}
|
||||
}
|
||||
|
||||
_sidenav.render();
|
||||
});
|
||||
|
||||
|
||||
this.notify = Zotero.Promise.coroutine(function* (action, _type, _ids, _extraData) {
|
||||
if (action == 'refresh' && _lastItem) {
|
||||
yield this.viewItem(_lastItem, null, false);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.blurOpenField = async function () {
|
||||
if (_itemBox.contains(document.activeElement)) {
|
||||
await _itemBox.blurOpenField();
|
||||
}
|
||||
else if (_header.contains(document.activeElement)) {
|
||||
await _header.blurOpenField();
|
||||
}
|
||||
_scrollParent.focus();
|
||||
};
|
||||
|
||||
|
||||
this.onNoteSelected = function (item, editable) {
|
||||
_selectedNoteID = item.id;
|
||||
|
||||
var noteEditor = document.getElementById('zotero-note-editor');
|
||||
noteEditor.mode = editable ? 'edit' : 'view';
|
||||
noteEditor.viewMode = 'library';
|
||||
noteEditor.parent = null;
|
||||
noteEditor.item = item;
|
||||
|
||||
document.getElementById('zotero-item-pane-content').selectedIndex = 2;
|
||||
};
|
||||
|
||||
// Keyboard navigation within the itemPane. Also handles contextPane keyboard nav
|
||||
this.handleKeypress = function (event) {
|
||||
let stopEvent = () => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
let isLibraryTab = Zotero_Tabs.selectedIndex == 0;
|
||||
let sidenav = document.getElementById(
|
||||
isLibraryTab ? 'zotero-view-item-sidenav' : 'zotero-context-pane-sidenav'
|
||||
);
|
||||
|
||||
// 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);
|
||||
pane.firstChild._head.focus();
|
||||
stopEvent();
|
||||
return;
|
||||
}
|
||||
// Tab tavigation between entries and buttons within library, related and notes boxes
|
||||
if (event.key == "Tab" && event.target.closest(".box")) {
|
||||
let next = null;
|
||||
if (event.key == "Tab" && !event.shiftKey) {
|
||||
next = event.target.nextElementSibling;
|
||||
}
|
||||
if (event.key == "Tab" && event.shiftKey) {
|
||||
next = event.target.parentNode.previousElementSibling?.lastChild;
|
||||
}
|
||||
// Force the element to be visible before focusing
|
||||
if (next) {
|
||||
next.style.visibility = "visible";
|
||||
next.focus();
|
||||
next.style.removeProperty("visibility");
|
||||
stopEvent();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Select the parent item and open the note editor
|
||||
*/
|
||||
this.openNoteWindow = async function () {
|
||||
var selectedNote = Zotero.Items.get(_selectedNoteID);
|
||||
ZoteroPane.openNoteWindow(selectedNote.id);
|
||||
};
|
||||
|
||||
|
||||
this.translateSelectedItems = Zotero.Promise.coroutine(function* () {
|
||||
var collectionID = _translationTarget.objectType == 'collection' ? _translationTarget.id : undefined;
|
||||
var items = ZoteroPane_Local.itemsView.getSelectedItems();
|
||||
for (let item of items) {
|
||||
yield item.translate(_translationTarget.libraryID, collectionID);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.buildTranslateSelectContextMenu = function (event) {
|
||||
var menu = document.getElementById('zotero-item-addTo-menu');
|
||||
// Don't trigger rebuilding on nested popupmenu open/close
|
||||
if (event.target != menu) {
|
||||
return;
|
||||
}
|
||||
// Clear previous items
|
||||
while (menu.firstChild) {
|
||||
menu.removeChild(menu.firstChild);
|
||||
}
|
||||
|
||||
let target = Zotero.Prefs.get('feeds.lastTranslationTarget');
|
||||
if (!target) {
|
||||
target = "L" + Zotero.Libraries.userLibraryID;
|
||||
}
|
||||
|
||||
var libraries = Zotero.Libraries.getAll();
|
||||
for (let library of libraries) {
|
||||
if (!library.editable || library.libraryType == 'publications') {
|
||||
continue;
|
||||
}
|
||||
Zotero.Utilities.Internal.createMenuForTarget(
|
||||
library,
|
||||
menu,
|
||||
target,
|
||||
function(event, libraryOrCollection) {
|
||||
if (event.target.tagName == 'menu') {
|
||||
Zotero.Promise.coroutine(function* () {
|
||||
// Simulate menuitem flash on OS X
|
||||
if (Zotero.isMac) {
|
||||
event.target.setAttribute('_moz-menuactive', false);
|
||||
yield Zotero.Promise.delay(50);
|
||||
event.target.setAttribute('_moz-menuactive', true);
|
||||
yield Zotero.Promise.delay(50);
|
||||
event.target.setAttribute('_moz-menuactive', false);
|
||||
yield Zotero.Promise.delay(50);
|
||||
event.target.setAttribute('_moz-menuactive', true);
|
||||
}
|
||||
menu.hidePopup();
|
||||
|
||||
ZoteroItemPane.setTranslationTarget(libraryOrCollection);
|
||||
event.stopPropagation();
|
||||
})();
|
||||
}
|
||||
else {
|
||||
ZoteroItemPane.setTranslationTarget(libraryOrCollection);
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.setTranslateButton = function() {
|
||||
var label = Zotero.getString('pane.item.addTo', _translationTarget.name);
|
||||
var elem = document.getElementById('zotero-feed-item-addTo-button');
|
||||
elem.label = label;
|
||||
|
||||
var key = Zotero.Keys.getKeyForCommand('saveToZotero');
|
||||
|
||||
var tooltip = label
|
||||
+ (Zotero.rtl ? ' \u202B' : ' ') + '('
|
||||
+ (Zotero.isMac ? '⇧⌘' : Zotero.getString('general.keys.ctrlShift'))
|
||||
+ key + ')';
|
||||
elem.title = tooltip;
|
||||
elem.image = _translationTarget.treeViewImage;
|
||||
};
|
||||
|
||||
|
||||
this.setTranslationTarget = function(translationTarget) {
|
||||
_translationTarget = translationTarget;
|
||||
Zotero.Prefs.set('feeds.lastTranslationTarget', translationTarget.treeViewID);
|
||||
ZoteroItemPane.setTranslateButton();
|
||||
};
|
||||
|
||||
|
||||
this.setReadLabel = function (isRead) {
|
||||
var elem = document.getElementById('zotero-feed-item-toggleRead-button');
|
||||
var label = Zotero.getString('pane.item.' + (isRead ? 'markAsUnread' : 'markAsRead'));
|
||||
elem.textContent = label;
|
||||
|
||||
var key = Zotero.Keys.getKeyForCommand('toggleRead');
|
||||
var tooltip = label + (Zotero.rtl ? ' \u202B' : ' ') + '(' + key + ')';
|
||||
elem.title = tooltip;
|
||||
};
|
||||
|
||||
|
||||
this.getPinnedPane = function () {
|
||||
return _sidenav.pinnedPane;
|
||||
};
|
||||
|
||||
|
||||
this.buildFieldTransformMenu = function ({ target, onTransform }) {
|
||||
let value = target.value;
|
||||
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
|
||||
let valueSentenceCased = Zotero.Utilities.sentenceCase(value);
|
||||
|
||||
let menupopup = document.createXULElement('menupopup');
|
||||
|
||||
let titleCase = document.createXULElement('menuitem');
|
||||
titleCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
|
||||
titleCase.addEventListener('command', () => {
|
||||
onTransform(valueTitleCased);
|
||||
});
|
||||
titleCase.disabled = valueTitleCased == value;
|
||||
menupopup.append(titleCase);
|
||||
|
||||
let sentenceCase = document.createXULElement('menuitem');
|
||||
sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
|
||||
sentenceCase.addEventListener('command', () => {
|
||||
onTransform(valueSentenceCased);
|
||||
});
|
||||
sentenceCase.disabled = valueSentenceCased == value;
|
||||
menupopup.append(sentenceCase);
|
||||
|
||||
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, target);
|
||||
|
||||
return menupopup;
|
||||
};
|
||||
};
|
||||
|
||||
addEventListener("load", function(e) { ZoteroItemPane.onLoad(e); }, false);
|
||||
addEventListener("unload", function(e) { ZoteroItemPane.onUnload(e); }, false);
|
|
@ -581,7 +581,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
}
|
||||
}
|
||||
else if (collectionTreeRow.isFeedsOrFeed()) {
|
||||
window.ZoteroPane.updateReadLabel();
|
||||
// Moved to itemPane CE
|
||||
}
|
||||
// If not a search, process modifications manually
|
||||
else {
|
||||
|
@ -1173,7 +1173,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
// Single item
|
||||
if (rowsToSelect.length == 1) {
|
||||
// this.selection.select() triggers the tree onSelect handler attribute, which calls
|
||||
// ZoteroPane.itemSelected(), which calls ZoteroItemPane.viewItem(), which refreshes the
|
||||
// ZoteroPane.itemSelected(), which calls ZoteroPane.itemPane.render(), which refreshes the
|
||||
// itembox. But since the 'onselect' doesn't handle promises, itemSelected() isn't waited for
|
||||
// here, which means that 'yield selectItem(itemID)' continues before the itembox has been
|
||||
// refreshed. To get around this, we wait for a select event that's triggered by
|
||||
|
|
|
@ -753,8 +753,7 @@ var Zotero_Tabs = new function () {
|
|||
// Used to move focus back to itemTree or contextPane from the tabs.
|
||||
this.focusWrapAround = function () {
|
||||
// If no item is selected, focus items list.
|
||||
const pane = document.getElementById("zotero-item-pane-content");
|
||||
if (pane.selectedIndex === "0") {
|
||||
if (ZoteroPane.itemPane.viewType == "message") {
|
||||
document.getElementById("item-tree-main-default").focus();
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -33,6 +33,7 @@ var ZoteroPane = new function()
|
|||
var _unserialized = false;
|
||||
this.collectionsView = false;
|
||||
this.itemsView = false;
|
||||
this.itemPane = false;
|
||||
this.progressWindow = false;
|
||||
this._listeners = {};
|
||||
this.__defineGetter__('loaded', function () { return _loaded; });
|
||||
|
@ -113,6 +114,7 @@ var ZoteroPane = new function()
|
|||
this.itemsView?.updateFontSize();
|
||||
});
|
||||
Zotero.UIProperties.registerRoot(document.getElementById('zotero-context-pane'));
|
||||
this.itemPane = document.querySelector("#zotero-item-pane");
|
||||
ZoteroPane_Local.updateLayout();
|
||||
ZoteroPane_Local.updateToolbarPosition();
|
||||
this.updateWindow();
|
||||
|
@ -1219,8 +1221,7 @@ var ZoteroPane = new function()
|
|||
if (this.itemsView && from == this.itemsView.id) {
|
||||
// Focus TinyMCE explicitly on tab key, since the normal focusing doesn't work right
|
||||
if (!event.shiftKey && event.keyCode == event.DOM_VK_TAB) {
|
||||
var deck = document.getElementById('zotero-item-pane-content');
|
||||
if (deck.selectedPanel.id == 'zotero-view-note') {
|
||||
if (ZoteroPane.itemPane.viewType == "note") {
|
||||
document.getElementById('zotero-note-editor').focus();
|
||||
event.preventDefault();
|
||||
return;
|
||||
|
@ -1310,7 +1311,7 @@ var ZoteroPane = new function()
|
|||
case 'saveToZotero':
|
||||
var collectionTreeRow = this.getCollectionTreeRow();
|
||||
if (collectionTreeRow.isFeedsOrFeed()) {
|
||||
ZoteroItemPane.translateSelectedItems();
|
||||
this.itemPane.translateSelectedItems();
|
||||
} else {
|
||||
Zotero.debug(command + ' does not do anything in non-feed views')
|
||||
}
|
||||
|
@ -1369,7 +1370,7 @@ var ZoteroPane = new function()
|
|||
}
|
||||
}
|
||||
|
||||
yield ZoteroItemPane.blurOpenField();
|
||||
yield this.itemPane._itemDetails.blurOpenField();
|
||||
|
||||
if (row !== undefined && row !== null) {
|
||||
var collectionTreeRow = this.collectionsView.getRow(row);
|
||||
|
@ -1395,9 +1396,8 @@ var ZoteroPane = new function()
|
|||
});
|
||||
|
||||
// Expand the item pane if it's closed
|
||||
var itemPane = document.getElementById("zotero-item-pane");
|
||||
if (itemPane.getAttribute("collapsed") == "true") {
|
||||
itemPane.setAttribute("collapsed", false)
|
||||
if (this.itemPane.getAttribute("collapsed") == "true") {
|
||||
this.itemPane.setAttribute("collapsed", false);
|
||||
}
|
||||
|
||||
//set to Info tab
|
||||
|
@ -1857,12 +1857,27 @@ var ZoteroPane = new function()
|
|||
Zotero.debug("Items view not available in itemSelected", 2);
|
||||
return false;
|
||||
}
|
||||
let collectionTreeRow = this.getCollectionTreeRow();
|
||||
// I don't think this happens in normal usage, but it can happen during tests
|
||||
if (!collectionTreeRow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var selectedItems = this.itemsView.getSelectedItems();
|
||||
|
||||
// Display buttons at top of item pane depending on context. This needs to run even if the
|
||||
// selection hasn't changed, because the selected items might have been modified.
|
||||
this.updateItemPaneButtons(selectedItems);
|
||||
this.itemPane.data = selectedItems;
|
||||
let viewMode = {
|
||||
isFeedsOrFeed: collectionTreeRow.isFeedsOrFeed(),
|
||||
isDuplicates: collectionTreeRow.isDuplicates(),
|
||||
isPublications: collectionTreeRow.isPublications(),
|
||||
isTrash: collectionTreeRow.isTrash(),
|
||||
rowCount: this.itemsView.rowCount,
|
||||
};
|
||||
this.itemPane.viewMode = viewMode;
|
||||
this.itemPane.editable = this.collectionsView.editable;
|
||||
this.itemPane.updateItemPaneButtons(selectedItems);
|
||||
|
||||
// Tab selection observer in standalone.js makes sure that
|
||||
// updateQuickCopyCommands is called
|
||||
|
@ -1880,163 +1895,7 @@ var ZoteroPane = new function()
|
|||
}
|
||||
_lastSelectedItems = ids;
|
||||
|
||||
var collectionTreeRow = this.getCollectionTreeRow();
|
||||
// I don't think this happens in normal usage, but it can happen during tests
|
||||
if (!collectionTreeRow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let pane = document.getElementById('zotero-item-pane');
|
||||
let deck = document.getElementById('zotero-item-pane-content');
|
||||
let sidenav = document.getElementById('zotero-view-item-sidenav');
|
||||
|
||||
let hideSidenav = false;
|
||||
|
||||
// Single item selected
|
||||
if (selectedItems.length == 1) {
|
||||
var item = selectedItems[0];
|
||||
sidenav.querySelectorAll('toolbarbutton').forEach(button => button.disabled = false);
|
||||
|
||||
if (item.isNote()) {
|
||||
hideSidenav = true;
|
||||
ZoteroItemPane.onNoteSelected(item, this.collectionsView.editable);
|
||||
}
|
||||
|
||||
// Regular item
|
||||
else {
|
||||
var isCommons = collectionTreeRow.isBucket();
|
||||
|
||||
deck.selectedIndex = 1;
|
||||
|
||||
let pane = ZoteroItemPane.getPinnedPane();
|
||||
|
||||
var button = document.getElementById('zotero-item-show-original');
|
||||
if (isCommons) {
|
||||
button.hidden = false;
|
||||
button.disabled = !this.getOriginalItem();
|
||||
}
|
||||
else {
|
||||
button.hidden = true;
|
||||
}
|
||||
|
||||
if (this.collectionsView.editable) {
|
||||
yield ZoteroItemPane.viewItem(item, null, pane);
|
||||
}
|
||||
else {
|
||||
yield ZoteroItemPane.viewItem(item, 'view', pane);
|
||||
}
|
||||
|
||||
if (item.isFeedItem) {
|
||||
// Too slow for now
|
||||
// if (!item.isTranslated) {
|
||||
// item.translate();
|
||||
// }
|
||||
this.updateReadLabel();
|
||||
this.startItemReadTimeout(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Zero or multiple items selected
|
||||
else {
|
||||
let defaultSidenavButtons = [
|
||||
"info", "abstract", "attachments", "notes", "libraries-collections", "tags", "related"
|
||||
];
|
||||
sidenav.querySelectorAll('toolbarbutton').forEach((button) => {
|
||||
button.disabled = true;
|
||||
button.parentElement.hidden = !defaultSidenavButtons.includes(button.dataset.pane);
|
||||
});
|
||||
if (collectionTreeRow.isFeedsOrFeed()) {
|
||||
this.updateReadLabel();
|
||||
}
|
||||
|
||||
let count = selectedItems.length;
|
||||
|
||||
// Display duplicates merge interface in item pane
|
||||
if (collectionTreeRow.isDuplicates()) {
|
||||
if (!collectionTreeRow.editable) {
|
||||
if (count) {
|
||||
var msg = Zotero.getString('pane.item.duplicates.writeAccessRequired');
|
||||
}
|
||||
else {
|
||||
var msg = Zotero.getString('pane.item.selected.zero');
|
||||
}
|
||||
this.setItemPaneMessage(msg);
|
||||
}
|
||||
else if (count) {
|
||||
deck.selectedIndex = 3;
|
||||
|
||||
// Load duplicates UI code
|
||||
if (typeof Zotero_Duplicates_Pane == 'undefined') {
|
||||
Zotero.debug("Loading duplicatesMerge.js");
|
||||
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Components.interfaces.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://zotero/content/duplicatesMerge.js");
|
||||
}
|
||||
|
||||
// On a Select All of more than a few items, display a row
|
||||
// count instead of the usual item type mismatch error
|
||||
var displayNumItemsOnTypeError = count > 5 && count == this.itemsView.rowCount;
|
||||
|
||||
// Initialize the merge pane with the selected items
|
||||
Zotero_Duplicates_Pane.setItems(selectedItems, displayNumItemsOnTypeError);
|
||||
}
|
||||
else {
|
||||
var msg = Zotero.getString('pane.item.duplicates.selectToMerge');
|
||||
this.setItemPaneMessage(msg);
|
||||
}
|
||||
}
|
||||
// Display label in the middle of the item pane
|
||||
else {
|
||||
if (count) {
|
||||
var msg = Zotero.getString('pane.item.selected.multiple', count);
|
||||
}
|
||||
else {
|
||||
var rowCount = this.itemsView.rowCount;
|
||||
var str = 'pane.item.unselected.';
|
||||
switch (rowCount){
|
||||
case 0:
|
||||
str += 'zero';
|
||||
break;
|
||||
case 1:
|
||||
str += 'singular';
|
||||
break;
|
||||
default:
|
||||
str += 'plural';
|
||||
break;
|
||||
}
|
||||
var msg = Zotero.getString(str, [rowCount]);
|
||||
}
|
||||
|
||||
this.setItemPaneMessage(msg);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!document.querySelector("#zotero-items-splitter").collapsed) {
|
||||
let isStackedMode = Zotero.Prefs.get("layout") === "stacked";
|
||||
const sidenavSize = 37;
|
||||
if (hideSidenav && !sidenav.hidden) {
|
||||
sidenav.hidden = true;
|
||||
if (isStackedMode) {
|
||||
pane.height = `${(pane.clientHeight) + sidenavSize}`;
|
||||
}
|
||||
else {
|
||||
pane.width = `${(pane.clientWidth) + sidenavSize}`;
|
||||
}
|
||||
}
|
||||
else if (!hideSidenav && sidenav.hidden) {
|
||||
sidenav.hidden = false;
|
||||
if (isStackedMode) {
|
||||
pane.height = `${(pane.clientHeight) - sidenavSize}`;
|
||||
}
|
||||
else {
|
||||
pane.width = `${(pane.clientWidth) - sidenavSize}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return this.itemPane.render();
|
||||
}.bind(this))()
|
||||
.catch(function (e) {
|
||||
Zotero.logError(e);
|
||||
|
@ -2048,56 +1907,6 @@ var ZoteroPane = new function()
|
|||
}.bind(this));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Display buttons at top of item pane depending on context
|
||||
*
|
||||
* @param {Zotero.Item[]}
|
||||
*/
|
||||
this.updateItemPaneButtons = function (selectedItems) {
|
||||
if (!selectedItems.length) {
|
||||
document.querySelectorAll('.zotero-item-pane-top-buttons').forEach(x => x.hidden = true);
|
||||
return;
|
||||
}
|
||||
|
||||
// My Publications buttons
|
||||
var isPublications = this.getCollectionTreeRow().isPublications();
|
||||
// Show in My Publications view if selected items are all notes or non-linked-file attachments
|
||||
var showMyPublicationsButtons = isPublications
|
||||
&& selectedItems.every((item) => {
|
||||
return item.isNote()
|
||||
|| (item.isAttachment()
|
||||
&& item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE);
|
||||
});
|
||||
var myPublicationsButtons = document.getElementById('zotero-item-pane-top-buttons-my-publications');
|
||||
myPublicationsButtons.hidden = !showMyPublicationsButtons;
|
||||
if (showMyPublicationsButtons) {
|
||||
let button = myPublicationsButtons.firstChild;
|
||||
let hiddenItemsSelected = selectedItems.some(item => !item.inPublications);
|
||||
let str, onclick;
|
||||
if (hiddenItemsSelected) {
|
||||
str = 'showInMyPublications';
|
||||
onclick = () => Zotero.Items.addToPublications(selectedItems);
|
||||
}
|
||||
else {
|
||||
str = 'hideFromMyPublications';
|
||||
onclick = () => Zotero.Items.removeFromPublications(selectedItems);
|
||||
}
|
||||
button.label = Zotero.getString('pane.item.' + str);
|
||||
button.onclick = onclick;
|
||||
}
|
||||
|
||||
// Trash button
|
||||
let nonDeletedItemsSelected = selectedItems.some(item => !item.deleted);
|
||||
document.getElementById('zotero-item-pane-top-buttons-trash').hidden
|
||||
= !this.getCollectionTreeRow().isTrash() || nonDeletedItemsSelected;
|
||||
|
||||
// Feed buttons
|
||||
document.getElementById('zotero-item-pane-top-buttons-feed').hidden
|
||||
= !this.getCollectionTreeRow().isFeedsOrFeed()
|
||||
};
|
||||
|
||||
|
||||
this.updateAddAttachmentMenu = function (popup) {
|
||||
if (!this.canEdit()) {
|
||||
for (let node of popup.childNodes) {
|
||||
|
@ -2436,17 +2245,10 @@ var ZoteroPane = new function()
|
|||
return;
|
||||
}
|
||||
|
||||
document.getElementById('zotero-item-pane-content').selectedIndex = 3;
|
||||
|
||||
if (typeof Zotero_Duplicates_Pane == 'undefined') {
|
||||
Zotero.debug("Loading duplicatesMerge.js");
|
||||
Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
|
||||
.getService(Components.interfaces.mozIJSSubScriptLoader)
|
||||
.loadSubScript("chrome://zotero/content/duplicatesMerge.js");
|
||||
}
|
||||
this.itemPane.viewType = "duplicates";
|
||||
|
||||
// Initialize the merge pane with the selected items
|
||||
Zotero_Duplicates_Pane.setItems(this.getSelectedItems());
|
||||
this.itemPane._duplicatesPane.setItems(this.getSelectedItems());
|
||||
}
|
||||
|
||||
|
||||
|
@ -2523,26 +2325,6 @@ var ZoteroPane = new function()
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Currently used only for Commons to find original linked item
|
||||
this.getOriginalItem = function () {
|
||||
var item = this.getSelectedItems()[0];
|
||||
var collectionTreeRow = this.getCollectionTreeRow();
|
||||
// TEMP: Commons buckets only
|
||||
return collectionTreeRow.ref.getLocalItem(item);
|
||||
}
|
||||
|
||||
|
||||
this.showOriginalItem = function () {
|
||||
var item = this.getOriginalItem();
|
||||
if (!item) {
|
||||
Zotero.debug("Original item not found");
|
||||
return;
|
||||
}
|
||||
this.selectItem(item.id).done();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether every selected item can be restored from trash
|
||||
*
|
||||
|
@ -4257,25 +4039,6 @@ var ZoteroPane = new function()
|
|||
}
|
||||
|
||||
|
||||
this.setItemPaneMessage = function (content) {
|
||||
document.getElementById('zotero-item-pane-content').selectedIndex = 0;
|
||||
|
||||
var elem = document.getElementById('zotero-item-pane-message-box');
|
||||
elem.textContent = '';
|
||||
if (typeof content == 'string') {
|
||||
let contentParts = content.split("\n\n");
|
||||
for (let part of contentParts) {
|
||||
let desc = document.createXULElement('description');
|
||||
desc.appendChild(document.createTextNode(part));
|
||||
elem.appendChild(desc);
|
||||
}
|
||||
}
|
||||
else {
|
||||
elem.appendChild(content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return {Promise<Integer|null|false>} - The id of the new note in non-popup mode, null in
|
||||
* popup mode (where a note isn't created immediately), or false if library isn't editable
|
||||
|
@ -4862,11 +4625,10 @@ var ZoteroPane = new function()
|
|||
}
|
||||
}
|
||||
else if (item.isNote()) {
|
||||
var type = Zotero.Libraries.get(item.libraryID).libraryType;
|
||||
if (!this.collectionsView.editable) {
|
||||
continue;
|
||||
}
|
||||
ZoteroItemPane.openNoteWindow();
|
||||
ZoteroPane.openNoteWindow(item.id);
|
||||
}
|
||||
else if (item.isAttachment()) {
|
||||
yield this.viewAttachment(item.id, event);
|
||||
|
@ -6074,20 +5836,6 @@ var ZoteroPane = new function()
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
this.updateReadLabel = function () {
|
||||
var items = this.getSelectedItems();
|
||||
var isUnread = false;
|
||||
for (let item of items) {
|
||||
if (!item.isRead) {
|
||||
isUnread = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ZoteroItemPane.setReadLabel(!isUnread);
|
||||
};
|
||||
|
||||
|
||||
var itemReadPromise;
|
||||
this.startItemReadTimeout = function (feedItemID) {
|
||||
if (itemReadPromise) {
|
||||
|
@ -6114,7 +5862,7 @@ var ZoteroPane = new function()
|
|||
}
|
||||
|
||||
await feedItem.toggleRead(true);
|
||||
ZoteroItemPane.setReadLabel(true);
|
||||
this.itemPane.setReadLabel(true);
|
||||
}.bind(this))
|
||||
.catch(function (e) {
|
||||
if (e instanceof Zotero.Promise.CancellationError) {
|
||||
|
@ -6270,10 +6018,13 @@ var ZoteroPane = new function()
|
|||
layoutSwitcher.setAttribute("orient", "vertical");
|
||||
itemsSplitter.setAttribute("orient", "vertical");
|
||||
sidenav.classList.add("stacked");
|
||||
} else { // three-vertical-pane
|
||||
this.itemPane.classList.add("stacked");
|
||||
}
|
||||
else { // three-vertical-pane
|
||||
layoutSwitcher.setAttribute("orient", "horizontal");
|
||||
itemsSplitter.setAttribute("orient", "horizontal");
|
||||
sidenav.classList.remove("stacked");
|
||||
this.itemPane.classList.remove("stacked");
|
||||
}
|
||||
|
||||
this.updateToolbarPosition();
|
||||
|
@ -6381,7 +6132,6 @@ var ZoteroPane = new function()
|
|||
|
||||
var collectionsPane = document.getElementById("zotero-collections-pane");
|
||||
var tagSelector = document.getElementById("zotero-tag-selector");
|
||||
var sidenav = document.getElementById("zotero-view-item-sidenav");
|
||||
|
||||
var collectionsPaneWidth = collectionsPane.getBoundingClientRect().width;
|
||||
tagSelector.style.maxWidth = collectionsPaneWidth + 'px';
|
||||
|
@ -6404,9 +6154,7 @@ var ZoteroPane = new function()
|
|||
|
||||
this.handleTagSelectorResize();
|
||||
|
||||
sidenav.render();
|
||||
// If the itemPane has just been expanded, scroll to the correct pane
|
||||
sidenav.showPendingPane();
|
||||
this.itemPane.handleResize();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6436,20 +6184,50 @@ var ZoteroPane = new function()
|
|||
* Implements nsIObserver for Zotero reload
|
||||
*/
|
||||
var _reloadObserver = {
|
||||
|
||||
/**
|
||||
* Called when Zotero is reloaded (i.e., if it is switched into or out of connector mode)
|
||||
*/
|
||||
"observe":function(aSubject, aTopic, aData) {
|
||||
observe: function (aSubject, aTopic, aData) {
|
||||
if (aTopic == "zotero-reloaded") {
|
||||
Zotero.debug("Reloading Zotero pane");
|
||||
for (let func of _reloadFunctions) func(aData);
|
||||
} else if(aTopic == "zotero-before-reload") {
|
||||
}
|
||||
else if (aTopic == "zotero-before-reload") {
|
||||
Zotero.debug("Zotero pane caught before-reload event");
|
||||
for (let func of _beforeReloadFunctions) func(aData);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.buildFieldTransformMenu = function ({ target, onTransform }) {
|
||||
let value = target.value;
|
||||
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
|
||||
let valueSentenceCased = Zotero.Utilities.sentenceCase(value);
|
||||
|
||||
let menupopup = document.createXULElement('menupopup');
|
||||
|
||||
let titleCase = document.createXULElement('menuitem');
|
||||
titleCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
|
||||
titleCase.addEventListener('command', () => {
|
||||
onTransform(valueTitleCased);
|
||||
});
|
||||
titleCase.disabled = valueTitleCased == value;
|
||||
menupopup.append(titleCase);
|
||||
|
||||
let sentenceCase = document.createXULElement('menuitem');
|
||||
sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
|
||||
sentenceCase.addEventListener('command', () => {
|
||||
onTransform(valueSentenceCased);
|
||||
});
|
||||
sentenceCase.disabled = valueSentenceCased == value;
|
||||
menupopup.append(sentenceCase);
|
||||
|
||||
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, target);
|
||||
|
||||
return menupopup;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Keep track of which ZoteroPane was local (since ZoteroPane object might get swapped out for a
|
||||
|
|
|
@ -30,8 +30,6 @@
|
|||
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform-version/content/style.css"?>
|
||||
<?xml-stylesheet href="chrome://zotero/skin/itemPane.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform/content/itemPane.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css"?>
|
||||
|
||||
<!DOCTYPE window [
|
||||
|
@ -79,7 +77,6 @@
|
|||
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/tabs.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/zoteroPane.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/itemPane.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/contextPane.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/fileInterface.js", this);
|
||||
Services.scriptloader.loadSubScript("chrome://zotero/content/reportInterface.js", this);
|
||||
|
@ -1182,105 +1179,7 @@
|
|||
</splitter>
|
||||
|
||||
<!-- itemPane.xul -->
|
||||
<vbox id="zotero-item-pane" flex="0" zotero-persist="width height" height="300">
|
||||
<!-- My Publications -->
|
||||
<hbox id="zotero-item-pane-top-buttons-my-publications" class="zotero-item-pane-top-buttons" hidden="true">
|
||||
<button id="zotero-item-collection-show-hide"/>
|
||||
</hbox>
|
||||
|
||||
<!-- Trash -->
|
||||
<hbox id="zotero-item-pane-top-buttons-trash" class="zotero-item-pane-top-buttons" hidden="true">
|
||||
<button id="zotero-item-restore-button" label="&zotero.items.menu.restoreToLibrary;"
|
||||
oncommand="ZoteroPane_Local.restoreSelectedItems()"/>
|
||||
<button id="zotero-item-delete-button" label="&zotero.item.deletePermanently;"
|
||||
oncommand="ZoteroPane_Local.deleteSelectedItems()"/>
|
||||
</hbox>
|
||||
|
||||
<!-- Feed -->
|
||||
<hbox id="zotero-item-pane-top-buttons-feed" class="zotero-item-pane-top-buttons" hidden="true">
|
||||
<html:button id="zotero-feed-item-toggleRead-button"
|
||||
onclick="ZoteroPane_Local.toggleSelectedItemsRead();"/>
|
||||
<html:button is="split-menu-button" id="zotero-feed-item-addTo-button"
|
||||
onclick="ZoteroItemPane.translateSelectedItems()"
|
||||
popup="zotero-item-addTo-menu"/>
|
||||
<menupopup id="zotero-item-addTo-menu" onpopupshowing="ZoteroItemPane.buildTranslateSelectContextMenu(event);"/>
|
||||
</hbox>
|
||||
|
||||
<!-- Commons -->
|
||||
<button id="zotero-item-show-original" label="Show Original"
|
||||
oncommand="ZoteroPane_Local.showOriginalItem()" hidden="true"/>
|
||||
|
||||
<deck id="zotero-item-pane-content" class="zotero-item-pane-content" selectedIndex="0" flex="1">
|
||||
<!-- Center label (for zero or multiple item selection) -->
|
||||
<groupbox id="zotero-item-pane-groupbox" pack="center" align="center">
|
||||
<vbox id="zotero-item-pane-message-box"/>
|
||||
</groupbox>
|
||||
|
||||
<!-- Regular item -->
|
||||
<!--
|
||||
Keep in sync with contextPane.js (_addItemContext function) which
|
||||
dynamically creates this itemPane part for each tab
|
||||
-->
|
||||
<hbox id="zotero-view-item-container" class="zotero-view-item-container">
|
||||
<html:div class="zotero-view-item-main">
|
||||
<pane-header id="zotero-item-pane-header" />
|
||||
|
||||
<html:div id="zotero-view-item" class="zotero-view-item" tabindex="0">
|
||||
<item-box id="zotero-editpane-item-box" data-pane="info"/>
|
||||
|
||||
<abstract-box id="zotero-editpane-abstract" class="zotero-editpane-abstract" data-pane="abstract"/>
|
||||
|
||||
<attachments-box id="zotero-editpane-attachments" data-pane="attachments"/>
|
||||
|
||||
<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-annotations-box id="zotero-editpane-attachment-annotations" flex="1" data-pane="attachment-annotations" hidden="true"/>
|
||||
|
||||
<libraries-collections-box id="zotero-editpane-libraries-collections" class="zotero-editpane-libraries-collections" data-pane="libraries-collections"/>
|
||||
|
||||
<tags-box id="zotero-editpane-tags" class="zotero-editpane-tags" data-pane="tags"/>
|
||||
|
||||
<related-box id="zotero-editpane-related" class="zotero-editpane-related" data-pane="related"/>
|
||||
</html:div>
|
||||
</html:div>
|
||||
</hbox>
|
||||
|
||||
<!-- Note item -->
|
||||
<groupbox id="zotero-view-note" flex="1">
|
||||
<!--
|
||||
'onerror' handler crashes the app on a save error to prevent typing in notes
|
||||
while they're not being saved
|
||||
-->
|
||||
<note-editor id="zotero-note-editor" flex="1" notitle="1"
|
||||
previousfocus="zotero-items-tree"/>
|
||||
</groupbox>
|
||||
|
||||
<!-- Duplicate merging -->
|
||||
<vbox id="zotero-duplicates-merge-pane">
|
||||
<groupbox>
|
||||
<button id="zotero-duplicates-merge-button" oncommand="Zotero_Duplicates_Pane.merge()"/>
|
||||
</groupbox>
|
||||
|
||||
<groupbox id="zotero-duplicates-merge-version-select">
|
||||
<description>&zotero.duplicatesMerge.versionSelect;</description>
|
||||
<hbox>
|
||||
<richlistbox id="zotero-duplicates-merge-original-date" onselect="Zotero_Duplicates_Pane.setMaster(this.selectedIndex)" rows="0"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
|
||||
<groupbox flex="1">
|
||||
<description id="zotero-duplicates-merge-field-select">&zotero.duplicatesMerge.fieldSelect;</description>
|
||||
<vbox id="zotero-duplicates-merge-item-box-container" flex="1">
|
||||
<item-box id="zotero-duplicates-merge-item-box" flex="1"/>
|
||||
</vbox>
|
||||
</groupbox>
|
||||
</vbox>
|
||||
</deck>
|
||||
</vbox>
|
||||
|
||||
<item-pane-sidenav id="zotero-view-item-sidenav" class="zotero-view-item-sidenav"/>
|
||||
<item-pane id="zotero-item-pane" zotero-persist="width height" flex="0"/>
|
||||
</box>
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
@ -1383,6 +1282,8 @@
|
|||
oncommand="ZoteroPane.tagSelector.deleteAutomatic();
|
||||
this.setAttribute('checked', false);"/>
|
||||
</menupopup>
|
||||
<!-- itemPane translateItem -->
|
||||
<menupopup id="zotero-item-addTo-menu" onpopupshowing="ZoteroPane.itemPane.buildTranslateSelectContextMenu(event);"></menupopup>
|
||||
</popupset>
|
||||
</hbox>
|
||||
|
||||
|
|
|
@ -37,6 +37,11 @@ menu-new-standalone-note =
|
|||
menu-new-item-note =
|
||||
.label = New Item Note
|
||||
|
||||
menu-restoreToLibrary =
|
||||
.label = Restore to Library
|
||||
menu-deletePermanently =
|
||||
.label = Delete Permanently…
|
||||
|
||||
zotero-toolbar-tabs-menu =
|
||||
.tooltiptext = List all tabs
|
||||
filter-collections = Filter Collections
|
||||
|
|
|
@ -60,7 +60,6 @@
|
|||
@import "components/tabsMenu";
|
||||
@import "components/newCollectionDialog";
|
||||
@import "components/reader";
|
||||
@import "components/itemPane";
|
||||
|
||||
// Elements
|
||||
// --------------------------------------------------
|
||||
|
@ -90,3 +89,8 @@
|
|||
@import "elements/annotationRow";
|
||||
@import "elements/noteRow";
|
||||
@import "elements/librariesCollectionsBox";
|
||||
@import "elements/duplicatesMergePane";
|
||||
@import "elements/itemMessagePane";
|
||||
@import "elements/itemDetails";
|
||||
@import "elements/itemPane";
|
||||
@import "elements/contextPane";
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#zotero-item-pane {
|
||||
width: $min-width-item-pane;
|
||||
min-width: $min-width-item-pane;
|
||||
/* Need a min height to prevent layout issues in stacked mode */
|
||||
min-height: 168px;
|
||||
background: var(--material-sidepane);
|
||||
}
|
2
scss/elements/_contextPane.scss
Normal file
2
scss/elements/_contextPane.scss
Normal file
|
@ -0,0 +1,2 @@
|
|||
context-pane {
|
||||
}
|
24
scss/elements/_duplicatesMergePane.scss
Normal file
24
scss/elements/_duplicatesMergePane.scss
Normal file
|
@ -0,0 +1,24 @@
|
|||
duplicates-merge-pane {
|
||||
-moz-box-orient: vertical;
|
||||
|
||||
groupbox {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
#zotero-duplicates-merge-button
|
||||
{
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#zotero-duplicates-merge-item-box-container {
|
||||
overflow-y: auto;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* Show duplicates date list item as selected even when not focused
|
||||
(default behavior on other platforms) */
|
||||
#zotero-duplicates-merge-original-date:not(:focus) > richlistitem[selected="true"] {
|
||||
background-color: -moz-cellhighlight;
|
||||
color: -moz-cellhighlighttext;
|
||||
}
|
||||
}
|
|
@ -11,6 +11,11 @@
|
|||
.zotero-item-pane-content {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
width: $min-width-item-pane;
|
||||
min-width: $min-width-item-pane;
|
||||
/* Need a min height to prevent layout issues in stacked mode */
|
||||
min-height: 168px;
|
||||
background: var(--material-sidepane);
|
||||
}
|
||||
|
||||
.zotero-view-item-container {
|
||||
|
@ -33,6 +38,9 @@
|
|||
padding: 0 8px;
|
||||
overflow-anchor: none; /* Work around tags box causing scroll to jump - figure this out */
|
||||
scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px; /* Need gap for intersection computing */
|
||||
}
|
||||
|
||||
.zotero-view-item::before {
|
||||
|
@ -54,33 +62,3 @@
|
|||
{
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* Buttons in trash and feed views */
|
||||
.zotero-item-pane-top-buttons > button {
|
||||
-moz-box-flex: 1
|
||||
}
|
||||
|
||||
/* Merge pane in duplicates view */
|
||||
#zotero-duplicates-merge-button
|
||||
{
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#zotero-duplicates-merge-pane > groupbox {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
|
||||
#zotero-duplicates-merge-item-box-container {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#zotero-feed-item-toggleRead-button {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
#zotero-feed-item-addTo-button {
|
||||
max-width: 250px;
|
||||
}
|
32
scss/elements/_itemMessagePane.scss
Normal file
32
scss/elements/_itemMessagePane.scss
Normal file
|
@ -0,0 +1,32 @@
|
|||
item-message-pane {
|
||||
-moz-box-orient: vertical;
|
||||
|
||||
.custom-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-self: stretch;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: var(--material-toolbar);
|
||||
border-bottom: var(--material-panedivider);
|
||||
height: 28px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 26px;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#zotero-item-pane-groupbox {
|
||||
-moz-box-flex: 1;
|
||||
-moz-box-pack: center;
|
||||
-moz-box-align: center;
|
||||
-moz-appearance: none !important;
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
13
scss/elements/_itemPane.scss
Normal file
13
scss/elements/_itemPane.scss
Normal file
|
@ -0,0 +1,13 @@
|
|||
item-pane {
|
||||
&[collapsed="true"] {
|
||||
visibility: inherit;
|
||||
|
||||
#zotero-item-pane-content {
|
||||
visibility: collapse;
|
||||
}
|
||||
}
|
||||
|
||||
&.stacked {
|
||||
-moz-box-orient: vertical;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,28 @@
|
|||
note-editor {
|
||||
-moz-box-orient: vertical;
|
||||
|
||||
.custom-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-self: stretch;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: var(--material-toolbar);
|
||||
border-bottom: var(--material-panedivider);
|
||||
height: 28px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 26px;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
links-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -2,17 +2,15 @@ pane-header {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 6px 8px 0 8px;
|
||||
padding: 6px 8px;
|
||||
gap: 6px;
|
||||
border-bottom: 1px solid var(--fill-quinary);
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
gap: 4px;
|
||||
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--fill-quinary);
|
||||
}
|
||||
|
||||
.title {
|
||||
align-self: center;
|
||||
margin-top: calc(0px - var(--editable-text-padding-block));
|
||||
|
@ -30,9 +28,35 @@ pane-header {
|
|||
align-self: start;
|
||||
}
|
||||
|
||||
.menu-button toolbarbutton {
|
||||
@include svgicon-menu("go-to", "universal", "20");
|
||||
}
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.menu-button toolbarbutton {
|
||||
@include svgicon-menu("go-to", "universal", "20");
|
||||
--width-focus-border: 2px;
|
||||
@include focus-ring;
|
||||
}
|
||||
|
||||
.custom-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-self: stretch;
|
||||
gap: 6px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 26px;
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -403,9 +403,11 @@ describe("Item pane", function () {
|
|||
|
||||
let button = doc.getElementById('zotero-feed-item-toggleRead-button');
|
||||
|
||||
assert.equal(button.textContent, Zotero.getString('pane.item.markAsUnread'));
|
||||
assert.equal(button.label, Zotero.getString('pane.item.markAsUnread'));
|
||||
yield item.toggleRead(false);
|
||||
assert.equal(button.textContent, Zotero.getString('pane.item.markAsRead'));
|
||||
// Button is re-created
|
||||
button = doc.getElementById('zotero-feed-item-toggleRead-button');
|
||||
assert.equal(button.label, Zotero.getString('pane.item.markAsRead'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -856,7 +856,7 @@ describe("Zotero.ItemTree", function() {
|
|||
yield itemsView.selectItem(attachment.id);
|
||||
yield Zotero.Promise.delay();
|
||||
|
||||
var box = win.document.getElementById('zotero-item-pane-top-buttons-my-publications');
|
||||
var box = win.document.getElementById('zotero-item-pane-my-publications-button');
|
||||
assert.isFalse(box.hidden);
|
||||
});
|
||||
|
||||
|
@ -872,8 +872,9 @@ describe("Zotero.ItemTree", function() {
|
|||
|
||||
yield itemsView.selectItem(attachment.id);
|
||||
|
||||
var box = win.document.getElementById('zotero-item-pane-top-buttons-my-publications');
|
||||
assert.isTrue(box.hidden);
|
||||
var box = win.document.getElementById('zotero-item-pane-my-publications-button');
|
||||
// box is not created if it shouldn't show
|
||||
assert.isNull(box);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue