Improve note-editor:

- Allow to format citations inside note-editor
- Allow quickFormat dialog to display and pick already cited items, even if an item no longer exists
- Watch and automatically update citation itemData in metadata container and re-format citations in body in opened notes
- Reorganize note metadata container handling and improve resistance to accidentally breaking it in further development
- Improve performance when typing in larger notes
- Rewrite note saving mechanism to support automatic note changes and reduce complexity for further development
- Cleanup and comment some core parts or note-editor as the preparation for further development
- Prepopulate quickFormat dialog with the currently opened PDF parent #1984 (doesn't include the currently scrolled page label yet)
This commit is contained in:
Martynas Bagdonas 2021-04-16 17:55:05 +03:00
parent 128ed5e4c2
commit 633710c90b
3 changed files with 240 additions and 109 deletions

View file

@ -482,7 +482,7 @@ var Zotero_QuickFormat = new function () {
// exactly by # of items cited from each library, but maybe it's better this way.
_updateCitationObject();
for(var citationItem of io.citation.citationItems) {
var citedItem = Zotero.Cite.getItem(citationItem.id);
var citedItem = io.customGetItem && io.customGetItem(citationItem) || Zotero.Cite.getItem(citationItem.id);
if(!citedItem.cslItemID) {
var libraryID = citedItem.libraryID;
if(libraryID in nCitedItemsFromLibrary) {
@ -720,7 +720,7 @@ var Zotero_QuickFormat = new function () {
* Builds the string to go inside a bubble
*/
function _buildBubbleString(citationItem) {
var item = Zotero.Cite.getItem(citationItem.id);
var item = io.customGetItem && io.customGetItem(citationItem) || Zotero.Cite.getItem(citationItem.id);
// create text for bubble
// Creator
@ -1098,7 +1098,7 @@ var Zotero_QuickFormat = new function () {
panelLocator.value = citationItem["locator"] ? citationItem["locator"] : "";
panelSuppressAuthor.checked = !!citationItem["suppress-author"];
var item = Zotero.Cite.getItem(citationItem.id);
var item = io.customGetItem && io.customGetItem(citationItem) || Zotero.Cite.getItem(citationItem.id);
document.getElementById("citation-properties-title").textContent = item.getDisplayTitle();
while(panelInfo.hasChildNodes()) panelInfo.removeChild(panelInfo.firstChild);
_buildItemDescription(item, panelInfo);

View file

@ -23,12 +23,18 @@
***** END LICENSE BLOCK *****
*/
// Note: TinyMCE is automatically doing some meaningless corrections to
// note-editor produced HTML. Which might result to more
// conflicts, especially in group libraries
// Note: Synchrounous save can still affect dateModified
// When changing this update in `note-editor` as well.
// This only filters images that are being imported from a URL.
// In all other cases `note-editor` should decide what
// image types can be imported, and if not then
// Zotero.Attachments.importEmbeddedImage does.
// Additionally, the allready imported images should never be
// Additionally, the already imported images should never be
// affected
const DOWNLOADED_IMAGE_TYPE = [
'image/jpeg',
@ -60,6 +66,7 @@ class EditorInstance {
this._deletedImages = {};
this._quickFormatWindow = null;
this._isAttachment = this._item.isAttachment();
this._citationItemsList = [];
this._prefObserverIDs = [
Zotero.Prefs.registerObserver('note.fontSize', this._handleFontChange),
Zotero.Prefs.registerObserver('note.fontFamily', this._handleFontChange)
@ -111,7 +118,7 @@ class EditorInstance {
// both sides, editor instance can continue its work
// in the backstage. Although the danger here is that
// multiple editor instances of the same note can start
// compeating
// competing
this._iframeWindow.removeEventListener('message', this._messageHandler);
Zotero.Notes.unregisterEditorInstance(this);
this.saveSync();
@ -121,30 +128,40 @@ class EditorInstance {
this._postMessage({ action: 'focus' });
}
async updateCitationsForURIs(uris) {
let subscriptions = this._subscriptions
.filter(s => s.data.citation && s.data.citation.citationItems
.some(citationItem => citationItem.uris && uris.some(uri => citationItem.uris.includes(uri))));
for (let subscription of subscriptions) {
await this._feedSubscription(subscription);
}
}
async notify(ids) {
let items = await Zotero.Items.getAsync(ids);
// Update itemData and formatted citation in notes
if (this._readOnly || !this._item) {
return;
}
// Update attachments
let keys = items.map(item => item.key);
this._subscriptions
.filter(s => keys.includes(s.data.attachmentKey))
.forEach(s => this._feedSubscription(s));
// Make sure only single sibling instance does automatic
// citation update, and preferably the focused one
// to prevent another instances in the background resetting
// each other and the one where typing happens
// This is a temporary solution, and in future sibling instances
// should have a better mechanism to share state, images,
// undo stack and know which one now is the "master"
let siblingInstances = Zotero.Notes._editorInstances
.filter(x => x._item.id === this._item.id);
if (siblingInstances.length) {
let masterInstance = siblingInstances
.find(x => x._iframeWindow.document.hasFocus());
if (!masterInstance) {
masterInstance = siblingInstances[0];
}
if (masterInstance !== this) {
return;
}
}
let items = await Zotero.Items.getAsync(ids);
// Update citations
let uris = items.map(x => Zotero.URI.getItemURI(x)).filter(x => x);
this._subscriptions
.filter(s => s.data.citation && s.data.citation.citationItems
.some(citationItem => citationItem.uris && uris.some(uri => citationItem.uris.includes(uri))))
.forEach(s => this._feedSubscription(s));
let citationItemsList = this._citationItemsList
.filter(ci => ci.uris && uris.some(uri => ci.uris.includes(uri)));
await this._updateCitationItems(citationItemsList);
}
saveSync() {
@ -253,7 +270,7 @@ class EditorInstance {
citationItems: [citationItem],
properties: {}
};
let formatted = (await this._getFormattedCitationParts(citation)).join(';');
let formatted = this._formatCitation(citation);
citationHTML = `<span class="citation" data-citation="${encodeURIComponent(JSON.stringify(citation))}">(${formatted})</span>`;
}
@ -310,7 +327,7 @@ class EditorInstance {
}],
properties: {}
};
let formatted = (await this._getFormattedCitationParts(citation)).join(';');
let formatted = this._formatCitation(citation);
html += `<p><span class="citation" data-citation="${encodeURIComponent(JSON.stringify(citation))}">(${formatted})</span></p>`;
}
else if (item.isNote()) {
@ -364,9 +381,12 @@ class EditorInstance {
let annotation = node.getAttribute('data-annotation');
try {
annotation = JSON.parse(decodeURIComponent(annotation));
fillWithItemData([annotation.citationItem]);
annotation = encodeURIComponent(JSON.stringify(annotation));
node.setAttribute('data-annotation', annotation);
// citationItem is allowed to not exist in annotation
if (annotation.citationItem) {
fillWithItemData([annotation.citationItem]);
annotation = encodeURIComponent(JSON.stringify(annotation));
node.setAttribute('data-annotation', annotation);
}
}
catch (e) {
Zotero.logError(e);
@ -503,21 +523,11 @@ class EditorInstance {
return;
}
case 'update': {
let { noteData } = message;
let { noteData, system } = message;
if (this._readOnly) {
return;
}
await this._save(noteData);
return;
}
case 'generateCitation': {
if (this._readOnly) {
return;
}
let { citation, pos } = message;
let formatted = (await this._getFormattedCitationParts(citation)).join(';');
let html = `<span class="citation" data-citation="${encodeURIComponent(JSON.stringify(citation))}">(${formatted})</span>`;
this._postMessage({ action: 'insertHTML', pos, html });
await this._save(noteData, system);
return;
}
case 'subscribeProvider': {
@ -531,23 +541,52 @@ class EditorInstance {
this._subscriptions.splice(this._subscriptions.findIndex(s => s.id === id), 1);
return;
}
case 'updateCitationItemsList': {
let { list } = message;
let newList = [];
for (let item of list) {
let existingItem = this._citationItemsList
.find(ci => ci.uris.some(uri => item.uris.includes(uri)));
if (!existingItem) {
newList.push(item);
}
}
await this._updateCitationItems(newList);
this._citationItemsList = list;
return;
}
case 'openCitationPopup': {
let { nodeID, citation } = message;
if (this._readOnly) {
return;
}
citation = JSON.parse(JSON.stringify(citation));
let availableCitationItems = [];
for (let citationItem of citation.citationItems) {
let item = await Zotero.EditorInstance.getItemFromURIs(citationItem.uris);
if (item) {
availableCitationItems.push({ ...citationItem, id: item.id });
citationItem.id = item.id;
}
}
let openedEmpty = !citation.citationItems.length;
if (!citation.citationItems.length) {
let win = Zotero.getMainWindow();
if (win) {
let reader = Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID);
if (reader) {
let item = Zotero.Items.get(reader.itemID);
if (item && item.parentItem) {
item = item.parentItem;
let citationItem = {};
citationItem.id = item.id;
citationItem.uris = [Zotero.URI.getItemURI(item)];
citationItem.itemData = Zotero.Cite.System.prototype.retrieveItem(item);
citation.citationItems.push(citationItem);
}
}
}
}
// Notice: Citation items that don't exist in the library aren't shown in the popup
citation.citationItems = availableCitationItems;
let libraryID = this._item.libraryID;
this._openQuickFormatDialog(nodeID, citation, [libraryID]);
this._openQuickFormatDialog(nodeID, citation, [libraryID], openedEmpty);
return;
}
case 'importImages': {
@ -605,13 +644,23 @@ class EditorInstance {
}
}
async _updateCitationItems(citationItemsList) {
let citationItems = [];
for (let { uris } of citationItemsList) {
let item = await Zotero.EditorInstance.getItemFromURIs(uris);
if (item) {
let itemData = Zotero.Cite.System.prototype.retrieveItem(item);
citationItems.push({ uris, itemData });
}
}
if (citationItems.length) {
this._postMessage({ action: 'updateCitationItems', citationItems });
}
}
async _feedSubscription(subscription) {
let { id, type, nodeID, data } = subscription;
if (type === 'citation') {
let parts = await this._getFormattedCitationParts(data.citation);
this._postMessage({ action: 'notifyProvider', id, type, data: { formattedCitation: parts.join(';') } });
}
else if (type === 'image') {
if (type === 'image') {
let { attachmentKey } = data;
let item = Zotero.Items.getByLibraryAndKey(this._item.libraryID, attachmentKey);
if (!item) {
@ -719,7 +768,7 @@ class EditorInstance {
}
}
async _save(noteData) {
async _save(noteData, skipDateModifiedUpdate) {
if (!noteData) return;
let { state, html } = noteData;
if (html === undefined) return;
@ -744,6 +793,7 @@ class EditorInstance {
let changed = this._item.setNote(html);
if (changed && !this._disableSaving) {
await this._item.save({
skipDateModifiedUpdate,
notifierData: {
noteEditorID: this.instanceID,
state
@ -779,13 +829,50 @@ class EditorInstance {
}
/**
* Builds the string to go inside a bubble
* Build citation item preview string (based on _buildBubbleString in quickFormat.js)
*/
_buildBubbleString(citationItem, str) {
_formatCitationItemPreview(citationItem) {
const STARTSWITH_ROMANESQUE_REGEXP = /^[&a-zA-Z\u0e01-\u0e5b\u00c0-\u017f\u0370-\u03ff\u0400-\u052f\u0590-\u05d4\u05d6-\u05ff\u1f00-\u1fff\u0600-\u06ff\u200c\u200d\u200e\u0218\u0219\u021a\u021b\u202a-\u202e]/;
const ENDSWITH_ROMANESQUE_REGEXP = /[.;:&a-zA-Z\u0e01-\u0e5b\u00c0-\u017f\u0370-\u03ff\u0400-\u052f\u0590-\u05d4\u05d6-\u05ff\u1f00-\u1fff\u0600-\u06ff\u200c\u200d\u200e\u0218\u0219\u021a\u021b\u202a-\u202e]$/;
let { itemData } = citationItem;
let str = '';
// Authors
let authors = itemData.author;
if (authors) {
if (authors.length === 1) {
str = authors[0].family || authors[0].literal;
}
else if (authors.length === 2) {
let a = authors[0].family || authors[0].literal;
let b = authors[1].family || authors[1].literal;
str = a + ' and ' + b;
}
else if (authors.length >= 3) {
str = (authors[0].family || authors[0].literal) + ' et al.';
}
}
// Title
if (!str && itemData.title) {
str = `${itemData.title}`;
}
// Date
if (itemData.issued
&& itemData.issued['date-parts']
&& itemData.issued['date-parts'][0]) {
let year = itemData.issued['date-parts'][0][0];
if (year && year != '0000') {
str += ', ' + year;
}
}
// Locator
if (citationItem.locator) {
if (citationItem.label) {
// TODO localize and use short forms
// TODO: Localize and use short forms
var label = citationItem.label;
}
else if (/[\-,]/.test(citationItem.locator)) {
@ -799,58 +886,23 @@ class EditorInstance {
}
// Prefix
if (citationItem.prefix && Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP) {
if (citationItem.prefix && ENDSWITH_ROMANESQUE_REGEXP) {
str = citationItem.prefix
+ (Zotero.CiteProc.CSL.ENDSWITH_ROMANESQUE_REGEXP.test(citationItem.prefix) ? ' ' : '')
+ (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) ? ' ' : '')
if (citationItem.suffix && STARTSWITH_ROMANESQUE_REGEXP) {
str += (STARTSWITH_ROMANESQUE_REGEXP.test(citationItem.suffix) ? ' ' : '')
+ citationItem.suffix;
}
return str;
}
async _getFormattedCitationParts(citation) {
let formattedItems = [];
for (let citationItem of citation.citationItems) {
if (!Array.isArray(citationItem.uris)) {
continue;
}
let item = await Zotero.EditorInstance.getItemFromURIs(citationItem.uris);
if (!item && citationItem.itemData) {
item = new Zotero.Item();
Zotero.Utilities.itemFromCSLJSON(item, citationItem.itemData);
}
if (item) {
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;
}
_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;
_formatCitation(citation) {
return citation.citationItems.map(x => this._formatCitationItemPreview(x)).join(';');
}
_arrayBufferToBase64(buffer) {
@ -886,21 +938,54 @@ class EditorInstance {
return 'data:' + item.attachmentContentType + ';base64,' + this._arrayBufferToBase64(buf);
}
async _openQuickFormatDialog(nodeID, citationData, filterLibraryIDs) {
// TODO: Allow only one quickFormat dialog
async _openQuickFormatDialog(nodeID, citationData, filterLibraryIDs, openedEmpty) {
await Zotero.Styles.init();
let that = this;
let win;
/**
* Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js
* Citation editing functions and properties accessible 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;
}
// Cited items updated in `getItems`
this.citedItems = [];
};
CI.prototype = {
/**
* 1) Provide `quickFormat` dialog with items created from
* `itemData`, without dealing with `Zotero.Integration.sessions`
*
* 2) Allow to pick already cited item from `quickFormat` dropdown
*
* @param citationItem
* @returns {Zotero.Item|undefined}
*/
customGetItem(citationItem) {
// Using `id` as cited item index from `getItems` below
let citedItem = typeof citationItem.id === 'string'
&& this.citedItems[parseInt(citationItem.id.split('cited:')[1])];
// Return cited item picked in `quickFormat` dropdown
if (citedItem) {
return citedItem.item;
}
// Provide an item created from `itemData`
else if (!citationItem.id && citationItem.itemData) {
let item = new Zotero.Item();
Zotero.Utilities.itemFromCSLJSON(item, citationItem.itemData);
return item;
}
// Otherwise returns `undefined` which makes this function to be
},
/**
* Execute a callback with a preview of the given citation
* @return {Promise} A promise resolved with the previewed citation string
@ -915,6 +1000,10 @@ class EditorInstance {
*/
sort: async function () {
// Zotero.debug('CI: sort');
// Normally `this.citation.citationItems` should be sorted by
// citation preview, but in our editor it doesn't make sense
// to do so, because we don't have a real style here and
// it's not the final document
},
/**
@ -933,19 +1022,31 @@ class EditorInstance {
let citation = {
citationItems: this.citation.citationItems,
properties: this.citation.properties
}
};
for (let citationItem of citation.citationItems) {
let item = await Zotero.Items.getAsync(parseInt(citationItem.id));
let citedItem = typeof citationItem.id === 'string'
&& this.citedItems[parseInt(citationItem.id.split('cited:')[1])];
// Cited item
if (citedItem) {
let ci = citedItem.citationItem;
citationItem.uris = ci.uris;
citationItem.itemData = ci.itemData;
}
// New item
else if (citationItem.id) {
let item = await Zotero.Items.getAsync(parseInt(citationItem.id));
citationItem.uris = [Zotero.URI.getItemURI(item)];
citationItem.itemData = Zotero.Cite.System.prototype.retrieveItem(item);
}
// Otherwise it's existing item, so just passing untouched citationItem
delete citationItem.id;
citationItem.uris = [Zotero.URI.getItemURI(item)];
citationItem.itemData = Zotero.Cite.System.prototype.retrieveItem(item);
}
let formattedCitation = (await that._getFormattedCitationParts(citation)).join(';');
if (progressCallback || !citationData.citationItems.length) {
that._postMessage({ action: 'setCitation', nodeID, citation, formattedCitation });
if (progressCallback || !citationData.citationItems.length || openedEmpty) {
that._postMessage({ action: 'setCitation', nodeID, citation });
}
},
@ -955,9 +1056,39 @@ class EditorInstance {
*/
getItems: async function () {
// Zotero.debug('CI: getItems');
let note = that._item.note;
let parser = Components.classes['@mozilla.org/xmlextras/domparser;1']
.createInstance(Components.interfaces.nsIDOMParser);
let doc = parser.parseFromString(note, 'text/html');
let metadataContainer = doc.querySelector('body > div[data-schema-version]');
if (metadataContainer) {
let citationItems = metadataContainer.getAttribute('data-citation-items');
if (citationItems) {
try {
citationItems = JSON.parse(decodeURIComponent(citationItems));
let items = [];
for (let citationItem of citationItems) {
let item = new Zotero.Item();
Zotero.Utilities.itemFromCSLJSON(item, citationItem.itemData);
// This is the only way to pass our custom id for already cited
// items, without modifying `quickFormat` dialog too much.
// Must not contain `/`
item.cslItemID = 'cited:' + items.length;
items.push({ item, citationItem });
}
this.citedItems = items;
return items.map(x => x.item);
}
catch (e) {
Zotero.logError(e);
}
}
}
return [];
}
}
};
let Citation = class {

@ -1 +1 @@
Subproject commit 53d2342f8d7c2dc1cd8b002ef8c76a8360328965
Subproject commit 743160b8c75a78aac1b4a5bb394840085cd1c50a