diff --git a/chrome/content/zotero/bindings/noteeditor.xml b/chrome/content/zotero/bindings/noteeditor.xml index 44fd54693b..07ed3e6ef5 100644 --- a/chrome/content/zotero/bindings/noteeditor.xml +++ b/chrome/content/zotero/bindings/noteeditor.xml @@ -39,7 +39,6 @@ Public properties --> false - false false false false @@ -55,15 +54,25 @@ this._noteEditorID = Zotero.Utilities.randomString(); this._iframe = document.getAnonymousElementByAttribute(this, 'anonid', 'editor-view'); this._iframe.addEventListener('DOMContentLoaded', (e) => { + // For iframes without chrome priviledges, for unknown reasons, + // dataTransfer.getData() returns empty value for `drop` event + // when dragging something from the outside of Zotero + this._iframe.contentWindow.addEventListener('drop', (event) => { + this._iframe.contentWindow.wrappedJSObject.droppedData = Components.utils.cloneInto({ + 'text/plain': event.dataTransfer.getData('text/plain'), + 'text/html': event.dataTransfer.getData('text/html'), + 'zotero/annotation': event.dataTransfer.getData('zotero/annotation'), + 'zotero/item': event.dataTransfer.getData('zotero/item') + }, this._iframe.contentWindow); + }, true); this._initialized = true; }); window.fillTooltip = (tooltip) => { let node = window.document.tooltipNode.closest('*[title]'); - if (!node) { + if (!node || !node.getAttribute('title')) { return false; } - tooltip.setAttribute('label', node.getAttribute('title')); return true; } @@ -84,7 +93,8 @@ item: this._item, iframeWindow: document.getAnonymousElementByAttribute(this, 'anonid', 'editor-view').contentWindow, popup: document.getAnonymousElementByAttribute(this, 'anonid', 'editor-menu'), - onNavigate: this._navigateHandler + onNavigate: this._navigateHandler, + readOnly: !this.editable }); } @@ -138,7 +148,6 @@ { // `item` field can be set before the constructor is called - // (which happens in the merge dialog i.e.), therefore we wait for - // the initialization + // or noteditor is attached to dom (which happens in the + // merge dialog i.e.), therefore we wait for the initialization let n = 0; while (!this._initialized && !this._destroyed) { if (n >= 1000) { @@ -197,8 +205,8 @@ n++; } - // The binding can also be immediately destrcutred - // (which also happens in the marge dialog) + // The binding can also be immediately destructed + // (which also happens in the merge dialog) if (this._destroyed) { return; } @@ -207,17 +215,10 @@ if (this._item && this._item.id === val.id) return; this._lastHtmlValue = val.note; - - this._editorInstance = new Zotero.EditorInstance(); - this._editorInstance.init({ - item: val, - iframeWindow: document.getAnonymousElementByAttribute(this, "anonid", "editor-view").contentWindow, - popup: document.getAnonymousElementByAttribute(this, "anonid", "editor-menu"), - readOnly: !this.editable, - onNavigate: this._navigateHandler - }); - + this._item = val; + + this.initEditor(); var parentKey = this._item.parentKey; if (parentKey) { diff --git a/chrome/content/zotero/standalone/standalone.js b/chrome/content/zotero/standalone/standalone.js index 8e64002015..fb9f26a881 100644 --- a/chrome/content/zotero/standalone/standalone.js +++ b/chrome/content/zotero/standalone/standalone.js @@ -422,7 +422,6 @@ const ZoteroStandalone = new function() { this.updateNoteFontSize = function (event) { var size = event.originalTarget.getAttribute('label'); Zotero.Prefs.set('note.fontSize', size); - this.promptForRestart(); }; diff --git a/chrome/content/zotero/xpcom/editorInstance.js b/chrome/content/zotero/xpcom/editorInstance.js index 2b98b36032..f2938a4994 100644 --- a/chrome/content/zotero/xpcom/editorInstance.js +++ b/chrome/content/zotero/xpcom/editorInstance.js @@ -27,7 +27,6 @@ class EditorInstance { constructor() { this.instanceID = Zotero.Utilities.randomString(); Zotero.Notes.registerEditorInstance(this); - Zotero.debug('Creating a new editor instance'); } async init(options) { @@ -37,13 +36,16 @@ class EditorInstance { this._iframeWindow = options.iframeWindow; this._popup = options.popup; this._state = options.state; - this._saveOnEdit = true; this._disableSaving = false; this._subscriptions = []; + this._deletedImages = {}; this._quickFormatWindow = null; - - await this._waitForEditor(); - + this._isAttachment = this._item.isAttachment(); + this._prefObserverIDs = [ + Zotero.Prefs.registerObserver('note.fontSize', this._handleFontChange), + Zotero.Prefs.registerObserver('note.fontFamily', this._handleFontChange) + ]; + // Run Cut/Copy/Paste with chrome privileges this._iframeWindow.wrappedJSObject.zoteroExecCommand = function (doc, command, ui, value) { // Is that safe enough? @@ -53,19 +55,40 @@ class EditorInstance { return doc.execCommand(command, ui, value); }; - this._iframeWindow.addEventListener('message', this._listener); + this._iframeWindow.addEventListener('message', this._messageHandler); + + let note = this._item.note; this._postMessage({ action: 'init', value: this._state || this._item.note, - schemaVersion: this._item.noteSchemaVersion, - readOnly: this._readOnly + readOnly: this._readOnly, + dir: Zotero.dir, + font: this._getFont(), + // TODO: We should avoid hitting `data-schema-version` in note text + hasBackup: note && note.toLowerCase().indexOf('data-schema-version') < 0 + || !!await Zotero.NoteBackups.getNote(this._item.id) }); } uninit() { - this._iframeWindow.removeEventListener('message', this._listener); + this._prefObserverIDs.forEach(id => Zotero.Prefs.unregisterObserver(id)); + + if (this._quickFormatWindow) { + this._quickFormatWindow.close(); + this._quickFormatWindow = null; + } + // TODO: Allow editor instance to finish its work before + // the uninitialization. I.e. to finish image importing + + // As long as the message listeners are attached on + // both sides, editor instance can continue its work + // in the backstage. Although the danger here is that + // multiple editor instances of the same note can start + // compeating + this._iframeWindow.removeEventListener('message', this._messageHandler); Zotero.Notes.unregisterEditorInstance(this); + this.saveSync(); } focus() { @@ -84,27 +107,32 @@ class EditorInstance { saveSync() { if (!this._readOnly && !this._disableSaving && this._iframeWindow) { let noteData = this._iframeWindow.wrappedJSObject.getDataSync(); - noteData = JSON.parse(JSON.stringify(noteData)); + if (noteData) { + noteData = JSON.parse(JSON.stringify(noteData)); + } this._save(noteData); } } - - async _waitForEditor() { - let n = 0; - while (!this._iframeWindow) { - if (n >= 1000) { - throw new Error('Waiting for editor failed'); - } - await Zotero.Promise.delay(10); - n++; - } - } - + _postMessage(message) { this._iframeWindow.postMessage({ instanceId: this.instanceID, message }, '*'); } - _listener = async (e) => { + _getFont() { + let fontSize = Zotero.Prefs.get('note.fontSize'); + // 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 }; + } + + _handleFontChange = () => { + this._postMessage({ action: 'updateFont', font: this._getFont() }); + } + + _messageHandler = async (e) => { if (e.data.instanceId !== this.instanceID) { return; } @@ -202,8 +230,7 @@ class EditorInstance { return; } case 'subscribeProvider': { - let { id, type, data } = message; - let subscription = { id, type, data }; + let { subscription } = message; this._subscriptions.push(subscription); await this._feedSubscription(subscription); return; @@ -230,32 +257,44 @@ class EditorInstance { } case 'importImages': { let { images } = message; + if (this._isAttachment) { + return; + } for (let image of images) { let { nodeId, src } = image; let attachmentKey = await this._importImage(src); + // TODO: Inform editor about the failed to import images this._postMessage({ action: 'attachImportedImage', nodeId, attachmentKey }); } return; } case 'syncAttachmentKeys': { let { attachmentKeys } = message; + if (this._isAttachment) { + return; + } + // TODO: Remove when fixed + this._item._loaded.childItems = true; let attachmentItems = this._item.getAttachments().map(id => Zotero.Items.get(id)); let abandonedItems = attachmentItems.filter(item => !attachmentKeys.includes(item.key)); for (let item of abandonedItems) { + // Store image data in case it will be necessary for undo, + // which is not ideal + this._deletedImages[item.key] = await this._getDataURL(item); await item.eraseTx(); } return; } - case 'popup': { - let { x, y, pos, items } = message; - this._openPopup(x, y, pos, items); + case 'openContextMenu': { + let { x, y, pos, itemGroups } = message; + this._openPopup(x, y, pos, itemGroups); return; } } } async _feedSubscription(subscription) { - let { id, type, data } = subscription; + let { id, type, nodeId, data } = subscription; if (type === 'citation') { let formattedCitation = await this._getFormattedCitation(data.citation); this._postMessage({ action: 'notifyProvider', id, type, data: { formattedCitation } }); @@ -263,11 +302,22 @@ class EditorInstance { else if (type === 'image') { let { attachmentKey } = data; let item = Zotero.Items.getByLibraryAndKey(this._item.libraryID, attachmentKey); - if (!item) return; - let path = await item.getFilePathAsync(); - let buf = await OS.File.read(path, {}); - buf = new Uint8Array(buf).buffer; - let src = 'data:' + item.attachmentContentType + ';base64,' + this._arrayBufferToBase64(buf); + if (!item) { + // TODO: Find a better way to undo image deletion, + // probably just keep it in a trash until the note is closed + // This recreates the attachment as a completely new item + let dataURL = this._deletedImages[attachmentKey]; + if (dataURL) { + // delete this._deletedImages[attachmentKey]; + // TODO: Fix every repeated undo-redo cycle caching a + // new image copy in memory + let newAttachmentKey = await this._importImage(dataURL); + // TODO: Inform editor about the failed to import images + this._postMessage({ action: 'attachImportedImage', nodeId, attachmentKey: newAttachmentKey }); + } + return; + } + let src = await this._getDataURL(item); this._postMessage({ action: 'notifyProvider', id, type, data: { src } }); } } @@ -303,25 +353,35 @@ class EditorInstance { return attachment.key; } - _openPopup(x, y, pos, items) { + _openPopup(x, y, pos, itemGroups) { this._popup.hidePopup(); while (this._popup.firstChild) { this._popup.removeChild(this._popup.firstChild); } - for (let item of items) { - let menuitem = this._popup.ownerDocument.createElement('menuitem'); - menuitem.setAttribute('value', item[0]); - menuitem.setAttribute('label', item[1]); - menuitem.addEventListener('command', () => { - this._postMessage({ - action: 'contextMenuAction', - ctxAction: item[0], - pos + for (let itemGroup of itemGroups) { + for (let item of itemGroup) { + let menuitem = this._popup.ownerDocument.createElement('menuitem'); + menuitem.setAttribute('value', item.name); + menuitem.setAttribute('label', item.label); + if (!item.enabled) { + menuitem.setAttribute('disabled', true); + } + menuitem.addEventListener('command', () => { + this._postMessage({ + action: 'contextMenuAction', + ctxAction: item.name, + pos + }); }); - }); - this._popup.appendChild(menuitem); + this._popup.appendChild(menuitem); + } + + if (itemGroups.indexOf(itemGroup) !== itemGroups.length - 1) { + let separator = this._popup.ownerDocument.createElement('menuseparator'); + this._popup.appendChild(separator); + } } this._popup.openPopupAtScreen(x, y, true); @@ -329,7 +389,7 @@ class EditorInstance { async _save(noteData) { if (!noteData) return; - let { schemaVersion, state, html } = noteData; + let { state, html } = noteData; if (html === undefined) return; try { if (this._disableSaving) { @@ -349,12 +409,8 @@ class EditorInstance { if (this._item) { await Zotero.NoteBackups.ensureBackup(this._item); await Zotero.DB.executeTransaction(async () => { - let changed = this._item.setNote(html, schemaVersion); - if (changed && this._saveOnEdit) { - // Make sure saving is not disabled - if (this._disableSaving) { - return; - } + let changed = this._item.setNote(html); + if (changed && !this._disableSaving) { await this._item.save({ notifierData: { noteEditorID: this.instanceID, @@ -370,11 +426,11 @@ class EditorInstance { if (this.parentItem) { item.libraryID = this.parentItem.libraryID; } - item.setNote(html, schemaVersion); + item.setNote(html); if (this.parentItem) { item.parentKey = this.parentItem.key; } - if (this._saveOnEdit) { + if (!this._disableSaving) { var id = await item.saveTx(); if (!this.parentItem && this.collection) { @@ -463,7 +519,7 @@ class EditorInstance { for (var i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } - return this._iframeWindow.btoa(binary); + return btoa(binary); } _dataURLtoBlob(dataurl) { @@ -482,7 +538,15 @@ class EditorInstance { return null; } - _openQuickFormatDialog(nodeId, citationData, filterLibraryIDs) { + async _getDataURL(item) { + let path = await item.getFilePathAsync(); + let buf = await OS.File.read(path, {}); + buf = new Uint8Array(buf).buffer; + return 'data:' + item.attachmentContentType + ';base64,' + this._arrayBufferToBase64(buf); + } + + async _openQuickFormatDialog(nodeId, citationData, filterLibraryIDs) { + await Zotero.Styles.init(); let that = this; let win; /** @@ -500,18 +564,16 @@ class EditorInstance { * Execute a callback with a preview of the given citation * @return {Promise} A promise resolved with the previewed citation string */ - preview: function () { - Zotero.debug('CI: preview'); + preview: async function () { + // Zotero.debug('CI: preview'); }, /** * Sort the citationItems within citation (depends on this.citation.properties.unsorted) * @return {Promise} A promise resolved with the previewed citation string */ - sort: function () { - Zotero.debug('CI: sort'); - return async function () { - }; + sort: async function () { + // Zotero.debug('CI: sort'); }, /** @@ -520,7 +582,7 @@ class EditorInstance { * Receives a number from 0 to 100 indicating current status. */ accept: async function (progressCallback) { - Zotero.debug('CI: accept'); + // Zotero.debug('CI: accept'); if (progressCallback) progressCallback(100); if (win) { @@ -533,13 +595,13 @@ class EditorInstance { } for (let citationItem of citation.citationItems) { - let itm = await Zotero.Items.getAsync(citationItem.id); + let itm = await Zotero.Items.getAsync(parseInt(citationItem.id)); delete citationItem.id; citationItem.uri = Zotero.URI.getItemURI(itm); citationItem.backupText = that._getBackupStr(itm); } - if (this.citation.citationItems.length) { + if (progressCallback || !citationData.citationItems.length) { that._postMessage({ action: 'setCitation', nodeId, citation }); } }, @@ -549,7 +611,7 @@ class EditorInstance { * @return {Promise} A promise resolved by the items */ getItems: async function () { - Zotero.debug('CI: getItems'); + // Zotero.debug('CI: getItems'); return []; } } @@ -578,19 +640,19 @@ class EditorInstance { * - Zotero.Integration.DELETE */ loadItemData() { - Zotero.debug('Citation: loadItemData'); + // Zotero.debug('Citation: loadItemData'); } async handleMissingItem(idx) { - Zotero.debug('Citation: handleMissingItem'); + // Zotero.debug('Citation: handleMissingItem'); } async prepareForEditing() { - Zotero.debug('Citation: prepareForEditing'); + // Zotero.debug('Citation: prepareForEditing'); } toJSON() { - Zotero.debug('Citation: toJSON'); + // Zotero.debug('Citation: toJSON'); } /** @@ -598,13 +660,13 @@ class EditorInstance { * @returns {string} */ serialize() { - Zotero.debug('Citation: serialize'); + // Zotero.debug('Citation: serialize'); } }; - if (that.quickFormatWindow) { - that.quickFormatWindow.close(); - that.quickFormatWindow = null; + if (that._quickFormatWindow) { + that._quickFormatWindow.close(); + that._quickFormatWindow = null; } let citation = new Citation(); @@ -624,7 +686,7 @@ class EditorInstance { var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised') ? 'popup' : 'alwaysRaised') + ',resizable=false,centerscreen'; - win = that.quickFormatWindow = Components.classes['@mozilla.org/embedcomp/window-watcher;1'] + win = that._quickFormatWindow = Components.classes['@mozilla.org/embedcomp/window-watcher;1'] .getService(Components.interfaces.nsIWindowWatcher) .openWindow(null, 'chrome://zotero/content/integration/quickFormat.xul', '', mode, { wrappedJSObject: io diff --git a/chrome/content/zotero/xpcom/noteBackups.js b/chrome/content/zotero/xpcom/noteBackups.js index 9804aa0926..ecad1cce51 100644 --- a/chrome/content/zotero/xpcom/noteBackups.js +++ b/chrome/content/zotero/xpcom/noteBackups.js @@ -33,7 +33,9 @@ Zotero.NoteBackups = { }, ensureBackup: async function(item) { - if (item.noteSchemaVersion === 0) { + let note = item.note; + // TODO: We should avoid hitting `data-schema-version` in note text + if (note && note.toLowerCase().indexOf('data-schema-version') < 0) { await Zotero.DB.queryAsync("INSERT OR IGNORE INTO noteBackups VALUES (?, ?)", [item.id, item.note]); } }, diff --git a/pdf-reader b/pdf-reader index cb09ac9738..7aefe43059 160000 --- a/pdf-reader +++ b/pdf-reader @@ -1 +1 @@ -Subproject commit cb09ac97385b17bad5ef3cb70daa57fb998b309f +Subproject commit 7aefe43059843f2b065f1ca9630d1bb48e08d4c3 diff --git a/pdf-worker b/pdf-worker index b855ed86d8..20b9f8f7a1 160000 --- a/pdf-worker +++ b/pdf-worker @@ -1 +1 @@ -Subproject commit b855ed86d8f50261a4b5437d5894d32fe5389a67 +Subproject commit 20b9f8f7a197b809c310db0c94bc7a5d22ed9bd0 diff --git a/zotero-note-editor b/zotero-note-editor index d60cd22eee..3e8ec22246 160000 --- a/zotero-note-editor +++ b/zotero-note-editor @@ -1 +1 @@ -Subproject commit d60cd22eee9d5cca35dd5ad77d73f3d99c755c92 +Subproject commit 3e8ec222463b2eb05c4fecd4c43b8b629311e583