Introduce PDF reader and note editor

This commit is contained in:
Martynas Bagdonas 2019-03-28 20:52:22 +02:00 committed by Dan Stillman
parent c3ff6eb66e
commit 2543a695e8
36 changed files with 2512 additions and 419 deletions

12
.gitmodules vendored
View file

@ -29,3 +29,15 @@
[submodule "resource/SingleFile"]
path = resource/SingleFile
url = https://github.com/gildas-lormeau/SingleFile.git
[submodule "pdf-reader"]
path = pdf-reader
url = https://github.com/zotero/pdf-reader.git
branch = master
[submodule "pdf-worker"]
path = pdf-worker
url = https://github.com/zotero/pdf-worker.git
branch = master
[submodule "zotero-note-editor"]
path = zotero-note-editor
url = https://github.com/zotero/zotero-note-editor.git
branch = master

View file

@ -49,6 +49,73 @@
<field name="keyDownHandler"/>
<field name="commandHandler"/>
<field name="clickHandler"/>
<field name="navigateHandler"/>
<constructor><![CDATA[
this._noteEditorID = Zotero.Utilities.randomString();
this._iframe = document.getAnonymousElementByAttribute(this, "anonid", "rt-view1");
this._iframe.addEventListener('DOMContentLoaded', (e) => {
this._initialized = true;
});
this.getNoteDataSync = () => {
return this._editor.getNoteDataSync();
}
this.initEditor = async (state) => {
if (this._editor) {
this._editor.uninit();
}
this._editor = new Zotero.NoteEditor();
await this._editor.init({
state,
item: this._item,
window: document.getAnonymousElementByAttribute(this, "anonid", "rt-view1").contentWindow,
onNavigate: this._navigateHandler,
});
}
this.notify = async (event, type, ids, extraData) => {
// Update citations
let uris = [];
let items = await Zotero.Items.getAsync(ids);
for (let item of items) {
let uri = Zotero.URI.getItemURI(item);
if (uri) {
uris.push(uri)
}
}
if (this._editor) {
await this._editor.updateCitationsForURIs(uris);
}
if (!this.item) return;
// Try to use the state from the item save event
let id = this.item.id;
if (ids.includes(id)) {
let state = extraData && extraData[id] && extraData[id].state;
if (state) {
if (extraData[id].noteEditorID !== this._editor.instanceID) {
this.initEditor(state);
}
}
else {
let curValue = this.item.getNote();
if (curValue !== this._lastHtmlValue) {
this.initEditor();
}
}
this._lastHtmlValue = this.item.getNote();
}
this._id('links-container').hidden = !(this.displayTags && this.displayRelated);
this._id('links-box').refresh();
}
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'noteeditor');
]]></constructor>
<!-- Modes are predefined settings groups for particular tasks -->
<field name="_mode">"view"</field>
@ -65,15 +132,10 @@
switch (val) {
case 'view':
case 'merge':
if (this.noteField) {
this.noteField.onInit(ed => ed.setMode('readonly'));
}
this.editable = false;
break;
case 'edit':
if (this.noteField) {
this.noteField.onInit(ed => ed.setMode('design'));
}
this.editable = true;
this.saveOnEdit = true;
this.parentClickHandler = this.selectParent;
@ -90,6 +152,8 @@
this._mode = val;
document.getAnonymousNodes(this)[0].setAttribute('mode', val);
this._id('links-box').mode = val;
this._id('links-container').hidden = !(this.displayTags && this.displayRelated);
this._id('links-box').refresh();
]]>
</setter>
</property>
@ -103,30 +167,58 @@
</setter>
</property>
<field name="_mtime"/>
<field name="_item"/>
<property name="item" onget="return this._item;">
<setter><![CDATA[
this._item = val;
// TODO: use clientDateModified instead
this._mtime = val.getField('dateModified');
var parentKey = this.item.parentKey;
if (parentKey) {
this.parentItem = Zotero.Items.getByLibraryAndKey(this.item.libraryID, parentKey);
return (async () => {
// `item` field can be set before the constructor is called
// (which happens in the merge dialog i.e.), therefore we wait for
// the initialization
let n = 0;
while (!this._initialized && !this._destroyed) {
if (n >= 1000) {
throw new Error('Waiting for noteeditor initialization failed');
}
await Zotero.Promise.delay(10);
n++;
}
this._id('links-box').item = this.item;
// The binding can also be immediately destrcutred
// (which also happens in the marge dialog)
if (this._destroyed) {
return;
}
this.refresh();
if (!val) this._item = null;
if (this._item && this._item.id === val.id) return;
this._lastHtmlValue = val.getNote();
this._editor = new Zotero.NoteEditor();
this._editor.init({
item: val,
window: document.getAnonymousElementByAttribute(this, "anonid", "rt-view1").contentWindow,
readOnly: !this.editable,
onNavigate: this._navigateHandler
});
this._item = val;
var parentKey = this._item.parentKey;
if (parentKey) {
this.parentItem = Zotero.Items.getByLibraryAndKey(this._item.libraryID, parentKey);
}
this._id('links-box').item = this._item;
})();
]]></setter>
</property>
<property name="linksOnTop">
<setter>
<![CDATA[
if(val) {
return;
if (val) {
var container = this._id('links-container');
var parent = container.parentNode;
var sib = container.nextSibling;
@ -138,182 +230,31 @@
</setter>
</property>
<property name="note"
onget="Zotero.debug('Getting note with .note deprecated -- use .item in zoteronoteeditor'); return this._item"
onset="Zotero.debug('Setting note with .note deprecated -- use .item in zoteronoteeditor'); this.item = val"/>
<property name="ref" onget="return this._item" onset="this.item = val"/>
<property name="navigateHandler">
<setter>
<![CDATA[
if (this._editor) {
this._editor.onNavigate = val;
}
this._navigateHandler = val;
]]>
</setter>
</property>
<field name="collection"/>
<property name="noteField" onget="return this._id('noteField')" readonly="true"/>
<property name="value" onget="return this._id('noteField').value;" onset="this._id('noteField').value = val;"/>
<constructor>
<![CDATA[
this.instanceID = Zotero.Utilities.randomString();
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'noteeditor');
]]>
</constructor>
<destructor>
<![CDATA[
Zotero.Notifier.unregisterObserver(this._notifierID);
this._destroyed = true;
]]>
</destructor>
<method name="notify">
<parameter name="event"/>
<parameter name="type"/>
<parameter name="ids"/>
<parameter name="extraData"/>
<body><![CDATA[
if (event != 'modify' || !this.item || !this.item.id) return;
for (let i = 0; i < ids.length; i++) {
let id = ids[i];
if (id != this.item.id) {
continue;
}
if (extraData && extraData[id] && extraData[id].noteEditorID == this.instanceID) {
//Zotero.debug("Skipping notification from current note field");
continue;
}
if (this.noteField.changed) {
//Zotero.debug("Note has changed since last save -- skipping refresh");
return;
}
this.refresh();
break;
}
]]></body>
</method>
<method name="refresh">
<body><![CDATA[
Zotero.debug('Refreshing note editor');
var textbox = this.noteField;
var textboxReadOnly = this._id('noteFieldReadOnly');
var button = this._id('goButton');
if (this.editable) {
textbox.hidden = false;
textboxReadOnly.hidden = true;
}
else {
textbox.hidden = true;
textboxReadOnly.hidden = false;
textbox = textboxReadOnly;
}
//var scrollPos = textbox.inputField.scrollTop;
if (this.item) {
// For sanity check in save()
textbox.setAttribute('itemID', this.item.id);
textbox.value = this.item.getNote();
}
else {
textbox.value = '';
textbox.removeAttribute('itemID');
}
//textbox.inputField.scrollTop = scrollPos;
this._id('links-container').hidden = !(this.displayTags && this.displayRelated);
this._id('links-box').refresh();
if (this.keyDownHandler) {
textbox.setAttribute('onkeydown',
'document.getBindingParent(this).handleKeyDown(event)');
}
else {
textbox.removeAttribute('onkeydown');
}
if (this.commandHandler) {
textbox.setAttribute('oncommand',
'document.getBindingParent(this).commandHandler()');
}
else {
textbox.removeAttribute('oncommand');
}
if (this.displayButton) {
button.label = this.buttonCaption;
button.hidden = false;
button.setAttribute('oncommand',
'document.getBindingParent(this).clickHandler(this)');
}
else {
button.hidden = true;
}
]]></body>
</method>
<method name="save">
<body><![CDATA[
return Zotero.spawn(function* () {
try {
if (this._mode == 'view') {
Zotero.debug("Not saving read-only note");
return;
}
return (async () => {
var noteField = this._id('noteField');
var value = noteField.value;
if (value === null) {
Zotero.debug("Note value not available -- not saving", 2);
return;
}
// Update note
if (this.item) {
// If note field doesn't match item, abort save and run error handler
if (noteField.getAttribute('itemID') != this.item.id) {
throw new Error("Note field doesn't match current item");
}
let changed = this.item.setNote(value);
if (changed && this.saveOnEdit) {
this.noteField.changed = false;
yield this.item.saveTx({
notifierData: {
noteEditorID: this.instanceID
}
});
}
return;
}
// Create new note
var item = new Zotero.Item('note');
if (this.parentItem) {
item.libraryID = this.parentItem.libraryID;
}
item.setNote(value);
if (this.parentItem) {
item.parentKey = this.parentItem.key;
}
if (this.saveOnEdit) {
var id = yield item.saveTx();
if (!this.parentItem && this.collection) {
this.collection.addItem(id);
}
}
this.item = item;
}
catch (e) {
Zotero.logError(e);
if (this.hasAttribute('onerror')) {
let fn = new Function("", this.getAttribute('onerror'));
fn.call(this)
}
if (this.onError) {
this.onError(e);
}
}
}.bind(this));
})();
]]></body>
</method>
@ -322,28 +263,28 @@
<parameter name="event"/>
<body>
<![CDATA[
var noteField = this._id('noteField');
switch (event.keyCode) {
case 9:
// On Shift-Tab, if focus was moved out of the note, focus the element
// specified in the 'previousfocus' attribute. We check for focus
// because Shift-Tab doesn't and shouldn't move focus out of the note if
// the cursor is in a list.
if (event.shiftKey) {
let id = this.getAttribute('previousfocus');
if (id) {
setTimeout(() => {
if (!noteField.hasFocus()) {
document.getElementById(id).focus();
}
}, 0);
}
return;
}
break;
}
// var noteField = this._id('noteField');
//
// switch (event.keyCode) {
// case 9:
// // On Shift-Tab, if focus was moved out of the note, focus the element
// // specified in the 'previousfocus' attribute. We check for focus
// // because Shift-Tab doesn't and shouldn't move focus out of the note if
// // the cursor is in a list.
// if (event.shiftKey) {
// let id = this.getAttribute('previousfocus');
// if (id) {
// setTimeout(() => {
// if (!noteField.hasFocus()) {
// document.getElementById(id).focus();
// }
// }, 0);
// }
// return;
// }
//
// break;
// }
]]>
</body>
</method>
@ -351,15 +292,13 @@
<method name="focus">
<body>
<![CDATA[
this._id('noteField').focus();
]]>
</body>
</method>
setTimeout(() => {
if (this._iframe && this._iframe.contentWindow) {
this._iframe.focus();
this._editor.focus();
}
<method name="clearUndo">
<body>
<![CDATA[
this._id('noteField').clearUndo();
}, 500);
]]>
</body>
</method>
@ -368,7 +307,7 @@
<parameter name="id"/>
<body>
<![CDATA[
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id)[0];
]]>
</body>
</method>
@ -376,15 +315,18 @@
<content>
<xul:vbox xbl:inherits="flex">
<xul:textbox id="noteField" type="styled" mode="note"
timeout="1000" flex="1" hidden="true"/>
<xul:textbox id="noteFieldReadOnly" type="styled" mode="note"
readonly="true" flex="1" hidden="true"/>
<xul:iframe anonid="rt-view1" flex="1" overflow="auto" style="width: 100%;margin-right: 5px;border: 0"
frameBorder="0" src="resource://zotero/zotero-note-editor/editor.html" type="content"/>
<xul:hbox id="links-container" hidden="true">
<xul:linksbox id="links-box" flex="1" xbl:inherits="notitle"/>
</xul:hbox>
<xul:button id="goButton" hidden="true"/>
<xul:popupset>
<xul:menupopup anonid="editor-menu" id="editor-menu" flex="1">
</xul:menupopup>
</xul:popupset>
</xul:vbox>
</content>
</binding>
@ -511,6 +453,10 @@
var lastWin = window.open();
}
if (lastWin.ZoteroOverlay && !lastWin.ZoteroPane.isShowing()) {
lastWin.ZoteroOverlay.toggleDisplay(true);
}
var zp = lastWin.ZoteroPane;
}
@ -526,7 +472,7 @@
<parameter name="id"/>
<body>
<![CDATA[
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id)[0];
]]>
</body>
</method>
@ -541,15 +487,18 @@
<xul:rows>
<xul:row id="parent-row" hidden="true">
<xul:label id="parentLabel"/>
<xul:label id="parentText" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).parentClick();"/>
<xul:label id="parentText" class="zotero-clicky" crop="end"
onclick="document.getBindingParent(this).parentClick();"/>
</xul:row>
<xul:row>
<xul:label id="relatedLabel"/>
<xul:label id="relatedClick" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).relatedClick();"/>
<xul:label id="relatedClick" class="zotero-clicky" crop="end"
onclick="document.getBindingParent(this).relatedClick();"/>
</xul:row>
<xul:row>
<xul:label id="tagsLabel"/>
<xul:label id="tagsClick" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).tagsClick();"/>
<xul:label id="tagsClick" class="zotero-clicky" crop="end"
onclick="document.getBindingParent(this).tagsClick();"/>
</xul:row>
</xul:rows>
</xul:grid>

View file

@ -26,9 +26,11 @@
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
<?xul-overlay href="chrome://zotero/content/containers/tagSelector.xul"?>
<?xul-overlay href="chrome://zotero/content/containers/tagsBox.xul"?>
<?xul-overlay href="chrome://zotero/content/containers/noteEditor.xul"?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://zotero/content/include.js"></script>
<script src="tagSelectorContainer.js"></script>
<script src="noteEditorContainer.js"></script>
</overlay>

View file

@ -0,0 +1,35 @@
<?xml version="1.0"?>
<!--
***** BEGIN LICENSE BLOCK *****
Copyright © 2017 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
-->
<!DOCTYPE overlay [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd"> %zoteroDTD;
]>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<popupset>
<menupopup anonid="editor-menu" id="editor-menu" flex="1"/>
</popupset>
</overlay>

View file

@ -0,0 +1,594 @@
class NoteEditor {
constructor() {
this.instanceID = Zotero.Utilities.randomString();
Zotero.Notes.editorInstances.push(this);
Zotero.debug('Creating a new editor instance');
}
async init(options) {
this.id = options.item.id;
this.item = options.item;
// this._onNavigate = options.onNavigate;
this.saveOnEdit = true;
this.state = options.state;
this.citations = [];
this.disableSaving = false;
this._readOnly = options.readOnly;
this.window = options.window;
await this.waitForEditor();
// Zotero.Notes.updateURIs(h1);
// Run Cut/Copy/Paste with chrome privileges
this.window.wrappedJSObject.zoteroExecCommand = function (doc, command, ui, value) {
// Is that safe enough?
if (!['cut', 'copy', 'paste'].includes(command)) {
return;
}
return doc.execCommand(command, ui, value);
}
this.window.addEventListener('message', this.listener);
this.quickFormatWindow = null;
let data = this.state ? { state: this.state } : { html: this.item.getNote() };
this.postMessage({
op: 'init', ...data,
libraryId: this.item.libraryID,
key: this.item.key,
readOnly: this._readOnly
});
}
uninit() {
this.window.removeEventListener('message', this.listener);
let index = Zotero.Notes.editorInstances.indexOf(this);
if (index >= 0) {
Zotero.Notes.editorInstances.splice(index, 1);
}
}
async waitForEditor() {
let n = 0;
while (!this.window) {
if (n >= 1000) {
throw new Error('Waiting for editor failed ');
}
await Zotero.Promise.delay(10);
n++;
}
}
postMessage(message) {
this.window.postMessage({ instanceId: this.instanceID, message }, '*');
}
listener = async (e) => {
if (e.data.instanceId !== this.instanceID) {
return;
}
// Zotero.debug('Message received from editor ' + e.data.instanceId + ' ' + this.instanceID + ' ' + e.data.message.op);
let message = e.data.message;
if (message.op === 'getItemData') {
let parent = message.parent;
let item = await Zotero.Items.getAsync(message.itemId);
if (parent && item && item.parentID) {
item = await Zotero.Items.getAsync(item.parentID);
}
if (item) {
let data = {
uri: Zotero.URI.getItemURI(item),
backupText: this.getBackupStr(item)
};
}
}
else if (message.op === 'insertObject') {
let { type, data, pos } = message;
if (type === 'zotero/item') {
let ids = data.split(',').map(id => parseInt(id));
let citations = [];
for (let id of ids) {
let item = await Zotero.Items.getAsync(id);
if (!item) {
continue;
}
citations.push({
citationItems: [{
uri: Zotero.URI.getItemURI(item),
backupText: this.getBackupStr(item)
}],
properties: {}
});
}
this.postMessage({ op: 'insertCitations', citations, pos });
}
else if (type === 'zotero/annotation') {
let annotations = JSON.parse(data);
let list = [];
for (let annotation of annotations) {
let attachmentItem = await Zotero.Items.getAsync(annotation.itemId);
if (!attachmentItem) {
continue;
}
let citationItem = attachmentItem.parentID && await Zotero.Items.getAsync(attachmentItem.parentID) || attachmentItem;
annotation.uri = Zotero.URI.getItemURI(attachmentItem);
let citation = {
citationItems: [{
uri: Zotero.URI.getItemURI(citationItem),
backupText: this.getBackupStr(citationItem),
locator: annotation.pageLabel
}],
properties: {}
};
list.push({ annotation, citation });
}
this.postMessage({ op: 'insertAnnotationsAndCitations', list, pos });
}
}
else if (message.op === 'navigate') {
if (this._onNavigate) {
this._onNavigate(message.uri, { position: message.position });
}
else {
await Zotero.Viewer.openURI(message.uri, { position: message.position });
}
}
else if (message.op === 'openURL') {
var zp = typeof ZoteroPane !== 'undefined' ? ZoteroPane : window.opener.ZoteroPane;
zp.loadURI(message.url);
}
else if (message.op === 'showInLibrary') {
let zp = Zotero.getActiveZoteroPane();
if (zp) {
let item = await Zotero.URI.getURIItem(message.itemURI);
if (item) {
zp.selectItems([item.id]);
let win = Zotero.getMainWindow();
if (win) {
win.focus();
}
}
}
}
else if (message.op === 'update') {
this.save(message.noteData);
}
else if (message.op === 'getFormattedCitations') {
let formattedCitations = await this.getFormattedCitations(message.citations);
for (let newCitation of message.citations) {
if (!this.citations.find(citation => citation.id === newCitation.id)) {
this.citations.push(newCitation);
}
}
this.postMessage({
op: 'setFormattedCitations',
formattedCitations
});
}
else if (message.op === 'quickFormat') {
let id = message.id;
let citation = message.citation;
citation = JSON.parse(JSON.stringify(citation));
let availableCitationItems = [];
for (let citationItem of citation.citationItems) {
let item = await Zotero.URI.getURIItem(citationItem.uri);
if (item) {
availableCitationItems.push({ ...citationItem, id: item.id });
}
}
citation.citationItems = availableCitationItems;
let libraryID = this.item.libraryID;
this.quickFormatDialog(id, citation, [libraryID]);
}
else if (message.op === 'updateImages') {
for (let image of message.added) {
let blob = this.dataURLtoBlob(image.dataUrl);
let imageAttachment = await Zotero.Attachments.importEmbeddedImage({
blob,
parentItemID: this.item.id,
itemKey: image.attachmentKey,
saveOptions: {
notifierData: {
noteEditorID: this.instanceID
}
}
});
}
let attachmentItems = this.item.getAttachments().map(id => Zotero.Items.get(id));
let abandonedItems = attachmentItems.filter(item => !message.all.includes(item.key));
for (let item of abandonedItems) {
await item.eraseTx();
}
}
else if (message.op === 'requestImage') {
let { attachmentKey } = message;
var item = Zotero.Items.getByLibraryAndKey(this.item.libraryID, attachmentKey);
if (!item) return;
let path = await item.getFilePathAsync();
let buf = await OS.File.read(path, {});
buf = new Uint8Array(buf).buffer;
let dataURL = 'data:' + item.attachmentContentType + ';base64,' + this.arrayBufferToBase64(buf);
this.postMessage({
op: 'updateImage',
attachmentKey,
dataUrl: dataURL
});
}
else if (message.op === 'popup') {
this.openPopup(message.x, message.y, message.items);
}
}
openPopup(x, y, items) {
let popup = document.getElementById('editor-menu');
popup.hidePopup();
while (popup.firstChild) {
popup.removeChild(popup.firstChild);
}
for (let item of items) {
let menuitem = document.createElement('menuitem');
menuitem.setAttribute('value', item[0]);
menuitem.setAttribute('label', item[1]);
menuitem.addEventListener('command', () => {
this.postMessage({
op: 'contextMenuAction',
ctxAction: item[0],
payload: item.payload
});
});
popup.appendChild(menuitem);
}
popup.openPopupAtScreen(x, y, true);
}
async save(noteData) {
if (!noteData) return;
let { state, html } = noteData;
if (html === undefined) return;
try {
if (this.disableSaving) {
Zotero.debug('Saving is disabled');
return;
}
if (this._readOnly) {
Zotero.debug('Not saving read-only note');
return;
}
if (html === null) {
Zotero.debug('Note value not available -- not saving', 2);
return;
}
// Update note
if (this.item) {
let changed = this.item.setNote(html);
if (changed && this.saveOnEdit) {
// this.noteField.changed = false;
await this.item.saveTx({
notifierData: {
noteEditorID: this.instanceID,
state
}
});
}
}
else {
// Create a new note
var item = new Zotero.Item('note');
if (this.parentItem) {
item.libraryID = this.parentItem.libraryID;
}
item.setNote(html);
if (this.parentItem) {
item.parentKey = this.parentItem.key;
}
if (this.saveOnEdit) {
var id = await item.saveTx();
if (!this.parentItem && this.collection) {
this.collection.addItem(id);
}
}
}
}
catch (e) {
Zotero.logError(e);
if (this.hasAttribute('onerror')) {
let fn = new Function('', this.getAttribute('onerror'));
fn.call(this)
}
if (this.onError) {
this.onError(e);
}
}
}
focus = () => {
}
getNoteDataSync = () => {
if (!this._readOnly && !this.disableSaving && this.window) {
return this.window.wrappedJSObject.getDataSync();
}
return null;
};
/**
* Builds the string to go inside a bubble
*/
_buildBubbleString(citationItem, str) {
// Locator
if (citationItem.locator) {
if (citationItem.label) {
// TODO localize and use short forms
var label = citationItem.label;
}
else if (/[\-,]/.test(citationItem.locator)) {
var label = 'pp.';
}
else {
var label = 'p.';
}
str += ', ' + label + ' ' + citationItem.locator;
}
// Prefix
if (citationItem.prefix && Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP) {
str = citationItem.prefix
+ (Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP.test(citationItem.prefix) ? ' ' : '')
+ str;
}
// Suffix
if (citationItem.suffix && Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP) {
str += (Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP.test(citationItem.suffix) ? ' ' : '')
+ citationItem.suffix;
}
return str;
}
async updateCitationsForURIs(uris) {
let citations = this.citations
.filter(citation => citation.citationItems
.some(citationItem => uris.includes(citationItem.uri)));
if (citations.length) {
let formattedCitations = await this.getFormattedCitations(citations);
this.postMessage({
op: 'setFormattedCitations',
formattedCitations
});
}
}
getFormattedCitations = async (citations) => {
let formattedCitations = {};
for (let citation of citations) {
formattedCitations[citation.id] = await this.getFormattedCitation(citation);
}
return formattedCitations;
}
getFormattedCitation = async (citation) => {
let formattedItems = [];
for (let citationItem of citation.citationItems) {
let item = await Zotero.URI.getURIItem(citationItem.uri);
if (item && !item.deleted) {
formattedItems.push(this._buildBubbleString(citationItem, this.getBackupStr(item)));
}
else {
let formattedItem = this._buildBubbleString(citationItem, citationItem.backupText);
formattedItem = `<span style="color: red;">${formattedItem}</span>`;
formattedItems.push(formattedItem);
}
}
return formattedItems.join(';');
}
getBackupStr(item) {
var str = item.getField('firstCreator');
// Title, if no creator (getDisplayTitle in order to get case, e-mail, statute which don't have a title field)
if (!str) {
str = Zotero.getString('punctuation.openingQMark') + item.getDisplayTitle() + Zotero.getString('punctuation.closingQMark');
}
// Date
var date = item.getField('date', true, true);
if (date && (date = date.substr(0, 4)) !== '0000') {
str += ', ' + date;
}
return str;
}
arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return self.btoa(binary);
}
dataURLtoBlob(dataurl) {
let parts = dataurl.split(',');
let mime = parts[0].match(/:(.*?);/)[1];
if (parts[0].indexOf('base64') !== -1) {
let bstr = atob(parts[1]);
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new self.Blob([u8arr], { type: mime });
}
return null;
}
quickFormatDialog(id, citationData, filterLibraryIDs) {
let that = this;
let win;
/**
* Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js
*/
let CI = function (citation, sortable, fieldIndexPromise, citationsByItemIDPromise, previewFn) {
this.citation = citation;
this.sortable = sortable;
this.filterLibraryIDs = filterLibraryIDs;
this.disableClassicDialog = true;
}
CI.prototype = {
/**
* Execute a callback with a preview of the given citation
* @return {Promise} A promise resolved with the previewed citation string
*/
preview: function () {
Zotero.debug('CI: preview')
},
/**
* Sort the citationItems within citation (depends on this.citation.properties.unsorted)
* @return {Promise} A promise resolved with the previewed citation string
*/
sort: function () {
Zotero.debug('CI: sort')
return async function () {
};
},
/**
* Accept changes to the citation
* @param {Function} [progressCallback] A callback to be run when progress has changed.
* Receives a number from 0 to 100 indicating current status.
*/
accept: async function (progressCallback) {
Zotero.debug('CI: accept');
if (progressCallback) progressCallback(100);
if (win) {
win.close();
}
let citation = {
citationItems: this.citation.citationItems,
properties: this.citation.properties
}
for (let citationItem of citation.citationItems) {
let itm = await Zotero.Items.getAsync(citationItem.id);
delete citationItem.id;
citationItem.uri = Zotero.URI.getItemURI(itm);
citationItem.backupText = that.getBackupStr(itm);
}
let formattedCitation = await that.getFormattedCitation(citation);
if (this.citation.citationItems.length) {
that.postMessage({
op: 'setCitation',
id, citation, formattedCitation
});
}
},
/**
* Get a list of items used in the current document
* @return {Promise} A promise resolved by the items
*/
getItems: async function () {
Zotero.debug('CI: getItems')
return [];
}
}
let Citation = class {
constructor(citationField, data, noteIndex) {
if (!data) {
data = { citationItems: [], properties: {} };
}
this.citationID = data.citationID;
this.citationItems = data.citationItems;
this.properties = data.properties;
this.properties.noteIndex = noteIndex;
this._field = citationField;
}
/**
* Load citation item data
* @param {Boolean} [promptToReselect=true] - will throw a MissingItemException if false
* @returns {Promise{Number}}
* - Zotero.Integration.NO_ACTION
* - Zotero.Integration.UPDATE
* - Zotero.Integration.REMOVE_CODE
* - Zotero.Integration.DELETE
*/
loadItemData() {
Zotero.debug('Citation: loadItemData');
}
async handleMissingItem(idx) {
Zotero.debug('Citation: handleMissingItem');
}
async prepareForEditing() {
Zotero.debug('Citation: prepareForEditing');
}
toJSON() {
Zotero.debug('Citation: toJSON');
}
/**
* Serializes the citation into CSL code representation
* @returns {string}
*/
serialize() {
Zotero.debug('Citation: serialize');
}
};
if (that.quickFormatWindow) {
that.quickFormatWindow.close();
that.quickFormatWindow = null;
}
let citation = new Citation();
citation.citationItems = citationData.citationItems;
citation.properties = citationData.properties;
let styleID = Zotero.Prefs.get('export.lastStyle');
let locale = Zotero.Prefs.get('export.lastLocale');
let csl = Zotero.Styles.get(styleID).getCiteProc(locale);
var io = new CI(citation, csl.opt.sort_citations);
var allOptions = 'chrome,centerscreen';
// without this, Firefox gets raised with our windows under Compiz
if (Zotero.isLinux) allOptions += ',dialog=no';
// if(options) allOptions += ','+options;
var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised')
? 'popup' : 'alwaysRaised') + ',resizable=false,centerscreen';
win = that.quickFormatWindow = Components.classes['@mozilla.org/embedcomp/window-watcher;1']
.getService(Components.interfaces.nsIWindowWatcher)
.openWindow(null, 'chrome://zotero/content/integration/quickFormat.xul', '', mode, {
wrappedJSObject: io
});
}
}
Zotero.NoteEditor = NoteEditor;

View file

@ -56,6 +56,10 @@ var Zotero_QuickFormat = new function () {
Zotero.debug(`Quick Format received citation:`);
Zotero.debug(JSON.stringify(io.citation.toJSON()));
if (io.disableClassicDialog) {
document.getElementById('classic-view').hidden = true;
}
// Only hide chrome on Windows or Mac
if(Zotero.isMac) {
document.documentElement.setAttribute("drawintitlebar", true);
@ -309,6 +313,10 @@ var Zotero_QuickFormat = new function () {
.forEach(feed => s.addCondition("libraryID", "isNot", feed.libraryID));
s.addCondition("quicksearch-titleCreatorYear", "contains", str);
s.addCondition("itemType", "isNot", "attachment");
if (io.filterLibraryIDs) {
io.filterLibraryIDs.forEach(id => s.addCondition("libraryID", "is", id));
}
haveConditions = true;
}
}

View file

@ -259,25 +259,25 @@ var ZoteroItemPane = new function() {
_selectedNoteID = item.id;
// If an external note window is open for this item, don't show the editor
if (ZoteroPane.findNoteWindow(item.id)) {
this.showNoteWindowMessage();
return;
}
// if (ZoteroPane.findNoteWindow(item.id)) {
// this.showNoteWindowMessage();
// return;
// }
var noteEditor = document.getElementById('zotero-note-editor');
// If loading new or different note, disable undo while we repopulate the text field
// so Undo doesn't end up clearing the field. This also ensures that Undo doesn't
// undo content from another note into the current one.
var clearUndo = noteEditor.item ? noteEditor.item.id != item.id : false;
// var clearUndo = noteEditor.item ? noteEditor.item.id != item.id : false;
noteEditor.mode = editable ? 'edit' : 'view';
noteEditor.parent = null;
noteEditor.item = item;
if (clearUndo) {
noteEditor.clearUndo();
}
// if (clearUndo) {
// noteEditor.clearUndo();
// }
document.getElementById('zotero-view-note-button').hidden = !editable;
document.getElementById('zotero-item-pane-content').selectedIndex = 2;
@ -285,7 +285,7 @@ var ZoteroItemPane = new function() {
this.showNoteWindowMessage = function () {
ZoteroPane.setItemPaneMessage(Zotero.getString('pane.item.notes.editingInWindow'));
// ZoteroPane.setItemPaneMessage(Zotero.getString('pane.item.notes.editingInWindow'));
};

View file

@ -115,7 +115,7 @@
-->
<zoteronoteeditor id="zotero-note-editor" flex="1" notitle="1"
previousfocus="zotero-items-tree"
onerror="ZoteroPane.displayErrorMessage(); this.mode = 'view'"/>
onerror="return;ZoteroPane.displayErrorMessage(); /*this.mode = 'view'*/"/>
<button id="zotero-view-note-button"
label="&zotero.notes.separate;"
oncommand="ZoteroItemPane.openNoteWindow()"/>

View file

@ -79,7 +79,12 @@ function onUnload() {
Zotero.Notifier.unregisterObserver(notifierUnregisterID);
if (noteEditor.item) {
window.opener.ZoteroPane.onNoteWindowClosed(noteEditor.item.id, noteEditor.value);
// noteData will be null if noteEditor current editor instance
// has disabled saving, which might happen at the time of the initial sync
let noteData = JSON.parse(JSON.stringify(noteEditor.getNoteDataSync()));
if (noteData) {
window.opener.ZoteroPane.onNoteWindowClosed(noteEditor.item.id, noteData);
}
}
}
@ -87,9 +92,7 @@ var NotifyCallback = {
notify: function(action, type, ids){
if (noteEditor.item && ids.includes(noteEditor.item.id)) {
var noteTitle = noteEditor.item.getNoteTitle();
if (!document.title && noteTitle != '') {
document.title = noteTitle;
}
// Update the window name (used for focusing) in case this is a new note
window.name = 'zotero-note-' + noteEditor.item.id;

View file

@ -22,5 +22,5 @@
</keyset>
<command id="cmd_close" oncommand="window.close();"/>
<zoteronoteeditor id="zotero-note-editor" flex="1" onerror="onError()"/>
<zoteronoteeditor id="zotero-note-editor" flex="1" onerror="return;onError()"/>
</window>

View file

@ -0,0 +1,66 @@
Components.utils.import('resource://gre/modules/Services.jsm');
function handleDragOver(event) {
if (event.dataTransfer.getData('zotero/item')) {
event.preventDefault();
event.stopPropagation();
}
}
function handleDrop(event) {
let data;
if (!(data = event.dataTransfer.getData('zotero/item'))) {
return;
}
let ids = data.split(',').map(id => parseInt(id));
let item = Zotero.Items.get(ids[0]);
if (!item) {
return;
}
if (item.isNote()) {
event.preventDefault();
event.stopPropagation();
let cover = document.getElementById('zotero-viewer-sidebar-cover');
let container = document.getElementById('zotero-viewer-sidebar-container');
cover.hidden = true;
container.hidden = false;
let editor = document.getElementById('zotero-viewer-editor');
let notebox = document.getElementById('zotero-viewer-note-sidebar');
editor.mode = 'edit';
notebox.hidden = false;
editor.item = item;
}
else if (item.isAttachment() && item.attachmentContentType === 'application/pdf') {
event.preventDefault();
event.stopPropagation();
let iframeWindow = document.getElementById('viewer').contentWindow;
let url = 'zotero://pdf.js/viewer.html?libraryID=' + item.libraryID + '&key=' + item.key;
if (url !== iframeWindow.location.href) {
iframeWindow.location = url;
}
}
else if (item.isRegularItem()) {
let attachments = item.getAttachments();
if (attachments.length === 1) {
let id = attachments[0];
let attachment = Zotero.Items.get(id);
if (attachment.attachmentContentType === 'application/pdf') {
event.preventDefault();
event.stopPropagation();
let iframeWindow = document.getElementById('viewer').contentWindow;
let url = 'zotero://pdf.js/viewer.html?libraryID=' + attachment.libraryID + '&key=' + attachment.key;
if (url !== iframeWindow.location.href) {
iframeWindow.location = url;
}
}
}
}
}
window.addEventListener('dragover', handleDragOver, true);
window.addEventListener('drop', handleDrop, true);

View file

@ -0,0 +1,98 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<?xul-overlay href="chrome://zotero-platform/content/standalone/menuOverlay.xul"?>
<!DOCTYPE window [
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> %globalDTD;
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd"> %zoteroDTD;
]>
<window
windowtype="zotero:viewer"
orient="vertical"
width="1300"
height="800"
persist="screenX screenY width height"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
>
<!-- TODO: Localize -->
<menubar>
<menu label="File">
<menupopup>
<menuitem label="Save As" oncommand="menuCmd('export')"/>
<menuitem label="Print" oncommand="menuCmd('print')"/>
</menupopup>
</menu>
<menu label="View">
<menupopup>
<menuitem label="Switch to Presentation Mode" oncommand="menuCmd('presentationmode')"/>
<menuseparator/>
<menuitem label="Go to First Page" oncommand="menuCmd('firstpage')"/>
<menuitem label="Go to Last Page" oncommand="menuCmd('lastpage')"/>
<menuseparator/>
<menuitem label="Rotate Clockwise" oncommand="menuCmd('rotatecw')"/>
<menuitem label="Rotate Counterclockwise" oncommand="menuCmd('rotateccw')"/>
<menuseparator/>
<menuitem label="Text Selection Tool" oncommand="menuCmd('switchcursortool_select')"/>
<menuitem label="Hand Tool" oncommand="menuCmd('switchcursortool_hand')"/>
<menuseparator/>
<menuitem label="Vertical Scrolling" oncommand="menuCmd('switchscrollmode_vertical')"/>
<menuitem label="Horizontal Scrolling" oncommand="menuCmd('switchscrollmode_horizontal')"/>
<menuitem label="Wrapped Scrolling" oncommand="menuCmd('switchscrollmode_wrapped')"/>
<menuseparator/>
<menuitem label="No Spreads" oncommand="menuCmd('switchspreadmode_none')"/>
<menuitem label="Odd Spreads" oncommand="menuCmd('switchspreadmode_odd')"/>
<menuitem label="Even Spreads" oncommand="menuCmd('switchspreadmode_even')"/>
</menupopup>
</menu>
<menu label="Tools">
<menupopup>
<menuitem label="Remove password (not implemented)" oncommand="menuCmd('remove_password')"/>
</menupopup>
</menu>
</menubar>
<hbox flex="1">
<vbox id="zotero-viewer" flex="3">
<browser tooltip="viewerTooltip" type="content" primary="true" transparent="transparent" src="" id="viewer"
flex="1"/>
<popupset>
<tooltip id="viewerTooltip" onpopupshowing="return fillTooltip(this);"/>
<menupopup id="tagsPopup" ignorekeys="true"
style="min-width: 300px;"
onpopupshown="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'true'); }"
onpopuphidden="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'false'); }">
<tagsbox id="tags" flex="1" mode="edit"/>
</menupopup>
<menupopup id="annotationPopup"/>
<menupopup id="colorPopup"/>
</popupset>
</vbox>
<splitter id="zotero-viewer-splitter"
hidden="true"
resizebefore="closest"
resizeafter="closest"
collapse="after"
orient="horizontal"
zotero-persist="state orient" />
<vbox flex="0" id="zotero-viewer-note-sidebar" width="350" hidden="true">
<vbox id="zotero-viewer-sidebar-cover" flex="1">
<label>Drag a note here…</label>
</vbox>
<vbox id="zotero-viewer-sidebar-container" flex="1" style="overflow:auto;" hidden="true">
<zoteronoteeditor id="zotero-viewer-editor" flex="1" notitle="1"
previousfocus="zotero-items-tree"
onerror="/*this.mode = 'view'*/"
/>
<button id="zotero-view-note-button" label="Close"
oncommand="document.getElementById('zotero-viewer-sidebar-container').hidden = true;document.getElementById('zotero-viewer-sidebar-cover').hidden = false;"/>
</vbox>
</vbox>
</hbox>
<script src="include.js"/>
<script src="viewer.js"/>
</window>

View file

@ -362,8 +362,8 @@ Zotero.Attachments = new function(){
* @param {Object} [params.saveOptions] - Options to pass to Zotero.Item::save()
* @return {Promise<Zotero.Item>}
*/
this.importEmbeddedImage = async function ({ blob, parentItemID, saveOptions }) {
Zotero.debug('Importing annotation image');
this.importEmbeddedImage = async function ({ blob, itemKey, parentItemID, saveOptions }) {
Zotero.debug('Importing note or annotation image');
var contentType = blob.type;
var fileExt;
@ -371,6 +371,12 @@ Zotero.Attachments = new function(){
case 'image/png':
fileExt = 'png';
break;
case 'image/jpeg':
fileExt = 'jpg';
break;
case 'image/jpg':
fileExt = 'jpg';
break;
default:
throw new Error(`Unsupported embedded image content type '${contentType}`);
@ -385,6 +391,11 @@ Zotero.Attachments = new function(){
attachmentItem = new Zotero.Item('attachment');
let { libraryID: parentLibraryID } = Zotero.Items.getLibraryAndKeyFromID(parentItemID);
attachmentItem.libraryID = parentLibraryID;
if (itemKey) {
// Let it fail if the key already exists
attachmentItem.key = itemKey;
await attachmentItem.loadPrimaryData();
}
attachmentItem.parentID = parentItemID;
attachmentItem.attachmentLinkMode = this.LINK_MODE_EMBEDDED_IMAGE;
attachmentItem.attachmentPath = 'storage:' + filename;

View file

@ -1936,7 +1936,7 @@ Zotero.Item.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
Zotero.Item.prototype.isRegularItem = function() {
return !(this.isNote() || this.isAttachment());
return !(this.isNote() || this.isAttachment() || this.isAnnotation());
}

View file

@ -26,6 +26,9 @@
Zotero.Notes = new function() {
this.noteToTitle = noteToTitle;
// Currently active editor instances
this.editorInstances = [];
this.__defineGetter__("MAX_TITLE_LENGTH", function() { return 120; });
this.__defineGetter__("defaultNote", function () { return '<div class="zotero-note znv1"></div>'; });
@ -60,6 +63,46 @@ Zotero.Notes = new function() {
}
return t;
}
/**
* Replaces local URIs for citation and highlight nodes
*
* Must be called just before the initial sync,
* if called later the item version will be increased,
* which might be incovenient for the future (better) notes sync
*
* @param item Note item
* @returns {Promise}
*/
this.updateURIs = async (item) => {
let html = item.getNote();
let num = 0;
// "uri":"http://zotero.org/users/local/(.+?)/items/(.+?)"
let regex = new RegExp(/%22uri%22%3A%22http%3A%2F%2Fzotero.org%2Fusers%2Flocal%2F(.+?)%2Fitems%2F(.+?)%22/g);
html = html.replace(regex, function (m, g1, g2) {
num++;
let libraryID = Zotero.URI.getURILibrary('http://zotero.org/users/local/' + g1);
let libraryURI = Zotero.URI.getLibraryURI(libraryID);
return encodeURIComponent('"uri":"' + libraryURI + '/items/' + g2 + '"');
});
if (num) {
item.setNote(html);
// Cut off saving for each editor instance for this item,
// to make sure none of the editor instances will concurrently
// overwrite our changes
this.editorInstances.forEach(editorInstance => {
if (editorInstance.item.id === item.id) {
editorInstance.disableSaving = true;
}
});
// Although, theoretically, a new editor instance with the old data can still
// be created while asynchronous `item.saveTx` is in progress, but really unlikely
// Observer notification will automatically recreate the affected editor instances
await item.saveTx();
Zotero.debug(`Updated URIs for item ${item.id}: ${num}`);
}
}
}
if (typeof process === 'object' && process + '' === '[object process]'){

View file

@ -330,6 +330,8 @@ Zotero.ItemTreeView.prototype.refresh = Zotero.serial(Zotero.Promise.coroutine(f
let newSearchItems = yield this.collectionTreeRow.getItems();
// TEMP: Hide annotations
newSearchItems = newSearchItems.filter(item => !item.isAnnotation());
// A temporary workaround to make item tree crash less often
newSearchItems = newSearchItems.filter(item => !(item.isAttachment() && item.attachmentLinkMode === Zotero.Attachments.LINK_MODE_EMBEDDED_IMAGE));
// Remove notes and attachments if necessary
if (this.regularOnly) {
newSearchItems = newSearchItems.filter(item => item.isRegularItem());

View file

@ -0,0 +1,168 @@
class PDFExport {
constructor() {
this._queue = [];
this._queueProcessing = false;
this._processingItemID = null;
this._progressQueue = Zotero.ProgressQueues.create({
id: 'pdf-export',
title: 'pdfExport.title',
columns: [
'recognizePDF.pdfName.label',
'pdfImport.annotations.label'
]
});
this._progressQueue.addListener('cancel', () => {
this._queue = [];
});
}
hasAnnotations(item) {
item._loaded.childItems = true;
return item.isAttachment() && item.getAnnotations().length;
}
canExport(item) {
if (this.hasAnnotations(item)) {
return true;
}
else if (item.isRegularItem()) {
let ids = item.getAttachments();
for (let id of ids) {
let attachment = Zotero.Items.get(id);
if (this.hasAnnotations(attachment)) {
return true;
}
}
}
return false;
}
/**
* Triggers queue processing and returns when all items in the queue are processed
* @return {Promise}
*/
async _processQueue() {
// await Zotero.Schema.schemaUpdatePromise;
if (this._queueProcessing) return;
this._queueProcessing = true;
while (1) {
let data = this._queue.pop();
if (!data) break;
let { itemID, path } = data;
this._processingItemID = itemID;
this._progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_PROCESSING, Zotero.getString('general.processing'));
try {
let item = await Zotero.Items.getAsync(itemID);
if (!item) {
throw new Error();
}
let num = await this._exportItemAnnotations(item, path);
this._progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, num);
}
catch (e) {
Zotero.logError(e);
this._progressQueue.updateRow(
itemID,
Zotero.ProgressQueue.ROW_FAILED,
e instanceof Zotero.Exception.Alert
? e.message
: Zotero.getString('general.error')
);
}
}
this._queueProcessing = false;
this._processingItemID = null;
}
/**
* Adds items to the queue and triggers processing
* @param {Zotero.Item[]} items
*/
async export(items) {
let pdfItems = [];
if (!Array.isArray(items)) {
items = [items];
}
for (let item of items) {
if (this.hasAnnotations(item)) {
pdfItems.push(item);
}
else if (item.isRegularItem()) {
let ids = item.getAttachments();
for (let id of ids) {
let attachment = Zotero.Items.get(id);
if (this.hasAnnotations(attachment)) {
pdfItems.push(attachment);
}
}
}
}
for (let item of pdfItems) {
if (
this._processingItemID === item.id ||
this._queue.find(x => x.itemID === item.id)
) {
continue;
}
this._queue.unshift({ itemID: item.id });
this._progressQueue.addRow(item);
}
await this._processQueue();
}
async exportToPath(item, path, isPriority) {
if (isPriority) {
this._queue.push({ itemID: item.id, path });
}
else {
this._queue.unshift({ itemID: item.id, path });
}
this._progressQueue.addRow(item);
await this._processQueue();
}
async _exportItemAnnotations(item, path) {
let ids = item.getAnnotations();
let annotations = [];
for (let id of ids) {
try {
annotations.push(Zotero.Annotations.toJSON(Zotero.Items.get(id)));
} catch (e) {
Zotero.logError(e);
}
}
annotations.id = annotations.key;
// annotations.image = annotations.imageURL;
for (let annotation of annotations) {
delete annotation.key;
for (let key in annotation) {
annotation[key] = annotation[key] || '';
}
annotation.authorName = '';
}
await Zotero.PDFWorker.writeAnnotations(item.id, annotations, path);
return annotations.length;
}
}
Zotero.PDFExport = new PDFExport();

View file

@ -0,0 +1,167 @@
// TODO: Import ToC
class PdfImport {
constructor() {
this._queue = [];
this._queueProcessing = false;
this._processingItemID = null;
this._progressQueue = Zotero.ProgressQueues.create({
id: 'pdf-import',
title: 'pdfImport.title',
columns: [
'recognizePDF.pdfName.label',
'pdfImport.annotations.label'
]
});
this._progressQueue.addListener('cancel', () => {
this._queue = [];
});
}
isPDFAttachment(item) {
return item.isAttachment() && item.attachmentContentType === 'application/pdf';
}
canImport(item) {
if (this.isPDFAttachment(item)) {
return true;
}
else if (item.isRegularItem()) {
let ids = item.getAttachments();
for (let id of ids) {
let attachment = Zotero.Items.get(id);
if (this.isPDFAttachment(attachment)) {
return true;
}
}
}
};
/**
* Triggers queue processing and returns when all items in the queue are processed
* @return {Promise}
*/
async _processQueue() {
// await Zotero.Schema.schemaUpdatePromise;
if (this._queueProcessing) return;
this._queueProcessing = true;
while (1) {
let itemID = this._queue.pop();
if (!itemID) break;
this._processingItemID = itemID;
this._progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_PROCESSING, Zotero.getString('general.processing'));
try {
let item = await Zotero.Items.getAsync(itemID);
if (!item) {
throw new Error();
}
let num = await this._importItemAnnotations(item);
this._progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, num);
}
catch (e) {
Zotero.logError(e);
this._progressQueue.updateRow(
itemID,
Zotero.ProgressQueue.ROW_FAILED,
e instanceof Zotero.Exception.Alert
? e.message
: Zotero.getString('general.error')
);
}
}
this._queueProcessing = false;
this._processingItemID = null;
}
/**
* Adds items to the queue and triggers processing
* @param {Zotero.Item[]} items
*/
async import(items, isPriority) {
let pdfItems = [];
if (!Array.isArray(items)) {
items = [items];
}
for (let item of items) {
if (this.isPDFAttachment(item)) {
pdfItems.push(item);
}
else if (item.isRegularItem()) {
let ids = item.getAttachments();
for (let id of ids) {
let attachment = Zotero.Items.get(id);
if (this.isPDFAttachment(attachment)) {
pdfItems.push(attachment);
}
}
}
}
for (let item of pdfItems) {
if (
this._processingItemID === item.id ||
this._queue.includes(item.id)
) {
continue;
}
this._queue.unshift(item.id);
this._progressQueue.addRow(item);
}
await this._processQueue();
}
similarAnnotions(annotation1, annotation2) {
return (annotation1.position.pageIndex === annotation2.position.pageIndex &&
JSON.stringify(annotation1.position.rects) === JSON.stringify(annotation2.position.rects));
}
async _importItemAnnotations(item) {
if (!item.isAttachment() || item.attachmentContentType !== 'application/pdf') {
throw new Error('Not a valid PDF attachment');
}
// TODO: Remove when fixed
item._loaded.childItems = true;
let ids = item.getAnnotations();
let existingAnnotations = [];
for (let id of ids) {
try {
existingAnnotations.push(Zotero.Annotations.toJSON(Zotero.Items.get(id)));
} catch (e) {
Zotero.logError(e);
}
}
let annotations = await Zotero.PDFWorker.readAnnotations(item.id);
annotations = annotations.filter(x => ['highlight', 'note'].includes(x.type));
let num = 0;
for (let annotation of annotations) {
annotation.comment = annotation.comment || '';
if (existingAnnotations.some(existingAnnotation => this.similarAnnotions(existingAnnotation, annotation))) {
continue;
}
// TODO: Utilize the saved Zotero item key for deduplication
annotation.key = Zotero.DataObjectUtilities.generateKey();
let annotationItem = await Zotero.Annotations.saveFromJSON(item, annotation);
num++;
}
return num;
}
}
Zotero.PDFImport = new PdfImport();

View file

@ -0,0 +1,106 @@
const CMAPS_URL = 'resource://zotero/pdf.js/cmaps/';
class PDFWorker {
constructor() {
this.worker = null;
this.promiseId = 0;
this.waitingPromises = {};
}
async query(op, data, transfer) {
return new Promise((resolve, reject) => {
this.promiseId++;
this.waitingPromises[this.promiseId] = {resolve, reject};
this.worker.postMessage({id: this.promiseId, op, data}, transfer);
});
}
init() {
if (this.worker) return;
this.worker = new Worker('chrome://zotero/content/xpcom/pdfWorker/worker.js');
this.worker.addEventListener('message', async e => {
let message = e.data;
// console.log(e.data)
if (message.responseId) {
let { resolve, reject } = this.waitingPromises[message.responseId];
if (message.data) {
resolve(message.data);
}
else {
reject(message.error);
}
return;
}
if (message.id) {
let respData = null;
try {
if (message.op === 'FetchBuiltInCMap') {
let response = await Zotero.HTTP.request(
"GET",
CMAPS_URL + e.data.data.name + '.bcmap',
{responseType: 'arraybuffer'}
);
respData = {
compressionType: 1,
cMapData: new Uint8Array(response.response)
};
}
}
catch (e) {
Zotero.debug('Failed to fetch CMap data:');
Zotero.debug(e);
}
this.worker.postMessage({responseId: e.data.id, data: respData});
}
});
this.worker.addEventListener('error', e => {
Zotero.debug('PDF Web Worker error:');
Zotero.debug(e);
});
}
async writeAnnotations(itemID, annotations, path) {
Zotero.debug("Writing annotations");
this.init();
let password = '';
let item = await Zotero.Items.getAsync(itemID);
let itemFilePath = await item.getFilePath();
let buf = await OS.File.read(itemFilePath, {});
buf = new Uint8Array(buf).buffer;
let res = await this.query('write', {buf, annotations, password}, [buf]);
if (!path) {
path = itemFilePath;
}
await OS.File.writeAtomic(path, new Uint8Array(res.buf));
}
async readAnnotations(itemID) {
this.init();
let password = '';
let item = await Zotero.Items.getAsync(itemID);
let path = await item.getFilePath();
let buf = await OS.File.read(path, {});
buf = new Uint8Array(buf).buffer;
let res = await this.query('read', {buf, password}, [buf]);
return res.annotations;
}
}
Zotero.PDFWorker = new PDFWorker();

View file

@ -0,0 +1,523 @@
let PDFStates = {};
const COLORS = [
['Red', '#ff6666'],
['Orange', '#ff8c19'],
['Green', '#5fb236'],
['Blue', '#2ea8e5'],
['Purple', '#a28ae5']
];
class ViewerWindow {
constructor() {
this._window = null;
this._iframeWindow = null;
this.popupData = null;
}
dataURLtoBlob(dataurl) {
let parts = dataurl.split(',');
let mime = parts[0].match(/:(.*?);/)[1];
if (parts[0].indexOf('base64') !== -1) {
let bstr = atob(parts[1]);
let n = bstr.length;
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new this._iframeWindow.Blob([u8arr], { type: mime });
}
}
toggleNoteSidebar(isToggled) {
let splitter = this._window.document.getElementById('zotero-viewer-splitter');
let sidebar = this._window.document.getElementById('zotero-viewer-note-sidebar');
if (isToggled) {
splitter.hidden = false;
sidebar.hidden = false;
}
else {
splitter.hidden = true;
sidebar.hidden = true;
}
}
openAnnotationPopup(x, y, annotationId, selectedColor) {
let popup = this._window.document.getElementById('annotationPopup');
popup.hidePopup();
while (popup.firstChild) {
popup.removeChild(popup.firstChild);
}
let menuitem = this._window.document.createElement('menuitem');
menuitem.setAttribute('label', 'Delete');
menuitem.addEventListener('command', () => {
let data = {
action: 'popupCmd',
cmd: 'deleteAnnotation',
id: this.popupData.id
};
this._iframeWindow.postMessage(data, '*');
});
popup.appendChild(menuitem);
popup.appendChild(this._window.document.createElement('menuseparator'));
for (let color of COLORS) {
menuitem = this._window.document.createElement('menuitem');
menuitem.setAttribute('label', color[0]);
menuitem.className = 'menuitem-iconic';
let stroke = color[1] === selectedColor ? 'lightgray' : 'transparent';
let fill = '%23' + color[1].slice(1);
menuitem.setAttribute('image', 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle shape-rendering="geometricPrecision" fill="' + fill + '" stroke-width="2" stroke="' + stroke + '" cx="8" cy="8" r="6"/></svg>');
menuitem.addEventListener('command', () => {
let data = {
action: 'popupCmd',
cmd: 'setAnnotationColor',
id: this.popupData.id,
color: color[1]
};
this._iframeWindow.postMessage(data, '*');
});
popup.appendChild(menuitem);
}
popup.openPopupAtScreen(x, y, true);
}
openColorPopup(x, y, selectedColor) {
let popup = this._window.document.getElementById('colorPopup');
popup.hidePopup();
while (popup.firstChild) {
popup.removeChild(popup.firstChild);
}
let menuitem;
for (let color of COLORS) {
menuitem = this._window.document.createElement('menuitem');
menuitem.setAttribute('label', color[0]);
menuitem.className = 'menuitem-iconic';
let stroke = color[1] === selectedColor ? 'lightgray' : 'transparent';
let fill = '%23' + color[1].slice(1);
menuitem.setAttribute('image', 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle shape-rendering="geometricPrecision" fill="' + fill + '" stroke-width="2" stroke="' + stroke + '" cx="8" cy="8" r="6"/></svg>');
menuitem.addEventListener('command', () => {
let data = {
action: 'popupCmd',
cmd: 'setColor',
color: color[1]
};
this._iframeWindow.postMessage(data, '*');
});
popup.appendChild(menuitem);
}
popup.openPopupAtScreen(x, y, true);
}
init() {
let win = Services.wm.getMostRecentWindow('navigator:browser');
if (!win) return;
this._window = win.open(
'chrome://zotero/content/viewer.xul', '', 'chrome,resizable,centerscreen'
);
this._window.addEventListener('DOMContentLoaded', (e) => {
this._window.fillTooltip = (tooltip) => {
let node = this._window.document.tooltipNode.closest('*[title]');
if (!node) {
return false;
}
tooltip.setAttribute('label', node.getAttribute('title'));
return true;
}
this._window.menuCmd = (cmd) => {
if (cmd === 'export') {
let zp = Zotero.getActiveZoteroPane();
zp.exportPDF(this.itemID);
return;
}
let data = {
action: 'menuCmd',
cmd
};
this._iframeWindow.postMessage(data, '*');
}
let viewerIframe = this._window.document.getElementById('viewer');
if (!(viewerIframe && viewerIframe.contentWindow && viewerIframe.contentWindow.document === e.target)) return;
let that = this;
let editor = this._window.document.getElementById('zotero-viewer-editor');
editor.navigateHandler = async function (uri, annotation) {
let item = await Zotero.URI.getURIItem(uri);
if (!item) return;
that.open(item.id, annotation);
}
this._iframeWindow = this._window.document.getElementById('viewer').contentWindow;
// In the iframe `window.performance` is null which firstly makes React to fail,
// because it expects `undefined` or `object` type, and secondly pdf.js is hardcoded
// to always use performance API
// By using the method below the performance API in the iframe appears not immediately,
// which can cause problems for scipts trying to access it too early
this._iframeWindow.performance = this._window.performance;
this._iframeWindow.addEventListener('message', async (e) => {
// Clone data to avoid the dead object error when the window is closed
let data = JSON.parse(JSON.stringify(e.data));
switch (data.action) {
case 'load':
this.load(data.libraryID, data.key);
break;
case 'setAnnotation':
var item = await Zotero.Items.getAsync(this.itemID);
data.annotation.key = data.annotation.id;
var annotation = await Zotero.Annotations.saveFromJSON(item, data.annotation);
if (data.annotation.image) {
let blob = this.dataURLtoBlob(data.annotation.image);
let attachmentIds = annotation.getAttachments();
if (attachmentIds.length) {
let attachment = Zotero.Items.get(attachmentIds[0]);
var path = await attachment.getFilePathAsync();
await Zotero.File.putContentsAsync(path, blob);
await Zotero.Sync.Storage.Local.updateSyncStates([attachment], 'to_upload');
}
else {
let imageAttachment = await Zotero.Attachments.importEmbeddedImage({
blob,
parentItemID: annotation.id
});
}
}
break;
case 'deleteAnnotations':
for (let id of data.ids) {
let item = Zotero.Items.getByLibraryAndKey(this.libraryID, id);
if (item) {
await Zotero.Items.trashTx([item.id]);
}
}
break;
case 'setState':
PDFStates[this.itemID] = data.state;
break;
case 'openTagsPopup':
var item = Zotero.Items.getByLibraryAndKey(this.libraryID, data.id);
if (item) {
this._window.document.getElementById('tags').item = item;
this._window.document.getElementById('tagsPopup').openPopupAtScreen(data.x, data.y, false);
}
break;
case 'openAnnotationPopup':
this.popupData = data;
this.openAnnotationPopup(data.x, data.y, data.id, data.selectedColor);
break;
case 'openColorPopup':
this.popupData = data;
this.openColorPopup(data.x, data.y, data.selectedColor);
break;
case 'openURL':
let win = Services.wm.getMostRecentWindow('navigator:browser');
if (win) {
win.ZoteroPane.loadURI(data.url);
}
break;
case 'import':
Zotero.debug('Importing PDF annotations');
let item1 = Zotero.Items.get(this.itemID);
Zotero.PDFImport.import(item1);
break;
case 'importDismiss':
Zotero.debug('Dismiss PDF annotations');
break;
case 'save':
Zotero.debug('Exporting PDF');
var zp = Zotero.getActiveZoteroPane();
zp.exportPDF(this.itemID);
break;
case 'toggleNoteSidebar':
this.toggleNoteSidebar(data.isToggled);
break;
}
});
});
return true;
};
async waitForViewer() {
await Zotero.Promise.delay(100);
let n = 0;
while (!this._iframeWindow || !this._iframeWindow.eval('window.isDocumentReady')) {
if (n >= 500) {
throw new Error('Waiting for viewer failed');
}
await Zotero.Promise.delay(100);
n++;
}
};
async waitForViewer2() {
let n = 0;
while (!this._iframeWindow) {
if (n >= 50) {
throw new Error('Waiting for viewer failed');
}
await Zotero.Promise.delay(10);
n++;
}
};
async open(itemID, annotation) {
await this.waitForViewer2();
let item = await Zotero.Items.getAsync(itemID);
if (!item) return;
let url = 'zotero://pdf.js/viewer.html?libraryID=' + item.libraryID + '&key=' + item.key;
if (url !== this._iframeWindow.location.href) {
this._iframeWindow.location = url;
}
this.navigate(annotation);
return true;
};
async load(libraryID, key) {
let item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
if (!item) return;
this.itemID = item.id;
this.libraryID = item.libraryID;
let title = item.getField('title');
let parentItemID = item.parentItemID;
if (parentItemID) {
let parentItem = await Zotero.Items.getAsync(parentItemID);
if (parentItem) {
title = parentItem.getField('title');
}
}
this._window.document.title = title;
Zotero.debug('Annots');
// TODO: Remove when fixed
item._loaded.childItems = true;
let ids = item.getAnnotations();
let annotations = ids.map(id => this.getAnnotation(id)).filter(x => x);
this.annotationIds = ids;
Zotero.debug(annotations);
let state = PDFStates[this.itemID];
let data = {
action: 'open',
libraryID,
key,
itemId: item.itemID,
annotations,
state
};
this._iframeWindow.postMessage(data, '*');
return true;
}
updateTitle() {
let item = Zotero.Items.get(this.itemID);
let title = item.getField('title');
let parentItemID = item.parentItemID;
if (parentItemID) {
let parentItem = Zotero.Items.get(parentItemID);
if (parentItem) {
title = parentItem.getField('title');
}
}
this._window.document.title = title;
}
/**
* Return item JSON in pdf-reader ready format
* @param itemID
* @returns {Object|null}
*/
getAnnotation(itemID) {
try {
let item = Zotero.Items.get(itemID);
if (!item || !item.isAnnotation()) {
return null;
}
item = Zotero.Annotations.toJSON(item);
item.id = item.key;
item.image = item.imageURL;
delete item.key;
for (let key in item) {
item[key] = item[key] || '';
}
item.tags = item.tags || [];
return item;
}
catch (e) {
Zotero.logError(e);
return null;
}
}
setAnnotations(ids) {
Zotero.debug('set annots')
Zotero.debug(ids);
let annotations = [];
for (let id of ids) {
let annotation = this.getAnnotation(id);
if (annotation) {
annotations.push(annotation);
}
}
if (annotations.length) {
let data = { action: 'setAnnotations', annotations };
this._iframeWindow.postMessage(data, '*');
}
}
unsetAnnotations(keys) {
Zotero.debug('unset annots')
Zotero.debug(keys)
let data = { action: 'unsetAnnotations', ids: keys };
this._iframeWindow.postMessage(data, '*');
}
async navigate(annotation) {
if (!annotation) return;
await this.waitForViewer();
// TODO: Wait until the document is loaded
let data = {
action: 'navigate',
annotationId: annotation.id,
position: annotation.position,
to: annotation
};
this._iframeWindow.postMessage(data, '*');
};
close() {
this._window.close();
}
}
class Viewer {
constructor() {
this._viewerWindows = [];
this.instanceID = Zotero.Utilities.randomString();
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'viewer');
}
notify(event, type, ids, extraData) {
// Listen for the parent item, PDF attachment and its annotation items updates
// TODO: Skip events that emerge in the current pdf-reader window
Zotero.debug('notification received')
Zotero.debug(event)
Zotero.debug(type)
Zotero.debug(ids)
Zotero.debug(extraData)
for (let viewerWindow of this._viewerWindows) {
if (event === 'delete') {
let disappearedIds = viewerWindow.annotationIds.filter(x => ids.includes(x));
if (disappearedIds.length) {
let keys = disappearedIds.map(id => extraData[id].itemKey);
viewerWindow.unsetAnnotations(keys);
}
if (ids.includes(viewerWindow.itemID)) {
viewerWindow.close();
}
}
else {
// Check if any annotation is involved
let item = Zotero.Items.get(viewerWindow.itemID);
// TODO: Remove when fixed
item._loaded.childItems = true;
let annotationIds = item.getAnnotations();
viewerWindow.annotationIds = annotationIds;
let affectedAnnotationIds = annotationIds.filter(x => ids.includes(x));
if (affectedAnnotationIds.length) {
viewerWindow.setAnnotations(ids);
}
// Update title if the PDF attachment or the parent item changes
if (ids.includes(viewerWindow.itemID) || ids.includes(item.parentItemID)) {
viewerWindow.updateTitle();
}
}
}
}
_getViewerWindow(itemID) {
return this._viewerWindows.find(v => v.itemID === itemID);
}
async openURI(itemURI, annotation) {
let item = await Zotero.URI.getURIItem(itemURI);
if (!item) return;
this.open(item.id, annotation);
}
async open(itemID, annotation) {
let viewer = this._getViewerWindow(itemID);
if (viewer) {
if (annotation) {
viewer.navigate(annotation);
}
}
else {
viewer = new ViewerWindow();
viewer.init();
if (!(await viewer.open(itemID))) return;
this._viewerWindows.push(viewer);
viewer._window.addEventListener('close', () => {
this._viewerWindows.splice(this._viewerWindows.indexOf(viewer), 1);
});
viewer.navigate(annotation);
}
viewer._window.focus();
}
}
Zotero.Viewer = new Viewer();

View file

@ -2712,6 +2712,8 @@ var ZoteroPane = new function()
'createParent',
'renameAttachments',
'reindexItem',
'importAnnotations',
'exportAnnotations'
];
var m = {};
@ -2758,7 +2760,9 @@ var ZoteroPane = new function()
canIndex = true,
canRecognize = true,
canUnrecognize = true,
canRename = true;
canRename = true,
canImportAnnotations = true,
canExportAnnotations = true;
var canMarkRead = collectionTreeRow.isFeed();
var markUnread = true;
@ -2780,6 +2784,14 @@ var ZoteroPane = new function()
canUnrecognize = false;
}
if (canImportAnnotations && !Zotero.PDFImport.canImport(item)) {
canImportAnnotations = false;
}
if (canExportAnnotations && !Zotero.PDFExport.canExport(item)) {
canExportAnnotations = false;
}
// Show rename option only if all items are child attachments
if (canRename && (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL)) {
canRename = false;
@ -2858,6 +2870,14 @@ var ZoteroPane = new function()
}
}
}
if (canImportAnnotations) {
show.push(m.importAnnotations);
}
if (canExportAnnotations) {
show.push(m.exportAnnotations);
}
}
// Single item selected
@ -2926,6 +2946,14 @@ var ZoteroPane = new function()
else if (!collectionTreeRow.isPublications()) {
show.push(m.duplicateItem);
}
if (Zotero.PDFImport.canImport(item)) {
show.push(m.importAnnotations);
}
if (Zotero.PDFExport.canExport(item)) {
show.push(m.exportAnnotations);
}
}
// Update attachment submenu
@ -3516,10 +3544,19 @@ var ZoteroPane = new function()
};
this.onNoteWindowClosed = async function (itemID, noteText) {
this.onNoteWindowClosed = async function (itemID, noteData) {
var item = Zotero.Items.get(itemID);
item.setNote(noteText);
await item.saveTx();
if (noteData) {
let changed = item.setNote(noteData.html);
if (changed) {
await item.saveTx({
notifierData: {
state: noteData.state
}
});
}
}
// If note is still selected, show the editor again when the note window closes
var selectedItems = this.getSelectedItems(true);
@ -4000,9 +4037,12 @@ var ZoteroPane = new function()
}
}
var launchFile = async function (path, contentType) {
var launchFile = async (path, contentType, itemID) => {
// Custom PDF handler
if (contentType === 'application/pdf') {
this.viewPDF(itemID);
// TODO: Still leave an option to use an external PDF viewer
return;
let pdfHandler = Zotero.Prefs.get("fileHandler.pdf");
if (pdfHandler) {
if (await OS.File.exists(pdfHandler)) {
@ -4058,7 +4098,7 @@ var ZoteroPane = new function()
let iCloudPath = Zotero.File.getEvictedICloudPath(path);
if (await OS.File.exists(iCloudPath)) {
Zotero.debug("Triggering download of iCloud file");
await launchFile(iCloudPath, item.attachmentContentType);
await launchFile(iCloudPath, item.attachmentContentType, itemID);
let time = new Date();
let maxTime = 5000;
let revealed = false;
@ -4118,7 +4158,7 @@ var ZoteroPane = new function()
if (fileExists && !redownload) {
Zotero.debug("Opening " + path);
Zotero.Notifier.trigger('open', 'file', item.id);
launchFile(path, item.attachmentContentType);
launchFile(path, item.attachmentContentType, item.id);
continue;
}
@ -4168,6 +4208,10 @@ var ZoteroPane = new function()
}
});
this.viewPDF = function (itemID) {
Zotero.Viewer.open(itemID);
};
/**
* @deprecated
@ -4482,6 +4526,17 @@ var ZoteroPane = new function()
}
};
this.exportAnnotationsForSelected = async function () {
var items = ZoteroPane.getSelectedItems();
Zotero.PDFExport.export(items);
Zotero.ProgressQueues.get('pdf-export').getDialog().open();
};
this.importAnnotationsForSelected = async function () {
var items = ZoteroPane.getSelectedItems();
Zotero.PDFImport.import(items);
Zotero.ProgressQueues.get('pdf-import').getDialog().open();
};
this.reportMetadataForSelected = async function () {
let items = ZoteroPane.getSelectedItems();
@ -4587,6 +4642,26 @@ var ZoteroPane = new function()
};
this.exportPDF = async function (itemID) {
let item = await Zotero.Items.getAsync(itemID);
if (!item || !item.isAttachment()) {
throw new Error('Item ' + itemID + ' is not attachment');
}
let filename = item.attachmentFilename;
var fp = new FilePicker();
fp.init(window, Zotero.getString('styles.editor.save'), fp.modeSave);
fp.appendFilter("PDF", "*.pdf");
fp.defaultString = filename;
var rv = await fp.show();
if (rv === fp.returnOK || rv === fp.returnReplace) {
let outputFile = fp.file;
Zotero.PDFExport.exportToPath(item, outputFile, true);
}
};
this.renameSelectedAttachmentsFromParents = Zotero.Promise.coroutine(function* () {
// TEMP: fix

View file

@ -290,6 +290,10 @@
<menuitem class="menuitem-iconic zotero-menuitem-create-parent" oncommand="ZoteroPane_Local.createParentItemsFromSelected();"/>
<menuitem class="menuitem-iconic zotero-menuitem-rename-from-parent" oncommand="ZoteroPane_Local.renameSelectedAttachmentsFromParents()"/>
<menuitem class="menuitem-iconic zotero-menuitem-reindex" oncommand="ZoteroPane_Local.reindexItem();"/>
<!-- <menuitem class="menuitem-iconic zotero-menuitem-import-annotations" label="&zotero.items.menu.importAnnotations;" oncommand="ZoteroPane.importAnnotationsForSelected()"/>-->
<menuitem class="menuitem-iconic zotero-menuitem-import-annotations" label="Import annotations" oncommand="ZoteroPane.importAnnotationsForSelected()"/>
<!-- <menuitem class="menuitem-iconic zotero-menuitem-export-annotations" label="&zotero.items.menu.exportAnnotations;" oncommand="ZoteroPane.exportAnnotationsForSelected()"/>-->
<menuitem class="menuitem-iconic zotero-menuitem-export-annotations" label="Export Annotations" oncommand="ZoteroPane.exportAnnotationsForSelected()"/>
</menupopup>
<tooltip id="fake-tooltip"/>

View file

@ -100,6 +100,8 @@
<!ENTITY zotero.items.menu.mergeItems "Merge Items…">
<!ENTITY zotero.items.menu.unrecognize "Undo Retrieve Metadata">
<!ENTITY zotero.items.menu.reportMetadata "Report Incorrect Metadata">
<!ENTITY zotero.items.menu.exportAnnotations "Export Annotations">
<!ENTITY zotero.items.menu.importAnnotations "Import Annotations">
<!ENTITY zotero.duplicatesMerge.versionSelect "Choose the version of the item to use as the master item:">
<!ENTITY zotero.duplicatesMerge.fieldSelect "Select fields to keep from other versions of the item:">

View file

@ -1130,6 +1130,11 @@ recognizePDF.reportMetadata = Report Incorrect Metadata
recognizePDF.pdfName.label = PDF Name
recognizePDF.itemName.label = Item Name
pdfExport.title = PDF Annotations Export
pdfImport.title = PDF Annotations Import
pdfImport.annotations.label = Annotations
rtfScan.openTitle = Select a file to scan
rtfScan.scanning.label = Scanning RTF Document…
rtfScan.saving.label = Formatting RTF Document…

View file

@ -622,6 +622,24 @@
margin-top: 1px;
}
#zotero-tb-pq-pdf-export {
list-style-image: url(chrome://zotero/skin/pdf-search.png);
}
#zotero-tb-pq-pdf-export .toolbarbutton-icon {
width: 18px;
margin-top: 1px;
}
#zotero-tb-pq-pdf-import {
list-style-image: url(chrome://zotero/skin/pdf-search.png);
}
#zotero-tb-pq-pdf-import .toolbarbutton-icon {
width: 18px;
margin-top: 1px;
}
/* Sync error icon */
#zotero-tb-sync-error {
list-style-image: url(chrome://zotero/skin/error.png);

View file

@ -46,6 +46,11 @@ zoteronoteeditor
-moz-binding: url('chrome://zotero/content/bindings/noteeditor.xml#note-editor');
}
zoteronoteeditor2
{
-moz-binding: url('chrome://zotero/content/bindings/noteeditor2.xml#note-editor');
}
linksbox
{
-moz-binding: url('chrome://zotero/content/bindings/noteeditor.xml#links-box');

View file

@ -1038,20 +1038,19 @@ function ZoteroProtocolHandler() {
/*
zotero://pdf.js/web/viewer.html
zotero://pdf.js/viewer.html
zotero://pdf.js/pdf/1/ABCD5678
*/
var PDFJSExtension = {
loadAsChrome: false,
loadAsChrome: true,
newChannel: function (uri) {
return new AsyncChannel(uri, function* () {
try {
uri = uri.spec;
// Proxy PDF.js files
if (uri.startsWith('zotero://pdf.js/web/')
|| uri.startsWith('zotero://pdf.js/build/')) {
uri = uri.replace(/zotero:\/\/pdf.js\//, 'resource://pdf.js/');
if (uri.startsWith('zotero://pdf.js/') && !uri.startsWith('zotero://pdf.js/pdf/')) {
uri = uri.replace(/zotero:\/\/pdf.js\//, 'resource://zotero/pdf.js/');
let newURI = Services.io.newURI(uri, null, null);
return this.getURIInputStream(newURI);
}
@ -1066,7 +1065,7 @@ function ZoteroProtocolHandler() {
var item = yield Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
if (!item) {
return self._errorChannel("Item not found");
return this._errorChannel("Item not found");
}
var path = yield item.getFilePathAsync();
if (!path) {
@ -1260,6 +1259,16 @@ ZoteroProtocolHandler.prototype = {
},
newURI: function (spec, charset, baseURI) {
// A temporary workaround because baseURI.resolve(spec) just returns spec
if (baseURI) {
if (!spec.includes('://') && baseURI.spec.includes('/pdf.js/')) {
let parts = baseURI.spec.split('/');
parts.pop();
parts.push(spec);
spec = parts.join('/');
}
}
return Components.classes["@mozilla.org/network/simple-uri-mutator;1"]
.createInstance(Components.interfaces.nsIURIMutator)
.setSpec(spec)

View file

@ -47,6 +47,7 @@ const xpcomFilesAll = [
'http',
'mimeTypeHandler',
'openurl',
'pdfWorker/transport',
'ipc',
'profile',
'progressWindow',
@ -105,10 +106,13 @@ const xpcomFilesLocal = [
'mime',
'notifier',
'openPDF',
'viewer',
'progressQueue',
'progressQueueDialog',
'quickCopy',
'recognizePDF',
'pdfExport',
'pdfImport',
'report',
'retractions',
'router',

1
pdf-reader Submodule

@ -0,0 +1 @@
Subproject commit cb09ac97385b17bad5ef3cb70daa57fb998b309f

1
pdf-worker Submodule

@ -0,0 +1 @@
Subproject commit b855ed86d8f50261a4b5437d5894d32fe5389a67

View file

@ -5,6 +5,9 @@ const getCopy = require('./copy');
const getJS = require('./js');
const getSass = require('./sass');
const getSymlinks = require('./symlinks');
const getPDFReader = require('./pdf-reader');
const getPDFWorker = require('./pdf-worker');
const getZoteroNoteEditor = require('./zotero-note-editor');
const { formatDirsForMatcher, getSignatures, writeSignatures, cleanUp, onSuccess, onError} = require('./utils');
const { dirs, symlinkDirs, copyDirs, symlinkFiles, jsFiles, scssFiles, ignoreMask } = require('./config');
@ -27,7 +30,10 @@ if (require.main === module) {
getSass(scssFiles, { ignore: ignoreMask }, signatures),
getSymlinks(symlinks, { nodir: true, ignore: ignoreMask }, signatures),
getSymlinks(symlinkDirs, { ignore: ignoreMask }, signatures),
cleanUp(signatures)
cleanUp(signatures),
getPDFReader(signatures),
getPDFWorker(signatures),
getZoteroNoteEditor(signatures)
]);
await writeSignatures(signatures);

View file

@ -17,6 +17,9 @@ if (require.main === module) {
try {
await getClean(path.join(ROOT, 'build'));
await getClean(path.join(ROOT, '.signatures.json'));
await getClean(path.join(ROOT, 'pdf-reader/build'));
await getClean(path.join(ROOT, 'pdf-worker/build'));
await getClean(path.join(ROOT, 'zotero-note-editor/build'));
} catch (err) {
process.exitCode = 1;
global.isError = true;

61
scripts/pdf-reader.js Normal file
View file

@ -0,0 +1,61 @@
'use strict';
const fs = require('fs-extra');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const { getSignatures, writeSignatures, onSuccess, onError } = require('./utils');
async function getPDFReader(signatures) {
const t1 = Date.now();
var { stdout } = await exec('git rev-parse HEAD', { cwd: './pdf-reader/pdf.js' });
const PDFJSHash = stdout.trim();
var { stdout } = await exec('git rev-parse HEAD', { cwd: './pdf-reader' });
const PDFReaderHash = stdout.trim();
let updated = false;
let name = 'pdf-reader/pdf.js';
if (!(name in signatures) || signatures[name].hash !== PDFJSHash) {
await exec('npm run build:pdf.js', { cwd: './pdf-reader' });
signatures[name] = { hash: PDFJSHash };
updated = true;
}
name = 'pdf-reader';
if (!(name in signatures) || signatures[name].hash !== PDFReaderHash) {
await exec('npm ci;npm run build:reader', { cwd: './pdf-reader' });
signatures[name] = { hash: PDFReaderHash };
updated = true;
}
if (updated) {
await fs.copy('./pdf-reader/build/zotero', './build/resource/pdf.js');
}
const t2 = Date.now();
return {
action: 'pdf-reader',
count: 1,
totalCount: 1,
processingTime: t2 - t1
};
}
module.exports = getPDFReader;
if (require.main === module) {
(async () => {
try {
const signatures = await getSignatures();
onSuccess(await getPDFReader(signatures));
await writeSignatures(signatures);
}
catch (err) {
process.exitCode = 1;
global.isError = true;
onError(err);
}
})();
}

61
scripts/pdf-worker.js Normal file
View file

@ -0,0 +1,61 @@
'use strict';
const fs = require('fs-extra');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const { getSignatures, writeSignatures, onSuccess, onError } = require('./utils');
async function getPDFWorker(signatures) {
const t1 = Date.now();
var { stdout } = await exec('git rev-parse HEAD', { cwd: './pdf-worker/pdf.js' });
const PDFJSHash = stdout.trim();
var { stdout } = await exec('git rev-parse HEAD', { cwd: './pdf-worker' });
const PDFWorkerHash = stdout.trim();
let updated = false;
let name = 'pdf-worker/pdf.js';
if (!(name in signatures) || signatures[name].hash !== PDFJSHash) {
await exec('npm run build:pdf.js', { cwd: './pdf-worker' });
signatures[name] = { hash: PDFJSHash };
updated = true;
}
name = 'pdf-worker';
if (!(name in signatures) || signatures[name].hash !== PDFWorkerHash) {
await exec('npm ci;npm run build:worker', { cwd: './pdf-worker' });
signatures[name] = { hash: PDFWorkerHash };
updated = true;
}
if (updated) {
await fs.copy('./pdf-worker/build/pdf-worker.js', './build/chrome/content/zotero/xpcom/pdfWorker/worker.js');
}
const t2 = Date.now();
return {
action: 'pdf-worker',
count: 1,
totalCount: 1,
processingTime: t2 - t1
};
}
module.exports = getPDFWorker;
if (require.main === module) {
(async () => {
try {
const signatures = await getSignatures();
onSuccess(await getPDFWorker(signatures));
await writeSignatures(signatures);
}
catch (err) {
process.exitCode = 1;
global.isError = true;
onError(err);
}
})();
}

View file

@ -0,0 +1,50 @@
'use strict';
const fs = require('fs-extra');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const { getSignatures, writeSignatures, onSuccess, onError } = require('./utils');
async function getZoteroNoteEditor(signatures) {
const t1 = Date.now();
var { stdout } = await exec('git rev-parse HEAD', { cwd: './zotero-note-editor' });
const zoteroNoteEditorHash = stdout.trim();
let updated = false;
let name = 'zotero-note-editor';
if (!(name in signatures) || signatures[name].hash !== zoteroNoteEditorHash) {
await exec('npm ci;npm run build', { cwd: './zotero-note-editor' });
signatures[name] = { hash: zoteroNoteEditorHash };
updated = true;
}
if (updated) {
await fs.copy('./zotero-note-editor/build/zotero', './build/resource/zotero-note-editor');
}
const t2 = Date.now();
return {
action: 'zotero-note-editor',
count: 1,
totalCount: 1,
processingTime: t2 - t1
};
}
module.exports = getZoteroNoteEditor;
if (require.main === module) {
(async () => {
try {
const signatures = await getSignatures();
onSuccess(await getZoteroNoteEditor(signatures));
await writeSignatures(signatures);
}
catch (err) {
process.exitCode = 1;
global.isError = true;
onError(err);
}
})();
}

1
zotero-note-editor Submodule

@ -0,0 +1 @@
Subproject commit 918d6d2787b2abfd90a8f27d96e6a9cba097433e