diff --git a/chrome/content/zotero/bindings/attachmentbox.xml b/chrome/content/zotero/bindings/attachmentbox.xml
index b959b6720c..5ea40c26f0 100644
--- a/chrome/content/zotero/bindings/attachmentbox.xml
+++ b/chrome/content/zotero/bindings/attachmentbox.xml
@@ -631,7 +631,7 @@
-
+
diff --git a/chrome/content/zotero/bindings/merge.xml b/chrome/content/zotero/bindings/merge.xml
index b35fd58fb3..55d312e013 100644
--- a/chrome/content/zotero/bindings/merge.xml
+++ b/chrome/content/zotero/bindings/merge.xml
@@ -340,7 +340,7 @@
case 'note':
var type = Zotero.Libraries.get(this.libraryID).libraryType;
- elementName = 'zoteronoteeditor';
+ elementName = 'note-editor';
break;
case 'annotation':
diff --git a/chrome/content/zotero/contextPane.js b/chrome/content/zotero/contextPane.js
index e71b031efa..ce4d910564 100644
--- a/chrome/content/zotero/contextPane.js
+++ b/chrome/content/zotero/contextPane.js
@@ -245,7 +245,7 @@ var ZoteroContextPane = new function () {
if (_panesDeck.selectedIndex == 1) {
var node = _notesPaneDeck.selectedPanel;
if (node.selectedIndex == 1) {
- return node.querySelector('zoteronoteeditor');
+ return node.querySelector('note-editor');
}
}
}
@@ -273,7 +273,7 @@ var ZoteroContextPane = new function () {
return true;
}
else {
- node.querySelector('zoteronoteeditor').focusFirst();
+ node.querySelector('note-editor').focusFirst();
return true;
}
}
@@ -415,7 +415,7 @@ var ZoteroContextPane = new function () {
var noteContainer = document.createXULElement('vbox');
var title = document.createXULElement('vbox');
title.className = 'zotero-context-pane-editor-parent-line';
- var editor = document.createXULElement('zoteronoteeditor');
+ var editor = new (customElements.get('note-editor'));
editor.className = 'zotero-context-pane-pinned-note';
editor.setAttribute('flex', 1);
noteContainer.append(title, editor);
diff --git a/chrome/content/zotero/elements/noteEditor.js b/chrome/content/zotero/elements/noteEditor.js
new file mode 100644
index 0000000000..03ebe25108
--- /dev/null
+++ b/chrome/content/zotero/elements/noteEditor.js
@@ -0,0 +1,373 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2020 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ https://www.zotero.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 .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+"use strict";
+
+{
+ class NoteEditor extends XULElement {
+ constructor() {
+ super();
+
+ this.editable = false;
+ this.displayTags = false;
+ this.displayRelated = false;
+ this.displayButton = false;
+ this.hideLinksContainer = false;
+
+ this.buttonCaption = null;
+ this.parentClickHandler = null;
+ this.keyDownHandler = null;
+ this.commandHandler = null;
+ this.clickHandler = null;
+ this.navigateHandler = null;
+ this.returnHandler = null;
+
+ this._mode = 'view';
+ this._destroyed = false;
+ this._noteEditorID = Zotero.Utilities.randomString();
+ this._iframe = null;
+ this._initialized = true;
+ this._editorInstance = null;
+ this._item = null;
+ this._parentItem = null;
+
+ this.clickable = false;
+ this.editable = false;
+ this.saveOnEdit = false;
+ this.showTypeMenu = false;
+ this.hideEmptyFields = false;
+ this.clickByRow = false;
+ this.clickByItem = false;
+
+ this.clickHandler = null;
+ this.blurHandler = null;
+ this.eventHandlers = [];
+
+ this._mode = 'view';
+
+ this.content = MozXULElement.parseXULToFragment(`
+
+
+
+
+
+
+
+ `, ['chrome://zotero/locale/zotero.dtd']);
+
+ this._destroyed = false;
+ this._noteEditorID = Zotero.Utilities.randomString();
+ this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'file'], 'noteeditor');
+ }
+
+ connectedCallback() {
+ this._destroyed = false;
+ window.addEventListener("unload", this.destroy);
+
+ var shadow = this.attachShadow({ mode: "open" });
+
+ // var s1 = document.createElement("link");
+ // s1.rel = "stylesheet";
+ // s1.href = "chrome://zotero-platform/content/noteEditor.css";
+ // shadow.append(s1);
+
+ let content = document.importNode(this.content, true);
+ this._iframe = content.querySelector('#editor-view');
+ this._iframe.addEventListener('DOMContentLoaded', (event) => {
+ // For iframes without chrome priviledges, for unknown reasons,
+ // dataTransfer.getData() returns empty value for `drop` event
+ // when dragging something from the outside of Zotero.
+ // Update: Since fx102 non-standard data types don't work when dragging into an§ iframe,
+ // while the original problem probably no longer exists
+ 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;
+ });
+ shadow.appendChild(content);
+
+ this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'itembox');
+ }
+
+ destroy() {
+ if (this._destroyed) {
+ return;
+ }
+ window.removeEventListener("unload", this.destroy);
+ this._destroyed = true;
+
+ Zotero.Notifier.unregisterObserver(this._notifierID);
+ }
+
+ disconnectedCallback() {
+ // Empty the DOM. We will rebuild if reconnected.
+ while (this.lastChild) {
+ this.removeChild(this.lastChild);
+ }
+ this.destroy();
+ }
+
+ async save() {
+
+ }
+
+ saveSync = () => {
+ if (this._editorInstance) {
+ this._editorInstance.saveSync();
+ }
+ }
+
+ getCurrentInstance = () => {
+ return this._editorInstance;
+ }
+
+ initEditor = async (state, reloaded) => {
+ if (this._editorInstance) {
+ this._editorInstance.uninit();
+ }
+
+ // Automatically upgrade editable v1 note before it's loaded
+ // TODO: Remove this at some point
+ if (this.editable) {
+ await Zotero.Notes.upgradeSchemaV1(this._item);
+ }
+
+ this._editorInstance = new Zotero.EditorInstance();
+ await this._editorInstance.init({
+ state,
+ item: this._item,
+ reloaded,
+ iframeWindow: this._id('editor-view').contentWindow,
+ popup: this._id('editor-menu'),
+ onNavigate: this._navigateHandler,
+ viewMode: this.viewMode,
+ readOnly: !this.editable,
+ disableUI: this._mode === 'merge',
+ onReturn: this._returnHandler,
+ placeholder: this.placeholder
+ });
+ if (this._onInitCallback) {
+ this._onInitCallback();
+ }
+ }
+
+ onInit = (callback) => {
+ if (this._editorInstance) {
+ return callback();
+ }
+ this._onInitCallback = callback;
+ }
+
+ notify = async (event, type, ids, extraData) => {
+ if (this._editorInstance) {
+ await this._editorInstance.notify(event, type, ids, extraData);
+ }
+
+ if (!this.item) {
+ return;
+ }
+ // Try to use the state from the item save event
+ let id = this.item.id;
+ if (ids.includes(id)) {
+ if (event === 'delete') {
+ if (this._returnHandler) {
+ this._returnHandler();
+ }
+ }
+ else {
+ let state = extraData && extraData[id] && extraData[id].state;
+ if (state) {
+ if (extraData[id].noteEditorID !== this._editorInstance.instanceID) {
+ this.initEditor(state, true);
+ }
+ }
+ else {
+ let curValue = this.item.note;
+ if (curValue !== this._lastHtmlValue) {
+ this.initEditor(null, true);
+ }
+ }
+ this._lastHtmlValue = this.item.note;
+ }
+ }
+
+ // this._id('links-container').hidden = !(this.displayTags && this.displayRelated) || this._hideLinksContainer;
+ // this._id('links-box').refresh();
+ }
+
+ set navigateHandler(val) {
+ if (this._editorInstance) {
+ this._editorInstance.onNavigate = val;
+ }
+ this._navigateHandler = val;
+ }
+
+ set returnHandler(val) {
+ this._returnHandler = val;
+ }
+
+ //
+ // Public properties
+ //
+
+ // Modes are predefined settings groups for particular tasks
+ get mode() {
+ return this._mode;
+ }
+
+ set mode(val) {
+ // Duplicate default property settings here
+ this.editable = false;
+ this.displayTags = false;
+ this.displayRelated = false;
+ this.displayButton = false;
+
+ switch (val) {
+ case 'view':
+ case 'merge':
+ this.editable = false;
+ break;
+
+ case 'edit':
+ this.editable = true;
+ this.parentClickHandler = this.selectParent;
+ this.keyDownHandler = this.handleKeyDown;
+ this.commandHandler = this.save;
+ this.displayTags = true;
+ this.displayRelated = true;
+ break;
+
+ default:
+ throw ("Invalid mode '" + val + "' in noteEditor.js");
+ }
+
+ this._mode = val;
+ // document.getAnonymousNodes(this)[0].setAttribute('mode', val);
+ // this._id('links-box').mode = val;
+ // this._id('links-container').hidden = !(this.displayTags && this.displayRelated) || this._hideLinksContainer;
+ // this._id('links-box').refresh();
+ }
+
+ get item() {
+ return this._item;
+ }
+
+ set item(val) {
+ // The binding can be immediately destroyed
+ // (which i.e. happens in merge dialog)
+ if (this._destroyed) {
+ return;
+ }
+
+ if (this._item && this._item.id && this._item.id === val.id) {
+ return;
+ }
+
+ if (this._editorInstance) {
+ this._editorInstance.uninit();
+ this._editorInstance = null;
+ }
+
+ this._lastHtmlValue = val.note;
+ this._item = val;
+
+ // var parentKey = this._item.parentKey;
+ // if (parentKey) {
+ // this.parentItem = Zotero.Items.getByLibraryAndKey(this._item.libraryID, parentKey);
+ // }
+
+ // this._id('links-box').item = this._item;
+
+ (async () => {
+ // `item` field can be set before the constructor is called
+ // or noteeditor 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) {
+ throw new Error('Waiting for noteeditor initialization failed');
+ }
+ await Zotero.Promise.delay(10);
+ n++;
+ }
+
+ if (this._destroyed) {
+ return;
+ }
+
+ this.initEditor();
+ // this._id('links-box').item = this._item;
+ })();
+ }
+
+ get parentItem() {
+ return this._parentItem;
+ }
+
+ set parentItem(val) {
+ this._parentItem = val;
+ // this._id('links-box').parentItem = val;
+ }
+
+ async focus() {
+ let n = 0;
+ while (!this._editorInstance && n++ < 100) {
+ await Zotero.Promise.delay(10);
+ }
+ await this._editorInstance._initPromise;
+ this._iframe.focus();
+ this._editorInstance.focus();
+ }
+
+ async focusFirst() {
+ let n = 0;
+ while (!this._editorInstance && n++ < 100) {
+ await Zotero.Promise.delay(10);
+ }
+ await this._editorInstance._initPromise;
+ this._iframe.focus();
+ try {
+ this._editorInstance._iframeWindow.document.querySelector('.toolbar-button-return').focus();
+ }
+ catch(e) {
+ }
+ }
+
+ _id(id) {
+ return this.shadowRoot.querySelector(`[id=${id}]`);
+ }
+ }
+ customElements.define("note-editor", NoteEditor);
+}
diff --git a/chrome/content/zotero/note.xul b/chrome/content/zotero/note.xhtml
similarity index 62%
rename from chrome/content/zotero/note.xul
rename to chrome/content/zotero/note.xhtml
index f1c164b317..4e27511d4f 100644
--- a/chrome/content/zotero/note.xul
+++ b/chrome/content/zotero/note.xhtml
@@ -15,13 +15,17 @@
windowtype="zotero:note"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-
-
+
-
+
diff --git a/chrome/content/zotero/xpcom/editorInstance.js b/chrome/content/zotero/xpcom/editorInstance.js
index f24430a5a1..94d8778b5c 100644
--- a/chrome/content/zotero/xpcom/editorInstance.js
+++ b/chrome/content/zotero/xpcom/editorInstance.js
@@ -679,15 +679,15 @@ class EditorInstance {
for (let itemGroup of itemGroups) {
for (let item of itemGroup) {
if (item.groups) {
- let menu = parentNode.ownerDocument.createElement('menu');
+ let menu = parentNode.ownerDocument.createXULElement('menu');
menu.setAttribute('label', item.label);
- let menupopup = parentNode.ownerDocument.createElement('menupopup');
+ let menupopup = parentNode.ownerDocument.createXULElement('menupopup');
menu.append(menupopup);
appendItems(menupopup, item.groups);
parentNode.appendChild(menu);
}
else {
- let menuitem = parentNode.ownerDocument.createElement('menuitem');
+ let menuitem = parentNode.ownerDocument.createXULElement('menuitem');
menuitem.setAttribute('value', item.name);
menuitem.setAttribute('label', item.label);
menuitem.setAttribute('disabled', !item.enabled);
@@ -707,7 +707,7 @@ class EditorInstance {
}
if (itemGroups.indexOf(itemGroup) !== itemGroups.length - 1) {
- let separator = parentNode.ownerDocument.createElement('menuseparator');
+ let separator = parentNode.ownerDocument.createXULElement('menuseparator');
parentNode.appendChild(separator);
}
}
@@ -742,10 +742,10 @@ class EditorInstance {
}
// Separator
- var separator = this._popup.ownerDocument.createElement('menuseparator');
+ var separator = this._popup.ownerDocument.createXULElement('menuseparator');
this._popup.appendChild(separator);
// Check Spelling
- var menuitem = this._popup.ownerDocument.createElement('menuitem');
+ var menuitem = this._popup.ownerDocument.createXULElement('menuitem');
menuitem.setAttribute('label', Zotero.getString('spellCheck.checkSpelling'));
menuitem.setAttribute('checked', spellChecker.enabled);
menuitem.setAttribute('type', 'checkbox');
@@ -757,11 +757,11 @@ class EditorInstance {
if (spellChecker.enabled) {
// Languages menu
- var menu = this._popup.ownerDocument.createElement('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.createElement('menupopup');
+ var menupopup = this._popup.ownerDocument.createXULElement('menupopup');
menu.append(menupopup);
spellChecker.addDictionaryListToMenu(menupopup, null);
@@ -778,10 +778,10 @@ class EditorInstance {
}
// Separator
- var separator = this._popup.ownerDocument.createElement('menuseparator');
+ var separator = this._popup.ownerDocument.createXULElement('menuseparator');
menupopup.appendChild(separator);
// Add Dictionaries
- var menuitem = this._popup.ownerDocument.createElement('menuitem');
+ 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.xul",
@@ -801,7 +801,7 @@ class EditorInstance {
let firstElementChild = this._popup.firstElementChild;
let suggestionCount = spellChecker.addSuggestionsToMenu(this._popup, firstElementChild, 5);
if (suggestionCount) {
- let separator = this._popup.ownerDocument.createElement('menuseparator');
+ let separator = this._popup.ownerDocument.createXULElement('menuseparator');
this._popup.insertBefore(separator, firstElementChild);
}
}
@@ -810,13 +810,10 @@ class EditorInstance {
}
_getSpellChecker() {
- let spellChecker = new InlineSpellChecker();
- let editingSession = this._iframeWindow
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIEditingSession);
- spellChecker.init(editingSession.getEditorForWindow(this._iframeWindow));
- return spellChecker;
+ let editingSession = this._iframeWindow.docShell.editingSession;
+ return new InlineSpellChecker(
+ editingSession.getEditorForWindow(this._iframeWindow)
+ );
}
async _ensureNoteCreated() {
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index f01b3a3726..64073ce594 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -1433,12 +1433,6 @@ var ZoteroPane = new function()
var tabs = document.getElementById('zotero-view-tabbox');
- // save note when switching from a note
- if(document.getElementById('zotero-item-pane-content').selectedIndex == 2) {
- // TODO: only try to save when selected item is different
- yield document.getElementById('zotero-note-editor').save();
- }
-
var collectionTreeRow = this.getCollectionTreeRow();
// I don't think this happens in normal usage, but it can happen during tests
if (!collectionTreeRow) {
@@ -3777,7 +3771,7 @@ var ZoteroPane = new function()
}
var io = { itemID: itemID, collectionID: col, parentItemKey: parentKey };
- window.openDialog('chrome://zotero/content/note.xul', name, 'chrome,resizable,centerscreen,dialog=false', io);
+ window.openDialog('chrome://zotero/content/note.xhtml', name, 'chrome,resizable,centerscreen,dialog=false', io);
}
diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml
index 4c55de26f3..ee7deb14d0 100644
--- a/chrome/content/zotero/zoteroPane.xhtml
+++ b/chrome/content/zotero/zoteroPane.xhtml
@@ -77,7 +77,8 @@
Services.scriptloader.loadSubScript("chrome://global/content/customElements.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/menulistItemTypes.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/elements/itemBox.js", this);
-
+ Services.scriptloader.loadSubScript("chrome://zotero/content/elements/noteEditor.js", this);
+
Services.scriptloader.loadSubScript("chrome://zotero/content/tabs.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/zoteroPane.js", this);
Services.scriptloader.loadSubScript("chrome://zotero/content/itemPane.js", this);
@@ -1146,7 +1147,7 @@
'onerror' handler crashes the app on a save error to prevent typing in notes
while they're not being saved
-->
-