Don't reselect items unnecessarily

Store and check the last selected items in ZoteroPane.itemSelected() to
see if it's necessary to refresh the item pane. This prevents loss of
textbox focus if another write occurs while editing a field.

Also optimize row adding/removing in itemTreeView.js
This commit is contained in:
Dan Stillman 2015-05-07 15:05:37 -04:00
parent afface4fba
commit 47f3c1efe6
3 changed files with 134 additions and 65 deletions

View File

@ -352,7 +352,7 @@ Zotero.ItemTreeView.prototype.refresh = Zotero.serial(Zotero.Promise.coroutine(f
yield item.loadItemData();
yield item.loadCollections();
this._addRow(
this._addRowToArray(
newRows,
new Zotero.ItemTreeRow(item, 0, false),
added + 1
@ -366,7 +366,7 @@ Zotero.ItemTreeView.prototype.refresh = Zotero.serial(Zotero.Promise.coroutine(f
for (let id in newSearchParentIDs) {
if (!newSearchItemIDs[id]) {
let item = yield Zotero.Items.getAsync(id);
this._addRow(
this._addRowToArray(
newRows,
new Zotero.ItemTreeRow(item, 0, false),
added + 1
@ -587,7 +587,6 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
if(row != null)
{
this._removeRow(row-i);
this._treebox.rowCountChanged(row-i,-1);
}
}
@ -619,15 +618,11 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
{
var items = yield Zotero.Items.getAsync(ids);
for each(var item in items) {
for (let i = 0; i < items.length; i++) {
let item = items[i];
let id = item.id;
// Make sure row map is up to date
// if we made changes in a previous loop
if (madeChanges) {
this._refreshItemRowMap();
}
var row = this._itemRowMap[id];
let row = this._itemRowMap[id];
// Deleted items get a modify that we have to ignore when
// not viewing the trash
@ -636,35 +631,25 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
}
// Item already exists in this view
if( row != null)
{
var parentItemID = this.getRow(row).ref.parentItemID;
var parentIndex = this.getParentIndex(row);
if (row !== undefined) {
let parentItemID = this.getRow(row).ref.parentItemID;
let parentIndex = this.getParentIndex(row);
// Top-level item
// Top-level items just have to be resorted
if (this.isContainer(row)) {
//yield this.toggleOpenState(row);
//yield this.toggleOpenState(row);
sort = id;
}
// If item moved from top-level to under another item, remove the old row.
// The container refresh above takes care of adding the new row.
else if (!this.isContainer(row) && parentIndex == -1 && parentItemID) {
else if (parentIndex == -1 && parentItemID) {
this._removeRow(row);
this._treebox.rowCountChanged(row, -1)
}
// If moved from under another item to top level, remove old row and add new one
else if (!this.isContainer(row) && parentIndex != -1 && !parentItemID) {
else if (parentIndex != -1 && !parentItemID) {
this._removeRow(row);
this._treebox.rowCountChanged(row, -1)
this._addRow(
this._rows,
new Zotero.ItemTreeRow(item, 0, false),
this.rowCount
);
this.rowCount++;
this._treebox.rowCountChanged(this.rowCount - 1, 1);
let beforeRow = this.rowCount;
this._addRow(new Zotero.ItemTreeRow(item, 0, false), beforeRow);
sort = id;
}
// If not moved from under one item to another, just resort the row,
@ -672,13 +657,14 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
else if (!(parentItemID && parentIndex != -1 && this._itemRowMap[parentItemID] != parentIndex)) {
sort = id;
}
madeChanges = true;
}
// Otherwise, for a top-level item in a root view or a collection
// Otherwise, for a top-level item in a library root or a collection
// containing the item, the item has to be added
else if (item.isTopLevelItem()) {
// Root view
let add = (collectionTreeRow.isLibrary() || collectionTreeRow.isGroup())
let add = collectionTreeRow.isLibrary(true)
&& collectionTreeRow.ref.libraryID == item.libraryID;
// Collection containing item
if (!add && collectionTreeRow.isCollection()) {
@ -687,15 +673,10 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
}
if (add) {
//most likely, the note or attachment's parent was removed.
this._addRow(
this._rows,
new Zotero.ItemTreeRow(item, 0, false),
this.rowCount
);
this.rowCount++;
this._treebox.rowCountChanged(this.rowCount-1,1);
let beforeRow = this.rowCount;
this._addRow(new Zotero.ItemTreeRow(item, 0, false), beforeRow);
madeChanges = true;
sort = true;
sort = id;
}
}
}
@ -756,13 +737,8 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
&& this._itemRowMap[item.id] == null
// Regular item or standalone note/attachment
&& item.isTopLevelItem()) {
this._addRow(
this._rows,
new Zotero.ItemTreeRow(item, 0, false),
this.rowCount
);
this.rowCount++;
this._treebox.rowCountChanged(this.rowCount-1,1);
let beforeRow = this.rowCount;
this._addRow(new Zotero.ItemTreeRow(item, 0, false), beforeRow);
madeChanges = true;
}
}
@ -1234,13 +1210,10 @@ Zotero.ItemTreeView.prototype.toggleOpenState = Zotero.Promise.coroutine(functio
for (let i = 0; i < newRows.length; i++) {
count++;
this._addRow(
this._rows,
new Zotero.ItemTreeRow(newRows[i], level + 1, false),
row + i + 1
);
}
this.rowCount += count;
this._treebox.rowCountChanged(row + 1, count);
}
this._rows[row].isOpen = true;
@ -1270,11 +1243,11 @@ Zotero.ItemTreeView.prototype._closeContainer = function (row, skipItemMapRefres
// Remove child rows
while ((row + 1 < this._rows.length) && (this.getLevel(row + 1) > level)) {
this._removeRow(row+1);
// Skip the map update here and just refresh the whole map below,
// since we might be removing multiple rows
this._removeRow(row + 1, true);
count--;
}
// Mark as removed from the end of the row's children
this._treebox.rowCountChanged(row + 1 + Math.abs(count), count);
this._rows[row].isOpen = false;
@ -1895,26 +1868,61 @@ Zotero.ItemTreeView.prototype.setFilter = Zotero.Promise.coroutine(function* (ty
/**
* Add a tree row into a given array
* Add a tree row to the main array, update the row count, tell the treebox that the row count
* changed, and update the row map
*
* @param {Array} newRows - Array to operate on
* @param {Zotero.ItemTreeRow} itemTreeRow
* @param {Number} beforeRow - Row index to insert new row before
*/
Zotero.ItemTreeView.prototype._addRow = function (newRows, itemTreeRow, beforeRow) {
newRows.splice(beforeRow, 0, itemTreeRow);
Zotero.ItemTreeView.prototype._addRow = function (itemTreeRow, beforeRow) {
this._addRowToArray(this._rows, itemTreeRow, beforeRow);
this.rowCount++;
this._treebox.rowCountChanged(beforeRow, 1);
// Increment all rows in map at or above insertion point
for (let i in this._itemRowMap) {
if (this._itemRowMap[j] >= beforeRow) {
this._itemRowMap[j]++
}
}
// Add new row to map
this._itemRowMap[itemTreeRow.id] = beforeRow;
}
/**
* Remove a row from the main rows array
* Add a tree row into a given array
*
* @param {Array} array - Array to operate on
* @param {Zotero.ItemTreeRow} itemTreeRow
* @param {Number} beforeRow - Row index to insert new row before
*/
Zotero.ItemTreeView.prototype._removeRow = function (row) {
Zotero.ItemTreeView.prototype._addRowToArray = function (array, itemTreeRow, beforeRow) {
array.splice(beforeRow, 0, itemTreeRow);
}
/**
* Remove a row from the main array, decrement the row count, tell the treebox that the row
* count changed, delete the row from the map, and optionally update all rows above it in the map
*/
Zotero.ItemTreeView.prototype._removeRow = function (row, skipItemMapUpdate) {
var id = this._rows[row].id;
this._rows.splice(row, 1);
this.rowCount--;
if (this.selection.isSelected(row)) {
this.selection.toggleSelect(row);
this._treebox.rowCountChanged(row + 1, -1);
delete this._itemRowMap[id];
if (!skipItemMapUpdate) {
for (let i in this._itemRowMap) {
if (this._itemRowMap[i] > row) {
this._itemRowMap[i]--;
}
}
}
/*if (this.selection.isSelected(row)) {
this.selection.toggleSelect(row);
}*/
}
/*

View File

@ -34,6 +34,7 @@ var ZoteroPane = new function()
this.itemsView = false;
this._listeners = {};
this.__defineGetter__('loaded', function () _loaded);
var _lastSelectedItems = [];
//Privileged methods
this.init = init;
@ -1229,11 +1230,23 @@ var ZoteroPane = new function()
*/
this.itemSelected = function (event) {
return Zotero.spawn(function* () {
var selectedItems = this.itemsView.getSelectedItems();
// Check if selection has actually changed. The onselect event that calls this
// can be called in various situations where the selection didn't actually change,
// such as whenever selectEventsSuppressed is set to false.
var ids = selectedItems.map(item => item.id);
ids.sort();
if (Zotero.Utilities.arrayEquals(_lastSelectedItems, ids)) {
return false;
}
_lastSelectedItems = ids;
yield Zotero.DB.waitForTransaction();
if (!this.itemsView) {
Zotero.debug("Items view not available in itemSelected", 2);
return;
return false;
}
// Display restore button if items selected in Trash
@ -1259,7 +1272,7 @@ var ZoteroPane = new function()
// Single item selected
if (this.itemsView.selection.count == 1 && this.itemsView.selection.currentIndex != -1)
{
var item = this.itemsView.getSelectedItems()[0];
var item = selectedItems[0];
if (item.isNote()) {
var noteEditor = document.getElementById('zotero-note-editor');
@ -1362,7 +1375,7 @@ var ZoteroPane = new function()
var displayNumItemsOnTypeError = count > 5 && count == this.itemsView.rowCount;
// Initialize the merge pane with the selected items
Zotero_Duplicates_Pane.setItems(this.getSelectedItems(), displayNumItemsOnTypeError);
Zotero_Duplicates_Pane.setItems(selectedItems, displayNumItemsOnTypeError);
}
else {
var msg = Zotero.getString('pane.item.duplicates.selectToMerge');
@ -1394,12 +1407,15 @@ var ZoteroPane = new function()
this.setItemPaneMessage(msg);
}
}
return true;
}, this)
.then(function () {
.then(function (result) {
// See note in itemTreeView.js::selectItem()
if (this.itemsView && this.itemsView._itemSelectedPromiseResolver) {
this.itemsView._itemSelectedPromiseResolver.resolve();
}
return result;
}.bind(this))
.catch(function (e) {
Zotero.debug(e, 1);

View File

@ -31,10 +31,20 @@ describe("Zotero.ItemTreeView", function() {
})
describe("#notify()", function () {
beforeEach(function () {
sinon.spy(win.ZoteroPane, "itemSelected");
})
afterEach(function () {
win.ZoteroPane.itemSelected.restore();
})
it("should select a new item", function* () {
itemsView.selection.clearSelection();
assert.lengthOf(itemsView.getSelectedItems(), 0);
assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
// Create item
var item = new Zotero.Item('book');
var id = yield item.save();
@ -43,6 +53,10 @@ describe("Zotero.ItemTreeView", function() {
var selected = itemsView.getSelectedItems();
assert.lengthOf(selected, 1);
assert.equal(selected[0].id, id);
// Item should have been selected once
assert.equal(win.ZoteroPane.itemSelected.callCount, 2);
assert.ok(win.ZoteroPane.itemSelected.returnValues[1].value());
});
it("shouldn't select a new item if skipNotifier is passed", function* () {
@ -52,12 +66,18 @@ describe("Zotero.ItemTreeView", function() {
assert.lengthOf(selected, 1);
assert.equal(selected[0], existingItemID);
// Reset call count on spy
win.ZoteroPane.itemSelected.reset();
// Create item with skipNotifier flag
var item = new Zotero.Item('book');
var id = yield item.save({
skipNotifier: true
});
// No select events should have occurred
assert.equal(win.ZoteroPane.itemSelected.callCount, 0);
// Existing item should still be selected
selected = itemsView.getSelectedItems(true);
assert.lengthOf(selected, 1);
@ -71,12 +91,20 @@ describe("Zotero.ItemTreeView", function() {
assert.lengthOf(selected, 1);
assert.equal(selected[0], existingItemID);
// Reset call count on spy
win.ZoteroPane.itemSelected.reset();
// Create item with skipSelect flag
var item = new Zotero.Item('book');
var id = yield item.save({
skipSelect: true
});
// itemSelected should have been called once (from 'selectEventsSuppressed = false'
// in notify()) as a no-op
assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
assert.isFalse(win.ZoteroPane.itemSelected.returnValues[0].value());
// Existing item should still be selected
selected = itemsView.getSelectedItems(true);
assert.lengthOf(selected, 1);
@ -91,15 +119,23 @@ describe("Zotero.ItemTreeView", function() {
itemsView.selection.clearSelection();
assert.lengthOf(itemsView.getSelectedItems(), 0);
// Reset call count on spy
win.ZoteroPane.itemSelected.reset();
// Modify item
item.setField('title', 'no select on modify');
yield item.save();
// itemSelected should have been called once (from 'selectEventsSuppressed = false'
// in notify()) as a no-op
assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
assert.isFalse(win.ZoteroPane.itemSelected.returnValues[0].value());
// Modified item should not be selected
assert.lengthOf(itemsView.getSelectedItems(), 0);
});
it("should reselect a selected modified item", function* () {
it("should maintain selection on a selected modified item", function* () {
// Create item
var item = new Zotero.Item('book');
var id = yield item.save();
@ -110,9 +146,18 @@ describe("Zotero.ItemTreeView", function() {
assert.lengthOf(selected, 1);
assert.equal(selected[0], id);
item.setField('title', 'reselect on modify');
// Reset call count on spy
win.ZoteroPane.itemSelected.reset();
// Modify item
item.setField('title', 'maintain selection on modify');
yield item.save();
// itemSelected should have been called once (from 'selectEventsSuppressed = false'
// in notify()) as a no-op
assert.equal(win.ZoteroPane.itemSelected.callCount, 1);
assert.isFalse(win.ZoteroPane.itemSelected.returnValues[0].value());
// Modified item should still be selected
selected = itemsView.getSelectedItems(true);
assert.lengthOf(selected, 1);