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