www

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

collection.js (23368B)


      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 Zotero.Collection = function(params = {}) {
     27 	Zotero.Collection._super.apply(this);
     28 	
     29 	this._name = null;
     30 	
     31 	this._childCollections = new Set();
     32 	this._childItems = new Set();
     33 	
     34 	Zotero.Utilities.assignProps(this, params, ['name', 'libraryID', 'parentID',
     35 		'parentKey', 'lastSync']);
     36 }
     37 
     38 Zotero.extendClass(Zotero.DataObject, Zotero.Collection);
     39 
     40 Zotero.Collection.prototype._objectType = 'collection';
     41 Zotero.Collection.prototype._dataTypes = Zotero.Collection._super.prototype._dataTypes.concat([
     42 	'childCollections',
     43 	'childItems',
     44 	'relations'
     45 ]);
     46 
     47 Zotero.defineProperty(Zotero.Collection.prototype, 'ChildObjects', {
     48 	get: function() { return Zotero.Items; }
     49 });
     50 
     51 Zotero.defineProperty(Zotero.Collection.prototype, 'id', {
     52 	get: function() { return this._get('id'); },
     53 	set: function(val) { return this._set('id', val); }
     54 });
     55 Zotero.defineProperty(Zotero.Collection.prototype, 'libraryID', {
     56 	get: function() { return this._get('libraryID'); },
     57 	set: function(val) { return this._set('libraryID', val); }
     58 });
     59 Zotero.defineProperty(Zotero.Collection.prototype, 'key', {
     60 	get: function() { return this._get('key'); },
     61 	set: function(val) { return this._set('key', val); }
     62 });
     63 Zotero.defineProperty(Zotero.Collection.prototype, 'name', {
     64 	get: function() { return this._get('name'); },
     65 	set: function(val) { return this._set('name', val); }
     66 });
     67 Zotero.defineProperty(Zotero.Collection.prototype, 'version', {
     68 	get: function() { return this._get('version'); },
     69 	set: function(val) { return this._set('version', val); }
     70 });
     71 Zotero.defineProperty(Zotero.Collection.prototype, 'synced', {
     72 	get: function() { return this._get('synced'); },
     73 	set: function(val) { return this._set('synced', val); }
     74 });
     75 Zotero.defineProperty(Zotero.Collection.prototype, 'parent', {
     76 	get: function() {
     77 		Zotero.debug("WARNING: Zotero.Collection.prototype.parent has been deprecated -- use .parentID or .parentKey", 2);
     78 		return this.parentID;
     79 	},
     80 	set: function(val) {
     81 		Zotero.debug("WARNING: Zotero.Collection.prototype.parent has been deprecated -- use .parentID or .parentKey", 2);
     82 		this.parentID = val;
     83 	},
     84 	enumerable: false
     85 });
     86 
     87 Zotero.defineProperty(Zotero.Collection.prototype, 'treeViewID', {
     88 	get: function () {
     89 		return "C" + this.id
     90 	}
     91 });
     92 
     93 Zotero.defineProperty(Zotero.Collection.prototype, 'treeViewImage', {
     94 	get: function () {
     95 		// Keep in sync with collectionTreeView::getImageSrc()
     96 		if (Zotero.isMac) {
     97 			return `chrome://zotero-platform/content/treesource-collection${Zotero.hiDPISuffix}.png`;
     98 		}
     99 		return "chrome://zotero/skin/treesource-collection" + Zotero.hiDPISuffix + ".png";
    100 	}
    101 });
    102 
    103 Zotero.Collection.prototype.getID = function() {
    104 	Zotero.debug('Collection.getID() deprecated -- use Collection.id');
    105 	return this.id;
    106 }
    107 
    108 Zotero.Collection.prototype.getName = function() {
    109 	Zotero.debug('Collection.getName() deprecated -- use Collection.name');
    110 	return this.name;
    111 }
    112 
    113 
    114 /*
    115  * Populate collection data from a database row
    116  */
    117 Zotero.Collection.prototype.loadFromRow = function(row) {
    118 	var primaryFields = this._ObjectsClass.primaryFields;
    119 	for (let i=0; i<primaryFields.length; i++) {
    120 		let col = primaryFields[i];
    121 		try {
    122 			var val = row[col];
    123 		}
    124 		catch (e) {
    125 			Zotero.debug('Skipping missing ' + this._objectType + ' field ' + col);
    126 			continue;
    127 		}
    128 		
    129 		switch (col) {
    130 		case this._ObjectsClass.idColumn:
    131 			col = 'id';
    132 			break;
    133 		
    134 		// Integer
    135 		case 'libraryID':
    136 			val = parseInt(val);
    137 			break;
    138 		
    139 		// Integer or 0
    140 		case 'version':
    141 			val = val ? parseInt(val) : 0;
    142 			break;
    143 		
    144 		// Value or false
    145 		case 'parentKey':
    146 			val = val || false;
    147 			break;
    148 		
    149 		// Integer or false if falsy
    150 		case 'parentID':
    151 			val = val ? parseInt(val) : false;
    152 			break;
    153 		
    154 		// Boolean
    155 		case 'synced':
    156 		case 'hasChildCollections':
    157 		case 'hasChildItems':
    158 			val = !!val;
    159 			break;
    160 		
    161 		default:
    162 			val = val || '';
    163 		}
    164 		
    165 		this['_' + col] = val;
    166 	}
    167 	
    168 	this._childCollectionsLoaded = false;
    169 	this._childItemsLoaded = false;
    170 	
    171 	this._loaded.primaryData = true;
    172 	this._clearChanged('primaryData');
    173 	this._identified = true;
    174 }
    175 
    176 
    177 Zotero.Collection.prototype.hasChildCollections = function() {
    178 	this._requireData('childCollections');
    179 	return this._childCollections.size > 0;
    180 }
    181 
    182 Zotero.Collection.prototype.hasChildItems = function() {
    183 	this._requireData('childItems');
    184 	return this._childItems.size > 0;
    185 }
    186 
    187 
    188 /**
    189  * Returns subcollections of this collection
    190  *
    191  * @param {Boolean} [asIDs=false] Return as collectionIDs
    192  * @return {Zotero.Collection[]|Integer[]}
    193  */
    194 Zotero.Collection.prototype.getChildCollections = function (asIDs) {
    195 	this._requireData('childCollections');
    196 	
    197 	// Return collectionIDs
    198 	if (asIDs) {
    199 		return this._childCollections.values();
    200 	}
    201 	
    202 	// Return Zotero.Collection objects
    203 	return Array.from(this._childCollections).map(id => this.ObjectsClass.get(id));
    204 }
    205 
    206 
    207 /**
    208  * Returns child items of this collection
    209  *
    210  * @param	{Boolean}	asIDs			Return as itemIDs
    211  * @param	{Boolean}	includeDeleted	Include items in Trash
    212  * @return {Zotero.Item[]|Integer[]} - Array of Zotero.Item instances or itemIDs
    213  */
    214 Zotero.Collection.prototype.getChildItems = function (asIDs, includeDeleted) {
    215 	this._requireData('childItems');
    216 	
    217 	if (this._childItems.size == 0) {
    218 		return [];
    219 	}
    220 	
    221 	// Remove deleted items if necessary
    222 	var childItems = [];
    223 	for (let itemID of this._childItems) {
    224 		let item = this.ChildObjects.get(itemID);
    225 		if (includeDeleted || !item.deleted) {
    226 			childItems.push(item);
    227 		}
    228 	}
    229 	
    230 	// Return itemIDs
    231 	if (asIDs) {
    232 		return childItems.map(item => item.id);
    233 	}
    234 	
    235 	// Return Zotero.Item objects
    236 	return childItems.slice();
    237 }
    238 
    239 Zotero.Collection.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
    240 	if (!this.name) {
    241 		throw new Error(this._ObjectType + ' name is empty');
    242 	}
    243 	
    244 	var proceed = yield Zotero.Collection._super.prototype._initSave.apply(this, arguments);
    245 	if (!proceed) return false;
    246 	
    247 		// Verify parent
    248 	if (this._parentKey) {
    249 		let newParent = yield this.ObjectsClass.getByLibraryAndKeyAsync(
    250 			this.libraryID, this._parentKey
    251 		);
    252 		
    253 		if (!newParent) {
    254 			throw new Error("Cannot set parent to invalid collection " + this._parentKey);
    255 		}
    256 		
    257 		if (newParent.id == this.id) {
    258 			throw new Error('Cannot move collection into itself!');
    259 		}
    260 		
    261 		if (this.id && this.hasDescendent('collection', newParent.id)) {
    262 			throw ('Cannot move collection "' + this.name + '" into one of its own descendents');
    263 		}
    264 		
    265 		env.parent = newParent.id;
    266 	}
    267 	else {
    268 		env.parent = null;
    269 	}
    270 	
    271 	return true;
    272 });
    273 
    274 Zotero.Collection.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
    275 	var isNew = env.isNew;
    276 	var options = env.options;
    277 
    278 	var collectionID = this._id = this.id ? this.id : Zotero.ID.get('collections');
    279 	
    280 	Zotero.debug("Saving collection " + this.id);
    281 	
    282 	env.sqlColumns.push(
    283 		'collectionName',
    284 		'parentCollectionID'
    285 	);
    286 	env.sqlValues.push(
    287 		{ string: this.name },
    288 		env.parent ? env.parent : null
    289 	);
    290 	
    291 	if (env.sqlColumns.length) {
    292 		if (isNew) {
    293 			env.sqlColumns.unshift('collectionID');
    294 			env.sqlValues.unshift(collectionID ? { int: collectionID } : null);
    295 			
    296 			let placeholders = env.sqlColumns.map(() => '?').join();
    297 			let sql = "INSERT INTO collections (" + env.sqlColumns.join(', ') + ") "
    298 				+ "VALUES (" + placeholders + ")";
    299 			yield Zotero.DB.queryAsync(sql, env.sqlValues);
    300 		}
    301 		else {
    302 			let sql = 'UPDATE collections SET '
    303 				+ env.sqlColumns.map(x => x + '=?').join(', ') + ' WHERE collectionID=?';
    304 			env.sqlValues.push(collectionID ? { int: collectionID } : null);
    305 			yield Zotero.DB.queryAsync(sql, env.sqlValues);
    306 		}
    307 	}
    308 	
    309 	if (this._changed.parentKey) {
    310 		// Add this item to the parent's cached item lists after commit,
    311 		// if the parent was loaded
    312 		if (this.parentKey) {
    313 			let parentCollectionID = this.ObjectsClass.getIDFromLibraryAndKey(
    314 				this.libraryID, this.parentKey
    315 			);
    316 			Zotero.DB.addCurrentCallback("commit", function () {
    317 				this.ObjectsClass.registerChildCollection(parentCollectionID, collectionID);
    318 			}.bind(this));
    319 		}
    320 		// Remove this from the previous parent's cached collection lists after commit,
    321 		// if the parent was loaded
    322 		if (!isNew && this._previousData.parentKey) {
    323 			let parentCollectionID = this.ObjectsClass.getIDFromLibraryAndKey(
    324 				this.libraryID, this._previousData.parentKey
    325 			);
    326 			Zotero.DB.addCurrentCallback("commit", function () {
    327 				this.ObjectsClass.unregisterChildCollection(parentCollectionID, collectionID);
    328 			}.bind(this));
    329 		}
    330 	}
    331 });
    332 
    333 Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
    334 	if (!env.options.skipNotifier) {
    335 		if (env.isNew) {
    336 			Zotero.Notifier.queue(
    337 				'add', 'collection', this.id, env.notifierData, env.options.notifierQueue
    338 			);
    339 		}
    340 		else {
    341 			Zotero.Notifier.queue(
    342 				'modify', 'collection', this.id, env.notifierData, env.options.notifierQueue
    343 			);
    344 		}
    345 	}
    346 	
    347 	if (!env.skipCache) {
    348 		yield this.reload();
    349 		// If new, there's no other data we don't have, so we can mark everything as loaded
    350 		if (env.isNew) {
    351 			this._markAllDataTypeLoadStates(true);
    352 		}
    353 		this._clearChanged();
    354 	}
    355 	
    356 	if (env.isNew) {
    357 		yield Zotero.Libraries.get(this.libraryID).updateCollections();
    358 	}
    359 	
    360 	return env.isNew ? this.id : true;
    361 });
    362 
    363 
    364 /**
    365  * @param {Number} itemID
    366  * @return {Promise}
    367  */
    368 Zotero.Collection.prototype.addItem = function (itemID, options) {
    369 	return this.addItems([itemID], options);
    370 }
    371 
    372 
    373 /**
    374  * Add multiple items to the collection in batch
    375  *
    376  * Requires a transaction
    377  * Does not require a separate save()
    378  *
    379  * @param {Number[]} itemIDs
    380  * @return {Promise}
    381  */
    382 Zotero.Collection.prototype.addItems = Zotero.Promise.coroutine(function* (itemIDs, options = {}) {
    383 	options.skipDateModifiedUpdate = true;
    384 
    385 	if (!itemIDs || !itemIDs.length) {
    386 		return;
    387 	}
    388 	
    389 	var current = this.getChildItems(true);
    390 	
    391 	Zotero.DB.requireTransaction();
    392 	for (let i = 0; i < itemIDs.length; i++) {
    393 		let itemID = itemIDs[i];
    394 		
    395 		if (current && current.indexOf(itemID) != -1) {
    396 			Zotero.debug("Item " + itemID + " already a child of collection " + this.id);
    397 			continue;
    398 		}
    399 		
    400 		let item = this.ChildObjects.get(itemID);
    401 		item.addToCollection(this.id);
    402 		yield item.save(options);
    403 	}
    404 	
    405 	yield this.loadDataType('childItems');
    406 });
    407 
    408 /**
    409  * Remove a item from the collection. The item is not deleted from the library.
    410  *
    411  * Requires a transaction
    412  * Does not require a separate save()
    413  *
    414  * @return {Promise}
    415  */
    416 Zotero.Collection.prototype.removeItem = function (itemID) {
    417 	return this.removeItems([itemID]);
    418 }
    419 
    420 
    421 /**
    422  * Remove multiple items from the collection in batch.
    423  * The items are not deleted from the library.
    424  *
    425  * Does not require a separate save()
    426  */
    427 Zotero.Collection.prototype.removeItems = Zotero.Promise.coroutine(function* (itemIDs) {
    428 	if (!itemIDs || !itemIDs.length) {
    429 		return;
    430 	}
    431 	
    432 	var current = this.getChildItems(true, true);
    433 	
    434 	Zotero.DB.requireTransaction();
    435 	for (let i=0; i<itemIDs.length; i++) {
    436 		let itemID = itemIDs[i];
    437 		
    438 		if (current.indexOf(itemID) == -1) {
    439 			Zotero.debug("Item " + itemID + " not a child of collection " + this.id);
    440 			continue;
    441 		}
    442 		
    443 		let item = yield this.ChildObjects.getAsync(itemID);
    444 		item.removeFromCollection(this.id);
    445 		yield item.save({
    446 			skipDateModifiedUpdate: true
    447 		})
    448 	}
    449 });
    450 
    451 
    452 /**
    453 * Check if an item belongs to the collection
    454 **/
    455 Zotero.Collection.prototype.hasItem = function(itemID) {
    456 	this._requireData('childItems');
    457 	return this._childItems.has(itemID);
    458 }
    459 
    460 
    461 Zotero.Collection.prototype.hasDescendent = function (type, id) {
    462 	var descendents = this.getDescendents();
    463 	for (var i=0, len=descendents.length; i<len; i++) {
    464 		if (descendents[i].type == type && descendents[i].id == id) {
    465 			return true;
    466 		}
    467 	}
    468 	return false;
    469 };
    470 
    471 
    472 /**
    473  * Compares this collection to another
    474  *
    475  * Returns a two-element array containing two objects with the differing values,
    476  * or FALSE if no differences
    477  *
    478  * @param	{Zotero.Collection}	collection			Zotero.Collection to compare this item to
    479  * @param	{Boolean}		includeMatches			Include all fields, even those that aren't different
    480  */
    481 Zotero.Collection.prototype.diff = function (collection, includeMatches) {
    482 	var diff = [];
    483 	var thisData = this.serialize();
    484 	var otherData = collection.serialize();
    485 	var numDiffs = this.ObjectsClass.diff(thisData, otherData, diff, includeMatches);
    486 	
    487 	// For the moment, just compare children and increase numDiffs if any differences
    488 	var d1 = Zotero.Utilities.arrayDiff(
    489 		thisData.childCollections, otherData.childCollections
    490 	);
    491 	var d2 = Zotero.Utilities.arrayDiff(
    492 		otherData.childCollections, thisData.childCollections
    493 	);
    494 	var d3 = Zotero.Utilities.arrayDiff(
    495 		thisData.childItems, otherData.childItems
    496 	);
    497 	var d4 = Zotero.Utilities.arrayDiff(
    498 		otherData.childItems, thisData.childItems
    499 	);
    500 	numDiffs += d1.length + d2.length;
    501 	
    502 	if (d1.length || d2.length) {
    503 		numDiffs += d1.length + d2.length;
    504 		diff[0].childCollections = d1;
    505 		diff[1].childCollections = d2;
    506 	}
    507 	else {
    508 		diff[0].childCollections = [];
    509 		diff[1].childCollections = [];
    510 	}
    511 	
    512 	if (d3.length || d4.length) {
    513 		numDiffs += d3.length + d4.length;
    514 		diff[0].childItems = d3;
    515 		diff[1].childItems = d4;
    516 	}
    517 	else {
    518 		diff[0].childItems = [];
    519 		diff[1].childItems = [];
    520 	}
    521 	
    522 	if (numDiffs == 0) {
    523 		return false;
    524 	}
    525 	
    526 	return diff;
    527 }
    528 
    529 
    530 /**
    531  * Returns an unsaved copy of the collection
    532  *
    533  * Does not copy parent collection or child items
    534  *
    535  * @param	{Boolean}		[includePrimary=false]
    536  * @param	{Zotero.Collection} [newCollection=null]
    537  */
    538 Zotero.Collection.prototype.clone = function (includePrimary, newCollection) {
    539 	Zotero.debug('Cloning collection ' + this.id);
    540 	
    541 	if (newCollection) {
    542 		var sameLibrary = newCollection.libraryID == this.libraryID;
    543 	}
    544 	else {
    545 		var newCollection = new this.constructor;
    546 		var sameLibrary = true;
    547 		
    548 		if (includePrimary) {
    549 			newCollection.id = this.id;
    550 			newCollection.libraryID = this.libraryID;
    551 			newCollection.key = this.key;
    552 			
    553 			// TODO: This isn't used, but if it were, it should probably include
    554 			// parent collection and child items
    555 		}
    556 	}
    557 	
    558 	newCollection.name = this.name;
    559 	
    560 	return newCollection;
    561 }
    562 
    563 
    564 /**
    565 * Deletes collection and all descendent collections (and optionally items)
    566 **/
    567 Zotero.Collection.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
    568 	Zotero.DB.requireTransaction();
    569 	
    570 	var collections = [this.id];
    571 	
    572 	var descendents = this.getDescendents(false, null, true);
    573 	var items = [];
    574 	var libraryHasTrash = Zotero.Libraries.hasTrash(this.libraryID);
    575 	
    576 	var del = [];
    577 	var itemsToUpdate = [];
    578 	for(var i=0, len=descendents.length; i<len; i++) {
    579 		// Descendent collections
    580 		if (descendents[i].type == 'collection') {
    581 			collections.push(descendents[i].id);
    582 			var c = yield this.ObjectsClass.getAsync(descendents[i].id);
    583 			if (c) {
    584 				env.notifierData[c.id] = {
    585 					libraryID: c.libraryID,
    586 					key: c.key
    587 				};
    588 			}
    589 		}
    590 		// Descendent items
    591 		else {
    592 			// Trash/delete items
    593 			if (env.options.deleteItems) {
    594 				del.push(descendents[i].id);
    595 			}
    596 			
    597 			// If item isn't being removed or is just moving to the trash, mark for update
    598 			if (!env.options.deleteItems || libraryHasTrash) {
    599 				itemsToUpdate.push(descendents[i].id);
    600 			}
    601 		}
    602 	}
    603 	if (del.length) {
    604 		if (libraryHasTrash) {
    605 			yield this.ChildObjects.trash(del);
    606 		}
    607 		// If library doesn't have trash, just erase
    608 		else {
    609 			Zotero.debug(Zotero.Libraries.getName(this.libraryID) + " library does not have trash. "
    610 				+ this.ChildObjects._ZDO_Objects + " will be erased");
    611 			let options = {};
    612 			Object.assign(options, env.options);
    613 			options.tx = false;
    614 			for (let i=0; i<del.length; i++) {
    615 				let obj = yield this.ChildObjects.getAsync(del[i]);
    616 				yield obj.erase(options);
    617 			}
    618 		}
    619 	}
    620 	
    621 	// Update child collection cache of parent collection
    622 	if (this.parentKey) {
    623 		let parentCollectionID = this.ObjectsClass.getIDFromLibraryAndKey(
    624 			this.libraryID, this.parentKey
    625 		);
    626 		Zotero.DB.addCurrentCallback("commit", function () {
    627 			this.ObjectsClass.unregisterChildCollection(parentCollectionID, this.id);
    628 		}.bind(this));
    629 	}
    630 	
    631 	var placeholders = collections.map(() => '?').join();
    632 	
    633 	// Remove item associations for all descendent collections
    634 	yield Zotero.DB.queryAsync('DELETE FROM collectionItems WHERE collectionID IN '
    635 		+ '(' + placeholders + ')', collections);
    636 	
    637 	// Remove parent definitions first for FK check
    638 	yield Zotero.DB.queryAsync('UPDATE collections SET parentCollectionID=NULL '
    639 		+ 'WHERE parentCollectionID IN (' + placeholders + ')', collections);
    640 	
    641 	// And delete all descendent collections
    642 	yield Zotero.DB.queryAsync ('DELETE FROM collections WHERE collectionID IN '
    643 		+ '(' + placeholders + ')', collections);
    644 	
    645 	env.deletedObjectIDs = collections;
    646 	
    647 	// Update collection cache for descendant items
    648 	if (itemsToUpdate.length) {
    649 		let deletedCollections = new Set(env.deletedObjectIDs);
    650 		itemsToUpdate.forEach(itemID => {
    651 			let item = Zotero.Items.get(itemID);
    652 			item._collections = item._collections.filter(c => !deletedCollections.has(c));
    653 		});
    654 	}
    655 });
    656 
    657 Zotero.Collection.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) {
    658 	yield Zotero.Collection._super.prototype._finalizeErase.call(this, env);
    659 	
    660 	yield Zotero.Libraries.get(this.libraryID).updateCollections();
    661 });
    662 
    663 Zotero.Collection.prototype.isCollection = function() {
    664 	return true;
    665 }
    666 
    667 
    668 Zotero.Collection.prototype.serialize = function(nested) {
    669 	var childCollections = this.getChildCollections(true);
    670 	var childItems = this.getChildItems(true);
    671 	var obj = {
    672 		primary: {
    673 			collectionID: this.id,
    674 			libraryID: this.libraryID,
    675 			key: this.key
    676 		},
    677 		fields: {
    678 			name: this.name,
    679 			parentKey: this.parentKey,
    680 		},
    681 		childCollections: childCollections ? childCollections : [],
    682 		childItems: childItems ? childItems : [],
    683 		descendents: this.id ? this.getDescendents(nested) : []
    684 	};
    685 	return obj;
    686 }
    687 
    688 
    689 /**
    690  * Populate the object's data from an API JSON data object
    691  *
    692  * If this object is identified (has an id or library/key), loadAllData() must have been called.
    693  */
    694 Zotero.Collection.prototype.fromJSON = function (json) {
    695 	if (!json.name) {
    696 		throw new Error("'name' property not provided for collection");
    697 	}
    698 	this.name = json.name;
    699 	this.parentKey = json.parentCollection ? json.parentCollection : false;
    700 	
    701 	this.setRelations(json.relations || {});
    702 }
    703 
    704 
    705 Zotero.Collection.prototype.toJSON = function (options = {}) {
    706 	var env = this._preToJSON(options);
    707 	var mode = env.mode;
    708 	
    709 	var obj = env.obj = {};
    710 	obj.key = this.key;
    711 	obj.version = this.version;
    712 	
    713 	obj.name = this.name;
    714 	obj.parentCollection = this.parentKey ? this.parentKey : false;
    715 	obj.relations = this.getRelations();
    716 	
    717 	return this._postToJSON(env);
    718 }
    719 
    720 
    721 /**
    722  * Returns an array of descendent collections and items
    723  *
    724  * @param	{Boolean}	[nested=false]		Return multidimensional array with 'children'
    725  *											nodes instead of flat array
    726  * @param	{String}	[type]				'item', 'collection', or NULL for both
    727  * @param	{Boolean}	[includeDeletedItems=false]		Include items in Trash
    728  * @return	{Object[]} - An array of objects with 'id', 'key', 'type' ('item' or 'collection'),
    729  *     'parent', and, if collection, 'name' and the nesting 'level'
    730  */
    731 Zotero.Collection.prototype.getDescendents = function (nested, type, includeDeletedItems, level) {
    732 	if (!this.id) {
    733 		throw new Error('Cannot be called on an unsaved item');
    734 	}
    735 	
    736 	if (!level) {
    737 		level = 1;
    738 	}
    739 	
    740 	if (type) {
    741 		switch (type) {
    742 			case 'item':
    743 			case 'collection':
    744 				break;
    745 			default:
    746 				throw new (`Invalid type '${type}'`);
    747 		}
    748 	}
    749 	
    750 	var collections = Zotero.Collections.getByParent(this.id);
    751 	var children = collections.map(c => ({
    752 		id: c.id,
    753 		name: c.name,
    754 		type: 0,
    755 		key: c.key
    756 	}));
    757 	if (!type || type == 'item') {
    758 		let items = this.getChildItems(false, includeDeletedItems);
    759 		children = children.concat(items.map(i => ({
    760 			id: i.id,
    761 			name: null,
    762 			type: 1,
    763 			key: i.key
    764 		})));
    765 	}
    766 	
    767 	children.sort(function (a, b) {
    768 		if (a.name === null || b.name === null) return 0;
    769 		return Zotero.localeCompare(a.name, b.name)
    770 	});
    771 	
    772 	var toReturn = [];
    773 	for(var i=0, len=children.length; i<len; i++) {
    774 		switch (children[i].type) {
    775 			case 0:
    776 				if (!type || type=='collection') {
    777 					toReturn.push({
    778 						id: children[i].id,
    779 						name: children[i].name,
    780 						key: children[i].key,
    781 						type: 'collection',
    782 						level: level,
    783 						parent: this.id
    784 					});
    785 				}
    786 				
    787 				let child = this.ObjectsClass.get(children[i].id);
    788 				let descendents = child.getDescendents(
    789 					nested, type, includeDeletedItems, level + 1
    790 				);
    791 				
    792 				if (nested) {
    793 					toReturn[toReturn.length-1].children = descendents;
    794 				}
    795 				else {
    796 					for (var j=0, len2=descendents.length; j<len2; j++) {
    797 						toReturn.push(descendents[j]);
    798 					}
    799 				}
    800 			break;
    801 			
    802 			case 1:
    803 				if (!type || type=='item') {
    804 					toReturn.push({
    805 						id: children[i].id,
    806 						key: children[i].key,
    807 						type: 'item',
    808 						parent: this.id
    809 					});
    810 				}
    811 			break;
    812 		}
    813 	}
    814 	
    815 	return toReturn;
    816 };
    817 
    818 
    819 /**
    820  * Return a collection in the specified library equivalent to this collection
    821  *
    822  * @return {Promise<Zotero.Collection>}
    823  */
    824 Zotero.Collection.prototype.getLinkedCollection = function (libraryID, bidrectional) {
    825 	return this._getLinkedObject(libraryID, bidrectional);
    826 }
    827 
    828 
    829 /**
    830  * Add a linked-object relation pointing to the given collection
    831  *
    832  * Does not require a separate save()
    833  */
    834 Zotero.Collection.prototype.addLinkedCollection = Zotero.Promise.coroutine(function* (collection) {
    835 	return this._addLinkedObject(collection);
    836 });
    837 
    838 
    839 //
    840 // Private methods
    841 //
    842 /**
    843  * Add a collection to the cached child collections list if loaded
    844  */
    845 Zotero.Collection.prototype._registerChildCollection = function (collectionID) {
    846 	if (this._loaded.childCollections) {
    847 		let collection = this.ObjectsClass.get(collectionID);
    848 		if (collection) {
    849 			this._childCollections.add(collectionID);
    850 		}
    851 	}
    852 }
    853 
    854 
    855 /**
    856  * Remove a collection from the cached child collections list if loaded
    857  */
    858 Zotero.Collection.prototype._unregisterChildCollection = function (collectionID) {
    859 	if (this._loaded.childCollections) {
    860 		this._childCollections.delete(collectionID);
    861 	}
    862 }
    863 
    864 
    865 /**
    866  * Add an item to the cached child items list if loaded
    867  */
    868 Zotero.Collection.prototype._registerChildItem = function (itemID) {
    869 	if (this._loaded.childItems) {
    870 		let item = this.ChildObjects.get(itemID);
    871 		if (item) {
    872 			this._childItems.add(itemID);
    873 		}
    874 	}
    875 }
    876 
    877 
    878 /**
    879  * Remove an item from the cached child items list if loaded
    880  */
    881 Zotero.Collection.prototype._unregisterChildItem = function (itemID) {
    882 	if (this._loaded.childItems) {
    883 		this._childItems.delete(itemID);
    884 	}
    885 }