1457 lines
44 KiB
JavaScript
1457 lines
44 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2021 Corporation for Digital Scholarship
|
|
Vienna, Virginia, USA
|
|
http://digitalscholar.org/
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
class ReaderInstance {
|
|
constructor() {
|
|
this.pdfStateFileName = '.zotero-pdf-state';
|
|
this.annotationItemIDs = [];
|
|
this.onChangeSidebarWidth = null;
|
|
this.state = null;
|
|
this._instanceID = Zotero.Utilities.randomString();
|
|
this._window = null;
|
|
this._iframeWindow = null;
|
|
this._itemID = null;
|
|
this._title = '';
|
|
this._isReaderInitialized = false;
|
|
this._showItemPaneToggle = false;
|
|
this._initPromise = new Promise((resolve, reject) => {
|
|
this._resolveInitPromise = resolve;
|
|
this._rejectInitPromise = reject;
|
|
});
|
|
}
|
|
|
|
focus() {
|
|
try {
|
|
this._iframeWindow.document.querySelector('#viewerContainer').focus();
|
|
}
|
|
catch (e) {
|
|
}
|
|
}
|
|
|
|
async open({ itemID, state, location }) {
|
|
let { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID);
|
|
let library = Zotero.Libraries.get(libraryID);
|
|
await library.waitForDataLoad('item');
|
|
|
|
let item = Zotero.Items.get(itemID);
|
|
if (!item) {
|
|
return false;
|
|
}
|
|
this.state = state;
|
|
this._itemID = item.id;
|
|
// Set `ReaderTab` title as fast as possible
|
|
this.updateTitle();
|
|
let path = await item.getFilePathAsync();
|
|
let buf = await OS.File.read(path, {});
|
|
buf = new Uint8Array(buf).buffer;
|
|
let annotationItems = item.getAnnotations();
|
|
let annotations = (await Promise.all(annotationItems.map(x => this._getAnnotation(x)))).filter(x => x);
|
|
this.annotationItemIDs = annotationItems.map(x => x.id);
|
|
state = state || await this._getState();
|
|
this._postMessage({
|
|
action: 'open',
|
|
buf,
|
|
annotations,
|
|
state,
|
|
location,
|
|
readOnly: this._isReadOnly(),
|
|
authorName: item.library.libraryType === 'group' ? Zotero.Users.getCurrentName() : '',
|
|
showItemPaneToggle: this._showItemPaneToggle,
|
|
sidebarWidth: this._sidebarWidth,
|
|
sidebarOpen: this._sidebarOpen,
|
|
bottomPlaceholderHeight: this._bottomPlaceholderHeight,
|
|
rtl: Zotero.rtl,
|
|
localizedStrings: {
|
|
...Zotero.Intl.getPrefixedStrings('general.'),
|
|
...Zotero.Intl.getPrefixedStrings('pdfReader.')
|
|
}
|
|
}, [buf]);
|
|
// Set title once again, because `ReaderWindow` isn't loaded the first time
|
|
this.updateTitle();
|
|
return true;
|
|
}
|
|
|
|
get itemID() {
|
|
return this._itemID;
|
|
}
|
|
|
|
updateTitle() {
|
|
let item = Zotero.Items.get(this._itemID);
|
|
let title = item.getDisplayTitle();
|
|
let parentItem = item.parentItem;
|
|
if (parentItem) {
|
|
let parts = [];
|
|
let displayTitle = parentItem.getDisplayTitle();
|
|
if (displayTitle) {
|
|
parts.push(displayTitle);
|
|
}
|
|
|
|
let firstCreator = parentItem.getField('firstCreator');
|
|
if (firstCreator) {
|
|
parts.push(firstCreator);
|
|
}
|
|
|
|
let year = parentItem.getField('year');
|
|
if (year) {
|
|
parts.push(year);
|
|
}
|
|
|
|
title = parts.join(' - ');
|
|
}
|
|
|
|
this._title = title;
|
|
this._setTitleValue(title);
|
|
}
|
|
|
|
async setAnnotations(items) {
|
|
let annotations = [];
|
|
for (let item of items) {
|
|
let annotation = await this._getAnnotation(item);
|
|
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 });
|
|
}
|
|
|
|
enableAddToNote(enable) {
|
|
this._postMessage({ action: 'enableAddToNote', enable });
|
|
}
|
|
|
|
setSidebarWidth(width) {
|
|
this._postMessage({ action: 'setSidebarWidth', width });
|
|
}
|
|
|
|
setSidebarOpen(open) {
|
|
this._postMessage({ action: 'setSidebarOpen', open });
|
|
}
|
|
|
|
focusLastToolbarButton() {
|
|
this._iframeWindow.focus();
|
|
this._postMessage({ action: 'focusLastToolbarButton' });
|
|
}
|
|
|
|
tabToolbar(reverse) {
|
|
this._postMessage({ action: 'tabToolbar', reverse });
|
|
// Avoid toolbar find button being focused for a short moment
|
|
setTimeout(() => this._iframeWindow.focus());
|
|
}
|
|
|
|
focusFirst() {
|
|
this._postMessage({ action: 'focusFirst' });
|
|
setTimeout(() => this._iframeWindow.focus());
|
|
}
|
|
|
|
async setBottomPlaceholderHeight(height) {
|
|
await this._initPromise;
|
|
this._postMessage({ action: 'setBottomPlaceholderHeight', height });
|
|
}
|
|
|
|
async setToolbarPlaceholderWidth(width) {
|
|
await this._initPromise;
|
|
this._postMessage({ action: 'setToolbarPlaceholderWidth', width });
|
|
}
|
|
|
|
isHandToolActive() {
|
|
return this._iframeWindow.eval('PDFViewerApplication.pdfCursorTools.handTool.active');
|
|
}
|
|
|
|
isZoomAutoActive() {
|
|
return this._iframeWindow.eval('PDFViewerApplication.pdfViewer.currentScaleValue === "auto"');
|
|
}
|
|
|
|
isZoomPageWidthActive() {
|
|
return this._iframeWindow.eval('PDFViewerApplication.pdfViewer.currentScaleValue === "page-width"');
|
|
}
|
|
|
|
isZoomPageHeightActive() {
|
|
return this._iframeWindow.eval('PDFViewerApplication.pdfViewer.currentScaleValue === "page-fit"');
|
|
}
|
|
|
|
allowNavigateFirstPage() {
|
|
return this._iframeWindow.eval('PDFViewerApplication.pdfViewer.currentPageNumber > 1');
|
|
}
|
|
|
|
allowNavigateLastPage() {
|
|
return this._iframeWindow.eval('PDFViewerApplication.pdfViewer.currentPageNumber < PDFViewerApplication.pdfViewer.pagesCount');
|
|
}
|
|
|
|
allowNavigateBack() {
|
|
try {
|
|
let { uid } = this._iframeWindow.history.state;
|
|
if (uid == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
catch (e) {
|
|
}
|
|
return true;
|
|
}
|
|
|
|
allowNavigateForward() {
|
|
try {
|
|
let { uid } = this._iframeWindow.history.state;
|
|
let length = this._iframeWindow.history.length;
|
|
if (uid == length - 1) {
|
|
return false;
|
|
}
|
|
}
|
|
catch (e) {
|
|
}
|
|
return true;
|
|
}
|
|
|
|
promptToTransferAnnotations() {
|
|
let ps = Services.prompt;
|
|
let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
|
|
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
|
|
let index = ps.confirmEx(
|
|
null,
|
|
Zotero.getString('pdfReader.promptTransferFromPDF.title'),
|
|
Zotero.getString('pdfReader.promptTransferFromPDF.text', Zotero.appName),
|
|
buttonFlags,
|
|
Zotero.getString('general.continue'),
|
|
null, null, null, {}
|
|
);
|
|
return !index;
|
|
}
|
|
|
|
promptToDeletePages(num) {
|
|
let ps = Services.prompt;
|
|
let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
|
|
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
|
|
let index = ps.confirmEx(
|
|
null,
|
|
Zotero.getString('pdfReader.promptDeletePages.title'),
|
|
Zotero.getString(
|
|
'pdfReader.promptDeletePages.text',
|
|
new Intl.NumberFormat().format(num),
|
|
num
|
|
),
|
|
buttonFlags,
|
|
Zotero.getString('general.continue'),
|
|
null, null, null, {}
|
|
);
|
|
return !index;
|
|
}
|
|
|
|
async reload() {
|
|
let item = Zotero.Items.get(this._itemID);
|
|
let path = await item.getFilePathAsync();
|
|
let buf = await OS.File.read(path, {});
|
|
buf = new Uint8Array(buf).buffer;
|
|
this._postMessage({ action: 'reload', buf, }, [buf]);
|
|
}
|
|
|
|
async menuCmd(cmd) {
|
|
if (cmd === 'transferFromPDF') {
|
|
if (this.promptToTransferAnnotations(true)) {
|
|
try {
|
|
await Zotero.PDFWorker.import(this._itemID, true, '', true);
|
|
}
|
|
catch (e) {
|
|
if (e.name === 'PasswordException') {
|
|
Zotero.alert(null, Zotero.getString('general.error'),
|
|
Zotero.getString('pdfReader.promptPasswordProtected'));
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
else if (cmd === 'export') {
|
|
let zp = Zotero.getActiveZoteroPane();
|
|
zp.exportPDF(this._itemID);
|
|
return;
|
|
}
|
|
else if (cmd === 'showInLibrary') {
|
|
let win = Zotero.getMainWindow();
|
|
if (win) {
|
|
let item = Zotero.Items.get(this._itemID);
|
|
let id = item.parentID || item.id;
|
|
win.ZoteroPane.selectItems([id]);
|
|
win.Zotero_Tabs.select('zotero-pane');
|
|
win.focus();
|
|
}
|
|
return;
|
|
}
|
|
|
|
let data = {
|
|
action: 'menuCmd',
|
|
cmd
|
|
};
|
|
this._postMessage(data);
|
|
}
|
|
|
|
_initIframeWindow() {
|
|
this._iframeWindow.addEventListener('message', this._handleMessage);
|
|
this._iframeWindow.addEventListener('error', (event) => {
|
|
Zotero.logError(event.error);
|
|
});
|
|
this._iframeWindow.wrappedJSObject.zoteroSetDataTransferAnnotations = (dataTransfer, annotations) => {
|
|
// A small hack to force serializeAnnotations to include image annotation
|
|
// even if image isn't saved and imageAttachmentKey isn't available
|
|
for (let annotation of annotations) {
|
|
if (annotation.image && !annotation.imageAttachmentKey) {
|
|
annotation.imageAttachmentKey = 'none';
|
|
delete annotation.image;
|
|
}
|
|
}
|
|
let res = Zotero.EditorInstanceUtilities.serializeAnnotations(annotations);
|
|
let tmpNote = new Zotero.Item('note');
|
|
tmpNote.libraryID = Zotero.Libraries.userLibraryID;
|
|
tmpNote.setNote(res.html);
|
|
let items = [tmpNote];
|
|
let format = Zotero.QuickCopy.getNoteFormat();
|
|
Zotero.debug('Copying/dragging annotation(s) with ' + format);
|
|
format = Zotero.QuickCopy.unserializeSetting(format);
|
|
// Basically the same code is used in itemTree.jsx onDragStart
|
|
try {
|
|
if (format.mode === 'export') {
|
|
// If exporting with virtual "Markdown + Rich Text" translator, call Note Markdown
|
|
// and Note HTML translators instead
|
|
if (format.id === Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) {
|
|
let markdownFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN };
|
|
let htmlFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_HTML };
|
|
Zotero.QuickCopy.getContentFromItems(items, markdownFormat, (obj, worked) => {
|
|
if (!worked) {
|
|
return;
|
|
}
|
|
Zotero.QuickCopy.getContentFromItems(items, htmlFormat, (obj2, worked) => {
|
|
if (!worked) {
|
|
return;
|
|
}
|
|
dataTransfer.setData('text/plain', obj.string.replace(/\r\n/g, '\n'));
|
|
dataTransfer.setData('text/html', obj2.string.replace(/\r\n/g, '\n'));
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
Zotero.QuickCopy.getContentFromItems(items, format, (obj, worked) => {
|
|
if (!worked) {
|
|
return;
|
|
}
|
|
var text = obj.string.replace(/\r\n/g, '\n');
|
|
// For Note HTML translator use body content only
|
|
if (format.id === Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) {
|
|
// Use body content only
|
|
let parser = Cc['@mozilla.org/xmlextras/domparser;1']
|
|
.createInstance(Ci.nsIDOMParser);
|
|
let doc = parser.parseFromString(text, 'text/html');
|
|
text = doc.body.innerHTML;
|
|
}
|
|
dataTransfer.setData('text/plain', text);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
Zotero.debug(e);
|
|
}
|
|
};
|
|
this._iframeWindow.wrappedJSObject.zoteroConfirmDeletion = function (plural) {
|
|
let ps = Services.prompt;
|
|
let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
|
|
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
|
|
let index = ps.confirmEx(
|
|
null,
|
|
'',
|
|
Zotero.getString('pdfReader.deleteAnnotation.' + (plural ? 'plural' : 'singular')),
|
|
buttonFlags,
|
|
Zotero.getString('general.delete'),
|
|
null, null, null, {}
|
|
);
|
|
return !index;
|
|
};
|
|
}
|
|
|
|
async _setState(state) {
|
|
this.state = state;
|
|
let item = Zotero.Items.get(this._itemID);
|
|
if (item) {
|
|
item.setAttachmentLastPageIndex(state.pageIndex);
|
|
let file = Zotero.Attachments.getStorageDirectory(item);
|
|
if (!await OS.File.exists(file.path)) {
|
|
await Zotero.Attachments.createDirectoryForItem(item);
|
|
}
|
|
file.append(this.pdfStateFileName);
|
|
// Using `writeAtomic` instead of `putContentsAsync` to avoid
|
|
// using temp file that causes conflicts on simultaneous writes (on slow systems)
|
|
await OS.File.writeAtomic(file.path, JSON.stringify(state));
|
|
}
|
|
}
|
|
|
|
async _getState() {
|
|
let item = Zotero.Items.get(this._itemID);
|
|
let file = Zotero.Attachments.getStorageDirectory(item);
|
|
file.append(this.pdfStateFileName);
|
|
file = file.path;
|
|
let state;
|
|
try {
|
|
if (await OS.File.exists(file)) {
|
|
state = JSON.parse(await Zotero.File.getContentsAsync(file));
|
|
}
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(e);
|
|
}
|
|
|
|
let pageIndex = item.getAttachmentLastPageIndex();
|
|
if (state) {
|
|
if (Number.isInteger(pageIndex) && state.pageIndex !== pageIndex) {
|
|
state.pageIndex = pageIndex;
|
|
delete state.top;
|
|
delete state.left;
|
|
}
|
|
return state;
|
|
}
|
|
else if (Number.isInteger(pageIndex)) {
|
|
return { pageIndex };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
_isReadOnly() {
|
|
let item = Zotero.Items.get(this._itemID);
|
|
return !item.isEditable()
|
|
|| item.deleted
|
|
|| item.parentItem && item.parentItem.deleted;
|
|
}
|
|
|
|
_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 });
|
|
}
|
|
}
|
|
|
|
_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>`;
|
|
}
|
|
|
|
_openTagsPopup(item, selector) {
|
|
let menupopup = this._window.document.createElement('menupopup');
|
|
menupopup.className = 'tags-popup';
|
|
menupopup.style.minWidth = '300px';
|
|
menupopup.setAttribute('ignorekeys', true);
|
|
let tagsbox = this._window.document.createElement('tagsbox');
|
|
menupopup.appendChild(tagsbox);
|
|
tagsbox.setAttribute('flex', '1');
|
|
this._popupset.appendChild(menupopup);
|
|
let element = this._iframeWindow.document.querySelector(selector);
|
|
menupopup.openPopup(element, 'overlap', 0, 0, true);
|
|
tagsbox.mode = 'edit';
|
|
tagsbox.item = item;
|
|
if (tagsbox.mode == 'edit' && tagsbox.count == 0) {
|
|
tagsbox.newTag();
|
|
}
|
|
}
|
|
|
|
_openPagePopup(data) {
|
|
let popup = this._window.document.createElement('menupopup');
|
|
this._popupset.appendChild(popup);
|
|
popup.addEventListener('popuphidden', function () {
|
|
popup.remove();
|
|
});
|
|
let menuitem;
|
|
if (data.text) {
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('general.copy'));
|
|
menuitem.addEventListener('command', () => {
|
|
this._window.document.getElementById('menu_copy').click();
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Separator
|
|
popup.appendChild(this._window.document.createElement('menuseparator'));
|
|
}
|
|
// Zoom in
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomIn'));
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({ action: 'popupCmd', cmd: 'zoomIn' });
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Zoom out
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomOut'));
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({ action: 'popupCmd', cmd: 'zoomOut' });
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Zoom 'Auto'
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomAuto'));
|
|
menuitem.setAttribute('type', 'checkbox');
|
|
menuitem.setAttribute('checked', data.isZoomAuto);
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({ action: 'popupCmd', cmd: 'zoomAuto' });
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Zoom 'Page Width'
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomPageWidth'));
|
|
menuitem.setAttribute('type', 'checkbox');
|
|
menuitem.setAttribute('checked', data.isZoomPageWidth);
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({ action: 'popupCmd', cmd: 'zoomPageWidth' });
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Zoom 'Page Height'
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomPageHeight'));
|
|
menuitem.setAttribute('type', 'checkbox');
|
|
menuitem.setAttribute('checked', data.isZoomPageHeight);
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({ action: 'popupCmd', cmd: 'zoomPageHeight' });
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Separator
|
|
popup.appendChild(this._window.document.createElement('menuseparator'));
|
|
// Next page
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.nextPage'));
|
|
menuitem.setAttribute('disabled', !data.enableNextPage);
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({ action: 'popupCmd', cmd: 'nextPage' });
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Previous page
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.previousPage'));
|
|
menuitem.setAttribute('disabled', !data.enablePrevPage);
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({ action: 'popupCmd', cmd: 'prevPage' });
|
|
});
|
|
popup.appendChild(menuitem);
|
|
popup.openPopupAtScreen(data.x, data.y, true);
|
|
}
|
|
|
|
_openAnnotationPopup(data) {
|
|
let popup = this._window.document.createElement('menupopup');
|
|
this._popupset.appendChild(popup);
|
|
popup.addEventListener('popuphidden', function () {
|
|
popup.remove();
|
|
});
|
|
let menuitem;
|
|
// Add to note
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.addToNote'));
|
|
let hasActiveEditor = this._window.ZoteroContextPane && this._window.ZoteroContextPane.getActiveEditor();
|
|
menuitem.setAttribute('disabled', !hasActiveEditor || !data.enableAddToNote);
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({
|
|
action: 'popupCmd',
|
|
cmd: 'addToNote',
|
|
ids: data.ids
|
|
});
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Separator
|
|
popup.appendChild(this._window.document.createElement('menuseparator'));
|
|
// Colors
|
|
for (let color of data.colors) {
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString(color[0]));
|
|
menuitem.className = 'menuitem-iconic';
|
|
menuitem.setAttribute('disabled', data.readOnly);
|
|
menuitem.setAttribute('image', this._getColorIcon(color[1], color[1] === data.selectedColor));
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({
|
|
action: 'popupCmd',
|
|
cmd: 'setAnnotationColor',
|
|
ids: data.ids,
|
|
color: color[1]
|
|
});
|
|
});
|
|
popup.appendChild(menuitem);
|
|
}
|
|
// Separator
|
|
if (data.enableEditPageNumber || data.enableEditHighlightedText) {
|
|
popup.appendChild(this._window.document.createElement('menuseparator'));
|
|
}
|
|
// Change page number
|
|
if (data.enableEditPageNumber) {
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.editPageNumber'));
|
|
menuitem.setAttribute('disabled', data.readOnly);
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({
|
|
action: 'popupCmd',
|
|
cmd: 'openPageLabelPopup',
|
|
data
|
|
});
|
|
});
|
|
popup.appendChild(menuitem);
|
|
}
|
|
// Edit highlighted text
|
|
if (data.enableEditHighlightedText) {
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.editHighlightedText'));
|
|
menuitem.setAttribute('disabled', data.readOnly);
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({
|
|
action: 'popupCmd',
|
|
cmd: 'editHighlightedText',
|
|
data
|
|
});
|
|
});
|
|
popup.appendChild(menuitem);
|
|
}
|
|
// Separator
|
|
popup.appendChild(this._window.document.createElement('menuseparator'));
|
|
// Delete
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('general.delete'));
|
|
menuitem.setAttribute('disabled', data.readOnly);
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({
|
|
action: 'popupCmd',
|
|
cmd: 'deleteAnnotation',
|
|
ids: data.ids
|
|
});
|
|
});
|
|
popup.appendChild(menuitem);
|
|
|
|
if (data.x) {
|
|
popup.openPopupAtScreen(data.x, data.y, true);
|
|
}
|
|
else if (data.selector) {
|
|
let element = this._iframeWindow.document.querySelector(data.selector);
|
|
popup.openPopup(element, 'after_start', 0, 0, true);
|
|
}
|
|
}
|
|
|
|
_openColorPopup(data) {
|
|
let popup = this._window.document.createElement('menupopup');
|
|
this._popupset.appendChild(popup);
|
|
popup.addEventListener('popuphidden', function () {
|
|
popup.remove();
|
|
});
|
|
let menuitem;
|
|
for (let color of data.colors) {
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString(color[0]));
|
|
menuitem.className = 'menuitem-iconic';
|
|
menuitem.setAttribute('image', this._getColorIcon(color[1], color[1] === data.selectedColor));
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({
|
|
action: 'popupCmd',
|
|
cmd: 'setColor',
|
|
color: color[1]
|
|
});
|
|
});
|
|
popup.appendChild(menuitem);
|
|
}
|
|
let element = this._iframeWindow.document.getElementById(data.elementID);
|
|
popup.openPopup(element, 'after_start', 0, 0, true);
|
|
}
|
|
|
|
_openThumbnailPopup(data) {
|
|
let popup = this._window.document.createElement('menupopup');
|
|
this._popupset.appendChild(popup);
|
|
popup.addEventListener('popuphidden', function () {
|
|
popup.remove();
|
|
});
|
|
let menuitem;
|
|
// Rotate Left
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.rotateLeft'));
|
|
menuitem.addEventListener('command', async () => {
|
|
this._postMessage({ action: 'reloading' });
|
|
await Zotero.PDFWorker.rotatePages(this._itemID, data.pageIndexes, 270, true);
|
|
await this.reload();
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Rotate Right
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.rotateRight'));
|
|
menuitem.addEventListener('command', async () => {
|
|
this._postMessage({ action: 'reloading' });
|
|
await Zotero.PDFWorker.rotatePages(this._itemID, data.pageIndexes, 90, true);
|
|
await this.reload();
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Rotate 180
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('pdfReader.rotate180'));
|
|
menuitem.addEventListener('command', async () => {
|
|
this._postMessage({ action: 'reloading' });
|
|
await Zotero.PDFWorker.rotatePages(this._itemID, data.pageIndexes, 180, true);
|
|
await this.reload();
|
|
});
|
|
popup.appendChild(menuitem);
|
|
// Separator
|
|
popup.appendChild(this._window.document.createElement('menuseparator'));
|
|
// Delete
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('general.delete'));
|
|
menuitem.addEventListener('command', async () => {
|
|
if (this.promptToDeletePages(data.pageIndexes.length)) {
|
|
this._postMessage({ action: 'reloading' });
|
|
try {
|
|
await Zotero.PDFWorker.deletePages(this._itemID, data.pageIndexes, true);
|
|
}
|
|
catch (e) {
|
|
}
|
|
await this.reload();
|
|
}
|
|
});
|
|
popup.appendChild(menuitem);
|
|
popup.openPopupAtScreen(data.x, data.y, true);
|
|
}
|
|
|
|
_openSelectorPopup(data) {
|
|
let popup = this._window.document.createElement('menupopup');
|
|
this._popupset.appendChild(popup);
|
|
popup.addEventListener('popuphidden', function () {
|
|
popup.remove();
|
|
});
|
|
let menuitem;
|
|
// Clear Selection
|
|
menuitem = this._window.document.createElement('menuitem');
|
|
menuitem.setAttribute('label', Zotero.getString('general.clearSelection'));
|
|
menuitem.setAttribute('disabled', !data.enableClearSelection);
|
|
menuitem.addEventListener('command', () => {
|
|
this._postMessage({
|
|
action: 'popupCmd',
|
|
cmd: 'clearSelector',
|
|
ids: data.ids
|
|
});
|
|
});
|
|
popup.appendChild(menuitem);
|
|
popup.openPopupAtScreen(data.x, data.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;
|
|
}
|
|
message = data.message;
|
|
switch (message.action) {
|
|
case 'initialized': {
|
|
this._resolveInitPromise();
|
|
return;
|
|
}
|
|
case 'saveAnnotations': {
|
|
let attachment = Zotero.Items.get(data.itemID);
|
|
let { annotations } = message;
|
|
let notifierQueue = new Zotero.Notifier.Queue();
|
|
try {
|
|
for (let annotation of annotations) {
|
|
annotation.key = annotation.id;
|
|
let saveOptions = {
|
|
notifierQueue,
|
|
notifierData: {
|
|
instanceID: this._instanceID
|
|
}
|
|
};
|
|
|
|
if (annotation.onlyTextOrComment) {
|
|
saveOptions.notifierData.autoSyncDelay = Zotero.Notes.AUTO_SYNC_DELAY;
|
|
}
|
|
|
|
// Note: annotation.image is always saved separately from the rest
|
|
// of annotation properties
|
|
|
|
let item = Zotero.Items.getByLibraryAndKey(attachment.libraryID, annotation.key);
|
|
// Save image for read-only annotation.
|
|
if (item
|
|
&& !item.isEditable()
|
|
&& annotation.image
|
|
&& !await Zotero.Annotations.hasCacheImage(item)
|
|
) {
|
|
let blob = this._dataURLtoBlob(annotation.image);
|
|
await Zotero.Annotations.saveCacheImage(item, blob);
|
|
continue;
|
|
}
|
|
|
|
let savedAnnotation = await Zotero.Annotations.saveFromJSON(attachment, annotation, saveOptions);
|
|
if (annotation.image && !await Zotero.Annotations.hasCacheImage(savedAnnotation)) {
|
|
let blob = this._dataURLtoBlob(annotation.image);
|
|
await Zotero.Annotations.saveCacheImage(savedAnnotation, blob);
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
await Zotero.Notifier.commit(notifierQueue);
|
|
}
|
|
return;
|
|
}
|
|
case 'deleteAnnotations': {
|
|
let { ids: keys } = message;
|
|
let attachment = Zotero.Items.get(this._itemID);
|
|
let libraryID = attachment.libraryID;
|
|
let notifierQueue = new Zotero.Notifier.Queue();
|
|
try {
|
|
for (let key of keys) {
|
|
let annotation = Zotero.Items.getByLibraryAndKey(libraryID, key);
|
|
// Make sure the annotation actually belongs to the current PDF
|
|
if (annotation && annotation.isAnnotation() && annotation.parentID === this._itemID) {
|
|
this.annotationItemIDs = this.annotationItemIDs.filter(id => id !== annotation.id);
|
|
await annotation.eraseTx({ notifierQueue });
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
await Zotero.Notifier.commit(notifierQueue);
|
|
}
|
|
return;
|
|
}
|
|
case 'setState': {
|
|
let { state } = message;
|
|
await this._setState(state);
|
|
return;
|
|
}
|
|
case 'openTagsPopup': {
|
|
let { id: key, selector } = message;
|
|
let attachment = Zotero.Items.get(this._itemID);
|
|
let libraryID = attachment.libraryID;
|
|
let annotation = Zotero.Items.getByLibraryAndKey(libraryID, key);
|
|
if (annotation) {
|
|
this._openTagsPopup(annotation, selector);
|
|
}
|
|
return;
|
|
}
|
|
case 'openPagePopup': {
|
|
this._openPagePopup(message.data);
|
|
return;
|
|
}
|
|
case 'openAnnotationPopup': {
|
|
this._openAnnotationPopup(message.data);
|
|
return;
|
|
}
|
|
case 'openColorPopup': {
|
|
this._openColorPopup(message.data);
|
|
return;
|
|
}
|
|
case 'openThumbnailPopup': {
|
|
this._openThumbnailPopup(message.data);
|
|
return;
|
|
}
|
|
case 'closePopup': {
|
|
// Note: This currently only closes tags popup when annotations are
|
|
// disappearing from pdf-reader sidebar
|
|
for (let child of Array.from(this._popupset.children)) {
|
|
if (child.classList.contains('tags-popup')) {
|
|
child.hidePopup();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
case 'openURL': {
|
|
let { url } = message;
|
|
let win = Services.wm.getMostRecentWindow('navigator:browser');
|
|
if (win) {
|
|
win.ZoteroPane.loadURI(url);
|
|
}
|
|
return;
|
|
}
|
|
case 'addToNote': {
|
|
let { annotations } = message;
|
|
this._addToNote(annotations);
|
|
return;
|
|
}
|
|
case 'save': {
|
|
let zp = Zotero.getActiveZoteroPane();
|
|
zp.exportPDF(this._itemID);
|
|
return;
|
|
}
|
|
case 'toggleNoteSidebar': {
|
|
let { isToggled } = message;
|
|
this._toggleNoteSidebar(isToggled);
|
|
return;
|
|
}
|
|
case 'changeSidebarWidth': {
|
|
let { width } = message;
|
|
if (this.onChangeSidebarWidth) {
|
|
this.onChangeSidebarWidth(width);
|
|
}
|
|
return;
|
|
}
|
|
case 'changeSidebarOpen': {
|
|
let { open } = message;
|
|
if (this.onChangeSidebarOpen) {
|
|
this.onChangeSidebarOpen(open);
|
|
}
|
|
return;
|
|
}
|
|
case 'focusSplitButton': {
|
|
let win = Zotero.getMainWindow();
|
|
if (win) {
|
|
win.document.getElementById('zotero-tb-toggle-item-pane').focus();
|
|
}
|
|
return;
|
|
}
|
|
case 'focusContextPane': {
|
|
let win = Zotero.getMainWindow();
|
|
if (win) {
|
|
if (!this._window.ZoteroContextPane.focus()) {
|
|
this.focusFirst();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(e);
|
|
let crash = message && ['setAnnotation'].includes(message.action);
|
|
this._postMessage({
|
|
action: crash ? 'crash' : 'error',
|
|
message: `${Zotero.getString('general.error')}: '${message ? message.action : ''}'`,
|
|
moreInfo: {
|
|
message: e.message,
|
|
stack: e.stack,
|
|
fileName: e.fileName,
|
|
lineNumber: e.lineNumber
|
|
}
|
|
});
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
|
|
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 {Zotero.Item} item
|
|
* @returns {Object|null}
|
|
*/
|
|
async _getAnnotation(item) {
|
|
try {
|
|
if (!item || !item.isAnnotation()) {
|
|
return null;
|
|
}
|
|
let json = await Zotero.Annotations.toJSON(item);
|
|
json.id = item.key;
|
|
delete json.key;
|
|
for (let key in json) {
|
|
json[key] = json[key] || '';
|
|
}
|
|
json.tags = json.tags || [];
|
|
return json;
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(e);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class ReaderTab extends ReaderInstance {
|
|
constructor({ itemID, title, sidebarWidth, sidebarOpen, bottomPlaceholderHeight, index, tabID, background }) {
|
|
super();
|
|
this._itemID = itemID;
|
|
this._sidebarWidth = sidebarWidth;
|
|
this._sidebarOpen = sidebarOpen;
|
|
this._bottomPlaceholderHeight = bottomPlaceholderHeight;
|
|
this._showItemPaneToggle = true;
|
|
this._window = Services.wm.getMostRecentWindow('navigator:browser');
|
|
let { id, container } = this._window.Zotero_Tabs.add({
|
|
id: tabID,
|
|
type: 'reader',
|
|
title: title || '',
|
|
index,
|
|
data: {
|
|
itemID
|
|
},
|
|
select: !background
|
|
});
|
|
this.tabID = id;
|
|
this._tabContainer = container;
|
|
|
|
this._iframe = this._window.document.createElement('browser');
|
|
this._iframe.setAttribute('class', 'reader');
|
|
this._iframe.setAttribute('flex', '1');
|
|
this._iframe.setAttribute('type', 'content');
|
|
this._iframe.setAttribute('src', 'resource://zotero/pdf-reader/viewer.html');
|
|
this._tabContainer.appendChild(this._iframe);
|
|
|
|
this._popupset = this._window.document.createElement('popupset');
|
|
this._tabContainer.appendChild(this._popupset);
|
|
|
|
this._window.addEventListener('DOMContentLoaded', (event) => {
|
|
if (this._iframe && this._iframe.contentWindow && this._iframe.contentWindow.document === event.target) {
|
|
this._iframeWindow = this._iframe.contentWindow;
|
|
this._initIframeWindow();
|
|
}
|
|
});
|
|
|
|
this._iframe.setAttribute('tooltip', 'html-tooltip');
|
|
|
|
// This is a nonsense work-around to trigger mouseup and pointerup
|
|
// events in PDF reader iframe when mouse up happens over another iframe
|
|
// i.e. note-editor. There should be a better way to solve this
|
|
this._window.addEventListener('pointerup', (event) => {
|
|
try {
|
|
if (this._window.Zotero_Tabs.selectedID === this.tabID
|
|
&& this._iframeWindow
|
|
&& event.target
|
|
&& event.target.closest
|
|
&& !event.target.closest('#outerContainer')) {
|
|
let evt = new this._iframeWindow.CustomEvent('mouseup', { bubbles: false });
|
|
evt.clientX = event.clientX;
|
|
evt.clientY = event.clientY;
|
|
this._iframeWindow.dispatchEvent(evt);
|
|
|
|
evt = new this._iframeWindow.CustomEvent('pointerup', { bubbles: false });
|
|
evt.clientX = event.clientX;
|
|
evt.clientY = event.clientY;
|
|
this._iframeWindow.dispatchEvent(evt);
|
|
}
|
|
}
|
|
catch(e) {
|
|
}
|
|
});
|
|
}
|
|
|
|
close() {
|
|
if (this.tabID) {
|
|
this._window.Zotero_Tabs.close(this.tabID);
|
|
}
|
|
}
|
|
|
|
_toggleNoteSidebar(isToggled) {
|
|
let itemPane = this._window.document.getElementById('zotero-item-pane');
|
|
if (itemPane.hidden) {
|
|
itemPane.hidden = false;
|
|
}
|
|
else {
|
|
itemPane.hidden = true;
|
|
}
|
|
}
|
|
|
|
_setTitleValue(title) {
|
|
this._window.Zotero_Tabs.rename(this.tabID, title);
|
|
}
|
|
|
|
_addToNote(annotations) {
|
|
let noteEditor = this._window.ZoteroContextPane && this._window.ZoteroContextPane.getActiveEditor();
|
|
if (!noteEditor) {
|
|
return;
|
|
}
|
|
let editorInstance = noteEditor.getCurrentInstance();
|
|
if (editorInstance) {
|
|
editorInstance.focus();
|
|
editorInstance.insertAnnotations(annotations);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class ReaderWindow extends ReaderInstance {
|
|
constructor({ sidebarWidth, sidebarOpen, bottomPlaceholderHeight }) {
|
|
super();
|
|
this._sidebarWidth = sidebarWidth;
|
|
this._sidebarOpen = sidebarOpen;
|
|
this._bottomPlaceholderHeight = 0;
|
|
this.init();
|
|
}
|
|
|
|
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) => {
|
|
if (event.target === this._window.document) {
|
|
this._window.addEventListener('keypress', this._handleKeyPress);
|
|
this._popupset = this._window.document.getElementById('zotero-reader-popupset');
|
|
this._window.menuCmd = this.menuCmd.bind(this);
|
|
this._window.onGoMenuOpen = this._onGoMenuOpen.bind(this);
|
|
this._window.onViewMenuOpen = this._onViewMenuOpen.bind(this);
|
|
this._iframe = this._window.document.getElementById('reader');
|
|
}
|
|
|
|
if (this._iframe.contentWindow && this._iframe.contentWindow.document === event.target) {
|
|
this._iframeWindow = this._window.document.getElementById('reader').contentWindow;
|
|
this._initIframeWindow();
|
|
}
|
|
});
|
|
}
|
|
|
|
close() {
|
|
this._window.close();
|
|
}
|
|
|
|
_setTitleValue(title) {
|
|
this._window.document.title = title;
|
|
}
|
|
|
|
_handleKeyPress = (event) => {
|
|
if ((Zotero.isMac && event.metaKey || event.ctrlKey)
|
|
&& !event.shiftKey && !event.altKey && event.key === 'w') {
|
|
this._window.close();
|
|
}
|
|
}
|
|
|
|
_onViewMenuOpen() {
|
|
this._window.document.getElementById('view-menuitem-vertical-scrolling').setAttribute('checked', this.state.scrollMode == 0);
|
|
this._window.document.getElementById('view-menuitem-horizontal-scrolling').setAttribute('checked', this.state.scrollMode == 1);
|
|
this._window.document.getElementById('view-menuitem-wrapped-scrolling').setAttribute('checked', this.state.scrollMode == 2);
|
|
this._window.document.getElementById('view-menuitem-no-spreads').setAttribute('checked', this.state.spreadMode == 0);
|
|
this._window.document.getElementById('view-menuitem-odd-spreads').setAttribute('checked', this.state.spreadMode == 1);
|
|
this._window.document.getElementById('view-menuitem-even-spreads').setAttribute('checked', this.state.spreadMode == 2);
|
|
this._window.document.getElementById('view-menuitem-hand-tool').setAttribute('checked', this.isHandToolActive());
|
|
this._window.document.getElementById('view-menuitem-zoom-auto').setAttribute('checked', this.isZoomAutoActive());
|
|
this._window.document.getElementById('view-menuitem-zoom-page-width').setAttribute('checked', this.isZoomPageWidthActive());
|
|
this._window.document.getElementById('view-menuitem-zoom-page-height').setAttribute('checked', this.isZoomPageHeightActive());
|
|
}
|
|
|
|
_onGoMenuOpen() {
|
|
let keyBack = this._window.document.getElementById('key_back');
|
|
let keyForward = this._window.document.getElementById('key_forward');
|
|
|
|
if (Zotero.isMac) {
|
|
keyBack.setAttribute('key', '[');
|
|
keyBack.setAttribute('modifiers', 'meta');
|
|
keyForward.setAttribute('key', ']');
|
|
keyForward.setAttribute('modifiers', 'meta');
|
|
}
|
|
else {
|
|
keyBack.setAttribute('keycode', 'VK_LEFT');
|
|
keyBack.setAttribute('modifiers', 'alt');
|
|
keyForward.setAttribute('keycode', 'VK_RIGHT');
|
|
keyForward.setAttribute('modifiers', 'alt');
|
|
}
|
|
|
|
let menuItemBack = this._window.document.getElementById('go-menuitem-back');
|
|
let menuItemForward = this._window.document.getElementById('go-menuitem-forward');
|
|
menuItemBack.setAttribute('key', 'key_back');
|
|
menuItemForward.setAttribute('key', 'key_forward');
|
|
|
|
this._window.document.getElementById('go-menuitem-first-page').setAttribute('disabled', !this.allowNavigateFirstPage());
|
|
this._window.document.getElementById('go-menuitem-last-page').setAttribute('disabled', !this.allowNavigateLastPage());
|
|
this._window.document.getElementById('go-menuitem-back').setAttribute('disabled', !this.allowNavigateBack());
|
|
this._window.document.getElementById('go-menuitem-forward').setAttribute('disabled', !this.allowNavigateForward());
|
|
}
|
|
}
|
|
|
|
|
|
class Reader {
|
|
constructor() {
|
|
this._sidebarWidth = 240;
|
|
this._sidebarOpen = false;
|
|
this._bottomPlaceholderHeight = 0;
|
|
this._readers = [];
|
|
this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'reader');
|
|
this.onChangeSidebarWidth = null;
|
|
this.onChangeSidebarOpen = null;
|
|
|
|
this._debounceSidebarWidthUpdate = Zotero.Utilities.debounce(() => {
|
|
let readers = this._readers.filter(r => r instanceof ReaderTab);
|
|
for (let reader of readers) {
|
|
reader.setSidebarWidth(this._sidebarWidth);
|
|
}
|
|
this._setSidebarState();
|
|
}, 500);
|
|
}
|
|
|
|
getSidebarWidth() {
|
|
return this._sidebarWidth;
|
|
}
|
|
|
|
async init() {
|
|
await Zotero.uiReadyPromise;
|
|
Zotero.Session.state.windows
|
|
.filter(x => x.type == 'reader' && Zotero.Items.exists(x.itemID))
|
|
.forEach(x => this.open(x.itemID, null, { title: x.title, openInWindow: true }));
|
|
}
|
|
|
|
_loadSidebarState() {
|
|
let win = Zotero.getMainWindow();
|
|
if (win) {
|
|
let pane = win.document.getElementById('zotero-reader-sidebar-pane');
|
|
this._sidebarOpen = pane.getAttribute('collapsed') == 'false';
|
|
let width = pane.getAttribute('width');
|
|
if (width) {
|
|
this._sidebarWidth = parseInt(width);
|
|
}
|
|
}
|
|
}
|
|
|
|
_setSidebarState() {
|
|
let win = Zotero.getMainWindow();
|
|
if (win) {
|
|
let pane = win.document.getElementById('zotero-reader-sidebar-pane');
|
|
pane.setAttribute('collapsed', this._sidebarOpen ? 'false' : 'true');
|
|
pane.setAttribute('width', this._sidebarWidth);
|
|
}
|
|
}
|
|
|
|
getSidebarOpen() {
|
|
return this._sidebarOpen;
|
|
}
|
|
|
|
setSidebarWidth(width) {
|
|
this._sidebarWidth = width;
|
|
let readers = this._readers.filter(r => r instanceof ReaderTab);
|
|
for (let reader of readers) {
|
|
reader.setSidebarWidth(width);
|
|
}
|
|
this._setSidebarState();
|
|
}
|
|
|
|
setSidebarOpen(open) {
|
|
this._sidebarOpen = open;
|
|
let readers = this._readers.filter(r => r instanceof ReaderTab);
|
|
for (let reader of readers) {
|
|
reader.setSidebarOpen(open);
|
|
}
|
|
this._setSidebarState();
|
|
}
|
|
|
|
setBottomPlaceholderHeight(height) {
|
|
this._bottomPlaceholderHeight = height;
|
|
let readers = this._readers.filter(r => r instanceof ReaderTab);
|
|
for (let reader of readers) {
|
|
reader.setBottomPlaceholderHeight(height);
|
|
}
|
|
}
|
|
|
|
notify(event, type, ids, extraData) {
|
|
if (type === 'tab') {
|
|
if (event === 'close') {
|
|
for (let id of ids) {
|
|
let reader = Zotero.Reader.getByTabID(id);
|
|
if (reader) {
|
|
this._readers.splice(this._readers.indexOf(reader), 1);
|
|
}
|
|
}
|
|
}
|
|
else if (event === 'select') {
|
|
let reader = Zotero.Reader.getByTabID(ids[0]);
|
|
if (reader) {
|
|
this.triggerAnnotationsImportCheck(reader._itemID);
|
|
}
|
|
}
|
|
|
|
if (event === 'add' || event === 'close') {
|
|
Zotero.Session.debounceSave();
|
|
}
|
|
}
|
|
// Listen for parent item, PDF attachment and its annotations updates
|
|
else if (type === 'item') {
|
|
for (let reader of this._readers.slice()) {
|
|
if (event === 'delete' && ids.includes(reader._itemID)) {
|
|
reader.close();
|
|
}
|
|
|
|
// Ignore other notifications if the attachment no longer exists
|
|
let item = Zotero.Items.get(reader._itemID);
|
|
if (item) {
|
|
if (event === 'trash' && (ids.includes(item.id) || ids.includes(item.parentItemID))) {
|
|
reader.close();
|
|
}
|
|
else if (event === 'delete') {
|
|
let disappearedIDs = reader.annotationItemIDs.filter(x => ids.includes(x));
|
|
if (disappearedIDs.length) {
|
|
let keys = disappearedIDs.map(id => extraData[id].key);
|
|
reader.unsetAnnotations(keys);
|
|
}
|
|
}
|
|
else {
|
|
let annotationItems = item.getAnnotations();
|
|
reader.annotationItemIDs = annotationItems.map(x => x.id);
|
|
let affectedAnnotations = annotationItems.filter(({ id }) => (
|
|
ids.includes(id)
|
|
&& !(extraData && extraData[id] && extraData[id].instanceID === reader._instanceID)
|
|
));
|
|
if (affectedAnnotations.length) {
|
|
reader.setAnnotations(affectedAnnotations);
|
|
}
|
|
// Update title if the PDF attachment or the parent item changes
|
|
if (ids.includes(reader._itemID) || ids.includes(item.parentItemID)) {
|
|
reader.updateTitle();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
getByTabID(tabID) {
|
|
return this._readers.find(r => (r instanceof ReaderTab) && r.tabID === tabID);
|
|
}
|
|
|
|
getWindowStates() {
|
|
return this._readers
|
|
.filter(r => r instanceof ReaderWindow)
|
|
.map(r => ({ type: 'reader', itemID: r._itemID, title: r._title }));
|
|
}
|
|
|
|
async openURI(itemURI, location, options) {
|
|
let item = await Zotero.URI.getURIItem(itemURI);
|
|
if (!item) return;
|
|
await this.open(item.id, location, options);
|
|
}
|
|
|
|
async open(itemID, location, { title, tabIndex, tabID, openInBackground, openInWindow, allowDuplicate } = {}) {
|
|
this._loadSidebarState();
|
|
this.triggerAnnotationsImportCheck(itemID);
|
|
let reader;
|
|
|
|
if (openInWindow) {
|
|
reader = this._readers.find(r => r._itemID === itemID && (r instanceof ReaderWindow));
|
|
}
|
|
else if (!allowDuplicate) {
|
|
reader = this._readers.find(r => r._itemID === itemID);
|
|
}
|
|
|
|
if (reader) {
|
|
if (reader instanceof ReaderTab) {
|
|
reader._window.Zotero_Tabs.select(reader.tabID, true);
|
|
}
|
|
|
|
if (location) {
|
|
reader.navigate(location);
|
|
}
|
|
}
|
|
else if (openInWindow) {
|
|
reader = new ReaderWindow({
|
|
sidebarWidth: this._sidebarWidth,
|
|
sidebarOpen: this._sidebarOpen,
|
|
bottomPlaceholderHeight: this._bottomPlaceholderHeight
|
|
});
|
|
this._readers.push(reader);
|
|
if (!(await reader.open({ itemID, location }))) {
|
|
return;
|
|
}
|
|
Zotero.Session.debounceSave();
|
|
reader._window.addEventListener('unload', () => {
|
|
this._readers.splice(this._readers.indexOf(reader), 1);
|
|
Zotero.Session.debounceSave();
|
|
});
|
|
}
|
|
else {
|
|
reader = new ReaderTab({
|
|
itemID,
|
|
title,
|
|
index: tabIndex,
|
|
tabID,
|
|
background: openInBackground,
|
|
sidebarWidth: this._sidebarWidth,
|
|
sidebarOpen: this._sidebarOpen,
|
|
bottomPlaceholderHeight: this._bottomPlaceholderHeight
|
|
});
|
|
this._readers.push(reader);
|
|
if (!(await reader.open({ itemID, location }))) {
|
|
return;
|
|
}
|
|
reader.onChangeSidebarWidth = (width) => {
|
|
this._sidebarWidth = width;
|
|
this._debounceSidebarWidthUpdate();
|
|
if (this.onChangeSidebarWidth) {
|
|
this.onChangeSidebarWidth(width);
|
|
}
|
|
};
|
|
reader.onChangeSidebarOpen = (open) => {
|
|
this._sidebarOpen = open;
|
|
this.setSidebarOpen(open);
|
|
if (this.onChangeSidebarOpen) {
|
|
this.onChangeSidebarOpen(open);
|
|
}
|
|
};
|
|
}
|
|
|
|
if (!openInBackground) {
|
|
reader._window.focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Trigger annotations import
|
|
*
|
|
* @param {Integer} itemID Attachment item id
|
|
* @returns {Promise}
|
|
*/
|
|
async triggerAnnotationsImportCheck(itemID) {
|
|
let item = await Zotero.Items.getAsync(itemID);
|
|
if (!item.isEditable()
|
|
|| item.deleted
|
|
|| item.parentItem && item.parentItem.deleted
|
|
) {
|
|
return;
|
|
}
|
|
let mtime = await item.attachmentModificationTime;
|
|
if (item.attachmentLastProcessedModificationTime < Math.floor(mtime / 1000)) {
|
|
await Zotero.PDFWorker.import(itemID, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
Zotero.Reader = new Reader();
|