zotero/chrome/content/zotero/bindings/noteeditor.xml
2022-07-01 03:28:20 -04:00

604 lines
18 KiB
XML

<?xml version="1.0"?>
<!--
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
-->
<bindings xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="note-editor">
<resources>
<stylesheet src="chrome://zotero/skin/bindings/noteeditor.css"/>
<stylesheet src="chrome://zotero-platform/content/noteeditor.css"/>
</resources>
<implementation>
<!--
Public properties
-->
<field name="editable">false</field>
<field name="displayTags">false</field>
<field name="displayRelated">false</field>
<field name="displayButton">false</field>
<field name="hideLinksContainer"/>
<field name="buttonCaption"/>
<field name="parentClickHandler"/>
<field name="keyDownHandler"/>
<field name="commandHandler"/>
<field name="clickHandler"/>
<field name="navigateHandler"/>
<field name="returnHandler"/>
<constructor><![CDATA[
this._destroyed = false;
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 || !node.getAttribute('title')) {
return false;
}
tooltip.setAttribute('label', node.getAttribute('title'));
return true;
}
this.saveSync = () => {
if (this._editorInstance) {
this._editorInstance.saveSync();
}
}
this.getCurrentInstance = () => {
return this._editorInstance;
}
this.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: document.getAnonymousElementByAttribute(this, 'anonid', 'editor-view').contentWindow,
popup: document.getAnonymousElementByAttribute(this, 'anonid', '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();
}
}
this.onInit = (callback) => {
if (this._editorInstance) {
return callback();
}
this._onInitCallback = callback;
}
this.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();
}
this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'file'], 'noteeditor');
]]></constructor>
<property name="editorInstance" onget="return this._editorInstance"/>
<!-- Modes are predefined settings groups for particular tasks -->
<field name="_mode">"view"</field>
<property name="mode" onget="return this._mode;">
<setter>
<![CDATA[
// 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.xml");
}
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();
]]>
</setter>
</property>
<field name="returnHandler"/>
<property name="returnHandler" onget="return this._returnHandler;">
<setter>
<![CDATA[
this._returnHandler = val;
]]>
</setter>
</property>
<field name="_parentItem"/>
<property name="parentItem" onget="return this._parentItem;">
<setter>
<![CDATA[
this._parentItem = this._id('links-box').parentItem = val;
]]>
</setter>
</property>
<field name="_item"/>
<property name="item" onget="return this._item;">
<setter><![CDATA[
// 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;
})();
]]></setter>
</property>
<property name="linksOnTop">
<setter>
<![CDATA[
// if (val) {
// var container = this._id('links-container');
// var parent = container.parentNode;
// var sib = container.nextSibling;
// while (parent.firstChild !== container) {
// parent.insertBefore(parent.removeChild(parent.firstChild), sib);
// }
// }
]]>
</setter>
</property>
<property name="navigateHandler">
<setter>
<![CDATA[
if (this._editorInstance) {
this._editorInstance.onNavigate = val;
}
this._navigateHandler = val;
]]>
</setter>
</property>
<property name="hideLinksContainer">
<setter>
<![CDATA[
this._hideLinksContainer = val;
this._id('links-container').hidden = val;
]]>
</setter>
</property>
<field name="collection"/>
<destructor>
<![CDATA[
Zotero.Notifier.unregisterObserver(this._notifierID);
if (this._editorInstance) {
this._editorInstance.uninit();
}
this._destroyed = true;
this._initialized = false;
this._editorInstance = null;
]]>
</destructor>
<method name="save">
<body><![CDATA[
return (async () => {
})();
]]></body>
</method>
<method name="focus">
<body>
<![CDATA[
(async () => {
let n = 0;
while (!this._editorInstance && n++ < 100) {
await Zotero.Promise.delay(10);
}
await this._editorInstance._initPromise;
this._iframe.focus();
this._editorInstance.focus();
})();
]]>
</body>
</method>
<method name="focusFirst">
<body>
<![CDATA[
(async () => {
try {
let n = 0;
while (!this._editorInstance && n++ < 100) {
await Zotero.Promise.delay(10);
}
await this._editorInstance._initPromise;
this._iframe.focus();
this._editorInstance._iframeWindow.document.querySelector('.toolbar-button-return').focus();
}
catch (e) {
}
})();
]]>
</body>
</method>
<method name="_id">
<parameter name="id"/>
<body>
<![CDATA[
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id)[0];
]]>
</body>
</method>
</implementation>
<content>
<xul:vbox xbl:inherits="flex" style="display: flex;flex-direction: column;flex-grow: 1;">
<!-- Notice: Update query selector for `iframe[anonid="editor-view"]` in contextPane.js if updating this -->
<xul:iframe tooltip="editor-tooltip" anonid="editor-view" flex="1" overflow="auto" style="border: 0;width: 100%;flex-grow: 1;"
frameBorder="0" src="resource://zotero/note-editor/editor.html" type="content"/>
<xul:hbox id="links-container" hidden="true">
<xul:linksbox id="links-box" flex="1" xbl:inherits="notitle"/>
</xul:hbox>
<xul:popupset>
<xul:tooltip id="editor-tooltip" onpopupshowing="return fillTooltip(this);"/>
<xul:menupopup anonid="editor-menu" id="editor-menu" flex="1">
</xul:menupopup>
</xul:popupset>
</xul:vbox>
</content>
</binding>
<binding id="links-box">
<implementation>
<field name="itemRef"/>
<property name="item" onget="return this.itemRef;">
<setter>
<![CDATA[
this.itemRef = val;
this.id('tags').item = this.item;
this.id('related').item = this.item;
this.refresh();
// Hide popup to prevent it being visible out of the context or
// in some cases even invisible but still blocking the next click
this.id('relatedPopup').addEventListener('click', (event) => {
let target = event.originalTarget;
if (target.classList.contains('zotero-box-label')) {
this.id('relatedPopup').hidePopup();
}
});
]]>
</setter>
</property>
<property name="mode">
<setter>
<![CDATA[
this.id('related').mode = val;
this.id('tags').mode = val;
]]>
</setter>
</property>
<field name="_parentItem"/>
<property name="parentItem" onget="return this._parentItem;">
<setter>
<![CDATA[
this._parentItem = val;
var parentText = this.id('parentText');
if (parentText.firstChild) {
parentText.removeChild(parentText.firstChild);
}
if (this._parentItem && this.getAttribute('notitle') != '1') {
this.id('parent-row').hidden = undefined;
this.id('parentLabel').value = Zotero.getString('pane.item.parentItem');
parentText.appendChild(document.createTextNode(this._parentItem.getDisplayTitle(true)));
}
]]>
</setter>
</property>
<method name="tagsClick">
<body><![CDATA[
this.id('tags').reload();
var x = this.boxObject.screenX;
var y = this.boxObject.screenY;
this.id('tagsPopup').openPopupAtScreen(x, y, false);
// If editable and no existing tags, open new empty row
var tagsBox = this.id('tags');
if (tagsBox.mode == 'edit' && tagsBox.count == 0) {
this.id('tags').newTag();
}
]]></body>
</method>
<method name="refresh">
<body><![CDATA[
this.updateTagsSummary();
this.updateRelatedSummary();
]]></body>
</method>
<method name="updateTagsSummary">
<body><![CDATA[
var v = this.id('tags').summary;
if (!v || v == "") {
v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]";
}
this.id('tagsLabel').value = Zotero.getString('itemFields.tags')
+ Zotero.getString('punctuation.colon');
this.id('tagsClick').value = v;
]]></body>
</method>
<method name="relatedClick">
<body><![CDATA[
var relatedList = this.item.relatedItems;
if (relatedList.length > 0) {
var x = this.boxObject.screenX;
var y = this.boxObject.screenY;
this.id('relatedPopup').openPopupAtScreen(x, y, false);
}
else {
this.id('related').add();
}
]]></body>
</method>
<method name="updateRelatedSummary">
<body><![CDATA[
var v = this.id('related').summary;
if (!v || v == "") {
v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]";
}
this.id('relatedLabel').value = Zotero.getString('itemFields.related')
+ Zotero.getString('punctuation.colon');
this.id('relatedClick').value = v;
]]></body>
</method>
<method name="parentClick">
<body>
<![CDATA[
if (!this.item || !this.item.id) {
return;
}
if (document.getElementById('zotero-pane')) {
var zp = ZoteroPane;
}
else {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
if (!lastWin) {
var lastWin = window.open();
}
if (lastWin.ZoteroOverlay && !lastWin.ZoteroPane.isShowing()) {
lastWin.ZoteroOverlay.toggleDisplay(true);
}
var zp = lastWin.ZoteroPane;
}
Zotero.spawn(function* () {
var parentID = this.item.parentID;
yield zp.clearQuicksearch();
zp.selectItem(parentID);
}, this);
]]>
</body>
</method>
<method name="id">
<parameter name="id"/>
<body>
<![CDATA[
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id)[0];
]]>
</body>
</method>
</implementation>
<content>
<xul:vbox xbl:inherits="flex">
<xul:grid>
<xul:columns>
<xul:column/>
<xul:column flex="1"/>
</xul:columns>
<xul:rows>
<xul:row id="parent-row" hidden="true">
<xul:label id="parentLabel"/>
<xul:label id="parentText" class="zotero-clicky" crop="end"
onclick="document.getBindingParent(this).parentClick();"/>
</xul:row>
<xul:row>
<xul:label id="relatedLabel"/>
<xul:label id="relatedClick" class="zotero-clicky" crop="end"
onclick="document.getBindingParent(this).relatedClick();"/>
</xul:row>
<xul:row>
<xul:label id="tagsLabel"/>
<xul:label id="tagsClick" class="zotero-clicky" crop="end"
onclick="document.getBindingParent(this).tagsClick();"/>
</xul:row>
</xul:rows>
</xul:grid>
<xul:popupset>
<xul:menupopup id="relatedPopup" width="300" onpopupshowing="this.firstChild.refresh();">
<xul:relatedbox id="related" flex="1"/>
</xul:menupopup>
<!-- The onpopup* stuff is an ugly hack to keep track of when the
popup is open (and not the descendent autocomplete popup, which also
seems to get triggered by these events for reasons that are less than
clear) so that we can manually refresh the popup if it's open after
autocomplete is used to prevent it from becoming unresponsive
Note: Code in tagsbox.xml is dependent on the DOM path between the
tagsbox and tagsLabel above, so be sure to update fixPopup() if it changes
-->
<xul:menupopup id="tagsPopup" ignorekeys="true"
onpopupshown="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ /* DEBUG: it would be nice to make this work -- if (this.firstChild.count==0){ this.firstChild.newTag(); } */ this.setAttribute('showing', 'true'); }"
onpopuphidden="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'false'); }">
<xul:tagsbox id="tags" flex="1" mode="edit"/>
</xul:menupopup>
</xul:popupset>
</xul:vbox>
</content>
</binding>
</bindings>