diff --git a/chrome/content/zotero/bindings/columnpicker.xml b/chrome/content/zotero/bindings/columnpicker.xml
deleted file mode 100644
index c406f22138..0000000000
--- a/chrome/content/zotero/bindings/columnpicker.xml
+++ /dev/null
@@ -1,146 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- return Components.interfaces.nsIAccessibleProvider.XULButton;
-
-
-
-
-
-
-
- 4) {
- aPopup.removeChild(aPopup.firstChild);
- }
- while (bPopup.childNodes.length > 0) {
- bPopup.removeChild(bPopup.firstChild);
- }
- var refChild = aPopup.firstChild;
- var refChild2 = bPopup.firstChild;
-
- var tree = this.parentNode.parentNode;
- for (var currCol = tree.columns.getFirstColumn(); currCol; currCol = currCol.getNext()) {
- var currElement = currCol.element;
-
- // Construct an entry for each column in the row, unless
- // it is not being shown.
- if ((!currElement.hasAttribute("ignoreincolumnpicker")) && (!currElement.hasAttribute("submenu"))) {
- var popupChild = document.createElement("menuitem");
- popupChild.setAttribute("type", "checkbox");
- var columnName = currElement.getAttribute("display") || currElement.getAttribute("label");
- popupChild.setAttribute("label", columnName);
- popupChild.setAttribute("colindex", currCol.index);
- if (currElement.getAttribute("hidden") != "true") {
- popupChild.setAttribute("checked", "true");
- }
- if (currCol.primary) {
- popupChild.setAttribute("disabled", "true");
- }
- aPopup.insertBefore(popupChild, refChild);
- }
-
- //Idem for the submenu
- if ((!currElement.hasAttribute("ignoreincolumnpicker")) && (currElement.hasAttribute("submenu"))) {
- var popupChild = document.createElement("menuitem");
- popupChild.setAttribute("type", "checkbox");
- var columnName = currElement.getAttribute("display") || currElement.getAttribute("label");
- popupChild.setAttribute("label", columnName);
- popupChild.setAttribute("colindex", currCol.index);
- if (currElement.getAttribute("hidden") != "true") {
- popupChild.setAttribute("checked", "true");
- }
- bPopup.insertBefore(popupChild, refChild2);
- }
- }
- ]]>
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/chrome/content/zotero/integration/addCitationDialog.xul b/chrome/content/zotero/integration/addCitationDialog.xul
index 1888eec78d..7e4bab6d0c 100644
--- a/chrome/content/zotero/integration/addCitationDialog.xul
+++ b/chrome/content/zotero/integration/addCitationDialog.xul
@@ -90,7 +90,7 @@
flex="1" persist="width ordinal hidden sortActive sortDirection"/>
diff --git a/chrome/content/zotero/selectItemsDialog.xul b/chrome/content/zotero/selectItemsDialog.xul
index 5967999cb6..e356ce528c 100644
--- a/chrome/content/zotero/selectItemsDialog.xul
+++ b/chrome/content/zotero/selectItemsDialog.xul
@@ -82,7 +82,7 @@
flex="1" persist="width ordinal hidden sortActive sortDirection"/>
diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js
index 8ad2453fa4..ceb4e7d16b 100644
--- a/chrome/content/zotero/xpcom/itemTreeView.js
+++ b/chrome/content/zotero/xpcom/itemTreeView.js
@@ -938,8 +938,7 @@ Zotero.ItemTreeView.prototype.getCellText = function(row, column)
if (column.id === "zotero-items-column-hasAttachment") {
return;
}
- else if(column.id == "zotero-items-column-type")
- {
+ else if (column.id == "zotero-items-column-itemType") {
val = Zotero.ItemTypes.getLocalizedString(obj.ref.itemTypeID);
}
// Year column is just date field truncated
@@ -1295,14 +1294,14 @@ Zotero.ItemTreeView.prototype.cycleHeader = function(column)
*/
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;
}
- else {
- this._needsSort = false;
- }
+ this._needsSort = false;
// Single child item sort -- just toggle parent open and closed
if (itemID && this._itemRowMap[itemID] &&
@@ -1313,36 +1312,19 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
return;
}
- var columnField = this.getSortField();
- var order = this.getSortDirection() == 'descending';
+ 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');
- // Year is really the date field truncated
- var originalColumnField = columnField;
- if (columnField == 'year') {
- columnField = 'date';
- }
-
- // The visible fields affect the secondary sorting
- var visibleFields = {};
- this.getVisibleFields().forEach(function (val) {
- visibleFields[val] = true;
- });
-
- // Some fields (e.g. dates) need to be retrieved unformatted for sorting
- switch (columnField) {
- case 'date':
- var unformatted = true;
- break;
-
- default:
- var unformatted = false;
- }
+ 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 (columnField) {
+ switch (primaryField) {
case 'title':
emptyFirst.title = true;
break;
@@ -1357,36 +1339,22 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
// 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)
- var getField;
- switch (originalColumnField) {
- case 'title':
- getField = function (row) {
- var field;
- var type = row.ref.itemTypeID;
- switch (type) {
- case 8: // letter
- case 10: // interview
- case 17: // case
- field = row.ref.getDisplayTitle();
- break;
-
- default:
- field = row.getField(columnField, unformatted);
- }
- // Ignore some leading and trailing characters when sorting
- return Zotero.Items.getSortTitle(field);
- };
- break;
+ function getField(field, row) {
+ var item = row.ref;
- case 'hasAttachment':
- getField = function (row) {
- if (row.ref.isAttachment()) {
- var state = row.ref.fileExists() ? 1 : -1;
+ switch (field) {
+ case 'title':
+ return Zotero.Items.getSortTitle(item.getDisplayTitle());
+
+ case 'hasAttachment':
+ if (item.isAttachment()) {
+ var state = item.fileExists() ? 1 : -1;
}
- else if (row.ref.isRegularItem()) {
- var state = row.ref.getBestAttachmentState();
+ else if (item.isRegularItem()) {
+ var state = item.getBestAttachmentState();
}
else {
return 0;
@@ -1396,20 +1364,23 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
state = 2;
}
return state * -1;
- };
- break;
-
- case 'numNotes':
- getField = function (row) {
- // Sort descending by default
- order = !order;
+
+ case 'numNotes':
return row.numNotes(false, true) || 0;
- };
- break;
-
- case 'year':
- getField = function (row) {
- var val = row.getField(columnField, unformatted);
+
+ // Use unformatted part of date strings (YYYY-MM-DD) for sorting
+ case 'date':
+ var val = row.ref.getField('date', 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);
if (val) {
val = val.substr(0, 4);
if (val == '0000') {
@@ -1417,127 +1388,58 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
}
}
return val;
- };
- break;
-
- default:
- getField = function (row) row.getField(columnField, unformatted);
+
+ default:
+ return row.ref.getField(field, false, true);
+ }
}
var includeTrashed = this._itemGroup.isTrash();
- var me = this,
- isEmptyFirst = emptyFirst[columnField];
- function rowSort(a, b) {
- var cmp,
- aItemID = a.id,
- bItemID = b.id,
- fieldA = cache[aItemID],
- fieldB = cache[bItemID];
+ function fieldCompare(a, b, sortField) {
+ var aItemID = a.id;
+ var bItemID = b.id;
+ var fieldA = cache[sortField][aItemID];
+ var fieldB = cache[sortField][bItemID];
- switch (columnField) {
- case 'date':
- fieldA = getField(a).substr(0, 10);
- fieldB = getField(b).substr(0, 10);
-
- cmp = strcmp(fieldA, fieldB);
- if (cmp !== 0) {
- return cmp;
- }
- break;
-
+ switch (sortField) {
case 'firstCreator':
- cmp = creatorSort(a, b);
- if (cmp !== 0) {
- return cmp;
- }
- break;
+ return creatorSort(a, b);
- case 'type':
+ case 'itemType':
var typeA = Zotero.ItemTypes.getLocalizedString(a.ref.itemTypeID);
var typeB = Zotero.ItemTypes.getLocalizedString(b.ref.itemTypeID);
-
- cmp = (typeA > typeB) ? -1 : (typeA < typeB) ? 1 : 0;
- if (cmp !== 0) {
- return cmp;
- }
- break;
+ return (typeA > typeB) ? 1 : (typeA < typeB) ? -1 : 0;
default:
if (fieldA === undefined) {
- cache[aItemID] = fieldA = getField(a);
+ cache[sortField][aItemID] = fieldA = getField(sortField, a);
}
if (fieldB === undefined) {
- cache[bItemID] = fieldB = getField(b);
+ cache[sortField][bItemID] = fieldB = getField(sortField, b);
}
// Display rows with empty values last
- if (!isEmptyFirst) {
- if(fieldA === '' && fieldB !== '') return -1;
- if(fieldA !== '' && fieldB === '') return 1;
+ if (!emptyFirst[sortField]) {
+ if(fieldA === '' && fieldB !== '') return 1;
+ if(fieldA !== '' && fieldB === '') return -1;
}
- cmp = collation.compareString(1, fieldB, fieldA);
- if (cmp !== 0) {
- return cmp;
- }
+ return collation.compareString(1, fieldA, fieldB);
}
-
- if (columnField !== 'firstCreator') {
- cmp = creatorSort(a, b);
+ }
+
+ 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;
}
}
-
- if (columnField !== 'date') {
- // If year is visible and not date, don't use full date
- if (visibleFields.year && !visibleFields.date) {
- fieldA = a.getField('date', true).substr(0, 4);
- if (fieldA == '0000') {
- fieldA = "";
- }
- fieldB = b.getField('date', true).substr(0, 4);
- if (fieldB == '0000') {
- fieldB = "";
- }
-
- cmp = strcmp(fieldA, fieldB);
- if (cmp !== 0) {
- return cmp;
- }
- }
- // Otherwise use full date, even if Date column is hidden
- else {
- fieldA = a.getField('date', true).substr(0, 10);
- fieldB = b.getField('date', true).substr(0, 10);
-
- cmp = strcmp(fieldA, fieldB);
- if (cmp !== 0) {
- return cmp;
- }
- }
- }
-
- if (columnField !== 'title') {
- fieldA = a.getField('title', true);
- fieldB = b.getField('title', true);
-
- if (!emptyFirst.title) {
- if (fieldA === '' && fieldB !== '') return -1;
- if (fieldA !== '' && fieldB === '') return 1;
- }
-
- cmp = collation.compareString(1, fieldB, fieldA);
- if (cmp !== 0) {
- return cmp;
- }
- }
-
- fieldA = a.getField('dateAdded');
- fieldB = b.getField('dateAdded');
- return (fieldA > fieldB) ? -1 : (fieldA < fieldB) ? 1 : 0;
+ return 0;
}
var firstCreatorSortCache = {};
@@ -1565,8 +1467,8 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
return 0;
}
- var cmp = strcmp(fieldA, fieldB, true);
- if (cmp !== 0) {
+ var cmp = strcmp(fieldA, fieldB);
+ if (cmp !== 0 || sortCreatorAsString) {
return cmp;
}
@@ -1663,14 +1565,14 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
// Compare names
fieldA = Zotero.Items.getSortTitle(aCreators[aPos].ref.lastName);
fieldB = Zotero.Items.getSortTitle(bCreators[bPos].ref.lastName);
- cmp = strcmp(fieldA, fieldB, true);
+ 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, true);
+ cmp = strcmp(fieldA, fieldB);
if (cmp) {
return cmp;
}
@@ -1728,14 +1630,10 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
function strcmp(a, b, collationSort) {
// Display rows with empty values last
- if(a === '' && b !== '') return -1;
- if(a !== '' && b === '') return 1;
+ if (a === '' && b !== '') return 1;
+ if (a !== '' && b === '') return -1;
- if (collationSort) {
- return collation.compareString(1, b, a);
- }
-
- return (a > b) ? -1 : (a < b) ? 1 : 0;
+ return collation.compareString(1, a, b);
}
// Need to close all containers before sorting
@@ -1748,23 +1646,19 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
// Single-row sort
if (itemID) {
- var row = this._itemRowMap[itemID];
- for (var i=0, len=this._dataItems.length; i 0) {
+ let rowItem = this._dataItems.splice(row, 1);
this._dataItems.splice(row < i ? i-1 : i, 0, rowItem[0]);
this._treebox.invalidate();
break;
@@ -1772,7 +1666,7 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
// If greater than last row, move to end
if (i == len-1) {
- var rowItem = this._dataItems.splice(row, 1);
+ let rowItem = this._dataItems.splice(row, 1);
this._dataItems.splice(i, 0, rowItem[0]);
this._treebox.invalidate();
}
@@ -1780,8 +1674,9 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
}
// Full sort
else {
- this._dataItems.sort(rowSort);
- if(!order) this._dataItems.reverse();
+ this._dataItems.sort(function (a, b) {
+ return rowSort.apply(this, [a, b].concat(sortFields)) * order;
+ }.bind(this));
}
this._refreshHashMap();
@@ -1793,6 +1688,8 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
this.selection.selectEventsSuppressed = false;
this._treebox.endUpdateBatch();
}
+
+ Zotero.debug("Sorted items list in " + (new Date - t) + " ms");
}
////////////////////////////////////////////////////////////////////////////////
@@ -2374,6 +2271,39 @@ Zotero.ItemTreeView.prototype.getSortField = function() {
}
+Zotero.ItemTreeView.prototype.getSortFields = function () {
+ var fields = [this.getSortField()];
+ var secondaryField = this.getSecondarySortField();
+ if (secondaryField) {
+ fields.push(secondaryField);
+ }
+ try {
+ var fallbackFields = Zotero.Prefs.get('fallbackSort')
+ .split(',')
+ .map((x) => x.trim())
+ .filter((x) => x !== '');
+ }
+ catch (e) {
+ Zotero.debug(e, 1);
+ Components.utils.reportError(e);
+ // This should match the default value for the fallbackSort pref
+ var fallbackFields = ['firstCreator', 'date', 'title', 'dateAdded'];
+ }
+ fields = Zotero.Utilities.arrayUnique(fields.concat(fallbackFields));
+
+ // If date appears after year, remove it, unless it's the explicit secondary sort
+ var yearPos = fields.indexOf('year');
+ if (yearPos != -1) {
+ let datePos = fields.indexOf('date');
+ if (datePos > yearPos && secondaryField != 'date') {
+ fields.splice(datePos, 1);
+ }
+ }
+
+ return fields;
+}
+
+
/*
* Returns 'ascending' or 'descending'
*/
@@ -2386,6 +2316,197 @@ Zotero.ItemTreeView.prototype.getSortDirection = function() {
}
+Zotero.ItemTreeView.prototype.getSecondarySortField = function () {
+ var primaryField = this.getSortField();
+ var secondaryField = Zotero.Prefs.get('secondarySort.' + primaryField);
+ if (!secondaryField || secondaryField == primaryField) {
+ return false;
+ }
+ return secondaryField;
+}
+
+
+Zotero.ItemTreeView.prototype.setSecondarySortField = function (secondaryField) {
+ var primaryField = this.getSortField();
+ var currentSecondaryField = this.getSecondarySortField();
+ var sortFields = this.getSortFields();
+
+ if (primaryField == secondaryField) {
+ return false;
+ }
+
+ if (currentSecondaryField) {
+ // If same as the current explicit secondary sort, ignore
+ if (currentSecondaryField == secondaryField) {
+ return false;
+ }
+
+ // If not, but same as first implicit sort, remove current explicit sort
+ if (sortFields[2] && sortFields[2] == secondaryField) {
+ Zotero.Prefs.clear('secondarySort.' + primaryField);
+ return true;
+ }
+ }
+ // If same as current implicit secondary sort, ignore
+ else if (sortFields[1] && sortFields[1] == secondaryField) {
+ return false;
+ }
+
+ Zotero.Prefs.set('secondarySort.' + primaryField, secondaryField);
+ return true;
+}
+
+
+/**
+ * Build the More Columns and Secondary Sort submenus while the popup is opening
+ */
+Zotero.ItemTreeView.prototype.onColumnPickerShowing = function (event) {
+ var menupopup = event.originalTarget;
+
+ var ns = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+ var prefix = 'zotero-column-header-';
+ var doc = menupopup.ownerDocument;
+
+ var anonid = menupopup.getAttribute('anonid');
+ if (anonid.indexOf(prefix) == 0) {
+ return;
+ }
+
+ var lastChild = menupopup.lastChild;
+
+ // More Columns menu
+ try {
+ let id = prefix + 'more-menu';
+
+ let moreMenu = doc.createElementNS(ns, 'menu');
+ moreMenu.setAttribute('label', Zotero.getString('pane.items.columnChooser.moreColumns'));
+ moreMenu.setAttribute('anonid', id);
+
+ let moreMenuPopup = doc.createElementNS(ns, 'menupopup');
+ moreMenuPopup.setAttribute('anonid', id + '-popup');
+
+ let treecols = menupopup.parentNode.parentNode;
+ let subs = [x.getAttribute('label') for (x of treecols.getElementsByAttribute('submenu', 'true'))];
+
+ var moreItems = [];
+
+ for (let i=0; i
@@ -385,21 +385,25 @@
diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd
index cd18041e4d..2db5d59c56 100644
--- a/chrome/locale/en-US/zotero/zotero.dtd
+++ b/chrome/locale/en-US/zotero/zotero.dtd
@@ -45,7 +45,7 @@
-
+
diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties
index 8efb3a5a90..b05da82a36 100644
--- a/chrome/locale/en-US/zotero/zotero.properties
+++ b/chrome/locale/en-US/zotero/zotero.properties
@@ -192,6 +192,8 @@ tagColorChooser.numberKeyInstructions = You can add this tag to selecte
tagColorChooser.maxTags = Up to %S tags in each library can have colors assigned.
pane.items.loading = Loading items list…
+pane.items.columnChooser.moreColumns = More Columns
+pane.items.columnChooser.secondarySort = Secondary Sort (%S)
pane.items.attach.link.uri.title = Attach Link to URI
pane.items.attach.link.uri = Enter a URI:
pane.items.trash.title = Move to Trash
diff --git a/chrome/skin/default/zotero/zotero.css b/chrome/skin/default/zotero/zotero.css
index b833c493ce..59df981c03 100644
--- a/chrome/skin/default/zotero/zotero.css
+++ b/chrome/skin/default/zotero/zotero.css
@@ -174,11 +174,6 @@ zoteroguidancepanel
-moz-binding: url('chrome://zotero/content/bindings/guidancepanel.xml#guidancepanel');
}
-#zotero-items-columns-header > treecolpicker
- {
- -moz-binding: url('chrome://zotero/content/bindings/columnpicker.xml#extended-columnpicker');
-}
-
zoterofilesyncstatus {
-moz-binding: url('chrome://zotero/content/bindings/filesyncstatus.xml#file-sync-status');
}
diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js
index 4054fb6f8a..c7d94842b2 100644
--- a/defaults/preferences/zotero.js
+++ b/defaults/preferences/zotero.js
@@ -61,6 +61,9 @@ pref("extensions.zotero.lastViewedFolder", 'L');
pref("extensions.zotero.lastLongTagMode", 0);
pref("extensions.zotero.lastLongTagDelimiter", ";");
+pref("extensions.zotero.fallbackSort", 'firstCreator,date,title,dateAdded');
+pref("extensions.zotero.sortCreatorAsString", false);
+
//Tag Cloud
pref("extensions.zotero.tagCloud", false);