www

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

collectionTreeView.js (63577B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 ////////////////////////////////////////////////////////////////////////////////
     27 ///
     28 ///  CollectionTreeView
     29 ///    -- handles the link between an individual tree and the data layer
     30 ///    -- displays only collections, in a hierarchy (no items)
     31 ///
     32 ////////////////////////////////////////////////////////////////////////////////
     33 
     34 /*
     35  *  Constructor for the CollectionTreeView object
     36  */
     37 Zotero.CollectionTreeView = function()
     38 {
     39 	Zotero.LibraryTreeView.apply(this);
     40 	
     41 	this.itemTreeView = null;
     42 	this.itemToSelect = null;
     43 	this.hideSources = [];
     44 	
     45 	this._highlightedRows = {};
     46 	this._unregisterID = Zotero.Notifier.registerObserver(
     47 		this,
     48 		[
     49 			'collection',
     50 			'search',
     51 			'feed',
     52 			'share',
     53 			'group',
     54 			'feedItem',
     55 			'trash',
     56 			'bucket'
     57 		],
     58 		'collectionTreeView',
     59 		25
     60 	);
     61 	this._containerState = {};
     62 	this._virtualCollectionLibraries = {};
     63 	this._trashNotEmpty = {};
     64 }
     65 
     66 Zotero.CollectionTreeView.prototype = Object.create(Zotero.LibraryTreeView.prototype);
     67 Zotero.CollectionTreeView.prototype.type = 'collection';
     68 
     69 Object.defineProperty(Zotero.CollectionTreeView.prototype, "selectedTreeRow", {
     70 	get: function () {
     71 		if (!this.selection || !this.selection.count) {
     72 			return false;
     73 		}
     74 		return this.getRow(this.selection.currentIndex);
     75 	}
     76 });
     77 
     78 
     79 Object.defineProperty(Zotero.CollectionTreeView.prototype, 'window', {
     80 	get: function () {
     81 		return this._ownerDocument.defaultView;
     82 	},
     83 	enumerable: true
     84 });
     85 
     86 
     87 /*
     88  *  Called by the tree itself
     89  */
     90 Zotero.CollectionTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (treebox)
     91 {
     92 	try {
     93 		if (this._treebox || !treebox) {
     94 			return;
     95 		}
     96 		this._treebox = treebox;
     97 		
     98 		if (!this._ownerDocument) {
     99 			try {
    100 				this._ownerDocument = treebox.treeBody.ownerDocument;
    101 			}
    102 			catch (e) {}
    103 		}
    104 		
    105 		// Add a keypress listener for expand/collapse
    106 		var tree = this._treebox.treeBody.parentNode;
    107 		tree.addEventListener('keypress', function(event) {
    108 			if (tree.editingRow != -1) return; // In-line editing active
    109 			
    110 			var libraryID = this.getSelectedLibraryID();
    111 			if (!libraryID) return;
    112 			
    113 			var key = String.fromCharCode(event.which);
    114 			if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
    115 				this.expandLibrary(libraryID, true);
    116 			}
    117 			else if (key == '-' && !(event.shiftKey || event.ctrlKey ||
    118 					event.altKey || event.metaKey)) {
    119 				this.collapseLibrary(libraryID);
    120 			}
    121 		}.bind(this), false);
    122 		
    123 		yield this.refresh();
    124 		if (!this._treebox.columns) {
    125 			return;
    126 		}
    127 		this.selection.currentColumn = this._treebox.columns.getFirstColumn();
    128 		
    129 		var lastViewedID = Zotero.Prefs.get('lastViewedFolder');
    130 		if (lastViewedID) {
    131 			var selected = yield this.selectByID(lastViewedID);
    132 		}
    133 		if (!selected) {
    134 			this.selection.select(0);
    135 		}
    136 		this.selection.selectEventsSuppressed = false;
    137 		
    138 		yield this.runListeners('load');
    139 		this._initialized = true;
    140 	}
    141 	catch (e) {
    142 		Zotero.debug(e, 1);
    143 		Components.utils.reportError(e);
    144 		if (this.onError) {
    145 			this.onError(e);
    146 		}
    147 		throw e;
    148 	}
    149 });
    150 
    151 
    152 /**
    153  *  Rebuild the tree from the data access methods and clear the selection
    154  *
    155  *  Calling code must invalidate the tree, restore the selection, and unsuppress selection events
    156  */
    157 Zotero.CollectionTreeView.prototype.refresh = Zotero.Promise.coroutine(function* ()
    158 {
    159 	Zotero.debug("Refreshing collections pane");
    160 	
    161 	// Record open states before refreshing
    162 	if (this._rows) {
    163 		for (var i=0, len=this._rows.length; i<len; i++) {
    164 			var treeRow = this._rows[i];
    165 			if (treeRow.ref && treeRow.ref.id == 'commons-header') {
    166 				var commonsExpand = this.isContainerOpen(i);
    167 			}
    168 		}
    169 	}
    170 	
    171 	try {
    172 		this._containerState = JSON.parse(Zotero.Prefs.get("sourceList.persist"));
    173 	}
    174 	catch (e) {
    175 		this._containerState = {};
    176 	}
    177 	
    178 	var userLibraryID = Zotero.Libraries.userLibraryID;
    179 	
    180 	if (this.hideSources.indexOf('duplicates') == -1) {
    181 		this._virtualCollectionLibraries.duplicates =
    182 			Zotero.Utilities.Internal.getVirtualCollectionState('duplicates')
    183 	}
    184 	this._virtualCollectionLibraries.unfiled =
    185 			Zotero.Utilities.Internal.getVirtualCollectionState('unfiled')
    186 	
    187 	var oldCount = this.rowCount || 0;
    188 	var newRows = [];
    189 	var added = 0;
    190 	
    191 	//
    192 	// Add "My Library"
    193 	//
    194 	this._addRowToArray(
    195 		newRows,
    196 		new Zotero.CollectionTreeRow(this, 'library', { libraryID: Zotero.Libraries.userLibraryID }),
    197 		added++
    198 	);
    199 	added += yield this._expandRow(newRows, 0);
    200 	
    201 	// TODO: Unify feed and group adding code
    202 	
    203 	// Add groups
    204 	var groups = Zotero.Groups.getAll();
    205 	if (groups.length) {
    206 		this._addRowToArray(
    207 			newRows,
    208 			new Zotero.CollectionTreeRow(this, 'separator', false),
    209 			added++
    210 		);
    211 		this._addRowToArray(
    212 			newRows,
    213 			new Zotero.CollectionTreeRow(this, 'header', {
    214 				id: "group-libraries-header",
    215 				label: Zotero.getString('pane.collections.groupLibraries'),
    216 				libraryID: -1
    217 			}, 0),
    218 			added++
    219 		);
    220 		for (let group of groups) {
    221 			this._addRowToArray(
    222 				newRows,
    223 				new Zotero.CollectionTreeRow(this, 'group', group),
    224 				added++
    225 			);
    226 			added += yield this._expandRow(newRows, added - 1);
    227 		}
    228 	}
    229 	
    230 	// Add feeds
    231 	if (this.hideSources.indexOf('feeds') == -1) {
    232 		var feeds = Zotero.Feeds.getAll();
    233 		
    234 		// Alphabetize
    235 		var collation = Zotero.getLocaleCollation();
    236 		feeds.sort(function(a, b) {
    237 			return collation.compareString(1, a.name, b.name);
    238 		});
    239 		
    240 		if (feeds.length) {
    241 			this._addRowToArray(
    242 				newRows,
    243 				new Zotero.CollectionTreeRow(this, 'separator', false),
    244 				added++
    245 			);
    246 			this._addRowToArray(
    247 				newRows,
    248 				new Zotero.CollectionTreeRow(this, 'header', {
    249 					id: "feed-libraries-header",
    250 					label: Zotero.getString('pane.collections.feedLibraries'),
    251 					libraryID: -1
    252 				}, 0),
    253 				added++
    254 			);
    255 			for (let feed of feeds) {
    256 				this._addRowToArray(
    257 					newRows,
    258 					new Zotero.CollectionTreeRow(this, 'feed', feed),
    259 					added++
    260 				);
    261 			}
    262 		}
    263 	}
    264 	
    265 	this.selection.selectEventsSuppressed = true;
    266 	this.selection.clearSelection();
    267 	this._rows = newRows;
    268 	this.rowCount = this._rows.length;
    269 	this._refreshRowMap();
    270 	
    271 	var diff = this.rowCount - oldCount;
    272 	if (diff != 0) {
    273 		this._treebox.rowCountChanged(0, diff);
    274 	}
    275 });
    276 
    277 
    278 /**
    279  * Refresh tree and invalidate
    280  *
    281  * See note for refresh() for requirements of calling code
    282  */
    283 Zotero.CollectionTreeView.prototype.reload = function()
    284 {
    285 	return this.refresh()
    286 	.then(function () {
    287 		this._treebox.invalidate();
    288 	}.bind(this));
    289 }
    290 
    291 
    292 /**
    293  * Select a row and wait for its items view to be created
    294  *
    295  * Note that this doesn't wait for the items view to be loaded. For that, add a 'load' event
    296  * listener to the items view.
    297  *
    298  * @param {Integer} row
    299  * @return {Promise}
    300  */
    301 Zotero.CollectionTreeView.prototype.selectWait = Zotero.Promise.method(function (row) {
    302 	if (this.selection.selectEventsSuppressed) {
    303 		this.selection.select(row);
    304 		return;
    305 	}
    306 	if (this.selection.currentIndex == row) {
    307 		return;
    308 	};
    309 	var promise = this.waitForSelect();
    310 	this.selection.select(row);
    311 	return promise;
    312 });
    313 
    314 
    315 
    316 /*
    317  *  Called by Zotero.Notifier on any changes to collections in the data layer
    318  */
    319 Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function* (action, type, ids, extraData) {
    320 	if ((!ids || ids.length == 0) && action != 'refresh' && action != 'redraw') {
    321 		return;
    322 	}
    323 	
    324 	if (!this._rowMap) {
    325 		Zotero.debug("Row map didn't exist in collectionTreeView.notify()");
    326 		return;
    327 	}
    328 	
    329 	if (!this.selection) {
    330 		Zotero.debug("Selection didn't exist in collectionTreeView.notify()");
    331 		return;
    332 	}
    333 	
    334 	//
    335 	// Actions that don't change the selection
    336 	//
    337 	if (action == 'redraw') {
    338 		this._treebox.invalidate();
    339 		return;
    340 	}
    341 	if (action == 'refresh') {
    342 		// If trash is refreshed, we probably need to update the icon from full to empty
    343 		if (type == 'trash') {
    344 			// libraryID is passed as parameter to 'refresh'
    345 			this._trashNotEmpty[ids[0]] = yield Zotero.Items.hasDeleted(ids[0]);
    346 			let row = this.getRowIndexByID("T" + ids[0]);
    347 			this._treebox.invalidateRow(row);
    348 		}
    349 		return;
    350 	}
    351 	if (type == 'feed' && (action == 'unreadCountUpdated' || action == 'statusChanged')) {
    352 		for (let i=0; i<ids.length; i++) {
    353 			this._treebox.invalidateRow(this._rowMap['L' + ids[i]]);
    354 		}
    355 		return;
    356 	}
    357 	
    358 	//
    359 	// Actions that can change the selection
    360 	//
    361 	var currentTreeRow = this.getRow(this.selection.currentIndex);
    362 	this.selection.selectEventsSuppressed = true;
    363 	
    364 	// If there's not at least one new collection to be selected, get a scroll position to restore later
    365 	var scrollPosition = false;
    366 	if (action != 'add' || ids.every(id => extraData[id] && extraData[id].skipSelect)) {
    367 		if (action == 'delete' && (type == 'group' || type == 'feed')) {
    368 			// Don't try to access deleted library
    369 		}
    370 		else {
    371 			scrollPosition = this._saveScrollPosition();
    372 		}
    373 	}
    374 	
    375 	if (action == 'delete') {
    376 		let selectedIndex = this.selection.count ? this.selection.currentIndex : 0;
    377 		let refreshFeeds = false;
    378 		
    379 		// Since a delete involves shifting of rows, we have to do it in reverse order
    380 		let rows = [];
    381 		for (let i = 0; i < ids.length; i++) {
    382 			let id = ids[i];
    383 			switch (type) {
    384 				case 'collection':
    385 					if (this._rowMap['C' + id] !== undefined) {
    386 						rows.push(this._rowMap['C' + id]);
    387 					}
    388 					break;
    389 				
    390 				case 'search':
    391 					if (this._rowMap['S' + id] !== undefined) {
    392 						rows.push(this._rowMap['S' + id]);
    393 					}
    394 					break;
    395 
    396 				case 'feed':
    397 				case 'group':
    398 					let row = this.getRowIndexByID("L" + extraData[id].libraryID);
    399 					let level = this.getLevel(row);
    400 					do {
    401 						rows.push(row);
    402 						row++;
    403 					}
    404 					while (row < this.rowCount && this.getLevel(row) > level);
    405 					
    406 					if (type == 'feed') {
    407 						refreshFeeds = true;
    408 					}
    409 					break;
    410 			}
    411 		}
    412 		
    413 		if (rows.length > 0) {
    414 			rows.sort(function(a,b) { return a-b });
    415 			
    416 			for (let i = rows.length - 1; i >= 0; i--) {
    417 				let row = rows[i];
    418 				this._removeRow(row);
    419 			}
    420 			
    421 			// If a feed was removed and there are no more, remove Feeds header
    422 			if (refreshFeeds && !Zotero.Feeds.haveFeeds()) {
    423 				for (let i = 0; i < this._rows.length; i++) {
    424 					let row = this._rows[i];
    425 					if (row.ref.id == 'feed-libraries-header') {
    426 						this._removeRow(i);
    427 						this._removeRow(i - 1);
    428 						break;
    429 					}
    430 				}
    431 			}
    432 			
    433 			this._refreshRowMap();
    434 		}
    435 		
    436 		this.selectAfterRowRemoval(selectedIndex);
    437 	}
    438 	else if (action == 'modify') {
    439 		let row;
    440 		let id = ids[0];
    441 		let rowID = "C" + id;
    442 		
    443 		switch (type) {
    444 		case 'collection':
    445 			row = this.getRowIndexByID(rowID);
    446 			if (row !== false) {
    447 				// TODO: Only move if name changed
    448 				let reopen = this.isContainerOpen(row);
    449 				if (reopen) {
    450 					this._closeContainer(row);
    451 				}
    452 				this._removeRow(row);
    453 				yield this._addSortedRow('collection', id);
    454 				yield this.selectByID(currentTreeRow.id);
    455 				if (reopen) {
    456 					let newRow = this.getRowIndexByID(rowID);
    457 					if (!this.isContainerOpen(newRow)) {
    458 						yield this.toggleOpenState(newRow);
    459 					}
    460 				}
    461 			}
    462 			break;
    463 		
    464 		case 'search':
    465 			row = this.getRowIndexByID("S" + id);
    466 			if (row !== false) {
    467 				// TODO: Only move if name changed
    468 				this._removeRow(row);
    469 				yield this._addSortedRow('search', id);
    470 				yield this.selectByID(currentTreeRow.id);
    471 			}
    472 			break;
    473 		
    474 		default:
    475 			yield this.reload();
    476 			yield this.selectByID(currentTreeRow.id);
    477 			break;
    478 		}
    479 	}
    480 	else if(action == 'add')
    481 	{
    482 		// skipSelect isn't necessary if more than one object
    483 		let selectRow = ids.length == 1 && (!extraData[ids[0]] || !extraData[ids[0]].skipSelect);
    484 		
    485 		for (let id of ids) {
    486 			switch (type) {
    487 				case 'collection':
    488 				case 'search':
    489 					yield this._addSortedRow(type, id);
    490 					
    491 					if (selectRow) {
    492 						if (type == 'collection') {
    493 							yield this.selectCollection(id);
    494 						}
    495 						else if (type == 'search') {
    496 							yield this.selectSearch(id);
    497 						}
    498 					}
    499 					
    500 					break;
    501 				
    502 				case 'group':
    503 				case 'feed':
    504 					if (type == 'groups' && ids.length != 1) {
    505 						Zotero.logError("WARNING: Multiple groups shouldn't currently be added "
    506 							+ "together in collectionTreeView::notify()")
    507 					}
    508 					yield this.reload();
    509 					yield this.selectByID(
    510 						// Groups only come from sync, so they should never be auto-selected
    511 						(type != 'group' && selectRow)
    512 							? "L" + id
    513 							: currentTreeRow.id
    514 					);
    515 					break;
    516 			}
    517 		}
    518 	}
    519 	
    520 	this._rememberScrollPosition(scrollPosition);
    521 	
    522 	var promise = this.waitForSelect();
    523 	this.selection.selectEventsSuppressed = false;
    524 	return promise;
    525 });
    526 
    527 /**
    528  * Add a row in the appropriate place
    529  *
    530  * This only adds a row if it would be visible without opening any containers
    531  *
    532  * @param {String} objectType
    533  * @param {Integer} id - collectionID
    534  * @return {Integer|false} - Index at which the row was added, or false if it wasn't added
    535  */
    536 Zotero.CollectionTreeView.prototype._addSortedRow = Zotero.Promise.coroutine(function* (objectType, id) {
    537 	let beforeRow;
    538 	if (objectType == 'collection') {
    539 		let collection = yield Zotero.Collections.getAsync(id);
    540 		let parentID = collection.parentID;
    541 		
    542 		// If parent isn't visible, don't add
    543 		if (parentID && this._rowMap["C" + parentID] === undefined) {
    544 			return false;
    545 		}
    546 		
    547 		let libraryID = collection.libraryID;
    548 		let startRow;
    549 		if (parentID) {
    550 			startRow = this._rowMap["C" + parentID];
    551 		}
    552 		else {
    553 			startRow = this._rowMap['L' + libraryID];
    554 		}
    555 		
    556 		// If container isn't open, don't add
    557 		if (!this.isContainerOpen(startRow)) {
    558 			return false;
    559 		}
    560 		
    561 		let level = this.getLevel(startRow) + 1;
    562 		// If container is empty, just add after
    563 		if (this.isContainerEmpty(startRow)) {
    564 			beforeRow = startRow + 1;
    565 		}
    566 		else {
    567 			// Get all collections at the same level that don't have a different parent
    568 			startRow++;
    569 			loop:
    570 			for (let i = startRow; i < this.rowCount; i++) {
    571 				let treeRow = this.getRow(i);
    572 				beforeRow = i;
    573 				
    574 				// Since collections come first, if we reach something that's not a collection,
    575 				// stop
    576 				if (!treeRow.isCollection()) {
    577 					break;
    578 				}
    579 				
    580 				let rowLevel = this.getLevel(i);
    581 				if (rowLevel < level) {
    582 					break;
    583 				}
    584 				else {
    585 					// Fast forward through subcollections
    586 					while (rowLevel > level) {
    587 						beforeRow = ++i;
    588 						if (i == this.rowCount || !this.getRow(i).isCollection()) {
    589 							break loop;
    590 						}
    591 						treeRow = this.getRow(i);
    592 						rowLevel = this.getLevel(i);
    593 						// If going from lower level to a row higher than the target level, we found
    594 						// our place:
    595 						//
    596 						// - 1
    597 						//   - 3
    598 						//     - 4
    599 						// - 2 <<<< 5, a sibling of 3, goes above here
    600 						if (rowLevel < level) {
    601 							break loop;
    602 						}
    603 					}
    604 					
    605 					if (Zotero.localeCompare(treeRow.ref.name, collection.name) > 0) {
    606 						break;
    607 					}
    608 				}
    609 			}
    610 		}
    611 		this._addRow(
    612 			new Zotero.CollectionTreeRow(this, 'collection', collection, level),
    613 			beforeRow
    614 		);
    615 	}
    616 	else if (objectType == 'search') {
    617 		let search = Zotero.Searches.get(id);
    618 		let libraryID = search.libraryID;
    619 		let startRow = this._rowMap['L' + libraryID];
    620 		
    621 		// If container isn't open, don't add
    622 		if (!this.isContainerOpen(startRow)) {
    623 			return false;
    624 		}
    625 		
    626 		let level = this.getLevel(startRow) + 1;
    627 		// If container is empty, just add after
    628 		if (this.isContainerEmpty(startRow)) {
    629 			beforeRow = startRow + 1;
    630 		}
    631 		else {
    632 			startRow++;
    633 			var inSearches = false;
    634 			for (let i = startRow; i < this.rowCount; i++) {
    635 				let treeRow = this.getRow(i);
    636 				beforeRow = i;
    637 				
    638 				// If we've reached something other than collections, stop
    639 				if (treeRow.isSearch()) {
    640 					// If current search sorts after, stop
    641 					if (Zotero.localeCompare(treeRow.ref.name, search.name) > 0) {
    642 						break;
    643 					}
    644 				}
    645 				// If it's not a search and it's not a collection, stop
    646 				else if (!treeRow.isCollection()) {
    647 					break;
    648 				}
    649 			}
    650 		}
    651 		this._addRow(
    652 			new Zotero.CollectionTreeRow(this, 'search', search, level),
    653 			beforeRow
    654 		);
    655 	}
    656 	return beforeRow;
    657 });
    658 
    659 
    660 /*
    661  * Set the rows that should be highlighted -- actual highlighting is done
    662  * by getRowProperties based on the array set here
    663  */
    664 Zotero.CollectionTreeView.prototype.setHighlightedRows = Zotero.Promise.coroutine(function* (ids) {
    665 	this._highlightedRows = {};
    666 	this._treebox.invalidate();
    667 	
    668 	if (!ids || !ids.length) {
    669 		return;
    670 	}
    671 	
    672 	// Make sure all highlighted collections are shown
    673 	for (let id of ids) {
    674 		if (id[0] == 'C') {
    675 			yield this.expandToCollection(parseInt(id.substr(1)));
    676 		}
    677 	}
    678 	
    679 	// Highlight rows
    680 	var rows = [];
    681 	for (let id of ids) {
    682 		let row = this._rowMap[id];
    683 		this._highlightedRows[row] = true;
    684 		this._treebox.invalidateRow(row);
    685 		rows.push(row);
    686 	}
    687 	rows.sort();
    688 	var firstRow = this._treebox.getFirstVisibleRow();
    689 	var lastRow = this._treebox.getLastVisibleRow();
    690 	var scrolled = false;
    691 	for (let row of rows) {
    692 		// If row is visible, stop
    693 		if (row >= firstRow && row <= lastRow) {
    694 			scrolled = true;
    695 			break;
    696 		}
    697 	}
    698 	// Select first collection
    699 	// TODO: Select closest? Select a few rows above or below?
    700 	if (!scrolled) {
    701 		this._treebox.ensureRowIsVisible(rows[0]);
    702 	}
    703 });
    704 
    705 
    706 /*
    707  *  Unregisters view from Zotero.Notifier (called on window close)
    708  */
    709 Zotero.CollectionTreeView.prototype.unregister = function()
    710 {
    711 	Zotero.Notifier.unregisterObserver(this._unregisterID);
    712 }
    713 
    714 
    715 ////////////////////////////////////////////////////////////////////////////////
    716 ///
    717 ///  nsITreeView functions
    718 ///  http://www.xulplanet.com/references/xpcomref/ifaces/nsITreeView.html
    719 ///
    720 ////////////////////////////////////////////////////////////////////////////////
    721 
    722 Zotero.CollectionTreeView.prototype.getCellText = function(row, column)
    723 {
    724 	var obj = this.getRow(row);
    725 	
    726 	if (column.id == 'zotero-collections-name-column') {
    727 		return obj.getName();
    728 	}
    729 	else
    730 		return "";
    731 }
    732 
    733 Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
    734 {
    735 	var suffix = Zotero.hiDPISuffix;
    736 	
    737 	var treeRow = this.getRow(row);
    738 	var collectionType = treeRow.type;
    739 	
    740 	if (collectionType == 'group') {
    741 		collectionType = 'library';
    742 	}
    743 	
    744 	// Show sync icons only in library rows
    745 	if (collectionType != 'library' && col.index != 0) {
    746 		return '';
    747 	}
    748 	
    749 	switch (collectionType) {
    750 		case 'library':
    751 		case 'feed':
    752 			// Better alternative needed: https://github.com/zotero/zotero/pull/902#issuecomment-183185973
    753 			/*
    754 			if (treeRow.ref.updating) {
    755 				collectionType += '-updating';
    756 			} else */if (treeRow.ref.lastCheckError) {
    757 				collectionType += '-error';
    758 			}
    759 			break;
    760 		
    761 		case 'trash':
    762 			if (this._trashNotEmpty[treeRow.ref.libraryID]) {
    763 				collectionType += '-full';
    764 			}
    765 			break;
    766 		
    767 		case 'header':
    768 			if (treeRow.ref.id == 'group-libraries-header') {
    769 				collectionType = 'groups';
    770 			}
    771 			else if (treeRow.ref.id == 'feed-libraries-header') {
    772 				collectionType = 'feedLibrary';
    773 			}
    774 			else if (treeRow.ref.id == 'commons-header') {
    775 				collectionType = 'commons';
    776 			}
    777 			break;
    778 		
    779 		
    780 			collectionType = 'library';
    781 			break;
    782 		
    783 		case 'collection':
    784 		case 'search':
    785 			// Keep in sync with Zotero.(Collection|Search).prototype.treeViewImage
    786 			if (Zotero.isMac) {
    787 				return `chrome://zotero-platform/content/treesource-${collectionType}${Zotero.hiDPISuffix}.png`;
    788 			}
    789 			break;
    790 		
    791 		case 'publications':
    792 			return "chrome://zotero/skin/treeitem-journalArticle" + suffix + ".png";
    793 	}
    794 	
    795 	return "chrome://zotero/skin/treesource-" + collectionType + suffix + ".png";
    796 }
    797 
    798 Zotero.CollectionTreeView.prototype.isContainer = function(row)
    799 {
    800 	var treeRow = this.getRow(row);
    801 	return treeRow.isLibrary(true) || treeRow.isCollection() || treeRow.isPublications() || treeRow.isBucket();
    802 }
    803 
    804 /*
    805  * Returns true if the collection has no child collections
    806  */
    807 Zotero.CollectionTreeView.prototype.isContainerEmpty = function(row)
    808 {
    809 	var treeRow = this.getRow(row);
    810 	if (treeRow.isLibrary()) {
    811 		return false;
    812 	}
    813 	if (treeRow.isBucket()) {
    814 		return true;
    815 	}
    816 	if (treeRow.isGroup()) {
    817 		var libraryID = treeRow.ref.libraryID;
    818 		
    819 		return !treeRow.ref.hasCollections()
    820 				&& !treeRow.ref.hasSearches()
    821 				// Duplicate Items not shown
    822 				&& (this.hideSources.indexOf('duplicates') != -1
    823 					|| this._virtualCollectionLibraries.duplicates[libraryID] === false)
    824 				// Unfiled Items not shown
    825 				&& this._virtualCollectionLibraries.unfiled[libraryID] === false
    826 				&& this.hideSources.indexOf('trash') != -1;
    827 	}
    828 	if (treeRow.isCollection()) {
    829 		return !treeRow.ref.hasChildCollections();
    830 	}
    831 	return true;
    832 }
    833 
    834 Zotero.CollectionTreeView.prototype.getParentIndex = function(row)
    835 {
    836 	var thisLevel = this.getLevel(row);
    837 	if(thisLevel == 0) return -1;
    838 	for(var i = row - 1; i >= 0; i--)
    839 		if(this.getLevel(i) < thisLevel)
    840 			return i;
    841 	return -1;
    842 }
    843 
    844 Zotero.CollectionTreeView.prototype.hasNextSibling = function(row, afterIndex)
    845 {
    846 	var thisLevel = this.getLevel(row);
    847 	for(var i = afterIndex + 1; i < this.rowCount; i++)
    848 	{	
    849 		var nextLevel = this.getLevel(i);
    850 		if(nextLevel == thisLevel) return true;
    851 		else if(nextLevel < thisLevel) return false;
    852 	}
    853 }
    854 
    855 /*
    856  *  Opens/closes the specified row
    857  */
    858 Zotero.CollectionTreeView.prototype.toggleOpenState = Zotero.Promise.coroutine(function* (row) {
    859 	if (this.isContainerOpen(row)) {
    860 		return this._closeContainer(row);
    861 	}
    862 	
    863 	var count = 0;
    864 	
    865 	var treeRow = this.getRow(row);
    866 	if (treeRow.isLibrary(true) || treeRow.isCollection()) {
    867 		count = yield this._expandRow(this._rows, row, true);
    868 	}
    869 	this.rowCount += count;
    870 	this._treebox.rowCountChanged(row + 1, count);
    871 	
    872 	this._rows[row].isOpen = true;
    873 	this._treebox.invalidateRow(row);
    874 	this._refreshRowMap();
    875 	this._startSaveOpenStatesTimer();
    876 });
    877 
    878 
    879 Zotero.CollectionTreeView.prototype._closeContainer = function (row) {
    880 	if (!this.isContainerOpen(row)) return;
    881 	
    882 	var count = 0;
    883 	var level = this.getLevel(row);
    884 	var nextRow = row + 1;
    885 	
    886 	// Remove child rows
    887 	while ((nextRow < this._rows.length) && (this.getLevel(nextRow) > level)) {
    888 		this._removeRow(nextRow);
    889 		count--;
    890 	}
    891 	
    892 	this._rows[row].isOpen = false;
    893 	this._treebox.invalidateRow(row);
    894 	this._refreshRowMap();
    895 	this._startSaveOpenStatesTimer();
    896 }
    897 
    898 
    899 /**
    900  * After a short delay, persist the open states of the tree, or if already queued, cancel and requeue.
    901  * This avoids repeated saving while opening or closing multiple rows.
    902  */
    903 Zotero.CollectionTreeView.prototype._startSaveOpenStatesTimer = function () {
    904 	if (this._saveOpenStatesTimeoutID) {
    905 		clearTimeout(this._saveOpenStatesTimeoutID);
    906 	}
    907 	this._saveOpenStatesTimeoutID = setTimeout(() => {
    908 		this._saveOpenStates();
    909 		this._saveOpenStatesTimeoutID = null;
    910 	}, 250)
    911 };
    912 
    913 
    914 Zotero.CollectionTreeView.prototype.isSelectable = function (row, col) {
    915 	var treeRow = this.getRow(row);
    916 	switch (treeRow.type) {
    917 	case 'separator':
    918 	case 'header':
    919 		return false;
    920 	}
    921 	return true;
    922 }
    923 
    924 
    925 /**
    926  * Tree method for whether to allow inline editing (not to be confused with this.editable)
    927  */
    928 Zotero.CollectionTreeView.prototype.isEditable = function (row, col) {
    929 	return this.selectedTreeRow.isCollection() && this.editable;
    930 }
    931 
    932 
    933 Zotero.CollectionTreeView.prototype.setCellText = function (row, col, val) {
    934 	val = val.trim();
    935 	if (val === "") {
    936 		return;
    937 	}
    938 	var treeRow = this.getRow(row);
    939 	treeRow.ref.name = val;
    940 	treeRow.ref.saveTx();
    941 }
    942 
    943 
    944 
    945 /**
    946  * Returns TRUE if the underlying view is editable
    947  */
    948 Zotero.CollectionTreeView.prototype.__defineGetter__('editable', function () {
    949 	return this.getRow(this.selection.currentIndex).editable;
    950 });
    951 
    952 
    953 /**
    954  * @param {Integer} libraryID
    955  * @param {Boolean} [recursive=false] - Expand all collections and subcollections
    956  */
    957 Zotero.CollectionTreeView.prototype.expandLibrary = Zotero.Promise.coroutine(function* (libraryID, recursive) {
    958 	var row = this._rowMap['L' + libraryID]
    959 	if (row === undefined) {
    960 		return false;
    961 	}
    962 	if (!this.isContainerOpen(row)) {
    963 		yield this.toggleOpenState(row);
    964 	}
    965 	
    966 	if (recursive) {
    967 		for (let i = row; i < this.rowCount && this.getRow(i).ref.libraryID == libraryID; i++) {
    968 			if (this.isContainer(i) && !this.isContainerOpen(i)) {
    969 				yield this.toggleOpenState(i);
    970 			}
    971 		}
    972 	}
    973 	
    974 	return true;
    975 });
    976 
    977 
    978 Zotero.CollectionTreeView.prototype.collapseLibrary = function (libraryID) {
    979 	var row = this._rowMap['L' + libraryID]
    980 	if (row === undefined) {
    981 		return false;
    982 	}
    983 	
    984 	var closed = [];
    985 	var found = false;
    986 	for (let i = this.rowCount - 1; i >= row; i--) {
    987 		let treeRow = this.getRow(i);
    988 		if (treeRow.ref.libraryID !== libraryID) {
    989 			// Once we've moved beyond the original library, stop looking
    990 			if (found) {
    991 				break;
    992 			}
    993 			continue;
    994 		}
    995 		found = true;
    996 		
    997 		if (this.isContainer(i) && this.isContainerOpen(i)) {
    998 			closed.push(treeRow.id);
    999 			this._closeContainer(i);
   1000 		}
   1001 	}
   1002 	
   1003 	// Select the collapsed library
   1004 	this.selection.select(row);
   1005 	
   1006 	// We have to manually delete closed rows from the container state object, because otherwise
   1007 	// _saveOpenStates() wouldn't see any of the rows under the library (since the library is now
   1008 	// collapsed) and they'd remain as open in the persisted object.
   1009 	closed.forEach(id => { delete this._containerState[id]; });
   1010 	this._saveOpenStates();
   1011 	
   1012 	return true;
   1013 };
   1014 
   1015 
   1016 Zotero.CollectionTreeView.prototype.expandToCollection = Zotero.Promise.coroutine(function* (collectionID) {
   1017 	var col = yield Zotero.Collections.getAsync(collectionID);
   1018 	if (!col) {
   1019 		Zotero.debug("Cannot expand to nonexistent collection " + collectionID, 2);
   1020 		return false;
   1021 	}
   1022 	
   1023 	// Open library if closed
   1024 	var libraryRow = this._rowMap['L' + col.libraryID];
   1025 	if (!this.isContainerOpen(libraryRow)) {
   1026 		yield this.toggleOpenState(libraryRow);
   1027 	}
   1028 	
   1029 	var row = this._rowMap["C" + collectionID];
   1030 	if (row !== undefined) {
   1031 		return true;
   1032 	}
   1033 	var path = [];
   1034 	var parentID;
   1035 	while (parentID = col.parentID) {
   1036 		path.unshift(parentID);
   1037 		col = yield Zotero.Collections.getAsync(parentID);
   1038 	}
   1039 	for (let id of path) {
   1040 		row = this._rowMap["C" + id];
   1041 		if (!this.isContainerOpen(row)) {
   1042 			yield this.toggleOpenState(row);
   1043 		}
   1044 	}
   1045 	return true;
   1046 });
   1047 
   1048 
   1049 
   1050 ////////////////////////////////////////////////////////////////////////////////
   1051 ///
   1052 ///  Additional functions for managing data in the tree
   1053 ///
   1054 ////////////////////////////////////////////////////////////////////////////////
   1055 Zotero.CollectionTreeView.prototype.selectByID = Zotero.Promise.coroutine(function* (id) {
   1056 	var type = id[0];
   1057 	id = parseInt(('' + id).substr(1));
   1058 	
   1059 	switch (type) {
   1060 	case 'L':
   1061 		return yield this.selectLibrary(id);
   1062 	
   1063 	case 'C':
   1064 		yield this.expandToCollection(id);
   1065 		break;
   1066 	
   1067 	case 'S':
   1068 		var search = yield Zotero.Searches.getAsync(id);
   1069 		yield this.expandLibrary(search.libraryID);
   1070 		break;
   1071 	
   1072 	case 'D':
   1073 	case 'U':
   1074 		yield this.expandLibrary(id);
   1075 		break;
   1076 	
   1077 	case 'T':
   1078 		return yield this.selectTrash(id);
   1079 	}
   1080 	
   1081 	var row = this._rowMap[type + id];
   1082 	if (!row) {
   1083 		return false;
   1084 	}
   1085 	this._treebox.ensureRowIsVisible(row);
   1086 	yield this.selectWait(row);
   1087 	
   1088 	return true;
   1089 });
   1090 
   1091 
   1092 /**
   1093  * @param	{Integer}		libraryID		Library to select
   1094  */
   1095 Zotero.CollectionTreeView.prototype.selectLibrary = Zotero.Promise.coroutine(function* (libraryID) {
   1096 	// Select local library
   1097 	if (!libraryID) {
   1098 		this._treebox.ensureRowIsVisible(0);
   1099 		yield this.selectWait(0);
   1100 		return true;
   1101 	}
   1102 	
   1103 	// Check if library is already selected
   1104 	if (this.selection && this.selection.count && this.selection.currentIndex != -1) {
   1105 		var treeRow = this.getRow(this.selection.currentIndex);
   1106 		if (treeRow.isLibrary(true) && treeRow.ref.libraryID == libraryID) {
   1107 			this._treebox.ensureRowIsVisible(this.selection.currentIndex);
   1108 			return true;
   1109 		}
   1110 	}
   1111 	
   1112 	// Find library
   1113 	var row = this._rowMap['L' + libraryID];
   1114 	if (row !== undefined) {
   1115 		this._treebox.ensureRowIsVisible(row);
   1116 		yield this.selectWait(row);
   1117 		return true;
   1118 	}
   1119 	
   1120 	return false;
   1121 });
   1122 
   1123 
   1124 Zotero.CollectionTreeView.prototype.selectCollection = function (id) {
   1125 	return this.selectByID('C' + id);
   1126 }
   1127 
   1128 
   1129 Zotero.CollectionTreeView.prototype.selectSearch = function (id) {
   1130 	return this.selectByID('S' + id);
   1131 }
   1132 
   1133 
   1134 Zotero.CollectionTreeView.prototype.selectTrash = Zotero.Promise.coroutine(function* (libraryID) {
   1135 	// Check if trash is already selected
   1136 	if (this.selection && this.selection.count && this.selection.currentIndex != -1) {
   1137 		let itemGroup = this.getRow(this.selection.currentIndex);
   1138 		if (itemGroup.isTrash() && itemGroup.ref.libraryID == libraryID) {
   1139 			this._treebox.ensureRowIsVisible(this.selection.currentIndex);
   1140 			return true;
   1141 		}
   1142 	}
   1143 	
   1144 	// Find library trash
   1145 	for (let i = 0; i < this.rowCount; i++) {
   1146 		let itemGroup = this.getRow(i);
   1147 		
   1148 		// If library is closed, open it
   1149 		if (itemGroup.isLibrary(true) && itemGroup.ref.libraryID == libraryID
   1150 				&& !this.isContainerOpen(i)) {
   1151 			yield this.toggleOpenState(i);
   1152 			continue;
   1153 		}
   1154 		
   1155 		if (itemGroup.isTrash() && itemGroup.ref.libraryID == libraryID) {
   1156 			this._treebox.ensureRowIsVisible(i);
   1157 			this.selection.select(i);
   1158 			return true;
   1159 		}
   1160 	}
   1161 	
   1162 	return false;
   1163 });
   1164 
   1165 
   1166 /**
   1167  * Find item in current collection, or, if not there, in a library root, and select it
   1168  *
   1169  * @param {Integer} itemID
   1170  * @param {Boolean} [inLibraryRoot=false] - Always show in library root
   1171  * @param {Boolean} [expand=false] - Open item if it's a container
   1172  * @return {Boolean} - True if item was found, false if not
   1173  */
   1174 Zotero.CollectionTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (itemID, inLibraryRoot, expand) {
   1175 	if (!itemID) {
   1176 		return false;
   1177 	}
   1178 	
   1179 	var item = yield Zotero.Items.getAsync(itemID);
   1180 	if (!item) {
   1181 		return false;
   1182 	}
   1183 	
   1184 	yield this.waitForLoad();
   1185 	
   1186 	var currentLibraryID = this.getSelectedLibraryID();
   1187 	// If in a different library
   1188 	if (item.libraryID != currentLibraryID) {
   1189 		Zotero.debug("Library ID differs; switching library");
   1190 		yield this.selectLibrary(item.libraryID);
   1191 	}
   1192 	// Force switch to library view
   1193 	else if (!this.selectedTreeRow.isLibrary() && inLibraryRoot) {
   1194 		Zotero.debug("Told to select in library; switching to library");
   1195 		yield this.selectLibrary(item.libraryID);
   1196 	}
   1197 	
   1198 	yield this.itemTreeView.waitForLoad();
   1199 	
   1200 	var selected = yield this.itemTreeView.selectItem(itemID, expand);
   1201 	if (selected) {
   1202 		return true;
   1203 	}
   1204 	
   1205 	if (item.deleted) {
   1206 		Zotero.debug("Item is deleted; switching to trash");
   1207 		yield this.selectTrash(item.libraryID);
   1208 	}
   1209 	else {
   1210 		Zotero.debug("Item was not selected; switching to library");
   1211 		yield this.selectLibrary(item.libraryID);
   1212 	}
   1213 	
   1214 	yield this.itemTreeView.waitForLoad();
   1215 	
   1216 	return this.itemTreeView.selectItem(itemID, expand);
   1217 });
   1218 
   1219 
   1220 /*
   1221  *  Delete the selection
   1222  */
   1223 Zotero.CollectionTreeView.prototype.deleteSelection = Zotero.Promise.coroutine(function* (deleteItems)
   1224 {
   1225 	if(this.selection.count == 0)
   1226 		return;
   1227 
   1228 	//collapse open collections
   1229 	for (let i=0; i<this.rowCount; i++) {
   1230 		if (this.selection.isSelected(i) && this.isContainer(i)) {
   1231 			this._closeContainer(i);
   1232 		}
   1233 	}
   1234 	this._refreshRowMap();
   1235 	
   1236 	//create an array of collections
   1237 	var rows = new Array();
   1238 	var start = new Object();
   1239 	var end = new Object();
   1240 	for (var i=0, len=this.selection.getRangeCount(); i<len; i++)
   1241 	{
   1242 		this.selection.getRangeAt(i,start,end);
   1243 		for (var j=start.value; j<=end.value; j++)
   1244 			if(!this.getRow(j).isLibrary())
   1245 				rows.push(j);
   1246 	}
   1247 	
   1248 	//iterate and erase...
   1249 	//this._treebox.beginUpdateBatch();
   1250 	for (var i=0; i<rows.length; i++)
   1251 	{
   1252 		//erase collection from DB:
   1253 		var treeRow = this.getRow(rows[i]-i);
   1254 		if (treeRow.isCollection() || treeRow.isFeed()) {
   1255 			yield treeRow.ref.eraseTx({ deleteItems });
   1256 			if (treeRow.isFeed()) {
   1257 				refreshFeeds = true;
   1258 			}
   1259 		}
   1260 		else if (treeRow.isSearch()) {
   1261 			yield Zotero.Searches.erase(treeRow.ref.id);
   1262 		}
   1263 	}
   1264 	//this._treebox.endUpdateBatch();
   1265 });
   1266 
   1267 
   1268 Zotero.CollectionTreeView.prototype.selectAfterRowRemoval = function (row) {
   1269 	// If last row was selected, stay on the last row
   1270 	if (row >= this.rowCount) {
   1271 		row = this.rowCount - 1;
   1272 	};
   1273 	
   1274 	// Make sure the selection doesn't land on a separator (e.g. deleting last feed)
   1275 	while (row >= 0 && !this.isSelectable(row)) {
   1276 		// move up, since we got shifted down
   1277 		row--;
   1278 	}
   1279 	
   1280 	this.selection.select(row);
   1281 };
   1282 
   1283 
   1284 /**
   1285  * Expand row based on last state
   1286  */
   1287 Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(function* (rows, row, forceOpen) {
   1288 	var treeRow = rows[row];
   1289 	var level = rows[row].level;
   1290 	var isLibrary = treeRow.isLibrary(true);
   1291 	var isCollection = treeRow.isCollection();
   1292 	var libraryID = treeRow.ref.libraryID;
   1293 	
   1294 	if (treeRow.isPublications() || treeRow.isFeed()) {
   1295 		return false;
   1296 	}
   1297 	
   1298 	if (isLibrary) {
   1299 		var collections = Zotero.Collections.getByLibrary(libraryID);
   1300 	}
   1301 	else if (isCollection) {
   1302 		var collections = Zotero.Collections.getByParent(treeRow.ref.id);
   1303 	}
   1304 	
   1305 	if (isLibrary) {
   1306 		var savedSearches = yield Zotero.Searches.getAll(libraryID);
   1307 		// Virtual collections default to showing if not explicitly hidden
   1308 		var showDuplicates = this.hideSources.indexOf('duplicates') == -1
   1309 				&& this._virtualCollectionLibraries.duplicates[libraryID] !== false;
   1310 		var showUnfiled = this._virtualCollectionLibraries.unfiled[libraryID] !== false;
   1311 		var showPublications = libraryID == Zotero.Libraries.userLibraryID;
   1312 		var showTrash = this.hideSources.indexOf('trash') == -1;
   1313 	}
   1314 	else {
   1315 		var savedSearches = [];
   1316 		var showDuplicates = false;
   1317 		var showUnfiled = false;
   1318 		var showPublications = false;
   1319 		var showTrash = false;
   1320 	}
   1321 	
   1322 	// If not a manual open and either the library is set to be collapsed or this is a collection that isn't explicitly opened,
   1323 	// set the initial state to closed
   1324 	if (!forceOpen &&
   1325 			(this._containerState[treeRow.id] === false
   1326 				|| (isCollection && !this._containerState[treeRow.id]))) {
   1327 		rows[row].isOpen = false;
   1328 		return 0;
   1329 	}
   1330 	
   1331 	var startOpen = !!(collections.length || savedSearches.length || showDuplicates || showUnfiled || showTrash);
   1332 	
   1333 	// If this isn't a manual open, set the initial state depending on whether
   1334 	// there are child nodes
   1335 	if (!forceOpen) {
   1336 		rows[row].isOpen = startOpen;
   1337 	}
   1338 	
   1339 	if (!startOpen) {
   1340 		return 0;
   1341 	}
   1342 	
   1343 	var newRows = 0;
   1344 	
   1345 	// Add collections
   1346 	for (var i = 0, len = collections.length; i < len; i++) {
   1347 		let beforeRow = row + 1 + newRows;
   1348 		this._addRowToArray(
   1349 			rows,
   1350 			new Zotero.CollectionTreeRow(this, 'collection', collections[i], level + 1),
   1351 			beforeRow
   1352 		);
   1353 		newRows++;
   1354 		// Recursively expand child collections that should be open
   1355 		newRows += yield this._expandRow(rows, beforeRow);
   1356 	}
   1357 	
   1358 	if (isCollection) {
   1359 		return newRows;
   1360 	}
   1361 	
   1362 	// Add searches
   1363 	for (var i = 0, len = savedSearches.length; i < len; i++) {
   1364 		this._addRowToArray(
   1365 			rows,
   1366 			new Zotero.CollectionTreeRow(this, 'search', savedSearches[i], level + 1),
   1367 			row + 1 + newRows
   1368 		);
   1369 		newRows++;
   1370 	}
   1371 	
   1372 	if (showPublications) {
   1373 		// Add "My Publications"
   1374 		this._addRowToArray(
   1375 			rows,
   1376 			new Zotero.CollectionTreeRow(this,
   1377 				'publications',
   1378 				{
   1379 					libraryID,
   1380 					treeViewID: "P" + libraryID
   1381 				},
   1382 				level + 1
   1383 			),
   1384 			row + 1 + newRows
   1385 		);
   1386 		newRows++
   1387 	}
   1388 	
   1389 	// Duplicate items
   1390 	if (showDuplicates) {
   1391 		let d = new Zotero.Duplicates(libraryID);
   1392 		this._addRowToArray(
   1393 			rows,
   1394 			new Zotero.CollectionTreeRow(this, 'duplicates', d, level + 1),
   1395 			row + 1 + newRows
   1396 		);
   1397 		newRows++;
   1398 	}
   1399 	
   1400 	// Unfiled items
   1401 	if (showUnfiled) {
   1402 		let s = new Zotero.Search;
   1403 		s.libraryID = libraryID;
   1404 		s.name = Zotero.getString('pane.collections.unfiled');
   1405 		s.addCondition('libraryID', 'is', libraryID);
   1406 		s.addCondition('unfiled', 'true');
   1407 		this._addRowToArray(
   1408 			rows,
   1409 			new Zotero.CollectionTreeRow(this, 'unfiled', s, level + 1),
   1410 			row + 1 + newRows
   1411 		);
   1412 		newRows++;
   1413 	}
   1414 	
   1415 	if (showTrash) {
   1416 		let deletedItems = yield Zotero.Items.getDeleted(libraryID);
   1417 		if (deletedItems.length || Zotero.Prefs.get("showTrashWhenEmpty")) {
   1418 			var ref = {
   1419 				libraryID: libraryID
   1420 			};
   1421 			this._addRowToArray(
   1422 				rows,
   1423 				new Zotero.CollectionTreeRow(this, 'trash', ref, level + 1),
   1424 				row + 1 + newRows
   1425 			);
   1426 			newRows++;
   1427 		}
   1428 		this._trashNotEmpty[libraryID] = !!deletedItems.length;
   1429 	}
   1430 	
   1431 	return newRows;
   1432 });
   1433 
   1434 
   1435 /**
   1436  * Return libraryID of selected row (which could be a collection, etc.)
   1437  */
   1438 Zotero.CollectionTreeView.prototype.getSelectedLibraryID = function() {
   1439 	if (!this.selection || !this.selection.count || this.selection.currentIndex == -1) return false;
   1440 	
   1441 	var treeRow = this.getRow(this.selection.currentIndex);
   1442 	return treeRow && treeRow.ref && treeRow.ref.libraryID !== undefined
   1443 			&& treeRow.ref.libraryID;
   1444 }
   1445 
   1446 
   1447 Zotero.CollectionTreeView.prototype.getSelectedCollection = function(asID) {
   1448 	if (this.selection
   1449 			&& this.selection.count > 0
   1450 			&& this.selection.currentIndex != -1) {
   1451 		var collection = this.getRow(this.selection.currentIndex);
   1452 		if (collection && collection.isCollection()) {
   1453 			return asID ? collection.ref.id : collection.ref;
   1454 		}
   1455 	}
   1456 	return false;
   1457 }
   1458 
   1459 
   1460 /**
   1461  * Creates mapping of item group ids to tree rows
   1462  */
   1463 Zotero.CollectionTreeView.prototype._refreshRowMap = function() {
   1464 	this._rowMap = {};
   1465 	for (let i = 0, len = this.rowCount; i < len; i++) {
   1466 		this._rowMap[this.getRow(i).id] = i;
   1467 	}
   1468 }
   1469 
   1470 
   1471 /**
   1472  * Persist the current open/closed state of rows to a pref
   1473  */
   1474 Zotero.CollectionTreeView.prototype._saveOpenStates = Zotero.Promise.coroutine(function* () {
   1475 	var state = this._containerState;
   1476 	
   1477 	// Every so often, remove obsolete rows
   1478 	if (Math.random() < 1/20) {
   1479 		Zotero.debug("Purging sourceList.persist");
   1480 		for (var id in state) {
   1481 			var m = id.match(/^C([0-9]+)$/);
   1482 			if (m) {
   1483 				if (!(yield Zotero.Collections.getAsync(parseInt(m[1])))) {
   1484 					delete state[id];
   1485 				}
   1486 				continue;
   1487 			}
   1488 			
   1489 			var m = id.match(/^G([0-9]+)$/);
   1490 			if (m) {
   1491 				if (!Zotero.Groups.get(parseInt(m[1]))) {
   1492 					delete state[id];
   1493 				}
   1494 				continue;
   1495 			}
   1496 		}
   1497 	}
   1498 	
   1499 	for (var i = 0, len = this.rowCount; i < len; i++) {
   1500 		if (!this.isContainer(i)) {
   1501 			continue;
   1502 		}
   1503 		
   1504 		var treeRow = this.getRow(i);
   1505 		if (!treeRow.id) {
   1506 			continue;
   1507 		}
   1508 		
   1509 		var open = this.isContainerOpen(i);
   1510 		
   1511 		// Collections and feeds default to closed
   1512 		if ((!open && treeRow.isCollection()) || treeRow.isFeed()) {
   1513 			delete state[treeRow.id];
   1514 			continue;
   1515 		}
   1516 		
   1517 		state[treeRow.id] = open;
   1518 	}
   1519 	
   1520 	this._containerState = state;
   1521 	Zotero.Prefs.set("sourceList.persist", JSON.stringify(state));
   1522 });
   1523 
   1524 
   1525 ////////////////////////////////////////////////////////////////////////////////
   1526 ///
   1527 ///  Command Controller:
   1528 ///		for Select All, etc.
   1529 ///
   1530 ////////////////////////////////////////////////////////////////////////////////
   1531 
   1532 Zotero.CollectionTreeCommandController = function(tree)
   1533 {
   1534 	this.tree = tree;
   1535 }
   1536 
   1537 Zotero.CollectionTreeCommandController.prototype.supportsCommand = function(cmd)
   1538 {
   1539 }
   1540 
   1541 Zotero.CollectionTreeCommandController.prototype.isCommandEnabled = function(cmd)
   1542 {
   1543 }
   1544 
   1545 Zotero.CollectionTreeCommandController.prototype.doCommand = function(cmd)
   1546 {
   1547 }
   1548 
   1549 Zotero.CollectionTreeCommandController.prototype.onEvent = function(evt)
   1550 {
   1551 }
   1552 
   1553 ////////////////////////////////////////////////////////////////////////////////
   1554 ///
   1555 ///  Drag-and-drop functions:
   1556 ///		canDrop() and drop() are for nsITreeView
   1557 ///		onDragStart() and onDrop() are for HTML 5 Drag and Drop
   1558 ///
   1559 ////////////////////////////////////////////////////////////////////////////////
   1560 
   1561 
   1562 /*
   1563  * Start a drag using HTML 5 Drag and Drop
   1564  */
   1565 Zotero.CollectionTreeView.prototype.onDragStart = function(event) {
   1566 	// See note in LibraryTreeView::_setDropEffect()
   1567 	if (Zotero.isWin || Zotero.isLinux) {
   1568 		event.dataTransfer.effectAllowed = 'copyMove';
   1569 	}
   1570 	
   1571 	var treeRow = this.selectedTreeRow;
   1572 	if (!treeRow.isCollection()) {
   1573 		return;
   1574 	}
   1575 	event.dataTransfer.setData("zotero/collection", treeRow.ref.id);
   1576 }
   1577 
   1578 
   1579 /**
   1580  * Called by treechildren.onDragOver() before setting the dropEffect,
   1581  * which is checked in libraryTreeView.canDrop()
   1582  */
   1583 Zotero.CollectionTreeView.prototype.canDropCheck = function (row, orient, dataTransfer) {
   1584 	//Zotero.debug("Row is " + row + "; orient is " + orient);
   1585 	
   1586 	var dragData = Zotero.DragDrop.getDataFromDataTransfer(dataTransfer);
   1587 	if (!dragData) {
   1588 		Zotero.debug("No drag data");
   1589 		return false;
   1590 	}
   1591 	var dataType = dragData.dataType;
   1592 	var data = dragData.data;
   1593 	
   1594 	// Empty space below rows
   1595 	if (row == -1) {
   1596 		return false;
   1597 	}
   1598 	
   1599 	// For dropping collections onto root level
   1600 	if (orient == 1 && row == 0 && dataType == 'zotero/collection') {
   1601 		return true;
   1602 	}
   1603 	// Directly on a row
   1604 	else if (orient == 0) {
   1605 		var treeRow = this.getRow(row); //the collection we are dragging over
   1606 		
   1607 		if (dataType == 'zotero/item' && treeRow.isBucket()) {
   1608 			return true;
   1609 		}
   1610 		
   1611 		if (!treeRow.editable) {
   1612 			Zotero.debug("Drop target not editable");
   1613 			return false;
   1614 		}
   1615 		
   1616 		if (treeRow.isFeed()) {
   1617 			Zotero.debug("Cannot drop into feeds");
   1618 			return false;
   1619 		}
   1620 		
   1621 		if (dataType == 'zotero/item') {
   1622 			var ids = data;
   1623 			var items = Zotero.Items.get(ids);
   1624 			items = Zotero.Items.keepParents(items);
   1625 			var skip = true;
   1626 			for (let item of items) {
   1627 				// Can only drag top-level items
   1628 				if (!item.isTopLevelItem()) {
   1629 					Zotero.debug("Can't drag child item");
   1630 					return false;
   1631 				}
   1632 				
   1633 				if (treeRow.isWithinGroup() && item.isAttachment()) {
   1634 					// Linked files can't be added to groups
   1635 					if (item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
   1636 						Zotero.debug("Linked files cannot be added to groups");
   1637 						return false;
   1638 					}
   1639 					if (!treeRow.filesEditable) {
   1640 						Zotero.debug("Drop target does not allow files to be edited");
   1641 						return false;
   1642 					}
   1643 					skip = false;
   1644 					continue;
   1645 				}
   1646 				
   1647 				if (treeRow.isPublications()) {
   1648 					if (item.isAttachment() || item.isNote()) {
   1649 						Zotero.debug("Top-level attachments and notes cannot be added to My Publications");
   1650 						return false;
   1651 					}
   1652 					if(item instanceof Zotero.FeedItem) {
   1653 						Zotero.debug("FeedItems cannot be added to My Publications");
   1654 						return false;
   1655 					}
   1656 					if (item.inPublications) {
   1657 						Zotero.debug("Item " + item.id + " already exists in My Publications");
   1658 						continue;
   1659 					}
   1660 					if (treeRow.ref.libraryID != item.libraryID) {
   1661 						Zotero.debug("Cross-library drag to My Publications not allowed");
   1662 						continue;
   1663 					}
   1664 					skip = false;
   1665 					continue;
   1666 				}
   1667 				
   1668 				// Cross-library drag
   1669 				if (treeRow.ref.libraryID != item.libraryID) {
   1670 					// Only allow cross-library drag to root library and collections
   1671 					if (!(treeRow.isLibrary(true) || treeRow.isCollection())) {
   1672 						Zotero.debug("Cross-library drag to non-collection not allowed");
   1673 						return false;
   1674 					}
   1675 					skip = false;
   1676 					continue;
   1677 				}
   1678 				
   1679 				// Intra-library drag
   1680 				
   1681 				// Don't allow drag onto root of same library
   1682 				if (treeRow.isLibrary(true)) {
   1683 					Zotero.debug("Can't drag into same library root");
   1684 					return false;
   1685 				}
   1686 				
   1687 				// Make sure there's at least one item that's not already in this destination
   1688 				if (treeRow.isCollection()) {
   1689 					if (treeRow.ref.hasItem(item.id)) {
   1690 						Zotero.debug("Item " + item.id + " already exists in collection");
   1691 						continue;
   1692 					}
   1693 					skip = false;
   1694 					continue;
   1695 				}
   1696 			}
   1697 			if (skip) {
   1698 				Zotero.debug("Drag skipped");
   1699 				return false;
   1700 			}
   1701 			return true;
   1702 		}
   1703 		else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
   1704 			if (treeRow.isSearch() || treeRow.isPublications()) {
   1705 				return false;
   1706 			}
   1707 			if (dataType == 'application/x-moz-file') {
   1708 				// Don't allow folder drag
   1709 				if (data[0].isDirectory()) {
   1710 					return false;
   1711 				}
   1712 				// Don't allow drop if no permissions
   1713 				if (!treeRow.filesEditable) {
   1714 					return false;
   1715 				}
   1716 			}
   1717 			
   1718 			return true;
   1719 		}
   1720 		else if (dataType == 'zotero/collection') {
   1721 			if (treeRow.isPublications()) {
   1722 				return false;
   1723 			}
   1724 			
   1725 			let draggedCollectionID = data[0];
   1726 			let draggedCollection = Zotero.Collections.get(draggedCollectionID);
   1727 			
   1728 			if (treeRow.ref.libraryID == draggedCollection.libraryID) {
   1729 				// Collections cannot be dropped on themselves
   1730 				if (draggedCollectionID == treeRow.ref.id) {
   1731 					return false;
   1732 				}
   1733 				
   1734 				// Nor in their children
   1735 				if (draggedCollection.hasDescendent('collection', treeRow.ref.id)) {
   1736 					return false;
   1737 				}
   1738 			}
   1739 			// Dragging a collection to a different library
   1740 			else {
   1741 				// Allow cross-library drag only to root library and collections
   1742 				if (!treeRow.isLibrary(true) && !treeRow.isCollection()) {
   1743 					return false;
   1744 				}
   1745 			}
   1746 			
   1747 			return true;
   1748 		}
   1749 	}
   1750 	return false;
   1751 };
   1752 
   1753 
   1754 /**
   1755  * Perform additional asynchronous drop checks
   1756  *
   1757  * Called by treechildren.drop()
   1758  */
   1759 Zotero.CollectionTreeView.prototype.canDropCheckAsync = Zotero.Promise.coroutine(function* (row, orient, dataTransfer) {
   1760 	//Zotero.debug("Row is " + row + "; orient is " + orient);
   1761 	
   1762 	var dragData = Zotero.DragDrop.getDataFromDataTransfer(dataTransfer);
   1763 	if (!dragData) {
   1764 		Zotero.debug("No drag data");
   1765 		return false;
   1766 	}
   1767 	var dataType = dragData.dataType;
   1768 	var data = dragData.data;
   1769 	
   1770 	if (orient == 0) {
   1771 		var treeRow = this.getRow(row); //the collection we are dragging over
   1772 		
   1773 		if (dataType == 'zotero/item' && treeRow.isBucket()) {
   1774 			return true;
   1775 		}
   1776 		
   1777 		if (dataType == 'zotero/item') {
   1778 			var ids = data;
   1779 			var items = Zotero.Items.get(ids);
   1780 			var skip = true;
   1781 			for (let i=0; i<items.length; i++) {
   1782 				let item = items[i];
   1783 				
   1784 				// Cross-library drag
   1785 				if (treeRow.ref.libraryID != item.libraryID) {
   1786 					let linkedItem = yield item.getLinkedItem(treeRow.ref.libraryID, true);
   1787 					if (linkedItem && !linkedItem.deleted) {
   1788 						// For drag to root, skip if linked item exists
   1789 						if (treeRow.isLibrary(true)) {
   1790 							Zotero.debug("Linked item " + linkedItem.key + " already exists "
   1791 								+ "in library " + treeRow.ref.libraryID);
   1792 							continue;
   1793 						}
   1794 						// For drag to collection
   1795 						else if (treeRow.isCollection()) {
   1796 							// skip if linked item is already in it
   1797 							if (treeRow.ref.hasItem(linkedItem.id)) {
   1798 								Zotero.debug("Linked item " + linkedItem.key + " already exists "
   1799 									+ "in collection");
   1800 								continue;
   1801 							}
   1802 							// or if linked item is a child item
   1803 							else if (!linkedItem.isTopLevelItem()) {
   1804 								Zotero.debug("Linked item " + linkedItem.key + " already exists "
   1805 									+ "as child item");
   1806 								continue;
   1807 							}
   1808 						}
   1809 					}
   1810 					skip = false;
   1811 					continue;
   1812 				}
   1813 				
   1814 				// Intra-library drags have already been vetted by canDrop(). This 'break' should be
   1815 				// changed to a 'continue' if any asynchronous checks that stop the drag are added above
   1816 				skip = false;
   1817 				break;
   1818 			}
   1819 			if (skip) {
   1820 				Zotero.debug("Drag skipped");
   1821 				return false;
   1822 			}
   1823 		}
   1824 		else if (dataType == 'zotero/collection') {
   1825 			let draggedCollectionID = data[0];
   1826 			let draggedCollection = Zotero.Collections.get(draggedCollectionID);
   1827 			
   1828 			// Dragging a collection to a different library
   1829 			if (treeRow.ref.libraryID != draggedCollection.libraryID) {
   1830 				// Disallow if linked collection already exists
   1831 				if (yield draggedCollection.getLinkedCollection(treeRow.ref.libraryID, true)) {
   1832 					Zotero.debug("Linked collection already exists in library");
   1833 					return false;
   1834 				}
   1835 				
   1836 				let descendents = draggedCollection.getDescendents(false, 'collection');
   1837 				for (let descendent of descendents) {
   1838 					descendent = Zotero.Collections.get(descendent.id);
   1839 					// Disallow if linked collection already exists for any subcollections
   1840 					//
   1841 					// If this is allowed in the future for the root collection,
   1842 					// need to allow drag only to root
   1843 					if (yield descendent.getLinkedCollection(treeRow.ref.libraryID, true)) {
   1844 						Zotero.debug("Linked subcollection already exists in library");
   1845 						return false;
   1846 					}
   1847 				}
   1848 			}
   1849 		}
   1850 	}
   1851 	return true;
   1852 });
   1853 
   1854 
   1855 /*
   1856  *  Called when something's been dropped on or next to a row
   1857  */
   1858 Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (row, orient, dataTransfer)
   1859 {
   1860 	if (!this.canDrop(row, orient, dataTransfer)
   1861 			|| !(yield this.canDropCheckAsync(row, orient, dataTransfer))) {
   1862 		return false;
   1863 	}
   1864 	
   1865 	var dragData = Zotero.DragDrop.getDataFromDataTransfer(dataTransfer);
   1866 	if (!dragData) {
   1867 		Zotero.debug("No drag data");
   1868 		return false;
   1869 	}
   1870 	var dropEffect = dragData.dropEffect;
   1871 	var dataType = dragData.dataType;
   1872 	var data = dragData.data;
   1873 	var event = Zotero.DragDrop.currentEvent;
   1874 	var sourceTreeRow = Zotero.DragDrop.getDragSource(dataTransfer);
   1875 	var targetTreeRow = Zotero.DragDrop.getDragTarget(event);
   1876 	
   1877 	var copyOptions = {
   1878 		tags: Zotero.Prefs.get('groups.copyTags'),
   1879 		childNotes: Zotero.Prefs.get('groups.copyChildNotes'),
   1880 		childLinks: Zotero.Prefs.get('groups.copyChildLinks'),
   1881 		childFileAttachments: Zotero.Prefs.get('groups.copyChildFileAttachments')
   1882 	};
   1883 	var copyItem = Zotero.Promise.coroutine(function* (item, targetLibraryID, options) {
   1884 		var targetLibraryType = Zotero.Libraries.get(targetLibraryID).libraryType;
   1885 		
   1886 		// Check if there's already a copy of this item in the library
   1887 		var linkedItem = yield item.getLinkedItem(targetLibraryID, true);
   1888 		if (linkedItem) {
   1889 			// If linked item is in the trash, undelete it and remove it from collections
   1890 			// (since it shouldn't be restored to previous collections)
   1891 			if (linkedItem.deleted) {
   1892 				linkedItem.setCollections();
   1893 				linkedItem.deleted = false;
   1894 				yield linkedItem.save({
   1895 					skipSelect: true
   1896 				});
   1897 			}
   1898 			return linkedItem.id;
   1899 			
   1900 			/*
   1901 			// TODO: support tags, related, attachments, etc.
   1902 			
   1903 			// Overlay source item fields on unsaved clone of linked item
   1904 			var newItem = item.clone(false, linkedItem.clone(true));
   1905 			newItem.setField('dateAdded', item.dateAdded);
   1906 			newItem.setField('dateModified', item.dateModified);
   1907 			
   1908 			var diff = newItem.diff(linkedItem, false, ["dateAdded", "dateModified"]);
   1909 			if (!diff) {
   1910 				// Check if creators changed
   1911 				var creatorsChanged = false;
   1912 				
   1913 				var creators = item.getCreators();
   1914 				var linkedCreators = linkedItem.getCreators();
   1915 				if (creators.length != linkedCreators.length) {
   1916 					Zotero.debug('Creators have changed');
   1917 					creatorsChanged = true;
   1918 				}
   1919 				else {
   1920 					for (var i=0; i<creators.length; i++) {
   1921 						if (!creators[i].ref.equals(linkedCreators[i].ref)) {
   1922 							Zotero.debug('changed');
   1923 							creatorsChanged = true;
   1924 							break;
   1925 						}
   1926 					}
   1927 				}
   1928 				if (!creatorsChanged) {
   1929 					Zotero.debug("Linked item hasn't changed -- skipping conflict resolution");
   1930 					continue;
   1931 				}
   1932 			}
   1933 			toReconcile.push([newItem, linkedItem]);
   1934 			continue;
   1935 			*/
   1936 		}
   1937 		
   1938 		// Standalone attachment
   1939 		if (item.isAttachment()) {
   1940 			var linkMode = item.attachmentLinkMode;
   1941 			
   1942 			// Skip linked files
   1943 			if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
   1944 				Zotero.debug("Skipping standalone linked file attachment on drag");
   1945 				return false;
   1946 			}
   1947 			
   1948 			if (!targetTreeRow.filesEditable) {
   1949 				Zotero.debug("Skipping standalone file attachment on drag");
   1950 				return false;
   1951 			}
   1952 			
   1953 			return Zotero.Attachments.copyAttachmentToLibrary(item, targetLibraryID);
   1954 		}
   1955 		
   1956 		// Create new clone item in target library
   1957 		var newItem = item.clone(targetLibraryID, { skipTags: !options.tags });
   1958 		
   1959 		var newItemID = yield newItem.save({
   1960 			skipSelect: true
   1961 		});
   1962 		
   1963 		// Record link
   1964 		yield newItem.addLinkedItem(item);
   1965 		
   1966 		if (item.isNote()) {
   1967 			return newItemID;
   1968 		}
   1969 		
   1970 		// For regular items, add child items if prefs and permissions allow
   1971 		
   1972 		// Child notes
   1973 		if (options.childNotes) {
   1974 			var noteIDs = item.getNotes();
   1975 			var notes = Zotero.Items.get(noteIDs);
   1976 			for (let note of notes) {
   1977 				let newNote = note.clone(targetLibraryID, { skipTags: !options.tags });
   1978 				newNote.parentID = newItemID;
   1979 				yield newNote.save({
   1980 					skipSelect: true
   1981 				})
   1982 				
   1983 				yield newNote.addLinkedItem(note);
   1984 			}
   1985 		}
   1986 		
   1987 		// Child attachments
   1988 		if (options.childLinks || options.childFileAttachments) {
   1989 			var attachmentIDs = item.getAttachments();
   1990 			var attachments = Zotero.Items.get(attachmentIDs);
   1991 			for (let attachment of attachments) {
   1992 				var linkMode = attachment.attachmentLinkMode;
   1993 				
   1994 				// Skip linked files
   1995 				if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
   1996 					Zotero.debug("Skipping child linked file attachment on drag");
   1997 					continue;
   1998 				}
   1999 				
   2000 				// Skip imported files if we don't have pref and permissions
   2001 				if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
   2002 					if (!options.childLinks) {
   2003 						Zotero.debug("Skipping child link attachment on drag");
   2004 						continue;
   2005 					}
   2006 				}
   2007 				else {
   2008 					if (!options.childFileAttachments
   2009 							|| (!targetTreeRow.filesEditable && !targetTreeRow.isPublications())) {
   2010 						Zotero.debug("Skipping child file attachment on drag");
   2011 						continue;
   2012 					}
   2013 				}
   2014 				yield Zotero.Attachments.copyAttachmentToLibrary(attachment, targetLibraryID, newItemID);
   2015 			}
   2016 		}
   2017 		
   2018 		return newItemID;
   2019 	});
   2020 	
   2021 	var targetLibraryID = targetTreeRow.ref.libraryID;
   2022 	var targetCollectionID = targetTreeRow.isCollection() ? targetTreeRow.ref.id : false;
   2023 	
   2024 	if (dataType == 'zotero/collection') {
   2025 		var droppedCollection = yield Zotero.Collections.getAsync(data[0]);
   2026 		
   2027 		// Collection drag between libraries
   2028 		if (targetLibraryID != droppedCollection.libraryID) {
   2029 			yield Zotero.DB.executeTransaction(function* () {
   2030 				var copyCollections = Zotero.Promise.coroutine(function* (descendents, parentID, addItems) {
   2031 					for (var desc of descendents) {
   2032 						// Collections
   2033 						if (desc.type == 'collection') {
   2034 							var c = yield Zotero.Collections.getAsync(desc.id);
   2035 							
   2036 							var newCollection = new Zotero.Collection;
   2037 							newCollection.libraryID = targetLibraryID;
   2038 							c.clone(false, newCollection);
   2039 							if (parentID) {
   2040 								newCollection.parentID = parentID;
   2041 							}
   2042 							var collectionID = yield newCollection.save();
   2043 							
   2044 							// Record link
   2045 							yield c.addLinkedCollection(newCollection);
   2046 							
   2047 							// Recursively copy subcollections
   2048 							if (desc.children.length) {
   2049 								yield copyCollections(desc.children, collectionID, addItems);
   2050 							}
   2051 						}
   2052 						// Items
   2053 						else {
   2054 							var item = yield Zotero.Items.getAsync(desc.id);
   2055 							var id = yield copyItem(item, targetLibraryID, copyOptions);
   2056 							// Standalone attachments might not get copied
   2057 							if (!id) {
   2058 								continue;
   2059 							}
   2060 							// Mark copied item for adding to collection
   2061 							if (parentID) {
   2062 								if (!addItems[parentID]) {
   2063 									addItems[parentID] = [];
   2064 								}
   2065 								
   2066 								// If source item is a top-level non-regular item (which can exist in a
   2067 								// collection) but target item is a child item (which can't), add
   2068 								// target item's parent to collection instead
   2069 								if (!item.isRegularItem()) {
   2070 									let targetItem = yield Zotero.Items.getAsync(id);
   2071 									let targetItemParentID = targetItem.parentItemID;
   2072 									if (targetItemParentID) {
   2073 										id = targetItemParentID;
   2074 									}
   2075 								}
   2076 								
   2077 								addItems[parentID].push(id);
   2078 							}
   2079 						}
   2080 					}
   2081 				});
   2082 				
   2083 				var collections = [{
   2084 					id: droppedCollection.id,
   2085 					children: droppedCollection.getDescendents(true),
   2086 					type: 'collection'
   2087 				}];
   2088 				
   2089 				var addItems = {};
   2090 				yield copyCollections(collections, targetCollectionID, addItems);
   2091 				for (var collectionID in addItems) {
   2092 					var collection = yield Zotero.Collections.getAsync(collectionID);
   2093 					yield collection.addItems(addItems[collectionID]);
   2094 				}
   2095 				
   2096 				// TODO: add subcollections and subitems, if they don't already exist,
   2097 				// and display a warning if any of the subcollections already exist
   2098 			});
   2099 		}
   2100 		// Collection drag within a library
   2101 		else {
   2102 			droppedCollection.parentID = targetCollectionID;
   2103 			yield droppedCollection.saveTx();
   2104 		}
   2105 	}
   2106 	else if (dataType == 'zotero/item') {
   2107 		var ids = data;
   2108 		if (ids.length < 1) {
   2109 			return;
   2110 		}
   2111 		
   2112 		if (targetTreeRow.isBucket()) {
   2113 			targetTreeRow.ref.uploadItems(ids);
   2114 			return;
   2115 		}
   2116 		
   2117 		var items = yield Zotero.Items.getAsync(ids);
   2118 		if (items.length == 0) {
   2119 			return;
   2120 		}
   2121 		
   2122 		if (items[0] instanceof Zotero.FeedItem) {
   2123 			if (!(targetTreeRow.isCollection() || targetTreeRow.isLibrary() || targetTreeRow.isGroup())) {
   2124 				return;
   2125 			}
   2126 			
   2127 			let promises = [];
   2128 			for (let item of items) {
   2129 				// No transaction, because most time is spent traversing urls
   2130 				promises.push(item.translate(targetLibraryID, targetCollectionID))
   2131 			}
   2132 			return Zotero.Promise.all(promises);	
   2133 		}
   2134 		
   2135 		if (targetTreeRow.isPublications()) {
   2136 			items = Zotero.Items.keepParents(items);
   2137 			let io = this._treebox.treeBody.ownerDocument.defaultView
   2138 				.ZoteroPane.showPublicationsWizard(items);
   2139 			if (!io) {
   2140 				return;
   2141 			}
   2142 			copyOptions.childNotes = io.includeNotes;
   2143 			copyOptions.childFileAttachments = io.includeFiles;
   2144 			copyOptions.childLinks = true;
   2145 			['keepRights', 'license', 'licenseName'].forEach(function (field) {
   2146 				copyOptions[field] = io[field];
   2147 			});
   2148 		}
   2149 		
   2150 		let newItems = [];
   2151 		let newIDs = [];
   2152 		let toMove = [];
   2153 		// TODO: support items coming from different sources?
   2154 		let sameLibrary = items[0].libraryID == targetLibraryID
   2155 		
   2156 		for (let item of items) {
   2157 			if (!item.isTopLevelItem()) {
   2158 				continue;
   2159 			}
   2160 			
   2161 			newItems.push(item);
   2162 			
   2163 			if (sameLibrary) {
   2164 				newIDs.push(item.id);
   2165 				toMove.push(item.id);
   2166 			}
   2167 		}
   2168 		
   2169 		if (!sameLibrary) {
   2170 			let toReconcile = [];
   2171 			
   2172 			yield Zotero.DB.executeTransaction(function* () {
   2173 				for (let item of newItems) {
   2174 					var id = yield copyItem(item, targetLibraryID, copyOptions)
   2175 					// Standalone attachments might not get copied
   2176 					if (!id) {
   2177 						continue;
   2178 					}
   2179 					newIDs.push(id);
   2180 				}
   2181 			});
   2182 			
   2183 			if (toReconcile.length) {
   2184 				let sourceName = Zotero.Libraries.getName(items[0].libraryID);
   2185 				let targetName = Zotero.Libraries.getName(targetLibraryID);
   2186 				
   2187 				let io = {
   2188 					dataIn: {
   2189 						type: "item",
   2190 						captions: [
   2191 							// TODO: localize
   2192 							sourceName,
   2193 							targetName,
   2194 							"Merged Item"
   2195 						],
   2196 						objects: toReconcile
   2197 					}
   2198 				};
   2199 				
   2200 				/*
   2201 				if (type == 'item') {
   2202 					if (!Zotero.Utilities.isEmpty(changedCreators)) {
   2203 						io.dataIn.changedCreators = changedCreators;
   2204 					}
   2205 				}
   2206 				*/
   2207 				
   2208 				let lastWin = Services.wm.getMostRecentWindow("navigator:browser");
   2209 				lastWin.openDialog('chrome://zotero/content/merge.xul', '', 'chrome,modal,centerscreen', io);
   2210 				
   2211 				yield Zotero.DB.executeTransaction(function* () {
   2212 					// DEBUG: This probably needs to be updated if this starts being used
   2213 					for (let obj of io.dataOut) {
   2214 						yield obj.ref.save();
   2215 					}
   2216 				});
   2217 			}
   2218 		}
   2219 		
   2220 		// Add items to target collection
   2221 		if (targetCollectionID) {
   2222 			let ids = newIDs.filter(itemID => Zotero.Items.get(itemID).isTopLevelItem());
   2223 			yield Zotero.DB.executeTransaction(function* () {
   2224 				let collection = yield Zotero.Collections.getAsync(targetCollectionID);
   2225 				yield collection.addItems(ids);
   2226 			}.bind(this));
   2227 		}
   2228 		else if (targetTreeRow.isPublications()) {
   2229 			yield Zotero.Items.addToPublications(newItems, copyOptions);
   2230 		}
   2231 		
   2232 		// If moving, remove items from source collection
   2233 		if (dropEffect == 'move' && toMove.length) {
   2234 			if (!sameLibrary) {
   2235 				throw new Error("Cannot move items between libraries");
   2236 			}
   2237 			if (!sourceTreeRow || !sourceTreeRow.isCollection()) {
   2238 				throw new Error("Drag source must be a collection for move action");
   2239 			}
   2240 			yield Zotero.DB.executeTransaction(function* () {
   2241 				yield sourceTreeRow.ref.removeItems(toMove);
   2242 			}.bind(this));
   2243 		}
   2244 	}
   2245 	else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
   2246 		var targetLibraryID = targetTreeRow.ref.libraryID;
   2247 		if (targetTreeRow.isCollection()) {
   2248 			var parentCollectionID = targetTreeRow.ref.id;
   2249 		}
   2250 		else {
   2251 			var parentCollectionID = false;
   2252 		}
   2253 		var addedItems = [];
   2254 		
   2255 		for (var i=0; i<data.length; i++) {
   2256 			var file = data[i];
   2257 			
   2258 			if (dataType == 'text/x-moz-url') {
   2259 				var url = data[i];
   2260 				let item;
   2261 				
   2262 				if (url.indexOf('file:///') == 0) {
   2263 					let win = Services.wm.getMostRecentWindow("navigator:browser");
   2264 					// If dragging currently loaded page, only convert to
   2265 					// file if not an HTML document
   2266 					if (win.content.location.href != url ||
   2267 							win.content.document.contentType != 'text/html') {
   2268 						var nsIFPH = Components.classes["@mozilla.org/network/protocol;1?name=file"]
   2269 								.getService(Components.interfaces.nsIFileProtocolHandler);
   2270 						try {
   2271 							var file = nsIFPH.getFileFromURLSpec(url);
   2272 						}
   2273 						catch (e) {
   2274 							Zotero.debug(e);
   2275 						}
   2276 					}
   2277 				}
   2278 				
   2279 				// Still string, so remote URL
   2280 				if (typeof file == 'string') {
   2281 					let win = Services.wm.getMostRecentWindow("navigator:browser");
   2282 					win.ZoteroPane.addItemFromURL(url, 'temporaryPDFHack', null, row); // TODO: don't do this
   2283 					continue;
   2284 				}
   2285 				
   2286 				// Otherwise file, so fall through
   2287 			}
   2288 			
   2289 			if (dropEffect == 'link') {
   2290 				item = yield Zotero.Attachments.linkFromFile({
   2291 					file: file,
   2292 					collections: parentCollectionID ? [parentCollectionID] : undefined
   2293 				});
   2294 			}
   2295 			else {
   2296 				item = yield Zotero.Attachments.importFromFile({
   2297 					file: file,
   2298 					libraryID: targetLibraryID,
   2299 					collections: parentCollectionID ? [parentCollectionID] : undefined
   2300 				});
   2301 				// If moving, delete original file
   2302 				if (dragData.dropEffect == 'move') {
   2303 					try {
   2304 						file.remove(false);
   2305 					}
   2306 					catch (e) {
   2307 						Components.utils.reportError("Error deleting original file " + file.path + " after drag");
   2308 					}
   2309 				}
   2310 			}
   2311 			
   2312 			addedItems.push(item);
   2313 		}
   2314 		
   2315 		// Automatically retrieve metadata for PDFs
   2316 		Zotero.RecognizePDF.autoRecognizeItems(addedItems);
   2317 	}
   2318 });
   2319 
   2320 
   2321 
   2322 ////////////////////////////////////////////////////////////////////////////////
   2323 ///
   2324 ///  Functions for nsITreeView that we have to stub out.
   2325 ///
   2326 ////////////////////////////////////////////////////////////////////////////////
   2327 
   2328 Zotero.CollectionTreeView.prototype.isSorted = function() 							{ return false; }
   2329 
   2330 /* Set 'highlighted' property on rows set by setHighlightedRows */
   2331 Zotero.CollectionTreeView.prototype.getRowProperties = function(row, prop) {
   2332 	var props = [];
   2333 	
   2334 	var treeRow = this.getRow(row);
   2335 	if (treeRow.isHeader()) {
   2336 		props.push("header");
   2337 	}
   2338 	else if (this._highlightedRows[row]) {
   2339 		props.push("highlighted");
   2340 	}
   2341 	
   2342 	return props.join(" ");
   2343 }
   2344 
   2345 Zotero.CollectionTreeView.prototype.getColumnProperties = function(col, prop) {}
   2346 
   2347 Zotero.CollectionTreeView.prototype.getCellProperties = function(row, col, prop) {
   2348 	var props = [];
   2349 	
   2350 	var treeRow = this.getRow(row);
   2351 	if (treeRow.isHeader()) {
   2352 		props.push("header");
   2353 		props.push("notwisty");
   2354 	}
   2355 	else if (treeRow.ref && treeRow.ref.unreadCount) {
   2356 		props.push('unread');
   2357 	}
   2358 	
   2359 	return props.join(" ");
   2360 
   2361 }
   2362 Zotero.CollectionTreeView.prototype.isSeparator = function(index) {
   2363 	var source = this.getRow(index);
   2364 	return source.type == 'separator';
   2365 }
   2366 Zotero.CollectionTreeView.prototype.performAction = function(action) 				{ }
   2367 Zotero.CollectionTreeView.prototype.performActionOnCell = function(action, row, col)	{ }
   2368 Zotero.CollectionTreeView.prototype.getProgressMode = function(row, col) 			{ }
   2369 Zotero.CollectionTreeView.prototype.cycleHeader = function(column)					{ }
   2370 
   2371 
   2372 Zotero.CollectionTreeCache = {
   2373 	"lastTreeRow":null,
   2374 	"lastTempTable":null,
   2375 	"lastSearch":null,
   2376 	"lastResults":null,
   2377 	
   2378 	"clear": function () {
   2379 		this.lastTreeRow = null;
   2380 		this.lastSearch = null;
   2381 		if (this.lastTempTable) {
   2382 			let tableName = this.lastTempTable;
   2383 			let id = Zotero.DB.addCallback('commit', async function () {
   2384 				await Zotero.DB.queryAsync("DROP TABLE IF EXISTS " + tableName);
   2385 				Zotero.DB.removeCallback('commit', id);
   2386 			});
   2387 		}
   2388 		this.lastTempTable = null;
   2389 		this.lastResults = null;
   2390 	}
   2391 };