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 };