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

@ -1,39 +1,39 @@
<?xml version="1.0"?>
<!--
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
-->
<bindings xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<bindings xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="note-editor">
<resources>
<stylesheet src="chrome://zotero/skin/bindings/noteeditor.css"/>
<stylesheet src="chrome://zotero-platform/content/noteeditor.css"/>
</resources>
<implementation>
<!--
Public properties
@ -43,57 +43,121 @@
<field name="displayTags">false</field>
<field name="displayRelated">false</field>
<field name="displayButton">false</field>
<field name="buttonCaption"/>
<field name="parentClickHandler"/>
<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>
<property name="mode" onget="return this._mode;">
<setter>
<![CDATA[
// Duplicate default property settings here
this.editable = false;
this.saveOnEdit = false;
this.displayTags = false;
this.displayRelated = false;
this.displayButton = false;
switch (val) {
case 'view':
case 'merge':
if (this.noteField) {
this.noteField.onInit(ed => ed.setMode('readonly'));
}
break;
case 'edit':
if (this.noteField) {
this.noteField.onInit(ed => ed.setMode('design'));
}
this.editable = true;
this.saveOnEdit = true;
this.parentClickHandler = this.selectParent;
this.keyDownHandler = this.handleKeyDown;
this.commandHandler = this.save;
this.displayTags = true;
this.displayRelated = true;
break;
default:
throw ("Invalid mode '" + val + "' in noteeditor.xml");
}
this._mode = val;
document.getAnonymousNodes(this)[0].setAttribute('mode', val);
this._id('links-box').mode = val;
]]>
// Duplicate default property settings here
this.editable = false;
this.saveOnEdit = false;
this.displayTags = false;
this.displayRelated = false;
this.displayButton = false;
switch (val) {
case 'view':
case 'merge':
this.editable = false;
break;
case 'edit':
this.editable = true;
this.saveOnEdit = true;
this.parentClickHandler = this.selectParent;
this.keyDownHandler = this.handleKeyDown;
this.commandHandler = this.save;
this.displayTags = true;
this.displayRelated = true;
break;
default:
throw ("Invalid mode '" + val + "' in noteeditor.xml");
}
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>
<field name="_parentItem"/>
<property name="parentItem" onget="return this._parentItem;">
<setter>
@ -102,31 +166,59 @@
]]>
</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);
}
this._id('links-box').item = this.item;
this.refresh();
]]></setter>
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++;
}
// The binding can also be immediately destrcutred
// (which also happens in the marge dialog)
if (this._destroyed) {
return;
}
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;
@ -137,266 +229,116 @@
]]>
</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[
<![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;
}
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>
return (async () => {
})();
]]></body>
</method>
<!-- Used to insert a tab manually -->
<method name="handleKeyDown">
<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;
}
]]>
</body>
</method>
<method name="focus">
<body>
<![CDATA[
this._id('noteField').focus();
// 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>
<method name="clearUndo">
<method name="focus">
<body>
<![CDATA[
this._id('noteField').clearUndo();
]]>
<![CDATA[
setTimeout(() => {
if (this._iframe && this._iframe.contentWindow) {
this._iframe.focus();
this._editor.focus();
}
}, 500);
]]>
</body>
</method>
<method name="_id">
<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>
</implementation>
<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>
<binding id="links-box">
<implementation>
<implementation>
<field name="itemRef"/>
<property name="item" onget="return this.itemRef;">
<setter>
<![CDATA[
this.itemRef = val;
this.id('tags').item = this.item;
this.id('related').item = this.item;
this.refresh();
@ -406,127 +348,131 @@
<property name="mode">
<setter>
<![CDATA[
this.id('related').mode = val;
this.id('tags').mode = val;
]]>
this.id('related').mode = val;
this.id('tags').mode = val;
]]>
</setter>
</property>
<field name="_parentItem"/>
<property name="parentItem" onget="return this._parentItem;">
<setter>
<![CDATA[
this._parentItem = val;
var parentText = this.id('parentText');
if (parentText.firstChild) {
parentText.removeChild(parentText.firstChild);
}
if (this._parentItem && this.getAttribute('notitle') != '1') {
this.id('parent-row').hidden = undefined;
this.id('parentLabel').value = Zotero.getString('pane.item.parentItem');
parentText.appendChild(document.createTextNode(this._parentItem.getDisplayTitle(true)));
}
]]>
this._parentItem = val;
var parentText = this.id('parentText');
if (parentText.firstChild) {
parentText.removeChild(parentText.firstChild);
}
if (this._parentItem && this.getAttribute('notitle') != '1') {
this.id('parent-row').hidden = undefined;
this.id('parentLabel').value = Zotero.getString('pane.item.parentItem');
parentText.appendChild(document.createTextNode(this._parentItem.getDisplayTitle(true)));
}
]]>
</setter>
</property>
<method name="tagsClick">
<body><![CDATA[
this.id('tags').reload();
var x = this.boxObject.screenX;
var y = this.boxObject.screenY;
this.id('tagsPopup').openPopupAtScreen(x, y, false);
// If editable and no existing tags, open new empty row
var tagsBox = this.id('tags');
if (tagsBox.mode == 'edit' && tagsBox.count == 0) {
this.id('tags').newTag();
}
]]></body>
this.id('tags').reload();
var x = this.boxObject.screenX;
var y = this.boxObject.screenY;
this.id('tagsPopup').openPopupAtScreen(x, y, false);
// If editable and no existing tags, open new empty row
var tagsBox = this.id('tags');
if (tagsBox.mode == 'edit' && tagsBox.count == 0) {
this.id('tags').newTag();
}
]]></body>
</method>
<method name="refresh">
<body><![CDATA[
this.updateTagsSummary();
this.updateRelatedSummary();
]]></body>
this.updateTagsSummary();
this.updateRelatedSummary();
]]></body>
</method>
<method name="updateTagsSummary">
<body><![CDATA[
var v = this.id('tags').summary;
if (!v || v == "") {
v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]";
}
this.id('tagsLabel').value = Zotero.getString('itemFields.tags')
+ Zotero.getString('punctuation.colon');
this.id('tagsClick').value = v;
]]></body>
var v = this.id('tags').summary;
if (!v || v == "") {
v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]";
}
this.id('tagsLabel').value = Zotero.getString('itemFields.tags')
+ Zotero.getString('punctuation.colon');
this.id('tagsClick').value = v;
]]></body>
</method>
<method name="relatedClick">
<body><![CDATA[
var relatedList = this.item.relatedItems;
if (relatedList.length > 0) {
var x = this.boxObject.screenX;
var y = this.boxObject.screenY;
this.id('relatedPopup').openPopupAtScreen(x, y, false);
}
else {
this.id('related').add();
}
]]></body>
var relatedList = this.item.relatedItems;
if (relatedList.length > 0) {
var x = this.boxObject.screenX;
var y = this.boxObject.screenY;
this.id('relatedPopup').openPopupAtScreen(x, y, false);
}
else {
this.id('related').add();
}
]]></body>
</method>
<method name="updateRelatedSummary">
<body><![CDATA[
var v = this.id('related').summary;
if (!v || v == "") {
v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]";
}
this.id('relatedLabel').value = Zotero.getString('itemFields.related')
+ Zotero.getString('punctuation.colon');
this.id('relatedClick').value = v;
]]></body>
var v = this.id('related').summary;
if (!v || v == "") {
v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]";
}
this.id('relatedLabel').value = Zotero.getString('itemFields.related')
+ Zotero.getString('punctuation.colon');
this.id('relatedClick').value = v;
]]></body>
</method>
<method name="parentClick">
<body>
<![CDATA[
if (!this.item || !this.item.id) {
return;
}
if (document.getElementById('zotero-pane')) {
var zp = ZoteroPane;
}
else {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
if (!lastWin) {
var lastWin = window.open();
if (!this.item || !this.item.id) {
return;
}
var zp = lastWin.ZoteroPane;
}
Zotero.spawn(function* () {
var parentID = this.item.parentID;
yield zp.clearQuicksearch();
zp.selectItem(parentID);
}, this);
]]>
if (document.getElementById('zotero-pane')) {
var zp = ZoteroPane;
}
else {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
if (!lastWin) {
var lastWin = window.open();
}
if (lastWin.ZoteroOverlay && !lastWin.ZoteroPane.isShowing()) {
lastWin.ZoteroOverlay.toggleDisplay(true);
}
var zp = lastWin.ZoteroPane;
}
Zotero.spawn(function* () {
var parentID = this.item.parentID;
yield zp.clearQuicksearch();
zp.selectItem(parentID);
}, this);
]]>
</body>
</method>
<method name="id">
<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>
@ -562,17 +511,17 @@
seems to get triggered by these events for reasons that are less than
clear) so that we can manually refresh the popup if it's open after
autocomplete is used to prevent it from becoming unresponsive
Note: Code in tagsbox.xml is dependent on the DOM path between the
tagsbox and tagsLabel above, so be sure to update fixPopup() if it changes
-->
<xul:menupopup id="tagsPopup" ignorekeys="true"
onpopupshown="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ /* DEBUG: it would be nice to make this work -- if (this.firstChild.count==0){ this.firstChild.newTag(); } */ this.setAttribute('showing', 'true'); }"
onpopuphidden="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'false'); }">
onpopupshown="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ /* DEBUG: it would be nice to make this work -- if (this.firstChild.count==0){ this.firstChild.newTag(); } */ this.setAttribute('showing', 'true'); }"
onpopuphidden="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'false'); }">
<xul:tagsbox id="tags" flex="1" mode="edit"/>
</xul:menupopup>
</xul:popupset>
</xul:vbox>
</content>
</binding>
</bindings>
</bindings>

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

@ -21,6 +21,6 @@
<key id="key_close" key="W" modifiers="accel" command="cmd_close"/>
</keyset>
<command id="cmd_close" oncommand="window.close();"/>
<zoteronoteeditor id="zotero-note-editor" flex="1" onerror="onError()"/>
</window>
<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();
@ -4563,8 +4618,8 @@ var ZoteroPane = new function()
}
}
};
this.createEmptyParent = async function (item) {
await Zotero.DB.executeTransaction(async function () {
// TODO: remove once there are no top-level web attachments
@ -4585,7 +4640,27 @@ var ZoteroPane = new function()
await item.save();
});
};
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