Skip to content

Instantly share code, notes, and snippets.

@adomasven
Created August 3, 2016 10:19
Show Gist options
  • Save adomasven/4bca052b9ceb80c1bc3d18c5f36fae2e to your computer and use it in GitHub Desktop.
Save adomasven/4bca052b9ceb80c1bc3d18c5f36fae2e to your computer and use it in GitHub Desktop.
Reverse Zotero 4.0 creator sort order
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