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 }