www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 878f70998f9024d9f3c1239cdaf3193bb92acc33
parent 449eae031f8f68aef16d52cdb4faf34347ffccdd
Author: Simon Kornblith <simon@simonster.com>
Date:   Mon, 19 Dec 2011 01:36:33 -0500

Show most recently cited items at the top of QuickFormat. Also, typing "ibid" into QuickFormat now shows a list of the most recently cited items.

Diffstat:
Mchrome/content/zotero/integration/quickFormat.js | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mchrome/content/zotero/xpcom/integration.js | 266+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mchrome/locale/en-US/zotero/zotero.properties | 3+++
3 files changed, 311 insertions(+), 130 deletions(-)

diff --git a/chrome/content/zotero/integration/quickFormat.js b/chrome/content/zotero/integration/quickFormat.js @@ -242,36 +242,130 @@ var Zotero_QuickFormat = new function () { } } - var ids = (haveConditions ? s.search() : []); - - // no need to refresh anything if box hasnt changed - if(ids.length === curIDs.length) { - var mismatch = false; - for(var i=0; i<ids.length; i++) { - if(curIDs[i] !== ids[i]) { - mismatch = true; - break; + if(haveConditions) { + var searchResultIDs = (haveConditions ? s.search() : []); + + // No need to refresh anything if box hasn't changed + if(searchResultIDs.length === curIDs.length) { + var mismatch = false; + for(var i=0; i<searchResultIDs.length; i++) { + if(curIDs[i] !== searchResultIDs[i]) { + mismatch = true; + break; + } + } + if(!mismatch) return; + } + curIDs = searchResultIDs; + + // Check to see which search results match items already in the document + var citedItems, completed = false, preserveSelection = false; + io.getItems(function(citedItems) { + completed = true; + + if(str.toLowerCase() === Zotero.getString("integration.ibid").toLowerCase()) { + // If "ibid" is entered, show all cited items + citedItemsMatchingSearch = citedItems; + } else { + Zotero.debug("Searching cited items"); + // Search against items. We do this here because it's possible that some of these + // items are only in the doc, and not in the DB. + var splits = Zotero.Fulltext.semanticSplitter(str), + citedItemsMatchingSearch = []; + for(var i=0, iCount=citedItems.length; i<iCount; i++) { + // Generate a string to search for each item + var item = citedItems[i], + itemStr = [creator.ref.firstName+" "+creator.ref.lastName for each(creator in item.getCreators())]; + itemStr = itemStr.concat([item.getField("title"), item.getField("date", true, true).substr(0, 4)]).join(" "); + + // See if words match + for(var j=0, jCount=splits.length; j<jCount; j++) { + var split = splits[j]; + if(itemStr.toLowerCase().indexOf(split) === -1) break; + } + + // If matched, add to citedItemsMatchingSearch + if(j === jCount) citedItemsMatchingSearch.push(item); + } + Zotero.debug("Searched cited items"); } + + _updateItemList(citedItemsMatchingSearch, searchResultIDs, preserveSelection); + }); + + if(!completed) { + // We are going to have to wait until items have been retrieved from the document. + // Until then, show item list without cited items. + _updateItemList(false, searchResultIDs); + preserveSelection = true; } - if(!mismatch) return; + } else { + // No search conditions, so just clear the box + _updateItemList([], []); + } + } + + /** + * Sorts items + */ + function _itemSort(a, b) { + // Sort by library ID + var libA = a.libraryID, libB = b.libraryID; + if(libA !== libB) { + return libA - libB; + } + + // Sort by last name of first author + var creatorsA = a.getCreators(), creatorsB = b.getCreators(), + caExists = creatorsA.length ? 1 : 0, cbExists = creatorsB.length ? 1 : 0; + if(caExists !== cbExists) { + return cbExists-caExists; + } else if(caExists) { + return creatorsA[0].ref.lastName.localeCompare(creatorsB[0].ref.lastName); + } + + // Sort by date + var yearA = a.getField("date", true, true).substr(0, 4), + yearB = b.getField("date", true, true).substr(0, 4); + return yearA - yearB; + } + + /** + * Updates the item list + */ + function _updateItemList(citedItems, searchResultIDs, preserveSelection) { + var selectedIndex = 1, previousItemID; + + // Do this so we can preserve the selected item after cited items have been loaded + if(preserveSelection && referenceBox.selectedIndex !== 2) { + previousItemID = parseInt(referenceBox.selectedItem.getAttribute("zotero-item"), 10); } - curIDs = ids; while(referenceBox.hasChildNodes()) referenceBox.removeChild(referenceBox.firstChild); - var firstSelectableIndex = 0; + if(!citedItems) { + // We don't know whether or not we have cited items, because we are waiting for document + // data + referenceBox.appendChild(_buildListSeparator(Zotero.getString("integration.cited.loading"))); + selectedIndex = 2; + } else if(citedItems.length) { + // We have cited items + referenceBox.appendChild(_buildListSeparator(Zotero.getString("integration.cited"))); + for(var i=0, n=citedItems.length; i<n; i++) { + referenceBox.appendChild(_buildListItem(citedItems[i])); + } + } - if(ids.length) { - if(ids.length > 50) ids = ids.slice(0, 50); - var items = Zotero.Items.get(ids); - - firstSelectableIndex = 1; + if(searchResultIDs.length && (!citedItems || citedItems.length < 50)) { + // Don't handle more than 50 results + if(searchResultIDs.length > 50-citedItems.length) { + searchResultIDs = searchResultIDs.slice(0, 50-citedItems.length); + } - //TODO: sort the currently used items in before any other items - items.sort(function(a, b) {return a.libraryID > b.libraryID}) + var items = Zotero.Items.get(searchResultIDs); + items.sort(_itemSort); var previousLibrary = -1; - for(var i=0, n=items.length; i<n; i++) { var item = items[i], libraryID = item.libraryID; @@ -281,17 +375,20 @@ var Zotero_QuickFormat = new function () { referenceBox.appendChild(_buildListSeparator(libraryName)); } - referenceBox.appendChild(_buildListItem(item),false); - + referenceBox.appendChild(_buildListItem(item)); previousLibrary = libraryID; - + + if(preserveSelection && item.id === previousItemID) { + selectedIndex = referenceBox.childNodes.length-1; + } } } _resize(); - - referenceBox.selectedIndex = firstSelectableIndex; - referenceBox.ensureIndexIsVisible(firstSelectableIndex); + if((citedItems && citedItems.length) || searchResultIDs.length) { + referenceBox.selectedIndex = selectedIndex; + referenceBox.ensureIndexIsVisible(selectedIndex); + } } @@ -391,7 +488,10 @@ var Zotero_QuickFormat = new function () { return rll; } - function _buildListSeparator(labelText) { + /** + * Creates a list separator to be added to the item list + */ + function _buildListSeparator(labelText, loading) { var titleNode = document.createElement("label"); titleNode.setAttribute("class", "quick-format-separator-title"); titleNode.setAttribute("flex", "1"); @@ -403,7 +503,7 @@ var Zotero_QuickFormat = new function () { rll.setAttribute("orient", "vertical"); rll.setAttribute("flex", "1"); rll.setAttribute("disabled", true); - rll.setAttribute("class", "quick-format-separator"); + rll.setAttribute("class", loading ? "quick-format-loading" : "quick-format-separator"); rll.appendChild(titleNode); rll.addEventListener("mousedown", _ignoreClick, true); rll.addEventListener("click", _ignoreClick, true); @@ -510,12 +610,16 @@ var Zotero_QuickFormat = new function () { var childNodes = referenceBox.childNodes, numReferences = 0, numSeparators = 0, + firstReference, + firstSeparator, height; - for(var i=0, n=childNodes.length; i<n; i++) { + for(var i=0, n=childNodes.length; i<n && numReferences < SHOWN_REFERENCES; i++) { if(childNodes[i].className === "quick-format-item") { numReferences++; + firstReference = childNodes[i]; } else if(childNodes[i].className === "quick-format-separator") { numSeparators++; + firstSeparator = childNodes[i]; } } @@ -540,8 +644,7 @@ var Zotero_QuickFormat = new function () { if(numReferences) { var height = referenceHeight ? - Math.min(numReferences*referenceHeight+1+numSeparators*separatorHeight, - SHOWN_REFERENCES*referenceHeight+1+separatorHeight) : 39; + Math.min(numReferences*referenceHeight+1+numSeparators*separatorHeight) : 39; if(panelShowing && height !== referencePanel.clientHeight) { referencePanel.sizeTo((window.outerWidth-30), height); @@ -554,10 +657,9 @@ var Zotero_QuickFormat = new function () { false, false, null); if(!referenceHeight) { - separatorHeight = referenceBox.firstChild.scrollHeight; - referenceHeight = referenceBox.childNodes[1].scrollHeight; - height = Math.min(numReferences*referenceHeight+1+numSeparators*separatorHeight, - SHOWN_REFERENCES*referenceHeight+1+separatorHeight); + separatorHeight = firstSeparator.scrollHeight; + referenceHeight = firstReference.scrollHeight; + height = Math.min(numReferences*referenceHeight+1+numSeparators*separatorHeight); referencePanel.sizeTo((window.outerWidth-30), height); } } diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js @@ -897,7 +897,7 @@ Zotero.Integration.Fields.prototype._retrieveFields = function() { var me = this; this._doc.getFieldsAsync(this._session.data.prefs['fieldType'], {"observe":function(subject, topic, data) { if(topic === "fields-available") { - if(me._progressCallback) me._progressCallback(75); + if(me.progressCallback) me.progressCallback(75); // Add fields to fields array var fieldsEnumerator = subject.QueryInterface(Components.interfaces.nsISimpleEnumerator); @@ -921,8 +921,8 @@ Zotero.Integration.Fields.prototype._retrieveFields = function() { } catch(e) { Zotero.Integration.handleError(e, me._doc); } - } else if(topic === "fields-progress" && me._progressCallback) { - me._progressCallback((data ? parseInt(data, 10)*(3/4) : null)); + } else if(topic === "fields-progress" && me.progressCallback) { + me.progressCallback((data ? parseInt(data, 10)*(3/4) : null)); } else if(topic === "fields-error") { Zotero.logError(data); Zotero.Integration.handleError(data, me._doc); @@ -1127,7 +1127,7 @@ Zotero.Integration.Fields.prototype._updateDocument = function(forceCitations, f var deleteCitations = this._session.updateCitations(); this._deleteFields = this._deleteFields.concat([i for(i in deleteCitations)]); - if(this._progressCallback) { + if(this.progressCallback) { var nFieldUpdates = [i for(i in this._session.updateIndices)].length; if(this._session.bibliographyHasChanged || forceBibliography) { nFieldUpdates += this._bibliographyFields.length*5; @@ -1136,8 +1136,8 @@ Zotero.Integration.Fields.prototype._updateDocument = function(forceCitations, f var nUpdated=0; for(var i in this._session.updateIndices) { - if(this._progressCallback && nUpdated % 10 == 0) { - this._progressCallback(75+(nUpdated/nFieldUpdates)*25); + if(this.progressCallback && nUpdated % 10 == 0) { + this.progressCallback(75+(nUpdated/nFieldUpdates)*25); yield true; } @@ -1238,8 +1238,8 @@ Zotero.Integration.Fields.prototype._updateDocument = function(forceCitations, f // set bibliography text for each(var field in bibliographyFields) { - if(this._progressCallback) { - this._progressCallback(75+(nUpdated/nFieldUpdates)*25); + if(this.progressCallback) { + this.progressCallback(75+(nUpdated/nFieldUpdates)*25); yield true; } @@ -1274,10 +1274,7 @@ Zotero.Integration.Fields.prototype._updateDocument = function(forceCitations, f * Brings up the addCitationDialog, prepopulated if a citation is provided */ Zotero.Integration.Fields.prototype.addEditCitation = function(field, callback) { - var newField, citation, fieldIndex - session = this._session, - me = this, - io = new function() { this.wrappedJSObject = this; } + var newField, citation, fieldIndex, session = this._session, me = this; // if there's already a citation, make sure we have item IDs in addition to keys if(field) { @@ -1287,7 +1284,7 @@ Zotero.Integration.Fields.prototype.addEditCitation = function(field, callback) throw new Zotero.Integration.DisplayException("notInCitation"); } - citation = io.citation = session.unserializeCitation(content); + citation = session.unserializeCitation(content); var zoteroItem; for each(var citationItem in citation.citationItems) { @@ -1320,105 +1317,184 @@ Zotero.Integration.Fields.prototype.addEditCitation = function(field, callback) var field = this.addField(true); field.setCode("TEMP"); - citation = io.citation = {"citationItems":{}, "properties":{}}; + citation = {"citationItems":{}, "properties":{}}; } - var sessionUpdated = false; - var previewing = false; + var io = new Zotero.Integration.CitationEditInterface(citation, field, this, session, newField, callback); - // assign preview function - var previewCallbackQueue; - function doPreview() { - // fieldIndex will already be set by the time we get here - citation.properties.zoteroIndex = fieldIndex; - citation.properties.noteIndex = field.getNoteIndex(); - returnVal = me._session.previewCitation(citation); - - for(var i=0, n=previewCallbackQueue.length; i<n; i++) previewCallbackQueue[i](returnVal); - previewCallbackQueue = undefined; - } - io.preview = function(callback) { - if(!previewCallbackQueue) previewCallbackQueue = []; - previewCallbackQueue.push(callback); - - var returnVal; - me.get(function() { - if(sessionUpdated) { - doPreview(); - } else { - me.updateSession(function() { - sessionUpdated = true; - doPreview(); - }); - } - }); + if(Zotero.Prefs.get("integration.useClassicAddCitationDialog")) { + session.displayDialog('chrome://zotero/content/integration/addCitationDialog.xul', 'alwaysRaised,resizable', io, true); + } else { + var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised') + ? 'popup' : 'alwaysRaised') + session.displayDialog('chrome://zotero/content/integration/quickFormat.xul', mode, io, true); } +} + +/** + * Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js + */ +Zotero.Integration.CitationEditInterface = function(citation, field, fields, session, deleteOnCancel, doneCallback) { + this.citation = citation; + this._field = field; + this._fields = fields; + this._session = session; + this._deleteOnCancel = deleteOnCancel; + this._doneCallback = doneCallback; - // assign sort function - io.sort = function() { - me._session.previewCitation(io.citation); - } + this._sessionUpdated = false; + this._sessionCallbackQueue = false; - // assign accept function - function doAccept() { - session.addCitation(fieldIndex, field.getNoteIndex(), io.citation); - session.updateIndices[fieldIndex] = true; - - if(!session.bibliographyHasChanged) { - for(var i=0, n=citation.citationItems.length; i<n; i++) { - if(session.citationsByItemID[citation.citationItems[i].itemID] && - session.citationsByItemID[citation.citationItems[i].itemID].length == 1) { - session.bibliographyHasChanged = true; - break; - } + // Needed to make this work across boundaries + this.wrappedJSObject = this; + + // Determine whether citation is sortable in current style + this.sortable = session.style.opt.sort_citations; + + // Citeproc-js style object for use of third-party extension + this.style = session.style; + + // Start getting citation data + var me = this; + fields.get(function(fields) { + for(var i=0, n=fields.length; i<n; i++) { + if(fields[i].equals(field)) { + me._fieldIndex = i; + return; } } + }); +} + +Zotero.Integration.CitationEditInterface.prototype = { + /** + * Run a function when the session information has been updated + * @param {Function} sessionUpdatedCallback + */ + "_runWhenSessionUpdated":function runWhenSessionUpdated(sessionUpdatedCallback) { + if(this._sessionUpdated) { + // session has been updated; run callback + sessionUpdatedCallback(); + } else if(this._sessionCallbackQueue) { + // session is being updated; add to queue + this._sessionCallbackQueue.push(sessionUpdatedCallback); + } else { + // session is not yet updated; start update + this._sessionCallbackQueue = [sessionUpdatedCallback]; + var me = this; + me._fields.updateSession(function() { + for(var i=0, n=me._sessionCallbackQueue.length; i<n; i++) { + me._sessionCallbackQueue[i](); + } + me._sessionUpdated = true; + delete me._sessionCallbackQueue; + }); + } + }, + + /** + * Execute a callback with a preview of the given citation + * @param {Function} previewCallback + */ + "preview":function preview(previewCallback) { + var me = this; + this._runWhenSessionUpdated(function() { + me.citation.properties.zoteroIndex = me._fieldIndex; + me.citation.properties.noteIndex = me._field.getNoteIndex(); + previewCallback(me._session.previewCitation(me.citation)); + }); + }, + + /** + * Sort the citation + */ + "sort":function() { + // Unlike above, we can do the previewing here without waiting for all the fields to load, + // since they won't change the sorting (I don't think) + this._session.previewCitation(this.citation); + }, + + /** + * 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":function(progressCallback) { + this._fields.progressCallback = progressCallback; - me.updateDocument(false, false, false, callback); - } - io.accept = function(progressCallback) { - me._progressCallback = progressCallback; - - if(io.citation.citationItems.length) { + if(this.citation.citationItems.length) { // Citation added - me.get(function() { - if(sessionUpdated) { - doAccept(); - } else { - me.updateSession(doAccept); + var me = this; + this._runWhenSessionUpdated(function() { + me._session.addCitation(me._fieldIndex, me._field.getNoteIndex(), me.citation); + me._session.updateIndices[me._fieldIndex] = true; + + if(!me._session.bibliographyHasChanged) { + var citationItems = me.citation.citationItems; + for(var i=0, n=citationItems.length; i<n; i++) { + if(me._session.citationsByItemID[citationItems[i].itemID] && + me._session.citationsByItemID[citationItems[i].itemID].length == 1) { + me._session.bibliographyHasChanged = true; + break; + } + } } + + me._fields.updateDocument(false, false, false, me._doneCallback); }); } else { - if(newField) { - // New citation was cancelled - field.delete(); - } - callback(); + if(this._deleteOnCancel) this._field.delete(); + if(this._doneCallback) this._doneCallback(); } - } - - // determine whether citation is sortable in current style - io.sortable = session.style.opt.sort_citations; - - // citeproc-js style object for use of third-party extension - io.style = session.style; + }, - // Start finding this citation this citation - this.get(function(fields) { - for(var i=0, n=fields.length; i<n; i++) { - if(fields[i].equals(field)) { - fieldIndex = i; - return; - } + /** + * Get a list of items used in the current document + * @param {Function} [itemsCallback] A callback to be run with item objects when items have been + * retrieved. + */ + "getItems":function(itemsCallback) { + if(this._fieldIndex || Zotero.Utilities.isEmpty(this._session.citationsByItemID)) { + // Either we already have field data for this run or we have no item data at all. + // Update session before continuing. + var me = this; + this._runWhenSessionUpdated(function() { me._getItems(itemsCallback); }); + } else { + // We have item data left over from a previous run with this document, so we don't need + // to wait. + this._getItems(itemsCallback); } - }); + }, - if(Zotero.Prefs.get("integration.useClassicAddCitationDialog")) { - session.displayDialog('chrome://zotero/content/integration/addCitationDialog.xul', 'alwaysRaised,resizable', io, true); - } else { - var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised') - ? 'popup' : 'alwaysRaised') - session.displayDialog('chrome://zotero/content/integration/quickFormat.xul', mode, io, true); + /** + * Helper function for getItems. Does the same thing, but this can assume that the session data + * has already been updated if it should be. + */ + "_getItems":function(itemsCallback) { + // TODO handle items not in library + var citationsByItemID = this._session.citationsByItemID; + var items = [itemID for(itemID in citationsByItemID) + if(citationsByItemID[itemID] && citationsByItemID[itemID].length + // Exclude this item + && (citationsByItemID[itemID].length > 1 + || citationsByItemID[itemID][0].properties.zoteroIndex !== this._fieldIndex))]; + + // Sort all previously cited items at top, and all items cited later at bottom + var fieldIndex = this._fieldIndex; + items.sort(function(a, b) { + var indexA = citationsByItemID[a][0].properties.zoteroIndex, + indexB = citationsByItemID[b][0].properties.zoteroIndex; + + if(indexA >= fieldIndex){ + if(indexB < fieldIndex) return 1; + return indexA - indexB; + } + + if(indexB > fieldIndex) return -1; + return indexB - indexA; + }); + + itemsCallback(Zotero.Items.get(items)); } } diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties @@ -581,6 +581,9 @@ integration.revert.button = Revert integration.removeBibEntry.title = The selected references is cited within your document. integration.removeBibEntry.body = Are you sure you want to omit it from your bibliography? +integration.cited = Cited +integration.cited.loading = Loading Cited Items… +integration.ibid = ibid integration.emptyCitationWarning.title = Blank Citation integration.emptyCitationWarning.body = The citation you have specified would be empty in the currently selected style. Are you sure you want to add it?