Introduce PDF reader and note editor
This commit is contained in:
parent
c3ff6eb66e
commit
2543a695e8
36 changed files with 2512 additions and 419 deletions
12
.gitmodules
vendored
12
.gitmodules
vendored
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
35
chrome/content/zotero/containers/noteEditor.xul
Normal file
35
chrome/content/zotero/containers/noteEditor.xul
Normal 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>
|
594
chrome/content/zotero/containers/noteEditorContainer.js
Normal file
594
chrome/content/zotero/containers/noteEditorContainer.js
Normal 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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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()"/>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
66
chrome/content/zotero/viewer.js
Normal file
66
chrome/content/zotero/viewer.js
Normal 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);
|
98
chrome/content/zotero/viewer.xul
Normal file
98
chrome/content/zotero/viewer.xul
Normal 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>
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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]'){
|
||||
|
|
|
@ -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());
|
||||
|
|
168
chrome/content/zotero/xpcom/pdfExport.js
Normal file
168
chrome/content/zotero/xpcom/pdfExport.js
Normal 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();
|
167
chrome/content/zotero/xpcom/pdfImport.js
Normal file
167
chrome/content/zotero/xpcom/pdfImport.js
Normal 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();
|
106
chrome/content/zotero/xpcom/pdfWorker/transport.js
Normal file
106
chrome/content/zotero/xpcom/pdfWorker/transport.js
Normal 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();
|
523
chrome/content/zotero/xpcom/viewer.js
Normal file
523
chrome/content/zotero/xpcom/viewer.js
Normal 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();
|
|
@ -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
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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:">
|
||||
|
|
|
@ -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…
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
1
pdf-reader
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit cb09ac97385b17bad5ef3cb70daa57fb998b309f
|
1
pdf-worker
Submodule
1
pdf-worker
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit b855ed86d8f50261a4b5437d5894d32fe5389a67
|
|
@ -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);
|
||||
|
|
|
@ -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
61
scripts/pdf-reader.js
Normal 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
61
scripts/pdf-worker.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
}
|
50
scripts/zotero-note-editor.js
Normal file
50
scripts/zotero-note-editor.js
Normal 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
1
zotero-note-editor
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 918d6d2787b2abfd90a8f27d96e6a9cba097433e
|
Loading…
Reference in a new issue