Reorganize and improve notes
This commit is contained in:
parent
078a18f7c0
commit
af57565acf
9 changed files with 708 additions and 678 deletions
|
@ -53,25 +53,38 @@
|
||||||
|
|
||||||
<constructor><![CDATA[
|
<constructor><![CDATA[
|
||||||
this._noteEditorID = Zotero.Utilities.randomString();
|
this._noteEditorID = Zotero.Utilities.randomString();
|
||||||
this._iframe = document.getAnonymousElementByAttribute(this, "anonid", "rt-view1");
|
this._iframe = document.getAnonymousElementByAttribute(this, 'anonid', 'editor-view');
|
||||||
this._iframe.addEventListener('DOMContentLoaded', (e) => {
|
this._iframe.addEventListener('DOMContentLoaded', (e) => {
|
||||||
this._initialized = true;
|
this._initialized = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.fillTooltip = (tooltip) => {
|
||||||
|
let node = window.document.tooltipNode.closest('*[title]');
|
||||||
|
if (!node) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
this.getNoteDataSync = () => {
|
tooltip.setAttribute('label', node.getAttribute('title'));
|
||||||
return this._editor.getNoteDataSync();
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveSync = () => {
|
||||||
|
if (this._editorInstance) {
|
||||||
|
this._editorInstance.saveSync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initEditor = async (state) => {
|
this.initEditor = async (state) => {
|
||||||
if (this._editor) {
|
if (this._editorInstance) {
|
||||||
this._editor.uninit();
|
this._editorInstance.uninit();
|
||||||
}
|
}
|
||||||
this._editor = new Zotero.NoteEditor();
|
this._editorInstance = new Zotero.EditorInstance();
|
||||||
await this._editor.init({
|
await this._editorInstance.init({
|
||||||
state,
|
state,
|
||||||
item: this._item,
|
item: this._item,
|
||||||
window: document.getAnonymousElementByAttribute(this, "anonid", "rt-view1").contentWindow,
|
iframeWindow: document.getAnonymousElementByAttribute(this, 'anonid', 'editor-view').contentWindow,
|
||||||
onNavigate: this._navigateHandler,
|
popup: document.getAnonymousElementByAttribute(this, 'anonid', 'editor-menu'),
|
||||||
|
onNavigate: this._navigateHandler
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,8 +99,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._editor) {
|
if (this._editorInstance) {
|
||||||
await this._editor.updateCitationsForURIs(uris);
|
await this._editorInstance.updateCitationsForURIs(uris);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.item) return;
|
if (!this.item) return;
|
||||||
|
@ -96,7 +109,7 @@
|
||||||
if (ids.includes(id)) {
|
if (ids.includes(id)) {
|
||||||
let state = extraData && extraData[id] && extraData[id].state;
|
let state = extraData && extraData[id] && extraData[id].state;
|
||||||
if (state) {
|
if (state) {
|
||||||
if (extraData[id].noteEditorID !== this._editor.instanceID) {
|
if (extraData[id].noteEditorID !== this._editorInstance.instanceID) {
|
||||||
this.initEditor(state);
|
this.initEditor(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,6 +129,7 @@
|
||||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'noteeditor');
|
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'noteeditor');
|
||||||
]]></constructor>
|
]]></constructor>
|
||||||
|
|
||||||
|
<property name="editorInstance" onget="return this._editorInstance"/>
|
||||||
|
|
||||||
<!-- Modes are predefined settings groups for particular tasks -->
|
<!-- Modes are predefined settings groups for particular tasks -->
|
||||||
<field name="_mode">"view"</field>
|
<field name="_mode">"view"</field>
|
||||||
|
@ -194,10 +208,11 @@
|
||||||
|
|
||||||
this._lastHtmlValue = val.note;
|
this._lastHtmlValue = val.note;
|
||||||
|
|
||||||
this._editor = new Zotero.NoteEditor();
|
this._editorInstance = new Zotero.EditorInstance();
|
||||||
this._editor.init({
|
this._editorInstance.init({
|
||||||
item: val,
|
item: val,
|
||||||
window: document.getAnonymousElementByAttribute(this, "anonid", "rt-view1").contentWindow,
|
iframeWindow: document.getAnonymousElementByAttribute(this, "anonid", "editor-view").contentWindow,
|
||||||
|
popup: document.getAnonymousElementByAttribute(this, "anonid", "editor-menu"),
|
||||||
readOnly: !this.editable,
|
readOnly: !this.editable,
|
||||||
onNavigate: this._navigateHandler
|
onNavigate: this._navigateHandler
|
||||||
});
|
});
|
||||||
|
@ -233,8 +248,8 @@
|
||||||
<property name="navigateHandler">
|
<property name="navigateHandler">
|
||||||
<setter>
|
<setter>
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
if (this._editor) {
|
if (this._editorInstance) {
|
||||||
this._editor.onNavigate = val;
|
this._editorInstance.onNavigate = val;
|
||||||
}
|
}
|
||||||
this._navigateHandler = val;
|
this._navigateHandler = val;
|
||||||
]]>
|
]]>
|
||||||
|
@ -246,6 +261,9 @@
|
||||||
<destructor>
|
<destructor>
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||||
|
if (this._editorInstance) {
|
||||||
|
this._editorInstance.uninit();
|
||||||
|
}
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
]]>
|
]]>
|
||||||
</destructor>
|
</destructor>
|
||||||
|
@ -258,44 +276,13 @@
|
||||||
]]></body>
|
]]></body>
|
||||||
</method>
|
</method>
|
||||||
|
|
||||||
<!-- Used to insert a tab manually -->
|
|
||||||
<method name="handleKeyDown">
|
|
||||||
<parameter name="event"/>
|
|
||||||
<body>
|
|
||||||
<![CDATA[
|
|
||||||
// var noteField = this._id('noteField');
|
|
||||||
//
|
|
||||||
// switch (event.keyCode) {
|
|
||||||
// case 9:
|
|
||||||
// // On Shift-Tab, if focus was moved out of the note, focus the element
|
|
||||||
// // specified in the 'previousfocus' attribute. We check for focus
|
|
||||||
// // because Shift-Tab doesn't and shouldn't move focus out of the note if
|
|
||||||
// // the cursor is in a list.
|
|
||||||
// if (event.shiftKey) {
|
|
||||||
// let id = this.getAttribute('previousfocus');
|
|
||||||
// if (id) {
|
|
||||||
// setTimeout(() => {
|
|
||||||
// if (!noteField.hasFocus()) {
|
|
||||||
// document.getElementById(id).focus();
|
|
||||||
// }
|
|
||||||
// }, 0);
|
|
||||||
// }
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
]]>
|
|
||||||
</body>
|
|
||||||
</method>
|
|
||||||
|
|
||||||
<method name="focus">
|
<method name="focus">
|
||||||
<body>
|
<body>
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this._iframe && this._iframe.contentWindow) {
|
if (this._iframe && this._iframe.contentWindow) {
|
||||||
this._iframe.focus();
|
this._iframe.focus();
|
||||||
this._editor.focus();
|
this._editorInstance.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 500);
|
}, 500);
|
||||||
|
@ -315,13 +302,14 @@
|
||||||
|
|
||||||
<content>
|
<content>
|
||||||
<xul:vbox xbl:inherits="flex">
|
<xul:vbox xbl:inherits="flex">
|
||||||
<xul:iframe anonid="rt-view1" flex="1" overflow="auto" style="width: 100%;margin-right: 5px;border: 0"
|
<xul:iframe tooltip="editor-tooltip" anonid="editor-view" flex="1" overflow="auto" style="width: 100%;margin-right: 5px;border: 0"
|
||||||
frameBorder="0" src="resource://zotero/zotero-note-editor/editor.html" type="content"/>
|
frameBorder="0" src="resource://zotero/zotero-note-editor/editor.html" type="content"/>
|
||||||
<xul:hbox id="links-container" hidden="true">
|
<xul:hbox id="links-container" hidden="true">
|
||||||
<xul:linksbox id="links-box" flex="1" xbl:inherits="notitle"/>
|
<xul:linksbox id="links-box" flex="1" xbl:inherits="notitle"/>
|
||||||
</xul:hbox>
|
</xul:hbox>
|
||||||
|
|
||||||
<xul:popupset>
|
<xul:popupset>
|
||||||
|
<xul:tooltip id="editor-tooltip" onpopupshowing="return fillTooltip(this);"/>
|
||||||
<xul:menupopup anonid="editor-menu" id="editor-menu" flex="1">
|
<xul:menupopup anonid="editor-menu" id="editor-menu" flex="1">
|
||||||
</xul:menupopup>
|
</xul:menupopup>
|
||||||
</xul:popupset>
|
</xul:popupset>
|
||||||
|
|
|
@ -26,11 +26,9 @@
|
||||||
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
|
<?xml-stylesheet href="chrome://zotero-platform/content/zotero-react-client.css"?>
|
||||||
<?xul-overlay href="chrome://zotero/content/containers/tagSelector.xul"?>
|
<?xul-overlay href="chrome://zotero/content/containers/tagSelector.xul"?>
|
||||||
<?xul-overlay href="chrome://zotero/content/containers/tagsBox.xul"?>
|
<?xul-overlay href="chrome://zotero/content/containers/tagsBox.xul"?>
|
||||||
<?xul-overlay href="chrome://zotero/content/containers/noteEditor.xul"?>
|
|
||||||
|
|
||||||
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||||
<script src="chrome://zotero/content/include.js"></script>
|
<script src="chrome://zotero/content/include.js"></script>
|
||||||
|
|
||||||
<script src="tagSelectorContainer.js"></script>
|
<script src="tagSelectorContainer.js"></script>
|
||||||
<script src="noteEditorContainer.js"></script>
|
|
||||||
</overlay>
|
</overlay>
|
|
@ -1,594 +0,0 @@
|
||||||
class NoteEditor {
|
|
||||||
constructor() {
|
|
||||||
this.instanceID = Zotero.Utilities.randomString();
|
|
||||||
Zotero.Notes.editorInstances.push(this);
|
|
||||||
Zotero.debug('Creating a new editor instance');
|
|
||||||
}
|
|
||||||
|
|
||||||
async init(options) {
|
|
||||||
this.id = options.item.id;
|
|
||||||
|
|
||||||
this.item = options.item;
|
|
||||||
// this._onNavigate = options.onNavigate;
|
|
||||||
this.saveOnEdit = true;
|
|
||||||
this.state = options.state;
|
|
||||||
this.citations = [];
|
|
||||||
this.disableSaving = false;
|
|
||||||
this._readOnly = options.readOnly;
|
|
||||||
this.window = options.window;
|
|
||||||
|
|
||||||
await this.waitForEditor();
|
|
||||||
// Zotero.Notes.updateURIs(h1);
|
|
||||||
|
|
||||||
// Run Cut/Copy/Paste with chrome privileges
|
|
||||||
this.window.wrappedJSObject.zoteroExecCommand = function (doc, command, ui, value) {
|
|
||||||
// Is that safe enough?
|
|
||||||
if (!['cut', 'copy', 'paste'].includes(command)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return doc.execCommand(command, ui, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.window.addEventListener('message', this.listener);
|
|
||||||
this.quickFormatWindow = null;
|
|
||||||
let data = this.state ? { state: this.state } : { html: this.item.note };
|
|
||||||
this.postMessage({
|
|
||||||
op: 'init', ...data,
|
|
||||||
libraryId: this.item.libraryID,
|
|
||||||
key: this.item.key,
|
|
||||||
readOnly: this._readOnly
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
uninit() {
|
|
||||||
this.window.removeEventListener('message', this.listener);
|
|
||||||
let index = Zotero.Notes.editorInstances.indexOf(this);
|
|
||||||
if (index >= 0) {
|
|
||||||
Zotero.Notes.editorInstances.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForEditor() {
|
|
||||||
let n = 0;
|
|
||||||
while (!this.window) {
|
|
||||||
if (n >= 1000) {
|
|
||||||
throw new Error('Waiting for editor failed ');
|
|
||||||
}
|
|
||||||
await Zotero.Promise.delay(10);
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
postMessage(message) {
|
|
||||||
this.window.postMessage({ instanceId: this.instanceID, message }, '*');
|
|
||||||
}
|
|
||||||
|
|
||||||
listener = async (e) => {
|
|
||||||
if (e.data.instanceId !== this.instanceID) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Zotero.debug('Message received from editor ' + e.data.instanceId + ' ' + this.instanceID + ' ' + e.data.message.op);
|
|
||||||
|
|
||||||
let message = e.data.message;
|
|
||||||
|
|
||||||
if (message.op === 'getItemData') {
|
|
||||||
let parent = message.parent;
|
|
||||||
let item = await Zotero.Items.getAsync(message.itemId);
|
|
||||||
if (parent && item && item.parentID) {
|
|
||||||
item = await Zotero.Items.getAsync(item.parentID);
|
|
||||||
}
|
|
||||||
if (item) {
|
|
||||||
let data = {
|
|
||||||
uri: Zotero.URI.getItemURI(item),
|
|
||||||
backupText: this.getBackupStr(item)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (message.op === 'insertObject') {
|
|
||||||
let { type, data, pos } = message;
|
|
||||||
|
|
||||||
if (type === 'zotero/item') {
|
|
||||||
let ids = data.split(',').map(id => parseInt(id));
|
|
||||||
let citations = [];
|
|
||||||
for (let id of ids) {
|
|
||||||
let item = await Zotero.Items.getAsync(id);
|
|
||||||
if (!item) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
citations.push({
|
|
||||||
citationItems: [{
|
|
||||||
uri: Zotero.URI.getItemURI(item),
|
|
||||||
backupText: this.getBackupStr(item)
|
|
||||||
}],
|
|
||||||
properties: {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.postMessage({ op: 'insertCitations', citations, pos });
|
|
||||||
}
|
|
||||||
else if (type === 'zotero/annotation') {
|
|
||||||
let annotations = JSON.parse(data);
|
|
||||||
let list = [];
|
|
||||||
for (let annotation of annotations) {
|
|
||||||
let attachmentItem = await Zotero.Items.getAsync(annotation.itemId);
|
|
||||||
if (!attachmentItem) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let citationItem = attachmentItem.parentID && await Zotero.Items.getAsync(attachmentItem.parentID) || attachmentItem;
|
|
||||||
annotation.uri = Zotero.URI.getItemURI(attachmentItem);
|
|
||||||
let citation = {
|
|
||||||
citationItems: [{
|
|
||||||
uri: Zotero.URI.getItemURI(citationItem),
|
|
||||||
backupText: this.getBackupStr(citationItem),
|
|
||||||
locator: annotation.pageLabel
|
|
||||||
}],
|
|
||||||
properties: {}
|
|
||||||
};
|
|
||||||
list.push({ annotation, citation });
|
|
||||||
}
|
|
||||||
this.postMessage({ op: 'insertAnnotationsAndCitations', list, pos });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (message.op === 'navigate') {
|
|
||||||
if (this._onNavigate) {
|
|
||||||
this._onNavigate(message.uri, { position: message.position });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
await Zotero.Viewer.openURI(message.uri, { position: message.position });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (message.op === 'openURL') {
|
|
||||||
var zp = typeof ZoteroPane !== 'undefined' ? ZoteroPane : window.opener.ZoteroPane;
|
|
||||||
zp.loadURI(message.url);
|
|
||||||
}
|
|
||||||
else if (message.op === 'showInLibrary') {
|
|
||||||
let zp = Zotero.getActiveZoteroPane();
|
|
||||||
if (zp) {
|
|
||||||
let item = await Zotero.URI.getURIItem(message.itemURI);
|
|
||||||
if (item) {
|
|
||||||
zp.selectItems([item.id]);
|
|
||||||
let win = Zotero.getMainWindow();
|
|
||||||
if (win) {
|
|
||||||
win.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (message.op === 'update') {
|
|
||||||
this.save(message.noteData);
|
|
||||||
}
|
|
||||||
else if (message.op === 'getFormattedCitations') {
|
|
||||||
let formattedCitations = await this.getFormattedCitations(message.citations);
|
|
||||||
for (let newCitation of message.citations) {
|
|
||||||
if (!this.citations.find(citation => citation.id === newCitation.id)) {
|
|
||||||
this.citations.push(newCitation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.postMessage({
|
|
||||||
op: 'setFormattedCitations',
|
|
||||||
formattedCitations
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (message.op === 'quickFormat') {
|
|
||||||
let id = message.id;
|
|
||||||
let citation = message.citation;
|
|
||||||
citation = JSON.parse(JSON.stringify(citation));
|
|
||||||
let availableCitationItems = [];
|
|
||||||
for (let citationItem of citation.citationItems) {
|
|
||||||
let item = await Zotero.URI.getURIItem(citationItem.uri);
|
|
||||||
if (item) {
|
|
||||||
availableCitationItems.push({ ...citationItem, id: item.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
citation.citationItems = availableCitationItems;
|
|
||||||
let libraryID = this.item.libraryID;
|
|
||||||
this.quickFormatDialog(id, citation, [libraryID]);
|
|
||||||
}
|
|
||||||
else if (message.op === 'updateImages') {
|
|
||||||
for (let image of message.added) {
|
|
||||||
let blob = this.dataURLtoBlob(image.dataUrl);
|
|
||||||
let imageAttachment = await Zotero.Attachments.importEmbeddedImage({
|
|
||||||
blob,
|
|
||||||
parentItemID: this.item.id,
|
|
||||||
itemKey: image.attachmentKey,
|
|
||||||
saveOptions: {
|
|
||||||
notifierData: {
|
|
||||||
noteEditorID: this.instanceID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let attachmentItems = this.item.getAttachments().map(id => Zotero.Items.get(id));
|
|
||||||
let abandonedItems = attachmentItems.filter(item => !message.all.includes(item.key));
|
|
||||||
for (let item of abandonedItems) {
|
|
||||||
await item.eraseTx();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (message.op === 'requestImage') {
|
|
||||||
let { attachmentKey } = message;
|
|
||||||
var 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 dataURL = 'data:' + item.attachmentContentType + ';base64,' + this.arrayBufferToBase64(buf);
|
|
||||||
this.postMessage({
|
|
||||||
op: 'updateImage',
|
|
||||||
attachmentKey,
|
|
||||||
dataUrl: dataURL
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (message.op === 'popup') {
|
|
||||||
this.openPopup(message.x, message.y, message.items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openPopup(x, y, items) {
|
|
||||||
let popup = document.getElementById('editor-menu');
|
|
||||||
popup.hidePopup();
|
|
||||||
|
|
||||||
while (popup.firstChild) {
|
|
||||||
popup.removeChild(popup.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let item of items) {
|
|
||||||
let menuitem = document.createElement('menuitem');
|
|
||||||
menuitem.setAttribute('value', item[0]);
|
|
||||||
menuitem.setAttribute('label', item[1]);
|
|
||||||
menuitem.addEventListener('command', () => {
|
|
||||||
this.postMessage({
|
|
||||||
op: 'contextMenuAction',
|
|
||||||
ctxAction: item[0],
|
|
||||||
payload: item.payload
|
|
||||||
});
|
|
||||||
});
|
|
||||||
popup.appendChild(menuitem);
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.openPopupAtScreen(x, y, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(noteData) {
|
|
||||||
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) {
|
|
||||||
let changed = this.item.setNote(html);
|
|
||||||
if (changed && this.saveOnEdit) {
|
|
||||||
// this.noteField.changed = false;
|
|
||||||
await this.item.saveTx({
|
|
||||||
notifierData: {
|
|
||||||
noteEditorID: this.instanceID,
|
|
||||||
state
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Create a new note
|
|
||||||
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.saveOnEdit) {
|
|
||||||
var id = await item.saveTx();
|
|
||||||
|
|
||||||
if (!this.parentItem && this.collection) {
|
|
||||||
this.collection.addItem(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
Zotero.logError(e);
|
|
||||||
if (this.hasAttribute('onerror')) {
|
|
||||||
let fn = new Function('', this.getAttribute('onerror'));
|
|
||||||
fn.call(this)
|
|
||||||
}
|
|
||||||
if (this.onError) {
|
|
||||||
this.onError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
focus = () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getNoteDataSync = () => {
|
|
||||||
if (!this._readOnly && !this.disableSaving && this.window) {
|
|
||||||
return this.window.wrappedJSObject.getDataSync();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the string to go inside a bubble
|
|
||||||
*/
|
|
||||||
_buildBubbleString(citationItem, str) {
|
|
||||||
// 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 && Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP) {
|
|
||||||
str = citationItem.prefix
|
|
||||||
+ (Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP.test(citationItem.prefix) ? ' ' : '')
|
|
||||||
+ str;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suffix
|
|
||||||
if (citationItem.suffix && Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP) {
|
|
||||||
str += (Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP.test(citationItem.suffix) ? ' ' : '')
|
|
||||||
+ citationItem.suffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateCitationsForURIs(uris) {
|
|
||||||
let citations = this.citations
|
|
||||||
.filter(citation => citation.citationItems
|
|
||||||
.some(citationItem => uris.includes(citationItem.uri)));
|
|
||||||
|
|
||||||
if (citations.length) {
|
|
||||||
let formattedCitations = await this.getFormattedCitations(citations);
|
|
||||||
this.postMessage({
|
|
||||||
op: 'setFormattedCitations',
|
|
||||||
formattedCitations
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormattedCitations = async (citations) => {
|
|
||||||
let formattedCitations = {};
|
|
||||||
for (let citation of citations) {
|
|
||||||
formattedCitations[citation.id] = await this.getFormattedCitation(citation);
|
|
||||||
}
|
|
||||||
return formattedCitations;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormattedCitation = async (citation) => {
|
|
||||||
let formattedItems = [];
|
|
||||||
for (let citationItem of citation.citationItems) {
|
|
||||||
let item = await Zotero.URI.getURIItem(citationItem.uri);
|
|
||||||
if (item && !item.deleted) {
|
|
||||||
formattedItems.push(this._buildBubbleString(citationItem, this.getBackupStr(item)));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let formattedItem = this._buildBubbleString(citationItem, citationItem.backupText);
|
|
||||||
formattedItem = `<span style="color: red;">${formattedItem}</span>`;
|
|
||||||
formattedItems.push(formattedItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return formattedItems.join(';');
|
|
||||||
}
|
|
||||||
|
|
||||||
getBackupStr(item) {
|
|
||||||
var str = item.getField('firstCreator');
|
|
||||||
|
|
||||||
// Title, if no creator (getDisplayTitle in order to get case, e-mail, statute which don't have a title field)
|
|
||||||
if (!str) {
|
|
||||||
str = Zotero.getString('punctuation.openingQMark') + item.getDisplayTitle() + Zotero.getString('punctuation.closingQMark');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date
|
|
||||||
var date = item.getField('date', true, true);
|
|
||||||
if (date && (date = date.substr(0, 4)) !== '0000') {
|
|
||||||
str += ', ' + date;
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
arrayBufferToBase64(buffer) {
|
|
||||||
var binary = '';
|
|
||||||
var bytes = new Uint8Array(buffer);
|
|
||||||
var len = bytes.byteLength;
|
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
binary += String.fromCharCode(bytes[i]);
|
|
||||||
}
|
|
||||||
return self.btoa(binary);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 self.Blob([u8arr], { type: mime });
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
quickFormatDialog(id, citationData, filterLibraryIDs) {
|
|
||||||
let that = this;
|
|
||||||
let win;
|
|
||||||
/**
|
|
||||||
* Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js
|
|
||||||
*/
|
|
||||||
let CI = function (citation, sortable, fieldIndexPromise, citationsByItemIDPromise, previewFn) {
|
|
||||||
this.citation = citation;
|
|
||||||
this.sortable = sortable;
|
|
||||||
this.filterLibraryIDs = filterLibraryIDs;
|
|
||||||
this.disableClassicDialog = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
CI.prototype = {
|
|
||||||
/**
|
|
||||||
* 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')
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 () {
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 itm = await Zotero.Items.getAsync(citationItem.id);
|
|
||||||
delete citationItem.id;
|
|
||||||
citationItem.uri = Zotero.URI.getItemURI(itm);
|
|
||||||
citationItem.backupText = that.getBackupStr(itm);
|
|
||||||
}
|
|
||||||
|
|
||||||
let formattedCitation = await that.getFormattedCitation(citation);
|
|
||||||
|
|
||||||
if (this.citation.citationItems.length) {
|
|
||||||
that.postMessage({
|
|
||||||
op: 'setCitation',
|
|
||||||
id, citation, formattedCitation
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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')
|
|
||||||
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.quickFormatWindow) {
|
|
||||||
that.quickFormatWindow.close();
|
|
||||||
that.quickFormatWindow = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let citation = new Citation();
|
|
||||||
citation.citationItems = citationData.citationItems;
|
|
||||||
citation.properties = citationData.properties;
|
|
||||||
let styleID = Zotero.Prefs.get('export.lastStyle');
|
|
||||||
let locale = Zotero.Prefs.get('export.lastLocale');
|
|
||||||
let csl = Zotero.Styles.get(styleID).getCiteProc(locale);
|
|
||||||
var io = new CI(citation, csl.opt.sort_citations);
|
|
||||||
|
|
||||||
|
|
||||||
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 = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised')
|
|
||||||
? 'popup' : 'alwaysRaised') + ',resizable=false,centerscreen';
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Zotero.NoteEditor = NoteEditor;
|
|
|
@ -77,15 +77,7 @@ function onError() {
|
||||||
|
|
||||||
function onUnload() {
|
function onUnload() {
|
||||||
Zotero.Notifier.unregisterObserver(notifierUnregisterID);
|
Zotero.Notifier.unregisterObserver(notifierUnregisterID);
|
||||||
|
noteEditor.saveSync();
|
||||||
if (noteEditor.item) {
|
|
||||||
// noteData will be null if noteEditor current editor instance
|
|
||||||
// has disabled saving, which might happen at the time of the initial sync
|
|
||||||
let noteData = JSON.parse(JSON.stringify(noteEditor.getNoteDataSync()));
|
|
||||||
if (noteData) {
|
|
||||||
window.opener.ZoteroPane.onNoteWindowClosed(noteEditor.item.id, noteData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var NotifyCallback = {
|
var NotifyCallback = {
|
||||||
|
|
|
@ -362,7 +362,7 @@ Zotero.Attachments = new function(){
|
||||||
* @param {Object} [params.saveOptions] - Options to pass to Zotero.Item::save()
|
* @param {Object} [params.saveOptions] - Options to pass to Zotero.Item::save()
|
||||||
* @return {Promise<Zotero.Item>}
|
* @return {Promise<Zotero.Item>}
|
||||||
*/
|
*/
|
||||||
this.importEmbeddedImage = async function ({ blob, itemKey, parentItemID, saveOptions }) {
|
this.importEmbeddedImage = async function ({ blob, parentItemID, saveOptions }) {
|
||||||
Zotero.debug('Importing note or annotation image');
|
Zotero.debug('Importing note or annotation image');
|
||||||
|
|
||||||
var contentType = blob.type;
|
var contentType = blob.type;
|
||||||
|
@ -391,11 +391,6 @@ Zotero.Attachments = new function(){
|
||||||
attachmentItem = new Zotero.Item('attachment');
|
attachmentItem = new Zotero.Item('attachment');
|
||||||
let { libraryID: parentLibraryID } = Zotero.Items.getLibraryAndKeyFromID(parentItemID);
|
let { libraryID: parentLibraryID } = Zotero.Items.getLibraryAndKeyFromID(parentItemID);
|
||||||
attachmentItem.libraryID = parentLibraryID;
|
attachmentItem.libraryID = parentLibraryID;
|
||||||
if (itemKey) {
|
|
||||||
// Let it fail if the key already exists
|
|
||||||
attachmentItem.key = itemKey;
|
|
||||||
await attachmentItem.loadPrimaryData();
|
|
||||||
}
|
|
||||||
attachmentItem.parentID = parentItemID;
|
attachmentItem.parentID = parentItemID;
|
||||||
attachmentItem.attachmentLinkMode = this.LINK_MODE_EMBEDDED_IMAGE;
|
attachmentItem.attachmentLinkMode = this.LINK_MODE_EMBEDDED_IMAGE;
|
||||||
attachmentItem.attachmentPath = 'storage:' + filename;
|
attachmentItem.attachmentPath = 'storage:' + filename;
|
||||||
|
|
632
chrome/content/zotero/xpcom/editorInstance.js
Normal file
632
chrome/content/zotero/xpcom/editorInstance.js
Normal file
|
@ -0,0 +1,632 @@
|
||||||
|
/*
|
||||||
|
***** 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 *****
|
||||||
|
*/
|
||||||
|
|
||||||
|
class EditorInstance {
|
||||||
|
constructor() {
|
||||||
|
this.instanceID = Zotero.Utilities.randomString();
|
||||||
|
Zotero.Notes.registerEditorInstance(this);
|
||||||
|
Zotero.debug('Creating a new editor instance');
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(options) {
|
||||||
|
this.onNavigate = options.onNavigate;
|
||||||
|
this._item = options.item;
|
||||||
|
this._readOnly = options.readOnly;
|
||||||
|
this._iframeWindow = options.iframeWindow;
|
||||||
|
this._popup = options.popup;
|
||||||
|
this._state = options.state;
|
||||||
|
this._saveOnEdit = true;
|
||||||
|
this._disableSaving = false;
|
||||||
|
this._subscriptions = [];
|
||||||
|
this._quickFormatWindow = null;
|
||||||
|
|
||||||
|
await this._waitForEditor();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
this._iframeWindow.addEventListener('message', this._listener);
|
||||||
|
|
||||||
|
this._postMessage({
|
||||||
|
action: 'init',
|
||||||
|
value: this._state || this._item.note,
|
||||||
|
schemaVersion: this._item.noteSchemaVersion,
|
||||||
|
readOnly: this._readOnly
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
uninit() {
|
||||||
|
this._iframeWindow.removeEventListener('message', this._listener);
|
||||||
|
Zotero.Notes.unregisterEditorInstance(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this._postMessage({ action: 'focus' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCitationsForURIs(uris) {
|
||||||
|
let subscriptions = this._subscriptions
|
||||||
|
.filter(s => s.data.citation && s.data.citation.citationItems
|
||||||
|
.some(citationItem => uris.includes(citationItem.uri)));
|
||||||
|
for (let subscription of subscriptions) {
|
||||||
|
await this._feedSubscription(subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSync() {
|
||||||
|
if (!this._readOnly && !this._disableSaving && this._iframeWindow) {
|
||||||
|
let noteData = this._iframeWindow.wrappedJSObject.getDataSync();
|
||||||
|
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) => {
|
||||||
|
if (e.data.instanceId !== this.instanceID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let message = e.data.message;
|
||||||
|
switch (message.action) {
|
||||||
|
case 'insertObject': {
|
||||||
|
let { type, data, pos } = message;
|
||||||
|
let list = [];
|
||||||
|
if (type === 'zotero/item') {
|
||||||
|
let ids = data.split(',').map(id => parseInt(id));
|
||||||
|
for (let id of ids) {
|
||||||
|
let item = await Zotero.Items.getAsync(id);
|
||||||
|
if (!item) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
list.push({
|
||||||
|
citation: {
|
||||||
|
citationItems: [{
|
||||||
|
uri: Zotero.URI.getItemURI(item),
|
||||||
|
backupText: this._getBackupStr(item)
|
||||||
|
}],
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type === 'zotero/annotation') {
|
||||||
|
let annotations = JSON.parse(data);
|
||||||
|
for (let annotation of annotations) {
|
||||||
|
let attachmentItem = await Zotero.Items.getAsync(annotation.itemId);
|
||||||
|
if (!attachmentItem) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let citationItem = attachmentItem.parentID && await Zotero.Items.getAsync(attachmentItem.parentID) || attachmentItem;
|
||||||
|
annotation.uri = Zotero.URI.getItemURI(attachmentItem);
|
||||||
|
let citation = {
|
||||||
|
citationItems: [{
|
||||||
|
uri: Zotero.URI.getItemURI(citationItem),
|
||||||
|
backupText: this._getBackupStr(citationItem),
|
||||||
|
locator: annotation.pageLabel
|
||||||
|
}],
|
||||||
|
properties: {}
|
||||||
|
};
|
||||||
|
list.push({ annotation, citation });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (list.length) {
|
||||||
|
this._postMessage({ action: 'insertAnnotationsAndCitations', list, pos });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'openAnnotation': {
|
||||||
|
let { uri, position } = message;
|
||||||
|
if (this.onNavigate) {
|
||||||
|
this.onNavigate(uri, { position });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await Zotero.Viewer.openURI(uri, { position });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'openUrl': {
|
||||||
|
let { url } = message;
|
||||||
|
let zp = Zotero.getActiveZoteroPane();
|
||||||
|
if (zp) {
|
||||||
|
zp.loadURI(url);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'showInLibrary': {
|
||||||
|
let { uri } = message;
|
||||||
|
let zp = Zotero.getActiveZoteroPane();
|
||||||
|
if (zp) {
|
||||||
|
let item = await Zotero.URI.getURIItem(uri);
|
||||||
|
if (item) {
|
||||||
|
zp.selectItems([item.id]);
|
||||||
|
let win = Zotero.getMainWindow();
|
||||||
|
if (win) {
|
||||||
|
win.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'openBackup': {
|
||||||
|
let zp = Zotero.getActiveZoteroPane();
|
||||||
|
if (zp) {
|
||||||
|
zp.openBackupNoteWindow(this._item.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
let { noteData } = message;
|
||||||
|
this._save(noteData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'subscribeProvider': {
|
||||||
|
let { id, type, data } = message;
|
||||||
|
let subscription = { id, type, data };
|
||||||
|
this._subscriptions.push(subscription);
|
||||||
|
await this._feedSubscription(subscription);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'unsubscribeProvider': {
|
||||||
|
let { id } = message;
|
||||||
|
this._subscriptions.splice(this._subscriptions.findIndex(s => s.id === id), 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'openCitationPopup': {
|
||||||
|
let { nodeId, citation } = message;
|
||||||
|
citation = JSON.parse(JSON.stringify(citation));
|
||||||
|
let availableCitationItems = [];
|
||||||
|
for (let citationItem of citation.citationItems) {
|
||||||
|
let item = await Zotero.URI.getURIItem(citationItem.uri);
|
||||||
|
if (item) {
|
||||||
|
availableCitationItems.push({ ...citationItem, id: item.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
citation.citationItems = availableCitationItems;
|
||||||
|
let libraryID = this._item.libraryID;
|
||||||
|
this._openQuickFormatDialog(nodeId, citation, [libraryID]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'importImages': {
|
||||||
|
let { images } = message;
|
||||||
|
for (let image of images) {
|
||||||
|
let { nodeId, src } = image;
|
||||||
|
let attachmentKey = await this._importImage(src);
|
||||||
|
this._postMessage({ action: 'attachImportedImage', nodeId, attachmentKey });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'syncAttachmentKeys': {
|
||||||
|
let { attachmentKeys } = message;
|
||||||
|
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) {
|
||||||
|
await item.eraseTx();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'popup': {
|
||||||
|
let { x, y, pos, items } = message;
|
||||||
|
this._openPopup(x, y, pos, items);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _feedSubscription(subscription) {
|
||||||
|
let { id, type, data } = subscription;
|
||||||
|
if (type === 'citation') {
|
||||||
|
let formattedCitation = await this._getFormattedCitation(data.citation);
|
||||||
|
this._postMessage({ action: 'notifyProvider', id, type, data: { formattedCitation } });
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
this._postMessage({ action: 'notifyProvider', id, type, data: { src } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _importImage(src) {
|
||||||
|
let blob;
|
||||||
|
if (src.startsWith('data:')) {
|
||||||
|
blob = this._dataURLtoBlob(src);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let res;
|
||||||
|
|
||||||
|
try {
|
||||||
|
res = await Zotero.HTTP.request('GET', src, { responseType: 'blob' });
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
blob = res.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachment = await Zotero.Attachments.importEmbeddedImage({
|
||||||
|
blob,
|
||||||
|
parentItemID: this._item.id,
|
||||||
|
saveOptions: {
|
||||||
|
notifierData: {
|
||||||
|
noteEditorID: this.instanceID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return attachment.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openPopup(x, y, pos, items) {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._popup.appendChild(menuitem);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._popup.openPopupAtScreen(x, y, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _save(noteData) {
|
||||||
|
if (!noteData) return;
|
||||||
|
let { schemaVersion, 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) {
|
||||||
|
let changed = this._item.setNote(html, schemaVersion);
|
||||||
|
if (changed && this._saveOnEdit) {
|
||||||
|
// Make sure saving is not disabled
|
||||||
|
if (this._disableSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this._item.saveTx({
|
||||||
|
notifierData: {
|
||||||
|
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, schemaVersion);
|
||||||
|
if (this.parentItem) {
|
||||||
|
item.parentKey = this.parentItem.key;
|
||||||
|
}
|
||||||
|
if (this._saveOnEdit) {
|
||||||
|
var id = await item.saveTx();
|
||||||
|
|
||||||
|
if (!this.parentItem && this.collection) {
|
||||||
|
this.collection.addItem(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the string to go inside a bubble
|
||||||
|
*/
|
||||||
|
_buildBubbleString(citationItem, str) {
|
||||||
|
// 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 && Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP) {
|
||||||
|
str = citationItem.prefix
|
||||||
|
+ (Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP.test(citationItem.prefix) ? ' ' : '')
|
||||||
|
+ str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suffix
|
||||||
|
if (citationItem.suffix && Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP) {
|
||||||
|
str += (Zotero.CiteProc.CSL.STARTSWITH_ROMANESQUE_REGEXP.test(citationItem.suffix) ? ' ' : '')
|
||||||
|
+ citationItem.suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getFormattedCitation(citation) {
|
||||||
|
let formattedItems = [];
|
||||||
|
for (let citationItem of citation.citationItems) {
|
||||||
|
let item = await Zotero.URI.getURIItem(citationItem.uri);
|
||||||
|
if (item && !item.deleted) {
|
||||||
|
formattedItems.push(this._buildBubbleString(citationItem, this._getBackupStr(item)));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let formattedItem = this._buildBubbleString(citationItem, citationItem.backupText);
|
||||||
|
formattedItem = `<span style="color: red;">${formattedItem}</span>`;
|
||||||
|
formattedItems.push(formattedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formattedItems.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
_getBackupStr(item) {
|
||||||
|
var str = item.getField('firstCreator');
|
||||||
|
|
||||||
|
// Title, if no creator (getDisplayTitle in order to get case, e-mail, statute which don't have a title field)
|
||||||
|
if (!str) {
|
||||||
|
str = Zotero.getString('punctuation.openingQMark') + item.getDisplayTitle() + Zotero.getString('punctuation.closingQMark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date
|
||||||
|
var date = item.getField('date', true, true);
|
||||||
|
if (date && (date = date.substr(0, 4)) !== '0000') {
|
||||||
|
str += ', ' + date;
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
_arrayBufferToBase64(buffer) {
|
||||||
|
var binary = '';
|
||||||
|
var bytes = new Uint8Array(buffer);
|
||||||
|
var len = bytes.byteLength;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return this._iframeWindow.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
_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 this._iframeWindow.Blob([u8arr], { type: mime });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openQuickFormatDialog(nodeId, citationData, filterLibraryIDs) {
|
||||||
|
let that = this;
|
||||||
|
let win;
|
||||||
|
/**
|
||||||
|
* Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js
|
||||||
|
*/
|
||||||
|
let CI = function (citation, sortable, fieldIndexPromise, citationsByItemIDPromise, previewFn) {
|
||||||
|
this.citation = citation;
|
||||||
|
this.sortable = sortable;
|
||||||
|
this.filterLibraryIDs = filterLibraryIDs;
|
||||||
|
this.disableClassicDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CI.prototype = {
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 () {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 itm = await Zotero.Items.getAsync(citationItem.id);
|
||||||
|
delete citationItem.id;
|
||||||
|
citationItem.uri = Zotero.URI.getItemURI(itm);
|
||||||
|
citationItem.backupText = that._getBackupStr(itm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.citation.citationItems.length) {
|
||||||
|
that._postMessage({ action: 'setCitation', nodeId, citation });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
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.quickFormatWindow) {
|
||||||
|
that.quickFormatWindow.close();
|
||||||
|
that.quickFormatWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let citation = new Citation();
|
||||||
|
citation.citationItems = citationData.citationItems;
|
||||||
|
citation.properties = citationData.properties;
|
||||||
|
let styleID = Zotero.Prefs.get('export.lastStyle');
|
||||||
|
let locale = Zotero.Prefs.get('export.lastLocale');
|
||||||
|
let csl = Zotero.Styles.get(styleID).getCiteProc(locale);
|
||||||
|
var io = new CI(citation, csl.opt.sort_citations);
|
||||||
|
|
||||||
|
|
||||||
|
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 = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised')
|
||||||
|
? 'popup' : 'alwaysRaised') + ',resizable=false,centerscreen';
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.EditorInstance = EditorInstance;
|
|
@ -3544,24 +3544,42 @@ var ZoteroPane = new function()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
this.onNoteWindowClosed = async function (itemID, noteData) {
|
this.openBackupNoteWindow = function (itemID) {
|
||||||
var item = Zotero.Items.get(itemID);
|
if (!this.canEdit()) {
|
||||||
|
this.displayCannotEditLibraryMessage();
|
||||||
if (noteData) {
|
return;
|
||||||
let changed = item.setNote(noteData.html);
|
|
||||||
if (changed) {
|
|
||||||
await item.saveTx({
|
|
||||||
notifierData: {
|
|
||||||
state: noteData.state
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If note is still selected, show the editor again when the note window closes
|
var name = null;
|
||||||
var selectedItems = this.getSelectedItems(true);
|
|
||||||
if (selectedItems.length == 1 && itemID == selectedItems[0]) {
|
if (itemID) {
|
||||||
ZoteroItemPane.onNoteSelected(item, this.collectionsView.editable);
|
let w = this.findBackupNoteWindow(itemID);
|
||||||
|
if (w) {
|
||||||
|
w.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a name for this window so we can focus it later
|
||||||
|
//
|
||||||
|
// Collection is only used on new notes, so we don't need to
|
||||||
|
// include it in the name
|
||||||
|
name = 'zotero-backup-note-' + itemID;
|
||||||
|
}
|
||||||
|
|
||||||
|
var io = { itemID: itemID };
|
||||||
|
window.openDialog('chrome://zotero/content/noteBackup.xul', name, 'chrome,resizable,centerscreen,dialog=false', io);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.findBackupNoteWindow = function (itemID) {
|
||||||
|
var name = 'zotero-backup-note-' + itemID;
|
||||||
|
var wm = Services.wm;
|
||||||
|
var e = wm.getEnumerator('zotero:note');
|
||||||
|
while (e.hasMoreElements()) {
|
||||||
|
var w = e.getNext();
|
||||||
|
if (w.name == name) {
|
||||||
|
return w;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,7 @@ const xpcomFilesLocal = [
|
||||||
'data/tags',
|
'data/tags',
|
||||||
'db',
|
'db',
|
||||||
'duplicates',
|
'duplicates',
|
||||||
|
'editorInstance',
|
||||||
'feedReader',
|
'feedReader',
|
||||||
'fulltext',
|
'fulltext',
|
||||||
'id',
|
'id',
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 918d6d2787b2abfd90a8f27d96e6a9cba097433e
|
Subproject commit d60cd22eee9d5cca35dd5ad77d73f3d99c755c92
|
Loading…
Add table
Add a link
Reference in a new issue