Reorganize and improve notes

This commit is contained in:
Martynas Bagdonas 2020-08-26 17:58:03 +03:00 committed by Dan Stillman
parent 078a18f7c0
commit af57565acf
9 changed files with 708 additions and 678 deletions

View file

@ -53,25 +53,38 @@
<constructor><![CDATA[
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._initialized = true;
});
window.fillTooltip = (tooltip) => {
let node = window.document.tooltipNode.closest('*[title]');
if (!node) {
return false;
}
this.getNoteDataSync = () => {
return this._editor.getNoteDataSync();
tooltip.setAttribute('label', node.getAttribute('title'));
return true;
}
this.saveSync = () => {
if (this._editorInstance) {
this._editorInstance.saveSync();
}
}
this.initEditor = async (state) => {
if (this._editor) {
this._editor.uninit();
if (this._editorInstance) {
this._editorInstance.uninit();
}
this._editor = new Zotero.NoteEditor();
await this._editor.init({
this._editorInstance = new Zotero.EditorInstance();
await this._editorInstance.init({
state,
item: this._item,
window: document.getAnonymousElementByAttribute(this, "anonid", "rt-view1").contentWindow,
onNavigate: this._navigateHandler,
iframeWindow: document.getAnonymousElementByAttribute(this, 'anonid', 'editor-view').contentWindow,
popup: document.getAnonymousElementByAttribute(this, 'anonid', 'editor-menu'),
onNavigate: this._navigateHandler
});
}
@ -86,8 +99,8 @@
}
}
if (this._editor) {
await this._editor.updateCitationsForURIs(uris);
if (this._editorInstance) {
await this._editorInstance.updateCitationsForURIs(uris);
}
if (!this.item) return;
@ -96,7 +109,7 @@
if (ids.includes(id)) {
let state = extraData && extraData[id] && extraData[id].state;
if (state) {
if (extraData[id].noteEditorID !== this._editor.instanceID) {
if (extraData[id].noteEditorID !== this._editorInstance.instanceID) {
this.initEditor(state);
}
}
@ -116,6 +129,7 @@
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'noteeditor');
]]></constructor>
<property name="editorInstance" onget="return this._editorInstance"/>
<!-- Modes are predefined settings groups for particular tasks -->
<field name="_mode">"view"</field>
@ -194,10 +208,11 @@
this._lastHtmlValue = val.note;
this._editor = new Zotero.NoteEditor();
this._editor.init({
this._editorInstance = new Zotero.EditorInstance();
this._editorInstance.init({
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,
onNavigate: this._navigateHandler
});
@ -233,8 +248,8 @@
<property name="navigateHandler">
<setter>
<![CDATA[
if (this._editor) {
this._editor.onNavigate = val;
if (this._editorInstance) {
this._editorInstance.onNavigate = val;
}
this._navigateHandler = val;
]]>
@ -246,6 +261,9 @@
<destructor>
<![CDATA[
Zotero.Notifier.unregisterObserver(this._notifierID);
if (this._editorInstance) {
this._editorInstance.uninit();
}
this._destroyed = true;
]]>
</destructor>
@ -258,44 +276,13 @@
]]></body>
</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">
<body>
<![CDATA[
setTimeout(() => {
if (this._iframe && this._iframe.contentWindow) {
this._iframe.focus();
this._editor.focus();
this._editorInstance.focus();
}
}, 500);
@ -315,13 +302,14 @@
<content>
<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"/>
<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>

View file

@ -26,11 +26,9 @@
<?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/tagsBox.xul"?>
<?xul-overlay href="chrome://zotero/content/containers/noteEditor.xul"?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://zotero/content/include.js"></script>
<script src="tagSelectorContainer.js"></script>
<script src="noteEditorContainer.js"></script>
</overlay>

View file

@ -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;

View file

@ -77,15 +77,7 @@ function onError() {
function onUnload() {
Zotero.Notifier.unregisterObserver(notifierUnregisterID);
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);
}
}
noteEditor.saveSync();
}
var NotifyCallback = {

View file

@ -362,7 +362,7 @@ Zotero.Attachments = new function(){
* @param {Object} [params.saveOptions] - Options to pass to Zotero.Item::save()
* @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');
var contentType = blob.type;
@ -391,11 +391,6 @@ Zotero.Attachments = new function(){
attachmentItem = new Zotero.Item('attachment');
let { libraryID: parentLibraryID } = Zotero.Items.getLibraryAndKeyFromID(parentItemID);
attachmentItem.libraryID = parentLibraryID;
if (itemKey) {
// Let it fail if the key already exists
attachmentItem.key = itemKey;
await attachmentItem.loadPrimaryData();
}
attachmentItem.parentID = parentItemID;
attachmentItem.attachmentLinkMode = this.LINK_MODE_EMBEDDED_IMAGE;
attachmentItem.attachmentPath = 'storage:' + filename;

View 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;

View file

@ -3544,24 +3544,42 @@ var ZoteroPane = new function()
};
this.onNoteWindowClosed = async function (itemID, noteData) {
var item = Zotero.Items.get(itemID);
if (noteData) {
let changed = item.setNote(noteData.html);
if (changed) {
await item.saveTx({
notifierData: {
state: noteData.state
}
});
}
this.openBackupNoteWindow = function (itemID) {
if (!this.canEdit()) {
this.displayCannotEditLibraryMessage();
return;
}
// If note is still selected, show the editor again when the note window closes
var selectedItems = this.getSelectedItems(true);
if (selectedItems.length == 1 && itemID == selectedItems[0]) {
ZoteroItemPane.onNoteSelected(item, this.collectionsView.editable);
var name = null;
if (itemID) {
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;
}
}
};

View file

@ -96,6 +96,7 @@ const xpcomFilesLocal = [
'data/tags',
'db',
'duplicates',
'editorInstance',
'feedReader',
'fulltext',
'id',

@ -1 +1 @@
Subproject commit 918d6d2787b2abfd90a8f27d96e6a9cba097433e
Subproject commit d60cd22eee9d5cca35dd5ad77d73f3d99c755c92