Implement attachment preview
- Implement attachment preview - Implement attachment-box redesign - Make filename editable - Use new reindex icon - Update attachment note layout - Fix reader.js eslint errors - Add fallback attachment icon and use redesign - Use attachment preview for regular items - Fix pinned pane not exists error - Double click preview to open to page - Fix itemPane pin bug - Preload preview iframe - Fix item pane scroll - Add media preview support - Fix item pane scroll bar on macos - Fix reader sidebar with standalone attachment - Fix attributeChangedCallback - Add attachmentBox _updateAttachmentIDs - Make attachment notes readonly and simplify note window script - Implement convert attachment note to new note - Support preview dragging - Annotations box redesign - Support custom buttons in the collapsible-section - Add preview toggle button - Fix collapsible section attribute listener - Make attachment box notify sync to fix errors in test
|
@ -21,6 +21,7 @@
|
|||
"XPCOMUtils": false,
|
||||
"XRegExp": false,
|
||||
"XULElement": false,
|
||||
"XULElementBase": false,
|
||||
"Cu": false,
|
||||
"ChromeWorker": false,
|
||||
"Localization": false,
|
||||
|
@ -28,6 +29,8 @@
|
|||
"L10nRegistry": false,
|
||||
"ZoteroPane_Local": false,
|
||||
"ZoteroPane": false,
|
||||
"Zotero_Tabs": false,
|
||||
"ZoteroItemPane": false,
|
||||
"IOUtils": false,
|
||||
"NetUtil": false,
|
||||
"FileUtils": false,
|
||||
|
|
|
@ -853,18 +853,7 @@ var ZoteroContextPane = new function () {
|
|||
};
|
||||
_itemContexts.push(context);
|
||||
|
||||
if (!parentID) {
|
||||
let vbox = document.createXULElement('vbox');
|
||||
vbox.setAttribute('flex', '1');
|
||||
vbox.setAttribute('align', 'center');
|
||||
vbox.setAttribute('pack', 'center');
|
||||
var description = document.createXULElement('description');
|
||||
vbox.append(description);
|
||||
description.append(Zotero.getString('pane.context.noParent'));
|
||||
container.append(vbox);
|
||||
return;
|
||||
}
|
||||
var parentItem = Zotero.Items.get(item.parentID);
|
||||
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
|
||||
|
@ -900,7 +889,11 @@ var ZoteroContextPane = new function () {
|
|||
abstractBox.setAttribute('data-pane', 'abstract');
|
||||
div.append(abstractBox);
|
||||
|
||||
// TODO: Attachments
|
||||
// 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'));
|
||||
|
@ -915,19 +908,22 @@ var ZoteroContextPane = new function () {
|
|||
div.append(relatedBox);
|
||||
|
||||
paneHeader.mode = readOnly ? 'view' : 'edit';
|
||||
paneHeader.item = parentItem;
|
||||
paneHeader.item = targetItem;
|
||||
|
||||
itemBox.mode = readOnly ? 'view' : 'edit';
|
||||
itemBox.item = parentItem;
|
||||
itemBox.item = targetItem;
|
||||
|
||||
abstractBox.mode = readOnly ? 'view' : 'edit';
|
||||
abstractBox.item = parentItem;
|
||||
abstractBox.item = targetItem;
|
||||
|
||||
attachmentBox.mode = readOnly ? 'view' : 'edit';
|
||||
attachmentBox.item = targetItem;
|
||||
|
||||
tagsBox.mode = readOnly ? 'view' : 'edit';
|
||||
tagsBox.item = parentItem;
|
||||
tagsBox.item = targetItem;
|
||||
|
||||
relatedBox.mode = readOnly ? 'view' : 'edit';
|
||||
relatedBox.item = parentItem;
|
||||
relatedBox.item = targetItem;
|
||||
|
||||
if (_itemPaneDeck.selectedPanel === container) {
|
||||
_sidenav.container = div;
|
||||
|
|
|
@ -32,6 +32,8 @@ Services.scriptloader.loadSubScript("chrome://zotero/content/elements/base.js",
|
|||
|
||||
// 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/guidancePanel.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemBox.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/mergeGroup.js', this);
|
||||
|
@ -52,6 +54,7 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/abstractBo
|
|||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/collapsibleSection.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentsBox.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentRow.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentAnnotationsBox.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/annotationRow.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/contextNotesList.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/noteRow.js', this);
|
||||
|
|
|
@ -48,7 +48,13 @@
|
|||
set item(item) {
|
||||
this.blurOpenField();
|
||||
this._item = item;
|
||||
this.render();
|
||||
if (item?.isRegularItem()) {
|
||||
this.hidden = false;
|
||||
this.render();
|
||||
}
|
||||
else {
|
||||
this.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
get mode() {
|
||||
|
|
102
chrome/content/zotero/elements/attachmentAnnotationsBox.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
***** 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 AttachmentAnnotationsBox extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-attachments-annotations" data-pane="attachment-annotations">
|
||||
<html:div class="body">
|
||||
</html:div>
|
||||
</collapsible-section>
|
||||
`);
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(item) {
|
||||
this._item = item;
|
||||
if (item?.isFileAttachment()) {
|
||||
this.hidden = false;
|
||||
this.render();
|
||||
}
|
||||
else {
|
||||
this.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this._section.addEventListener("toggle", this._handleSectionOpen);
|
||||
this._body = this.querySelector('.body');
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._section.removeEventListener("toggle", this._handleSectionOpen);
|
||||
}
|
||||
|
||||
notify(action, type, ids) {
|
||||
if (action == 'modify' && this.item && ids.includes(this.item.id)) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.initialized || !this.item?.isFileAttachment()) return;
|
||||
|
||||
let annotations = this.item.getAnnotations();
|
||||
this._section.setCount(annotations.length);
|
||||
|
||||
this._body.replaceChildren();
|
||||
|
||||
if (!this._section.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
let count = annotations.length;
|
||||
if (count === 0) {
|
||||
this.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.hidden = false;
|
||||
for (let annotation of annotations) {
|
||||
let row = document.createXULElement('annotation-row');
|
||||
row.annotation = annotation;
|
||||
this._body.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
_handleSectionOpen = (event) => {
|
||||
if (event.target !== this._section || !this._section.open) {
|
||||
return;
|
||||
}
|
||||
this.render();
|
||||
};
|
||||
}
|
||||
customElements.define("attachment-annotations-box", AttachmentAnnotationsBox);
|
||||
}
|
|
@ -26,8 +26,54 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
|
||||
{
|
||||
class AttachmentBox extends XULElement {
|
||||
class AttachmentBox extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-attachment-info" data-pane="attachment-info">
|
||||
<html:div class="body">
|
||||
<attachment-preview id="attachment-preview"/>
|
||||
<label id="url" crop="end"
|
||||
ondragstart="let dt = event.dataTransfer; dt.setData('text/x-moz-url', this.value); dt.setData('text/uri-list', this.value); dt.setData('text/plain', this.value);"/>
|
||||
<html:div class="metadata-table">
|
||||
<html:div id="fileNameRow" class="meta-row">
|
||||
<html:div class="meta-label"><label id="fileName-label" data-l10n-id="attachment-info-filename"/></html:div>
|
||||
<html:div class="meta-data"><editable-text id="fileName" nowrap="true" tight="true"/></html:div>
|
||||
</html:div>
|
||||
<html:div id="accessedRow" class="meta-row">
|
||||
<html:div class="meta-label"><label id="accessed-label" data-l10n-id="attachment-info-accessed"/></html:div>
|
||||
<html:div class="meta-data"><editable-text id="accessed" nowrap="true" tight="true" readonly="true"/></html:div>
|
||||
</html:div>
|
||||
<html:div id="pagesRow" class="meta-row">
|
||||
<html:div class="meta-label"><label id="pages-label" data-l10n-id="attachment-info-pages"/></html:div>
|
||||
<html:div class="meta-data"><editable-text id="pages" nowrap="true" tight="true" readonly="true"/></html:div>
|
||||
</html:div>
|
||||
<html:div id="dateModifiedRow" class="meta-row" hidden="true" >
|
||||
<html:div class="meta-label"><label id="dateModified-label" data-l10n-id="attachment-info-modified"/></html:div>
|
||||
<html:div class="meta-data"><editable-text id="dateModified" nowrap="true" tight="true" readonly="true"/></html:div>
|
||||
</html:div>
|
||||
<html:div id="indexStatusRow" class="meta-row">
|
||||
<html:div class="meta-label"><label id="index-status-label" data-l10n-id="attachment-info-index"/></html:div>
|
||||
<html:div class="meta-data">
|
||||
<label id="index-status"/>
|
||||
<toolbarbutton id="reindex" oncommand="this.hidden = true; setTimeout(function () { ZoteroPane_Local.reindexItem(); }, 50)"/>
|
||||
</html:div>
|
||||
</html:div>
|
||||
</html:div>
|
||||
<html:div id="note-container">
|
||||
<note-editor id="attachment-note-editor" notitle="1" flex="1"/>
|
||||
<button id="note-button" data-l10n-id="attachment-info-convert-note"/>
|
||||
</html:div>
|
||||
<button id="select-button" hidden="true"/>
|
||||
<popupset>
|
||||
<menupopup id="url-menu">
|
||||
<menuitem id="url-menuitem-copy"/>
|
||||
</menupopup>
|
||||
</popupset>
|
||||
</html:div>
|
||||
</collapsible-section>
|
||||
`);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
@ -43,52 +89,8 @@
|
|||
|
||||
this._item = null;
|
||||
|
||||
this.content = MozXULElement.parseXULToFragment(`
|
||||
<vbox id="attachment-box" flex="1" orient="vertical"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml">
|
||||
<vbox id="metadata">
|
||||
<label id="title"/>
|
||||
<label id="url" crop="end"
|
||||
ondragstart="var dt = event.dataTransfer; dt.setData('text/x-moz-url', this.value); dt.setData('text/uri-list', this.value); dt.setData('text/plain', this.value);"/>
|
||||
<html:table>
|
||||
<html:tr id="fileNameRow">
|
||||
<html:td><label id="fileName-label"/></html:td>
|
||||
<html:td><label id="fileName" crop="end"/></html:td>
|
||||
</html:tr>
|
||||
<html:tr id="accessedRow">
|
||||
<html:td><label id="accessed-label"/></html:td>
|
||||
<html:td><label id="accessed"/></html:td>
|
||||
</html:tr>
|
||||
<html:tr id="pagesRow">
|
||||
<html:td><label id="pages-label"/></html:td>
|
||||
<html:td><label id="pages"/></html:td>
|
||||
</html:tr>
|
||||
<html:tr id="dateModifiedRow" hidden="true">
|
||||
<html:td><label id="dateModified-label"/></html:td>
|
||||
<html:td><label id="dateModified"/></html:td>
|
||||
</html:tr>
|
||||
<html:tr id="indexStatusRow">
|
||||
<html:td><label id="index-status-label"/></html:td>
|
||||
<html:td><hbox>
|
||||
<label id="index-status"/>
|
||||
<image id="reindex" onclick="this.hidden = true; setTimeout(function () { ZoteroPane_Local.reindexItem(); }, 50)"/>
|
||||
</hbox></html:td>
|
||||
</html:tr>
|
||||
</html:table>
|
||||
</vbox>
|
||||
|
||||
<note-editor id="attachment-note-editor" notitle="1" flex="1"/>
|
||||
|
||||
<button id="select-button" hidden="true"/>
|
||||
|
||||
<popupset>
|
||||
<menupopup id="url-menu">
|
||||
<menuitem id="url-menuitem-copy"/>
|
||||
</menupopup>
|
||||
</popupset>
|
||||
</vbox>
|
||||
`, ['chrome://zotero/locale/zotero.dtd']);
|
||||
this._section = null;
|
||||
this._preview = null;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
|
@ -108,7 +110,6 @@
|
|||
this.displayDateModified = false;
|
||||
this.displayIndexed = false;
|
||||
this.displayNote = false;
|
||||
this.displayNoteIfEmpty = false;
|
||||
|
||||
switch (val) {
|
||||
case 'view':
|
||||
|
@ -131,7 +132,6 @@
|
|||
this.displayPages = true;
|
||||
this.displayIndexed = true;
|
||||
this.displayNote = true;
|
||||
this.displayNoteIfEmpty = true;
|
||||
this.displayDateModified = true;
|
||||
break;
|
||||
|
||||
|
@ -152,7 +152,6 @@
|
|||
this.displayAccessed = true;
|
||||
this.displayNote = true;
|
||||
// Notes aren't currently editable in mergeedit pane
|
||||
this.displayNoteIfEmpty = false;
|
||||
this.displayDateModified = true;
|
||||
break;
|
||||
|
||||
|
@ -168,7 +167,14 @@
|
|||
}
|
||||
|
||||
this._mode = val;
|
||||
this.querySelector('#attachment-box').setAttribute('mode', val);
|
||||
}
|
||||
|
||||
get usePreview() {
|
||||
return this.hasAttribute('data-use-preview');
|
||||
}
|
||||
|
||||
set usePreview(val) {
|
||||
this.toggleAttribute('data-use-preview', val);
|
||||
}
|
||||
|
||||
get item() {
|
||||
|
@ -180,23 +186,33 @@
|
|||
throw new Error("'item' must be a Zotero.Item");
|
||||
}
|
||||
this._item = val;
|
||||
this.refresh();
|
||||
if (this._item.isAttachment()) {
|
||||
this.hidden = false;
|
||||
this.render();
|
||||
}
|
||||
else {
|
||||
this.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.appendChild(document.importNode(this.content, true));
|
||||
|
||||
// For the time being, use a silly little popup
|
||||
this._id('title').addEventListener('click', () => {
|
||||
if (this.editable) {
|
||||
this.editTitle();
|
||||
}
|
||||
});
|
||||
init() {
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
|
||||
this._id('url').addEventListener('contextmenu', (event) => {
|
||||
this._id('url-menu').openPopupAtScreen(event.screenX, event.screenY, true);
|
||||
});
|
||||
|
||||
this._id("fileName").addEventListener('blur', () => {
|
||||
this.editFileName(this._id("fileName").value);
|
||||
});
|
||||
|
||||
this._preview = this._id("attachment-preview");
|
||||
|
||||
let noteButton = this._id('note-button');
|
||||
noteButton.addEventListener("command", () => {
|
||||
this.convertAttachmentNote();
|
||||
});
|
||||
|
||||
let copyMenuitem = this._id('url-menuitem-copy');
|
||||
copyMenuitem.label = Zotero.getString('general.copy');
|
||||
copyMenuitem.addEventListener('command', () => {
|
||||
|
@ -204,85 +220,53 @@
|
|||
});
|
||||
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentbox');
|
||||
|
||||
this._section.addEventListener("toggle", (ev) => {
|
||||
if (ev.target.open && this.usePreview) {
|
||||
this._preview.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
destroy() {
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
this.replaceChildren();
|
||||
}
|
||||
|
||||
notify(event, type, ids, extraData) {
|
||||
notify(event, _type, ids, _extraData) {
|
||||
if (event != 'modify' || !this.item || !this.item.id) return;
|
||||
for (let id of ids) {
|
||||
if (id != this.item.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var noteEditor = this._id('attachment-note-editor');
|
||||
if (extraData
|
||||
&& extraData[id]
|
||||
&& extraData[id].noteEditorID
|
||||
&& extraData[id].noteEditorID == noteEditor.instanceID) {
|
||||
//Zotero.debug("Skipping notification from current attachment note field");
|
||||
continue;
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
this.render();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
render() {
|
||||
Zotero.debug('Refreshing attachment box');
|
||||
|
||||
var title = this._id('title');
|
||||
var fileNameRow = this._id('fileNameRow');
|
||||
var urlField = this._id('url');
|
||||
var accessed = this._id('accessedRow');
|
||||
var pagesRow = this._id('pagesRow');
|
||||
var dateModifiedRow = this._id('dateModifiedRow');
|
||||
var indexStatusRow = this._id('indexStatusRow');
|
||||
var selectButton = this._id('select-button');
|
||||
|
||||
// DEBUG: this is annoying -- we really want to use an abstracted
|
||||
// version of createValueElement() from itemPane.js
|
||||
// (ideally in an XBL binding)
|
||||
|
||||
// Wrap title to multiple lines if necessary
|
||||
while (title.hasChildNodes()) {
|
||||
title.removeChild(title.firstChild);
|
||||
}
|
||||
var val = this.item.getField('title');
|
||||
|
||||
if (typeof val != 'string') {
|
||||
val += "";
|
||||
|
||||
if (this.usePreview) {
|
||||
this._preview.item = this.item;
|
||||
}
|
||||
|
||||
var firstSpace = val.indexOf(" ");
|
||||
// Crop long uninterrupted text, and use value attribute for empty field
|
||||
if ((firstSpace == -1 && val.length > 29 ) || firstSpace > 29 || val === "") {
|
||||
title.setAttribute('crop', 'end');
|
||||
title.setAttribute('value', val);
|
||||
}
|
||||
// Create a <description> element, essentially
|
||||
else {
|
||||
title.removeAttribute('value');
|
||||
title.appendChild(document.createTextNode(val));
|
||||
}
|
||||
|
||||
if (this.editable) {
|
||||
title.className = 'zotero-clicky';
|
||||
}
|
||||
|
||||
var isImportedURL = this.item.attachmentLinkMode ==
|
||||
Zotero.Attachments.LINK_MODE_IMPORTED_URL;
|
||||
let fileNameRow = this._id('fileNameRow');
|
||||
let urlField = this._id('url');
|
||||
let accessed = this._id('accessedRow');
|
||||
let pagesRow = this._id('pagesRow');
|
||||
let dateModifiedRow = this._id('dateModifiedRow');
|
||||
let indexStatusRow = this._id('indexStatusRow');
|
||||
let selectButton = this._id('select-button');
|
||||
|
||||
let isImportedURL = this.item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL;
|
||||
let isLinkedURL = this.item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL;
|
||||
|
||||
// Metadata for URL's
|
||||
if (this.item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL
|
||||
|| isImportedURL) {
|
||||
if (isImportedURL || isLinkedURL) {
|
||||
// URL
|
||||
if (this.displayURL) {
|
||||
var urlSpec = this.item.getField('url');
|
||||
let urlSpec = this.item.getField('url');
|
||||
urlField.setAttribute('value', urlSpec);
|
||||
urlField.setAttribute('tooltiptext', urlSpec);
|
||||
urlField.setAttribute('hidden', false);
|
||||
|
@ -305,14 +289,10 @@
|
|||
|
||||
// Access date
|
||||
if (this.displayAccessed) {
|
||||
this._id("accessed-label").value = Zotero.getString('itemFields.accessDate')
|
||||
+ Zotero.getString('punctuation.colon');
|
||||
let val = this.item.getField('accessDate');
|
||||
if (val) {
|
||||
val = Zotero.Date.sqlToDate(val, true);
|
||||
}
|
||||
if (val) {
|
||||
this._id("accessed").value = val.toLocaleString();
|
||||
let itemAccessDate = this.item.getField('accessDate');
|
||||
if (itemAccessDate) {
|
||||
itemAccessDate = Zotero.Date.sqlToDate(itemAccessDate, true);
|
||||
this._id("accessed").value = itemAccessDate.toLocaleString();
|
||||
accessed.hidden = false;
|
||||
}
|
||||
else {
|
||||
|
@ -329,14 +309,10 @@
|
|||
accessed.hidden = true;
|
||||
}
|
||||
|
||||
if (this.item.attachmentLinkMode
|
||||
!= Zotero.Attachments.LINK_MODE_LINKED_URL
|
||||
&& this.displayFileName) {
|
||||
var fileName = this.item.attachmentFilename;
|
||||
if (this.displayFileName && !isLinkedURL) {
|
||||
let fileName = this.item.attachmentFilename;
|
||||
|
||||
if (fileName) {
|
||||
this._id("fileName-label").value = Zotero.getString('pane.item.attachments.filename')
|
||||
+ Zotero.getString('punctuation.colon');
|
||||
this._id("fileName").value = fileName;
|
||||
fileNameRow.hidden = false;
|
||||
}
|
||||
|
@ -347,17 +323,16 @@
|
|||
else {
|
||||
fileNameRow.hidden = true;
|
||||
}
|
||||
|
||||
this._id("fileName").readonly = !this.editable;
|
||||
|
||||
// Page count
|
||||
if (this.displayPages) {
|
||||
if (this.displayPages && this._item.isPDFAttachment()) {
|
||||
Zotero.Fulltext.getPages(this.item.id)
|
||||
.then(function (pages) {
|
||||
if (!this.item) return;
|
||||
|
||||
pages = pages ? pages.total : null;
|
||||
if (pages) {
|
||||
this._id("pages-label").value = Zotero.getString('itemFields.pages')
|
||||
+ Zotero.getString('punctuation.colon');
|
||||
this._id("pages").value = pages;
|
||||
pagesRow.hidden = false;
|
||||
}
|
||||
|
@ -370,9 +345,7 @@
|
|||
pagesRow.hidden = true;
|
||||
}
|
||||
|
||||
if (this.displayDateModified) {
|
||||
this._id("dateModified-label").value = Zotero.getString('itemFields.dateModified')
|
||||
+ Zotero.getString('punctuation.colon');
|
||||
if (this.displayDateModified && !this._item.isWebAttachment()) {
|
||||
// Conflict resolution uses a modal window, so promises won't work, but
|
||||
// the sync process passes in the file mod time as dateModified
|
||||
if (this.synchronous) {
|
||||
|
@ -400,36 +373,16 @@
|
|||
// Full-text index information
|
||||
if (this.displayIndexed) {
|
||||
this.updateItemIndexedState()
|
||||
.then(function () {
|
||||
if (!this.item) return;
|
||||
indexStatusRow.hidden = false;
|
||||
}.bind(this));
|
||||
.then(function () {
|
||||
if (!this.item) return;
|
||||
indexStatusRow.hidden = false;
|
||||
}.bind(this));
|
||||
}
|
||||
else {
|
||||
indexStatusRow.hidden = true;
|
||||
}
|
||||
|
||||
var noteEditor = this._id('attachment-note-editor');
|
||||
|
||||
if (this.displayNote && (this.displayNoteIfEmpty || this.item.note != '')) {
|
||||
noteEditor.linksOnTop = true;
|
||||
noteEditor.hidden = false;
|
||||
|
||||
// Don't make note editable (at least for now)
|
||||
if (this.mode == 'merge' || this.mode == 'mergeedit') {
|
||||
noteEditor.mode = 'merge';
|
||||
noteEditor.displayButton = false;
|
||||
}
|
||||
else {
|
||||
noteEditor.mode = this.mode;
|
||||
}
|
||||
noteEditor.parent = null;
|
||||
noteEditor.item = this.item;
|
||||
}
|
||||
else {
|
||||
noteEditor.hidden = true;
|
||||
}
|
||||
noteEditor.viewMode = 'library';
|
||||
this.initAttachmentNoteEditor();
|
||||
|
||||
if (this.displayButton) {
|
||||
selectButton.label = this.buttonCaption;
|
||||
|
@ -442,104 +395,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
async editTitle() {
|
||||
var item = this.item;
|
||||
var oldTitle = item.getField('title');
|
||||
|
||||
var nsIPS = Services.prompt;
|
||||
|
||||
var newTitle = { value: oldTitle };
|
||||
var checkState = { value: Zotero.Prefs.get('lastRenameAssociatedFile') };
|
||||
|
||||
while (true) {
|
||||
// Don't show "Rename associated file" option for
|
||||
// linked URLs
|
||||
if (item.attachmentLinkMode ==
|
||||
Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
||||
var result = nsIPS.prompt(
|
||||
window,
|
||||
'',
|
||||
Zotero.getString('pane.item.attachments.rename.title'),
|
||||
newTitle,
|
||||
null,
|
||||
{}
|
||||
);
|
||||
|
||||
// If they hit cancel or left it blank
|
||||
if (!result || !newTitle.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var result = nsIPS.prompt(
|
||||
window,
|
||||
'',
|
||||
Zotero.getString('pane.item.attachments.rename.title'),
|
||||
newTitle,
|
||||
Zotero.getString('pane.item.attachments.rename.renameAssociatedFile'),
|
||||
checkState
|
||||
);
|
||||
|
||||
// If they hit cancel or left it blank
|
||||
if (!result || !newTitle.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
Zotero.Prefs.set('lastRenameAssociatedFile', checkState.value);
|
||||
|
||||
// Rename associated file
|
||||
if (checkState.value) {
|
||||
var newFilename = newTitle.value.trim();
|
||||
if (newFilename.search(/\.\w{1,10}$/) == -1) {
|
||||
// User did not specify extension. Use current
|
||||
var oldExt = item.getFilename().match(/\.\w{1,10}$/);
|
||||
if (oldExt) newFilename += oldExt[0];
|
||||
}
|
||||
var renamed = await item.renameAttachmentFile(newFilename);
|
||||
if (renamed == -1) {
|
||||
var confirmed = nsIPS.confirm(
|
||||
window,
|
||||
'',
|
||||
newFilename + ' exists. Overwrite existing file?'
|
||||
);
|
||||
if (!confirmed) {
|
||||
// If they said not to overwrite existing file,
|
||||
// start again
|
||||
continue;
|
||||
}
|
||||
|
||||
// Force overwrite, but make sure we check that this doesn't fail
|
||||
renamed = await item.renameAttachmentFile(newFilename, true);
|
||||
}
|
||||
|
||||
if (renamed == -2) {
|
||||
nsIPS.alert(
|
||||
window,
|
||||
Zotero.getString('general.error'),
|
||||
Zotero.getString('pane.item.attachments.rename.error')
|
||||
);
|
||||
return;
|
||||
}
|
||||
else if (!renamed) {
|
||||
nsIPS.alert(
|
||||
window,
|
||||
Zotero.getString('pane.item.attachments.fileNotFound.title'),
|
||||
Zotero.getString('pane.item.attachments.fileNotFound.text1')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (newTitle.value != oldTitle) {
|
||||
item.setField('title', newTitle.value);
|
||||
await item.saveTx();
|
||||
}
|
||||
}
|
||||
|
||||
onViewClick(event) {
|
||||
ZoteroPane_Local.viewAttachment(this.item.id, event, !this.editable);
|
||||
}
|
||||
|
@ -550,13 +405,13 @@
|
|||
|
||||
updateItemIndexedState() {
|
||||
return (async () => {
|
||||
var indexStatus = this._id('index-status');
|
||||
var reindexButton = this._id('reindex');
|
||||
let indexStatus = this._id('index-status');
|
||||
let reindexButton = this._id('reindex');
|
||||
|
||||
var status = await Zotero.Fulltext.getIndexedState(this.item);
|
||||
let status = await Zotero.Fulltext.getIndexedState(this.item);
|
||||
if (!this.item) return;
|
||||
|
||||
var str = 'fulltext.indexState.';
|
||||
let str = 'fulltext.indexState.';
|
||||
switch (status) {
|
||||
case Zotero.Fulltext.INDEX_STATE_UNAVAILABLE:
|
||||
str += 'unavailable';
|
||||
|
@ -574,15 +429,13 @@
|
|||
str = 'general.yes';
|
||||
break;
|
||||
}
|
||||
this._id("index-status-label").value = Zotero.getString('fulltext.indexState.indexed')
|
||||
+ Zotero.getString('punctuation.colon');
|
||||
indexStatus.value = Zotero.getString(str);
|
||||
|
||||
// Reindex button tooltip (string stored in zotero.properties)
|
||||
var str = Zotero.getString('pane.items.menu.reindexItem');
|
||||
str = Zotero.getString('pane.items.menu.reindexItem');
|
||||
reindexButton.setAttribute('tooltiptext', str);
|
||||
|
||||
var show = false;
|
||||
let show = false;
|
||||
if (this.editable) {
|
||||
show = await Zotero.Fulltext.canReindex(this.item);
|
||||
if (!this.item) return;
|
||||
|
@ -597,6 +450,102 @@
|
|||
})();
|
||||
}
|
||||
|
||||
async editFileName(newFilename) {
|
||||
let item = this.item;
|
||||
// Rename associated file
|
||||
let nsIPS = Services.prompt;
|
||||
newFilename = newFilename.trim();
|
||||
let oldFilename = item.getFilename();
|
||||
if (oldFilename === newFilename) {
|
||||
return;
|
||||
}
|
||||
if (newFilename.search(/\.\w{1,10}$/) == -1) {
|
||||
// User did not specify extension. Use current
|
||||
let oldExt = oldFilename.match(/\.\w{1,10}$/);
|
||||
if (oldExt) newFilename += oldExt[0];
|
||||
}
|
||||
let renamed = await item.renameAttachmentFile(newFilename);
|
||||
if (renamed == -1) {
|
||||
let confirmed = nsIPS.confirm(
|
||||
window,
|
||||
'',
|
||||
newFilename + ' exists. Overwrite existing file?'
|
||||
);
|
||||
if (!confirmed) {
|
||||
// If they said not to overwrite existing file,
|
||||
// do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
// Force overwrite, but make sure we check that this doesn't fail
|
||||
renamed = await item.renameAttachmentFile(newFilename, true);
|
||||
}
|
||||
|
||||
if (renamed == -2) {
|
||||
nsIPS.alert(
|
||||
window,
|
||||
Zotero.getString('general.error'),
|
||||
Zotero.getString('pane.item.attachments.rename.error')
|
||||
);
|
||||
}
|
||||
else if (!renamed) {
|
||||
nsIPS.alert(
|
||||
window,
|
||||
Zotero.getString('pane.item.attachments.fileNotFound.title'),
|
||||
Zotero.getString('pane.item.attachments.fileNotFound.text1')
|
||||
);
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
initAttachmentNoteEditor() {
|
||||
let noteContainer = this._id('note-container');
|
||||
let noteButton = this._id('note-button');
|
||||
let noteEditor = this._id('attachment-note-editor');
|
||||
|
||||
if (!this.displayNote || this.item.note === '') {
|
||||
noteContainer.hidden = true;
|
||||
noteEditor.hidden = true;
|
||||
noteButton.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
noteContainer.hidden = false;
|
||||
noteButton.hidden = this.mode !== 'edit';
|
||||
noteButton.setAttribute("data-l10n-args", `{"type": "${this.item.parentItem ? "child" : "standalone"}"}`);
|
||||
noteEditor.hidden = false;
|
||||
|
||||
// Don't make note editable (at least for now)
|
||||
if (this.mode == 'merge' || this.mode == 'mergeedit') {
|
||||
noteEditor.mode = 'merge';
|
||||
noteEditor.displayButton = false;
|
||||
}
|
||||
else {
|
||||
// Force read-only
|
||||
noteEditor.mode = "view";
|
||||
}
|
||||
noteEditor.parent = null;
|
||||
noteEditor.item = this.item;
|
||||
|
||||
noteEditor.viewMode = 'library';
|
||||
|
||||
// Force hide note editor tags & related
|
||||
noteEditor._id('links-container').hidden = true;
|
||||
}
|
||||
|
||||
async convertAttachmentNote() {
|
||||
if (!this.item.note || this.mode !== "edit") {
|
||||
return;
|
||||
}
|
||||
let newNote = new Zotero.Item('note');
|
||||
newNote.libraryID = this.item.libraryID;
|
||||
newNote.parentID = this.item.parentID;
|
||||
newNote.setNote(this.item.note);
|
||||
await newNote.saveTx();
|
||||
this.item.setNote("");
|
||||
await this.item.saveTx();
|
||||
}
|
||||
|
||||
_id(id) {
|
||||
return this.querySelector(`#${id}`);
|
||||
}
|
||||
|
|
434
chrome/content/zotero/elements/attachmentPreview.js
Normal file
|
@ -0,0 +1,434 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2023 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 *****
|
||||
*/
|
||||
|
||||
{
|
||||
// eslint-disable-next-line no-undef
|
||||
class AttachmentPreview extends XULElementBase {
|
||||
static fileTypeMap = {
|
||||
'application/pdf': 'pdf',
|
||||
'application/epub+zip': 'epub',
|
||||
'text/html': 'snapshot',
|
||||
// TODO: support video and audio
|
||||
// 'video/mp4': 'video',
|
||||
// 'video/webm': 'video',
|
||||
// 'video/ogg': 'video',
|
||||
// 'audio/': 'audio',
|
||||
'image/': 'image',
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._item = null;
|
||||
this._reader = null;
|
||||
this._previewInitializePromise = Zotero.Promise.defer();
|
||||
this._nextPreviewInitializePromise = Zotero.Promise.defer();
|
||||
|
||||
this._renderingItemID = null;
|
||||
|
||||
this._isDiscardPlanned = false;
|
||||
this._isDiscarding = false;
|
||||
|
||||
this._intersectionOb = new IntersectionObserver(this._handleIntersection.bind(this));
|
||||
this._resizeOb = new ResizeObserver(this._handleResize.bind(this));
|
||||
}
|
||||
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<browser id="preview"
|
||||
tooltip="iframeTooltip"
|
||||
type="content"
|
||||
primary="true"
|
||||
transparent="transparent"
|
||||
src="resource://zotero/reader/reader.html"
|
||||
flex="1"/>
|
||||
<browser id="next-preview"
|
||||
tooltip="iframeTooltip"
|
||||
type="content"
|
||||
primary="true"
|
||||
transparent="transparent"
|
||||
src="resource://zotero/reader/reader.html"
|
||||
flex="1"/>
|
||||
<html:img id="image-preview"></html:img>
|
||||
<html:span class="icon"></html:span>
|
||||
<html:div class="btn-container">
|
||||
<toolbarbutton id="prev" class="btn-prev" ondblclick="event.stopPropagation()"
|
||||
data-goto="prev" oncommand="this.closest('attachment-preview').goto(event)"/>
|
||||
<toolbarbutton id="next" class="btn-next" ondblclick="event.stopPropagation()"
|
||||
data-goto="next" oncommand="this.closest('attachment-preview').goto(event)"/>
|
||||
</html:div>
|
||||
<html:div class="drag-container"></html:div>
|
||||
`);
|
||||
|
||||
get nextPreview() {
|
||||
return MozXULElement.parseXULToFragment(`
|
||||
<browser id="next-preview"
|
||||
tooltip="iframeTooltip"
|
||||
type="content"
|
||||
primary="true"
|
||||
transparent="transparent"
|
||||
src="resource://zotero/reader/reader.html"
|
||||
flex="1"/>
|
||||
`);
|
||||
}
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(val) {
|
||||
this._item = (val instanceof Zotero.Item && val.isAttachment()) ? val : null;
|
||||
if (this.isVisible) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
setItemAndRender(item) {
|
||||
this._item = item;
|
||||
this.render();
|
||||
}
|
||||
|
||||
get previewType() {
|
||||
let contentType = this._item?.attachmentContentType;
|
||||
if (!contentType) {
|
||||
return "file";
|
||||
}
|
||||
for (let type in AttachmentPreview.fileTypeMap) {
|
||||
if (contentType.startsWith(type)) {
|
||||
return AttachmentPreview.fileTypeMap[type];
|
||||
}
|
||||
}
|
||||
return "file";
|
||||
}
|
||||
|
||||
get isValidType() {
|
||||
return this.previewType !== "file";
|
||||
}
|
||||
|
||||
get isReaderType() {
|
||||
return ["pdf", "epub", "snapshot"].includes(this.previewType);
|
||||
}
|
||||
|
||||
get isMediaType() {
|
||||
return ["video", "audio", "image"].includes(this.previewType);
|
||||
}
|
||||
|
||||
get hasPreview() {
|
||||
return this.getAttribute("data-preview-status") === "success";
|
||||
}
|
||||
|
||||
setPreviewStatus(val) {
|
||||
if (!val) {
|
||||
this.setAttribute("data-preview-status", "fail");
|
||||
return;
|
||||
}
|
||||
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);
|
||||
});
|
||||
this.addEventListener("DOMContentLoaded", this._handleReaderLoad);
|
||||
this.addEventListener("mouseenter", this.updateGoto);
|
||||
this.addEventListener("dragstart", this._handleDragStart);
|
||||
this.addEventListener("dragend", this._handleDragEnd);
|
||||
this.setAttribute("data-preview-type", "unknown");
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._reader?.uninit();
|
||||
this._intersectionOb.disconnect();
|
||||
this._resizeOb.disconnect();
|
||||
this.removeEventListener("DOMContentLoaded", this._handleReaderLoad);
|
||||
this.removeEventListener("mouseenter", this.updateGoto);
|
||||
this.removeEventListener("dragstart", this._handleDragStart);
|
||||
this.removeEventListener("dragend", this._handleDragEnd);
|
||||
}
|
||||
|
||||
async render() {
|
||||
let itemID = this._item?.id;
|
||||
if (!this.initialized && itemID === this._renderingItemID) {
|
||||
return;
|
||||
}
|
||||
this._renderingItemID = itemID;
|
||||
let success = false;
|
||||
if (this.isValidType && await IOUtils.exists(this._item.getFilePath())) {
|
||||
if (this.isReaderType) {
|
||||
success = await this._renderReader();
|
||||
}
|
||||
else if (this.isMediaType) {
|
||||
success = await this._renderMedia();
|
||||
}
|
||||
}
|
||||
if (itemID !== this._item?.id) {
|
||||
return;
|
||||
}
|
||||
this._updateWidthHeightRatio();
|
||||
this.setAttribute("data-preview-type", this.previewType);
|
||||
this.setPreviewStatus(success ? "success" : "fail");
|
||||
if (this._renderingItemID === itemID) {
|
||||
this._renderingItemID = null;
|
||||
}
|
||||
}
|
||||
|
||||
async discard(force = false) {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
this._isDiscardPlanned = false;
|
||||
if (this._isDiscarding) {
|
||||
return;
|
||||
}
|
||||
if (!force && this.isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isDiscarding = true;
|
||||
if (this._reader) {
|
||||
let _reader = this._reader;
|
||||
this._reader = null;
|
||||
try {
|
||||
_reader.uninit();
|
||||
}
|
||||
catch (e) {}
|
||||
}
|
||||
this._id("preview")?.remove();
|
||||
// Make previously loaded next-preview be current preview browser
|
||||
let nextPreview = this._id("next-preview");
|
||||
if (nextPreview) {
|
||||
nextPreview.id = "preview";
|
||||
}
|
||||
// Preload a new next-preview
|
||||
await this._nextPreviewInitializePromise.promise;
|
||||
this._nextPreviewInitializePromise = Zotero.Promise.defer();
|
||||
this._id("preview")?.after(this.nextPreview);
|
||||
this.setPreviewStatus("loading");
|
||||
this._isDiscarding = false;
|
||||
}
|
||||
|
||||
async openAttachment(event) {
|
||||
if (!this.isValidType) {
|
||||
return;
|
||||
}
|
||||
let options = {
|
||||
location: {},
|
||||
};
|
||||
if (this.previewType === "pdf") {
|
||||
let state = await this._reader?._internalReader?._state;
|
||||
options.location = state?.primaryViewStats;
|
||||
}
|
||||
ZoteroPane.viewAttachment(this._item.id, event, false, options);
|
||||
}
|
||||
|
||||
goto(ev) {
|
||||
this._reader?.goto(ev.target.getAttribute("data-goto"));
|
||||
ev.stopPropagation();
|
||||
setTimeout(() => this.updateGoto(), 300);
|
||||
}
|
||||
|
||||
updateGoto() {
|
||||
this._id("prev").disabled = !this._reader?.canGoto("prev");
|
||||
this._id("next").disabled = !this._reader?.canGoto("next");
|
||||
}
|
||||
|
||||
async _renderReader() {
|
||||
this.setPreviewStatus("loading");
|
||||
// This only need to be awaited during first load
|
||||
await this._previewInitializePromise.promise;
|
||||
// This should be awaited in the following refreshes
|
||||
await this._nextPreviewInitializePromise.promise;
|
||||
let prev = this._id("prev");
|
||||
let next = this._id("next");
|
||||
prev && (prev.disabled = true);
|
||||
next && (next.disabled = true);
|
||||
let success = false;
|
||||
if (this._reader?._item?.id !== this._item?.id) {
|
||||
await this.discard(true);
|
||||
this._reader = await Zotero.Reader.openPreview(this._item.id, this._id("preview"));
|
||||
success = await this._reader._open({});
|
||||
if (!success) {
|
||||
this._nextPreviewInitializePromise.resolve();
|
||||
// If failed on half-way of initialization, discard it
|
||||
this.discard(true);
|
||||
setTimeout(() => {
|
||||
// Try to re-render later
|
||||
this.render();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
else {
|
||||
success = true;
|
||||
}
|
||||
prev && (prev.disabled = true);
|
||||
next && (next.disabled = false);
|
||||
return success;
|
||||
}
|
||||
|
||||
async _renderMedia() {
|
||||
let mediaLoadPromise = new Zotero.Promise.defer();
|
||||
let mediaID = `${this.previewType}-preview`;
|
||||
let media = this._id(mediaID);
|
||||
// Create media element when needed to avoid unnecessarily loading libs like libavcodec, libvpx, etc.
|
||||
if (!media) {
|
||||
if (this.previewType === "video") {
|
||||
media = document.createElement("video");
|
||||
}
|
||||
else if (this.previewType === "audio") {
|
||||
media = document.createElement("audio");
|
||||
}
|
||||
media.id = mediaID;
|
||||
this._id("next-preview").after(media);
|
||||
}
|
||||
media.onload = () => {
|
||||
mediaLoadPromise.resolve();
|
||||
};
|
||||
media.src = `zotero://attachment/${Zotero.API.getLibraryPrefix(this._item.libraryID)}/items/${this._item.key}/`;
|
||||
await mediaLoadPromise.promise;
|
||||
return true;
|
||||
}
|
||||
|
||||
_handleReaderLoad(event) {
|
||||
if (this._id("preview")?.contentWindow?.document === event.target) {
|
||||
this._previewInitializePromise.resolve();
|
||||
}
|
||||
else if (this._id("next-preview")?.contentWindow?.document === event.target) {
|
||||
this._nextPreviewInitializePromise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
this.style.setProperty("--preview-width", `${this.clientWidth}px`);
|
||||
}
|
||||
|
||||
_handleDragStart(event) {
|
||||
this._updateDragImage();
|
||||
Zotero.Utilities.Internal.onDragItems(event, [this.item.id], this._dragImageContainer);
|
||||
}
|
||||
|
||||
_handleDragEnd() {
|
||||
this._dragImageContainer.innerHTML = "";
|
||||
}
|
||||
|
||||
_updateDragImage() {
|
||||
let dragImage;
|
||||
if (this.isMediaType) {
|
||||
dragImage = this._id(`${this.previewType}-preview`).cloneNode(true);
|
||||
}
|
||||
else {
|
||||
dragImage = this.querySelector(".icon").cloneNode(true);
|
||||
}
|
||||
this._dragImageContainer.append(dragImage);
|
||||
}
|
||||
|
||||
_updateWidthHeightRatio() {
|
||||
const A4Size = 0.7070707071;
|
||||
const BookSize = 1.25;
|
||||
let defaultSize = this.previewType === "pdf" ? A4Size : BookSize;
|
||||
let scaleRatio = defaultSize;
|
||||
if (this.previewType === "pdf") {
|
||||
scaleRatio = this._reader?.getPageWidthHeightRatio();
|
||||
}
|
||||
else if (this.previewType === "image") {
|
||||
let img = this._id("image-preview");
|
||||
scaleRatio = img.naturalWidth / img.naturalHeight;
|
||||
}
|
||||
!scaleRatio && (scaleRatio = defaultSize);
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("attachment-preview", AttachmentPreview);
|
||||
}
|
89
chrome/content/zotero/elements/attachmentPreviewBox.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
***** 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 AttachmentPreviewBox extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-attachment-preview" data-pane="attachment-preview">
|
||||
<html:div class="body">
|
||||
<attachment-preview id="attachment-preview"/>
|
||||
<html:span id="preview-placeholder" data-l10n-id="attachment-preview-placeholder"></html:span>
|
||||
</html:div>
|
||||
</collapsible-section>
|
||||
`);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._item = null;
|
||||
this._section = null;
|
||||
this._preview = null;
|
||||
}
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(item) {
|
||||
if (!(item instanceof Zotero.Item)) {
|
||||
throw new Error("'item' must be a Zotero.Item");
|
||||
}
|
||||
// TEMP: disable the preview section for now
|
||||
this.hidden = true;
|
||||
// this._item = item;
|
||||
// if (this._item.isRegularItem()) {
|
||||
// this.hidden = false;
|
||||
// this.render();
|
||||
// }
|
||||
// else {
|
||||
// this.hidden = true;
|
||||
// }
|
||||
}
|
||||
|
||||
init() {
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this._preview = this.querySelector("#attachment-preview");
|
||||
|
||||
this._section.addEventListener("toggle", (ev) => {
|
||||
if (ev.target.open && this.usePreview) {
|
||||
this._preview.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
|
||||
async render() {
|
||||
let bestAttachment = await this.item.getBestAttachment();
|
||||
if (bestAttachment) {
|
||||
this._preview.item = bestAttachment;
|
||||
}
|
||||
this.toggleAttribute("data-use-preview", !!bestAttachment);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("attachment-preview-box", AttachmentPreviewBox);
|
||||
}
|
|
@ -31,19 +31,19 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
class AttachmentRow extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<html:div class="head">
|
||||
<html:span class="twisty"/>
|
||||
<html:div class="clicky-item">
|
||||
<html:div class="clicky-item attachment-btn">
|
||||
<html:span class="icon"/>
|
||||
<html:div class="label"/>
|
||||
</html:div>
|
||||
<html:div class="clicky-item annotation-btn">
|
||||
<html:span class="icon"/>
|
||||
<html:span class="label"/>
|
||||
</html:div>
|
||||
</html:div>
|
||||
<html:div class="body"/>
|
||||
`);
|
||||
|
||||
_attachment = null;
|
||||
|
||||
_mode = null;
|
||||
|
||||
_listenerAdded = false;
|
||||
|
||||
static get observedAttributes() {
|
||||
|
@ -59,34 +59,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
this.render();
|
||||
}
|
||||
|
||||
get open() {
|
||||
if (this.empty) {
|
||||
return false;
|
||||
}
|
||||
return this.hasAttribute('open');
|
||||
}
|
||||
|
||||
set open(newOpen) {
|
||||
newOpen = !!newOpen;
|
||||
let oldOpen = this.open;
|
||||
if (oldOpen === newOpen || this.empty) return;
|
||||
this.render();
|
||||
let openHeight = this._body.scrollHeight;
|
||||
if (openHeight) {
|
||||
this.style.setProperty('--open-height', `${openHeight}px`);
|
||||
}
|
||||
else {
|
||||
this.style.setProperty('--open-height', 'auto');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-void
|
||||
void getComputedStyle(this).maxHeight; // Force style calculation! Without this the animation doesn't work
|
||||
this.toggleAttribute('open', newOpen);
|
||||
if (!newOpen && this.ownerDocument?.activeElement && this.contains(this.ownerDocument?.activeElement)) {
|
||||
this.ownerDocument.activeElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
get attachment() {
|
||||
return this._attachment;
|
||||
}
|
||||
|
@ -100,76 +72,56 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
return this._attachment.getField('title');
|
||||
}
|
||||
|
||||
get empty() {
|
||||
return !this._attachment
|
||||
|| !this._attachment.isFileAttachment()
|
||||
|| !this._attachment.numAnnotations();
|
||||
}
|
||||
|
||||
get contextRow() {
|
||||
return this.classList.contains('context');
|
||||
}
|
||||
|
||||
set contextRow(val) {
|
||||
this.classList.toggle('context', !!val);
|
||||
}
|
||||
|
||||
init() {
|
||||
this._head = this.querySelector('.head');
|
||||
this._head.addEventListener('click', this._handleClick);
|
||||
this._head.addEventListener('keydown', this._handleKeyDown);
|
||||
|
||||
this._label = this.querySelector('.label');
|
||||
this._body = this.querySelector('.body');
|
||||
this.open = false;
|
||||
this._attachmentButton = this.querySelector('.attachment-btn');
|
||||
this._annotationButton = this.querySelector('.annotation-btn');
|
||||
|
||||
this._attachmentButton.addEventListener('click', this._handleAttachmentClick);
|
||||
this._annotationButton.addEventListener('click', this._handleAnnotationClick);
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._attachmentButton.removeEventListener('click', this._handleAttachmentClick);
|
||||
this._annotationButton.removeEventListener('click', this._handleAnnotationClick);
|
||||
}
|
||||
|
||||
_handleClick = (event) => {
|
||||
if (event.target.closest('.clicky-item')) {
|
||||
let win = Zotero.getMainWindow();
|
||||
if (win) {
|
||||
win.ZoteroPane.selectItem(this._attachment.id);
|
||||
win.Zotero_Tabs.select('zotero-pane');
|
||||
win.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.open = !this.open;
|
||||
_handleAttachmentClick = (event) => {
|
||||
ZoteroPane.viewAttachment(this._attachment.id, event);
|
||||
};
|
||||
|
||||
_handleKeyDown = (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
this.open = !this.open;
|
||||
event.preventDefault();
|
||||
|
||||
_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;
|
||||
}
|
||||
let win = Zotero.getMainWindow();
|
||||
if (win) {
|
||||
win.ZoteroPane.selectItem(this._attachment.id);
|
||||
win.Zotero_Tabs.select('zotero-pane');
|
||||
win.focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
_getSidenav() {
|
||||
// TODO: update this after unifying item pane & context pane
|
||||
return document.querySelector(
|
||||
Zotero_Tabs.selectedType === 'library'
|
||||
? "#zotero-view-item-sidenav"
|
||||
: "#zotero-context-pane-sidenav");
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.initialized) return;
|
||||
|
||||
this.querySelector('.icon').replaceWith(getCSSItemTypeIcon(this._attachment.getItemTypeIconName()));
|
||||
this._label.textContent = this._attachment.getField('title');
|
||||
|
||||
this._body.replaceChildren();
|
||||
|
||||
if (this._attachment.isFileAttachment()) {
|
||||
for (let annotation of this._attachment.getAnnotations()) {
|
||||
let row = document.createXULElement('annotation-row');
|
||||
row.annotation = annotation;
|
||||
this._body.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._listenerAdded) {
|
||||
this._body.addEventListener('transitionend', () => {
|
||||
this.style.setProperty('--open-height', 'auto');
|
||||
});
|
||||
this._listenerAdded = true;
|
||||
}
|
||||
|
||||
this._head.setAttribute('aria-expanded', this.open);
|
||||
this.toggleAttribute('empty', this.empty);
|
||||
this._attachmentButton.querySelector(".icon").replaceWith(getCSSItemTypeIcon(this._attachment.getItemTypeIconName()));
|
||||
this._attachmentButton.querySelector(".label").textContent = this._attachment.getField('title');
|
||||
let annotationCount = this.attachment.getAnnotations().length;
|
||||
this._annotationButton.hidden = annotationCount === 0;
|
||||
this._annotationButton.querySelector(".label").textContent = annotationCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,10 @@
|
|||
{
|
||||
class AttachmentsBox extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-attachments" data-pane="attachments" show-add="true">
|
||||
<collapsible-section data-l10n-id="section-attachments" data-pane="attachments" extra-buttons="add">
|
||||
<html:div class="body">
|
||||
<attachment-preview/>
|
||||
<html:div class="attachments-container"></html:div>
|
||||
</html:div>
|
||||
</collapsible-section>
|
||||
<popupset/>
|
||||
|
@ -37,27 +39,30 @@
|
|||
|
||||
_item = null;
|
||||
|
||||
_attachmentIDs = [];
|
||||
|
||||
_mode = null;
|
||||
|
||||
_inTrash = false;
|
||||
|
||||
|
||||
_preview = null;
|
||||
|
||||
get item() {
|
||||
return this._item;
|
||||
}
|
||||
|
||||
set item(item) {
|
||||
let isRegularItem = item?.isRegularItem();
|
||||
this.hidden = !isRegularItem;
|
||||
if (this._item === item) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._item = item;
|
||||
this._body.replaceChildren();
|
||||
if (item) {
|
||||
for (let attachment of Zotero.Items.get(item.getAttachments(true))) {
|
||||
this.addRow(attachment);
|
||||
}
|
||||
this.updateCount();
|
||||
if (!isRegularItem) {
|
||||
return;
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
get mode() {
|
||||
|
@ -73,23 +78,50 @@
|
|||
}
|
||||
|
||||
set inTrash(inTrash) {
|
||||
if (this._inTrash === inTrash) {
|
||||
return;
|
||||
}
|
||||
this._inTrash = inTrash;
|
||||
for (let row of this._body.children) {
|
||||
if (!this._item.isRegularItem()) {
|
||||
return;
|
||||
}
|
||||
for (let row of Array.from(this._attachments.querySelectorAll("attachment-row"))) {
|
||||
this._updateRowAttributes(row, row.attachment);
|
||||
}
|
||||
this.updateCount();
|
||||
}
|
||||
|
||||
get usePreview() {
|
||||
return this.hasAttribute('data-use-preview');
|
||||
}
|
||||
|
||||
set usePreview(val) {
|
||||
this.toggleAttribute('data-use-preview', val);
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
init() {
|
||||
this._section = this.querySelector('collapsible-section');
|
||||
this._section.addEventListener('add', this._handleAdd);
|
||||
this._body = this.querySelector('.body');
|
||||
// this._section.addEventListener('togglePreview', this._handleTogglePreview);
|
||||
|
||||
this._attachments = this.querySelector('.attachments-container');
|
||||
|
||||
this._addPopup = document.getElementById('zotero-add-attachment-popup').cloneNode(true);
|
||||
this._addPopup.id = '';
|
||||
this.querySelector('popupset').append(this._addPopup);
|
||||
|
||||
this._preview = this.querySelector('attachment-preview');
|
||||
|
||||
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() {
|
||||
|
@ -97,67 +129,119 @@
|
|||
}
|
||||
|
||||
notify(action, type, ids) {
|
||||
if (!this._item) return;
|
||||
|
||||
let itemAttachmentIDs = this._item.getAttachments(true);
|
||||
let attachments = Zotero.Items.get(ids.filter(id => itemAttachmentIDs.includes(id)));
|
||||
if (action == 'add') {
|
||||
for (let attachment of attachments) {
|
||||
this.addRow(attachment);
|
||||
if (!this._item?.isRegularItem()) return;
|
||||
|
||||
this.updatePreview();
|
||||
|
||||
this._updateAttachmentIDs().then(() => {
|
||||
let attachments = Zotero.Items.get((this._attachmentIDs).filter(id => ids.includes(id)));
|
||||
if (attachments.length === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (action == 'modify') {
|
||||
for (let attachment of attachments) {
|
||||
let row = this.querySelector(`attachment-row[attachment-id="${attachment.id}"]`);
|
||||
let open = false;
|
||||
if (row) {
|
||||
open = row.open;
|
||||
row.remove();
|
||||
}
|
||||
this.addRow(attachment).open = open;
|
||||
}
|
||||
}
|
||||
else if (action == 'delete') {
|
||||
for (let attachment of attachments) {
|
||||
let row = this.querySelector(`attachment-row[attachment-id="${attachment.id}"]`);
|
||||
if (row) {
|
||||
row.remove();
|
||||
if (action == 'add') {
|
||||
for (let attachment of attachments) {
|
||||
this.addRow(attachment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateCount();
|
||||
else if (action == 'modify') {
|
||||
for (let attachment of attachments) {
|
||||
let row = this.querySelector(`attachment-row[attachment-id="${attachment.id}"]`);
|
||||
let open = false;
|
||||
if (row) {
|
||||
open = row.open;
|
||||
row.remove();
|
||||
}
|
||||
this.addRow(attachment).open = open;
|
||||
}
|
||||
}
|
||||
else if (action == 'delete') {
|
||||
for (let attachment of attachments) {
|
||||
let row = this.querySelector(`attachment-row[attachment-id="${attachment.id}"]`);
|
||||
if (row) {
|
||||
row.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateCount();
|
||||
});
|
||||
}
|
||||
|
||||
addRow(attachment) {
|
||||
addRow(attachment, open = false) {
|
||||
let row = document.createXULElement('attachment-row');
|
||||
this._updateRowAttributes(row, attachment);
|
||||
// Set open state before adding to dom to prevent animation
|
||||
row.toggleAttribute("open", open);
|
||||
|
||||
let inserted = false;
|
||||
for (let existingRow of this._body.children) {
|
||||
if (Zotero.localeCompare(row.attachmentTitle, existingRow.attachmentTitle) < 0) {
|
||||
continue;
|
||||
}
|
||||
existingRow.before(row);
|
||||
inserted = true;
|
||||
break;
|
||||
let index = this._attachmentIDs.indexOf(attachment.id);
|
||||
if (index < 0 || index >= this._attachments.children.length) {
|
||||
this._attachments.append(row);
|
||||
}
|
||||
if (!inserted) {
|
||||
this._body.append(row);
|
||||
else {
|
||||
this._attachments.insertBefore(row, this._attachments.children[index]);
|
||||
}
|
||||
console.log("attch box addRow", attachment, open, row);
|
||||
return row;
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.usePreview = Zotero.Prefs.get('showAttachmentPreview');
|
||||
|
||||
await this._updateAttachmentIDs();
|
||||
|
||||
let itemAttachments = Zotero.Items.get(this._attachmentIDs);
|
||||
|
||||
this._attachments.querySelectorAll("attachment-row").forEach(e => e.remove());
|
||||
for (let attachment of itemAttachments) {
|
||||
this.addRow(attachment);
|
||||
}
|
||||
this.updateCount();
|
||||
}
|
||||
|
||||
updateCount() {
|
||||
let count = this._item.numAttachments(this._inTrash);
|
||||
this._section.setCount(count);
|
||||
}
|
||||
|
||||
|
||||
async updatePreview() {
|
||||
if (!this.usePreview) {
|
||||
return;
|
||||
}
|
||||
let attachment = await this._item.getBestAttachment();
|
||||
if (!this._preview.hasPreview) {
|
||||
this._preview.setItemAndRender(attachment);
|
||||
return;
|
||||
}
|
||||
this._preview.item = attachment;
|
||||
}
|
||||
|
||||
_handleAdd = (event) => {
|
||||
this._section.open = true;
|
||||
ZoteroPane.updateAddAttachmentMenu(this._addPopup);
|
||||
this._addPopup.openPopup(event.detail.button, 'after_end');
|
||||
};
|
||||
|
||||
_handleTogglePreview = () => {
|
||||
let toOpen = !Zotero.Prefs.get('showAttachmentPreview');
|
||||
Zotero.Prefs.set('showAttachmentPreview', toOpen);
|
||||
this.usePreview = toOpen;
|
||||
let menu = this._section._contextMenu.querySelector('.zotero-menuitem-toggle-preview');
|
||||
menu.dataset.l10nArgs = `{ "type": "${this.usePreview ? "open" : "collapsed"}" }`;
|
||||
|
||||
if (toOpen) {
|
||||
this._preview.render();
|
||||
}
|
||||
};
|
||||
|
||||
_handleContextMenu = () => {
|
||||
let contextMenu = this._section._contextMenu;
|
||||
let menu = document.createXULElement("menuitem");
|
||||
menu.classList.add('menuitem-iconic', 'zotero-menuitem-toggle-preview');
|
||||
menu.setAttribute('data-l10n-id', 'toggle-preview');
|
||||
menu.addEventListener('command', this._handleTogglePreview);
|
||||
menu.dataset.l10nArgs = `{ "type": "${this.usePreview ? "open" : "collapsed"}" }`;
|
||||
contextMenu.append(menu);
|
||||
};
|
||||
|
||||
_updateRowAttributes(row, attachment) {
|
||||
let hidden = !this._inTrash && attachment.deleted;
|
||||
|
@ -166,6 +250,22 @@
|
|||
row.hidden = hidden;
|
||||
row.contextRow = context;
|
||||
}
|
||||
|
||||
async _updateAttachmentIDs() {
|
||||
let sortedAttachmentIDs = [];
|
||||
let allAttachmentIDs = this._item.getAttachments(true);
|
||||
let bestAttachment = await this._item.getBestAttachment();
|
||||
if (bestAttachment) {
|
||||
sortedAttachmentIDs.push(
|
||||
bestAttachment.id,
|
||||
...allAttachmentIDs.filter(id => id && id !== bestAttachment.id)
|
||||
);
|
||||
}
|
||||
else {
|
||||
sortedAttachmentIDs = allAttachmentIDs;
|
||||
}
|
||||
this._attachmentIDs = sortedAttachmentIDs;
|
||||
}
|
||||
}
|
||||
customElements.define("attachments-box", AttachmentsBox);
|
||||
}
|
||||
|
|
|
@ -31,8 +31,6 @@
|
|||
|
||||
_title = null;
|
||||
|
||||
_addButton = null;
|
||||
|
||||
_listenerAdded = false;
|
||||
|
||||
get open() {
|
||||
|
@ -62,6 +60,12 @@
|
|||
// eslint-disable-next-line no-void
|
||||
void getComputedStyle(this).maxHeight; // Force style calculation! Without this the animation doesn't work
|
||||
this.toggleAttribute('open', newOpen);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('toggle'), {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
});
|
||||
|
||||
if (!newOpen && this.ownerDocument?.activeElement && this.contains(this.ownerDocument?.activeElement)) {
|
||||
this.ownerDocument.activeElement.blur();
|
||||
}
|
||||
|
@ -92,20 +96,17 @@
|
|||
this.setAttribute('label', val);
|
||||
}
|
||||
|
||||
get showAdd() {
|
||||
return this.hasAttribute('show-add');
|
||||
}
|
||||
|
||||
set showAdd(val) {
|
||||
this.toggleAttribute('show-add', !!val);
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['open', 'empty', 'label', 'show-add'];
|
||||
return ['open', 'empty', 'label', 'extra-buttons'];
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this.render();
|
||||
attributeChangedCallback(name) {
|
||||
if (name === "extra-buttons") {
|
||||
this._buildExtraButtons();
|
||||
}
|
||||
else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -125,18 +126,7 @@
|
|||
this._title = document.createElement('span');
|
||||
this._title.className = 'title';
|
||||
this._head.append(this._title);
|
||||
|
||||
this._addButton = document.createXULElement('toolbarbutton');
|
||||
this._addButton.className = 'add';
|
||||
this._addButton.addEventListener('command', (event) => {
|
||||
this.dispatchEvent(new CustomEvent('add', {
|
||||
...event,
|
||||
detail: { button: this._addButton },
|
||||
bubbles: false
|
||||
}));
|
||||
});
|
||||
this._head.append(this._addButton);
|
||||
|
||||
|
||||
this._contextMenu = this._buildContextMenu();
|
||||
if (this._contextMenu) {
|
||||
let popupset = document.createXULElement('popupset');
|
||||
|
@ -148,6 +138,8 @@
|
|||
twisty.className = 'twisty';
|
||||
this._head.append(twisty);
|
||||
|
||||
this._buildExtraButtons();
|
||||
|
||||
this.prepend(this._head);
|
||||
|
||||
this._runWithTransitionsDisabled(() => {
|
||||
|
@ -232,6 +224,30 @@
|
|||
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
_buildExtraButtons() {
|
||||
if (!this.initialized) {
|
||||
return;
|
||||
}
|
||||
this.querySelectorAll('.section-custom-button').forEach(elem => elem.remove());
|
||||
let extraButtons = [];
|
||||
let buttonTypes = (this.getAttribute('extra-buttons') || "").split(",");
|
||||
for (let buttonType of buttonTypes) {
|
||||
buttonType = buttonType.trim();
|
||||
if (!buttonType) continue;
|
||||
let button = document.createXULElement('toolbarbutton');
|
||||
button.classList.add(buttonType, 'section-custom-button');
|
||||
button.addEventListener('command', (event) => {
|
||||
this.dispatchEvent(new CustomEvent(buttonType, {
|
||||
...event,
|
||||
detail: { button },
|
||||
bubbles: false
|
||||
}));
|
||||
});
|
||||
extraButtons.push(button);
|
||||
}
|
||||
this._head.querySelector('.twisty').before(...extraButtons);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._head.removeEventListener('click', this._handleClick);
|
||||
|
@ -264,12 +280,12 @@
|
|||
}
|
||||
|
||||
_handleClick = (event) => {
|
||||
if (event.target.closest('.add, menupopup')) return;
|
||||
if (event.target.closest('.section-custom-button, menupopup')) return;
|
||||
this.open = !this.open;
|
||||
};
|
||||
|
||||
_handleKeyDown = (event) => {
|
||||
if (event.target.closest('.add')) return;
|
||||
if (event.target.closest('.section-custom-button')) return;
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
this.open = !this.open;
|
||||
event.preventDefault();
|
||||
|
@ -277,13 +293,17 @@
|
|||
};
|
||||
|
||||
_handleContextMenu = (event) => {
|
||||
if (event.target.closest('.add')) return;
|
||||
if (event.target.closest('.section-custom-button')) return;
|
||||
event.preventDefault();
|
||||
this._contextMenu?.openPopupAtScreen(event.screenX, event.screenY, true);
|
||||
};
|
||||
|
||||
_getSidenav() {
|
||||
return this.closest('.zotero-view-item-container')?.querySelector('item-pane-sidenav');
|
||||
// TODO: update this after unifying item pane & context pane
|
||||
return document.querySelector(
|
||||
Zotero_Tabs.selectedType === 'library'
|
||||
? "#zotero-view-item-sidenav"
|
||||
: "#zotero-context-pane-sidenav");
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -291,7 +311,7 @@
|
|||
|
||||
if (!this._listenerAdded && this._head?.nextSibling) {
|
||||
this._head.nextSibling.addEventListener('transitionend', () => {
|
||||
Zotero.debug('Animation done; height is ' + this._head.nextSibling.scrollHeight)
|
||||
Zotero.debug('Animation done; height is ' + this._head.nextSibling.scrollHeight);
|
||||
this.style.setProperty('--open-height', 'auto');
|
||||
});
|
||||
this._listenerAdded = true;
|
||||
|
@ -299,7 +319,6 @@
|
|||
|
||||
this._head.setAttribute('aria-expanded', this.open);
|
||||
this._title.textContent = this.label;
|
||||
this._addButton.hidden = !this.showAdd;
|
||||
}
|
||||
}
|
||||
customElements.define("collapsible-section", CollapsibleSection);
|
||||
|
|
|
@ -31,12 +31,12 @@
|
|||
class ContextNotesList extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<html:div>
|
||||
<collapsible-section data-pane="context-item-notes" show-add="true" class="item-notes">
|
||||
<collapsible-section data-pane="context-item-notes" class="item-notes" extra-buttons="add">
|
||||
<html:div class="body"/>
|
||||
</collapsible-section>
|
||||
</html:div>
|
||||
<html:div>
|
||||
<collapsible-section data-pane="context-all-notes" show-add="true" class="all-notes">
|
||||
<collapsible-section data-pane="context-all-notes" class="all-notes" extra-buttons="add">
|
||||
<html:div class="body"/>
|
||||
</collapsible-section>
|
||||
</html:div>
|
||||
|
|
|
@ -299,6 +299,14 @@
|
|||
if (!(val instanceof Zotero.Item)) {
|
||||
throw new Error("'item' must be a Zotero.Item");
|
||||
}
|
||||
|
||||
if (val?.isRegularItem()) {
|
||||
this.hidden = false;
|
||||
}
|
||||
else {
|
||||
this.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// When changing items, reset truncation of creator list
|
||||
if (!this._item || val.id != this._item.id) {
|
||||
|
|
|
@ -44,6 +44,11 @@
|
|||
data-l10n-id="sidenav-abstract"
|
||||
data-pane="abstract"/>
|
||||
</html:div>
|
||||
<html:div class="pin-wrapper">
|
||||
<toolbarbutton
|
||||
data-l10n-id="sidenav-attachment-preview"
|
||||
data-pane="attachment-preview"/>
|
||||
</html:div>
|
||||
<html:div class="pin-wrapper">
|
||||
<toolbarbutton
|
||||
data-l10n-id="sidenav-attachments"
|
||||
|
@ -54,6 +59,16 @@
|
|||
data-l10n-id="sidenav-notes"
|
||||
data-pane="notes"/>
|
||||
</html:div>
|
||||
<html:div class="pin-wrapper">
|
||||
<toolbarbutton
|
||||
data-l10n-id="sidenav-attachment-info"
|
||||
data-pane="attachment-info"/>
|
||||
</html:div>
|
||||
<html:div class="pin-wrapper">
|
||||
<toolbarbutton
|
||||
data-l10n-id="sidenav-attachment-annotations"
|
||||
data-pane="attachment-annotations"/>
|
||||
</html:div>
|
||||
<html:div class="pin-wrapper">
|
||||
<toolbarbutton
|
||||
data-l10n-id="sidenav-libraries-collections"
|
||||
|
@ -93,7 +108,7 @@
|
|||
|
||||
_contextMenuTarget = null;
|
||||
|
||||
_preserveMinScrollHeightTimeout = null;
|
||||
_disableScrollHandler = false;
|
||||
|
||||
_pendingPane = null;
|
||||
|
||||
|
@ -124,7 +139,10 @@
|
|||
}
|
||||
|
||||
set pinnedPane(val) {
|
||||
this.setAttribute('pinnedPane', val || '');
|
||||
if (!val || !this.getPane(val)) {
|
||||
val = '';
|
||||
}
|
||||
this.setAttribute('pinnedPane', val);
|
||||
if (val) {
|
||||
this._pinnedPaneMinScrollHeight = this._getMinScrollHeightForPane(this.getPane(val));
|
||||
}
|
||||
|
@ -216,14 +234,9 @@
|
|||
// 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' && pane.getBoundingClientRect().top > this._container.getBoundingClientRect().top) {
|
||||
if (this._preserveMinScrollHeightTimeout) {
|
||||
clearTimeout(this._preserveMinScrollHeightTimeout);
|
||||
}
|
||||
this._preserveMinScrollHeightTimeout = setTimeout(() => {
|
||||
this._preserveMinScrollHeightTimeout = null;
|
||||
this._handleContainerScroll();
|
||||
}, 1000);
|
||||
if (behavior == 'smooth') {
|
||||
this._disableScrollHandler = true;
|
||||
this._waitForScroll().then(() => this._disableScrollHandler = false);
|
||||
}
|
||||
pane.scrollIntoView({ block: 'start', behavior });
|
||||
pane.focus();
|
||||
|
@ -246,7 +259,8 @@
|
|||
}
|
||||
|
||||
_handleContainerScroll = () => {
|
||||
if (this._preserveMinScrollHeightTimeout) return;
|
||||
// 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;
|
||||
|
@ -259,6 +273,48 @@
|
|||
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])'));
|
||||
|
|
|
@ -30,7 +30,7 @@ import { getCSSIcon } from 'components/icons';
|
|||
{
|
||||
class LibrariesCollectionsBox extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-libraries-collections" data-pane="libraries-collections">
|
||||
<collapsible-section data-l10n-id="section-libraries-collections" data-pane="libraries-collections" extra-buttons="add">
|
||||
<html:div class="body"/>
|
||||
</collapsible-section>
|
||||
|
||||
|
@ -53,6 +53,13 @@ import { getCSSIcon } from 'components/icons';
|
|||
}
|
||||
|
||||
set item(item) {
|
||||
if (item?.isRegularItem()) {
|
||||
this.hidden = false;
|
||||
}
|
||||
else {
|
||||
this.hidden = true;
|
||||
return;
|
||||
}
|
||||
this._item = item;
|
||||
// Getting linked items is an async process, so start by rendering without them
|
||||
this._linkedItems = [];
|
||||
|
@ -67,6 +74,7 @@ import { getCSSIcon } from 'components/icons';
|
|||
|
||||
set mode(mode) {
|
||||
this._mode = mode;
|
||||
this.setAttribute('mode', mode);
|
||||
this.render();
|
||||
}
|
||||
|
||||
|
@ -248,8 +256,6 @@ import { getCSSIcon } from 'components/icons';
|
|||
this._addObject(collection, item);
|
||||
}
|
||||
}
|
||||
|
||||
this._section.showAdd = this._mode == 'edit';
|
||||
}
|
||||
}
|
||||
customElements.define("libraries-collections-box", LibrariesCollectionsBox);
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
|
||||
let content = document.importNode(this.content, true);
|
||||
this._iframe = content.querySelector('#editor-view');
|
||||
this._iframe.addEventListener('DOMContentLoaded', (event) => {
|
||||
this._iframe.addEventListener('DOMContentLoaded', (_event) => {
|
||||
// For iframes without chrome priviledges, for unknown reasons,
|
||||
// dataTransfer.getData() returns empty value for `drop` event
|
||||
// when dragging something from the outside of Zotero.
|
||||
|
@ -154,6 +154,7 @@
|
|||
return callback();
|
||||
}
|
||||
this._onInitCallback = callback;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
notify = async (event, type, ids, extraData) => {
|
||||
|
@ -219,6 +220,7 @@
|
|||
switch (val) {
|
||||
case 'merge':
|
||||
displayLinks = false;
|
||||
break;
|
||||
case 'view':
|
||||
break;
|
||||
|
||||
|
@ -315,7 +317,7 @@
|
|||
this._iframe.focus();
|
||||
this._editorInstance._iframeWindow.document.querySelector('.toolbar-button-return').focus();
|
||||
}
|
||||
catch(e) {
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
{
|
||||
class NotesBox extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-notes" data-pane="notes">
|
||||
<collapsible-section data-l10n-id="section-notes" data-pane="notes" extra-buttons="addd">
|
||||
<html:div class="body"/>
|
||||
</collapsible-section>
|
||||
`);
|
||||
|
@ -69,7 +69,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
default:
|
||||
throw new Error(`Invalid mode '${val}'`);
|
||||
}
|
||||
|
||||
this.setAttribute('mode', val);
|
||||
this._mode = val;
|
||||
}
|
||||
|
||||
|
@ -78,6 +78,13 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
}
|
||||
|
||||
set item(val) {
|
||||
if (val?.isRegularItem()) {
|
||||
this.hidden = false;
|
||||
}
|
||||
else {
|
||||
this.hidden = true;
|
||||
return;
|
||||
}
|
||||
this._item = val;
|
||||
this._refresh();
|
||||
}
|
||||
|
@ -131,7 +138,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
}
|
||||
|
||||
let count = this._noteIDs.length;
|
||||
this._section.showAdd = this._mode == 'edit';
|
||||
this._section.setCount(count);
|
||||
}
|
||||
|
||||
|
|
|
@ -149,7 +149,9 @@
|
|||
this.titleField.initialValue = '';
|
||||
}
|
||||
this.titleField.readOnly = this._mode == 'view';
|
||||
this.titleField.placeholder = Zotero.ItemFields.getLocalizedString(this._titleFieldID);
|
||||
if (this._titleFieldID) {
|
||||
this.titleField.placeholder = Zotero.ItemFields.getLocalizedString(this._titleFieldID);
|
||||
}
|
||||
this.menuButton.hidden = !this.item.isRegularItem() && !this.item.isAttachment();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
{
|
||||
class RelatedBox extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-related" data-pane="related">
|
||||
<collapsible-section data-l10n-id="section-related" data-pane="related" extra-buttons="add">
|
||||
<html:div class="body"/>
|
||||
</collapsible-section>
|
||||
`);
|
||||
|
@ -68,7 +68,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
default:
|
||||
throw new Error(`Invalid mode '${val}'`);
|
||||
}
|
||||
|
||||
this.setAttribute('mode', val);
|
||||
this._mode = val;
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
this.refresh();
|
||||
}
|
||||
|
||||
notify(event, type, ids, extraData) {
|
||||
notify(event, type, ids, _extraData) {
|
||||
if (!this._item || !this._item.id) return;
|
||||
|
||||
// Refresh if this item has been modified
|
||||
|
@ -148,8 +148,6 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
}
|
||||
this._updateCount();
|
||||
}
|
||||
|
||||
this._section.showAdd = this._mode == 'edit';
|
||||
}
|
||||
|
||||
add = async () => {
|
||||
|
@ -225,7 +223,7 @@ import { getCSSItemTypeIcon } from 'components/icons';
|
|||
return this.querySelector(`[id=${id}]`);
|
||||
}
|
||||
|
||||
receiveKeyboardFocus(direction) {
|
||||
receiveKeyboardFocus(_direction) {
|
||||
this._id("addButton").focus();
|
||||
// TODO: the relatedbox is not currently keyboard accessible
|
||||
// so we are ignoring the direction
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
this._item = null;
|
||||
|
||||
this.content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-tags" data-pane="tags">
|
||||
<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"/>
|
||||
<popupset>
|
||||
|
@ -131,7 +131,7 @@
|
|||
default:
|
||||
throw new Error(`Invalid mode ${val}`);
|
||||
}
|
||||
|
||||
this.setAttribute('mode', val);
|
||||
this._mode = val;
|
||||
}
|
||||
|
||||
|
@ -149,17 +149,14 @@
|
|||
}
|
||||
|
||||
notify(event, type, ids, extraData) {
|
||||
if (type == 'setting') {
|
||||
if (ids.some(val => val.split("/")[1] == 'tagColors') && this.item) {
|
||||
this.reload();
|
||||
return;
|
||||
}
|
||||
if (type == 'setting' && ids.some(val => val.split("/")[1] == 'tagColors') && this.item) {
|
||||
this.reload();
|
||||
}
|
||||
else if (type == 'item-tag') {
|
||||
let itemID, tagID;
|
||||
let itemID, _tagID;
|
||||
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
[itemID, tagID] = ids[i].split('-').map(x => parseInt(x));
|
||||
[itemID, _tagID] = ids[i].split('-').map(x => parseInt(x));
|
||||
if (!this.item || itemID != this.item.id) {
|
||||
continue;
|
||||
}
|
||||
|
@ -182,11 +179,8 @@
|
|||
|
||||
this.updateCount();
|
||||
}
|
||||
else if (type == 'tag') {
|
||||
if (event == 'modify') {
|
||||
this.reload();
|
||||
return;
|
||||
}
|
||||
else if (type == 'tag' && event == 'modify') {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,7 +220,6 @@
|
|||
this.addDynamicRow(tags[i], i + 1);
|
||||
}
|
||||
this.updateCount(tags.length);
|
||||
this._section.showAdd = this.editable;
|
||||
|
||||
this._reloading = false;
|
||||
|
||||
|
@ -308,7 +301,7 @@
|
|||
// "-" button
|
||||
if (this.editable) {
|
||||
remove.setAttribute('disabled', false);
|
||||
remove.addEventListener('click', async (event) => {
|
||||
remove.addEventListener('click', async (_event) => {
|
||||
if (tagData) {
|
||||
let item = this.item;
|
||||
this.remove(tagName);
|
||||
|
@ -667,7 +660,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
_handleAddButtonClick = async (event) => {
|
||||
_handleAddButtonClick = async (_event) => {
|
||||
await this.blurOpenField();
|
||||
this.newTag();
|
||||
};
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
var ZoteroItemPane = new function() {
|
||||
var _container;
|
||||
var _header, _sidenav, _scrollParent, _itemBox, _abstractBox, _attachmentsBox, _tagsBox, _notesBox, _librariesCollectionsBox, _relatedBox, _boxes;
|
||||
var _header, _sidenav, _scrollParent, _itemBox, _abstractBox, _attachmentsBox, _attachmentInfoBox, _attachmentPreviewBox, _attachmentAnnotationsBox, _tagsBox, _notesBox, _librariesCollectionsBox, _relatedBox, _boxes;
|
||||
var _deck;
|
||||
var _lastItem;
|
||||
var _selectedNoteID;
|
||||
|
@ -43,11 +43,14 @@ var ZoteroItemPane = new function() {
|
|||
_itemBox = document.getElementById('zotero-editpane-item-box');
|
||||
_abstractBox = document.getElementById('zotero-editpane-abstract');
|
||||
_notesBox = document.getElementById('zotero-editpane-notes');
|
||||
_attachmentPreviewBox = document.getElementById('zotero-editpane-attachment-preview');
|
||||
_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, _librariesCollectionsBox, _tagsBox, _relatedBox];
|
||||
_boxes = [_itemBox, _abstractBox, _notesBox, _attachmentPreviewBox, _attachmentsBox, _attachmentInfoBox, _attachmentAnnotationsBox, _librariesCollectionsBox, _tagsBox, _relatedBox];
|
||||
|
||||
_deck = document.getElementById('zotero-item-pane-content');
|
||||
|
||||
|
@ -114,6 +117,10 @@ var ZoteroItemPane = new function() {
|
|||
box.item = item;
|
||||
box.inTrash = inTrash;
|
||||
}
|
||||
|
||||
if (pinnedPane && !_sidenav.getPane(pinnedPane)) {
|
||||
pinnedPane = "";
|
||||
}
|
||||
|
||||
_scrollParent.style.paddingBottom = '';
|
||||
if (pinnedPane) {
|
||||
|
@ -121,7 +128,7 @@ var ZoteroItemPane = new function() {
|
|||
_sidenav.pinnedPane = pinnedPane;
|
||||
}
|
||||
else if (pinnedPane !== false) {
|
||||
_sidenav.scrollToPane('info', 'instant');
|
||||
_sidenav.scrollToPane(_sidenav.getPanes()[0]?.getAttribute('data-pane'), 'instant');
|
||||
}
|
||||
|
||||
_sidenav.render();
|
||||
|
|
|
@ -26,57 +26,41 @@
|
|||
// Auto-suggester fails without this
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
var noteEditor;
|
||||
var notifierUnregisterID;
|
||||
var type;
|
||||
let noteEditor;
|
||||
let notifierUnregisterID;
|
||||
|
||||
async function onLoad() {
|
||||
if (window.arguments) {
|
||||
var io = window.arguments[0];
|
||||
}
|
||||
|
||||
var itemID = parseInt(io.itemID);
|
||||
var collectionID = parseInt(io.collectionID);
|
||||
var parentItemKey = io.parentItemKey;
|
||||
|
||||
if (itemID) {
|
||||
var ref = await Zotero.Items.getAsync(itemID);
|
||||
var libraryID = ref.libraryID;
|
||||
}
|
||||
else {
|
||||
if (parentItemKey) {
|
||||
var ref = Zotero.Items.getByLibraryAndKey(parentItemKey);
|
||||
var libraryID = ref.libraryID;
|
||||
}
|
||||
else {
|
||||
if (collectionID && collectionID != '' && collectionID != 'undefined') {
|
||||
var collection = Zotero.Collections.get(collectionID);
|
||||
var libraryID = collection.libraryID;
|
||||
}
|
||||
}
|
||||
}
|
||||
type = Zotero.Libraries.get(libraryID).libraryType;
|
||||
let itemID = parseInt(io.itemID);
|
||||
let collectionID = parseInt(io.collectionID);
|
||||
let parentItemKey = io.parentItemKey;
|
||||
let ref;
|
||||
|
||||
noteEditor = document.getElementById('zotero-note-editor');
|
||||
noteEditor.mode = 'edit';
|
||||
noteEditor.viewMode = 'window';
|
||||
|
||||
// Set font size from pref
|
||||
Zotero.UIProperties.registerRoot(noteEditor);
|
||||
|
||||
if (itemID) {
|
||||
var ref = await Zotero.Items.getAsync(itemID);
|
||||
ref = await Zotero.Items.getAsync(itemID);
|
||||
noteEditor.item = ref;
|
||||
document.title = ref.getNoteTitle();
|
||||
// Readonly for attachment notes
|
||||
if (ref.isAttachment()) {
|
||||
noteEditor.mode = 'view';
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (parentItemKey) {
|
||||
var ref = Zotero.Items.getByLibraryAndKey(parentItemKey);
|
||||
ref = Zotero.Items.getByLibraryAndKey(parentItemKey);
|
||||
noteEditor.parentItem = ref;
|
||||
}
|
||||
else {
|
||||
if (collectionID && collectionID != '' && collectionID != 'undefined') {
|
||||
noteEditor.collection = Zotero.Collections.get(collectionID);
|
||||
}
|
||||
else if (collectionID && collectionID != 'undefined') {
|
||||
noteEditor.collection = Zotero.Collections.get(collectionID);
|
||||
}
|
||||
noteEditor.refresh();
|
||||
}
|
||||
|
@ -86,7 +70,7 @@ async function onLoad() {
|
|||
}
|
||||
|
||||
// If there's an error saving a note, close the window and crash the app
|
||||
function onError() {
|
||||
window.onEditorError = function () {
|
||||
try {
|
||||
window.opener.ZoteroPane.displayErrorMessage();
|
||||
}
|
||||
|
@ -94,8 +78,7 @@ function onError() {
|
|||
Zotero.logError(e);
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function onUnload() {
|
||||
Zotero.Notifier.unregisterObserver(notifierUnregisterID);
|
||||
|
@ -103,7 +86,7 @@ function onUnload() {
|
|||
}
|
||||
|
||||
var NotifyCallback = {
|
||||
notify: function(action, type, ids){
|
||||
notify: function (action, type, ids) {
|
||||
if (noteEditor.item && ids.includes(noteEditor.item.id)) {
|
||||
if (action == 'delete') {
|
||||
window.close();
|
||||
|
@ -117,7 +100,7 @@ var NotifyCallback = {
|
|||
window.name = 'zotero-note-' + noteEditor.item.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addEventListener("load", function(e) { onLoad(e); }, false);
|
||||
addEventListener("unload", function(e) { onUnload(e); }, false);
|
||||
addEventListener("load", onLoad, false);
|
||||
addEventListener("unload", onUnload, false);
|
||||
|
|
|
@ -33,5 +33,5 @@
|
|||
</keyset>
|
||||
<command id="cmd_close" oncommand="window.close();"/>
|
||||
|
||||
<note-editor id="zotero-note-editor" flex="1" onerror="return;onError()"/>
|
||||
<note-editor id="zotero-note-editor" flex="1" onerror="return;onEditorError()"/>
|
||||
</window>
|
||||
|
|
|
@ -613,7 +613,7 @@ class ReaderInstance {
|
|||
// this._postMessage({ action: 'focusLastToolbarButton' });
|
||||
}
|
||||
|
||||
tabToolbar(reverse) {
|
||||
tabToolbar(_reverse) {
|
||||
// this._postMessage({ action: 'tabToolbar', reverse });
|
||||
// Avoid toolbar find button being focused for a short moment
|
||||
setTimeout(() => this._iframeWindow.focus());
|
||||
|
@ -848,6 +848,7 @@ class ReaderInstance {
|
|||
}
|
||||
return new this._iframeWindow.Blob([u8arr], { type: mime });
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
_getColorIcon(color, selected) {
|
||||
|
@ -1144,7 +1145,7 @@ class ReaderWindow extends ReaderInstance {
|
|||
'.menu-type-reader.pdf, .menu-type-reader.epub, .menu-type-reader.snapshot'
|
||||
).forEach(el => el.hidden = true);
|
||||
this._window.document.querySelectorAll('.menu-type-reader.' + subtype).forEach(el => el.hidden = false);
|
||||
};
|
||||
}
|
||||
|
||||
close() {
|
||||
this.uninit();
|
||||
|
@ -1211,6 +1212,266 @@ class ReaderWindow extends ReaderInstance {
|
|||
}
|
||||
|
||||
|
||||
class ReaderPreview extends ReaderInstance {
|
||||
// TODO: implement these inside reader after redesign is done there
|
||||
static CSS = {
|
||||
global: `
|
||||
#split-view, .split-view {
|
||||
top: 0 !important;
|
||||
inset-inline-start: 0 !important;
|
||||
}
|
||||
#reader-ui {
|
||||
display: none !important;
|
||||
}`,
|
||||
pdf: `
|
||||
#mainContainer {
|
||||
/* Hide left-side vertical line */
|
||||
margin-inline-start: -1px;
|
||||
}
|
||||
#viewerContainer {
|
||||
overflow: hidden;
|
||||
}
|
||||
.pdfViewer {
|
||||
padding: 6px 0px;
|
||||
}
|
||||
.pdfViewer .page {
|
||||
border-radius: 5px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.pdfViewer .page::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
#viewerContainer {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
.pdfViewer .page::before {
|
||||
box-shadow: inset 0 0 0px 1px #0000001a;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#viewerContainer {
|
||||
background: #303030;
|
||||
}
|
||||
.pdfViewer .page::before {
|
||||
box-shadow: inset 0 0 0px 1px #ffffff1f;
|
||||
}
|
||||
}`,
|
||||
epub: `
|
||||
body.flow-mode-paginated {
|
||||
margin: 8px !important;
|
||||
}
|
||||
body.flow-mode-paginated > .sections {
|
||||
min-height: calc(100vh - 16px);
|
||||
max-height: calc(100vh - 16px);
|
||||
}
|
||||
body.flow-mode-paginated > .sections.spread-mode-odd {
|
||||
column-width: calc(50vw - 16px);
|
||||
}
|
||||
body.flow-mode-paginated replaced-body img, body.flow-mode-paginated replaced-body svg,
|
||||
body.flow-mode-paginated replaced-body audio, body.flow-mode-paginated replaced-body video {
|
||||
max-width: calc(50vw - 16px) !important;
|
||||
max-height: calc(100vh - 16px) !important;
|
||||
}
|
||||
body.flow-mode-paginated replaced-body .table-like {
|
||||
max-height: calc(100vh - 16px);
|
||||
}
|
||||
`,
|
||||
snapshot: `
|
||||
html {
|
||||
pointer-events: none;
|
||||
min-width: 1024px;
|
||||
transform: scale(var(--win-scale));
|
||||
transform-origin: 0 0;
|
||||
overflow-x: hidden;
|
||||
}`
|
||||
};
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this._iframe = options.iframe;
|
||||
this._iframeWindow = this._iframe.contentWindow;
|
||||
this._iframeWindow.addEventListener('error', event => Zotero.logError(event.error));
|
||||
}
|
||||
|
||||
async _open({ state, location, secondViewState }) {
|
||||
let success;
|
||||
try {
|
||||
success = await super._open({ state, location, secondViewState });
|
||||
|
||||
this._injectCSS(this._iframeWindow.document, ReaderPreview.CSS.global);
|
||||
|
||||
let ready = await this._waitForInternalReader();
|
||||
if (!ready) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let win = this._internalReader._primaryView._iframeWindow;
|
||||
if (this._type === "snapshot") {
|
||||
win.addEventListener(
|
||||
"resize", this.updateSnapshotAttr);
|
||||
this.updateSnapshotAttr();
|
||||
}
|
||||
else if (this._type === "pdf") {
|
||||
let viewer = win?.PDFViewerApplication?.pdfViewer;
|
||||
let t = 0;
|
||||
while (!viewer?.firstPagePromise && t < 100) {
|
||||
t++;
|
||||
await Zotero.Promise.delay(10);
|
||||
viewer = win?.PDFViewerApplication?.pdfViewer;
|
||||
}
|
||||
await viewer?.firstPagePromise;
|
||||
win.addEventListener("resize", this.updatePDFAttr);
|
||||
this.updatePDFAttr();
|
||||
}
|
||||
else if (this._type === "epub") {
|
||||
this.updateEPUBAttr();
|
||||
}
|
||||
|
||||
this._injectCSS(
|
||||
win.document,
|
||||
ReaderPreview.CSS[this._type]
|
||||
);
|
||||
|
||||
return success;
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.warn(`Failed to load preview for attachment ${await this._item.getFilePathAsync()}: ${String(e)}`);
|
||||
this._item = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uninit() {
|
||||
if (this._type === "snapshot") {
|
||||
this._internalReader?._primaryView?._iframeWindow.removeEventListener(
|
||||
"resize", this.updateSnapshotAttr);
|
||||
}
|
||||
else if (this._type === "pdf") {
|
||||
this._internalReader?._primaryView?._iframeWindow.removeEventListener(
|
||||
"resize", this.updatePDFAttr);
|
||||
}
|
||||
super.uninit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Goto previous/next page
|
||||
* @param {"prev" | "next"} type goto previous or next page
|
||||
* @returns {void}
|
||||
*/
|
||||
goto(type) {
|
||||
if (type === "prev") {
|
||||
this._internalReader.navigateToPreviousPage();
|
||||
}
|
||||
else {
|
||||
this._internalReader.navigateToNextPage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if can goto previous/next page
|
||||
* @param {"prev" | "next"} type goto previous or next page
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canGoto(type) {
|
||||
if (type === "prev") {
|
||||
return this._internalReader?._state?.primaryViewStats?.canNavigateToPreviousPage;
|
||||
}
|
||||
else {
|
||||
return this._internalReader?._state?.primaryViewStats?.canNavigateToNextPage;
|
||||
}
|
||||
}
|
||||
|
||||
_isReadOnly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async _getState() {
|
||||
if (this._type === "pdf") {
|
||||
return { pageIndex: 0, scale: "page-height", scrollMode: 0, spreadMode: 0 };
|
||||
}
|
||||
else if (this._type === "epub") {
|
||||
return Object.assign(await super._getState(), {
|
||||
scale: 1,
|
||||
flowMode: "paginated",
|
||||
spreadMode: 0
|
||||
});
|
||||
}
|
||||
else if (this._type === "snapshot") {
|
||||
return { scale: 1, scrollYPercent: 0 };
|
||||
}
|
||||
return super._getState();
|
||||
}
|
||||
|
||||
async _setState() {}
|
||||
|
||||
updateTitle() {}
|
||||
|
||||
_injectCSS(doc, content) {
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
let style = doc.createElement("style");
|
||||
style.textContent = content;
|
||||
doc.head.appendChild(style);
|
||||
}
|
||||
|
||||
updateSnapshotAttr = () => {
|
||||
let win = this._internalReader?._primaryView?._iframeWindow;
|
||||
let root = win?.document?.documentElement;
|
||||
root?.style.setProperty('--win-scale', String(this._iframe.getBoundingClientRect().width / 1024));
|
||||
};
|
||||
|
||||
updateEPUBAttr() {
|
||||
let view = this._internalReader?._primaryView;
|
||||
let currentSize = parseFloat(
|
||||
view._iframeWindow?.getComputedStyle(view?._iframeDocument?.documentElement).fontSize);
|
||||
let scale = 12 / currentSize;
|
||||
view?._setScale(scale);
|
||||
}
|
||||
|
||||
updatePDFAttr = () => {
|
||||
this._internalReader._primaryView._iframeWindow.PDFViewerApplication.pdfViewer.currentScaleValue = 'page-height';
|
||||
};
|
||||
|
||||
getPageWidthHeightRatio() {
|
||||
if (this._type !== 'pdf') {
|
||||
return NaN;
|
||||
}
|
||||
try {
|
||||
let viewport = this._internalReader?._primaryView?._iframeWindow
|
||||
?.PDFViewerApplication?.pdfViewer._pages[0].viewport;
|
||||
return viewport?.width / viewport?.height;
|
||||
}
|
||||
catch (e) {
|
||||
return NaN;
|
||||
}
|
||||
}
|
||||
|
||||
async _waitForInternalReader() {
|
||||
let n = 0;
|
||||
try {
|
||||
while (!this._internalReader?._primaryView?._iframeWindow) {
|
||||
if (n >= 500) {
|
||||
return false;
|
||||
}
|
||||
await Zotero.Promise.delay(10);
|
||||
n++;
|
||||
}
|
||||
await this._internalReader._primaryView.initializedPromise;
|
||||
return true;
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Reader {
|
||||
constructor() {
|
||||
this._sidebarWidth = 240;
|
||||
|
@ -1455,7 +1716,7 @@ class Reader {
|
|||
let existingTabID = win.Zotero_Tabs.getTabIDByItemID(itemID);
|
||||
if (existingTabID) {
|
||||
win.Zotero_Tabs.select(existingTabID, false, { location });
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1531,6 +1792,26 @@ class Reader {
|
|||
return reader;
|
||||
}
|
||||
|
||||
async openPreview(itemID, iframe) {
|
||||
let { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID);
|
||||
let library = Zotero.Libraries.get(libraryID);
|
||||
await library.waitForDataLoad('item');
|
||||
|
||||
let item = Zotero.Items.get(itemID);
|
||||
if (!item) {
|
||||
throw new Error('Item does not exist');
|
||||
}
|
||||
|
||||
let reader = new ReaderPreview({
|
||||
item,
|
||||
sidebarWidth: 0,
|
||||
sidebarOpen: false,
|
||||
bottomPlaceholderHeight: 0,
|
||||
iframe,
|
||||
});
|
||||
return reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger annotations import
|
||||
*
|
||||
|
|
|
@ -1815,14 +1815,6 @@ var ZoteroPane = new function()
|
|||
ZoteroItemPane.onNoteSelected(item, this.collectionsView.editable);
|
||||
}
|
||||
|
||||
else if (item.isAttachment()) {
|
||||
var attachmentBox = document.getElementById('zotero-attachment-box');
|
||||
attachmentBox.mode = this.collectionsView.editable ? 'edit' : 'view';
|
||||
attachmentBox.item = item;
|
||||
|
||||
document.getElementById('zotero-item-pane-content').selectedIndex = 3;
|
||||
}
|
||||
|
||||
// Regular item
|
||||
else {
|
||||
var isCommons = collectionTreeRow.isBucket();
|
||||
|
@ -1878,7 +1870,7 @@ var ZoteroPane = new function()
|
|||
this.setItemPaneMessage(msg);
|
||||
}
|
||||
else if (count) {
|
||||
document.getElementById('zotero-item-pane-content').selectedIndex = 4;
|
||||
document.getElementById('zotero-item-pane-content').selectedIndex = 3;
|
||||
|
||||
// Load duplicates UI code
|
||||
if (typeof Zotero_Duplicates_Pane == 'undefined') {
|
||||
|
|
|
@ -1228,9 +1228,15 @@
|
|||
|
||||
<abstract-box id="zotero-editpane-abstract" class="zotero-editpane-abstract" data-pane="abstract"/>
|
||||
|
||||
<attachment-preview-box id="zotero-editpane-attachment-preview" flex="1" data-pane="attachment-preview"/>
|
||||
|
||||
<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"/>
|
||||
|
||||
<attachment-annotations-box id="zotero-editpane-attachment-annotations" flex="1" data-pane="attachment-annotations"/>
|
||||
|
||||
<libraries-collections-box id="zotero-editpane-libraries-collections" class="zotero-editpane-libraries-collections" data-pane="libraries-collections"/>
|
||||
|
||||
|
@ -1251,11 +1257,6 @@
|
|||
previousfocus="zotero-items-tree"/>
|
||||
</groupbox>
|
||||
|
||||
<!-- Attachment item -->
|
||||
<groupbox>
|
||||
<attachment-box id="zotero-attachment-box" flex="1"/>
|
||||
</groupbox>
|
||||
|
||||
<!-- Duplicate merging -->
|
||||
<vbox id="zotero-duplicates-merge-pane">
|
||||
<groupbox>
|
||||
|
|
|
@ -287,6 +287,12 @@ pane-notes = Notes
|
|||
pane-libraries-collections = Libraries and Collections
|
||||
pane-tags = Tags
|
||||
pane-related = Related
|
||||
pane-attachment-info = Attachment Info
|
||||
pane-attachment-preview = Preview
|
||||
pane-attachment-annotations = Annotations
|
||||
|
||||
pane-header-attachment-associated =
|
||||
.label = Rename associated file
|
||||
|
||||
section-info =
|
||||
.label = { pane-info }
|
||||
|
@ -297,6 +303,13 @@ section-attachments =
|
|||
[one] { $count } Attachment
|
||||
*[other] { $count } Attachments
|
||||
}
|
||||
section-attachment-preview =
|
||||
.label = { pane-attachment-preview }
|
||||
section-attachments-annotations =
|
||||
.label = { $count ->
|
||||
[one] { $count } Annotation
|
||||
*[other] { $count } Annotations
|
||||
}
|
||||
section-notes =
|
||||
.label = { $count ->
|
||||
[one] { $count } Note
|
||||
|
@ -311,6 +324,8 @@ section-tags =
|
|||
}
|
||||
section-related =
|
||||
.label = { $count } Related
|
||||
section-attachment-info =
|
||||
.label = { pane-attachment-info }
|
||||
|
||||
sidenav-info =
|
||||
.tooltiptext = { pane-info }
|
||||
|
@ -320,6 +335,12 @@ sidenav-attachments =
|
|||
.tooltiptext = { pane-attachments }
|
||||
sidenav-notes =
|
||||
.tooltiptext = { pane-notes }
|
||||
sidenav-attachment-info =
|
||||
.tooltiptext = { pane-attachment-info }
|
||||
sidenav-attachment-preview =
|
||||
.tooltiptext = { pane-attachment-preview }
|
||||
sidenav-attachment-annotations =
|
||||
.tooltiptext = { pane-attachment-annotations }
|
||||
sidenav-libraries-collections =
|
||||
.tooltiptext = { pane-libraries-collections }
|
||||
sidenav-tags =
|
||||
|
@ -355,3 +376,33 @@ new-collection-dialog =
|
|||
.buttonlabelaccept = Create Collection
|
||||
new-collection-name = Name:
|
||||
new-collection-create-in = Create in:
|
||||
|
||||
attachment-info-filename =
|
||||
.value = Filename
|
||||
attachment-info-accessed =
|
||||
.value = Accessed
|
||||
attachment-info-pages =
|
||||
.value = Pages
|
||||
attachment-info-modified =
|
||||
.value = Modified
|
||||
attachment-info-index =
|
||||
.value = Indexed
|
||||
attachment-info-convert-note =
|
||||
.label = Migrate to {
|
||||
$type ->
|
||||
[standalone] Standalone
|
||||
[child] Item
|
||||
*[unknown] New
|
||||
} Note
|
||||
.tooltiptext = Adding notes to attachments is no longer supported, but you can edit this note
|
||||
by migrating it to a separate note.
|
||||
|
||||
attachment-preview-placeholder = No attachment to preview
|
||||
|
||||
toggle-preview =
|
||||
.label = {
|
||||
$type ->
|
||||
[open] Hide
|
||||
[collapsed] Show
|
||||
*[unknown] Toggle
|
||||
} Attachment Preview
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 3.21655V0.216553H12V9.21655H9V11.7166V12.2166H8.5H4.5H4.29289L4.14645 12.0701L0.146447 8.07011L0 7.92366V7.71655V3.71655V3.21655H0.5H3ZM4 3.21655H8.5H9V3.71655V8.21655H11V1.21655H4V3.21655ZM1 7.21655V4.21655H8V11.2166H5V7.71655V7.21655H4.5H1ZM1.70711 8.21655L4 10.5094V8.21655H1.70711Z" fill="context-fill"/>
|
||||
</svg>
|
After Width: | Height: | Size: 464 B |
11
chrome/skin/default/zotero/16/universal/preview-hide.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5981_57378)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9998 12.8786V5.29289L9.70693 0H2.12126L3.12126 1H8.99982V6H13.9998V11.8786L14.9998 12.8786ZM12.8786 15L13.8786 16H1.99982V12.978C2.31557 13.1801 2.64995 13.3558 2.99982 13.5017V15H12.8786ZM4.06708 6.18846C2.37457 6.64386 1.0048 7.88668 0.374756 9.49991C1.1748 11.5486 3.16783 13 5.49985 13C6.9878 13 8.33775 12.4091 9.32785 11.4492L8.62075 10.7421C7.81124 11.5214 6.71094 12 5.49985 12C3.73435 12 2.20428 10.983 1.4673 9.49993C2.12175 8.1831 3.4015 7.23369 4.91613 7.03751L4.06708 6.18846ZM2.99982 5.1212L1.99982 4.1212V6.0219C2.31557 5.81974 2.64996 5.64413 2.99982 5.49822V5.1212ZM13.2927 5L9.99982 1.70711V5H13.2927ZM6 9.5C6 9.77614 5.77614 10 5.5 10C5.22386 10 5 9.77614 5 9.5C5 9.22386 5.22386 9 5.5 9C5.77614 9 6 9.22386 6 9.5ZM7 9.5C7 10.3284 6.32843 11 5.5 11C4.67157 11 4 10.3284 4 9.5C4 8.67157 4.67157 8 5.5 8C6.32843 8 7 8.67157 7 9.5Z" fill="context-fill"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.293 16L0 0.70706L0.707197 0L16 15.2928L15.293 16Z" fill="context-fill"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5981_57378">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
3
chrome/skin/default/zotero/16/universal/preview-show.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.99982 0H9.70693L14.9998 5.29289V16H1.99982V12.978C2.31557 13.1801 2.64995 13.3558 2.99982 13.5017V15H13.9998V6H8.99982V1H2.99982V5.49822C2.64996 5.64413 2.31557 5.81974 1.99982 6.0219V0ZM9.99982 1.70711L13.2927 5H9.99982V1.70711ZM1.4673 9.49993C2.20428 10.983 3.73435 12 5.49985 12C7.2653 12 8.79532 10.983 9.53233 9.50007C8.79535 8.01705 7.26528 7 5.49978 7C3.73433 7 2.20431 8.01698 1.4673 9.49993ZM10.6249 9.50009C9.82483 7.45137 7.8318 6 5.49978 6C3.16783 6 1.17485 7.45128 0.374756 9.49991C1.1748 11.5486 3.16783 13 5.49985 13C7.8318 13 9.82478 11.5487 10.6249 9.50009ZM5.5 10C5.77614 10 6 9.77614 6 9.5C6 9.22386 5.77614 9 5.5 9C5.22386 9 5 9.22386 5 9.5C5 9.77614 5.22386 10 5.5 10ZM5.5 11C6.32843 11 7 10.3284 7 9.5C7 8.67157 6.32843 8 5.5 8C4.67157 8 4 8.67157 4 9.5C4 10.3284 4.67157 11 5.5 11Z" fill="context-fill"/>
|
||||
</svg>
|
After Width: | Height: | Size: 983 B |
3
chrome/skin/default/zotero/20/universal/arrow-left.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.11621 17L9.00009 16.1161L3.50898 10.625L19 10.625V9.375L3.50898 9.375L9.00009 3.88388L8.11621 3L1.11621 10L8.11621 17Z" fill="context-fill"/>
|
||||
</svg>
|
After Width: | Height: | Size: 297 B |
3
chrome/skin/default/zotero/20/universal/arrow-right.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8839 3L11 3.88388L16.4911 9.375H1V10.625H16.4911L11 16.1161L11.8839 17L18.8839 10L11.8839 3Z" fill="context-fill"/>
|
||||
</svg>
|
After Width: | Height: | Size: 272 B |
|
@ -28,7 +28,7 @@
|
|||
.zotero-view-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-y: scroll;
|
||||
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);
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4088_28048)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 3V0H16V13H13V15.5V16H12.5H6.5H6.29289L6.14645 15.8536L0.146447 9.85355L0 9.70711V9.5V3.5V3H0.5H3ZM4 3H12.5H13V3.5V12H15V1H4V3ZM1 9V4H12V15H7V9.5V9H6.5H1ZM1.70711 10L6 14.2929V10H1.70711Z" fill="context-fill"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4088_28048">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 513 B |
10
chrome/skin/default/zotero/itempane/16/attachment-info.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3830_27858)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8964 2.60356C12.8491 1.55618 11.1509 1.55618 10.1035 2.60356L1.85354 10.8536C1.22037 11.4867 1.22037 12.5133 1.85354 13.1465C2.4867 13.7796 3.51326 13.7796 4.14643 13.1465L7.23656 10.0563C7.08316 10.5095 6.99998 10.995 6.99998 11.5C6.99998 11.5679 7.00149 11.6355 7.00446 11.7026L4.85354 13.8536C3.82985 14.8772 2.17012 14.8772 1.14643 13.8536C0.12274 12.8299 0.12274 11.1701 1.14643 10.1465L9.39643 1.89645C10.8343 0.458549 13.1656 0.458546 14.6035 1.89645C16.0414 3.33435 16.0414 5.66565 14.6035 7.10356L13.9695 7.73756C15.1926 8.54196 16 9.92669 16 11.5C16 13.9853 13.9853 16 11.5 16C9.01472 16 7 13.9853 7 11.5C7 9.5197 8.27917 7.83815 10.0563 7.23658L12.3964 4.89645C12.6154 4.6775 12.6154 4.32251 12.3964 4.10356C12.1775 3.88461 11.8225 3.88461 11.6035 4.10356L4.85354 10.8536C4.65827 11.0488 4.34169 11.0488 4.14643 10.8536C3.95117 10.6583 3.95117 10.3417 4.14643 10.1465L10.8964 3.39645C11.5059 2.78697 12.4941 2.78697 13.1035 3.39645C13.713 4.00593 13.713 4.99408 13.1035 5.60356L11.7026 7.00448C12.1658 7.02501 12.6107 7.11553 13.0271 7.26575L13.8964 6.39645C14.9438 5.34907 14.9438 3.65094 13.8964 2.60356ZM7.14643 13.1465L7.26573 13.0271C7.38331 13.3531 7.53744 13.6615 7.72319 13.9476C7.53602 14.0409 7.30248 14.0096 7.14643 13.8536C6.95117 13.6583 6.95117 13.3417 7.14643 13.1465ZM15 11.5C15 13.433 13.433 15 11.5 15C9.567 15 8 13.433 8 11.5C8 9.567 9.567 8 11.5 8C13.433 8 15 9.567 15 11.5ZM10.875 9.625C10.875 9.27982 11.1548 9 11.5 9C11.8452 9 12.125 9.27982 12.125 9.625C12.125 9.97018 11.8452 10.25 11.5 10.25C11.1548 10.25 10.875 9.97018 10.875 9.625ZM12 14V11H11V14H12Z" fill="context-fill"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3830_27858">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,10 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6067_173)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 0C0.89543 0 0 0.895431 0 2V5H1V2C1 1.44772 1.44772 1 2 1H5V0H2ZM11 1H14C14.5523 1 15 1.44772 15 2V5H16V2C16 0.895431 15.1046 0 14 0H11V1ZM1 14V11H0V14C0 15.1046 0.89543 16 2 16H5V15H2C1.44772 15 1 14.5523 1 14ZM16 14V11H15V14C15 14.5523 14.5523 15 14 15H11V16H14C15.1046 16 16 15.1046 16 14ZM8.70711 2H3V14H13V6.29289L8.70711 2ZM4 13V3H8V7H12V13H4ZM9 6V3.70711L11.2929 6H9Z" fill="context-fill"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6067_173">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 696 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 4.75V1.75H18.25V15H15.25V17.625V18.25H14.625H8.375H8.11612L7.93306 18.0669L1.93306 12.0669L1.75 11.8839V11.625V5.375V4.75H2.375H5ZM6.25 6H5H3V11H8.375H9V11.625V17H14V15V13.75V6H6.25ZM15.25 13.75V5.375V4.75H14.625H6.25V3H17V13.75H15.25ZM3.88388 12.25L7.75 16.1161V12.25H3.88388Z" fill="context-fill"/>
|
||||
</svg>
|
After Width: | Height: | Size: 456 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5581 4.44194C15.1453 3.02916 12.8548 3.02916 11.442 4.44194L2.942 12.9419C2.08151 13.8024 2.08151 15.1976 2.942 16.0581C3.80249 16.9185 5.19762 16.9185 6.05812 16.0581L14.5581 7.55805C14.8663 7.24985 14.8663 6.75014 14.5581 6.44194C14.2499 6.13373 13.7502 6.13373 13.442 6.44194L6.942 12.9419C6.69792 13.186 6.30219 13.186 6.05812 12.9419C5.81404 12.6979 5.81404 12.3021 6.05812 12.0581L12.5581 5.55805C13.3545 4.76169 14.6456 4.76169 15.442 5.55805C16.2384 6.35441 16.2384 7.64557 15.442 8.44194L6.942 16.9419C5.59335 18.2906 3.40676 18.2906 2.05812 16.9419C0.709469 15.5933 0.709469 13.4067 2.05812 12.0581L10.5581 3.55805C12.459 1.65712 15.5411 1.65712 17.442 3.55805C19.3429 5.45898 19.3429 8.541 17.442 10.4419L16.8827 11.0013C16.4101 10.8384 15.9029 10.75 15.3751 10.75L15.375 10.75C17.9293 10.75 20 12.8207 20 15.375C20 17.9293 17.9293 20 15.375 20C13.3486 20 11.6265 18.6967 11.0013 16.8826L10.942 16.9419C10.6979 17.186 10.3022 17.186 10.0581 16.9419C9.81404 16.6979 9.81404 16.3021 10.0581 16.0581L10.75 15.3662C10.7548 12.8174 12.8212 10.7525 15.3704 10.75L15.3662 10.75L16.5581 9.55805C17.9709 8.14528 17.9709 5.85471 16.5581 4.44194ZM18.75 15.375C18.75 17.239 17.239 18.75 15.375 18.75C13.511 18.75 12 17.239 12 15.375C12 13.511 13.511 12 15.375 12C17.239 12 18.75 13.511 18.75 15.375ZM14.625 13.75C14.625 13.3358 14.9608 13 15.375 13C15.7892 13 16.125 13.3358 16.125 13.75C16.125 14.1642 15.7892 14.5 15.375 14.5C14.9608 14.5 14.625 14.1642 14.625 13.75ZM14.75 15V17.75H16V15H14.75Z" fill="context-fill"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 1C2.11929 1 1 2.11929 1 3.5V7H2.25L2.25 3.5C2.25 2.80964 2.80964 2.25 3.5 2.25H7V1H3.5ZM13 2.25H16.5C17.1904 2.25 17.75 2.80964 17.75 3.5V7H19V3.5C19 2.11929 17.8807 1 16.5 1H13V2.25ZM2.25 16.5V13H1V16.5C1 17.8807 2.11929 19 3.5 19H7V17.75H3.5C2.80964 17.75 2.25 17.1904 2.25 16.5ZM19 16.5V13H17.75V16.5C17.75 17.1904 17.1904 17.75 16.5 17.75H13V19H16.5C17.8807 19 19 17.8807 19 16.5ZM11.1339 4H5V16.5H15.5V8.36612L11.1339 4ZM6.25 15.25V5.25H10.25V9.25H14.25V15.25H6.25ZM11.5 8V6.13388L13.3661 8H11.5Z" fill="context-fill"/>
|
||||
</svg>
|
After Width: | Height: | Size: 682 B |
|
@ -184,8 +184,8 @@
|
|||
|
||||
#zotero-item-pane
|
||||
{
|
||||
width: 338px;
|
||||
min-width: 338px;
|
||||
width: 342px;
|
||||
min-width: 342px;
|
||||
}
|
||||
|
||||
#zotero-layout-switcher
|
||||
|
|
|
@ -186,6 +186,7 @@ pref("extensions.zotero.purge.tags", false);
|
|||
|
||||
// Zotero pane persistent data
|
||||
pref("extensions.zotero.pane.persist", "");
|
||||
pref("extensions.zotero.showAttachmentPreview", true);
|
||||
|
||||
pref("extensions.zotero.fileHandler.pdf", "");
|
||||
pref("extensions.zotero.fileHandler.epub", "");
|
||||
|
|
|
@ -57,6 +57,8 @@
|
|||
// --------------------------------------------------
|
||||
|
||||
@import "elements/attachmentBox";
|
||||
@import "elements/attachmentPreview";
|
||||
@import "elements/attachmentPreviewBox";
|
||||
@import "elements/colorPicker";
|
||||
@import "elements/guidancePanel";
|
||||
@import "elements/itemBox";
|
||||
|
@ -75,6 +77,7 @@
|
|||
@import "elements/collapsibleSection";
|
||||
@import "elements/attachmentsBox";
|
||||
@import "elements/attachmentRow";
|
||||
@import "elements/attachmentAnnotationsBox";
|
||||
@import "elements/annotationRow";
|
||||
@import "elements/noteRow";
|
||||
@import "elements/librariesCollectionsBox";
|
||||
|
|
|
@ -74,7 +74,7 @@ $z-index-level: 10;
|
|||
$z-index-level-active: 20;
|
||||
$z-index-navbar: 20;
|
||||
$z-index-menu: 30;
|
||||
$z-index-modal: 40;
|
||||
$z-index-modal: 40;
|
||||
$z-index-drag-layer: 50;
|
||||
$z-index-loading-cover: 60;
|
||||
|
||||
|
@ -85,19 +85,22 @@ $item-pane-sections: (
|
|||
"abstract": var(--accent-azure),
|
||||
"attachments": var(--accent-green),
|
||||
"notes": var(--accent-yellow),
|
||||
"attachment-info": var(--accent-green),
|
||||
"attachment-preview": #926d70,
|
||||
"attachment-annotations": var(--tag-purple),
|
||||
"libraries-collections": var(--accent-teal),
|
||||
"tags": var(--accent-orange),
|
||||
"related": var(--accent-wood),
|
||||
);
|
||||
|
||||
$tagColorsLookup: (
|
||||
'#ff6666': --tag-red,
|
||||
'#ff8c19': --tag-orange,
|
||||
'#999999': --tag-gray,
|
||||
'#5fb236': --tag-green,
|
||||
'#009980': --tag-teal,
|
||||
'#2ea8e5': --tag-blue,
|
||||
'#576dd9': --tag-indigo,
|
||||
'#a28ae5': --tag-purple,
|
||||
'#a6507b': --tag-plum,
|
||||
'#ff6666': --tag-red,
|
||||
'#ff8c19': --tag-orange,
|
||||
'#999999': --tag-gray,
|
||||
'#5fb236': --tag-green,
|
||||
'#009980': --tag-teal,
|
||||
'#2ea8e5': --tag-blue,
|
||||
'#576dd9': --tag-indigo,
|
||||
'#a28ae5': --tag-purple,
|
||||
'#a6507b': --tag-plum,
|
||||
);
|
||||
|
|
|
@ -64,6 +64,14 @@
|
|||
color: var(--fill-secondary);
|
||||
}
|
||||
|
||||
.zotero-clicky-preview-control {
|
||||
@include svgicon-menu("preview-show", "universal", "16");
|
||||
&[data-show-preview] {
|
||||
@include svgicon-menu("preview-hide", "universal", "16");
|
||||
}
|
||||
border: 0px !important;
|
||||
}
|
||||
|
||||
.zotero-clicky-minus[disabled=true], .zotero-clicky-plus[disabled=true] {
|
||||
opacity: .5;
|
||||
}
|
||||
|
|
|
@ -233,6 +233,23 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
border-bottom: var(--material-border-quarternary);
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: calc(1.83333333em - 1px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
|
||||
@include comfortable {
|
||||
top: calc(2.33333333em - 1px);
|
||||
}
|
||||
}
|
||||
|
||||
.column-picker {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
abstract-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
abstract-box .body {
|
||||
|
|
16
scss/elements/_attachmentAnnotationsBox.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
attachment-annotations-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > collapsible-section {
|
||||
& > .body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,71 +1,108 @@
|
|||
attachment-box {
|
||||
#metadata {
|
||||
padding: 5px 2px 2px 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#title
|
||||
{
|
||||
font-weight: bold;
|
||||
/* Don't collapse blank attachment titles, since it prevents renaming */
|
||||
min-height: 1.25em;
|
||||
}
|
||||
|
||||
#metadata > label {
|
||||
margin: 6px 10px 4px !important;
|
||||
}
|
||||
|
||||
#reindex
|
||||
{
|
||||
padding-left: 5px;
|
||||
list-style-image: url(chrome://zotero/skin/arrow_refresh.png);
|
||||
}
|
||||
|
||||
@media (min-resolution: 1.25dppx) {
|
||||
#reindex {
|
||||
list-style-image: url(chrome://zotero/skin/arrow_refresh@2x.png);
|
||||
width: 20px;
|
||||
&:not([data-use-preview]) {
|
||||
attachment-preview {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#linksbox
|
||||
.metadata-table {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
column-gap: 10px;
|
||||
row-gap: 2px;
|
||||
width: inherit;
|
||||
& > .meta-row {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-column: span 2;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: 8px;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > .meta-label {
|
||||
display: flex;
|
||||
font-weight: normal;
|
||||
text-align: end;
|
||||
|
||||
& > label {
|
||||
color: var(--fill-secondary);
|
||||
margin-top: 2px;
|
||||
width: 100%;
|
||||
@include comfortable {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .meta-data {
|
||||
width: 0;
|
||||
min-width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
editable-text {
|
||||
flex: 1; // stretch value field as much as possible
|
||||
max-width: 100%; // stay within .meta-data when the itemBox is narrow
|
||||
.input {
|
||||
// keep input within editable-text when the itemBox is narrow
|
||||
width: calc(100% - 2*var(--editable-text-padding-inline) - 1px)
|
||||
}
|
||||
}
|
||||
|
||||
#index-status {
|
||||
margin-inline: 0;
|
||||
margin-block: 0;
|
||||
height: 22px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
#reindex
|
||||
{
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
padding: 1px;
|
||||
margin-left: 4px;
|
||||
color: var(--fill-secondary);
|
||||
@include svgicon-menu("sync", "universal", "20");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#url
|
||||
{
|
||||
margin-bottom: 4px;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
tr label
|
||||
{
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
#note-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
margin: 4px 0;
|
||||
|
||||
td > label, td > hbox
|
||||
{
|
||||
margin-top: 1px !important;
|
||||
margin-bottom: 1px !important;
|
||||
-moz-box-pack: start;
|
||||
-moz-margin-start: 1px !important;
|
||||
-moz-margin-end: 5px !important;
|
||||
padding: 0 2px 0 2px !important;
|
||||
border-radius: 6px;
|
||||
border: var(--material-border-transparent);
|
||||
}
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td > hbox {
|
||||
-moz-box-align: center;
|
||||
}
|
||||
|
||||
/* Reindex icon makes the row larger */
|
||||
#indexStatusRow > td > hbox {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
-moz-margin-start: 3px !important;
|
||||
-moz-margin-end: 0 !important;
|
||||
width: 62px;
|
||||
text-align: right;
|
||||
font-weight:bold;
|
||||
#attachment-note-editor {
|
||||
margin: 4px 0;
|
||||
display: flex;
|
||||
height: 300px;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
198
scss/elements/_attachmentPreview.scss
Normal file
|
@ -0,0 +1,198 @@
|
|||
attachment-preview {
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transition: height 0.2s ease-in-out, opacity 0.2s ease-in-out;
|
||||
// This is set in JS
|
||||
// Suppose it's A4 size
|
||||
--width-height-ratio: 0.7070707071;
|
||||
--min-height: 56px;
|
||||
--max-height: 600px;
|
||||
// This is set in JS
|
||||
--preview-width: 400;
|
||||
--preview-height: calc(min(var(--preview-width) / var(--width-height-ratio), var(--max-height)));
|
||||
max-height: var(--max-height);
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#preview {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// Make sure minimal height before loading
|
||||
min-height: var(--preview-height);
|
||||
border-radius: 5px;
|
||||
border: var(--material-border-quarternary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#next-preview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
// Force the image to load, otherwise the first dragging will not show a drag image
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
max-height: 1px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
$preview-icons: (
|
||||
file: "document",
|
||||
pdf: "attachment-pdf",
|
||||
snapshot: "attachment-snapshot",
|
||||
epub: "attachment-epub",
|
||||
);
|
||||
|
||||
@each $cls, $icon in $preview-icons {
|
||||
&[data-preview-type="#{$cls}"] {
|
||||
.icon {
|
||||
@include focus-states using ($color) {
|
||||
@include svgicon($icon, $color, "28", "item-type");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
margin-top: -28px;
|
||||
bottom: 6px;
|
||||
|
||||
toolbarbutton {
|
||||
padding: 4px;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
&:active, &[selected] {
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled="true"] {
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-prev {
|
||||
@include svgicon-menu("arrow-left", "universal", "20", false, false, (#fff, #fff));
|
||||
}
|
||||
|
||||
.btn-next {
|
||||
@include svgicon-menu("arrow-right", "universal", "20", false, false, (#fff, #fff));
|
||||
}
|
||||
|
||||
.drag-container {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
position: absolute;
|
||||
top: -10000px;
|
||||
|
||||
.icon {
|
||||
opacity: 1;
|
||||
height: 56px;
|
||||
max-height: 56px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-preview-type=pdf] {
|
||||
#preview {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
// PDF page has 3px vertical padding so we offset 6px more
|
||||
bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-preview-type=snapshot] {
|
||||
#preview {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
$other-preview-types: (
|
||||
image,
|
||||
video,
|
||||
audio,
|
||||
);
|
||||
|
||||
@each $cls in $other-preview-types {
|
||||
##{$cls}-preview {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 5px;
|
||||
border: var(--material-border-quarternary);
|
||||
max-height: var(--preview-height);
|
||||
min-height: var(--min-height);
|
||||
max-width: calc((var(--preview-height) - 2px) * var(--width-height-ratio));
|
||||
}
|
||||
|
||||
&[data-preview-type="#{$cls}"] {
|
||||
#preview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
##{$cls}-preview {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:hover) {
|
||||
.btn-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-preview-status=fail] {
|
||||
#preview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
height: 56px;
|
||||
max-height: 56px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-preview-status=loading] {
|
||||
opacity: 0;
|
||||
#preview {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
30
scss/elements/_attachmentPreviewBox.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
attachment-preview-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#preview-placeholder {
|
||||
display: none;
|
||||
color: var(--fill-secondary);
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
&:not([data-use-preview]) {
|
||||
#preview-placeholder {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
#attachment-preview {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,61 +9,40 @@ attachment-row {
|
|||
|
||||
& > .head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.twisty {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin: 4px;
|
||||
align-self: flex-start;
|
||||
|
||||
@include comfortable {
|
||||
padding-block: 2px;
|
||||
}
|
||||
|
||||
@include svgicon("chevron-8", "universal", "8");
|
||||
fill: var(--fill-secondary);
|
||||
|
||||
transform: rotate(0deg);
|
||||
transform-origin: center;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
|
||||
.clicky-item {
|
||||
@include clicky-item;
|
||||
flex: 1;
|
||||
gap: 4px;
|
||||
padding-inline: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.annotation-btn {
|
||||
flex-grow: 0;
|
||||
flex-basis: content;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
@include svgicon("annotation-12", "universal", "16");
|
||||
color: var(--fill-secondary);
|
||||
padding-block: 4px;
|
||||
}
|
||||
|
||||
&[open]:not([empty]) > .head .twisty {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
&[empty] > .head .twisty {
|
||||
fill: var(--fill-tertiary);
|
||||
}
|
||||
|
||||
&.context > .head .label {
|
||||
color: var(--fill-secondary);
|
||||
}
|
||||
|
||||
& > .body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
max-height: var(--open-height, auto);
|
||||
opacity: 1;
|
||||
transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:not([open]) {
|
||||
& > .body {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
overflow-y: hidden;
|
||||
transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out, visibility 0s 0.2s, overflow-y 0s 0.2s;
|
||||
.label {
|
||||
padding-block: 2px;
|
||||
line-height: 16px;
|
||||
width: 100%;
|
||||
color: var(--fill-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,65 @@
|
|||
attachments-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.head {
|
||||
.togglePreview {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.togglePreview {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-use-preview] {
|
||||
.togglePreview {
|
||||
@include svgicon-menu('preview-hide', 'universal', '16');
|
||||
}
|
||||
}
|
||||
|
||||
&:not([data-use-preview]) {
|
||||
attachment-preview {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.togglePreview {
|
||||
@include svgicon-menu('preview-show', 'universal', '16');
|
||||
}
|
||||
}
|
||||
|
||||
& > collapsible-section > .body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
attachment-preview {
|
||||
opacity: 1;
|
||||
padding: 2px 0px 4px 0px;
|
||||
|
||||
&[data-preview-status=fail] {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
|
||||
.icon {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-preview-status=loading] {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.attachments-container {
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,11 +32,11 @@ collapsible-section {
|
|||
height: 20px;
|
||||
padding: 2px;
|
||||
color: var(--fill-secondary);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
toolbarbutton.add {
|
||||
@include svgicon-menu("plus", "universal", "16");
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background: var(--fill-quinary);
|
||||
|
|
|
@ -4,6 +4,10 @@ item-box {
|
|||
min-width: 0;
|
||||
width: 100%;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#item-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -69,6 +73,11 @@ item-box {
|
|||
// needed to have the outline appear on all platforms
|
||||
-moz-appearance: none;
|
||||
align-self: center;
|
||||
// Make all buttons tigher to not stretch the rows
|
||||
height: auto;
|
||||
width: auto;
|
||||
padding: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
libraries-collections-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
|
@ -42,6 +46,7 @@ libraries-collections-box {
|
|||
toolbarbutton {
|
||||
margin-inline-start: auto;
|
||||
visibility: hidden;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:is(:hover, :focus-within) toolbarbutton {
|
||||
|
@ -49,4 +54,10 @@ libraries-collections-box {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not([mode=edit]) {
|
||||
.add {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,34 +2,44 @@ notes-box, related-box {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
notes-box .body, related-box .body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-inline-start: 16px;
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
&:not([mode=edit]) {
|
||||
.add {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
|
||||
@include comfortable {
|
||||
padding-block: 2px;
|
||||
}
|
||||
|
||||
.box {
|
||||
@include clicky-item;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
toolbarbutton {
|
||||
margin-inline-start: auto;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:is(:hover, :focus-within) toolbarbutton {
|
||||
visibility: visible;
|
||||
flex-direction: column;
|
||||
padding-inline-start: 16px;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
|
||||
@include comfortable {
|
||||
padding-block: 2px;
|
||||
}
|
||||
|
||||
.box {
|
||||
@include clicky-item;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
toolbarbutton {
|
||||
margin-inline-start: auto;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:is(:hover, :focus-within) toolbarbutton {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,74 +1,80 @@
|
|||
tags-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
tags-box .body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding-inline-start: 16px;
|
||||
|
||||
.tags-box-list {
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
margin: 0;
|
||||
padding-inline-start: 16px;
|
||||
|
||||
.tags-box-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 12px 1fr 20px;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 12px 1fr 20px;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
|
||||
// Shift-Enter
|
||||
&.multiline {
|
||||
align-items: start;
|
||||
min-height: 9em;
|
||||
// Shift-Enter
|
||||
&.multiline {
|
||||
align-items: start;
|
||||
min-height: 9em;
|
||||
|
||||
textarea.editable {
|
||||
resize: none;
|
||||
textarea.editable {
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zotero-box-icon {
|
||||
grid-column: 1;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
-moz-context-properties: fill;
|
||||
background: icon-url('tag.svg') center no-repeat;
|
||||
}
|
||||
|
||||
&[tagType="0"] .zotero-box-icon {
|
||||
// User tag: use tag color if we have one, blue accent if we don't
|
||||
fill: var(--tag-color, var(--accent-blue));
|
||||
}
|
||||
|
||||
&[tagType="1"] .zotero-box-icon {
|
||||
// Automatic tag: use tag color if we have one, gray if we don't
|
||||
fill: var(--tag-color, var(--fill-secondary));
|
||||
}
|
||||
|
||||
.zotero-box-label {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
&.has-color {
|
||||
.zotero-box-icon {
|
||||
background-image: icon-url('tag-fill.svg');
|
||||
grid-column: 1;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
-moz-context-properties: fill;
|
||||
background: icon-url('tag.svg') center no-repeat;
|
||||
}
|
||||
|
||||
&[tagType="0"] .zotero-box-icon {
|
||||
// User tag: use tag color if we have one, blue accent if we don't
|
||||
fill: var(--tag-color, var(--accent-blue));
|
||||
}
|
||||
|
||||
&[tagType="1"] .zotero-box-icon {
|
||||
// Automatic tag: use tag color if we have one, gray if we don't
|
||||
fill: var(--tag-color, var(--fill-secondary));
|
||||
}
|
||||
|
||||
.zotero-box-label {
|
||||
font-weight: 590;
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
&.has-color {
|
||||
.zotero-box-icon {
|
||||
background-image: icon-url('tag-fill.svg');
|
||||
}
|
||||
|
||||
.zotero-box-label {
|
||||
font-weight: 590;
|
||||
}
|
||||
}
|
||||
|
||||
toolbarbutton {
|
||||
margin-inline-start: auto;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:is(:hover, :focus-within) toolbarbutton {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toolbarbutton {
|
||||
margin-inline-start: auto;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:is(:hover, :focus-within) toolbarbutton {
|
||||
visibility: visible;
|
||||
&:not([mode=edit]) {
|
||||
.add {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
attachment-box {
|
||||
td:first-child > label
|
||||
{
|
||||
color: #7f7f7f;
|
||||
}
|
||||
}
|
|
@ -11,5 +11,3 @@
|
|||
@import "mac/menupopup";
|
||||
|
||||
// Elements
|
||||
|
||||
@import "mac/elements/attachmentBox";
|
||||
|
|