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
This commit is contained in:
windingwind 2023-12-25 13:51:53 +08:00 committed by Dan Stillman
parent 2a5d713f98
commit 6af4605bd0
59 changed files with 2253 additions and 773 deletions

View file

@ -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,

View file

@ -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;

View file

@ -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);

View file

@ -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() {

View 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);
}

View file

@ -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}`);
}

View 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);
}

View 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);
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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>

View file

@ -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) {

View file

@ -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])'));

View file

@ -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);

View file

@ -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) {
}
}

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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

View file

@ -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();
};

View file

@ -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();

View file

@ -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);

View file

@ -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>

View file

@ -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
*

View file

@ -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') {

View file

@ -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>

View file

@ -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

View file

@ -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

View 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

View 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

View 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

View 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

View file

@ -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);

View 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_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

View 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

View 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_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

View 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="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

View 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="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

View 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="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

View file

@ -184,8 +184,8 @@
#zotero-item-pane
{
width: 338px;
min-width: 338px;
width: 342px;
min-width: 342px;
}
#zotero-layout-switcher

View file

@ -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", "");

View file

@ -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";

View file

@ -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,
);

View file

@ -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;
}

View file

@ -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;
}

View file

@ -1,6 +1,10 @@
abstract-box {
display: flex;
flex-direction: column;
&[hidden] {
display: none;
}
}
abstract-box .body {

View 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;
}
}
}

View file

@ -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;
}
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}
}

View file

@ -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;
}
}
}

View file

@ -1,6 +0,0 @@
attachment-box {
td:first-child > label
{
color: #7f7f7f;
}
}

View file

@ -11,5 +11,3 @@
@import "mac/menupopup";
// Elements
@import "mac/elements/attachmentBox";