Add Zotero.Integration.Citation

- Moves a bunch of citation related processing from Integration.Session
- Replaces missing item handling with a function instead of exception
- Solves some really confusing flow issues in _processFields
This commit is contained in:
Adomas Venčkauskas 2017-05-25 10:48:43 +03:00
parent a1acbd4038
commit f44d563a15
3 changed files with 329 additions and 422 deletions

View file

@ -526,7 +526,7 @@ Zotero.Cite.System.prototype = {
}
if(!zoteroItem) {
throw "Zotero.Cite.System.retrieveItem called on non-item "+item;
throw new Error("Zotero.Cite.System.retrieveItem called on non-item "+item);
}
var cslItem = Zotero.Utilities.itemToCSLJSON(zoteroItem);

View file

@ -305,8 +305,8 @@ Zotero.Integration = new function() {
* @param {String} [io] Data to pass to the window
* @return {Promise} Promise resolved when the window is closed
*/
this.displayDialog = function displayDialog(doc, url, options, io) {
doc.cleanup();
this.displayDialog = function displayDialog(url, options, io) {
Zotero.Integration.currentDoc.cleanup();
var allOptions = 'chrome,centerscreen';
// without this, Firefox gets raised with our windows under Compiz
@ -442,66 +442,13 @@ Zotero.Integration = new function() {
/**
* An exception thrown when a document contains an item that no longer exists in the current document.
*
* @param reselectKeys {Array} Keys representing the missing item
* @param reselectKeyType {Integer} The type of the keys (see RESELECT_KEY_* constants)
* @param citationIndex {Integer} The index of the missing item within the citation cluster
* @param citationLength {Integer} The number of items cited in this citation cluster
*/
Zotero.Integration.MissingItemException = function(reselectKeys, reselectKeyType, citationIndex, citationLength) {
this.reselectKeys = reselectKeys;
this.reselectKeyType = reselectKeyType;
this.citationIndex = citationIndex;
this.citationLength = citationLength;
}
Zotero.Integration.MissingItemException = function() {};
Zotero.Integration.MissingItemException.prototype = {
"name":"MissingItemException",
"message":"An item in this document is missing from your Zotero library.",
"toString":function() { return this.message },
"setContext":function(fieldGetter, fieldIndex) {
this.fieldGetter = fieldGetter;
this.fieldIndex = fieldIndex;
},
"attemptToResolve":function() {
Zotero.logError(this);
if(!this.fieldGetter) {
throw new Error("Could not resolve "+this.name+": setContext not called");
}
// Ask user what to do with this item
if(this.citationLength == 1) {
var msg = Zotero.getString("integration.missingItem.single");
} else {
var msg = Zotero.getString("integration.missingItem.multiple", (this.citationIndex+1).toString());
}
msg += '\n\n'+Zotero.getString('integration.missingItem.description');
this.fieldGetter._fields[this.fieldIndex].select();
this.fieldGetter._doc.activate();
var result = this.fieldGetter._doc.displayAlert(msg, 1, 3);
if(result == 0) { // Cancel
return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("document update"));
} else if(result == 1) { // No
for (let reselectKey of this.reselectKeys) {
this.fieldGetter._removeCodeKeys[reselectKey] = true;
}
this.fieldGetter._removeCodeFields[this.fieldIndex] = true;
return this.fieldGetter._processFields(this.fieldIndex+1);
} else { // Yes
// Display reselect item dialog
var fieldGetter = this.fieldGetter,
fieldIndex = this.fieldIndex,
oldCurrentWindow = Zotero.Integration.currentWindow;
return fieldGetter._session.reselectItem(fieldGetter._doc, this)
.then(function() {
// Now try again
Zotero.Integration.currentWindow = oldCurrentWindow;
fieldGetter._doc.activate();
return fieldGetter._processFields(fieldIndex);
});
}
}
}
"toString":function() { return this.message }
};
Zotero.Integration.CorruptFieldException = function(code, cause) {
this.code = code;
@ -697,26 +644,23 @@ Zotero.Integration.Interface.prototype.addEditCitation = function() {
* Adds a bibliography to the current document.
* @return {Promise}
*/
Zotero.Integration.Interface.prototype.addBibliography = function() {
Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutine(function* () {
var me = this;
return this._prepareData(true, false).then(function() {
// Make sure we can have a bibliography
if(!me._session.data.style.hasBibliography) {
throw new Zotero.Exception.Alert("integration.error.noBibliography", [],
"integration.error.title");
}
var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError);
return fieldGetter.addField().then(function(field) {
field.clearCode();
field.type = INTEGRATION_TYPE_BIBLIOGRAPHY;
field.writeToDoc();
return fieldGetter.updateSession().then(function() {
return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false);
});
});
});
}
yield this._prepareData(true, false);
// Make sure we can have a bibliography
if(!me._session.data.style.hasBibliography) {
throw new Zotero.Exception.Alert("integration.error.noBibliography", [],
"integration.error.title");
}
var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError);
let field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField());
field.clearCode();
field.type = INTEGRATION_TYPE_BIBLIOGRAPHY;
field.writeToDoc();
yield fieldGetter.updateSession();
yield fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false);
})
/**
* Edits bibliography metadata.
@ -774,11 +718,10 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro
if (haveBibliography) {
yield fieldGetter.updateSession();
yield this._session.editBibliography(this._doc);
yield this._session.editBibliography();
} else {
var field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField());
field.clearCode();
field.type = INTEGRATION_TYPE_BIBLIOGRAPHY;
field.writeToDoc();
yield fieldGetter.updateSession();
}
@ -921,7 +864,6 @@ Zotero.Integration.Fields = function(session, doc, fieldErrorHandler) {
this._doc = doc;
this._deferreds = null;
this._removeCodeKeys = {};
this._removeCodeFields = {};
this._bibliographyFields = [];
this._bibliographyData = "";
@ -1054,7 +996,6 @@ Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(fun
yield this.get();
this._session.resetRequest(this._doc);
this._removeCodeKeys = {};
this._removeCodeFields = {};
this._bibliographyFields = [];
this._bibliographyData = "";
@ -1104,31 +1045,28 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu
for(var n = this._fields.length; i<n; i++) {
let field = Zotero.Integration.Field.loadExisting(this._fields[i]);
if (field.type === INTEGRATION_TYPE_ITEM) {
var noteIndex = field.getNoteIndex();
var noteIndex = field.getNoteIndex(),
citation;
try {
yield this._session.addCitation(i, noteIndex, field.unserialize());
citation = new Zotero.Integration.Citation(field);
let action = yield citation.loadItemData();
if (action == Zotero.Integration.Citation.DELETE) {
this._removeCodeFields[i] = true;
// Mark for removal and continue
continue;
} else if (action == Zotero.Integration.Citation.UPDATE) {
this._session.updateIndices[index] = true;
}
} catch(e) {
var removeCode = false;
if(e instanceof Zotero.Integration.CorruptFieldException) {
e.setContext(this, i)
} else if(e instanceof Zotero.Integration.MissingItemException) {
// Check if we've already decided to remove this field code
for (let reselectKey of e.reselectKeys) {
if(this._removeCodeKeys[reselectKey]) {
this._removeCodeFields[i] = true;
removeCode = true;
break;
}
}
if(!removeCode) e.setContext(this, i);
if (e instanceof Zotero.Integration.CorruptFieldException) {
e.setContext(this, i);
}
if(!removeCode) {
if(this.fieldErrorHandler) return this.fieldErrorHandler(e);
throw e;
}
if (this.fieldErrorHandler) return this.fieldErrorHandler(e);
throw e;
}
yield this._session.addCitation(i, noteIndex, citation);
} else if (field.type === INTEGRATION_TYPE_BIBLIOGRAPHY) {
if (this.ignoreEmptyBibliography && field.text.trim() === "") {
this._removeCodeFields[i] = true;
@ -1194,23 +1132,16 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations,
// If there is no citation, we're deleting it, or we shouldn't update it, ignore
// it
if(!citation || citation.properties.delete) continue;
var isRich = false;
if(!citation.properties.dontUpdate) {
var formattedCitation = citation.properties.custom
? citation.properties.custom : this._session.citationText[i];
if(formattedCitation.indexOf("\\") !== -1) {
// need to set text as RTF
formattedCitation = "{\\rtf "+formattedCitation+"}"
isRich = true;
}
if(forceCitations === FORCE_CITATIONS_RESET_TEXT
|| citation.properties.formattedCitation !== formattedCitation) {
// Check if citation has been manually modified
if(!ignoreCitationChanges && citation.properties.plainCitation) {
var plainCitation = field.getText();
var plainCitation = field.text;
if(plainCitation !== citation.properties.plainCitation) {
// Citation manually modified; ask user if they want to save changes
Zotero.debug("[_updateDocument] Attempting to update manually modified citation.\n"
@ -1228,25 +1159,19 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations,
}
if(!citation.properties.dontUpdate) {
field.setText(formattedCitation, isRich);
field.text = formattedCitation;
citation.properties.formattedCitation = formattedCitation;
citation.properties.plainCitation = field.getText();
citation.properties.plainCitation = field.text;
}
}
}
var fieldCode = this._session.getCitationField(citation);
if(fieldCode != citation.properties.field) {
field.setCode(`ITEM CSL_CITATION ${fieldCode}`);
if(this._session.data.prefs.fieldType === "ReferenceMark"
&& this._session.data.prefs.noteType != 0 && isRich
&& !citation.properties.dontUpdate) {
// For ReferenceMarks with formatting, we need to set the text again, because
// setting the field code removes formatting from the mark. I don't like this.
field.setText(formattedCitation, isRich);
}
var serializedCitation = citation.serialize();
if (serializedCitation != citation.properties.field) {
field.code = serializedCitation;
}
field.writeToDoc();
nUpdated++;
}
@ -1260,7 +1185,7 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations,
if(forceBibliography || this._session.bibliographyDataHasChanged) {
var bibliographyData = this._session.getBibliographyData();
for (let field of bibliographyFields) {
field.setCode(`BIBL ${bibliographyData} CSL_BIBLIOGRAPHY`);
field.code = bibliographyData;
}
}
@ -1295,11 +1220,12 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations,
yield;
}
if(bibliographyText) {
field.setText(bibliographyText, true);
if (bibliographyText) {
field.text = bibliographyText;
} else {
field.setText("{Bibliography}", false);
field.text = "{Bibliography}";
}
field.writeToDoc();
nUpdated += 5;
}
}
@ -1321,64 +1247,24 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations,
* Brings up the addCitationDialog, prepopulated if a citation is provided
*/
Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(function* (field) {
var newField, citation;
var newField;
// TODO: refactor citation/field preparation
// Citation loading should be moved into Zotero.Integration.Citation
// if there's already a citation, make sure we have item IDs in addition to keys
if (field) {
field = Zotero.Integration.Field.loadExisting(field);
if (field.type != INTEGRATION_TYPE_ITEM) {
if (field.type != INTEGRATION_TYPE_ITEM) {
throw new Zotero.Exception.Alert("integration.error.notInCitation");
}
try {
citation = field.unserialize();
} catch(e) {}
if (citation) {
try {
yield this._session.lookupItems(citation);
} catch(e) {
if(e instanceof Zotero.Integration.MissingItemException) {
citation.citationItems = [];
} else {
throw e;
}
}
if(citation.properties.dontUpdate
|| (citation.properties.plainCitation
&& field.getText() !== citation.properties.plainCitation)) {
this._doc.activate();
Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n"
+ "citation.properties.dontUpdate: " + citation.properties.dontUpdate + "\n"
+ "Original: " + citation.properties.plainCitation + "\n"
+ "Current: " + field.getText()
);
if(!this._doc.displayAlert(Zotero.getString("integration.citationChanged.edit"),
DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) {
throw new Zotero.Exception.UserCancelled("editing citation");
}
}
// make sure it's going to get updated
delete citation.properties["formattedCitation"];
delete citation.properties["plainCitation"];
delete citation.properties["dontUpdate"];
}
} else {
newField = true;
field = new Zotero.Integration.CitationField(yield this.addField(true));
}
if (!citation) {
field.clearCode();
field.writeToDoc();
citation = {"citationItems":[], "properties":{}};
}
var citation = new Zotero.Integration.Citation(field);
yield citation.prepareForEditing();
// -------------------
// Preparing stuff to pass into CitationEditInterface
var fieldIndexPromise = this.get().then(function(fields) {
@ -1399,6 +1285,7 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f
citation.properties.noteIndex = field.getNoteIndex();
var previewFn = Zotero.Promise.coroutine(function* (citation) {
let idx = yield fieldIndexPromise;
yield citationsByItemIDPromise;
let citationsPre, citationsPost, citationIndices;
[citationsPre, citationsPost, citationIndices] = this._session._getPrePost(idx);
try {
@ -1409,38 +1296,32 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f
}.bind(this));
var io = new Zotero.Integration.CitationEditInterface(
// Clone citation
JSON.parse(JSON.stringify(citation)),
citation,
this._session.style.opt.sort_citations, fieldIndexPromise, citationsByItemIDPromise,
previewFn
);
if (Zotero.Prefs.get("integration.useClassicAddCitationDialog")) {
Zotero.Integration.displayDialog(this._doc,
'chrome://zotero/content/integration/addCitationDialog.xul', 'alwaysRaised,resizable',
io);
Zotero.Integration.displayDialog('chrome://zotero/content/integration/addCitationDialog.xul',
'alwaysRaised,resizable', io);
} else {
var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised')
? 'popup' : 'alwaysRaised')+',resizable=false';
Zotero.Integration.displayDialog(this._doc,
'chrome://zotero/content/integration/quickFormat.xul', mode, io);
Zotero.Integration.displayDialog('chrome://zotero/content/integration/quickFormat.xul',
mode, io);
}
// -------------------
// io.promise resolves/rejects when the citation dialog is closed
try {
this.progressCallback = yield io.promise;
} catch (e) {
// io.promise resolves when the citation dialog is closed
this.progressCallback = yield io.promise;
if (!io.citation.citationItems.length) {
// Try to delete new field on cancel
if (newField) {
// Try to delete new field on failure
try {
field.delete();
} catch(e) {}
throw e;
}
}
if (!io.citation.citationItems.length) {
throw new Zotero.Exception.UserCancelled("inserting citation");
}
@ -1644,8 +1525,7 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func
// Make sure styles are initialized for new docs
yield Zotero.Styles.init();
yield Zotero.Integration.displayDialog(this.doc,
'chrome://zotero/content/integration/integrationDocPrefs.xul', '', io);
yield Zotero.Integration.displayDialog('chrome://zotero/content/integration/integrationDocPrefs.xul', '', io);
if (!io.style || !io.fieldType) {
throw new Zotero.Exception.UserCancelled("document preferences window");
@ -1681,97 +1561,6 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func
return oldData || null;
})
/**
* Reselects an item to replace a deleted item
* @param exception {Zotero.Integration.MissingItemException}
*/
Zotero.Integration.Session.prototype.reselectItem = function(doc, exception) {
var io = new function() { this.wrappedJSObject = this; },
me = this;
io.addBorder = Zotero.isWin;
io.singleSelection = true;
return Zotero.Integration.displayDialog(doc, 'chrome://zotero/content/selectItemsDialog.xul',
'resizable', io).then(function() {
if(io.dataOut && io.dataOut.length) {
var itemID = io.dataOut[0];
// add reselected item IDs to hash, so they can be used
for (let reselectKey of exception.reselectKeys) {
me.reselectedItems[reselectKey] = itemID;
}
// add old URIs to map, so that they will be included
if(exception.reselectKeyType == RESELECT_KEY_URI) {
me.uriMap.add(itemID, exception.reselectKeys.concat(me.uriMap.getURIsForItemID(itemID)));
}
// flag for update
me.updateItemIDs[itemID] = true;
}
});
}
/**
* Generates a field from a citation object
*/
Zotero.Integration.Session.prototype.getCitationField = function(citation) {
const saveProperties = ["custom", "unsorted", "formattedCitation", "plainCitation", "dontUpdate"];
const saveCitationItemKeys = ["locator", "label", "suppress-author", "author-only", "prefix",
"suffix"];
var type;
var field = [];
field.push('"citationID":'+uneval(citation.citationID));
var properties = JSON.stringify(citation.properties, saveProperties);
if(properties != "{}") {
field.push('"properties":'+properties);
}
var m = citation.citationItems.length;
var citationItems = new Array(m);
for(var j=0; j<m; j++) {
var citationItem = citation.citationItems[j],
serializeCitationItem = {},
key, value;
// add URI and itemData
var slashIndex;
if(typeof citationItem.id === "string" && (slashIndex = citationItem.id.indexOf("/")) !== -1) {
// this is an embedded item
serializeCitationItem.id = citationItem.itemData.id;
serializeCitationItem.uris = citationItem.uris;
// XXX For compatibility with older versions of Zotero; to be removed at a later date
serializeCitationItem.uri = serializeCitationItem.uris;
// always store itemData, since we have no way to get it back otherwise
serializeCitationItem.itemData = citationItem.itemData;
} else {
serializeCitationItem.id = citationItem.id;
serializeCitationItem.uris = this.uriMap.getURIsForItemID(citationItem.id);
// XXX For compatibility with older versions of Zotero; to be removed at a later date
serializeCitationItem.uri = serializeCitationItem.uris;
serializeCitationItem.itemData = this.style.sys.retrieveItem(citationItem.id);
}
// copy saveCitationItemKeys
for(var i=0, n=saveCitationItemKeys.length; i<n; i++) {
if((value = citationItem[(key = saveCitationItemKeys[i])])) {
serializeCitationItem[key] = value;
}
}
citationItems[j] = JSON.stringify(serializeCitationItem);
}
field.push('"citationItems":['+citationItems.join(",")+"]");
field.push('"schema":"https://github.com/citation-style-language/schema/raw/master/csl-citation.json"');
return "{"+field.join(",")+"}";
}
/**
* Adds a citation based on a serialized Word field
*/
@ -1786,10 +1575,7 @@ Zotero.Integration._oldCitationLocatorMap = {
*/
Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(function* (index, noteIndex, citation) {
var index = parseInt(index, 10);
// get items
yield this.lookupItems(citation, index);
citation.properties.added = true;
citation.properties.zoteroIndex = index;
citation.properties.noteIndex = noteIndex;
@ -1828,122 +1614,6 @@ Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(func
this.documentCitationIDs[citation.citationID] = citation.citationID;
});
/**
* Looks up item IDs to correspond with keys or generates embedded items for given citation object.
* Throws a MissingItemException if item was not found.
*/
Zotero.Integration.Session.prototype.lookupItems = Zotero.Promise.coroutine(function* (citation, index) {
let items = [];
for(var i=0, n=citation.citationItems.length; i<n; i++) {
var citationItem = citation.citationItems[i];
// get Zotero item
var zoteroItem = false,
needUpdate;
if(citationItem.uris) {
[zoteroItem, needUpdate] = yield this.uriMap.getZoteroItemForURIs(citationItem.uris);
if(needUpdate && index) this.updateIndices[index] = true;
// Unfortunately, people do weird things with their documents. One weird thing people
// apparently like to do (http://forums.zotero.org/discussion/22262/) is to copy and
// paste citations from other documents created with earlier versions of Zotero into
// their documents and then not refresh the document. Usually, this isn't a problem. If
// document is edited by the same user, it will work without incident. If the first
// citation of a given item doesn't contain itemData, the user will get a
// MissingItemException. However, it may also happen that the first citation contains
// itemData, but later citations don't, because the user inserted the item properly and
// then copied and pasted the same citation from another document. We check for that
// possibility here.
if(zoteroItem.cslItemData && !citationItem.itemData) {
citationItem.itemData = zoteroItem.cslItemData;
this.updateIndices[index] = true;
}
} else {
if(citationItem.key && citationItem.libraryID) {
// DEBUG: why no library id?
zoteroItem = Zotero.Items.getByLibraryAndKey(citationItem.libraryID, citationItem.key);
} else if(citationItem.itemID) {
zoteroItem = Zotero.Items.get(citationItem.itemID);
} else if(citationItem.id) {
zoteroItem = Zotero.Items.get(citationItem.id);
}
if(zoteroItem && index) this.updateIndices[index] = true;
}
// if no item, check if it was already reselected and otherwise handle as a missing item
if(!zoteroItem) {
if(citationItem.uris) {
var reselectKeys = citationItem.uris;
var reselectKeyType = RESELECT_KEY_URI;
} else if(citationItem.key) {
var reselectKeys = [citationItem.key];
var reselectKeyType = RESELECT_KEY_ITEM_KEY;
} else if(citationItem.id) {
var reselectKeys = [citationItem.id];
var reselectKeyType = RESELECT_KEY_ITEM_ID;
} else {
var reselectKeys = [citationItem.itemID];
var reselectKeyType = RESELECT_KEY_ITEM_ID;
}
// look to see if item has already been reselected
for (let reselectKey of reselectKeys) {
if(this.reselectedItems[reselectKey]) {
zoteroItem = Zotero.Items.get(this.reselectedItems[reselectKey]);
citationItem.id = zoteroItem.id;
if(index) this.updateIndices[index] = true;
break;
}
}
if(!zoteroItem) {
if(citationItem.itemData) {
// add new embedded item
var itemData = Zotero.Utilities.deepCopy(citationItem.itemData);
// assign a random string as an item ID
var anonymousID = Zotero.randomString();
var globalID = itemData.id = citationItem.id = this.data.sessionID+"/"+anonymousID;
this.embeddedItems[anonymousID] = itemData;
// assign a Zotero item
var surrogateItem = this.embeddedZoteroItems[anonymousID] = new Zotero.Item();
Zotero.Utilities.itemFromCSLJSON(surrogateItem, itemData);
surrogateItem.cslItemID = globalID;
surrogateItem.cslURIs = citationItem.uris;
surrogateItem.cslItemData = itemData;
for(var j=0, m=citationItem.uris.length; j<m; j++) {
this.embeddedZoteroItemsByURI[citationItem.uris[j]] = surrogateItem;
}
} else {
// if not already reselected, throw a MissingItemException
throw(new Zotero.Integration.MissingItemException(
reselectKeys, reselectKeyType, i, citation.citationItems.length));
}
}
}
if(zoteroItem) {
if (zoteroItem.cslItemID) {
citationItem.id = zoteroItem.cslItemID;
}
else {
citationItem.id = zoteroItem.id;
items.push(zoteroItem);
}
}
}
// Items may be in libraries that haven't been loaded, and retrieveItem() is synchronous, so load
// all data (as required by toJSON(), which is used by itemToExportFormat(), which is used by
// itemToCSLJSON()) now
if (items.length) {
yield Zotero.Items.loadDataTypes(items);
}
});
/**
* Gets integration bibliography
*/
@ -2245,14 +1915,13 @@ Zotero.Integration.Session.prototype.getBibliographyData = function() {
/**
* Edits integration bibliography
*/
Zotero.Integration.Session.prototype.editBibliography = function(doc) {
Zotero.Integration.Session.prototype.editBibliography = function() {
var bibliographyEditor = new Zotero.Integration.Session.BibliographyEditInterface(this);
var io = new function() { this.wrappedJSObject = bibliographyEditor; }
this.bibliographyDataHasChanged = this.bibliographyHasChanged = true;
return Zotero.Integration.displayDialog(doc,
'chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io);
return Zotero.Integration.displayDialog('chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io);
}
/**
@ -2620,16 +2289,8 @@ Zotero.Integration.Field = class {
this.code = '{}';
};
writeToDoc(doc) {
let text = this.text;
let isRich = false;
// If RTF wrap with RTF tags
if (text.indexOf("\\") !== -1) {
text = "{\\rtf "+text+"}";
isRich = true;
}
this._field.setText(text, isRich);
writeToDoc() {
if (!this.dirty) return;
// Boo. Inconsistent.
if (this.type == INTEGRATION_TYPE_ITEM) {
this._field.setCode(`ITEM CSL_CITATION ${this.code}`);
@ -2639,6 +2300,16 @@ Zotero.Integration.Field = class {
this._field.setCode(`TEMP`);
}
this.dirty = false;
// NB: Setting code in LO removes rtf formatting, so the order here is important
let text = this.text;
let isRich = false;
// If RTF wrap with RTF tags
if (text.includes("\\")) {
text = "{\\rtf "+text+"}";
isRich = true;
}
this._field.setText(text, isRich);
// Retrigger retrieval from doc.
this._text = null;
@ -2651,7 +2322,7 @@ Zotero.Integration.Field = class {
if (start == -1) {
return '{}';
}
return code.substring(start, code.indexOf('}')+1);
return code.substring(start, code.lastIndexOf('}')+1);
};
};
@ -2841,3 +2512,236 @@ Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field {
}
}
};
Zotero.Integration.Citation = class {
constructor(citationField) {
let data = citationField.unserialize();
this.citationItems = data.citationItems;
this.properties = data.properties;
this.properties.noteIndex = citationField.getNoteIndex();
this._field = citationField;
}
/**
* Load item data for current item
* @param {Boolean} [promptToReselect=true] - will throw a MissingItemException if false
* @returns {Promise{Number}}
* - Zotero.Integration.Citation.NO_ACTION
* - Zotero.Integration.Citation.UPDATE
* - Zotero.Integration.Citation.DELETE
*/
loadItemData() {
return Zotero.Promise.coroutine(function *(promptToReselect=true){
let items = [];
var needUpdate = false;
for (var i=0, n=this.citationItems.length; i<n; i++) {
var citationItem = this.citationItems[i];
// get Zotero item
var zoteroItem = false;
if (citationItem.uris) {
let itemNeedUpdate;
[zoteroItem, itemNeedUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(citationItem.uris);
needUpdate = needUpdate || itemNeedUpdate;
// Unfortunately, people do weird things with their documents. One weird thing people
// apparently like to do (http://forums.zotero.org/discussion/22262/) is to copy and
// paste citations from other documents created with earlier versions of Zotero into
// their documents and then not refresh the document. Usually, this isn't a problem. If
// document is edited by the same user, it will work without incident. If the first
// citation of a given item doesn't contain itemData, the user will get a
// MissingItemException. However, it may also happen that the first citation contains
// itemData, but later citations don't, because the user inserted the item properly and
// then copied and pasted the same citation from another document. We check for that
// possibility here.
if (zoteroItem.cslItemData && !citationItem.itemData) {
citationItem.itemData = zoteroItem.cslItemData;
needUpdate = true;
}
} else {
if (citationItem.key && citationItem.libraryID) {
// DEBUG: why no library id?
zoteroItem = Zotero.Items.getByLibraryAndKey(citationItem.libraryID, citationItem.key);
} else if (citationItem.itemID) {
zoteroItem = Zotero.Items.get(citationItem.itemID);
} else if (citationItem.id) {
zoteroItem = Zotero.Items.get(citationItem.id);
}
if (zoteroItem) needUpdate = true;
}
// Item no longer in library
if (!zoteroItem) {
// Use embedded item
if (citationItem.itemData) {
// add new embedded item
var itemData = Zotero.Utilities.deepCopy(citationItem.itemData);
// assign a random string as an item ID
var anonymousID = Zotero.randomString();
var globalID = itemData.id = citationItem.id = Zotero.Integration.currentSession.data.sessionID+"/"+anonymousID;
Zotero.Integration.currentSession.embeddedItems[anonymousID] = itemData;
// assign a Zotero item
var surrogateItem = Zotero.Integration.currentSession.embeddedZoteroItems[anonymousID] = new Zotero.Item();
Zotero.Utilities.itemFromCSLJSON(surrogateItem, itemData);
surrogateItem.cslItemID = globalID;
surrogateItem.cslURIs = citationItem.uris;
surrogateItem.cslItemData = itemData;
for(var j=0, m=citationItem.uris.length; j<m; j++) {
Zotero.Integration.currentSession.embeddedZoteroItemsByURI[citationItem.uris[j]] = surrogateItem;
}
} else if (promptToReselect) {
zoteroItem = yield this.handleMissingItem(i);
if (zoteroItem) needUpdate = true;
else return Zotero.Integration.Citation.DELETE;
} else {
// throw a MissingItemException
throw (new Zotero.Integration.MissingItemException(this, i));
}
}
if (zoteroItem) {
if (zoteroItem.cslItemID) {
citationItem.id = zoteroItem.cslItemID;
}
else {
citationItem.id = zoteroItem.id;
items.push(zoteroItem);
}
}
}
// Items may be in libraries that haven't been loaded, and retrieveItem() is synchronous, so load
// all data (as required by toJSON(), which is used by itemToExportFormat(), which is used by
// itemToCSLJSON()) now
if (items.length) {
yield Zotero.Items.loadDataTypes(items);
}
return needUpdate ? Zotero.Integration.Citation.UPDATE : Zotero.Integration.Citation.NO_ACTION;
}).apply(this, arguments);
}
handleMissingItem() {
return Zotero.Promise.coroutine(function* (idx) {
// Ask user what to do with this item
if (this.citationItems.length == 1) {
var msg = Zotero.getString("integration.missingItem.single");
} else {
var msg = Zotero.getString("integration.missingItem.multiple", (idx).toString());
}
msg += '\n\n'+Zotero.getString('integration.missingItem.description');
this._field.select();
Zotero.Integration.currentDoc.activate();
var result = Zotero.Integration.currentDoc.displayAlert(msg, 1, 3);
if (result == 0) { // Cancel
throw new Zotero.Exception.UserCancelled("document update");
} else if(result == 1) { // No
return false;
}
// Yes - prompt to reselect
var io = new function() { this.wrappedJSObject = this; };
io.addBorder = Zotero.isWin;
io.singleSelection = true;
yield Zotero.Integration.displayDialog('chrome://zotero/content/selectItemsDialog.xul', 'resizable', io);
if (io.dataOut && io.dataOut.length) {
return Zotero.Items.get(io.dataOut[0]);
}
}).apply(this, arguments);
}
prepareForEditing() {
return Zotero.Promise.coroutine(function *(){
// Check for modified field text or dontUpdate flag
if (this.properties.dontUpdate
|| (this.properties.plainCitation
&& this._field.text !== this.properties.plainCitation)) {
Zotero.Integration.currentDoc.activate();
Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n"
+ "citaion.properties.dontUpdate: " + this.properties.dontUpdate + "\n"
+ "Original: " + this.properties.plainCitation + "\n"
+ "Current: " + this._field.text
);
if (!Zotero.Integration.currentDoc.displayAlert(Zotero.getString("integration.citationChanged.edit"),
DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) {
throw new Zotero.Exception.UserCancelled("editing citation");
}
}
// make sure it's going to get updated
delete this.properties["formattedCitation"];
delete this.properties["plainCitation"];
delete this.properties["dontUpdate"];
// Load items to be displayed in edit dialog
yield this.loadItemData();
}).apply(this, arguments);
}
/**
* Serializes the citation into CSL code representation
* @returns {string}
*/
serialize() {
const saveProperties = ["custom", "unsorted", "formattedCitation", "plainCitation", "dontUpdate"];
const saveCitationItemKeys = ["locator", "label", "suppress-author", "author-only", "prefix",
"suffix"];
var citation = {};
citation.citationID = this.citationID;
citation.properties = {};
for (let key of saveProperties) {
if (key in this.properties) citation.properties[key] = this.properties[key];
}
citation.citationItems = new Array(this.citationItems.length);
for (let i=0; i < this.citationItems.length; i++) {
var citationItem = this.citationItems[i],
serializeCitationItem = {};
// add URI and itemData
var slashIndex;
if (typeof citationItem.id === "string" && (slashIndex = citationItem.id.indexOf("/")) !== -1) {
// this is an embedded item
serializeCitationItem.id = citationItem.itemData.id;
serializeCitationItem.uris = citationItem.uris;
// XXX For compatibility with older versions of Zotero; to be removed at a later date
serializeCitationItem.uri = serializeCitationItem.uris;
// always store itemData, since we have no way to get it back otherwise
serializeCitationItem.itemData = citationItem.itemData;
} else {
serializeCitationItem.id = citationItem.id;
serializeCitationItem.uris = Zotero.Integration.currentSession.uriMap.getURIsForItemID(citationItem.id);
// XXX For compatibility with older versions of Zotero; to be removed at a later date
serializeCitationItem.uri = serializeCitationItem.uris;
serializeCitationItem.itemData = Zotero.Integration.currentSession.style.sys.retrieveItem(citationItem.id);
}
for (let key of saveCitationItemKeys) {
if (key in citationItem) serializeCitationItem[key] = citationItem[key];
}
citation.citationItems[i] = serializeCitationItem;
}
citation.schema = "https://github.com/citation-style-language/schema/raw/master/csl-citation.json";
return JSON.stringify(citation);
}
};
Zotero.Integration.Citation.NO_ACTION = 0;
Zotero.Integration.Citation.UPDATE = 1;
Zotero.Integration.Citation.DELETE = 2;

View file

@ -273,14 +273,16 @@ describe("Zotero.Integration", function () {
function setAddEditItems(items) {
if (items.length == undefined) items = [items];
dialogResults.quickFormat = function(doc, dialogName) {
dialogResults.quickFormat = function(dialogName) {
var citationItems = items.map((i) => {return {id: i.id} });
var field = doc.insertField("Field", 0);
field.setCode('TEMP');
var field = new Zotero.Integration.CitationField(Zotero.Integration.currentDoc.insertField("Field", 0));
field.clearCode();
field.writeToDoc();
var citation = new Zotero.Integration.Citation(field);
var integrationDoc = addEditCitationSpy.lastCall.thisValue;
var fieldGetter = new Zotero.Integration.Fields(integrationDoc._session, integrationDoc._doc, () => 0);
var io = new Zotero.Integration.CitationEditInterface(
{ citationItems, properties: {} },
citation,
field,
fieldGetter,
integrationDoc._session
@ -313,10 +315,11 @@ describe("Zotero.Integration", function () {
// possible bug that reset() erases callsFake.
// @NOTE: https://github.com/sinonjs/sinon/issues/1341
// displayDialogStub.callsFake(function(doc, dialogName, prefs, io) {
function(doc, dialogName, prefs, io) {
function(dialogName, prefs, io) {
Zotero.debug(`Display dialog: ${dialogName}`, 2);
var ioResult = dialogResults[dialogName.substring(dialogName.lastIndexOf('/')+1, dialogName.length-4)];
if (typeof ioResult == 'function') {
ioResult = ioResult(doc, dialogName);
ioResult = ioResult(dialogName);
}
Object.assign(io, ioResult);
return Zotero.Promise.resolve();
@ -460,7 +463,7 @@ describe("Zotero.Integration", function () {
displayDialogStub.reset();
yield execCommand('addEditBibliography', docID);
assert.isTrue(displayDialogStub.calledOnce);
assert.isTrue(displayDialogStub.lastCall.args[1].includes('editBibliographyDialog'));
assert.isTrue(displayDialogStub.lastCall.args[0].includes('editBibliographyDialog'));
});
});
});