mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
- Fix "redo" command not enabled in the note editor - Centralize note editor undo and redo commands - Update note editor submodule Co-authored-by: Tom Najdek <tom@doppnet.com>
1965 lines
59 KiB
JavaScript
1965 lines
59 KiB
JavaScript
/*
|
||
***** BEGIN LICENSE BLOCK *****
|
||
|
||
Copyright © 2020 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 *****
|
||
*/
|
||
|
||
(function () {
|
||
var { InlineSpellChecker } = ChromeUtils.importESModule("resource://gre/modules/InlineSpellChecker.sys.mjs");
|
||
var { FilePicker } = ChromeUtils.importESModule('chrome://zotero/content/modules/filePicker.mjs');
|
||
|
||
let lazy = {};
|
||
ChromeUtils.defineESModuleGetters(lazy, {
|
||
generateHTMLFromTemplate: "chrome://zotero/content/modules/templates.mjs",
|
||
});
|
||
|
||
// Note: TinyMCE is automatically doing some meaningless corrections to
|
||
// note-editor produced HTML. Which might result to more
|
||
// conflicts, especially in group libraries
|
||
|
||
// Note: Synchronous save can still affect dateModified
|
||
|
||
// When changing this update in `note-editor` as well.
|
||
// This only filters images that are being imported from a URL.
|
||
// In all other cases `note-editor` should decide what
|
||
// image types can be imported, and if not then
|
||
// Zotero.Attachments.importEmbeddedImage does.
|
||
// Additionally, the already imported images should never be
|
||
// affected
|
||
const DOWNLOADED_IMAGE_TYPE = [
|
||
'image/jpeg',
|
||
'image/png'
|
||
];
|
||
|
||
class EditorInstance {
|
||
constructor() {
|
||
this.instanceID = Zotero.Utilities.randomString();
|
||
this._undoRedoController = null;
|
||
}
|
||
|
||
get itemID() {
|
||
return this._item.id;
|
||
}
|
||
|
||
get viewMode() {
|
||
return this._viewMode;
|
||
}
|
||
|
||
set viewMode(mode) {
|
||
this._viewMode = mode;
|
||
}
|
||
|
||
get tabID() {
|
||
return this._tabID;
|
||
}
|
||
|
||
async init(options) {
|
||
Zotero.Notes.registerEditorInstance(this);
|
||
this.onNavigate = options.onNavigate;
|
||
// TODO: Consider to use only itemID instead of loaded item
|
||
this._item = options.item;
|
||
this._reloaded = options.reloaded;
|
||
this._viewMode = options.viewMode;
|
||
this._tabID = options.tabID;
|
||
this._readOnly = options.readOnly || this._isReadOnly();
|
||
this._filesReadOnly = !Zotero.Libraries.get(this._item.libraryID).filesEditable;
|
||
this._disableUI = options.disableUI;
|
||
this._onReturn = options.onReturn;
|
||
this._iframeWindow = options.iframeWindow;
|
||
this._popup = options.popup;
|
||
this._state = options.state;
|
||
this._disableSaving = false;
|
||
this._subscriptions = [];
|
||
this._citationDialogWindow = null;
|
||
this._citationItemsList = [];
|
||
this._initPromise = new Promise((resolve, reject) => {
|
||
this._resolveInitPromise = resolve;
|
||
this._rejectInitPromise = reject;
|
||
});
|
||
this._prefObserverIDs = [
|
||
Zotero.Prefs.registerObserver('note.fontSize', this._handleFontChange),
|
||
Zotero.Prefs.registerObserver('note.tabFontSize', this._handleFontChange),
|
||
Zotero.Prefs.registerObserver('note.fontFamily', this._handleFontChange),
|
||
Zotero.Prefs.registerObserver('note.css', this._handleStyleChange),
|
||
Zotero.Prefs.registerObserver('layout.spellcheckDefault', this._handleSpellCheckChange, true)
|
||
];
|
||
this._spellChecker = null;
|
||
|
||
// Run Cut/Copy/Paste with chrome privileges
|
||
this._iframeWindow.wrappedJSObject.zoteroExecCommand = function (doc, command, ui, value) {
|
||
// Is that safe enough?
|
||
if (!['cut', 'copy', 'paste'].includes(command)) {
|
||
return;
|
||
}
|
||
return doc.execCommand(command, ui, value);
|
||
};
|
||
|
||
// Translate note HTML into Markdown, for setting it as text/plain in clipboard (on text copy/drag)
|
||
this._iframeWindow.wrappedJSObject.zoteroTranslateToMarkdown = (html) => {
|
||
let item = new Zotero.Item('note');
|
||
item.libraryID = this._item.libraryID;
|
||
item.setNote(html);
|
||
let text = '';
|
||
var translation = new Zotero.Translate.Export;
|
||
translation.noWait = true;
|
||
translation.setItems([item]);
|
||
translation.setTranslator(Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN);
|
||
translation.setHandler("done", (obj, worked) => {
|
||
if (worked) {
|
||
text = obj.string.replace(/\r\n/g, '\n');
|
||
}
|
||
});
|
||
translation.translate();
|
||
return text;
|
||
};
|
||
|
||
this._iframeWindow.wrappedJSObject.zoteroCopyImage = async (dataURL) => {
|
||
let parts = dataURL.split(',');
|
||
if (!parts[0].includes('base64')) {
|
||
return;
|
||
}
|
||
let mime = parts[0].match(/:(.*?);/)[1];
|
||
let bstr = atob(parts[1]);
|
||
let n = bstr.length;
|
||
let u8arr = new Uint8Array(n);
|
||
while (n--) {
|
||
u8arr[n] = bstr.charCodeAt(n);
|
||
}
|
||
let imgTools = Components.classes["@mozilla.org/image/tools;1"]
|
||
.getService(Components.interfaces.imgITools);
|
||
let transferable = Components.classes['@mozilla.org/widget/transferable;1']
|
||
.createInstance(Components.interfaces.nsITransferable);
|
||
let clipboardService = Components.classes['@mozilla.org/widget/clipboard;1']
|
||
.getService(Components.interfaces.nsIClipboard);
|
||
let img = imgTools.decodeImageFromArrayBuffer(u8arr.buffer, mime);
|
||
transferable.init(null);
|
||
let kNativeImageMime = 'application/x-moz-nativeimage';
|
||
transferable.addDataFlavor(kNativeImageMime);
|
||
transferable.setTransferData(kNativeImageMime, img);
|
||
clipboardService.setData(transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard);
|
||
};
|
||
|
||
this._iframeWindow.wrappedJSObject.zoteroSaveImageAs = async (dataURL) => {
|
||
let parts = dataURL.split(',');
|
||
if (!parts[0].includes('base64')) {
|
||
return;
|
||
}
|
||
let mime = parts[0].match(/:(.*?);/)[1];
|
||
let bstr = atob(parts[1]);
|
||
let n = bstr.length;
|
||
let u8arr = new Uint8Array(n);
|
||
while (n--) {
|
||
u8arr[n] = bstr.charCodeAt(n);
|
||
}
|
||
let ext = Zotero.MIME.getPrimaryExtension(mime, '');
|
||
let fp = new FilePicker();
|
||
fp.init(this._iframeWindow, Zotero.getString('note-editor-save-image-as'), fp.modeSave);
|
||
fp.appendFilters(fp.filterImages);
|
||
fp.defaultString = Zotero.getString('file-type-image').toLowerCase() + '.' + ext;
|
||
let rv = await fp.show();
|
||
if (rv === fp.returnOK || rv === fp.returnReplace) {
|
||
let outputPath = fp.file;
|
||
await OS.File.writeAtomic(outputPath, u8arr);
|
||
}
|
||
};
|
||
|
||
this._iframeWindow.addEventListener('message', this._messageHandler);
|
||
this._iframeWindow.addEventListener('error', (event) => {
|
||
Zotero.logError(event.error);
|
||
});
|
||
|
||
let note = this._item.note;
|
||
|
||
// TODO: From Firefox 64 this is no longer necessary
|
||
this._iframeWindow.document.execCommand('enableObjectResizing', false, 'false');
|
||
this._iframeWindow.document.execCommand('enableInlineTableEditing', false, 'false');
|
||
|
||
let style = Zotero.Prefs.get('note.css');
|
||
if (style) {
|
||
Zotero.debug('Using a custom CSS style:');
|
||
Zotero.debug(style);
|
||
}
|
||
|
||
let contentViewMode = this._getContentViewMode();
|
||
|
||
// Prepare Fluent data
|
||
let ftl = ['-app-name = ' + Zotero.appName];
|
||
|
||
try {
|
||
ftl.push(Zotero.File.getContentsFromURL(`chrome://zotero/locale/zotero.ftl`));
|
||
}
|
||
catch (e) {
|
||
Zotero.logError(e);
|
||
}
|
||
|
||
try {
|
||
ftl.push(Zotero.File.getContentsFromURL(`chrome://zotero/locale/note-editor.ftl`));
|
||
}
|
||
catch (e) {
|
||
Zotero.logError(e);
|
||
}
|
||
|
||
this._postMessage({
|
||
action: 'init',
|
||
value: this._state || this._item.note,
|
||
reloaded: this._reloaded,
|
||
viewMode: this._viewMode,
|
||
readOnly: this._readOnly,
|
||
unsaved: !this._item.id,
|
||
disableUI: this._disableUI,
|
||
enableReturnButton: !!this._onReturn,
|
||
isAttachmentNote: this._item.isAttachment(),
|
||
placeholder: options.placeholder,
|
||
dir: Zotero.dir,
|
||
font: this._getFont(),
|
||
contentViewMode,
|
||
style,
|
||
smartQuotes: Zotero.Prefs.get('note.smartQuotes'),
|
||
ftl
|
||
});
|
||
|
||
if (!this._item.isAttachment()) {
|
||
Zotero.Notes.ensureEmbeddedImagesAreAvailable(this._item);
|
||
}
|
||
}
|
||
|
||
async uninit() {
|
||
this._unregisterUndoRedoController();
|
||
this._prefObserverIDs.forEach(id => Zotero.Prefs.unregisterObserver(id));
|
||
if (this._citationDialogWindow) {
|
||
this._citationDialogWindow.close();
|
||
this._citationDialogWindow = null;
|
||
}
|
||
this._iframeWindow.removeEventListener('message', this._messageHandler);
|
||
this.saveSync();
|
||
await Zotero.Notes.unregisterEditorInstance(this);
|
||
if (!this._item.isAttachment() && !this._filesReadOnly) {
|
||
await Zotero.Notes.deleteUnusedEmbeddedImages(this._item);
|
||
}
|
||
}
|
||
|
||
_registerUndoRedoController() {
|
||
if (this._undoRedoController) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
let editorWindow = this._iframeWindow.wrappedJSObject;
|
||
let commands = new Map([
|
||
['cmd_undo', { can: 'canUndo', run: 'doUndo' }],
|
||
['cmd_redo', { can: 'canRedo', run: 'doRedo' }],
|
||
]);
|
||
let invoke = (command, operation) => {
|
||
let method = commands.get(command)?.[operation];
|
||
if (!method) {
|
||
return false;
|
||
}
|
||
try {
|
||
return typeof editorWindow[method] == 'function'
|
||
? editorWindow[method]()
|
||
: false;
|
||
}
|
||
catch (e) {
|
||
if (!Components.utils.isDeadWrapper(editorWindow)) {
|
||
Zotero.logError(e);
|
||
}
|
||
return false;
|
||
}
|
||
};
|
||
let controller = {
|
||
supportsCommand: command => commands.has(command),
|
||
isCommandEnabled: command => !!invoke(command, 'can'),
|
||
doCommand: command => invoke(command, 'run'),
|
||
onEvent() {},
|
||
};
|
||
|
||
this._iframeWindow.controllers.insertControllerAt(0, controller);
|
||
this._undoRedoController = controller;
|
||
}
|
||
catch (e) {
|
||
if (!Components.utils.isDeadWrapper(this._iframeWindow)) {
|
||
Zotero.logError(e);
|
||
}
|
||
}
|
||
}
|
||
|
||
_unregisterUndoRedoController() {
|
||
if (!this._undoRedoController) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
this._iframeWindow.controllers.removeController(this._undoRedoController);
|
||
}
|
||
catch (e) {
|
||
if (!Components.utils.isDeadWrapper(this._iframeWindow)) {
|
||
Zotero.logError(e);
|
||
}
|
||
}
|
||
finally {
|
||
this._undoRedoController = null;
|
||
}
|
||
}
|
||
|
||
async reinit(state = null) {
|
||
let currentOptions = {
|
||
state: state || this._state,
|
||
item: this._item,
|
||
reloaded: true,
|
||
iframeWindow: this._iframeWindow,
|
||
popup: this._popup,
|
||
onNavigate: this.onNavigate,
|
||
viewMode: this._viewMode,
|
||
readOnly: this._readOnly,
|
||
disableUI: this._disableUI,
|
||
onReturn: this._onReturn,
|
||
placeholder: this._placeholder
|
||
};
|
||
|
||
await this.uninit();
|
||
await this.init(currentOptions);
|
||
}
|
||
|
||
focus() {
|
||
this._iframeWindow.focus();
|
||
this._postMessage({ action: 'focus' });
|
||
}
|
||
|
||
async notify(event, type, ids, extraData) {
|
||
if (type === 'file' && event === 'download') {
|
||
let items = await Zotero.Items.getAsync(ids);
|
||
for (let item of items) {
|
||
if (item.isAttachment() && (await item.getFilePathAsync())) {
|
||
let subscription = this._subscriptions.find(x => x.data.attachmentKey === item.key);
|
||
if (subscription) {
|
||
await this._feedSubscription(subscription);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (type === 'item' && ['delete', 'trash'].includes(event) && this._tabID) {
|
||
if (this._item && (ids.includes(this._item.id) || ids.includes(this._item.parentItemID))) {
|
||
Zotero.getMainWindow().Zotero_Tabs.close(this._tabID);
|
||
}
|
||
}
|
||
|
||
if (this._readOnly || !this._item) {
|
||
return;
|
||
}
|
||
|
||
// Update citations itemData
|
||
let items = await Zotero.Items.getAsync(ids);
|
||
let uris = items.map(x => Zotero.URI.getItemURI(x)).filter(x => x);
|
||
let citationItemsList = this._citationItemsList
|
||
.filter(ci => ci.uris && uris.some(uri => ci.uris.includes(uri)));
|
||
await this._updateCitationItems(citationItemsList);
|
||
}
|
||
|
||
saveSync() {
|
||
if (!this._readOnly && !this._disableSaving && this._iframeWindow) {
|
||
let noteData = this._iframeWindow.wrappedJSObject.getDataSync(true);
|
||
if (noteData) {
|
||
noteData = JSON.parse(JSON.stringify(noteData));
|
||
}
|
||
this._save(noteData);
|
||
}
|
||
}
|
||
|
||
async insertAnnotations(annotations) {
|
||
await this._ensureNoteCreated();
|
||
await this.importImages(annotations);
|
||
let { html } = Zotero.EditorInstanceUtilities.serializeAnnotations(annotations);
|
||
if (html) {
|
||
this._postMessage({ action: 'insertHTML', pos: null, html });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Apply external changes incrementally instead of reinitializing the entire editor
|
||
* @param {Object} noteData - The note data containing state and html
|
||
* @param {Object} [noteData.state] - state object of editor
|
||
* @param {String} noteData.html - html for fallback update
|
||
* @param {boolean} preserveSelection - Whether to preserve the current selection
|
||
*/
|
||
applyIncrementalUpdate(noteData, preserveSelection = true) {
|
||
if (this._readOnly || !this._iframeWindow) {
|
||
this.reinit(noteData.state || null);
|
||
return;
|
||
}
|
||
|
||
this._postMessage({
|
||
action: 'updateIncrementally',
|
||
noteData,
|
||
preserveSelection
|
||
});
|
||
}
|
||
|
||
// TODO: implement navigating to note editor location
|
||
navigate(_location) {}
|
||
|
||
_postMessage(message) {
|
||
this._iframeWindow.postMessage({ instanceID: this.instanceID, message }, '*');
|
||
}
|
||
|
||
_isReadOnly() {
|
||
let item = this._item;
|
||
return !item.isEditable()
|
||
|| item.deleted
|
||
|| item.parentItem && item.parentItem.deleted;
|
||
}
|
||
|
||
_getFont() {
|
||
let fontSizePrefKey;
|
||
if (this._tabID) {
|
||
fontSizePrefKey = `note.tabFontSize`;
|
||
}
|
||
else {
|
||
fontSizePrefKey = `note.fontSize`;
|
||
}
|
||
let fontSize = Zotero.Prefs.get(fontSizePrefKey);
|
||
// Fix empty old font prefs before a value was enforced
|
||
if (fontSize < 6) {
|
||
fontSize = 11;
|
||
}
|
||
let fontFamily = Zotero.Prefs.get('note.fontFamily');
|
||
return { fontSize, fontFamily };
|
||
}
|
||
|
||
_getContentViewMode() {
|
||
// TODO: Use prefs
|
||
return this._tabID ? 'comfortable' : 'wide';
|
||
}
|
||
|
||
_handleFontChange = () => {
|
||
this._postMessage({ action: 'setFont', font: this._getFont() });
|
||
};
|
||
|
||
_handleContentViewModeChange = () => {
|
||
this._postMessage({ action: 'setContentViewMode', contentViewMode: this._getContentViewMode() });
|
||
};
|
||
|
||
_handleStyleChange = () => {
|
||
this._postMessage({ action: 'setStyle', style: Zotero.Prefs.get('note.css') });
|
||
};
|
||
|
||
_handleSpellCheckChange = () => {
|
||
try {
|
||
let spellChecker = this._getSpellChecker();
|
||
let value = Zotero.Prefs.get('layout.spellcheckDefault', true);
|
||
if (!value && spellChecker.enabled
|
||
|| value && !spellChecker.enabled) {
|
||
spellChecker.toggleEnabled();
|
||
}
|
||
}
|
||
catch (e) {
|
||
Zotero.logError(e);
|
||
}
|
||
};
|
||
|
||
_showInLibrary(ids) {
|
||
if (!Array.isArray(ids)) {
|
||
ids = [ids];
|
||
}
|
||
let win = Zotero.getMainWindow();
|
||
if (win) {
|
||
win.ZoteroPane.selectItems(ids);
|
||
win.focus();
|
||
}
|
||
}
|
||
|
||
setToggleContextPaneButtonMode(mode) {
|
||
this._postMessage({ action: 'setToggleContextPaneButtonMode', mode });
|
||
}
|
||
|
||
focusToolbar() {
|
||
this._iframeWindow.focus();
|
||
this._postMessage({ action: 'focusToolbar' });
|
||
}
|
||
|
||
async importImages(annotations) {
|
||
for (let annotation of annotations) {
|
||
if (annotation.image && !this._filesReadOnly) {
|
||
annotation.imageAttachmentKey = await this._importImage(annotation.image);
|
||
}
|
||
delete annotation.image;
|
||
}
|
||
}
|
||
|
||
async _digestItems(ids) {
|
||
let html = '';
|
||
let items = await Zotero.Items.getAsync(ids);
|
||
for (let item of items) {
|
||
if (item.isNote()
|
||
&& !(await Zotero.Notes.ensureEmbeddedImagesAreAvailable(item))
|
||
&& !Zotero.Notes.promptToIgnoreMissingImage()) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
for (let item of items) {
|
||
if (item.isRegularItem()) {
|
||
let itemData = Zotero.Utilities.Item.itemToCSLJSON(item);
|
||
let citation = {
|
||
citationItems: [{
|
||
uris: [Zotero.URI.getItemURI(item)],
|
||
itemData
|
||
}],
|
||
properties: {}
|
||
};
|
||
let formatted = Zotero.EditorInstanceUtilities.formatCitation(citation);
|
||
html += `<p><span class="citation" data-citation="${encodeURIComponent(JSON.stringify(citation))}">${formatted}</span></p>`;
|
||
}
|
||
else if (item.isNote()) {
|
||
let note = item.note;
|
||
|
||
let parser = new DOMParser();
|
||
let doc = parser.parseFromString(note, 'text/html');
|
||
|
||
// Get citationItems with itemData from note metadata
|
||
let storedCitationItems = [];
|
||
let containerNode = doc.querySelector('body > div[data-schema-version]');
|
||
if (containerNode) {
|
||
try {
|
||
let data = JSON.parse(decodeURIComponent(containerNode.getAttribute('data-citation-items')));
|
||
if (Array.isArray(data)) {
|
||
storedCitationItems = data;
|
||
}
|
||
}
|
||
catch (e) {
|
||
}
|
||
}
|
||
|
||
if (storedCitationItems.length) {
|
||
let fillWithItemData = (citationItems) => {
|
||
for (let citationItem of citationItems) {
|
||
let item = storedCitationItems.find(item => item.uris.some(uri => citationItem.uris.includes(uri)));
|
||
if (item) {
|
||
citationItem.itemData = item.itemData;
|
||
}
|
||
}
|
||
};
|
||
|
||
let nodes = doc.querySelectorAll('.citation[data-citation]');
|
||
for (let node of nodes) {
|
||
let citation = node.getAttribute('data-citation');
|
||
try {
|
||
citation = JSON.parse(decodeURIComponent(citation));
|
||
fillWithItemData(citation.citationItems);
|
||
citation = encodeURIComponent(JSON.stringify(citation));
|
||
node.setAttribute('data-citation', citation);
|
||
}
|
||
catch (e) {
|
||
Zotero.logError(e);
|
||
}
|
||
}
|
||
|
||
// img[data-annotation] and div.highlight[data-annotation]
|
||
nodes = doc.querySelectorAll('*[data-annotation]');
|
||
for (let node of nodes) {
|
||
let annotation = node.getAttribute('data-annotation');
|
||
try {
|
||
annotation = JSON.parse(decodeURIComponent(annotation));
|
||
// citationItem is allowed to not exist in annotation
|
||
if (annotation.citationItem) {
|
||
fillWithItemData([annotation.citationItem]);
|
||
annotation = encodeURIComponent(JSON.stringify(annotation));
|
||
node.setAttribute('data-annotation', annotation);
|
||
}
|
||
}
|
||
catch (e) {
|
||
Zotero.logError(e);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Clone all note image attachments and replace keys in the new note
|
||
if (!this._filesReadOnly) {
|
||
let attachments = Zotero.Items.get(item.getAttachments());
|
||
for (let attachment of attachments) {
|
||
if (!(await attachment.fileExists())) {
|
||
continue;
|
||
}
|
||
await Zotero.DB.executeTransaction(async () => {
|
||
let copiedAttachment = await Zotero.Attachments.copyEmbeddedImage({
|
||
attachment,
|
||
note: this._item,
|
||
saveOptions: {
|
||
notifierData: {
|
||
noteEditorID: this.instanceID
|
||
}
|
||
}
|
||
});
|
||
let node = doc.querySelector(`img[data-attachment-key="${attachment.key}"]`);
|
||
if (node) {
|
||
node.setAttribute('data-attachment-key', copiedAttachment.key);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
html += `<p></p>${doc.body.innerHTML}<p></p>`;
|
||
}
|
||
}
|
||
return html;
|
||
}
|
||
|
||
_messageHandler = async (e) => {
|
||
if (e.source !== this._iframeWindow
|
||
|| e.data.instanceID !== this.instanceID) {
|
||
return;
|
||
}
|
||
let message = e.data.message;
|
||
try {
|
||
switch (message.action) {
|
||
case 'initialized': {
|
||
this._registerUndoRedoController();
|
||
this._resolveInitPromise();
|
||
return;
|
||
}
|
||
case 'insertObject': {
|
||
let { type, data, pos } = message;
|
||
if (this._readOnly) {
|
||
return;
|
||
}
|
||
let html = '';
|
||
await this._ensureNoteCreated();
|
||
if (type === 'zotero/item') {
|
||
let ids = data.split(',').map(id => parseInt(id));
|
||
html = await this._digestItems(ids);
|
||
if (!html) {
|
||
return;
|
||
}
|
||
}
|
||
else if (type === 'zotero/annotation') {
|
||
let annotations = JSON.parse(data);
|
||
await this.importImages(annotations);
|
||
let { html: serializedHTML } = Zotero.EditorInstanceUtilities.serializeAnnotations(annotations);
|
||
html = serializedHTML;
|
||
}
|
||
if (html) {
|
||
this._postMessage({ action: 'insertHTML', pos, html });
|
||
}
|
||
return;
|
||
}
|
||
case 'openAnnotation': {
|
||
let { attachmentURI, position } = message;
|
||
if (this.onNavigate) {
|
||
this.onNavigate(attachmentURI, { position });
|
||
}
|
||
else {
|
||
let zp = Zotero.getActiveZoteroPane();
|
||
if (zp) {
|
||
let item = await Zotero.URI.getURIItem(attachmentURI);
|
||
if (item) {
|
||
zp.viewPDF(item.id, { position });
|
||
}
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
case 'openCitationPage': {
|
||
let { citation } = message;
|
||
if (!citation.citationItems.length) {
|
||
return;
|
||
}
|
||
let citationItem = citation.citationItems[0];
|
||
let item = await Zotero.EditorInstance.getItemFromURIs(citationItem.uris);
|
||
if (!item) {
|
||
return;
|
||
}
|
||
|
||
if (citationItem.locator) {
|
||
let attachments = await item.getBestAttachments();
|
||
attachments = attachments.filter(x => x.isPDFAttachment());
|
||
if (attachments.length) {
|
||
let zp = Zotero.getActiveZoteroPane();
|
||
if (zp) {
|
||
zp.viewPDF(attachments[0].id, { pageLabel: citationItem.locator });
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
this._showInLibrary(item.id);
|
||
}
|
||
return;
|
||
}
|
||
case 'showCitationItem': {
|
||
let { citation } = message;
|
||
let items = [];
|
||
for (let citationItem of citation.citationItems) {
|
||
let item = await Zotero.EditorInstance.getItemFromURIs(citationItem.uris);
|
||
if (item) {
|
||
items.push(item);
|
||
}
|
||
}
|
||
|
||
if (items.length) {
|
||
this._showInLibrary(items.map(item => item.id));
|
||
}
|
||
return;
|
||
}
|
||
case 'openURL': {
|
||
let { url } = message;
|
||
let zp = Zotero.getActiveZoteroPane();
|
||
if (zp) {
|
||
zp.loadURI(url);
|
||
}
|
||
return;
|
||
}
|
||
case 'showNote': {
|
||
this._showInLibrary(this._item.id);
|
||
return;
|
||
}
|
||
case 'openWindow': {
|
||
// TODO: Can we can avoid creating empty note just to open it in a new window?
|
||
await this._ensureNoteCreated();
|
||
let zp = Zotero.getActiveZoteroPane();
|
||
zp.openNoteWindow(this._item.id);
|
||
return;
|
||
}
|
||
case 'update': {
|
||
let { noteData, system } = message;
|
||
if (this._readOnly) {
|
||
return;
|
||
}
|
||
await this._save(noteData, system);
|
||
return;
|
||
}
|
||
case 'subscribe': {
|
||
let { subscription } = message;
|
||
subscription = JSON.parse(JSON.stringify(subscription));
|
||
this._subscriptions.push(subscription);
|
||
if (subscription.type === 'image') {
|
||
await this._feedSubscription(subscription);
|
||
}
|
||
return;
|
||
}
|
||
case 'unsubscribe': {
|
||
let { id } = message;
|
||
this._subscriptions.splice(this._subscriptions.findIndex(s => s.id === id), 1);
|
||
return;
|
||
}
|
||
// Called on note editor load
|
||
case 'updateCitationItemsList': {
|
||
let { list } = message;
|
||
list = list.slice();
|
||
let newList = [];
|
||
for (let item of list) {
|
||
let existingItem = this._citationItemsList
|
||
.find(ci => ci.uris.some(uri => item.uris.includes(uri)));
|
||
if (!existingItem) {
|
||
newList.push(item);
|
||
}
|
||
}
|
||
await this._updateCitationItems(newList);
|
||
this._citationItemsList = list;
|
||
return;
|
||
}
|
||
case 'openCitationPopup': {
|
||
let { nodeID, citation } = message;
|
||
if (this._readOnly) {
|
||
return;
|
||
}
|
||
citation = JSON.parse(JSON.stringify(citation));
|
||
for (let citationItem of citation.citationItems) {
|
||
let item = await Zotero.EditorInstance.getItemFromURIs(citationItem.uris);
|
||
if (item) {
|
||
citationItem.id = item.id;
|
||
}
|
||
}
|
||
let openedEmpty = !citation.citationItems.length;
|
||
if (!citation.citationItems.length) {
|
||
let win = Zotero.getMainWindow();
|
||
if (win) {
|
||
let reader = Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID);
|
||
if (reader) {
|
||
let item = Zotero.Items.get(reader.itemID);
|
||
if (item && item.parentItem) {
|
||
item = item.parentItem;
|
||
let citationItem = {};
|
||
citationItem.id = item.id;
|
||
citationItem.uris = [Zotero.URI.getItemURI(item)];
|
||
citationItem.itemData = Zotero.Utilities.Item.itemToCSLJSON(item);
|
||
citation.citationItems.push(citationItem);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let libraryID = this._item.libraryID;
|
||
this._openCitationDialog(nodeID, citation, [libraryID], openedEmpty);
|
||
return;
|
||
}
|
||
case 'importImages': {
|
||
await this._ensureNoteCreated();
|
||
let { images } = message;
|
||
if (this._readOnly || this._filesReadOnly) {
|
||
return;
|
||
}
|
||
if (this._item.isAttachment()) {
|
||
return;
|
||
}
|
||
for (let image of images) {
|
||
let { nodeID, src } = image;
|
||
let attachmentKey = await this._importImage(src, true);
|
||
// TODO: Inform editor about the failed to import images
|
||
if (attachmentKey) {
|
||
this._postMessage({ action: 'attachImportedImage', nodeID, attachmentKey });
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
case 'openContextMenu': {
|
||
let { x, y, pos, itemGroups } = message;
|
||
this._openPopup(x, y, pos, itemGroups);
|
||
return;
|
||
}
|
||
case 'toggleContextPane': {
|
||
let win = Zotero.getMainWindow();
|
||
win.ZoteroContextPane.togglePane();
|
||
return;
|
||
}
|
||
case 'focusBack': {
|
||
// If editor is in a tab, let Zotero_Tabs handle the focus
|
||
if (this._tabID) {
|
||
let win = Zotero.getMainWindow();
|
||
win.Zotero_Tabs.focusBack();
|
||
}
|
||
// If the editor is in a standalone window, itemPane, or contextPane,
|
||
// move focus back from the iframe
|
||
else {
|
||
let iframe = this._iframeWindow.browsingContext.embedderElement;
|
||
// Forget the previously focused element within the iframe to avoid
|
||
// it receiving focus for a moment if you later tab back into the editor
|
||
iframe.ownerDocument.activeElement.blur();
|
||
Services.focus.moveFocus(iframe.ownerGlobal, iframe, Services.focus.MOVEFOCUS_BACKWARD, Services.focus.FLAG_SHOWRING);
|
||
};
|
||
return;
|
||
}
|
||
case 'focusForward': {
|
||
let win = Zotero.getMainWindow();
|
||
win.Zotero_Tabs.focusForward();
|
||
return;
|
||
}
|
||
case 'return': {
|
||
this._onReturn();
|
||
return;
|
||
}
|
||
case 'incrementalUpdateFailed': {
|
||
// Incremental update failed, do full reinitialize
|
||
this.reinit();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
catch (e) {
|
||
Zotero.logError(e);
|
||
if (message && ['update', 'importImages'].includes(message.action)) {
|
||
this._postMessage({ action: 'crash' });
|
||
}
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
async _updateCitationItems(citationItemsList) {
|
||
let citationItems = [];
|
||
for (let { uris } of citationItemsList) {
|
||
let item = await Zotero.EditorInstance.getItemFromURIs(uris);
|
||
if (item) {
|
||
let itemData = Zotero.Utilities.Item.itemToCSLJSON(item);
|
||
citationItems.push({ uris, itemData });
|
||
}
|
||
}
|
||
if (citationItems.length) {
|
||
this._postMessage({ action: 'updateCitationItems', citationItems });
|
||
}
|
||
}
|
||
|
||
async _feedSubscription(subscription) {
|
||
let { id, type, data } = subscription;
|
||
if (type === 'image') {
|
||
let { attachmentKey } = data;
|
||
let n = 0;
|
||
// For now wait up to 60 seconds, as there is no point to wait for very long sync to finish
|
||
while (n++ < 60) {
|
||
let item = Zotero.Items.getByLibraryAndKey(this._item.libraryID, attachmentKey);
|
||
// Attachment item (not the file) might not be synced at the time
|
||
if (!item && Zotero.Sync.Runner.syncInProgress) {
|
||
await Zotero.Promise.delay(1000);
|
||
continue;
|
||
}
|
||
// Check if the attachment is actually the child
|
||
if (item.parentID === this._item.id) {
|
||
if (await item.getFilePathAsync()) {
|
||
let src = await this._getDataURL(item);
|
||
this._postMessage({ action: 'notifySubscription', id, data: { src } });
|
||
}
|
||
else {
|
||
await Zotero.Notes.ensureEmbeddedImagesAreAvailable(this._item);
|
||
// this._postMessage({ action: 'notifySubscription', id, data: { src: 'error' } });
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
async _importImage(src, download) {
|
||
let blob;
|
||
if (src.startsWith('data:')) {
|
||
blob = this._dataURLtoBlob(src);
|
||
}
|
||
else if (download) {
|
||
let res;
|
||
|
||
try {
|
||
res = await Zotero.HTTP.request('GET', src, { responseType: 'blob' });
|
||
}
|
||
catch (e) {
|
||
return;
|
||
}
|
||
|
||
blob = res.response;
|
||
|
||
if (!DOWNLOADED_IMAGE_TYPE.includes(blob.type)) {
|
||
return;
|
||
}
|
||
}
|
||
else {
|
||
return;
|
||
}
|
||
|
||
let attachment = await Zotero.Attachments.importEmbeddedImage({
|
||
blob,
|
||
parentItemID: this._item.id,
|
||
saveOptions: {
|
||
notifierData: {
|
||
noteEditorID: this.instanceID
|
||
}
|
||
}
|
||
});
|
||
|
||
return attachment.key;
|
||
}
|
||
|
||
async _openPopup(x, y, pos, itemGroups) {
|
||
let appendItems = (parentNode, itemGroups) => {
|
||
for (let itemGroup of itemGroups) {
|
||
for (let item of itemGroup) {
|
||
if (item.groups) {
|
||
let menu = parentNode.ownerDocument.createXULElement('menu');
|
||
menu.setAttribute('label', item.label);
|
||
let menupopup = parentNode.ownerDocument.createXULElement('menupopup');
|
||
menu.append(menupopup);
|
||
appendItems(menupopup, item.groups);
|
||
parentNode.appendChild(menu);
|
||
}
|
||
else {
|
||
let menuitem = parentNode.ownerDocument.createXULElement('menuitem');
|
||
menuitem.setAttribute('value', item.name);
|
||
menuitem.setAttribute('label', item.label);
|
||
menuitem.setAttribute('disabled', !item.enabled);
|
||
if (item.checked) {
|
||
menuitem.setAttribute('type', 'checkbox');
|
||
}
|
||
menuitem.setAttribute('checked', item.checked);
|
||
menuitem.addEventListener('command', () => {
|
||
if (item.name === 'insertImage') {
|
||
return this._iframeWindow.eval('openImageFilePicker()');
|
||
}
|
||
this._postMessage({
|
||
action: 'contextMenuAction',
|
||
ctxAction: item.name,
|
||
pos
|
||
});
|
||
});
|
||
parentNode.appendChild(menuitem);
|
||
}
|
||
}
|
||
|
||
if (itemGroups.indexOf(itemGroup) !== itemGroups.length - 1) {
|
||
let separator = parentNode.ownerDocument.createXULElement('menuseparator');
|
||
parentNode.appendChild(separator);
|
||
}
|
||
}
|
||
};
|
||
|
||
this._popup.hidePopup();
|
||
|
||
while (this._popup.firstChild) {
|
||
this._popup.removeChild(this._popup.firstChild);
|
||
}
|
||
|
||
appendItems(this._popup, itemGroups);
|
||
|
||
// Spell checker
|
||
let spellChecker = this._getSpellChecker();
|
||
|
||
// If `contenteditable` area wasn't focused before, the spell checker
|
||
// might not be fully initialized on right-click.
|
||
// The wait time depends on system performance/load
|
||
let n = 0;
|
||
// Wait for 200ms
|
||
while (n++ < 20) {
|
||
try {
|
||
if (spellChecker.mInlineSpellChecker.spellChecker.GetCurrentDictionary()) {
|
||
break;
|
||
}
|
||
}
|
||
catch (e) {
|
||
break;
|
||
}
|
||
await Zotero.Promise.delay(10);
|
||
}
|
||
|
||
// Separator
|
||
var separator = this._popup.ownerDocument.createXULElement('menuseparator');
|
||
this._popup.appendChild(separator);
|
||
// Check Spelling
|
||
var menuitem = this._popup.ownerDocument.createXULElement('menuitem');
|
||
menuitem.setAttribute('label', Zotero.getString('spellCheck.checkSpelling'));
|
||
menuitem.setAttribute('checked', spellChecker.enabled);
|
||
menuitem.setAttribute('type', 'checkbox');
|
||
menuitem.addEventListener('command', () => {
|
||
// Possible values: 0 - off, 1 - only multi-line, 2 - multi and single line input boxes
|
||
Zotero.Prefs.set('layout.spellcheckDefault', spellChecker.enabled ? 0 : 1, true);
|
||
});
|
||
this._popup.append(menuitem);
|
||
|
||
if (spellChecker.enabled) {
|
||
// Languages menu
|
||
var menu = this._popup.ownerDocument.createXULElement('menu');
|
||
menu.setAttribute('label', Zotero.getString('general.languages'));
|
||
this._popup.append(menu);
|
||
// Languages menu popup
|
||
var menupopup = this._popup.ownerDocument.createXULElement('menupopup');
|
||
menu.append(menupopup);
|
||
|
||
spellChecker.addDictionaryListToMenu(menupopup, null);
|
||
|
||
// The menu is prepopulated with names from InlineSpellChecker::getDictionaryDisplayName(),
|
||
// which will be in English, so swap in native locale names where we have them
|
||
for (var menuitem of menupopup.children) {
|
||
// 'spell-check-dictionary-en-US'
|
||
let locale = menuitem.id.slice(23);
|
||
let label = Zotero.Dictionaries.getBestDictionaryName(locale);
|
||
if (label && label != locale) {
|
||
menuitem.setAttribute('label', label);
|
||
}
|
||
}
|
||
|
||
// Separator
|
||
var separator = this._popup.ownerDocument.createXULElement('menuseparator');
|
||
menupopup.appendChild(separator);
|
||
// Add Dictionaries
|
||
var menuitem = this._popup.ownerDocument.createXULElement('menuitem');
|
||
menuitem.setAttribute('label', Zotero.getString('spellCheck.addRemoveDictionaries'));
|
||
menuitem.addEventListener('command', () => {
|
||
Services.ww.openWindow(null, "chrome://zotero/content/dictionaryManager.xhtml",
|
||
"dictionary-manager", "chrome,centerscreen", {});
|
||
|
||
});
|
||
menupopup.append(menuitem);
|
||
|
||
let selection = this._iframeWindow.getSelection();
|
||
if (selection) {
|
||
spellChecker.initFromEvent(
|
||
selection.anchorNode,
|
||
selection.anchorOffset
|
||
);
|
||
}
|
||
|
||
let firstElementChild = this._popup.firstElementChild;
|
||
let showSeparator = false;
|
||
let suggestionCount = spellChecker.addSuggestionsToMenuOnParent(this._popup, firstElementChild, 5);
|
||
if (suggestionCount) {
|
||
showSeparator = true;
|
||
}
|
||
if (spellChecker.overMisspelling) {
|
||
let addToDictionary = this._popup.ownerDocument.createXULElement('menuitem');
|
||
addToDictionary.setAttribute('data-l10n-id', 'text-action-spell-add-to-dictionary');
|
||
addToDictionary.addEventListener('command', () => {
|
||
spellChecker.addToDictionary();
|
||
});
|
||
this._popup.insertBefore(addToDictionary, firstElementChild);
|
||
showSeparator = true;
|
||
}
|
||
if (spellChecker.canUndo()) {
|
||
let undo = this._popup.ownerDocument.createXULElement('menuitem');
|
||
undo.setAttribute('data-l10n-id', 'text-action-spell-undo-add-to-dictionary');
|
||
undo.addEventListener('command', () => {
|
||
spellChecker.undoAddToDictionary();
|
||
});
|
||
this._popup.insertBefore(undo, firstElementChild);
|
||
showSeparator = true;
|
||
}
|
||
|
||
if (showSeparator) {
|
||
let separator = this._popup.ownerDocument.createXULElement('menuseparator');
|
||
this._popup.insertBefore(separator, firstElementChild);
|
||
}
|
||
}
|
||
|
||
this._popup.openPopupAtScreen(x, y, true);
|
||
}
|
||
|
||
_getSpellChecker() {
|
||
// Fix cannot access dead object error
|
||
if (Components.utils.isDeadWrapper(this._iframeWindow)) return null;
|
||
if (!this._spellChecker) {
|
||
let editingSession = this._iframeWindow.docShell.editingSession;
|
||
this._spellChecker = new InlineSpellChecker(
|
||
editingSession.getEditorForWindow(this._iframeWindow)
|
||
);
|
||
}
|
||
return this._spellChecker;
|
||
}
|
||
|
||
async _ensureNoteCreated() {
|
||
if (!this._item.id) {
|
||
return this._item.saveTx();
|
||
}
|
||
}
|
||
|
||
async _save(noteData, skipDateModifiedUpdate) {
|
||
if (!noteData) return;
|
||
let { state, html } = noteData;
|
||
if (html === undefined) return;
|
||
try {
|
||
if (this._disableSaving) {
|
||
Zotero.debug('Saving is disabled');
|
||
return;
|
||
}
|
||
|
||
if (this._readOnly) {
|
||
Zotero.debug('Not saving read-only note');
|
||
return;
|
||
}
|
||
if (html === null) {
|
||
Zotero.debug('Note value not available -- not saving', 2);
|
||
return;
|
||
}
|
||
// Update note
|
||
if (this._item) {
|
||
await Zotero.DB.executeTransaction(async () => {
|
||
let changed = this._item.setNote(html);
|
||
if (changed && !this._disableSaving) {
|
||
await this._item.save({
|
||
skipDateModifiedUpdate,
|
||
notifierData: {
|
||
// Use a longer timeout to avoid repeated syncing during typing
|
||
autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
|
||
noteEditorID: this.instanceID,
|
||
state
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
// Create a new note
|
||
else {
|
||
var item = new Zotero.Item('note');
|
||
if (this.parentItem) {
|
||
item.libraryID = this.parentItem.libraryID;
|
||
}
|
||
item.setNote(html);
|
||
if (this.parentItem) {
|
||
item.parentKey = this.parentItem.key;
|
||
}
|
||
if (!this._disableSaving) {
|
||
var id = await item.saveTx({
|
||
notifierData: {
|
||
autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY
|
||
}
|
||
});
|
||
if (!this.parentItem && this.collection) {
|
||
this.collection.addItem(id);
|
||
}
|
||
this._item = item;
|
||
}
|
||
}
|
||
}
|
||
catch (e) {
|
||
Zotero.logError(e);
|
||
Zotero.crash(true);
|
||
throw e;
|
||
}
|
||
|
||
// Reset spell checker as ProseMirror DOM modifications are
|
||
// often ignored otherwise
|
||
try {
|
||
let spellChecker = this._getSpellChecker();
|
||
spellChecker.toggleEnabled();
|
||
spellChecker.toggleEnabled();
|
||
} catch(e) {
|
||
Zotero.logError(e);
|
||
}
|
||
}
|
||
|
||
_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 ((Zotero.getMainWindow()).Blob)([u8arr], { type: mime });
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async _getDataURL(item) {
|
||
let path = await item.getFilePathAsync();
|
||
let buf = await OS.File.read(path, {});
|
||
buf = new Uint8Array(buf).buffer;
|
||
return new Promise((resolve, reject) => {
|
||
let blob = new Blob([buf], { type: item.attachmentContentType });
|
||
let reader = new FileReader();
|
||
reader.onloadend = function () {
|
||
resolve(reader.result);
|
||
}
|
||
reader.onerror = function (e) {
|
||
reject("FileReader error: " + e);
|
||
};
|
||
reader.readAsDataURL(blob);
|
||
});
|
||
}
|
||
|
||
async _openCitationDialog(nodeID, citationData, filterLibraryIDs, openedEmpty) {
|
||
await Zotero.Styles.init();
|
||
let that = this;
|
||
let win;
|
||
|
||
/**
|
||
* Citation editing functions and properties accessible to citationDialog.js
|
||
*/
|
||
let CI = function (citation) {
|
||
this.citation = citation;
|
||
this.filterLibraryIDs = filterLibraryIDs;
|
||
this.disableDialogTypeSwitch = true;
|
||
|
||
// Cited items updated in `getItems`
|
||
this.citedItems = [];
|
||
};
|
||
|
||
CI.prototype = {
|
||
|
||
/**
|
||
* 1) Provide citation dialog with items created from
|
||
* `itemData`, without dealing with `Zotero.Integration.sessions`
|
||
*
|
||
* 2) Allow to pick already cited item from citation dialog
|
||
*
|
||
* @param citationItem
|
||
* @returns {Zotero.Item|undefined}
|
||
*/
|
||
customGetItem(citationItem) {
|
||
// Using `id` as cited item index from `getItems` below
|
||
let citedItem = typeof citationItem.id === 'string'
|
||
&& this.citedItems[parseInt(citationItem.id.split('cited:')[1])];
|
||
|
||
// Return cited item picked in citation dialog
|
||
if (citedItem) {
|
||
return citedItem.item;
|
||
}
|
||
// Provide an item created from `itemData`
|
||
else if (!citationItem.id && citationItem.itemData) {
|
||
let item = new Zotero.Item();
|
||
Zotero.Utilities.itemFromCSLJSON(item, citationItem.itemData);
|
||
// Add csl data in the same format as in Zotero.Integration.Citation.loadItemData
|
||
item.cslItemID = citationItem.id;
|
||
item.cslURIs = citationItem.uris;
|
||
item.cslItemData = citationItem.itemData;
|
||
return item;
|
||
}
|
||
// Otherwise returns `undefined` which makes this function to be
|
||
},
|
||
|
||
/**
|
||
* Sort the citationItems within citation (depends on this.citation.properties.unsorted)
|
||
* @return {Promise} A promise resolved with the previewed citation string
|
||
*/
|
||
sort: async function () {
|
||
// Zotero.debug('CI: sort');
|
||
// Normally `this.citation.citationItems` should be sorted by
|
||
// citation preview, but in our editor it doesn't make sense
|
||
// to do so, because we don't have a real style here and
|
||
// it's not the final document
|
||
},
|
||
|
||
/**
|
||
* Accept changes to the citation
|
||
* @param {Function} [progressCallback] A callback to be run when progress has changed.
|
||
* Receives a number from 0 to 100 indicating current status.
|
||
*/
|
||
accept: async function (progressCallback) {
|
||
// Zotero.debug('CI: accept');
|
||
if (progressCallback) progressCallback(100);
|
||
|
||
if (win) {
|
||
win.close();
|
||
}
|
||
|
||
let citation = {
|
||
citationItems: this.citation.citationItems,
|
||
properties: this.citation.properties
|
||
};
|
||
|
||
for (let citationItem of citation.citationItems) {
|
||
let citedItem = typeof citationItem.id === 'string'
|
||
&& this.citedItems[parseInt(citationItem.id.split('cited:')[1])];
|
||
|
||
// Cited item
|
||
if (citedItem) {
|
||
let ci = citedItem.citationItem;
|
||
citationItem.uris = ci.uris;
|
||
citationItem.itemData = ci.itemData;
|
||
}
|
||
// New item
|
||
else if (citationItem.id) {
|
||
let item = await Zotero.Items.getAsync(parseInt(citationItem.id));
|
||
citationItem.uris = [Zotero.URI.getItemURI(item)];
|
||
citationItem.itemData = Zotero.Utilities.Item.itemToCSLJSON(item);
|
||
}
|
||
// Otherwise it's existing item, so just passing untouched citationItem
|
||
|
||
delete citationItem.id;
|
||
}
|
||
|
||
if (progressCallback || !citationData.citationItems.length || openedEmpty) {
|
||
that._postMessage({ action: 'setCitation', nodeID, citation });
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Cancel changes to the citation
|
||
*/
|
||
cancel: function () {
|
||
this.citation.citationItems = [];
|
||
return this.accept();
|
||
},
|
||
|
||
/**
|
||
* Get a list of items used in the current document
|
||
* @return {Promise} A promise resolved by the items
|
||
*/
|
||
getItems: async function () {
|
||
// Zotero.debug('CI: getItems');
|
||
let note = that._item.note;
|
||
|
||
let parser = new DOMParser();
|
||
let doc = parser.parseFromString(note, 'text/html');
|
||
|
||
let metadataContainer = doc.querySelector('body > div[data-schema-version]');
|
||
if (metadataContainer) {
|
||
let citationItems = metadataContainer.getAttribute('data-citation-items');
|
||
if (citationItems) {
|
||
try {
|
||
citationItems = JSON.parse(decodeURIComponent(citationItems));
|
||
let items = [];
|
||
for (let citationItem of citationItems) {
|
||
let item = new Zotero.Item();
|
||
Zotero.Utilities.itemFromCSLJSON(item, citationItem.itemData);
|
||
// This is the only way to pass our custom id for already cited
|
||
// items, without modifying citationDialog.js too much.
|
||
// Must not contain `/`
|
||
item.cslItemID = 'cited:' + items.length;
|
||
items.push({ item, citationItem });
|
||
}
|
||
this.citedItems = items;
|
||
return items.map(x => x.item);
|
||
}
|
||
catch (e) {
|
||
Zotero.logError(e);
|
||
}
|
||
}
|
||
}
|
||
return [];
|
||
}
|
||
};
|
||
|
||
|
||
let Citation = class {
|
||
constructor(citationField, data, noteIndex) {
|
||
if (!data) {
|
||
data = { citationItems: [], properties: {} };
|
||
}
|
||
this.citationID = data.citationID;
|
||
this.citationItems = data.citationItems;
|
||
this.properties = data.properties;
|
||
this.properties.noteIndex = noteIndex;
|
||
|
||
this._field = citationField;
|
||
}
|
||
|
||
/**
|
||
* Load citation item data
|
||
* @param {Boolean} [promptToReselect=true] - will throw a MissingItemException if false
|
||
* @returns {Promise{Number}}
|
||
* - Zotero.Integration.NO_ACTION
|
||
* - Zotero.Integration.UPDATE
|
||
* - Zotero.Integration.REMOVE_CODE
|
||
* - Zotero.Integration.DELETE
|
||
*/
|
||
loadItemData() {
|
||
// Zotero.debug('Citation: loadItemData');
|
||
}
|
||
|
||
async handleMissingItem(idx) {
|
||
// Zotero.debug('Citation: handleMissingItem');
|
||
}
|
||
|
||
async prepareForEditing() {
|
||
// Zotero.debug('Citation: prepareForEditing');
|
||
}
|
||
|
||
toJSON() {
|
||
// Zotero.debug('Citation: toJSON');
|
||
}
|
||
|
||
/**
|
||
* Serializes the citation into CSL code representation
|
||
* @returns {string}
|
||
*/
|
||
serialize() {
|
||
// Zotero.debug('Citation: serialize');
|
||
}
|
||
};
|
||
|
||
if (that._citationDialogWindow) {
|
||
that._citationDialogWindow.close();
|
||
that._citationDialogWindow = null;
|
||
}
|
||
|
||
let citation = new Citation();
|
||
citation.citationItems = citationData.citationItems;
|
||
citation.properties = citationData.properties;
|
||
var io = new CI(citation);
|
||
|
||
var allOptions = 'chrome,centerscreen';
|
||
// without this, Firefox gets raised with our windows under Compiz
|
||
if (Zotero.isLinux) allOptions += ',dialog=no';
|
||
// if(options) allOptions += ','+options;
|
||
|
||
var mode = 'chrome,centerscreen,resizable=true';
|
||
if (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised')) {
|
||
mode += ",popup";
|
||
}
|
||
else {
|
||
mode += ",alwaysRaised";
|
||
}
|
||
|
||
win = that._citationDialogWindow = Components.classes['@mozilla.org/embedcomp/window-watcher;1']
|
||
.getService(Components.interfaces.nsIWindowWatcher)
|
||
.openWindow(null, 'chrome://zotero/content/integration/citationDialog.xhtml', '', mode, {
|
||
wrappedJSObject: io
|
||
});
|
||
}
|
||
|
||
// TODO: This should be moved to utilities
|
||
static async getItemFromURIs(uris) {
|
||
for (let uri of uris) {
|
||
// Try getting URI directly
|
||
try {
|
||
let item = await Zotero.URI.getURIItem(uri);
|
||
if (item) {
|
||
// Ignore items in the trash
|
||
if (!item.deleted) {
|
||
return item;
|
||
}
|
||
}
|
||
}
|
||
catch (e) {
|
||
}
|
||
|
||
// Try merged item mapping
|
||
var replacer = await Zotero.Relations.getByPredicateAndObject(
|
||
'item', Zotero.Relations.replacedItemPredicate, uri
|
||
);
|
||
if (replacer.length && !replacer[0].deleted) {
|
||
return replacer[0];
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create note from annotations
|
||
*
|
||
* @param {Zotero.Item[]} annotations
|
||
* @param {Object} options
|
||
* @param {Integer} options.parentID - Creates standalone note if not provided
|
||
* @param {Integer} options.collectionID - Only valid if parentID not provided
|
||
* @param {Boolean} options.noSave - If true, note item is not saved and ink/image annotations are
|
||
* embedded as "data:image/..." strings instead of via imageAttachmentKey.
|
||
* @param {Boolean} options.noComments - If true, annotation comments are skipped
|
||
* @param {Boolean} options.noHeader - If true, header is not added to the note
|
||
* @returns {Promise<Zotero.Item>}
|
||
*/
|
||
static async createNoteFromAnnotations(annotations, { parentID, collectionID, noSave, noComments, noHeader } = {}) {
|
||
if (!annotations.length) {
|
||
throw new Error("No annotations provided");
|
||
}
|
||
|
||
for (let annotation of annotations) {
|
||
if (annotation.annotationType === 'image'
|
||
&& !(await Zotero.Annotations.hasCacheImage(annotation))) {
|
||
try {
|
||
await Zotero.PDFWorker.renderAttachmentAnnotations(annotation.parentID);
|
||
}
|
||
catch (e) {
|
||
Zotero.debug(e);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
let note = new Zotero.Item('note');
|
||
if (!noSave) {
|
||
note.libraryID = annotations[0].libraryID;
|
||
if (parentID) {
|
||
note.parentID = parentID;
|
||
}
|
||
else if (collectionID) {
|
||
note.addToCollection(collectionID);
|
||
}
|
||
await note.saveTx();
|
||
}
|
||
let editorInstance = new EditorInstance();
|
||
editorInstance._item = note;
|
||
let jsonAnnotations = [];
|
||
for (let annotation of annotations) {
|
||
let attachmentItem = Zotero.Items.get(annotation.parentID);
|
||
let jsonAnnotation = await Zotero.Annotations.toJSON(annotation);
|
||
if (noComments) {
|
||
jsonAnnotation.comment = null;
|
||
}
|
||
jsonAnnotation.attachmentItemID = attachmentItem.id;
|
||
jsonAnnotation.id = annotation.key;
|
||
jsonAnnotations.push(jsonAnnotation);
|
||
}
|
||
|
||
let html = '';
|
||
if (!noHeader) {
|
||
let vars = {
|
||
title: Zotero.getString('reader-annotations'),
|
||
date: new Date().toLocaleString()
|
||
};
|
||
html = lazy.generateHTMLFromTemplate(Zotero.Prefs.get('annotations.noteTemplates.title'), vars);
|
||
// New line is needed for note title parser
|
||
html += '\n';
|
||
}
|
||
|
||
// If there is no real note item saved, do not import images as annotations.
|
||
// Instead, serializeAnnotations below will embed images as "data:image/..." strings
|
||
if (!noSave) {
|
||
await editorInstance.importImages(jsonAnnotations);
|
||
}
|
||
|
||
let multipleParentParent = false;
|
||
let lastParentParentID;
|
||
let lastParentID;
|
||
// Group annotations per attachment
|
||
let groups = [];
|
||
for (let i = 0; i < annotations.length; i++) {
|
||
let annotation = annotations[i];
|
||
let jsonAnnotation = jsonAnnotations[i];
|
||
let parentParentID = annotation.parentItem.parentID;
|
||
let parentID = annotation.parentID;
|
||
if (groups.length) {
|
||
if (parentParentID !== lastParentParentID) {
|
||
// Multiple top level regular items detected, allow including their titles
|
||
multipleParentParent = true;
|
||
}
|
||
}
|
||
if (!groups.length || parentID !== lastParentID) {
|
||
groups.push({
|
||
parentTitle: annotation.parentItem.getDisplayTitle(),
|
||
parentParentID,
|
||
parentParentTitle: annotation.parentItem.parentItem && annotation.parentItem.parentItem.getDisplayTitle(),
|
||
jsonAnnotations: [jsonAnnotation]
|
||
});
|
||
}
|
||
else {
|
||
let group = groups[groups.length - 1];
|
||
group.jsonAnnotations.push(jsonAnnotation);
|
||
}
|
||
lastParentParentID = parentParentID;
|
||
lastParentID = parentID;
|
||
}
|
||
let citationItems = [];
|
||
lastParentParentID = null;
|
||
for (let group of groups) {
|
||
if (multipleParentParent && group.parentParentTitle && lastParentParentID !== group.parentParentID) {
|
||
html += `<h2>${group.parentParentTitle}</h2>\n`;
|
||
}
|
||
lastParentParentID = group.parentParentID;
|
||
// If attachment doesn't have a parent or there are more attachments with the same parent, show attachment title
|
||
if (!group.parentParentID || groups.filter(x => x.parentParentID === group.parentParentID).length > 1) {
|
||
html += `<h3>${group.parentTitle}</h3>\n`;
|
||
}
|
||
let { html: _html, citationItems: _citationItems } = Zotero.EditorInstanceUtilities.serializeAnnotations(group.jsonAnnotations, true);
|
||
html += _html + '\n';
|
||
for (let _citationItem of _citationItems) {
|
||
if (!citationItems.find(item => item.uris.some(uri => _citationItem.uris.includes(uri)))) {
|
||
citationItems.push(_citationItem);
|
||
}
|
||
}
|
||
}
|
||
citationItems = encodeURIComponent(JSON.stringify(citationItems));
|
||
// Note: Update schema version only if using new features.
|
||
let schemaVersion = 9;
|
||
// If using underline annotations, increase schema version number
|
||
// TODO: Can be removed once most clients support schema version 10
|
||
if (schemaVersion === 9 && annotations.some(x => x.annotationType === 'underline')) {
|
||
schemaVersion = 10;
|
||
}
|
||
html = `<div data-citation-items="${citationItems}" data-schema-version="${schemaVersion}">${html}</div>`;
|
||
note.setNote(html);
|
||
if (!noSave) {
|
||
await note.saveTx();
|
||
}
|
||
return note;
|
||
}
|
||
}
|
||
|
||
class EditorInstanceUtilities {
|
||
/**
|
||
* Serialize annotations into HTML
|
||
*
|
||
* @param {Object[]} annotations JSON annotations
|
||
* @param {Boolean} skipEmbeddingItemData Do not add itemData to citation items
|
||
* @return {Object} Object with `html` string and `citationItems` array to embed into metadata container
|
||
*/
|
||
serializeAnnotations(annotations, skipEmbeddingItemData) {
|
||
let storedCitationItems = [];
|
||
let html = '';
|
||
for (let annotation of annotations) {
|
||
let attachmentItem = Zotero.Items.get(annotation.attachmentItemID);
|
||
if (!attachmentItem) {
|
||
continue;
|
||
}
|
||
|
||
if (!annotation.text
|
||
&& !annotation.comment
|
||
&& !annotation.image
|
||
&& annotation.type !== 'image') {
|
||
continue;
|
||
}
|
||
|
||
let citationHTML = '';
|
||
let imageHTML = '';
|
||
let highlightHTML = '';
|
||
let quotedHighlightHTML = '';
|
||
let commentHTML = '';
|
||
|
||
let storedAnnotation = {
|
||
attachmentURI: Zotero.URI.getItemURI(attachmentItem),
|
||
annotationKey: annotation.id,
|
||
color: annotation.color,
|
||
pageLabel: annotation.pageLabel,
|
||
position: annotation.position
|
||
};
|
||
|
||
// Citation
|
||
let parentItem = attachmentItem.parentID && Zotero.Items.get(attachmentItem.parentID);
|
||
if (parentItem) {
|
||
let uris = [Zotero.URI.getItemURI(parentItem)];
|
||
let citationItem = {
|
||
uris,
|
||
locator: annotation.pageLabel
|
||
};
|
||
|
||
// Note: integration.js` uses `Zotero.Cite.System.prototype.retrieveItem`,
|
||
// which produces a little bit different CSL JSON
|
||
let itemData = Zotero.Utilities.Item.itemToCSLJSON(parentItem);
|
||
if (!skipEmbeddingItemData) {
|
||
citationItem.itemData = itemData;
|
||
}
|
||
|
||
let item = storedCitationItems.find(item => item.uris.some(uri => uris.includes(uri)));
|
||
if (!item) {
|
||
storedCitationItems.push({ uris, itemData });
|
||
}
|
||
|
||
storedAnnotation.citationItem = citationItem;
|
||
let citation = {
|
||
citationItems: [citationItem],
|
||
properties: {}
|
||
};
|
||
|
||
let citationWithData = JSON.parse(JSON.stringify(citation));
|
||
citationWithData.citationItems[0].itemData = itemData;
|
||
let formatted = Zotero.EditorInstanceUtilities.formatCitation(citationWithData);
|
||
citationHTML = `<span class="citation" data-citation="${encodeURIComponent(JSON.stringify(citation))}">${formatted}</span>`;
|
||
}
|
||
|
||
// Image
|
||
if (annotation.imageAttachmentKey || annotation.image) {
|
||
// Find the bounding rectangle of the annotation
|
||
let rect;
|
||
if (annotation.type == "ink") {
|
||
let x = annotation.position.paths[0][0];
|
||
let y = annotation.position.paths[0][1];
|
||
rect = [x, y, x, y];
|
||
for (let path of annotation.position.paths) {
|
||
for (let i = 0; i < path.length - 1; i += 2) {
|
||
let x = path[i];
|
||
let y = path[i + 1];
|
||
rect[0] = Math.min(rect[0], x);
|
||
rect[1] = Math.min(rect[1], y);
|
||
rect[2] = Math.max(rect[2], x);
|
||
rect[3] = Math.max(rect[3], y);
|
||
}
|
||
}
|
||
}
|
||
else if (annotation.type == "image") {
|
||
rect = annotation.position.rects[0];
|
||
}
|
||
else {
|
||
throw new Error("Unexpected annotation type for image embedding: " + annotation.type);
|
||
}
|
||
// Normalize image dimensions to 1.25 of the print size
|
||
let rectWidth = rect[2] - rect[0];
|
||
let rectHeight = rect[3] - rect[1];
|
||
// Constants from pdf.js
|
||
const CSS_UNITS = 96.0 / 72.0;
|
||
const PDFJS_DEFAULT_SCALE = 1.25;
|
||
let width = Math.round(rectWidth * CSS_UNITS * PDFJS_DEFAULT_SCALE);
|
||
let height = Math.round(rectHeight * width / rectWidth);
|
||
// if imageAttachmentKey is provided, use it in the HTML, otherwise use data:image/... string as src
|
||
if (annotation.imageAttachmentKey) {
|
||
imageHTML = `<img data-attachment-key="${annotation.imageAttachmentKey}" width="${width}" height="${height}" data-annotation="${encodeURIComponent(JSON.stringify(storedAnnotation))}"/>`;
|
||
}
|
||
else {
|
||
imageHTML = `<img class="${annotation.type}" src="${annotation.image}" width="${width}" height="${height}" data-annotation="${encodeURIComponent(JSON.stringify(storedAnnotation))}"/>`;
|
||
}
|
||
}
|
||
else if (annotation.type === 'image') {
|
||
imageHTML = Zotero.getString('annotation-image-not-available');
|
||
}
|
||
|
||
// Text
|
||
if (annotation.text) {
|
||
let text = this._transformTextToHTML(annotation.text.trim());
|
||
highlightHTML = `<span class="${annotation.type}" data-annotation="${encodeURIComponent(JSON.stringify(storedAnnotation))}">${text}</span>`;
|
||
quotedHighlightHTML = `<span class="${annotation.type}" data-annotation="${encodeURIComponent(JSON.stringify(storedAnnotation))}">${Zotero.getString('punctuation.openingQMark')}${text}${Zotero.getString('punctuation.closingQMark')}</span>`;
|
||
}
|
||
|
||
// Note
|
||
if (annotation.comment) {
|
||
commentHTML = this._transformTextToHTML(annotation.comment.trim());
|
||
}
|
||
|
||
let template;
|
||
if (['highlight', 'underline'].includes(annotation.type)) {
|
||
template = Zotero.Prefs.get('annotations.noteTemplates.highlight');
|
||
}
|
||
else if (['note', 'text'].includes(annotation.type)) {
|
||
template = Zotero.Prefs.get('annotations.noteTemplates.note');
|
||
}
|
||
else {
|
||
template = '<p>{{image}}<br/>{{citation}} {{comment}}</p>';
|
||
}
|
||
|
||
Zotero.debug('Using note template:');
|
||
Zotero.debug(template);
|
||
|
||
template = template.replace(
|
||
/(<blockquote>[^<>]*?)({{highlight}})([\s\S]*?<\/blockquote>)/g,
|
||
(match, p1, p2, p3) => p1 + "{{highlight quotes='false'}}" + p3
|
||
);
|
||
|
||
let vars = {
|
||
color: annotation.color || '',
|
||
// Include quotation marks by default, but allow to disable with `quotes='false'`
|
||
highlight: (attrs) => attrs.quotes === 'false' ? highlightHTML : quotedHighlightHTML,
|
||
comment: commentHTML,
|
||
citation: citationHTML,
|
||
image: imageHTML,
|
||
tags: (attrs) => (annotation.tags && annotation.tags.map(tag => tag.name) || []).join(attrs.join || ' ')
|
||
};
|
||
|
||
let templateHTML = lazy.generateHTMLFromTemplate(template, vars);
|
||
// Remove some spaces at the end of paragraph
|
||
templateHTML = templateHTML.replace(/([\s]*)(<\/p)/g, '$2');
|
||
// Remove multiple spaces
|
||
templateHTML = templateHTML.replace(/\s\s+/g, ' ');
|
||
html += templateHTML;
|
||
}
|
||
return { html, citationItems: storedCitationItems };
|
||
}
|
||
|
||
/**
|
||
* Transform plain text, containing some supported HTML tags, into actual HTML.
|
||
* A similar code is also used in pdf-reader mini editor for annotation text and comments.
|
||
* It basically creates a text node and then parses and wraps specific parts
|
||
* of it into supported HTML tags
|
||
*
|
||
* @param text Plain text flavored with some HTML tags
|
||
* @returns {string} HTML
|
||
* @private
|
||
*/
|
||
_transformTextToHTML(text) {
|
||
const supportedFormats = ['i', 'b', 'sub', 'sup'];
|
||
|
||
function getFormatter(str) {
|
||
let results = supportedFormats.map(format => str.toLowerCase().indexOf('<' + format + '>'));
|
||
results = results.map((offset, idx) => [supportedFormats[idx], offset]);
|
||
results.sort((a, b) => a[1] - b[1]);
|
||
for (let result of results) {
|
||
let format = result[0];
|
||
let offset = result[1];
|
||
if (offset < 0) continue;
|
||
let lastIndex = str.toLowerCase().indexOf('</' + format + '>', offset);
|
||
if (lastIndex >= 0) {
|
||
let parts = [];
|
||
parts.push(str.slice(0, offset));
|
||
parts.push(str.slice(offset + format.length + 2, lastIndex));
|
||
parts.push(str.slice(lastIndex + format.length + 3));
|
||
return {
|
||
format,
|
||
parts
|
||
};
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function walkFormat(parent) {
|
||
let child = parent.firstChild;
|
||
while (child) {
|
||
if (child.nodeType === 3) {
|
||
let text = child.nodeValue;
|
||
let formatter = getFormatter(text);
|
||
if (formatter) {
|
||
let nodes = [];
|
||
nodes.push(doc.createTextNode(formatter.parts[0]));
|
||
let midNode = doc.createElement(formatter.format);
|
||
midNode.appendChild(doc.createTextNode(formatter.parts[1]));
|
||
nodes.push(midNode);
|
||
nodes.push(doc.createTextNode(formatter.parts[2]));
|
||
child.replaceWith(...nodes);
|
||
child = midNode;
|
||
}
|
||
}
|
||
walkFormat(child);
|
||
child = child.nextSibling;
|
||
}
|
||
}
|
||
|
||
let parser = new DOMParser();
|
||
let doc = parser.parseFromString('', 'text/html');
|
||
|
||
// innerText transforms \n into <br>
|
||
doc.body.innerText = text;
|
||
walkFormat(doc.body);
|
||
return doc.body.innerHTML;
|
||
}
|
||
|
||
/**
|
||
* Build citation item preview string (based on buildBubbleString in citationDialog/helpers.js)
|
||
* TODO: Try to avoid duplicating this code here and inside note-editor
|
||
*/
|
||
_formatCitationItemPreview(citationItem) {
|
||
const STARTSWITH_ROMANESQUE_REGEXP = /^[&a-zA-Z\u0e01-\u0e5b\u00c0-\u017f\u0370-\u03ff\u0400-\u052f\u0590-\u05d4\u05d6-\u05ff\u1f00-\u1fff\u0600-\u06ff\u200c\u200d\u200e\u0218\u0219\u021a\u021b\u202a-\u202e]/;
|
||
const ENDSWITH_ROMANESQUE_REGEXP = /[.;:&a-zA-Z\u0e01-\u0e5b\u00c0-\u017f\u0370-\u03ff\u0400-\u052f\u0590-\u05d4\u05d6-\u05ff\u1f00-\u1fff\u0600-\u06ff\u200c\u200d\u200e\u0218\u0219\u021a\u021b\u202a-\u202e]$/;
|
||
|
||
let { itemData } = citationItem;
|
||
let str = '';
|
||
|
||
// Authors
|
||
let authors = itemData.author;
|
||
if (authors) {
|
||
if (authors.length === 1) {
|
||
str = authors[0].family || authors[0].literal;
|
||
}
|
||
else if (authors.length === 2) {
|
||
let a = authors[0].family || authors[0].literal;
|
||
let b = authors[1].family || authors[1].literal;
|
||
str = Zotero.getString('general.andJoiner', [a, b]);
|
||
}
|
||
else if (authors.length >= 3) {
|
||
str = (authors[0].family || authors[0].literal) + ' ' + Zotero.getString('general.etAl');
|
||
}
|
||
}
|
||
|
||
// Title
|
||
if (!str && itemData.title) {
|
||
str = `“${itemData.title}”`;
|
||
}
|
||
|
||
// Date
|
||
if (itemData.issued
|
||
&& itemData.issued['date-parts']
|
||
&& itemData.issued['date-parts'][0]) {
|
||
let year = itemData.issued['date-parts'][0][0];
|
||
if (year && year != '0000') {
|
||
str += ', ' + year;
|
||
}
|
||
}
|
||
|
||
// Locator
|
||
if (citationItem.locator) {
|
||
if (citationItem.label) {
|
||
// TODO: Localize and use short forms
|
||
var label = citationItem.label;
|
||
}
|
||
else if (/[\-–,]/.test(citationItem.locator)) {
|
||
var label = 'pp.';
|
||
}
|
||
else {
|
||
var label = 'p.';
|
||
}
|
||
|
||
str += ', ' + label + ' ' + citationItem.locator;
|
||
}
|
||
|
||
// Prefix
|
||
if (citationItem.prefix && ENDSWITH_ROMANESQUE_REGEXP) {
|
||
str = citationItem.prefix
|
||
+ (ENDSWITH_ROMANESQUE_REGEXP.test(citationItem.prefix) ? ' ' : '')
|
||
+ str;
|
||
}
|
||
|
||
// Suffix
|
||
if (citationItem.suffix && STARTSWITH_ROMANESQUE_REGEXP) {
|
||
str += (STARTSWITH_ROMANESQUE_REGEXP.test(citationItem.suffix) ? ' ' : '')
|
||
+ citationItem.suffix;
|
||
}
|
||
|
||
return str;
|
||
}
|
||
|
||
formatCitation(citation) {
|
||
return '(' + citation.citationItems.map((x) => {
|
||
return `<span class="citation-item">${this._formatCitationItemPreview(x)}</span>`;
|
||
}).join('; ') + ')';
|
||
}
|
||
}
|
||
|
||
Zotero.EditorInstance = EditorInstance;
|
||
Zotero.EditorInstanceUtilities = new EditorInstanceUtilities();
|
||
})();
|