places-tree.js (27045B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* import-globals-from controller.js */ 6 /* import-globals-from treeView.js */ 7 8 // This is loaded into all XUL windows. Wrap in a block to prevent 9 // leaking to window scope. 10 { 11 /** 12 * Custom element definition for the places tree. 13 */ 14 class MozPlacesTree extends customElements.get("tree") { 15 constructor() { 16 super(); 17 18 this.addEventListener("focus", () => { 19 this._cachedInsertionPoint = undefined; 20 // See select handler. We need the sidebar's places commandset to be 21 // updated as well 22 document.commandDispatcher.updateCommands("focus"); 23 }); 24 25 this.addEventListener("select", () => { 26 this._cachedInsertionPoint = undefined; 27 28 // This additional complexity is here for the sidebars 29 var win = window; 30 while (true) { 31 win.document.commandDispatcher.updateCommands("focus"); 32 if (win == window.top) { 33 break; 34 } 35 36 win = win.parent; 37 } 38 }); 39 40 this.addEventListener("dragstart", event => { 41 if (event.target.localName != "treechildren") { 42 return; 43 } 44 45 if (this.disableUserActions) { 46 event.preventDefault(); 47 event.stopPropagation(); 48 return; 49 } 50 51 let nodes = this.selectedNodes; 52 for (let i = 0; i < nodes.length; i++) { 53 let node = nodes[i]; 54 55 // Disallow dragging the root node of a tree. 56 if (!node.parent) { 57 event.preventDefault(); 58 event.stopPropagation(); 59 return; 60 } 61 62 // If this node is child of a readonly container or cannot be moved, 63 // we must force a copy. 64 if (!this.controller.canMoveNode(node)) { 65 event.dataTransfer.effectAllowed = "copyLink"; 66 break; 67 } 68 } 69 70 // Indicate to drag and drop listeners 71 // whether or not this was the start of the drag 72 this._isDragSource = true; 73 74 this._controller.setDataTransfer(event); 75 event.stopPropagation(); 76 }); 77 78 this.addEventListener("dragover", event => { 79 if (event.target.localName != "treechildren") { 80 return; 81 } 82 83 let cell = this.getCellAt(event.clientX, event.clientY); 84 let node = 85 cell.row != -1 86 ? this.view.nodeForTreeIndex(cell.row) 87 : this.result.root; 88 // cache the dropTarget for the view 89 PlacesControllerDragHelper.currentDropTarget = node; 90 91 // We have to calculate the orientation since view.canDrop will use 92 // it and we want to be consistent with the dropfeedback. 93 let rowHeight = this.rowHeight; 94 let eventY = 95 event.clientY - 96 this.treeBody.getBoundingClientRect().y - 97 rowHeight * (cell.row - this.getFirstVisibleRow()); 98 99 let orientation = Ci.nsITreeView.DROP_BEFORE; 100 101 if (cell.row == -1) { 102 // If the row is not valid we try to insert inside the resultNode. 103 orientation = Ci.nsITreeView.DROP_ON; 104 } else if ( 105 PlacesUtils.nodeIsContainer(node) && 106 eventY > rowHeight * 0.75 107 ) { 108 // If we are below the 75% of a container the treeview we try 109 // to drop after the node. 110 orientation = Ci.nsITreeView.DROP_AFTER; 111 } else if ( 112 PlacesUtils.nodeIsContainer(node) && 113 eventY > rowHeight * 0.25 114 ) { 115 // If we are below the 25% of a container the treeview we try 116 // to drop inside the node. 117 orientation = Ci.nsITreeView.DROP_ON; 118 } 119 120 if (!this.view.canDrop(cell.row, orientation, event.dataTransfer)) { 121 return; 122 } 123 124 event.preventDefault(); 125 event.stopPropagation(); 126 }); 127 128 this.addEventListener("dragend", () => { 129 this._isDragSource = false; 130 PlacesControllerDragHelper.currentDropTarget = null; 131 }); 132 } 133 134 connectedCallback() { 135 if (this.delayConnectedCallback()) { 136 return; 137 } 138 super.connectedCallback(); 139 this._contextMenuShown = false; 140 141 this._active = true; 142 143 // Force an initial build. 144 if (this.place) { 145 // eslint-disable-next-line no-self-assign 146 this.place = this.place; 147 } 148 149 window.addEventListener("unload", this.disconnectedCallback); 150 } 151 152 get controller() { 153 return this._controller; 154 } 155 156 set disableUserActions(val) { 157 if (val) { 158 this.setAttribute("disableUserActions", "true"); 159 } else { 160 this.removeAttribute("disableUserActions"); 161 } 162 } 163 164 get disableUserActions() { 165 return this.getAttribute("disableUserActions") == "true"; 166 } 167 /** 168 * overriding 169 * 170 * @param {PlacesTreeView} val 171 * The parent view 172 */ 173 set view(val) { 174 // We save the view so that we can avoid expensive get calls when 175 // we need to get the view again. 176 this._view = val; 177 Object.getOwnPropertyDescriptor( 178 // eslint-disable-next-line no-undef 179 XULTreeElement.prototype, 180 "view" 181 ).set.call(this, val); 182 } 183 184 get view() { 185 return this._view; 186 } 187 188 get associatedElement() { 189 return this; 190 } 191 192 set flatList(val) { 193 if (this.flatList != val) { 194 this.setAttribute("flatList", val); 195 // reload with the last place set 196 if (this.place) { 197 // eslint-disable-next-line no-self-assign 198 this.place = this.place; 199 } 200 } 201 } 202 203 get flatList() { 204 return this.getAttribute("flatList") == "true"; 205 } 206 207 get result() { 208 try { 209 return this.view.QueryInterface(Ci.nsINavHistoryResultObserver).result; 210 } catch (e) { 211 return null; 212 } 213 } 214 215 set place(val) { 216 this.setAttribute("place", val); 217 218 let query = {}, 219 options = {}; 220 PlacesUtils.history.queryStringToQuery(val, query, options); 221 this.load(query.value, options.value); 222 } 223 224 get place() { 225 return this.getAttribute("place"); 226 } 227 228 get selectedCount() { 229 return this.view?.selection?.count || 0; 230 } 231 232 get hasSelection() { 233 return this.selectedCount >= 1; 234 } 235 236 get selectedNodes() { 237 let nodes = []; 238 if (!this.hasSelection) { 239 return nodes; 240 } 241 242 let selection = this.view.selection; 243 let rc = selection.getRangeCount(); 244 let resultview = this.view; 245 for (let i = 0; i < rc; ++i) { 246 let min = {}, 247 max = {}; 248 selection.getRangeAt(i, min, max); 249 for (let j = min.value; j <= max.value; ++j) { 250 nodes.push(resultview.nodeForTreeIndex(j)); 251 } 252 } 253 return nodes; 254 } 255 256 get removableSelectionRanges() { 257 // This property exists in addition to selectedNodes because it 258 // encodes selection ranges (which only occur in list views) into 259 // the return value. For each removed range, the index at which items 260 // will be re-inserted upon the remove transaction being performed is 261 // the first index of the range, so that the view updates correctly. 262 // 263 // For example, if we remove rows 2,3,4 and 7,8 from a list, when we 264 // undo that operation, if we insert what was at row 3 at row 3 again, 265 // it will show up _after_ the item that was at row 5. So we need to 266 // insert all items at row 2, and the tree view will update correctly. 267 // 268 // Also, this function collapses the selection to remove redundant 269 // data, e.g. when deleting this selection: 270 // 271 // http://www.foo.com/ 272 // (-) Some Folder 273 // http://www.bar.com/ 274 // 275 // ... returning http://www.bar.com/ as part of the selection is 276 // redundant because it is implied by removing "Some Folder". We 277 // filter out all such redundancies since some partial amount of 278 // the folder's children may be selected. 279 // 280 let nodes = []; 281 if (!this.hasSelection) { 282 return nodes; 283 } 284 285 var selection = this.view.selection; 286 var rc = selection.getRangeCount(); 287 var resultview = this.view; 288 // This list is kept independently of the range selected (i.e. OUTSIDE 289 // the for loop) since the row index of a container is unique for the 290 // entire view, and we could have some really wacky selection and we 291 // don't want to blow up. 292 var containers = {}; 293 for (var i = 0; i < rc; ++i) { 294 var range = []; 295 var min = {}, 296 max = {}; 297 selection.getRangeAt(i, min, max); 298 299 for (var j = min.value; j <= max.value; ++j) { 300 if (this.view.isContainer(j)) { 301 containers[j] = true; 302 } 303 if (!(this.view.getParentIndex(j) in containers)) { 304 range.push(resultview.nodeForTreeIndex(j)); 305 } 306 } 307 nodes.push(range); 308 } 309 return nodes; 310 } 311 312 get draggableSelection() { 313 return this.selectedNodes; 314 } 315 316 get selectedNode() { 317 if (this.selectedCount != 1) { 318 return null; 319 } 320 321 var selection = this.view.selection; 322 var min = {}, 323 max = {}; 324 selection.getRangeAt(0, min, max); 325 326 return this.view.nodeForTreeIndex(min.value); 327 } 328 329 get singleClickOpens() { 330 return this.getAttribute("singleclickopens") == "true"; 331 } 332 333 get insertionPoint() { 334 // invalidated on selection and focus changes 335 if (this._cachedInsertionPoint !== undefined) { 336 return this._cachedInsertionPoint; 337 } 338 339 // there is no insertion point for history queries 340 // so bail out now and save a lot of work when updating commands 341 var resultNode = this.result.root; 342 if ( 343 PlacesUtils.nodeIsQuery(resultNode) && 344 PlacesUtils.asQuery(resultNode).queryOptions.queryType == 345 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY 346 ) { 347 return (this._cachedInsertionPoint = null); 348 } 349 350 var orientation = Ci.nsITreeView.DROP_BEFORE; 351 // If there is no selection, insert at the end of the container. 352 if (!this.hasSelection) { 353 var index = this.view.rowCount - 1; 354 this._cachedInsertionPoint = this._getInsertionPoint( 355 index, 356 orientation 357 ); 358 return this._cachedInsertionPoint; 359 } 360 361 // This is a two-part process. The first part is determining the drop 362 // orientation. 363 // * The default orientation is to drop _before_ the selected item. 364 // * If the selected item is a container, the default orientation 365 // is to drop _into_ that container. 366 // 367 // Warning: It may be tempting to use tree indexes in this code, but 368 // you must not, since the tree is nested and as your tree 369 // index may change when folders before you are opened and 370 // closed. You must convert your tree index to a node, and 371 // then use getChildIndex to find your absolute index in 372 // the parent container instead. 373 // 374 var resultView = this.view; 375 var selection = resultView.selection; 376 var rc = selection.getRangeCount(); 377 var min = {}, 378 max = {}; 379 selection.getRangeAt(rc - 1, min, max); 380 381 // If the sole selection is a container, and we are not in 382 // a flatlist, insert into it. 383 // Note that this only applies to _single_ selections, 384 // if the last element within a multi-selection is a 385 // container, insert _adjacent_ to the selection. 386 // 387 // If the sole selection is the bookmarks toolbar folder, we insert 388 // into it even if it is not opened 389 if ( 390 selection.count == 1 && 391 resultView.isContainer(max.value) && 392 !this.flatList 393 ) { 394 orientation = Ci.nsITreeView.DROP_ON; 395 } 396 397 this._cachedInsertionPoint = this._getInsertionPoint( 398 max.value, 399 orientation 400 ); 401 return this._cachedInsertionPoint; 402 } 403 404 get isDragSource() { 405 return this._isDragSource; 406 } 407 408 get ownerWindow() { 409 return window; 410 } 411 412 set active(val) { 413 this._active = val; 414 } 415 416 get active() { 417 return this._active; 418 } 419 420 applyFilter(filterString, folderRestrict, includeHidden) { 421 // preserve grouping 422 var queryNode = PlacesUtils.asQuery(this.result.root); 423 var options = queryNode.queryOptions.clone(); 424 425 // Make sure we're getting uri results. 426 // We do not yet support searching into grouped queries or into 427 // tag containers, so we must fall to the default case. 428 if ( 429 PlacesUtils.nodeIsHistoryContainer(queryNode) || 430 PlacesUtils.nodeIsTagQuery(queryNode) || 431 options.resultType == options.RESULTS_AS_TAGS_ROOT || 432 options.resultType == options.RESULTS_AS_ROOTS_QUERY 433 ) { 434 options.resultType = options.RESULTS_AS_URI; 435 } 436 437 var query = PlacesUtils.history.getNewQuery(); 438 query.searchTerms = filterString; 439 440 if (folderRestrict) { 441 query.setParents(folderRestrict); 442 options.queryType = options.QUERY_TYPE_BOOKMARKS; 443 Glean.sidebar.search.bookmarks.add(1); 444 } 445 446 options.includeHidden = !!includeHidden; 447 448 this.load(query, options); 449 } 450 451 load(query, options) { 452 let result = PlacesUtils.history.executeQuery(query, options); 453 454 if (!this._controller) { 455 this._controller = new PlacesController(this); 456 this._controller.disableUserActions = this.disableUserActions; 457 this.controllers.appendController(this._controller); 458 } 459 460 let treeView = new PlacesTreeView(this); 461 462 // Observer removal is done within the view itself. When the tree 463 // goes away, view.setTree(null) is called, which then 464 // calls removeObserver. 465 result.addObserver(treeView); 466 this.view = treeView; 467 468 if ( 469 this.getAttribute("selectfirstnode") == "true" && 470 treeView.rowCount > 0 471 ) { 472 treeView.selection.select(0); 473 } 474 475 this._cachedInsertionPoint = undefined; 476 } 477 478 /** 479 * Causes a particular node represented by the specified placeURI to be 480 * selected in the tree. All containers above the node in the hierarchy 481 * will be opened, so that the node is visible. 482 * 483 * @param {string} placeURI 484 * The URI that should be selected 485 */ 486 selectPlaceURI(placeURI) { 487 // Do nothing if a node matching the given uri is already selected 488 if (this.hasSelection && this.selectedNode.uri == placeURI) { 489 return; 490 } 491 492 function findNode(container, nodesURIChecked) { 493 var containerURI = container.uri; 494 if (containerURI == placeURI) { 495 return container; 496 } 497 if (nodesURIChecked.includes(containerURI)) { 498 return null; 499 } 500 501 // never check the contents of the same query 502 nodesURIChecked.push(containerURI); 503 504 var wasOpen = container.containerOpen; 505 if (!wasOpen) { 506 container.containerOpen = true; 507 } 508 for (let i = 0, count = container.childCount; i < count; ++i) { 509 var child = container.getChild(i); 510 var childURI = child.uri; 511 if (childURI == placeURI) { 512 return child; 513 } else if (PlacesUtils.nodeIsContainer(child)) { 514 var nested = findNode( 515 PlacesUtils.asContainer(child), 516 nodesURIChecked 517 ); 518 if (nested) { 519 return nested; 520 } 521 } 522 } 523 524 if (!wasOpen) { 525 container.containerOpen = false; 526 } 527 528 return null; 529 } 530 531 var container = this.result.root; 532 console.assert(container, "No result, cannot select place URI!"); 533 if (!container) { 534 return; 535 } 536 537 var child = findNode(container, []); 538 if (child) { 539 this.selectNode(child); 540 } else { 541 // If the specified child could not be located, clear the selection 542 var selection = this.view.selection; 543 selection.clearSelection(); 544 } 545 } 546 547 /** 548 * Causes a particular node to be selected in the tree, resulting in all 549 * containers above the node in the hierarchy to be opened, so that the 550 * node is visible. 551 * 552 * @param {object} node 553 * The node that should be selected 554 */ 555 selectNode(node) { 556 var view = this.view; 557 558 var parent = node.parent; 559 if (parent && !parent.containerOpen) { 560 // Build a list of all of the nodes that are the parent of this one 561 // in the result. 562 var parents = []; 563 var root = this.result.root; 564 while (parent && parent != root) { 565 parents.push(parent); 566 parent = parent.parent; 567 } 568 569 // Walk the list backwards (opening from the root of the hierarchy) 570 // opening each folder as we go. 571 for (var i = parents.length - 1; i >= 0; --i) { 572 let index = view.treeIndexForNode(parents[i]); 573 if ( 574 index != -1 && 575 view.isContainer(index) && 576 !view.isContainerOpen(index) 577 ) { 578 view.toggleOpenState(index); 579 } 580 } 581 // Select the specified node... 582 } 583 584 let index = view.treeIndexForNode(node); 585 if (index == -1) { 586 return; 587 } 588 589 view.selection.select(index); 590 // ... and ensure it's visible, not scrolled off somewhere. 591 this.ensureRowIsVisible(index); 592 } 593 594 toggleCutNode(aNode, aValue) { 595 this.view.toggleCutNode(aNode, aValue); 596 } 597 598 _getInsertionPoint(index, orientation) { 599 var result = this.result; 600 var resultview = this.view; 601 var container = result.root; 602 var dropNearNode = null; 603 console.assert(container, "null container"); 604 // When there's no selection, assume the container is the container 605 // the view is populated from (i.e. the result's itemId). 606 if (index != -1) { 607 var lastSelected = resultview.nodeForTreeIndex(index); 608 if ( 609 resultview.isContainer(index) && 610 orientation == Ci.nsITreeView.DROP_ON 611 ) { 612 // If the last selected item is an open container, append _into_ 613 // it, rather than insert adjacent to it. 614 container = lastSelected; 615 index = -1; 616 } else if ( 617 lastSelected.containerOpen && 618 orientation == Ci.nsITreeView.DROP_AFTER && 619 lastSelected.hasChildren 620 ) { 621 // If the last selected item is an open container and the user is 622 // trying to drag into it as a first item, really insert into it. 623 container = lastSelected; 624 orientation = Ci.nsITreeView.DROP_ON; 625 index = 0; 626 } else { 627 // Use the last-selected node's container. 628 container = lastSelected.parent; 629 630 // See comment in the treeView.js's copy of this method 631 if (!container || !container.containerOpen) { 632 return null; 633 } 634 635 // Avoid the potentially expensive call to getChildIndex 636 // if we know this container doesn't allow insertion 637 if (this.controller.disallowInsertion(container)) { 638 return null; 639 } 640 641 var queryOptions = PlacesUtils.asQuery(result.root).queryOptions; 642 if ( 643 queryOptions.sortingMode != 644 Ci.nsINavHistoryQueryOptions.SORT_BY_NONE 645 ) { 646 // If we are within a sorted view, insert at the end 647 index = -1; 648 } else if (queryOptions.excludeItems || queryOptions.excludeQueries) { 649 // Some item may be invisible, insert near last selected one. 650 // We don't replace index here to avoid requests to the db, 651 // instead it will be calculated later by the controller. 652 index = -1; 653 dropNearNode = lastSelected; 654 } else { 655 var lsi = container.getChildIndex(lastSelected); 656 index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1; 657 } 658 } 659 } 660 661 if (this.controller.disallowInsertion(container)) { 662 return null; 663 } 664 665 let tagName = PlacesUtils.nodeIsTagQuery(container) 666 ? PlacesUtils.asQuery(container).query.tags[0] 667 : null; 668 669 return new PlacesInsertionPoint({ 670 parentGuid: PlacesUtils.getConcreteItemGuid(container), 671 index, 672 orientation, 673 tagName, 674 dropNearNode, 675 }); 676 } 677 678 selectAll() { 679 this.view.selection.selectAll(); 680 } 681 682 /** 683 * This method will select the first node in the tree that matches 684 * each given item guid. It will open any folder nodes that it needs 685 * to in order to show the selected items. 686 * 687 * @param {Array} aGuids 688 * Guids to select. 689 * @param {boolean} aOpenContainers 690 * Whether or not to open containers. 691 */ 692 selectItems(aGuids, aOpenContainers) { 693 // Never open containers in flat lists. 694 if (this.flatList) { 695 aOpenContainers = false; 696 } 697 // By default, we do search and select within containers which were 698 // closed (note that containers in which nodes were not found are 699 // closed). 700 if (aOpenContainers === undefined) { 701 aOpenContainers = true; 702 } 703 704 var guids = aGuids; // don't manipulate the caller's array 705 706 // Array of nodes found by findNodes which are to be selected 707 var nodes = []; 708 709 // Array of nodes found by findNodes which should be opened 710 var nodesToOpen = []; 711 712 // A set of GUIDs of container-nodes that were previously searched, 713 // and thus shouldn't be searched again. This is empty at the initial 714 // start of the recursion and gets filled in as the recursion 715 // progresses. 716 var checkedGuidsSet = new Set(); 717 718 /** 719 * Recursively search through a node's children for items 720 * with the given GUIDs. When a matching item is found, remove its GUID 721 * from the GUIDs array, and add the found node to the nodes dictionary. 722 * 723 * NOTE: This method will leave open any node that had matching items 724 * in its subtree. 725 * 726 * @param {object} node 727 * The node to search. 728 * @returns {boolean} 729 * Returns true if at least one item was found. 730 */ 731 function findNodes(node) { 732 var foundOne = false; 733 // See if node matches an ID we wanted; add to results. 734 // For simple folder queries, check both itemId and the concrete 735 // item id. 736 var index = guids.indexOf(node.bookmarkGuid); 737 if (index == -1) { 738 let concreteGuid = PlacesUtils.getConcreteItemGuid(node); 739 if (concreteGuid != node.bookmarkGuid) { 740 index = guids.indexOf(concreteGuid); 741 } 742 } 743 744 if (index != -1) { 745 nodes.push(node); 746 foundOne = true; 747 guids.splice(index, 1); 748 } 749 750 var concreteGuid = PlacesUtils.getConcreteItemGuid(node); 751 if ( 752 !guids.length || 753 !PlacesUtils.nodeIsContainer(node) || 754 checkedGuidsSet.has(concreteGuid) 755 ) { 756 return foundOne; 757 } 758 759 // Only follow a query if it has been been explicitly opened by the 760 // caller. We support the "AllBookmarks" case to allow callers to 761 // specify just the top-level bookmark folders. 762 let shouldOpen = 763 aOpenContainers && 764 (PlacesUtils.nodeIsFolderOrShortcut(node) || 765 (PlacesUtils.nodeIsQuery(node) && 766 node.bookmarkGuid == PlacesUIUtils.virtualAllBookmarksGuid)); 767 768 PlacesUtils.asContainer(node); 769 if (!node.containerOpen && !shouldOpen) { 770 return foundOne; 771 } 772 773 checkedGuidsSet.add(concreteGuid); 774 775 // Remember the beginning state so that we can re-close 776 // this node if we don't find any additional results here. 777 let previousOpenness = node.containerOpen; 778 node.containerOpen = true; 779 for ( 780 let i = 0, count = node.childCount; 781 i < count && guids.length; 782 ++i 783 ) { 784 let childNode = node.getChild(i); 785 let found = findNodes(childNode); 786 if (!foundOne) { 787 foundOne = found; 788 } 789 } 790 791 // If we didn't find any additional matches in this node's 792 // subtree, revert the node to its previous openness. 793 if (foundOne) { 794 nodesToOpen.unshift(node); 795 } 796 node.containerOpen = previousOpenness; 797 return foundOne; 798 } 799 800 // Disable notifications while looking for nodes. 801 let result = this.result; 802 let didSuppressNotifications = result.suppressNotifications; 803 if (!didSuppressNotifications) { 804 result.suppressNotifications = true; 805 } 806 try { 807 findNodes(this.result.root); 808 } finally { 809 if (!didSuppressNotifications) { 810 result.suppressNotifications = false; 811 } 812 } 813 814 // For all the nodes we've found, highlight the corresponding 815 // index in the tree. 816 var resultview = this.view; 817 var selection = this.view.selection; 818 selection.selectEventsSuppressed = true; 819 selection.clearSelection(); 820 // Open nodes containing found items 821 for (let i = 0; i < nodesToOpen.length; i++) { 822 nodesToOpen[i].containerOpen = true; 823 } 824 let firstValidTreeIndex = -1; 825 for (let i = 0; i < nodes.length; i++) { 826 var index = resultview.treeIndexForNode(nodes[i]); 827 if (index == -1) { 828 continue; 829 } 830 if (firstValidTreeIndex < 0 && index >= 0) { 831 firstValidTreeIndex = index; 832 } 833 selection.rangedSelect(index, index, true); 834 } 835 selection.selectEventsSuppressed = false; 836 837 // Bring the first valid node into view if necessary 838 if (firstValidTreeIndex >= 0) { 839 this.ensureRowIsVisible(firstValidTreeIndex); 840 } 841 } 842 843 buildContextMenu(aPopup) { 844 this._contextMenuShown = true; 845 return this.controller.buildContextMenu(aPopup); 846 } 847 848 destroyContextMenu() {} 849 850 disconnectedCallback() { 851 window.removeEventListener("unload", this.disconnectedCallback); 852 // Unregister the controller before unlinking the view, otherwise it 853 // may still try to update commands on a view with a null result. 854 if (this._controller) { 855 this._controller.terminate(); 856 this.controllers.removeController(this._controller); 857 } 858 859 if (this.view) { 860 this.view.uninit(); 861 this.view = null; 862 } 863 } 864 } 865 866 customElements.define("places-tree", MozPlacesTree, { 867 extends: "tree", 868 }); 869 }