Improve PDF reader
This commit is contained in:
parent
875e9f674f
commit
a89f7e8ec7
12 changed files with 668 additions and 636 deletions
|
@ -11,13 +11,17 @@
|
||||||
]>
|
]>
|
||||||
|
|
||||||
<window
|
<window
|
||||||
windowtype="zotero:viewer"
|
id="pdf-reader"
|
||||||
|
windowtype="zotero:reader"
|
||||||
orient="vertical"
|
orient="vertical"
|
||||||
width="1300"
|
width="1300"
|
||||||
height="800"
|
height="800"
|
||||||
persist="screenX screenY width height"
|
persist="screenX screenY width height"
|
||||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||||
>
|
>
|
||||||
|
<script type="application/javascript">
|
||||||
|
Components.utils.import('resource://gre/modules/Services.jsm');
|
||||||
|
</script>
|
||||||
<!-- TODO: Localize -->
|
<!-- TODO: Localize -->
|
||||||
<menubar>
|
<menubar>
|
||||||
<menu label="File">
|
<menu label="File">
|
||||||
|
@ -56,11 +60,16 @@
|
||||||
</menubar>
|
</menubar>
|
||||||
|
|
||||||
<hbox flex="1">
|
<hbox flex="1">
|
||||||
<vbox id="zotero-viewer" flex="3">
|
<vbox id="zotero-reader" flex="3">
|
||||||
<browser tooltip="viewerTooltip" type="content" primary="true" transparent="transparent" src="" id="viewer"
|
<browser id="reader"
|
||||||
|
tooltip="readerTooltip"
|
||||||
|
type="content"
|
||||||
|
primary="true"
|
||||||
|
transparent="transparent"
|
||||||
|
src="resource://zotero/pdf-reader/viewer.html"
|
||||||
flex="1"/>
|
flex="1"/>
|
||||||
<popupset>
|
<popupset>
|
||||||
<tooltip id="viewerTooltip" onpopupshowing="return fillTooltip(this);"/>
|
<tooltip id="readerTooltip" onpopupshowing="return fillTooltip(this);"/>
|
||||||
<menupopup id="tagsPopup" ignorekeys="true"
|
<menupopup id="tagsPopup" ignorekeys="true"
|
||||||
style="min-width: 300px;"
|
style="min-width: 300px;"
|
||||||
onpopupshown="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'true'); }"
|
onpopupshown="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'true'); }"
|
||||||
|
@ -71,28 +80,27 @@
|
||||||
<menupopup id="colorPopup"/>
|
<menupopup id="colorPopup"/>
|
||||||
</popupset>
|
</popupset>
|
||||||
</vbox>
|
</vbox>
|
||||||
<splitter id="zotero-viewer-splitter"
|
<splitter id="zotero-reader-splitter"
|
||||||
hidden="true"
|
hidden="true"
|
||||||
resizebefore="closest"
|
resizebefore="closest"
|
||||||
resizeafter="closest"
|
resizeafter="closest"
|
||||||
collapse="after"
|
collapse="after"
|
||||||
orient="horizontal"
|
orient="horizontal"
|
||||||
zotero-persist="state orient" />
|
zotero-persist="state orient" />
|
||||||
<vbox flex="0" id="zotero-viewer-note-sidebar" width="350" hidden="true">
|
<vbox flex="0" id="zotero-reader-note-sidebar" width="350" hidden="true">
|
||||||
<vbox id="zotero-viewer-sidebar-cover" flex="1">
|
<vbox id="zotero-reader-sidebar-cover" flex="1">
|
||||||
<label>Drag a note here…</label>
|
<label>Drag a note here…</label>
|
||||||
</vbox>
|
</vbox>
|
||||||
<vbox id="zotero-viewer-sidebar-container" flex="1" style="overflow:auto;" hidden="true">
|
<vbox id="zotero-reader-sidebar-container" flex="1" style="overflow:auto;" hidden="true">
|
||||||
<zoteronoteeditor id="zotero-viewer-editor" flex="1" notitle="1"
|
<zoteronoteeditor id="zotero-reader-editor" flex="1" notitle="1"
|
||||||
previousfocus="zotero-items-tree"
|
previousfocus="zotero-items-tree"
|
||||||
onerror="/*this.mode = 'view'*/"
|
onerror="/*this.mode = 'view'*/"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button id="zotero-view-note-button" label="Close"
|
<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;"/>
|
oncommand="document.getElementById('zotero-reader-sidebar-container').hidden = true;document.getElementById('zotero-reader-sidebar-cover').hidden = false;"/>
|
||||||
</vbox>
|
</vbox>
|
||||||
</vbox>
|
</vbox>
|
||||||
</hbox>
|
</hbox>
|
||||||
<script src="include.js"/>
|
<script src="include.js"/>
|
||||||
<script src="viewer.js"/>
|
|
||||||
</window>
|
</window>
|
|
@ -1,66 +0,0 @@
|
||||||
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);
|
|
|
@ -32,7 +32,7 @@ Zotero.Annotations = new function () {
|
||||||
Zotero.defineProperty(this, 'ANNOTATION_TYPE_IMAGE', { value: 3 });
|
Zotero.defineProperty(this, 'ANNOTATION_TYPE_IMAGE', { value: 3 });
|
||||||
|
|
||||||
|
|
||||||
this.toJSON = function (item) {
|
this.toJSON = async function (item) {
|
||||||
var o = {};
|
var o = {};
|
||||||
o.key = item.key;
|
o.key = item.key;
|
||||||
o.type = item.annotationType;
|
o.type = item.annotationType;
|
||||||
|
@ -44,7 +44,13 @@ Zotero.Annotations = new function () {
|
||||||
o.text = item.annotationText;
|
o.text = item.annotationText;
|
||||||
}
|
}
|
||||||
else if (o.type == 'image') {
|
else if (o.type == 'image') {
|
||||||
o.imageURL = item.annotationImageURL;
|
var attachments = item.getAttachments();
|
||||||
|
if (attachments.length) {
|
||||||
|
let imageAttachment = Zotero.Items.get(attachments[0]);
|
||||||
|
if (imageAttachment) {
|
||||||
|
o.image = await imageAttachment.attachmentDataURI;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
o.comment = item.annotationComment;
|
o.comment = item.annotationComment;
|
||||||
o.pageLabel = item.annotationPageLabel;
|
o.pageLabel = item.annotationPageLabel;
|
||||||
|
|
|
@ -3332,6 +3332,31 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentText', {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return dataURI of attachment content
|
||||||
|
*
|
||||||
|
* @return {Promise<String>} - A promise for attachment dataURI or empty string if unavailable
|
||||||
|
*/
|
||||||
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentDataURI', {
|
||||||
|
get: async function () {
|
||||||
|
if (!this.isAttachment()) {
|
||||||
|
throw new Error("'attachmentDataURI' is only valid for attachments");
|
||||||
|
}
|
||||||
|
let path = await this.getFilePathAsync();
|
||||||
|
if (!path || !(await OS.File.exists(path))) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
let buf = await OS.File.read(path, {});
|
||||||
|
let bytes = new Uint8Array(buf);
|
||||||
|
let binary = '';
|
||||||
|
let len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return 'data:' + this.attachmentContentType + ';base64,' + btoa(binary);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns child attachments of this item
|
* Returns child attachments of this item
|
||||||
|
@ -3549,34 +3574,6 @@ Zotero.defineProperty(Zotero.Item.prototype, 'annotationImageAttachment', {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @property {String} annotationImageURL
|
|
||||||
*/
|
|
||||||
Zotero.defineProperty(Zotero.Item.prototype, 'annotationImageURL', {
|
|
||||||
get: function () {
|
|
||||||
if (!this.isImageAnnotation()) {
|
|
||||||
throw new Error("'annotationImageURL' is only valid for image annotations");
|
|
||||||
}
|
|
||||||
var attachments = this.getAttachments();
|
|
||||||
if (!attachments.length) {
|
|
||||||
throw new Error("No attachments found for image annotation");
|
|
||||||
}
|
|
||||||
|
|
||||||
var { libraryID, key } = Zotero.Items.getLibraryAndKeyFromID(attachments[0]);
|
|
||||||
var url = 'zotero://attachment/';
|
|
||||||
if (libraryID == Zotero.Libraries.userLibraryID) {
|
|
||||||
url += 'library';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
url += Zotero.URI.getLibraryPath(libraryID);
|
|
||||||
}
|
|
||||||
url += '/items/' + key;
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if an item is an annotation
|
* Determine if an item is an annotation
|
||||||
*
|
*
|
||||||
|
|
|
@ -190,7 +190,7 @@ class EditorInstance {
|
||||||
this.onNavigate(uri, { position });
|
this.onNavigate(uri, { position });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await Zotero.Viewer.openURI(uri, { position });
|
await Zotero.Reader.openURI(uri, { position });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
const CMAPS_URL = 'resource://zotero/pdf.js/cmaps/';
|
const CMAPS_URL = 'resource://zotero/pdf-reader/cmaps/';
|
||||||
|
|
||||||
class PDFWorker {
|
class PDFWorker {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
610
chrome/content/zotero/xpcom/reader.js
Normal file
610
chrome/content/zotero/xpcom/reader.js
Normal file
|
@ -0,0 +1,610 @@
|
||||||
|
let PDFStates = {};
|
||||||
|
|
||||||
|
class ReaderWindow {
|
||||||
|
constructor() {
|
||||||
|
this.annotationItemIds = [];
|
||||||
|
this._instanceID = Zotero.Utilities.randomString();
|
||||||
|
this._window = null;
|
||||||
|
this._iframeWindow = null;
|
||||||
|
this._itemID = null;
|
||||||
|
this._state = null;
|
||||||
|
this._prevHistory = [];
|
||||||
|
this._nextHistory = [];
|
||||||
|
this._isReaderInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let win = Services.wm.getMostRecentWindow('navigator:browser');
|
||||||
|
if (!win) return;
|
||||||
|
|
||||||
|
this._window = win.open(
|
||||||
|
'chrome://zotero/content/reader.xul', '', 'chrome,resizable'
|
||||||
|
);
|
||||||
|
|
||||||
|
this._window.addEventListener('DOMContentLoaded', (event) => {
|
||||||
|
this._window.addEventListener('dragover', this._handleDragOver, true);
|
||||||
|
this._window.addEventListener('drop', this._handleDrop, true);
|
||||||
|
this._window.addEventListener('keypress', this._handleKeyPress);
|
||||||
|
|
||||||
|
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._postMessage(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
let readerIframe = this._window.document.getElementById('reader');
|
||||||
|
if (!(readerIframe && readerIframe.contentWindow && readerIframe.contentWindow.document === event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let editor = this._window.document.getElementById('zotero-reader-editor');
|
||||||
|
editor.navigateHandler = async (uri, location) => {
|
||||||
|
let item = await Zotero.URI.getURIItem(uri);
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.id === this._itemID) {
|
||||||
|
this.navigate(location);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await this.open({
|
||||||
|
itemID: item.id,
|
||||||
|
location
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._iframeWindow = this._window.document.getElementById('reader').contentWindow;
|
||||||
|
this._iframeWindow.addEventListener('message', this._handleMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async open({ itemID, state, location, skipHistory }) {
|
||||||
|
if (itemID === this._itemID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let item = await Zotero.Items.getAsync(itemID);
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let path = await item.getFilePathAsync();
|
||||||
|
let buf = await OS.File.read(path, {});
|
||||||
|
buf = new Uint8Array(buf).buffer;
|
||||||
|
if (this._itemID && !skipHistory) {
|
||||||
|
this._prevHistory.push({
|
||||||
|
itemID: this._itemID,
|
||||||
|
state: this._state
|
||||||
|
});
|
||||||
|
this._nextHistory = [];
|
||||||
|
}
|
||||||
|
this._itemID = item.id;
|
||||||
|
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;
|
||||||
|
// TODO: Remove when fixed
|
||||||
|
item._loaded.childItems = true;
|
||||||
|
let ids = item.getAnnotations();
|
||||||
|
let annotations = (await Promise.all(ids.map(id => this._getAnnotation(id)))).filter(x => x);
|
||||||
|
this.annotationItemIds = ids;
|
||||||
|
state = state || PDFStates[this._itemID];
|
||||||
|
this._state = state;
|
||||||
|
this._postMessage({
|
||||||
|
action: 'open',
|
||||||
|
buf,
|
||||||
|
annotations,
|
||||||
|
state,
|
||||||
|
location,
|
||||||
|
enablePrev: !!this._prevHistory.length,
|
||||||
|
enableNext: !!this._nextHistory.length
|
||||||
|
}, [buf]);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAnnotations(ids) {
|
||||||
|
let annotations = [];
|
||||||
|
for (let id of ids) {
|
||||||
|
let annotation = await this._getAnnotation(id);
|
||||||
|
if (annotation) {
|
||||||
|
annotations.push(annotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (annotations.length) {
|
||||||
|
let data = { action: 'setAnnotations', annotations };
|
||||||
|
this._postMessage(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsetAnnotations(keys) {
|
||||||
|
let data = { action: 'unsetAnnotations', ids: keys };
|
||||||
|
this._postMessage(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigate(location) {
|
||||||
|
this._postMessage({ action: 'navigate', location });
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pass sidebar state to the responsible pdf-reader button
|
||||||
|
_toggleNoteSidebar(isToggled) {
|
||||||
|
let splitter = this._window.document.getElementById('zotero-reader-splitter');
|
||||||
|
let sidebar = this._window.document.getElementById('zotero-reader-note-sidebar');
|
||||||
|
if (isToggled) {
|
||||||
|
splitter.hidden = false;
|
||||||
|
sidebar.hidden = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
splitter.hidden = true;
|
||||||
|
sidebar.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getColorIcon(color, selected) {
|
||||||
|
let stroke = selected ? 'lightgray' : 'transparent';
|
||||||
|
let fill = '%23' + color.slice(1);
|
||||||
|
return `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect shape-rendering="geometricPrecision" fill="${fill}" stroke-width="2" x="2" y="2" stroke="${stroke}" width="12" height="12" rx="3"/></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openAnnotationPopup(x, y, annotationId, colors, 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: annotationId
|
||||||
|
};
|
||||||
|
this._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';
|
||||||
|
menuitem.setAttribute('image', this._getColorIcon(color[1], color[1] === selectedColor));
|
||||||
|
menuitem.addEventListener('command', () => {
|
||||||
|
let data = {
|
||||||
|
action: 'popupCmd',
|
||||||
|
cmd: 'setAnnotationColor',
|
||||||
|
id: annotationId,
|
||||||
|
color: color[1]
|
||||||
|
};
|
||||||
|
this._postMessage(data);
|
||||||
|
});
|
||||||
|
popup.appendChild(menuitem);
|
||||||
|
}
|
||||||
|
popup.openPopupAtScreen(x, y, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_openColorPopup(x, y, colors, 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';
|
||||||
|
menuitem.setAttribute('image', this._getColorIcon(color[1], color[1] === selectedColor));
|
||||||
|
menuitem.addEventListener('command', () => {
|
||||||
|
let data = {
|
||||||
|
action: 'popupCmd',
|
||||||
|
cmd: 'setColor',
|
||||||
|
color: color[1]
|
||||||
|
};
|
||||||
|
this._postMessage(data);
|
||||||
|
});
|
||||||
|
popup.appendChild(menuitem);
|
||||||
|
}
|
||||||
|
popup.openPopupAtScreen(x, y, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _postMessage(message, transfer) {
|
||||||
|
await this._waitForReader();
|
||||||
|
this._iframeWindow.postMessage({ itemId: this._itemID, message }, this._iframeWindow.origin, transfer);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleMessage = async (event) => {
|
||||||
|
let message;
|
||||||
|
try {
|
||||||
|
if (event.source !== this._iframeWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Clone data to avoid the dead object error when the window is closed
|
||||||
|
let data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
// Filter messages coming from previous reader instances,
|
||||||
|
// except for `setAnnotation` to still allow saving it
|
||||||
|
if (data.itemId !== this._itemID && data.message.action !== 'setAnnotation') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Zotero.debug('Received message from pdf-reader iframe: ' + JSON.stringify(data));
|
||||||
|
message = data.message;
|
||||||
|
switch (message.action) {
|
||||||
|
case 'navigatePrev': {
|
||||||
|
let prev = this._prevHistory.pop();
|
||||||
|
if (prev) {
|
||||||
|
this._nextHistory.push({
|
||||||
|
itemID: this._itemID,
|
||||||
|
state: this._state
|
||||||
|
});
|
||||||
|
this.open({ itemID: prev.itemID, state: prev.state, skipHistory: true });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'navigateNext': {
|
||||||
|
let next = this._nextHistory.pop();
|
||||||
|
if (next) {
|
||||||
|
this._prevHistory.push({
|
||||||
|
itemID: this._itemID,
|
||||||
|
state: this._state
|
||||||
|
});
|
||||||
|
this.open({ itemID: next.itemID, state: next.state, skipHistory: true });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'setAnnotation': {
|
||||||
|
let attachment = Zotero.Items.get(data.itemId);
|
||||||
|
let { annotation } = message;
|
||||||
|
annotation.key = annotation.id;
|
||||||
|
let saveOptions = {
|
||||||
|
notifierData: {
|
||||||
|
instanceID: this._instanceID
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let savedAnnotation = await Zotero.Annotations.saveFromJSON(attachment, annotation, saveOptions);
|
||||||
|
if (annotation.image) {
|
||||||
|
let blob = this._dataURLtoBlob(annotation.image);
|
||||||
|
let attachmentIds = savedAnnotation.getAttachments();
|
||||||
|
if (attachmentIds.length) {
|
||||||
|
let attachment = Zotero.Items.get(attachmentIds[0]);
|
||||||
|
let path = await attachment.getFilePathAsync();
|
||||||
|
await Zotero.File.putContentsAsync(path, blob);
|
||||||
|
await Zotero.Sync.Storage.Local.updateSyncStates([attachment], 'to_upload');
|
||||||
|
Zotero.Notifier.trigger('modify', 'item', attachment.id, { instanceID: this._instanceID });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await Zotero.Attachments.importEmbeddedImage({
|
||||||
|
blob,
|
||||||
|
parentItemID: savedAnnotation.id,
|
||||||
|
saveOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'deleteAnnotations': {
|
||||||
|
let { ids: keys } = message;
|
||||||
|
let attachment = Zotero.Items.get(this._itemID);
|
||||||
|
let libraryID = attachment.libraryID;
|
||||||
|
for (let key of keys) {
|
||||||
|
let annotation = Zotero.Items.getByLibraryAndKey(libraryID, key);
|
||||||
|
// A small check, as we are receiving a list of item keys from a less secure code
|
||||||
|
if (annotation && annotation.isAnnotation() && annotation.parentID === this._itemID) {
|
||||||
|
this.annotationItemIds = this.annotationItemIds.filter(id => id !== annotation.id);
|
||||||
|
await annotation.eraseTx();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'setState': {
|
||||||
|
let { state } = message;
|
||||||
|
PDFStates[this._itemID] = state;
|
||||||
|
this._state = state;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'openTagsPopup': {
|
||||||
|
let { id: key, x, y } = message;
|
||||||
|
let attachment = Zotero.Items.get(this._itemID);
|
||||||
|
let libraryID = attachment.libraryID;
|
||||||
|
let annotation = Zotero.Items.getByLibraryAndKey(libraryID, key);
|
||||||
|
if (annotation) {
|
||||||
|
this._window.document.getElementById('tags').item = annotation;
|
||||||
|
this._window.document.getElementById('tagsPopup').openPopupAtScreen(x, y, false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'openAnnotationPopup': {
|
||||||
|
let { x, y, id, colors, selectedColor } = message;
|
||||||
|
this._openAnnotationPopup(x, y, id, colors, selectedColor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'openColorPopup': {
|
||||||
|
let { x, y, colors, selectedColor } = message;
|
||||||
|
this._openColorPopup(x, y, colors, selectedColor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'openUrl': {
|
||||||
|
let { url } = message;
|
||||||
|
let win = Services.wm.getMostRecentWindow('navigator:browser');
|
||||||
|
if (win) {
|
||||||
|
win.ZoteroPane.loadURI(url);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'import': {
|
||||||
|
Zotero.debug('Importing PDF annotations');
|
||||||
|
let item = Zotero.Items.get(this._itemID);
|
||||||
|
Zotero.PDFImport.import(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'importDismiss': {
|
||||||
|
Zotero.debug('Dismiss PDF annotations');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'save': {
|
||||||
|
Zotero.debug('Exporting PDF');
|
||||||
|
let zp = Zotero.getActiveZoteroPane();
|
||||||
|
zp.exportPDF(this._itemID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'toggleNoteSidebar': {
|
||||||
|
let { isToggled } = message;
|
||||||
|
this._toggleNoteSidebar(isToggled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this._postMessage({
|
||||||
|
action: 'error',
|
||||||
|
message: `An error occured during '${message ? message.action : ''}'`,
|
||||||
|
moreInfo: {
|
||||||
|
message: e.message,
|
||||||
|
stack: e.stack,
|
||||||
|
fileName: e.fileName,
|
||||||
|
lineNumber: e.lineNumber
|
||||||
|
}
|
||||||
|
});
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleDragOver = (event) => {
|
||||||
|
if (event.dataTransfer.getData('zotero/item')) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_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 = this._window.document.getElementById('zotero-reader-sidebar-cover');
|
||||||
|
let container = this._window.document.getElementById('zotero-reader-sidebar-container');
|
||||||
|
let splitter = this._window.document.getElementById('zotero-reader-splitter');
|
||||||
|
|
||||||
|
cover.hidden = true;
|
||||||
|
container.hidden = false;
|
||||||
|
splitter.hidden = false;
|
||||||
|
|
||||||
|
let editor = this._window.document.getElementById('zotero-reader-editor');
|
||||||
|
let notebox = this._window.document.getElementById('zotero-reader-note-sidebar');
|
||||||
|
editor.mode = 'edit';
|
||||||
|
notebox.hidden = false;
|
||||||
|
editor.item = item;
|
||||||
|
}
|
||||||
|
else if (item.isAttachment() && item.attachmentContentType === 'application/pdf') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.open({ itemID: item.id });
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
this.open({ itemID: attachment.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleKeyPress = (event) => {
|
||||||
|
if ((Zotero.isMac && event.metaKey || event.ctrlKey)
|
||||||
|
&& !event.shiftKey && !event.altKey && event.key === 'w') {
|
||||||
|
this._window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _waitForReader() {
|
||||||
|
if (this._isReaderInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let n = 0;
|
||||||
|
while (!this._iframeWindow || !this._iframeWindow.eval('window.isReady')) {
|
||||||
|
if (n >= 500) {
|
||||||
|
throw new Error('Waiting for reader failed');
|
||||||
|
}
|
||||||
|
await Zotero.Promise.delay(10);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
this._isReaderInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return item JSON in the pdf-reader ready format
|
||||||
|
* @param itemID
|
||||||
|
* @returns {Object|null}
|
||||||
|
*/
|
||||||
|
async _getAnnotation(itemID) {
|
||||||
|
try {
|
||||||
|
let item = Zotero.Items.get(itemID);
|
||||||
|
if (!item || !item.isAnnotation()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// TODO: Remve when fixed
|
||||||
|
item._loaded.childItems = true;
|
||||||
|
item = await Zotero.Annotations.toJSON(item);
|
||||||
|
item.id = item.key;
|
||||||
|
item.image = item.image;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Reader {
|
||||||
|
constructor() {
|
||||||
|
this._readerWindows = [];
|
||||||
|
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'reader');
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(event, type, ids, extraData) {
|
||||||
|
// Listen for the parent item, PDF attachment and its annotation items updates
|
||||||
|
for (let readerWindow of this._readerWindows) {
|
||||||
|
if (event === 'delete') {
|
||||||
|
let disappearedIds = readerWindow.annotationItemIds.filter(x => ids.includes(x));
|
||||||
|
if (disappearedIds.length) {
|
||||||
|
let keys = disappearedIds.map(id => extraData[id].key);
|
||||||
|
readerWindow.unsetAnnotations(keys);
|
||||||
|
}
|
||||||
|
if (ids.includes(readerWindow._itemID)) {
|
||||||
|
readerWindow.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let item = Zotero.Items.get(readerWindow._itemID);
|
||||||
|
// TODO: Remove when fixed
|
||||||
|
item._loaded.childItems = true;
|
||||||
|
let annotationItemIds = item.getAnnotations();
|
||||||
|
readerWindow.annotationItemIds = annotationItemIds;
|
||||||
|
let affectedAnnotationIds = annotationItemIds.filter(annotationID => {
|
||||||
|
let annotation = Zotero.Items.get(annotationID);
|
||||||
|
let imageAttachmentID = null;
|
||||||
|
annotation._loaded.childItems = true;
|
||||||
|
let annotationAttachments = annotation.getAttachments();
|
||||||
|
if (annotationAttachments.length) {
|
||||||
|
imageAttachmentID = annotationAttachments[0];
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
ids.includes(annotationID) && !(extraData[annotationID]
|
||||||
|
&& extraData[annotationID].instanceID === readerWindow._instanceID)
|
||||||
|
|| ids.includes(imageAttachmentID) && !(extraData[imageAttachmentID]
|
||||||
|
&& extraData[imageAttachmentID].instanceID === readerWindow._instanceID)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (affectedAnnotationIds.length) {
|
||||||
|
readerWindow.setAnnotations(affectedAnnotationIds);
|
||||||
|
}
|
||||||
|
// Update title if the PDF attachment or the parent item changes
|
||||||
|
if (ids.includes(readerWindow._itemID) || ids.includes(item.parentItemID)) {
|
||||||
|
readerWindow.updateTitle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getReaderWindow(itemID) {
|
||||||
|
return this._readerWindows.find(v => v._itemID === itemID);
|
||||||
|
}
|
||||||
|
|
||||||
|
async openURI(itemURI, location) {
|
||||||
|
let item = await Zotero.URI.getURIItem(itemURI);
|
||||||
|
if (!item) return;
|
||||||
|
this.open(item.id, location);
|
||||||
|
}
|
||||||
|
|
||||||
|
async open(itemID, location) {
|
||||||
|
let reader = this._getReaderWindow(itemID);
|
||||||
|
if (reader) {
|
||||||
|
if (location) {
|
||||||
|
reader.navigate(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reader = new ReaderWindow();
|
||||||
|
reader.init();
|
||||||
|
if (!(await reader.open({ itemID, location }))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._readerWindows.push(reader);
|
||||||
|
reader._window.addEventListener('unload', () => {
|
||||||
|
this._readerWindows.splice(this._readerWindows.indexOf(reader), 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
reader._window.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.Reader = new Reader();
|
|
@ -1,523 +0,0 @@
|
||||||
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();
|
|
|
@ -4232,7 +4232,7 @@ var ZoteroPane = new function()
|
||||||
});
|
});
|
||||||
|
|
||||||
this.viewPDF = function (itemID) {
|
this.viewPDF = function (itemID) {
|
||||||
Zotero.Viewer.open(itemID);
|
Zotero.Reader.open(itemID);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ const xpcomFilesLocal = [
|
||||||
'noteBackups',
|
'noteBackups',
|
||||||
'notifier',
|
'notifier',
|
||||||
'openPDF',
|
'openPDF',
|
||||||
'viewer',
|
'reader',
|
||||||
'progressQueue',
|
'progressQueue',
|
||||||
'progressQueueDialog',
|
'progressQueueDialog',
|
||||||
'quickCopy',
|
'quickCopy',
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7aefe43059843f2b065f1ca9630d1bb48e08d4c3
|
Subproject commit acd34e852d38768c8f31e59aa7238d4d934081eb
|
|
@ -30,7 +30,7 @@ async function getPDFReader(signatures) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
await fs.copy('./pdf-reader/build/zotero', './build/resource/pdf.js');
|
await fs.copy('./pdf-reader/build/zotero', './build/resource/pdf-reader');
|
||||||
}
|
}
|
||||||
|
|
||||||
const t2 = Date.now();
|
const t2 = Date.now();
|
||||||
|
|
Loading…
Reference in a new issue