Created
August 3, 2016 10:19
-
-
Save adomasven/4bca052b9ceb80c1bc3d18c5f36fae2e to your computer and use it in GitHub Desktop.
Reverse Zotero 4.0 creator sort order
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Zotero.ItemTreeView.prototype.sort = function(itemID) | |
{ | |
var t = new Date; | |
// If Zotero pane is hidden, mark tree for sorting later in setTree() | |
if (!this._treebox.columns) { | |
this._needsSort = true; | |
return; | |
} | |
this._needsSort = false; | |
// Single child item sort -- just toggle parent open and closed | |
if (itemID && this._itemRowMap[itemID] && | |
this._getItemAtRow(this._itemRowMap[itemID]).ref.getSource()) { | |
var parentIndex = this.getParentIndex(this._itemRowMap[itemID]); | |
this.toggleOpenState(parentIndex); | |
this.toggleOpenState(parentIndex); | |
return; | |
} | |
var primaryField = this.getSortField(); | |
var sortFields = this.getSortFields(); | |
var dir = this.getSortDirection(); | |
var order = dir == 'descending' ? -1 : 1; | |
var collation = Zotero.getLocaleCollation(); | |
var sortCreatorAsString = Zotero.Prefs.get('sortCreatorAsString'); | |
Zotero.debug("Sorting items list by " + sortFields.join(", ") + " " + dir); | |
// Set whether rows with empty values should be displayed last, | |
// which may be different for primary and secondary sorting. | |
var emptyFirst = {}; | |
switch (primaryField) { | |
case 'title': | |
emptyFirst.title = true; | |
break; | |
// When sorting by title we want empty titles at the top, but if not | |
// sorting by title, empty titles should sort to the bottom so that new | |
// empty items don't get sorted to the middle of the items list. | |
default: | |
emptyFirst.title = false; | |
} | |
// Cache primary values while sorting, since base-field-mapped getField() | |
// calls are relatively expensive | |
var cache = {}; | |
sortFields.forEach(function (x) cache[x] = {}) | |
// Get the display field for a row (which might be a placeholder title) | |
function getField(field, row) { | |
var item = row.ref; | |
switch (field) { | |
case 'title': | |
return Zotero.Items.getSortTitle(item.getDisplayTitle()); | |
case 'hasAttachment': | |
if (item.isAttachment()) { | |
var state = item.fileExists() ? 1 : -1; | |
} | |
else if (item.isRegularItem()) { | |
var state = item.getBestAttachmentState(); | |
} | |
else { | |
return 0; | |
} | |
// Make sort order present, missing, empty when ascending | |
if (state === -1) { | |
state = 2; | |
} | |
return state * -1; | |
case 'numNotes': | |
return row.numNotes(false, true) || 0; | |
// Use unformatted part of date strings (YYYY-MM-DD) for sorting | |
case 'date': | |
var val = row.ref.getField('date', true, true); | |
if (val) { | |
val = val.substr(0, 10); | |
if (val.indexOf('0000') == 0) { | |
val = ""; | |
} | |
} | |
return val; | |
case 'year': | |
var val = row.ref.getField('date', true, true); | |
if (val) { | |
val = val.substr(0, 4); | |
if (val == '0000') { | |
val = ""; | |
} | |
} | |
return val; | |
default: | |
return row.ref.getField(field, false, true); | |
} | |
} | |
var includeTrashed = this._itemGroup.isTrash(); | |
function fieldCompare(a, b, sortField) { | |
var aItemID = a.id; | |
var bItemID = b.id; | |
var fieldA = cache[sortField][aItemID]; | |
var fieldB = cache[sortField][bItemID]; | |
switch (sortField) { | |
case 'firstCreator': | |
return creatorSort(a, b) * -1; | |
case 'itemType': | |
var typeA = Zotero.ItemTypes.getLocalizedString(a.ref.itemTypeID); | |
var typeB = Zotero.ItemTypes.getLocalizedString(b.ref.itemTypeID); | |
return (typeA > typeB) ? 1 : (typeA < typeB) ? -1 : 0; | |
default: | |
if (fieldA === undefined) { | |
cache[sortField][aItemID] = fieldA = getField(sortField, a); | |
} | |
if (fieldB === undefined) { | |
cache[sortField][bItemID] = fieldB = getField(sortField, b); | |
} | |
// Display rows with empty values last | |
if (!emptyFirst[sortField]) { | |
if(fieldA === '' && fieldB !== '') return 1; | |
if(fieldA !== '' && fieldB === '') return -1; | |
} | |
return collation.compareString(1, fieldA, fieldB); | |
} | |
} | |
function rowSort(a, b) { | |
var sortFields = Array.slice(arguments, 2); | |
var sortField; | |
while (sortField = sortFields.shift()) { | |
let cmp = fieldCompare(a, b, sortField); | |
if (cmp !== 0) { | |
return cmp; | |
} | |
} | |
return 0; | |
} | |
var firstCreatorSortCache = {}; | |
// Regexp to extract the whole string up to an optional "and" or "et al." | |
var andEtAlRegExp = new RegExp( | |
// Extract the beginning of the string in non-greedy mode | |
"^.+?" | |
// up to either the end of the string, "et al." at the end of string | |
+ "(?=(?: " + Zotero.getString('general.etAl').replace('.', '\.') + ")?$" | |
// or ' and ' | |
+ "| " + Zotero.getString('general.and') + " " | |
+ ")" | |
); | |
function creatorSort(a, b) { | |
// | |
// Try sorting by the first name in the firstCreator field, since we already have it | |
// | |
// For sortCreatorAsString mode, just use the whole string | |
// | |
var aItemID = a.id, | |
bItemID = b.id, | |
fieldA = firstCreatorSortCache[aItemID], | |
fieldB = firstCreatorSortCache[bItemID]; | |
if (fieldA === undefined) { | |
let firstCreator = Zotero.Items.getSortTitle(a.getField('firstCreator')); | |
if (sortCreatorAsString) { | |
var fieldA = firstCreator; | |
} | |
else { | |
var matches = andEtAlRegExp.exec(firstCreator); | |
var fieldA = matches ? matches[0] : ''; | |
} | |
firstCreatorSortCache[aItemID] = fieldA; | |
} | |
if (fieldB === undefined) { | |
let firstCreator = Zotero.Items.getSortTitle(b.getField('firstCreator')); | |
if (sortCreatorAsString) { | |
var fieldB = firstCreator; | |
} | |
else { | |
var matches = andEtAlRegExp.exec(firstCreator); | |
var fieldB = matches ? matches[0] : ''; | |
} | |
firstCreatorSortCache[bItemID] = fieldB; | |
} | |
if (fieldA === "" && fieldB === "") { | |
return 0; | |
} | |
var cmp = strcmp(fieldA, fieldB); | |
if (cmp !== 0 || sortCreatorAsString) { | |
return cmp; | |
} | |
// | |
// If first name is the same, compare actual creators | |
// | |
var aRef = a.ref, | |
bRef = b.ref, | |
aCreators = aRef.getCreators(), | |
bCreators = bRef.getCreators(), | |
aNumCreators = aCreators.length, | |
bNumCreators = bCreators.length, | |
aPrimary = Zotero.CreatorTypes.getPrimaryIDForType(aRef.itemTypeID), | |
bPrimary = Zotero.CreatorTypes.getPrimaryIDForType(bRef.itemTypeID); | |
const editorTypeID = 3, | |
contributorTypeID = 2; | |
// Find the first position of each possible creator type | |
var aPrimaryFoundAt = false; | |
var aEditorFoundAt = false; | |
var aContributorFoundAt = false; | |
loop: | |
for (var orderIndex in aCreators) { | |
switch (aCreators[orderIndex].creatorTypeID) { | |
case aPrimary: | |
aPrimaryFoundAt = orderIndex; | |
// If we find a primary, no need to continue looking | |
break loop; | |
case editorTypeID: | |
if (aEditorFoundAt === false) { | |
aEditorFoundAt = orderIndex; | |
} | |
break; | |
case contributorTypeID: | |
if (aContributorFoundAt === false) { | |
aContributorFoundAt = orderIndex; | |
} | |
break; | |
} | |
} | |
if (aPrimaryFoundAt !== false) { | |
var aFirstCreatorTypeID = aPrimary; | |
var aPos = aPrimaryFoundAt; | |
} | |
else if (aEditorFoundAt !== false) { | |
var aFirstCreatorTypeID = editorTypeID; | |
var aPos = aEditorFoundAt; | |
} | |
else { | |
var aFirstCreatorTypeID = contributorTypeID; | |
var aPos = aContributorFoundAt; | |
} | |
// Same for b | |
var bPrimaryFoundAt = false; | |
var bEditorFoundAt = false; | |
var bContributorFoundAt = false; | |
loop: | |
for (var orderIndex in bCreators) { | |
switch (bCreators[orderIndex].creatorTypeID) { | |
case bPrimary: | |
bPrimaryFoundAt = orderIndex; | |
break loop; | |
case 3: | |
if (bEditorFoundAt === false) { | |
bEditorFoundAt = orderIndex; | |
} | |
break; | |
case 2: | |
if (bContributorFoundAt === false) { | |
bContributorFoundAt = orderIndex; | |
} | |
break; | |
} | |
} | |
if (bPrimaryFoundAt !== false) { | |
var bFirstCreatorTypeID = bPrimary; | |
var bPos = bPrimaryFoundAt; | |
} | |
else if (bEditorFoundAt !== false) { | |
var bFirstCreatorTypeID = editorTypeID; | |
var bPos = bEditorFoundAt; | |
} | |
else { | |
var bFirstCreatorTypeID = contributorTypeID; | |
var bPos = bContributorFoundAt; | |
} | |
while (true) { | |
// Compare names | |
fieldA = Zotero.Items.getSortTitle(aCreators[aPos].ref.lastName); | |
fieldB = Zotero.Items.getSortTitle(bCreators[bPos].ref.lastName); | |
cmp = strcmp(fieldA, fieldB); | |
if (cmp) { | |
return cmp; | |
} | |
fieldA = Zotero.Items.getSortTitle(aCreators[aPos].ref.firstName); | |
fieldB = Zotero.Items.getSortTitle(bCreators[bPos].ref.firstName); | |
cmp = strcmp(fieldA, fieldB); | |
if (cmp) { | |
return cmp; | |
} | |
// If names match, find next creator of the relevant type | |
aPos++; | |
var aFound = false; | |
while (aPos < aNumCreators) { | |
// Don't die if there's no creator at an index | |
if (!aCreators[aPos]) { | |
Components.utils.reportError( | |
"Creator is missing at position " + aPos | |
+ " for item " + aRef.libraryID + "/" + aRef.key | |
); | |
return -1; | |
} | |
if (aCreators[aPos].creatorTypeID == aFirstCreatorTypeID) { | |
aFound = true; | |
break; | |
} | |
aPos++; | |
} | |
bPos++; | |
var bFound = false; | |
while (bPos < bNumCreators) { | |
// Don't die if there's no creator at an index | |
if (!bCreators[bPos]) { | |
Components.utils.reportError( | |
"Creator is missing at position " + bPos | |
+ " for item " + bRef.libraryID + "/" + bRef.key | |
); | |
return -1; | |
} | |
if (bCreators[bPos].creatorTypeID == bFirstCreatorTypeID) { | |
bFound = true; | |
break; | |
} | |
bPos++; | |
} | |
if (aFound && !bFound) { | |
return -1; | |
} | |
if (bFound && !aFound) { | |
return 1; | |
} | |
if (!aFound && !bFound) { | |
return 0; | |
} | |
} | |
} | |
function strcmp(a, b, collationSort) { | |
// Display rows with empty values last | |
if (a === '' && b !== '') return 1; | |
if (a !== '' && b === '') return -1; | |
return collation.compareString(1, a, b); | |
} | |
// Need to close all containers before sorting | |
if (!this.selection.selectEventsSuppressed) { | |
var unsuppress = this.selection.selectEventsSuppressed = true; | |
this._treebox.beginUpdateBatch(); | |
} | |
var savedSelection = this.saveSelection(); | |
var openItemIDs = this.saveOpenState(true); | |
// Single-row sort | |
if (itemID) { | |
let row = this._itemRowMap[itemID]; | |
for (let i=0, len=this._dataItems.length; i<len; i++) { | |
if (i === row) { | |
continue; | |
} | |
let cmp = rowSort.apply(this, | |
[this._dataItems[i], this._dataItems[row]].concat(sortFields)) * order; | |
// As soon as we find a value greater (or smaller if reverse sort), | |
// insert row at that position | |
if (cmp > 0) { | |
let rowItem = this._dataItems.splice(row, 1); | |
this._dataItems.splice(row < i ? i-1 : i, 0, rowItem[0]); | |
this._treebox.invalidate(); | |
break; | |
} | |
// If greater than last row, move to end | |
if (i == len-1) { | |
let rowItem = this._dataItems.splice(row, 1); | |
this._dataItems.splice(i, 0, rowItem[0]); | |
this._treebox.invalidate(); | |
} | |
} | |
} | |
// Full sort | |
else { | |
this._dataItems.sort(function (a, b) { | |
return rowSort.apply(this, [a, b].concat(sortFields)) * order; | |
}.bind(this)); | |
} | |
this._refreshHashMap(); | |
this.rememberOpenState(openItemIDs); | |
this.rememberSelection(savedSelection); | |
if (unsuppress) { | |
this.selection.selectEventsSuppressed = false; | |
this._treebox.endUpdateBatch(); | |
} | |
Zotero.debug("Sorted items list in " + (new Date - t) + " ms"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment