browserPlacesViews.js (71670B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * The base view implements everything that's common to all the views. 7 * It should not be instanced directly, use a derived class instead. 8 */ 9 class PlacesViewBase { 10 /** 11 * @param {string} placesUrl 12 * The query string associated with the view. 13 * @param {DOMElement} rootElt 14 * The root element for the view. 15 * @param {DOMElement} viewElt 16 * The view element. 17 */ 18 constructor(placesUrl, rootElt, viewElt) { 19 this._rootElt = rootElt; 20 this._viewElt = viewElt; 21 // Do initialization in subclass now that `this` exists. 22 this._init?.(); 23 this._controller = new PlacesController(this); 24 this.place = placesUrl; 25 this._viewElt.controllers.appendController(this._controller); 26 } 27 28 // The xul element that holds the entire view. 29 _viewElt = null; 30 31 get associatedElement() { 32 return this._viewElt; 33 } 34 35 get controllers() { 36 return this._viewElt.controllers; 37 } 38 39 // The xul element that represents the root container. 40 _rootElt = null; 41 42 get rootElement() { 43 return this._rootElt; 44 } 45 46 // Set to true for views that are represented by native widgets (i.e. 47 // the native mac menu). 48 _nativeView = false; 49 50 static interfaces = [ 51 Ci.nsINavHistoryResultObserver, 52 Ci.nsISupportsWeakReference, 53 ]; 54 55 QueryInterface = ChromeUtils.generateQI(PlacesViewBase.interfaces); 56 57 _place = ""; 58 get place() { 59 return this._place; 60 } 61 set place(val) { 62 this._place = val; 63 64 let history = PlacesUtils.history; 65 let query = {}, 66 options = {}; 67 history.queryStringToQuery(val, query, options); 68 let result = history.executeQuery(query.value, options.value); 69 result.addObserver(this); 70 } 71 72 _result = null; 73 get result() { 74 return this._result; 75 } 76 set result(val) { 77 if (this._result == val) { 78 return; 79 } 80 81 if (this._result) { 82 this._result.removeObserver(this); 83 this._resultNode.containerOpen = false; 84 } 85 86 if (this._rootElt.localName == "menupopup") { 87 this._rootElt._built = false; 88 } 89 90 this._result = val; 91 if (val) { 92 this._resultNode = val.root; 93 this._rootElt._placesNode = this._resultNode; 94 this._domNodes = new Map(); 95 this._domNodes.set(this._resultNode, this._rootElt); 96 97 // This calls _rebuild through invalidateContainer. 98 this._resultNode.containerOpen = true; 99 } else { 100 this._resultNode = null; 101 delete this._domNodes; 102 } 103 } 104 105 /** 106 * Gets the DOM node used for the given places node. 107 * 108 * @param {object} aPlacesNode 109 * a places result node. 110 * @param {boolean} aAllowMissing 111 * whether the node may be missing 112 * @returns {object|null} The associated DOM node. 113 * @throws if there is no DOM node set for aPlacesNode. 114 */ 115 _getDOMNodeForPlacesNode(aPlacesNode, aAllowMissing = false) { 116 let node = this._domNodes.get(aPlacesNode, null); 117 if (!node && !aAllowMissing) { 118 throw new Error( 119 "No DOM node set for aPlacesNode.\nnode.type: " + 120 aPlacesNode.type + 121 ". node.parent: " + 122 aPlacesNode 123 ); 124 } 125 return node; 126 } 127 128 get controller() { 129 return this._controller; 130 } 131 132 get selType() { 133 return "single"; 134 } 135 selectItems() {} 136 selectAll() {} 137 138 get selectedNode() { 139 if (this._contextMenuShown) { 140 let anchor = this._contextMenuShown.triggerNode; 141 if (!anchor) { 142 return null; 143 } 144 145 if (anchor._placesNode) { 146 return this._rootElt == anchor ? null : anchor._placesNode; 147 } 148 149 anchor = anchor.parentNode; 150 return this._rootElt == anchor ? null : anchor._placesNode || null; 151 } 152 return null; 153 } 154 155 get hasSelection() { 156 return this.selectedNode != null; 157 } 158 159 get selectedNodes() { 160 let selectedNode = this.selectedNode; 161 return selectedNode ? [selectedNode] : []; 162 } 163 164 get singleClickOpens() { 165 return true; 166 } 167 168 get removableSelectionRanges() { 169 // On static content the current selectedNode would be the selection's 170 // parent node. We don't want to allow removing a node when the 171 // selection is not explicit. 172 let popupNode = PlacesUIUtils.lastContextMenuTriggerNode; 173 if (popupNode && (popupNode == "menupopup" || !popupNode._placesNode)) { 174 return []; 175 } 176 177 return [this.selectedNodes]; 178 } 179 180 get draggableSelection() { 181 return [this._draggedElt]; 182 } 183 184 get insertionPoint() { 185 // There is no insertion point for history queries, so bail out now and 186 // save a lot of work when updating commands. 187 let resultNode = this._resultNode; 188 if ( 189 PlacesUtils.nodeIsQuery(resultNode) && 190 PlacesUtils.asQuery(resultNode).queryOptions.queryType == 191 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY 192 ) { 193 return null; 194 } 195 196 // By default, the insertion point is at the top level, at the end. 197 let index = PlacesUtils.bookmarks.DEFAULT_INDEX; 198 let container = this._resultNode; 199 let orientation = Ci.nsITreeView.DROP_BEFORE; 200 let tagName = null; 201 202 let selectedNode = this.selectedNode; 203 if (selectedNode) { 204 let popupNode = PlacesUIUtils.lastContextMenuTriggerNode; 205 if ( 206 !popupNode._placesNode || 207 popupNode._placesNode == this._resultNode || 208 popupNode._placesNode.itemId == -1 || 209 !selectedNode.parent 210 ) { 211 // If a static menuitem is selected, or if the root node is selected, 212 // the insertion point is inside the folder, at the end. 213 container = selectedNode; 214 orientation = Ci.nsITreeView.DROP_ON; 215 } else { 216 // In all other cases the insertion point is before that node. 217 container = selectedNode.parent; 218 index = container.getChildIndex(selectedNode); 219 if (PlacesUtils.nodeIsTagQuery(container)) { 220 tagName = PlacesUtils.asQuery(container).query.tags[0]; 221 } 222 } 223 } 224 225 if (this.controller.disallowInsertion(container)) { 226 return null; 227 } 228 229 return new PlacesInsertionPoint({ 230 parentGuid: PlacesUtils.getConcreteItemGuid(container), 231 index, 232 orientation, 233 tagName, 234 }); 235 } 236 237 buildContextMenu(aPopup) { 238 this._contextMenuShown = aPopup; 239 window.updateCommands("places"); 240 241 // Ensure that an existing "Show Other Bookmarks" item is removed before adding it 242 // again. 243 let existingOtherBookmarksItem = aPopup.querySelector( 244 "#show-other-bookmarks_PersonalToolbar" 245 ); 246 existingOtherBookmarksItem?.remove(); 247 248 let manageBookmarksMenu = aPopup.querySelector( 249 "#placesContext_showAllBookmarks" 250 ); 251 // Add the View menu for the Bookmarks Toolbar and "Show Other Bookmarks" menu item 252 // if the click originated from the Bookmarks Toolbar. 253 let existingSubmenu = aPopup.querySelector("#toggle_PersonalToolbar"); 254 existingSubmenu?.remove(); 255 let bookmarksToolbar = document.getElementById("PersonalToolbar"); 256 if (bookmarksToolbar?.contains(aPopup.triggerNode)) { 257 manageBookmarksMenu.removeAttribute("hidden"); 258 259 let menu = BookmarkingUI.buildBookmarksToolbarSubmenu(bookmarksToolbar); 260 aPopup.insertBefore(menu, manageBookmarksMenu); 261 262 if ( 263 aPopup.triggerNode.id === "OtherBookmarks" || 264 aPopup.triggerNode.id === "PlacesChevron" || 265 aPopup.triggerNode.id === "PlacesToolbarItems" || 266 aPopup.triggerNode.parentNode.id === "PlacesToolbarItems" 267 ) { 268 let otherBookmarksMenuItem = 269 BookmarkingUI.buildShowOtherBookmarksMenuItem(); 270 271 if (otherBookmarksMenuItem) { 272 aPopup.insertBefore(otherBookmarksMenuItem, menu.nextElementSibling); 273 } 274 } 275 } else { 276 manageBookmarksMenu.setAttribute("hidden", "true"); 277 } 278 279 return this.controller.buildContextMenu(aPopup); 280 } 281 282 destroyContextMenu() { 283 this._contextMenuShown = null; 284 } 285 286 clearAllContents(aPopup) { 287 let kid = aPopup.firstElementChild; 288 while (kid) { 289 let next = kid.nextElementSibling; 290 if (!kid.classList.contains("panel-header")) { 291 kid.remove(); 292 } 293 kid = next; 294 } 295 aPopup._emptyMenuitem = aPopup._startMarker = aPopup._endMarker = null; 296 } 297 298 _cleanPopup(aPopup, aDelay) { 299 // Ensure markers are here when `invalidateContainer` is called before the 300 // popup is shown, which may the case for panelviews, for example. 301 this._ensureMarkers(aPopup); 302 // Remove Places nodes from the popup. 303 let child = aPopup._startMarker; 304 while (child.nextElementSibling != aPopup._endMarker) { 305 let sibling = child.nextElementSibling; 306 if (sibling._placesNode && !aDelay) { 307 aPopup.removeChild(sibling); 308 } else if (sibling._placesNode && aDelay) { 309 // HACK (bug 733419): the popups originating from the OS X native 310 // menubar don't live-update while open, thus we don't clean it 311 // until the next popupshowing, to avoid zombie menuitems. 312 if (!aPopup._delayedRemovals) { 313 aPopup._delayedRemovals = []; 314 } 315 aPopup._delayedRemovals.push(sibling); 316 child = child.nextElementSibling; 317 } else { 318 child = child.nextElementSibling; 319 } 320 } 321 } 322 323 _rebuildPopup(aPopup) { 324 let resultNode = aPopup._placesNode; 325 if (!resultNode.containerOpen) { 326 return; 327 } 328 329 this._cleanPopup(aPopup); 330 331 let cc = resultNode.childCount; 332 if (cc > 0) { 333 this._setEmptyPopupStatus(aPopup, false); 334 let fragment = document.createDocumentFragment(); 335 for (let i = 0; i < cc; ++i) { 336 let child = resultNode.getChild(i); 337 this._insertNewItemToPopup(child, fragment); 338 } 339 aPopup.insertBefore(fragment, aPopup._endMarker); 340 } else { 341 this._setEmptyPopupStatus(aPopup, true); 342 } 343 aPopup._built = true; 344 } 345 346 _removeChild(aChild) { 347 aChild.remove(); 348 } 349 350 _setEmptyPopupStatus(aPopup, aEmpty) { 351 if (!aPopup._emptyMenuitem) { 352 aPopup._emptyMenuitem = document.createXULElement("menuitem"); 353 aPopup._emptyMenuitem.setAttribute("disabled", true); 354 aPopup._emptyMenuitem.className = "bookmark-item"; 355 document.l10n.setAttributes( 356 aPopup._emptyMenuitem, 357 "places-empty-bookmarks-folder" 358 ); 359 } 360 361 if (aEmpty) { 362 aPopup.setAttribute("emptyplacesresult", "true"); 363 // Don't add the menuitem if there is static content. 364 if ( 365 !aPopup._startMarker.previousElementSibling && 366 !aPopup._endMarker.nextElementSibling 367 ) { 368 aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker); 369 } 370 } else { 371 aPopup.removeAttribute("emptyplacesresult"); 372 try { 373 aPopup.removeChild(aPopup._emptyMenuitem); 374 } catch (ex) {} 375 } 376 } 377 378 _createDOMNodeForPlacesNode(aPlacesNode) { 379 this._domNodes.delete(aPlacesNode); 380 381 let element; 382 let type = aPlacesNode.type; 383 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { 384 element = document.createXULElement("menuseparator"); 385 } else { 386 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) { 387 element = document.createXULElement("menuitem"); 388 element.className = 389 "menuitem-iconic bookmark-item menuitem-with-favicon"; 390 element.setAttribute( 391 "scheme", 392 PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri) 393 ); 394 } else if (PlacesUtils.containerTypes.includes(type)) { 395 element = document.createXULElement("menu"); 396 element.setAttribute("container", "true"); 397 398 if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) { 399 element.setAttribute("query", "true"); 400 if (PlacesUtils.nodeIsTagQuery(aPlacesNode)) { 401 element.setAttribute("tagContainer", "true"); 402 } else if (PlacesUtils.nodeIsDay(aPlacesNode)) { 403 element.setAttribute("dayContainer", "true"); 404 } else if (PlacesUtils.nodeIsHost(aPlacesNode)) { 405 element.setAttribute("hostContainer", "true"); 406 } 407 } 408 409 let popup = document.createXULElement("menupopup", { 410 is: "places-popup", 411 }); 412 popup._placesNode = PlacesUtils.asContainer(aPlacesNode); 413 414 if (!this._nativeView) { 415 popup.setAttribute("placespopup", "true"); 416 } 417 418 element.appendChild(popup); 419 element.className = "menu-iconic bookmark-item"; 420 421 this._domNodes.set(aPlacesNode, popup); 422 } else { 423 throw new Error("Unexpected node"); 424 } 425 426 element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode)); 427 428 let icon = aPlacesNode.icon; 429 if (icon) { 430 element.setAttribute("image", ChromeUtils.encodeURIForSrcset(icon)); 431 } 432 } 433 434 element._placesNode = aPlacesNode; 435 if (!this._domNodes.has(aPlacesNode)) { 436 this._domNodes.set(aPlacesNode, element); 437 } 438 439 return element; 440 } 441 442 _insertNewItemToPopup(aNewChild, aInsertionNode, aBefore = null) { 443 let element = this._createDOMNodeForPlacesNode(aNewChild); 444 445 aInsertionNode.insertBefore(element, aBefore); 446 return element; 447 } 448 449 toggleCutNode(aPlacesNode, aValue) { 450 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); 451 452 // We may get the popup for menus, but we need the menu itself. 453 if (elt.localName == "menupopup") { 454 elt = elt.parentNode; 455 } 456 if (aValue) { 457 elt.setAttribute("cutting", "true"); 458 } else { 459 elt.removeAttribute("cutting"); 460 } 461 } 462 463 nodeURIChanged(aPlacesNode) { 464 let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); 465 466 // There's no DOM node, thus there's nothing to be done when the URI changes. 467 if (!elt) { 468 return; 469 } 470 471 // Here we need the <menu>. 472 if (elt.localName == "menupopup") { 473 elt = elt.parentNode; 474 } 475 476 elt.setAttribute( 477 "scheme", 478 PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri) 479 ); 480 } 481 482 nodeIconChanged(aPlacesNode) { 483 let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); 484 485 // There's no UI representation for the root node, or there's no DOM node, 486 // thus there's nothing to be done when the icon changes. 487 if (!elt || elt == this._rootElt) { 488 return; 489 } 490 491 // Here we need the <menu>. 492 if (elt.localName == "menupopup") { 493 elt = elt.parentNode; 494 } 495 // We must remove and reset the attribute to force an update. 496 elt.removeAttribute("image"); 497 elt.setAttribute("image", ChromeUtils.encodeURIForSrcset(aPlacesNode.icon)); 498 } 499 500 nodeTitleChanged(aPlacesNode, aNewTitle) { 501 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); 502 503 // There's no UI representation for the root node, thus there's 504 // nothing to be done when the title changes. 505 if (elt == this._rootElt) { 506 return; 507 } 508 509 // Here we need the <menu>. 510 if (elt.localName == "menupopup") { 511 elt = elt.parentNode; 512 } 513 514 if (!aNewTitle && elt.localName != "toolbarbutton") { 515 // Many users consider toolbars as shortcuts containers, so explicitly 516 // allow empty labels on toolbarbuttons. For any other element try to be 517 // smarter, guessing a title from the uri. 518 elt.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode)); 519 } else { 520 elt.setAttribute("label", aNewTitle); 521 } 522 } 523 524 nodeRemoved(aParentPlacesNode, aPlacesNode) { 525 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); 526 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); 527 528 // Here we need the <menu>. 529 if (elt.localName == "menupopup") { 530 elt = elt.parentNode; 531 } 532 533 if (parentElt._built) { 534 parentElt.removeChild(elt); 535 536 // Figure out if we need to show the "<Empty>" menu-item. 537 // TODO Bug 517701: This doesn't seem to handle the case of an empty 538 // root. 539 if (parentElt._startMarker.nextElementSibling == parentElt._endMarker) { 540 this._setEmptyPopupStatus(parentElt, true); 541 } 542 } 543 } 544 545 // Opt-out of history details updates, since all the views derived from this 546 // are not showing them. 547 skipHistoryDetailsNotifications = true; 548 nodeHistoryDetailsChanged() {} 549 nodeTagsChanged() {} 550 nodeDateAddedChanged() {} 551 nodeLastModifiedChanged() {} 552 nodeKeywordChanged() {} 553 sortingChanged() {} 554 batching() {} 555 556 nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) { 557 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); 558 if (!parentElt._built) { 559 return; 560 } 561 562 let index = 563 Array.prototype.indexOf.call(parentElt.children, parentElt._startMarker) + 564 aIndex + 565 1; 566 this._insertNewItemToPopup( 567 aPlacesNode, 568 parentElt, 569 parentElt.children[index] || parentElt._endMarker 570 ); 571 this._setEmptyPopupStatus(parentElt, false); 572 } 573 574 nodeMoved( 575 aPlacesNode, 576 aOldParentPlacesNode, 577 aOldIndex, 578 aNewParentPlacesNode, 579 aNewIndex 580 ) { 581 // Note: the current implementation of moveItem does not actually 582 // use this notification when the item in question is moved from one 583 // folder to another. Instead, it calls nodeRemoved and nodeInserted 584 // for the two folders. Thus, we can assume old-parent == new-parent. 585 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); 586 587 // Here we need the <menu>. 588 if (elt.localName == "menupopup") { 589 elt = elt.parentNode; 590 } 591 592 // If our root node is a folder, it might be moved. There's nothing 593 // we need to do in that case. 594 if (elt == this._rootElt) { 595 return; 596 } 597 598 let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode); 599 if (parentElt._built) { 600 // Move the node. 601 parentElt.removeChild(elt); 602 let index = 603 Array.prototype.indexOf.call( 604 parentElt.children, 605 parentElt._startMarker 606 ) + 607 aNewIndex + 608 1; 609 parentElt.insertBefore(elt, parentElt.children[index]); 610 } 611 } 612 613 containerStateChanged(aPlacesNode, aOldState, aNewState) { 614 if ( 615 aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED || 616 aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED 617 ) { 618 this.invalidateContainer(aPlacesNode); 619 } 620 } 621 622 /** 623 * Checks whether the popup associated with the provided element is open. 624 * This method may be overridden by classes that extend this base class. 625 * 626 * @param {Element} elt 627 * The element to check. 628 * @returns {boolean} 629 */ 630 _isPopupOpen(elt) { 631 return !!elt.parentNode.open; 632 } 633 634 invalidateContainer(aPlacesNode) { 635 let elt = this._getDOMNodeForPlacesNode(aPlacesNode); 636 elt._built = false; 637 638 // If the menupopup is open we should live-update it. 639 if (this._isPopupOpen(elt)) { 640 this._rebuildPopup(elt); 641 } 642 } 643 644 uninit() { 645 if (this._result) { 646 this._result.removeObserver(this); 647 this._resultNode.containerOpen = false; 648 this._resultNode = null; 649 this._result = null; 650 } 651 652 if (this._controller) { 653 this._controller.terminate(); 654 // Removing the controller will fail if it is already no longer there. 655 // This can happen if the view element was removed/reinserted without 656 // our knowledge. There is no way to check for that having happened 657 // without the possibility of an exception. :-( 658 try { 659 this._viewElt.controllers.removeController(this._controller); 660 } catch (ex) { 661 } finally { 662 this._controller = null; 663 } 664 } 665 666 delete this._viewElt._placesView; 667 } 668 669 get isRTL() { 670 if ("_isRTL" in this) { 671 return this._isRTL; 672 } 673 674 return (this._isRTL = 675 document.defaultView.getComputedStyle(this._viewElt).direction == "rtl"); 676 } 677 678 get ownerWindow() { 679 return window; 680 } 681 682 /** 683 * Adds an "Open All in Tabs" menuitem to the bottom of the popup. 684 * 685 * @param {object} aPopup 686 * a Places popup. 687 */ 688 _mayAddCommandsItems(aPopup) { 689 // The command items are never added to the root popup. 690 if (aPopup == this._rootElt) { 691 return; 692 } 693 694 let hasMultipleURIs = false; 695 696 // Check if the popup contains at least 2 menuitems with places nodes. 697 // We don't currently support opening multiple uri nodes when they are not 698 // populated by the result. 699 if (aPopup._placesNode.childCount > 0) { 700 let currentChild = aPopup.firstElementChild; 701 let numURINodes = 0; 702 while (currentChild) { 703 if (currentChild.localName == "menuitem" && currentChild._placesNode) { 704 if (++numURINodes == 2) { 705 break; 706 } 707 } 708 currentChild = currentChild.nextElementSibling; 709 } 710 hasMultipleURIs = numURINodes > 1; 711 } 712 713 if (!hasMultipleURIs) { 714 // We don't have to show any option. 715 if (aPopup._endOptOpenAllInTabs) { 716 aPopup.removeChild(aPopup._endOptOpenAllInTabs); 717 aPopup._endOptOpenAllInTabs = null; 718 719 aPopup.removeChild(aPopup._endOptSeparator); 720 aPopup._endOptSeparator = null; 721 } 722 } else if (!aPopup._endOptOpenAllInTabs) { 723 // Create a separator before options. 724 aPopup._endOptSeparator = document.createXULElement("menuseparator"); 725 aPopup._endOptSeparator.className = "bookmarks-actions-menuseparator"; 726 aPopup.appendChild(aPopup._endOptSeparator); 727 728 // Add the "Open All in Tabs" menuitem. 729 aPopup._endOptOpenAllInTabs = document.createXULElement("menuitem"); 730 aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem"; 731 aPopup._endOptOpenAllInTabs.setAttribute( 732 "label", 733 gNavigatorBundle.getString("menuOpenAllInTabs.label") 734 ); 735 aPopup._endOptOpenAllInTabs.addEventListener("command", event => { 736 PlacesUIUtils.openMultipleLinksInTabs( 737 event.currentTarget.parentNode._placesNode, 738 event, 739 PlacesUIUtils.getViewForNode(event.currentTarget) 740 ); 741 }); 742 aPopup.appendChild(aPopup._endOptOpenAllInTabs); 743 } 744 } 745 746 _ensureMarkers(aPopup) { 747 if (aPopup._startMarker) { 748 return; 749 } 750 751 // Places nodes are appended between _startMarker and _endMarker, that 752 // are hidden menuseparators. By default they take the whole panel... 753 aPopup._startMarker = document.createXULElement("menuseparator"); 754 aPopup._startMarker.hidden = true; 755 aPopup.insertBefore(aPopup._startMarker, aPopup.firstElementChild); 756 aPopup._endMarker = document.createXULElement("menuseparator"); 757 aPopup._endMarker.hidden = true; 758 aPopup.appendChild(aPopup._endMarker); 759 760 // ...but there can be static content before or after the places nodes, thus 761 // we move the markers to the right position, by checking for static content 762 // at the beginning of the view, and for an element with "afterplacescontent" 763 // attribute. 764 // TODO: In the future we should just use a container element. 765 let firstNonStaticNodeFound = false; 766 for (let child of aPopup.children) { 767 if (child.hasAttribute("afterplacescontent")) { 768 aPopup.insertBefore(aPopup._endMarker, child); 769 break; 770 } 771 772 // Check for the first Places node that is not a view. 773 if (child._placesNode && !child._placesView && !firstNonStaticNodeFound) { 774 firstNonStaticNodeFound = true; 775 aPopup.insertBefore(aPopup._startMarker, child); 776 } 777 } 778 if (!firstNonStaticNodeFound) { 779 // Just put the start marker before the end marker. 780 aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker); 781 } 782 } 783 784 _onPopupShowing(aEvent) { 785 // Avoid handling popupshowing of inner views. 786 let popup = aEvent.originalTarget; 787 788 this._ensureMarkers(popup); 789 790 // Remove any delayed element, see _cleanPopup for details. 791 if ("_delayedRemovals" in popup) { 792 while (popup._delayedRemovals.length) { 793 popup.removeChild(popup._delayedRemovals.shift()); 794 } 795 } 796 797 if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) { 798 if (this.#isPopupForRecursiveFolderShortcut(popup)) { 799 // Show as an empty container for now. We may want to show a better 800 // message in the future, but since we are likely to remove recursive 801 // shortcuts in maintenance at a certain point, this should be enough. 802 this._setEmptyPopupStatus(popup, true); 803 popup._built = true; 804 return; 805 } 806 807 if (!popup._placesNode.containerOpen) { 808 popup._placesNode.containerOpen = true; 809 } 810 if (!popup._built) { 811 this._rebuildPopup(popup); 812 } 813 814 this._mayAddCommandsItems(popup); 815 } 816 } 817 818 _addEventListeners(aObject, aEventNames, aCapturing = false) { 819 for (let i = 0; i < aEventNames.length; i++) { 820 aObject.addEventListener(aEventNames[i], this, aCapturing); 821 } 822 } 823 824 _removeEventListeners(aObject, aEventNames, aCapturing = false) { 825 for (let i = 0; i < aEventNames.length; i++) { 826 aObject.removeEventListener(aEventNames[i], this, aCapturing); 827 } 828 } 829 830 /** 831 * Walks up the parent chain to detect whether a folder shortcut resolves to 832 * a folder already present in the ancestry. 833 * 834 * @param {DOMElement} popup 835 * @returns {boolean} Whether this popup is for a recursive folder shortcut. 836 */ 837 #isPopupForRecursiveFolderShortcut(popup) { 838 if ( 839 !popup._placesNode || 840 !PlacesUtils.nodeIsFolderOrShortcut(popup._placesNode) 841 ) { 842 return false; 843 } 844 let guid = PlacesUtils.getConcreteItemGuid(popup._placesNode); 845 for ( 846 let parentView = popup.parentNode?.parentNode; 847 parentView?._placesNode; 848 parentView = parentView.parentNode?.parentNode 849 ) { 850 if (PlacesUtils.getConcreteItemGuid(parentView._placesNode) == guid) { 851 return true; 852 } 853 } 854 return false; 855 } 856 } 857 858 /** 859 * Toolbar View implementation. 860 */ 861 class PlacesToolbar extends PlacesViewBase { 862 constructor(placesUrl, rootElt, viewElt) { 863 let timerId = Glean.bookmarksToolbar.init.start(); 864 super(placesUrl, rootElt, viewElt); 865 this._addEventListeners(this._dragRoot, this._cbEvents, false); 866 this._addEventListeners( 867 this._rootElt, 868 ["popupshowing", "popuphidden"], 869 true 870 ); 871 this._addEventListeners(this._rootElt, ["overflow", "underflow"], true); 872 this._addEventListeners(window, ["resize", "unload"], false); 873 874 // If personal-bookmarks has been dragged to the tabs toolbar, 875 // we have to track addition and removals of tabs, to properly 876 // recalculate the available space for bookmarks. 877 // TODO (bug 734730): Use a performant mutation listener when available. 878 if ( 879 this._viewElt.parentNode.parentNode == 880 document.getElementById("TabsToolbar") 881 ) { 882 this._addEventListeners( 883 gBrowser.tabContainer, 884 ["TabOpen", "TabClose"], 885 false 886 ); 887 } 888 889 Glean.bookmarksToolbar.init.stopAndAccumulate(timerId); 890 } 891 892 // Called by PlacesViewBase so we can init properties that class 893 // initialization depends on. PlacesViewBase will assign this.place which 894 // calls which sets `this.result` through its places observer, which changes 895 // containerOpen, which calls invalidateContainer(), which calls rebuild(), 896 // which needs `_overFolder`, `_chevronPopup` and various other things to 897 // exist. 898 _init() { 899 this._overFolder = { 900 elt: null, 901 openTimer: null, 902 hoverTime: 350, 903 closeTimer: null, 904 }; 905 906 // Add some smart getters for our elements. 907 let thisView = this; 908 [ 909 ["_dropIndicator", "PlacesToolbarDropIndicator"], 910 ["_chevron", "PlacesChevron"], 911 ["_chevronPopup", "PlacesChevronPopup"], 912 ].forEach(function (elementGlobal) { 913 let [name, id] = elementGlobal; 914 thisView.__defineGetter__(name, function () { 915 let element = document.getElementById(id); 916 if (!element) { 917 return null; 918 } 919 920 delete thisView[name]; 921 return (thisView[name] = element); 922 }); 923 }); 924 925 this._viewElt._placesView = this; 926 927 this._dragRoot = BookmarkingUI.toolbar.contains(this._viewElt) 928 ? BookmarkingUI.toolbar 929 : this._viewElt; 930 931 this._updatingNodesVisibility = false; 932 } 933 934 _cbEvents = [ 935 "dragstart", 936 "dragover", 937 "dragleave", 938 "dragend", 939 "drop", 940 "mousemove", 941 "mouseover", 942 "mouseout", 943 "mousedown", 944 ]; 945 946 QueryInterface = ChromeUtils.generateQI([ 947 "nsINamed", 948 "nsITimerCallback", 949 ...PlacesViewBase.interfaces, 950 ]); 951 952 uninit() { 953 if (this._dragRoot) { 954 this._removeEventListeners(this._dragRoot, this._cbEvents, false); 955 } 956 this._removeEventListeners( 957 this._rootElt, 958 ["popupshowing", "popuphidden"], 959 true 960 ); 961 this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true); 962 this._removeEventListeners(window, ["resize", "unload"], false); 963 this._removeEventListeners( 964 gBrowser.tabContainer, 965 ["TabOpen", "TabClose"], 966 false 967 ); 968 969 if (this._chevron._placesView) { 970 this._chevron._placesView.uninit(); 971 } 972 973 this._chevronPopup.uninit(); 974 975 if (this._otherBookmarks?._placesView) { 976 this._otherBookmarks._placesView.uninit(); 977 } 978 979 super.uninit(); 980 } 981 982 _openedMenuButton = null; 983 _allowPopupShowing = true; 984 985 promiseRebuilt() { 986 return this._rebuilding?.promise; 987 } 988 989 get _isAlive() { 990 return this._resultNode && this._rootElt; 991 } 992 993 _runBeforeFrameRender(callback) { 994 return new Promise((resolve, reject) => { 995 window.requestAnimationFrame(() => { 996 try { 997 resolve(callback()); 998 } catch (err) { 999 reject(err); 1000 } 1001 }); 1002 }); 1003 } 1004 1005 async _rebuild() { 1006 // Clear out references to existing nodes, since they will be removed 1007 // and re-added. 1008 if (this._overFolder.elt) { 1009 this._clearOverFolder(); 1010 } 1011 1012 this._openedMenuButton = null; 1013 while (this._rootElt.hasChildNodes()) { 1014 this._rootElt.firstChild.remove(); 1015 } 1016 1017 let cc = this._resultNode.childCount; 1018 if (cc > 0) { 1019 // There could be a lot of nodes, but we only want to build the ones that 1020 // are more likely to be shown, not all of them. 1021 // We also don't want to wait for reflows at every node insertion, to 1022 // calculate a precise number of visible items, thus we guess a size from 1023 // the first non-separator node (because separators have flexible size). 1024 let startIndex = 0; 1025 let limit = await this._runBeforeFrameRender(() => { 1026 if (!this._isAlive) { 1027 return cc; 1028 } 1029 1030 // Look for the first non-separator node. 1031 let elt; 1032 while (startIndex < cc) { 1033 elt = this._insertNewItem( 1034 this._resultNode.getChild(startIndex), 1035 this._rootElt 1036 ); 1037 ++startIndex; 1038 if (elt.localName != "toolbarseparator") { 1039 break; 1040 } 1041 } 1042 if (!elt) { 1043 return cc; 1044 } 1045 1046 return window.promiseDocumentFlushed(() => { 1047 // We assume a button with just the icon will be more or less a square, 1048 // then compensate the measurement error by considering a larger screen 1049 // width. Moreover the window could be bigger than the screen. 1050 let size = elt.clientHeight || 1; // Sanity fallback. 1051 return Math.min(cc, parseInt((window.screen.width * 1.5) / size)); 1052 }); 1053 }); 1054 1055 if (!this._isAlive) { 1056 return; 1057 } 1058 1059 let fragment = document.createDocumentFragment(); 1060 for (let i = startIndex; i < limit; ++i) { 1061 this._insertNewItem(this._resultNode.getChild(i), fragment); 1062 } 1063 await new Promise(resolve => window.requestAnimationFrame(resolve)); 1064 if (!this._isAlive) { 1065 return; 1066 } 1067 this._rootElt.appendChild(fragment); 1068 this.updateNodesVisibility(); 1069 } 1070 1071 if (this._chevronPopup.hasAttribute("type")) { 1072 // Chevron has already been initialized, but since we are forcing 1073 // a rebuild of the toolbar, it has to be rebuilt. 1074 // Otherwise, it will be initialized when the toolbar overflows. 1075 this._chevronPopup.place = this.place; 1076 } 1077 1078 // Rebuild the "Other Bookmarks" folder if it already exists. 1079 let otherBookmarks = document.getElementById("OtherBookmarks"); 1080 otherBookmarks?.remove(); 1081 1082 BookmarkingUI.maybeShowOtherBookmarksFolder().catch(console.error); 1083 } 1084 1085 _insertNewItem(aChild, aInsertionNode, aBefore = null) { 1086 this._domNodes.delete(aChild); 1087 1088 let type = aChild.type; 1089 let button; 1090 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { 1091 button = document.createXULElement("toolbarseparator"); 1092 } else { 1093 button = document.createXULElement("toolbarbutton"); 1094 button.className = "bookmark-item"; 1095 button.setAttribute("label", aChild.title || ""); 1096 1097 if (PlacesUtils.containerTypes.includes(type)) { 1098 button.setAttribute("type", "menu"); 1099 button.setAttribute("container", "true"); 1100 1101 if (PlacesUtils.nodeIsQuery(aChild)) { 1102 button.setAttribute("query", "true"); 1103 if (PlacesUtils.nodeIsTagQuery(aChild)) { 1104 button.setAttribute("tagContainer", "true"); 1105 } 1106 } 1107 1108 let popup = document.createXULElement("menupopup", { 1109 is: "places-popup", 1110 }); 1111 popup.setAttribute("placespopup", "true"); 1112 popup.classList.add("toolbar-menupopup"); 1113 button.appendChild(popup); 1114 popup._placesNode = PlacesUtils.asContainer(aChild); 1115 popup.setAttribute("context", "placesContext"); 1116 1117 this._domNodes.set(aChild, popup); 1118 } else if (PlacesUtils.nodeIsURI(aChild)) { 1119 button.setAttribute( 1120 "scheme", 1121 PlacesUIUtils.guessUrlSchemeForUI(aChild.uri) 1122 ); 1123 } 1124 } 1125 1126 button._placesNode = aChild; 1127 let { icon } = button._placesNode; 1128 if (icon) { 1129 button.setAttribute("image", icon); 1130 } 1131 if (!this._domNodes.has(aChild)) { 1132 this._domNodes.set(aChild, button); 1133 } 1134 1135 if (aBefore) { 1136 aInsertionNode.insertBefore(button, aBefore); 1137 } else { 1138 aInsertionNode.appendChild(button); 1139 } 1140 return button; 1141 } 1142 1143 _updateChevronPopupNodesVisibility() { 1144 // Note the toolbar by default builds less nodes than the chevron popup. 1145 for ( 1146 let toolbarNode = this._rootElt.firstElementChild, 1147 node = this._chevronPopup._startMarker.nextElementSibling; 1148 toolbarNode && node; 1149 toolbarNode = toolbarNode.nextElementSibling, 1150 node = node.nextElementSibling 1151 ) { 1152 node.hidden = toolbarNode.style.visibility != "hidden"; 1153 } 1154 } 1155 1156 _onChevronPopupShowing(aEvent) { 1157 // Handle popupshowing only for the chevron popup, not for nested ones. 1158 if (aEvent.target != this._chevronPopup) { 1159 return; 1160 } 1161 1162 if (!this._chevron._placesView) { 1163 this._chevron._placesView = new PlacesMenu(aEvent, this.place); 1164 } 1165 1166 this._updateChevronPopupNodesVisibility(); 1167 } 1168 1169 _onOtherBookmarksPopupShowing(aEvent) { 1170 if (aEvent.target != this._otherBookmarksPopup) { 1171 return; 1172 } 1173 1174 if (!this._otherBookmarks._placesView) { 1175 this._otherBookmarks._placesView = new PlacesMenu( 1176 aEvent, 1177 "place:parent=" + PlacesUtils.bookmarks.unfiledGuid 1178 ); 1179 } 1180 } 1181 1182 handleEvent(aEvent) { 1183 switch (aEvent.type) { 1184 case "unload": 1185 this.uninit(); 1186 break; 1187 case "resize": 1188 // This handler updates nodes visibility in both the toolbar 1189 // and the chevron popup when a window resize does not change 1190 // the overflow status of the toolbar. 1191 if (aEvent.target == aEvent.currentTarget) { 1192 this.updateNodesVisibility(); 1193 } 1194 break; 1195 case "overflow": 1196 if (!this._isOverflowStateEventRelevant(aEvent)) { 1197 return; 1198 } 1199 // Avoid triggering overflow in containers if possible 1200 aEvent.stopPropagation(); 1201 this._onOverflow(); 1202 break; 1203 case "underflow": 1204 if (!this._isOverflowStateEventRelevant(aEvent)) { 1205 return; 1206 } 1207 // Avoid triggering underflow in containers if possible 1208 aEvent.stopPropagation(); 1209 this._onUnderflow(); 1210 break; 1211 case "TabOpen": 1212 case "TabClose": 1213 this.updateNodesVisibility(); 1214 break; 1215 case "dragstart": 1216 this._onDragStart(aEvent); 1217 break; 1218 case "dragover": 1219 this._onDragOver(aEvent); 1220 break; 1221 case "dragleave": 1222 this._onDragLeave(aEvent); 1223 break; 1224 case "dragend": 1225 this._onDragEnd(aEvent); 1226 break; 1227 case "drop": 1228 this._onDrop(aEvent); 1229 break; 1230 case "mouseover": 1231 this._onMouseOver(aEvent); 1232 break; 1233 case "mousemove": 1234 this._onMouseMove(aEvent); 1235 break; 1236 case "mouseout": 1237 this._onMouseOut(aEvent); 1238 break; 1239 case "mousedown": 1240 this._onMouseDown(aEvent); 1241 break; 1242 case "popupshowing": 1243 this._onPopupShowing(aEvent); 1244 break; 1245 case "popuphidden": 1246 this._onPopupHidden(aEvent); 1247 break; 1248 default: 1249 throw new Error("Trying to handle unexpected event."); 1250 } 1251 } 1252 1253 _isOverflowStateEventRelevant(aEvent) { 1254 // Ignore events not aimed at ourselves, as well as purely vertical ones: 1255 return aEvent.target == aEvent.currentTarget && aEvent.detail > 0; 1256 } 1257 1258 _onOverflow() { 1259 // Attach the popup binding to the chevron popup if it has not yet 1260 // been initialized. 1261 if (!this._chevronPopup.hasAttribute("type")) { 1262 this._chevronPopup.setAttribute("place", this.place); 1263 this._chevronPopup.setAttribute("type", "places"); 1264 } 1265 this._chevron.collapsed = false; 1266 this.updateNodesVisibility(); 1267 } 1268 1269 _onUnderflow() { 1270 this.updateNodesVisibility(); 1271 this._chevron.collapsed = true; 1272 } 1273 1274 updateNodesVisibility() { 1275 // Update the chevron on a timer. This will avoid repeated work when 1276 // lot of changes happen in a small timeframe. 1277 if (this._updateNodesVisibilityTimer) { 1278 this._updateNodesVisibilityTimer.cancel(); 1279 } 1280 1281 this._updateNodesVisibilityTimer = this._setTimer(100); 1282 } 1283 1284 async _updateNodesVisibilityTimerCallback() { 1285 if (this._updatingNodesVisibility || window.closed) { 1286 return; 1287 } 1288 this._updatingNodesVisibility = true; 1289 1290 let dwu = window.windowUtils; 1291 1292 let scrollRect = await window.promiseDocumentFlushed(() => 1293 dwu.getBoundsWithoutFlushing(this._rootElt) 1294 ); 1295 1296 let childOverflowed = false; 1297 1298 // We're about to potentially update a bunch of nodes, so we do it 1299 // in a requestAnimationFrame so that other JS that's might execute 1300 // in the same tick can avoid flushing styles and layout for these 1301 // changes. 1302 window.requestAnimationFrame(() => { 1303 for (let child of this._rootElt.children) { 1304 // Once a child overflows, all the next ones will. 1305 if (!childOverflowed) { 1306 let childRect = dwu.getBoundsWithoutFlushing(child); 1307 childOverflowed = this.isRTL 1308 ? childRect.left < scrollRect.left 1309 : childRect.right > scrollRect.right; 1310 } 1311 1312 if (childOverflowed) { 1313 child.removeAttribute("image"); 1314 child.style.visibility = "hidden"; 1315 } else { 1316 let icon = child._placesNode.icon; 1317 if (icon) { 1318 child.setAttribute("image", icon); 1319 } 1320 child.style.removeProperty("visibility"); 1321 } 1322 } 1323 1324 // We rebuild the chevron on popupShowing, so if it is open 1325 // we must update it. 1326 if (!this._chevron.collapsed && this._chevron.open) { 1327 this._updateChevronPopupNodesVisibility(); 1328 } 1329 1330 let event = new CustomEvent("BookmarksToolbarVisibilityUpdated", { 1331 bubbles: true, 1332 }); 1333 this._viewElt.dispatchEvent(event); 1334 this._updatingNodesVisibility = false; 1335 }); 1336 } 1337 1338 nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) { 1339 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); 1340 if (parentElt == this._rootElt) { 1341 // Node is on the toolbar. 1342 let children = this._rootElt.children; 1343 // Nothing to do if it's a never-visible node, but note it's possible 1344 // we are appending. 1345 if (aIndex > children.length) { 1346 return; 1347 } 1348 1349 // Note that childCount is already accounting for the node being added, 1350 // thus we must subtract one node from it. 1351 if (this._resultNode.childCount - 1 > children.length) { 1352 if (aIndex == children.length) { 1353 // If we didn't build all the nodes and new node is being appended, 1354 // we can skip it as well. 1355 return; 1356 } 1357 // Keep the number of built nodes consistent. 1358 this._rootElt.removeChild(this._rootElt.lastElementChild); 1359 } 1360 1361 let button = this._insertNewItem( 1362 aPlacesNode, 1363 this._rootElt, 1364 children[aIndex] || null 1365 ); 1366 let prevSiblingOverflowed = 1367 aIndex > 0 && 1368 aIndex <= children.length && 1369 children[aIndex - 1].style.visibility == "hidden"; 1370 if (prevSiblingOverflowed) { 1371 button.style.visibility = "hidden"; 1372 } else { 1373 let icon = aPlacesNode.icon; 1374 if (icon) { 1375 button.setAttribute("image", ChromeUtils.encodeURIForSrcset(icon)); 1376 } 1377 this.updateNodesVisibility(); 1378 } 1379 return; 1380 } 1381 1382 super.nodeInserted(aParentPlacesNode, aPlacesNode, aIndex); 1383 } 1384 1385 nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) { 1386 let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); 1387 if (parentElt == this._rootElt) { 1388 // Node is on the toolbar. 1389 let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); 1390 // Nothing to do if it's a never-visible node. 1391 if (!elt) { 1392 return; 1393 } 1394 1395 // Here we need the <menu>. 1396 if (elt.localName == "menupopup") { 1397 elt = elt.parentNode; 1398 } 1399 1400 let overflowed = elt.style.visibility == "hidden"; 1401 this._removeChild(elt); 1402 if (this._resultNode.childCount > this._rootElt.children.length) { 1403 // A new node should be built to keep a coherent number of children. 1404 this._insertNewItem( 1405 this._resultNode.getChild(this._rootElt.children.length), 1406 this._rootElt 1407 ); 1408 } 1409 if (!overflowed) { 1410 this.updateNodesVisibility(); 1411 } 1412 return; 1413 } 1414 1415 super.nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex); 1416 } 1417 1418 nodeMoved( 1419 aPlacesNode, 1420 aOldParentPlacesNode, 1421 aOldIndex, 1422 aNewParentPlacesNode, 1423 aNewIndex 1424 ) { 1425 let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode); 1426 if (parentElt == this._rootElt) { 1427 // Node is on the toolbar. 1428 // Do nothing if the node will never be visible. 1429 let lastBuiltIndex = this._rootElt.children.length - 1; 1430 if (aOldIndex > lastBuiltIndex && aNewIndex > lastBuiltIndex + 1) { 1431 return; 1432 } 1433 1434 let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); 1435 if (elt) { 1436 // Here we need the <menu>. 1437 if (elt.localName == "menupopup") { 1438 elt = elt.parentNode; 1439 } 1440 this._removeChild(elt); 1441 } 1442 1443 if (aNewIndex > lastBuiltIndex + 1) { 1444 if (this._resultNode.childCount > this._rootElt.children.length) { 1445 // If the element was built and becomes non built, another node should 1446 // be built to keep a coherent number of children. 1447 this._insertNewItem( 1448 this._resultNode.getChild(this._rootElt.children.length), 1449 this._rootElt 1450 ); 1451 } 1452 return; 1453 } 1454 1455 if (!elt) { 1456 // The node has not been inserted yet, so we must create it. 1457 elt = this._insertNewItem( 1458 aPlacesNode, 1459 this._rootElt, 1460 this._rootElt.children[aNewIndex] 1461 ); 1462 let icon = aPlacesNode.icon; 1463 if (icon) { 1464 elt.setAttribute("image", ChromeUtils.encodeURIForSrcset(icon)); 1465 } 1466 } else { 1467 this._rootElt.insertBefore(elt, this._rootElt.children[aNewIndex]); 1468 } 1469 1470 // The chevron view may get nodeMoved after the toolbar. In such a case, 1471 // we should ensure (by manually swapping menuitems) that the actual nodes 1472 // are in the final position before updateNodesVisibility tries to update 1473 // their visibility, or the chevron may go out of sync. 1474 // Luckily updateNodesVisibility runs on a timer, so, by the time it updates 1475 // nodes, the menu has already handled the notification. 1476 1477 this.updateNodesVisibility(); 1478 return; 1479 } 1480 1481 super.nodeMoved( 1482 aPlacesNode, 1483 aOldParentPlacesNode, 1484 aOldIndex, 1485 aNewParentPlacesNode, 1486 aNewIndex 1487 ); 1488 } 1489 1490 nodeTitleChanged(aPlacesNode, aNewTitle) { 1491 let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); 1492 1493 // Nothing to do if it's a never-visible node. 1494 if (!elt || elt == this._rootElt) { 1495 return; 1496 } 1497 1498 super.nodeTitleChanged(aPlacesNode, aNewTitle); 1499 1500 // Here we need the <menu>. 1501 if (elt.localName == "menupopup") { 1502 elt = elt.parentNode; 1503 } 1504 1505 if (elt.parentNode == this._rootElt) { 1506 // Node is on the toolbar. 1507 if (elt.style.visibility != "hidden") { 1508 this.updateNodesVisibility(); 1509 } 1510 } 1511 } 1512 1513 invalidateContainer(aPlacesNode) { 1514 let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); 1515 // Nothing to do if it's a never-visible node. 1516 if (!elt) { 1517 return; 1518 } 1519 1520 if (elt == this._rootElt) { 1521 // Container is the toolbar itself. 1522 let instance = (this._rebuildingInstance = {}); 1523 if (!this._rebuilding) { 1524 this._rebuilding = Promise.withResolvers(); 1525 } 1526 this._rebuild() 1527 .catch(console.error) 1528 .finally(() => { 1529 if (instance == this._rebuildingInstance) { 1530 this._rebuilding.resolve(); 1531 this._rebuilding = null; 1532 } 1533 }); 1534 return; 1535 } 1536 1537 super.invalidateContainer(aPlacesNode); 1538 } 1539 1540 _clearOverFolder() { 1541 // The mouse is no longer dragging over the stored menubutton. 1542 // Close the menubutton, clear out drag styles, and clear all 1543 // timers for opening/closing it. 1544 if (this._overFolder.elt && this._overFolder.elt.menupopup) { 1545 if (!this._overFolder.elt.menupopup.hasAttribute("dragover")) { 1546 this._overFolder.elt.menupopup.hidePopup(); 1547 } 1548 this._overFolder.elt.removeAttribute("dragover"); 1549 this._overFolder.elt = null; 1550 } 1551 if (this._overFolder.openTimer) { 1552 this._overFolder.openTimer.cancel(); 1553 this._overFolder.openTimer = null; 1554 } 1555 if (this._overFolder.closeTimer) { 1556 this._overFolder.closeTimer.cancel(); 1557 this._overFolder.closeTimer = null; 1558 } 1559 } 1560 1561 /** 1562 * This function returns information about where to drop when dragging over 1563 * the toolbar. 1564 * 1565 * @param {object} aEvent 1566 * The associated event. 1567 * @returns {object} 1568 * - ip: the insertion point for the bookmarks service. 1569 * - beforeIndex: child index to drop before, for the drop indicator. 1570 * - folderElt: the folder to drop into, if applicable. 1571 */ 1572 _getDropPoint(aEvent) { 1573 if (!PlacesUtils.nodeIsFolderOrShortcut(this._resultNode)) { 1574 return null; 1575 } 1576 1577 let dropPoint = { ip: null, beforeIndex: null, folderElt: null }; 1578 let elt = aEvent.target; 1579 if ( 1580 elt._placesNode && 1581 elt != this._rootElt && 1582 elt.localName != "menupopup" 1583 ) { 1584 let eltRect = elt.getBoundingClientRect(); 1585 let eltIndex = Array.prototype.indexOf.call(this._rootElt.children, elt); 1586 if ( 1587 PlacesUtils.nodeIsFolderOrShortcut(elt._placesNode) && 1588 !PlacesUIUtils.isFolderReadOnly(elt._placesNode) 1589 ) { 1590 // This is a folder. 1591 // If we are in the middle of it, drop inside it. 1592 // Otherwise, drop before it, with regards to RTL mode. 1593 let threshold = eltRect.width * 0.25; 1594 if ( 1595 this.isRTL 1596 ? aEvent.clientX > eltRect.right - threshold 1597 : aEvent.clientX < eltRect.left + threshold 1598 ) { 1599 // Drop before this folder. 1600 dropPoint.ip = new PlacesInsertionPoint({ 1601 parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode), 1602 index: eltIndex, 1603 orientation: Ci.nsITreeView.DROP_BEFORE, 1604 }); 1605 dropPoint.beforeIndex = eltIndex; 1606 } else if ( 1607 this.isRTL 1608 ? aEvent.clientX > eltRect.left + threshold 1609 : aEvent.clientX < eltRect.right - threshold 1610 ) { 1611 // Drop inside this folder. 1612 let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) 1613 ? elt._placesNode.title 1614 : null; 1615 dropPoint.ip = new PlacesInsertionPoint({ 1616 parentGuid: PlacesUtils.getConcreteItemGuid(elt._placesNode), 1617 tagName, 1618 }); 1619 dropPoint.beforeIndex = eltIndex; 1620 dropPoint.folderElt = elt; 1621 } else { 1622 // Drop after this folder. 1623 let beforeIndex = 1624 eltIndex == this._rootElt.children.length - 1 ? -1 : eltIndex + 1; 1625 1626 dropPoint.ip = new PlacesInsertionPoint({ 1627 parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode), 1628 index: beforeIndex, 1629 orientation: Ci.nsITreeView.DROP_BEFORE, 1630 }); 1631 dropPoint.beforeIndex = beforeIndex; 1632 } 1633 } else { 1634 // This is a non-folder node or a read-only folder. 1635 // Drop before it with regards to RTL mode. 1636 let threshold = eltRect.width * 0.5; 1637 if ( 1638 this.isRTL 1639 ? aEvent.clientX > eltRect.left + threshold 1640 : aEvent.clientX < eltRect.left + threshold 1641 ) { 1642 // Drop before this bookmark. 1643 dropPoint.ip = new PlacesInsertionPoint({ 1644 parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode), 1645 index: eltIndex, 1646 orientation: Ci.nsITreeView.DROP_BEFORE, 1647 }); 1648 dropPoint.beforeIndex = eltIndex; 1649 } else { 1650 // Drop after this bookmark. 1651 let beforeIndex = 1652 eltIndex == this._rootElt.children.length - 1 ? -1 : eltIndex + 1; 1653 dropPoint.ip = new PlacesInsertionPoint({ 1654 parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode), 1655 index: beforeIndex, 1656 orientation: Ci.nsITreeView.DROP_BEFORE, 1657 }); 1658 dropPoint.beforeIndex = beforeIndex; 1659 } 1660 } 1661 } else if (elt == this._chevron) { 1662 // If drop on the chevron, insert after the last bookmark. 1663 dropPoint.ip = new PlacesInsertionPoint({ 1664 parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode), 1665 orientation: Ci.nsITreeView.DROP_BEFORE, 1666 }); 1667 dropPoint.beforeIndex = -1; 1668 } else { 1669 dropPoint.ip = new PlacesInsertionPoint({ 1670 parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode), 1671 orientation: Ci.nsITreeView.DROP_BEFORE, 1672 }); 1673 1674 // If could not find an insertion point before bookmark items or empty, 1675 // drop after the last bookmark. 1676 dropPoint.beforeIndex = -1; 1677 1678 let canInsertHere = this.isRTL 1679 ? (x, rect) => x >= Math.round(rect.right) 1680 : (x, rect) => x <= Math.round(rect.left); 1681 1682 // Find the bookmark placed just after the mouse point as the insertion 1683 // point. 1684 for (let i = 0; i < this._rootElt.children.length; i++) { 1685 let childRect = window.windowUtils.getBoundsWithoutFlushing( 1686 this._rootElt.children[i] 1687 ); 1688 if (canInsertHere(aEvent.clientX, childRect)) { 1689 dropPoint.beforeIndex = i; 1690 dropPoint.ip.index = i; 1691 break; 1692 } 1693 } 1694 } 1695 1696 return dropPoint; 1697 } 1698 1699 _setTimer(aTime) { 1700 let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1701 timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT); 1702 return timer; 1703 } 1704 1705 get name() { 1706 return "PlacesToolbar"; 1707 } 1708 1709 notify(aTimer) { 1710 if (aTimer == this._updateNodesVisibilityTimer) { 1711 this._updateNodesVisibilityTimer = null; 1712 this._updateNodesVisibilityTimerCallback(); 1713 } else if (aTimer == this._overFolder.openTimer) { 1714 // * Timer to open a menubutton that's being dragged over. 1715 // Set the autoopen attribute on the folder's menupopup so that 1716 // the menu will automatically close when the mouse drags off of it. 1717 this._overFolder.elt.menupopup.setAttribute("autoopened", "true"); 1718 this._overFolder.elt.open = true; 1719 this._overFolder.openTimer = null; 1720 } else if (aTimer == this._overFolder.closeTimer) { 1721 // * Timer to close a menubutton that's been dragged off of. 1722 // Close the menubutton if we are not dragging over it or one of 1723 // its children. The autoopened attribute will let the menu know to 1724 // close later if the menu is still being dragged over. 1725 let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget; 1726 let inHierarchy = false; 1727 while (currentPlacesNode) { 1728 if (currentPlacesNode == this._rootElt) { 1729 inHierarchy = true; 1730 break; 1731 } 1732 currentPlacesNode = currentPlacesNode.parentNode; 1733 } 1734 // The _clearOverFolder() function will close the menu for 1735 // _overFolder.elt. So null it out if we don't want to close it. 1736 if (inHierarchy) { 1737 this._overFolder.elt = null; 1738 } 1739 1740 // Clear out the folder and all associated timers. 1741 this._clearOverFolder(); 1742 } 1743 } 1744 1745 _onMouseOver(aEvent) { 1746 let button = aEvent.target; 1747 if ( 1748 button.parentNode == this._rootElt && 1749 button._placesNode && 1750 PlacesUtils.nodeIsURI(button._placesNode) 1751 ) { 1752 window.XULBrowserWindow.setOverLink(aEvent.target._placesNode.uri); 1753 } 1754 } 1755 1756 _onMouseOut() { 1757 window.XULBrowserWindow.setOverLink(""); 1758 } 1759 1760 _onMouseDown(aEvent) { 1761 let target = aEvent.target; 1762 if ( 1763 aEvent.button == 0 && 1764 target.localName == "toolbarbutton" && 1765 target.getAttribute("type") == "menu" 1766 ) { 1767 let modifKey = aEvent.shiftKey || aEvent.getModifierState("Accel"); 1768 if (modifKey) { 1769 // Do not open the popup since BEH_onClick is about to 1770 // open all child uri nodes in tabs. 1771 this._allowPopupShowing = false; 1772 } 1773 } 1774 PlacesUIUtils.maybeSpeculativeConnectOnMouseDown(aEvent); 1775 } 1776 1777 _cleanupDragDetails() { 1778 // Called on dragend and drop. 1779 PlacesControllerDragHelper.currentDropTarget = null; 1780 this._draggedElt = null; 1781 this._dropIndicator.collapsed = true; 1782 } 1783 1784 _onDragStart(aEvent) { 1785 // Sub menus have their own d&d handlers. 1786 let draggedElt = aEvent.target; 1787 if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode) { 1788 return; 1789 } 1790 1791 if ( 1792 draggedElt.localName == "toolbarbutton" && 1793 draggedElt.getAttribute("type") == "menu" 1794 ) { 1795 // If the drag gesture on a container is toward down we open instead 1796 // of dragging. 1797 let translateY = this._cachedMouseMoveEvent.clientY - aEvent.clientY; 1798 let translateX = this._cachedMouseMoveEvent.clientX - aEvent.clientX; 1799 if (translateY >= Math.abs(translateX / 2)) { 1800 // Don't start the drag. 1801 aEvent.preventDefault(); 1802 // Open the menu. 1803 draggedElt.open = true; 1804 return; 1805 } 1806 1807 // If the menu is open, close it. 1808 if (draggedElt.open) { 1809 draggedElt.menupopup.hidePopup(); 1810 draggedElt.open = false; 1811 } 1812 } 1813 1814 // Activate the view and cache the dragged element. 1815 this._draggedElt = draggedElt._placesNode; 1816 this._rootElt.focus(); 1817 1818 this._controller.setDataTransfer(aEvent); 1819 aEvent.stopPropagation(); 1820 } 1821 1822 _onDragOver(aEvent) { 1823 // Cache the dataTransfer 1824 PlacesControllerDragHelper.currentDropTarget = aEvent.target; 1825 let dt = aEvent.dataTransfer; 1826 1827 let dropPoint = this._getDropPoint(aEvent); 1828 if ( 1829 !dropPoint || 1830 !dropPoint.ip || 1831 !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt) 1832 ) { 1833 this._dropIndicator.collapsed = true; 1834 aEvent.stopPropagation(); 1835 return; 1836 } 1837 1838 if (dropPoint.folderElt || aEvent.originalTarget == this._chevron) { 1839 // Dropping over a menubutton or chevron button. 1840 // Set styles and timer to open relative menupopup. 1841 let overElt = dropPoint.folderElt || this._chevron; 1842 if (this._overFolder.elt != overElt) { 1843 this._clearOverFolder(); 1844 this._overFolder.elt = overElt; 1845 this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime); 1846 } 1847 if (!this._overFolder.elt.hasAttribute("dragover")) { 1848 this._overFolder.elt.setAttribute("dragover", "true"); 1849 } 1850 1851 this._dropIndicator.collapsed = true; 1852 } else { 1853 // Dragging over a normal toolbarbutton, 1854 // show indicator bar and move it to the appropriate drop point. 1855 let ind = this._dropIndicator; 1856 ind.parentNode.collapsed = false; 1857 let halfInd = ind.clientWidth / 2; 1858 let translateX; 1859 if (this.isRTL) { 1860 halfInd = Math.ceil(halfInd); 1861 translateX = 0 - this._rootElt.getBoundingClientRect().right - halfInd; 1862 if (this._rootElt.firstElementChild) { 1863 if (dropPoint.beforeIndex == -1) { 1864 translateX += 1865 this._rootElt.lastElementChild.getBoundingClientRect().left; 1866 } else { 1867 translateX += 1868 this._rootElt.children[ 1869 dropPoint.beforeIndex 1870 ].getBoundingClientRect().right; 1871 } 1872 } 1873 } else { 1874 halfInd = Math.floor(halfInd); 1875 translateX = 0 - this._rootElt.getBoundingClientRect().left + halfInd; 1876 if (this._rootElt.firstElementChild) { 1877 if (dropPoint.beforeIndex == -1) { 1878 translateX += 1879 this._rootElt.lastElementChild.getBoundingClientRect().right; 1880 } else { 1881 translateX += 1882 this._rootElt.children[ 1883 dropPoint.beforeIndex 1884 ].getBoundingClientRect().left; 1885 } 1886 } 1887 } 1888 1889 ind.style.transform = "translate(" + Math.round(translateX) + "px)"; 1890 ind.style.marginInlineStart = -ind.clientWidth + "px"; 1891 ind.collapsed = false; 1892 1893 // Clear out old folder information. 1894 this._clearOverFolder(); 1895 } 1896 1897 aEvent.preventDefault(); 1898 aEvent.stopPropagation(); 1899 } 1900 1901 _onDrop(aEvent) { 1902 PlacesControllerDragHelper.currentDropTarget = aEvent.target; 1903 1904 let dropPoint = this._getDropPoint(aEvent); 1905 if (dropPoint && dropPoint.ip) { 1906 PlacesControllerDragHelper.onDrop( 1907 dropPoint.ip, 1908 aEvent.dataTransfer 1909 ).catch(console.error); 1910 aEvent.preventDefault(); 1911 } 1912 1913 this._cleanupDragDetails(); 1914 aEvent.stopPropagation(); 1915 } 1916 1917 _onDragLeave() { 1918 PlacesControllerDragHelper.currentDropTarget = null; 1919 1920 this._dropIndicator.collapsed = true; 1921 1922 // If we hovered over a folder, close it now. 1923 if (this._overFolder.elt) { 1924 this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime); 1925 } 1926 } 1927 1928 _onDragEnd() { 1929 this._cleanupDragDetails(); 1930 } 1931 1932 _onPopupShowing(aEvent) { 1933 if (!this._allowPopupShowing) { 1934 this._allowPopupShowing = true; 1935 aEvent.preventDefault(); 1936 return; 1937 } 1938 1939 let parent = aEvent.target.parentNode; 1940 if (parent.localName == "toolbarbutton") { 1941 this._openedMenuButton = parent; 1942 } 1943 1944 super._onPopupShowing(aEvent); 1945 } 1946 1947 _onPopupHidden(aEvent) { 1948 let popup = aEvent.target; 1949 let placesNode = popup._placesNode; 1950 // Avoid handling popuphidden of inner views 1951 if ( 1952 placesNode && 1953 PlacesUIUtils.getViewForNode(popup) == this && 1954 // UI performance: folder queries are cheap, keep the resultnode open 1955 // so we don't rebuild its contents whenever the popup is reopened. 1956 !PlacesUtils.nodeIsFolderOrShortcut(placesNode) 1957 ) { 1958 placesNode.containerOpen = false; 1959 } 1960 1961 let parent = popup.parentNode; 1962 if (parent.localName == "toolbarbutton") { 1963 this._openedMenuButton = null; 1964 // Clear the dragover attribute if present, if we are dragging into a 1965 // folder in the hierachy of current opened popup we don't clear 1966 // this attribute on clearOverFolder. See Notify for closeTimer. 1967 if (parent.hasAttribute("dragover")) { 1968 parent.removeAttribute("dragover"); 1969 } 1970 } 1971 } 1972 1973 _onMouseMove(aEvent) { 1974 // Used in dragStart to prevent dragging folders when dragging down. 1975 this._cachedMouseMoveEvent = aEvent; 1976 1977 if ( 1978 this._openedMenuButton == null || 1979 PlacesControllerDragHelper.getSession() 1980 ) { 1981 return; 1982 } 1983 1984 let target = aEvent.originalTarget; 1985 if ( 1986 this._openedMenuButton != target && 1987 target.localName == "toolbarbutton" && 1988 target.type == "menu" 1989 ) { 1990 this._openedMenuButton.open = false; 1991 target.open = true; 1992 } 1993 } 1994 } 1995 1996 /** 1997 * View for Places menus. This object should be created during the first 1998 * popupshowing that's dispatched on the menu. 1999 * 2000 */ 2001 class PlacesMenu extends PlacesViewBase { 2002 /** 2003 * 2004 * @param {Event} popupShowingEvent 2005 * The event associated with opening the menu. 2006 * @param {string} placesUrl 2007 * The query associated with the view on the menu. 2008 */ 2009 constructor(popupShowingEvent, placesUrl) { 2010 super( 2011 placesUrl, 2012 popupShowingEvent.target, // <menupopup> 2013 popupShowingEvent.target.parentNode // <menu> 2014 ); 2015 2016 this._addEventListeners( 2017 this._rootElt, 2018 ["popupshowing", "popuphidden"], 2019 true 2020 ); 2021 this._addEventListeners(window, ["unload"], false); 2022 this._addEventListeners(this._rootElt, ["mousedown"], false); 2023 if (AppConstants.platform === "macosx") { 2024 // Must walk up to support views in sub-menus, like Bookmarks Toolbar menu. 2025 for (let elt = this._viewElt.parentNode; elt; elt = elt.parentNode) { 2026 if (elt.localName == "menubar") { 2027 this._nativeView = true; 2028 break; 2029 } 2030 } 2031 } 2032 2033 this._onPopupShowing(popupShowingEvent); 2034 } 2035 2036 _init() { 2037 this._viewElt._placesView = this; 2038 } 2039 2040 _removeChild(aChild) { 2041 super._removeChild(aChild); 2042 } 2043 2044 uninit() { 2045 this._removeEventListeners( 2046 this._rootElt, 2047 ["popupshowing", "popuphidden"], 2048 true 2049 ); 2050 this._removeEventListeners(window, ["unload"], false); 2051 this._removeEventListeners(this._rootElt, ["mousedown"], false); 2052 2053 super.uninit(); 2054 } 2055 2056 handleEvent(aEvent) { 2057 switch (aEvent.type) { 2058 case "unload": 2059 this.uninit(); 2060 break; 2061 case "popupshowing": 2062 this._onPopupShowing(aEvent); 2063 break; 2064 case "popuphidden": 2065 this._onPopupHidden(aEvent); 2066 break; 2067 case "mousedown": 2068 this._onMouseDown(aEvent); 2069 break; 2070 } 2071 } 2072 2073 _onPopupHidden(aEvent) { 2074 // Avoid handling popuphidden of inner views. 2075 let popup = aEvent.originalTarget; 2076 let placesNode = popup._placesNode; 2077 if (!placesNode || PlacesUIUtils.getViewForNode(popup) != this) { 2078 return; 2079 } 2080 2081 // UI performance: folder queries are cheap, keep the resultnode open 2082 // so we don't rebuild its contents whenever the popup is reopened. 2083 if (!PlacesUtils.nodeIsFolderOrShortcut(placesNode)) { 2084 placesNode.containerOpen = false; 2085 } 2086 2087 // The autoopened attribute is set for folders which have been 2088 // automatically opened when dragged over. Turn off this attribute 2089 // when the folder closes because it is no longer applicable. 2090 popup.removeAttribute("autoopened"); 2091 popup.removeAttribute("dragstart"); 2092 } 2093 2094 // We don't have a facility for catch "mousedown" events on the native 2095 // Mac menus because Mac doesn't expose it 2096 _onMouseDown(aEvent) { 2097 PlacesUIUtils.maybeSpeculativeConnectOnMouseDown(aEvent); 2098 } 2099 } 2100 2101 // This is used from CustomizableWidgets.sys.mjs using a `window` reference, 2102 // so we have to expose this on the global. 2103 this.PlacesPanelview = class PlacesPanelview extends PlacesViewBase { 2104 constructor(placeUrl, rootElt, viewElt) { 2105 super(placeUrl, rootElt, viewElt); 2106 this._viewElt._placesView = this; 2107 // We're simulating a popup show, because a panelview may only be shown when 2108 // its containing popup is already shown. 2109 this._onPopupShowing({ originalTarget: this._rootElt }); 2110 this._addEventListeners(window, ["unload"]); 2111 this._rootElt.setAttribute("context", "placesContext"); 2112 } 2113 2114 get events() { 2115 if (this._events) { 2116 return this._events; 2117 } 2118 return (this._events = [ 2119 "click", 2120 "command", 2121 "dragend", 2122 "dragstart", 2123 "ViewHiding", 2124 "ViewShown", 2125 "mousedown", 2126 ]); 2127 } 2128 2129 handleEvent(event) { 2130 switch (event.type) { 2131 case "click": 2132 // For middle clicks, fall through to the command handler. 2133 if (event.button != 1) { 2134 break; 2135 } 2136 // fall through 2137 case "command": 2138 this._onCommand(event); 2139 break; 2140 case "dragend": 2141 this._onDragEnd(event); 2142 break; 2143 case "dragstart": 2144 this._onDragStart(event); 2145 break; 2146 case "unload": 2147 this.uninit(event); 2148 break; 2149 case "ViewHiding": 2150 this._onPopupHidden(event); 2151 break; 2152 case "ViewShown": 2153 this._onViewShown(event); 2154 break; 2155 case "mousedown": 2156 this._onMouseDown(event); 2157 break; 2158 } 2159 } 2160 2161 _onCommand(event) { 2162 event = BrowserUtils.getRootEvent(event); 2163 let button = event.originalTarget; 2164 if (!button._placesNode) { 2165 return; 2166 } 2167 2168 let modifKey = 2169 AppConstants.platform === "macosx" ? event.metaKey : event.ctrlKey; 2170 if (!PlacesUIUtils.openInTabClosesMenu && modifKey) { 2171 // If 'Recent Bookmarks' in Bookmarks Panel. 2172 if (button.parentNode.id == "panelMenu_bookmarksMenu") { 2173 button.setAttribute("closemenu", "none"); 2174 } 2175 } else { 2176 button.removeAttribute("closemenu"); 2177 } 2178 PlacesUIUtils.openNodeWithEvent(button._placesNode, event); 2179 // Unlike left-click, middle-click requires manual menu closing. 2180 if ( 2181 button.parentNode.id != "panelMenu_bookmarksMenu" || 2182 (event.type == "click" && 2183 event.button == 1 && 2184 PlacesUIUtils.openInTabClosesMenu) 2185 ) { 2186 this.panelMultiView.closest("panel").hidePopup(); 2187 } 2188 } 2189 2190 destroyContextMenu() { 2191 super.destroyContextMenu(); 2192 this.maybeClosePanel(PlacesUIUtils.lastContextMenuCommand); 2193 } 2194 2195 /** 2196 * Closes the view depending on the command. 2197 * 2198 * This is necessary because PlacesPanelview's buttons are not 2199 * XUL menuitems and are not affected by the closemenu attribute. 2200 * 2201 * @param {string} command the placesCommands command 2202 */ 2203 maybeClosePanel(command) { 2204 switch (command) { 2205 // placesCmd_open:newcontainertab is not a placesCommand but it 2206 // is set by PlacesUIUtils.openInContainerTab to close the panel. 2207 case "placesCmd_open:newcontainertab": 2208 case "placesCmd_open:tab": 2209 if ( 2210 this._viewElt.id != "PanelUI-bookmarks" || 2211 PlacesUIUtils.openInTabClosesMenu 2212 ) { 2213 this.panelMultiView.closest("panel").hidePopup(); 2214 } 2215 break; 2216 case "placesCmd_createBookmark": 2217 case "placesCmd_deleteDataHost": 2218 this.panelMultiView.closest("panel").hidePopup(); 2219 break; 2220 } 2221 } 2222 2223 _onDragEnd() { 2224 this._draggedElt = null; 2225 } 2226 2227 _onDragStart(event) { 2228 let draggedElt = event.originalTarget; 2229 if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode) { 2230 return; 2231 } 2232 2233 // Activate the view and cache the dragged element. 2234 this._draggedElt = draggedElt._placesNode; 2235 this._rootElt.focus(); 2236 2237 this._controller.setDataTransfer(event); 2238 event.stopPropagation(); 2239 } 2240 2241 uninit(event) { 2242 this._removeEventListeners(this.panelMultiView, this.events); 2243 this._removeEventListeners(window, ["unload"]); 2244 delete this.panelMultiView; 2245 super.uninit(event); 2246 } 2247 2248 _createDOMNodeForPlacesNode(placesNode) { 2249 this._domNodes.delete(placesNode); 2250 2251 let element; 2252 let type = placesNode.type; 2253 if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { 2254 element = document.createXULElement("toolbarseparator"); 2255 } else { 2256 if (type != Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) { 2257 throw new Error("Unexpected node"); 2258 } 2259 2260 element = document.createXULElement("toolbarbutton"); 2261 element.classList.add( 2262 "subviewbutton", 2263 "subviewbutton-iconic", 2264 "bookmark-item" 2265 ); 2266 element.setAttribute( 2267 "scheme", 2268 PlacesUIUtils.guessUrlSchemeForUI(placesNode.uri) 2269 ); 2270 element.setAttribute("label", PlacesUIUtils.getBestTitle(placesNode)); 2271 2272 let icon = placesNode.icon; 2273 if (icon) { 2274 element.setAttribute("image", icon); 2275 } 2276 } 2277 2278 element._placesNode = placesNode; 2279 if (!this._domNodes.has(placesNode)) { 2280 this._domNodes.set(placesNode, element); 2281 } 2282 2283 return element; 2284 } 2285 2286 _setEmptyPopupStatus(panelview, empty = false) { 2287 if (!panelview._emptyMenuitem) { 2288 panelview._emptyMenuitem = document.createXULElement("toolbarbutton"); 2289 panelview._emptyMenuitem.setAttribute("disabled", true); 2290 panelview._emptyMenuitem.className = "subviewbutton"; 2291 document.l10n.setAttributes( 2292 panelview._emptyMenuitem, 2293 "places-empty-bookmarks-folder" 2294 ); 2295 } 2296 2297 if (empty) { 2298 panelview.setAttribute("emptyplacesresult", "true"); 2299 // Don't add the menuitem if there is static content. 2300 // We also support external usage for custom crafted panels - which'll have 2301 // no markers present. 2302 if ( 2303 !panelview._startMarker || 2304 (!panelview._startMarker.previousElementSibling && 2305 !panelview._endMarker.nextElementSibling) 2306 ) { 2307 panelview.insertBefore(panelview._emptyMenuitem, panelview._endMarker); 2308 } 2309 } else { 2310 panelview.removeAttribute("emptyplacesresult"); 2311 try { 2312 panelview.removeChild(panelview._emptyMenuitem); 2313 } catch (ex) {} 2314 } 2315 } 2316 2317 _isPopupOpen() { 2318 return PanelView.forNode(this._viewElt).active; 2319 } 2320 2321 _onPopupHidden(event) { 2322 let panelview = event.originalTarget; 2323 let placesNode = panelview._placesNode; 2324 // Avoid handling ViewHiding of inner views 2325 if ( 2326 placesNode && 2327 PlacesUIUtils.getViewForNode(panelview) == this && 2328 // UI performance: folder queries are cheap, keep the resultnode open 2329 // so we don't rebuild its contents whenever the popup is reopened. 2330 !PlacesUtils.nodeIsFolderOrShortcut(placesNode) 2331 ) { 2332 placesNode.containerOpen = false; 2333 } 2334 } 2335 2336 _onPopupShowing(event) { 2337 // If the event came from the root element, this is the first time 2338 // we ever get here. 2339 if (event.originalTarget == this._rootElt) { 2340 // Start listening for events from all panels inside the panelmultiview. 2341 this.panelMultiView = this._viewElt.panelMultiView; 2342 this._addEventListeners(this.panelMultiView, this.events); 2343 } 2344 super._onPopupShowing(event); 2345 } 2346 2347 _onViewShown(event) { 2348 if (event.originalTarget != this._viewElt) { 2349 return; 2350 } 2351 2352 // Because PanelMultiView reparents the panelview internally, the controller 2353 // may get lost. In that case we'll append it again, because we certainly 2354 // need it later! 2355 if (!this.controllers.getControllerCount() && this._controller) { 2356 this.controllers.appendController(this._controller); 2357 } 2358 } 2359 2360 _onMouseDown(aEvent) { 2361 PlacesUIUtils.maybeSpeculativeConnectOnMouseDown(aEvent); 2362 } 2363 };