browser-places.js (72454B)
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 XPCOMUtils.defineLazyPreferenceGetter( 6 this, 7 "NEWTAB_ENABLED", 8 "browser.newtabpage.enabled", 9 false 10 ); 11 12 XPCOMUtils.defineLazyPreferenceGetter( 13 this, 14 "SHOW_OTHER_BOOKMARKS", 15 "browser.toolbars.bookmarks.showOtherBookmarks", 16 true, 17 () => { 18 BookmarkingUI.maybeShowOtherBookmarksFolder().then(() => { 19 document 20 .getElementById("PlacesToolbar") 21 ?._placesView?.updateNodesVisibility(); 22 }, console.error); 23 } 24 ); 25 26 // Set by sync after syncing bookmarks successfully once. 27 XPCOMUtils.defineLazyPreferenceGetter( 28 this, 29 "SHOW_MOBILE_BOOKMARKS", 30 "browser.bookmarks.showMobileBookmarks", 31 false 32 ); 33 34 ChromeUtils.defineESModuleGetters(this, { 35 PanelMultiView: 36 "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs", 37 RecentlyClosedTabsAndWindowsMenuUtils: 38 "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs", 39 }); 40 41 var StarUI = { 42 userHasTags: undefined, 43 _itemGuids: null, 44 _isNewBookmark: false, 45 _isComposing: false, 46 _autoCloseTimer: 0, 47 // The autoclose timer is diasbled if the user interacts with the 48 // popup, such as making a change through typing or clicking on 49 // the popup. 50 _autoCloseTimerEnabled: true, 51 // The autoclose timeout length. 52 _autoCloseTimeout: 3500, 53 _removeBookmarksOnPopupHidden: false, 54 55 _element(aID) { 56 return document.getElementById(aID); 57 }, 58 59 // Edit-bookmark panel 60 get panel() { 61 delete this.panel; 62 this._createPanelIfNeeded(); 63 var element = this._element("editBookmarkPanel"); 64 // initially the panel is hidden 65 // to avoid impacting startup / new window performance 66 element.hidden = false; 67 element.addEventListener("keypress", this, { mozSystemGroup: true }); 68 element.addEventListener("mousedown", this); 69 element.addEventListener("mouseout", this); 70 element.addEventListener("mousemove", this); 71 element.addEventListener("compositionstart", this); 72 element.addEventListener("compositionend", this); 73 element.addEventListener("input", this); 74 element.addEventListener("popuphidden", this); 75 element.addEventListener("popupshown", this); 76 return (this.panel = element); 77 }, 78 79 // nsIDOMEventListener 80 handleEvent(aEvent) { 81 switch (aEvent.type) { 82 case "mousemove": 83 clearTimeout(this._autoCloseTimer); 84 // The autoclose timer is not disabled on generic mouseout 85 // because the user may not have actually interacted with the popup. 86 break; 87 case "popuphidden": { 88 clearTimeout(this._autoCloseTimer); 89 if (aEvent.originalTarget == this.panel) { 90 this._handlePopupHiddenEvent().catch(console.error); 91 } 92 break; 93 } 94 case "keypress": 95 clearTimeout(this._autoCloseTimer); 96 this._autoCloseTimerEnabled = false; 97 98 if (aEvent.defaultPrevented) { 99 // The event has already been consumed inside of the panel. 100 break; 101 } 102 103 switch (aEvent.keyCode) { 104 case KeyEvent.DOM_VK_ESCAPE: 105 if (this._isNewBookmark) { 106 this._removeBookmarksOnPopupHidden = true; 107 } 108 this.panel.hidePopup(); 109 break; 110 case KeyEvent.DOM_VK_RETURN: 111 if ( 112 aEvent.target.classList.contains("expander-up") || 113 aEvent.target.classList.contains("expander-down") || 114 aEvent.target.id == "editBMPanel_newFolderButton" || 115 aEvent.target.id == "editBookmarkPanelRemoveButton" 116 ) { 117 // XXX Why is this necessary? The defaultPrevented check should 118 // be enough. 119 break; 120 } 121 this.panel.hidePopup(); 122 break; 123 // This case is for catching character-generating keypresses 124 case 0: { 125 let accessKey = document.getElementById("key_close"); 126 if (eventMatchesKey(aEvent, accessKey)) { 127 this.panel.hidePopup(); 128 } 129 break; 130 } 131 } 132 break; 133 case "compositionend": 134 // After composition is committed, "mouseout" or something can set 135 // auto close timer. 136 this._isComposing = false; 137 break; 138 case "compositionstart": 139 if (aEvent.defaultPrevented) { 140 // If the composition was canceled, nothing to do here. 141 break; 142 } 143 this._isComposing = true; 144 // Explicit fall-through, during composition, panel shouldn't be hidden automatically. 145 case "input": 146 // Might have edited some text without keyboard events nor composition 147 // events. Fall-through to cancel auto close in such case. 148 case "mousedown": 149 clearTimeout(this._autoCloseTimer); 150 this._autoCloseTimerEnabled = false; 151 break; 152 case "mouseout": 153 if (!this._autoCloseTimerEnabled) { 154 // Don't autoclose the popup if the user has made a selection 155 // or keypress and then subsequently mouseout. 156 break; 157 } 158 // Explicit fall-through 159 case "popupshown": 160 // Don't handle events for descendent elements. 161 if (aEvent.target != aEvent.currentTarget) { 162 break; 163 } 164 // auto-close if new and not interacted with 165 if (this._isNewBookmark && !this._isComposing) { 166 let delay = this._autoCloseTimeout; 167 if (this._closePanelQuickForTesting) { 168 delay /= 10; 169 } 170 clearTimeout(this._autoCloseTimer); 171 this._autoCloseTimer = setTimeout(() => { 172 if (!this.panel.matches(":hover")) { 173 this.panel.hidePopup(true); 174 } 175 }, delay); 176 this._autoCloseTimerEnabled = true; 177 } 178 break; 179 } 180 }, 181 182 /** 183 * Handle popup hidden event. 184 */ 185 async _handlePopupHiddenEvent() { 186 const { bookmarkState, didChangeFolder, selectedFolderGuid } = 187 gEditItemOverlay; 188 gEditItemOverlay.uninitPanel(true); 189 190 // Capture _removeBookmarksOnPopupHidden and _itemGuids values. Reset them 191 // before we handle the next popup. 192 const removeBookmarksOnPopupHidden = this._removeBookmarksOnPopupHidden; 193 this._removeBookmarksOnPopupHidden = false; 194 const guidsForRemoval = this._itemGuids; 195 this._itemGuids = null; 196 197 if (removeBookmarksOnPopupHidden && guidsForRemoval) { 198 if (!this._isNewBookmark) { 199 // Remove all bookmarks for the bookmark's url, this also removes 200 // the tags for the url. 201 await PlacesTransactions.Remove(guidsForRemoval).transact(); 202 } else { 203 BookmarkingUI.star.removeAttribute("starred"); 204 } 205 return; 206 } 207 208 await this._storeRecentlyUsedFolder(selectedFolderGuid, didChangeFolder); 209 await bookmarkState.save(); 210 if (this._isNewBookmark) { 211 this.showConfirmation(); 212 } 213 }, 214 215 async showEditBookmarkPopup(aNode, aIsNewBookmark, aUrl) { 216 // Slow double-clicks (not true double-clicks) shouldn't 217 // cause the panel to flicker. 218 if (this.panel.state != "closed") { 219 return; 220 } 221 222 this._isNewBookmark = aIsNewBookmark; 223 this._itemGuids = null; 224 225 let titleL10nID = this._isNewBookmark 226 ? "bookmarks-add-bookmark" 227 : "bookmarks-edit-bookmark"; 228 document.l10n.setAttributes( 229 this._element("editBookmarkPanelTitle"), 230 titleL10nID 231 ); 232 233 this._element("editBookmarkPanel_showForNewBookmarks").checked = 234 this.showForNewBookmarks; 235 236 this._itemGuids = []; 237 await PlacesUtils.bookmarks.fetch({ url: aUrl }, bookmark => 238 this._itemGuids.push(bookmark.guid) 239 ); 240 241 let removeButton = this._element("editBookmarkPanelRemoveButton"); 242 if (this._isNewBookmark) { 243 document.l10n.setAttributes(removeButton, "bookmark-panel-cancel"); 244 } else { 245 // The label of the remove button differs if the URI is bookmarked 246 // multiple times. 247 document.l10n.setAttributes(removeButton, "bookmark-panel-remove", { 248 count: this._itemGuids.length, 249 }); 250 } 251 252 let onPanelReady = fn => { 253 let target = this.panel; 254 if (target.parentNode) { 255 // By targeting the panel's parent and using a capturing listener, we 256 // can have our listener called before others waiting for the panel to 257 // be shown (which probably expect the panel to be fully initialized) 258 target = target.parentNode; 259 } 260 target.addEventListener( 261 "popupshown", 262 function () { 263 fn(); 264 }, 265 { capture: true, once: true } 266 ); 267 }; 268 269 let hiddenRows = ["location", "keyword"]; 270 271 if (this.userHasTags === undefined) { 272 // Cache must be initialized 273 const fetchedTags = await PlacesUtils.bookmarks.fetchTags(); 274 this.userHasTags = !!fetchedTags.length; 275 } 276 277 if (!this.userHasTags) { 278 // Hide tags ui because user has no tags defined 279 hiddenRows.push("tags"); 280 } 281 282 await gEditItemOverlay.initPanel({ 283 node: aNode, 284 onPanelReady, 285 hiddenRows, 286 focusedElement: "preferred", 287 isNewBookmark: this._isNewBookmark, 288 }); 289 290 this.panel.openPopup(BookmarkingUI.anchor, "bottomright topright"); 291 }, 292 293 _createPanelIfNeeded() { 294 // Lazy load the editBookmarkPanel the first time we need to display it. 295 if (!this._element("editBookmarkPanel")) { 296 MozXULElement.insertFTLIfNeeded("browser/editBookmarkOverlay.ftl"); 297 let template = this._element("editBookmarkPanelTemplate"); 298 let clone = template.content.cloneNode(true); 299 template.replaceWith(clone); 300 } 301 }, 302 303 removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() { 304 this._removeBookmarksOnPopupHidden = true; 305 this.panel.hidePopup(); 306 }, 307 308 async _storeRecentlyUsedFolder(selectedFolderGuid, didChangeFolder) { 309 if (!selectedFolderGuid) { 310 return; 311 } 312 313 // If we're changing where a bookmark gets saved, persist that location. 314 if (didChangeFolder) { 315 Services.prefs.setCharPref( 316 "browser.bookmarks.defaultLocation", 317 selectedFolderGuid 318 ); 319 } 320 321 // Don't store folders that are always displayed in "Recent Folders". 322 if (PlacesUtils.bookmarks.userContentRoots.includes(selectedFolderGuid)) { 323 return; 324 } 325 326 // List of recently used folders: 327 let lastUsedFolderGuids = await PlacesUtils.metadata.get( 328 PlacesUIUtils.LAST_USED_FOLDERS_META_KEY, 329 [] 330 ); 331 332 let index = lastUsedFolderGuids.indexOf(selectedFolderGuid); 333 if (index > 1) { 334 // The guid is in the array but not the most recent. 335 lastUsedFolderGuids.splice(index, 1); 336 lastUsedFolderGuids.unshift(selectedFolderGuid); 337 } else if (index == -1) { 338 lastUsedFolderGuids.unshift(selectedFolderGuid); 339 } 340 while (lastUsedFolderGuids.length > PlacesUIUtils.maxRecentFolders) { 341 lastUsedFolderGuids.pop(); 342 } 343 344 await PlacesUtils.metadata.set( 345 PlacesUIUtils.LAST_USED_FOLDERS_META_KEY, 346 lastUsedFolderGuids 347 ); 348 }, 349 350 onShowForNewBookmarksCheckboxCommand() { 351 Services.prefs.setBoolPref( 352 "browser.bookmarks.editDialog.showForNewBookmarks", 353 this._element("editBookmarkPanel_showForNewBookmarks").checked 354 ); 355 }, 356 357 showConfirmation() { 358 // Show the "Saved to bookmarks" hint for the first three times 359 const HINT_COUNT_PREF = 360 "browser.bookmarks.editDialog.confirmationHintShowCount"; 361 const HINT_COUNT = Services.prefs.getIntPref(HINT_COUNT_PREF, 0); 362 363 if (HINT_COUNT >= 3) { 364 return; 365 } 366 Services.prefs.setIntPref(HINT_COUNT_PREF, HINT_COUNT + 1); 367 368 let anchor; 369 if (window.toolbar.visible) { 370 for (let id of ["library-button", "bookmarks-menu-button"]) { 371 let element = document.getElementById(id); 372 if ( 373 element && 374 element.getAttribute("cui-areatype") != "panel" && 375 element.getAttribute("overflowedItem") != "true" 376 ) { 377 anchor = element; 378 break; 379 } 380 } 381 } 382 if (!anchor) { 383 anchor = document.getElementById("PanelUI-menu-button"); 384 } 385 ConfirmationHint.show(anchor, "confirmation-hint-page-bookmarked"); 386 }, 387 }; 388 389 XPCOMUtils.defineLazyPreferenceGetter( 390 StarUI, 391 "showForNewBookmarks", 392 "browser.bookmarks.editDialog.showForNewBookmarks" 393 ); 394 395 var PlacesCommandHook = { 396 /** 397 * Adds a bookmark to the page loaded in the current browser. 398 */ 399 async bookmarkPage() { 400 let browser = gBrowser.selectedBrowser; 401 let url = URL.fromURI(Services.io.createExposableURI(browser.currentURI)); 402 let info = await PlacesUtils.bookmarks.fetch({ url }); 403 let isNewBookmark = !info; 404 let showEditUI = !isNewBookmark || StarUI.showForNewBookmarks; 405 if (isNewBookmark) { 406 // This is async because we have to validate the guid 407 // coming from prefs. 408 let parentGuid = await PlacesUIUtils.defaultParentGuid; 409 info = { url, parentGuid }; 410 // Bug 1148838 - Make this code work for full page plugins. 411 let charset = null; 412 413 let isErrorPage = false; 414 if (browser.documentURI) { 415 isErrorPage = /^about:(neterror|certerror|blocked)/.test( 416 browser.documentURI.spec 417 ); 418 } 419 420 try { 421 if (isErrorPage) { 422 let entry = await PlacesUtils.history.fetch(browser.currentURI); 423 if (entry) { 424 info.title = entry.title; 425 } 426 } else { 427 info.title = browser.contentTitle; 428 } 429 info.title = info.title || url.href; 430 charset = browser.characterSet; 431 } catch (e) { 432 console.error(e); 433 } 434 435 if (!StarUI.showForNewBookmarks) { 436 info.guid = await PlacesTransactions.NewBookmark(info).transact(); 437 } else { 438 info.guid = PlacesUtils.bookmarks.unsavedGuid; 439 BookmarkingUI.star.setAttribute("starred", "true"); 440 } 441 442 if (charset) { 443 PlacesUIUtils.setCharsetForPage(url, charset, window).catch( 444 console.error 445 ); 446 } 447 } 448 449 // Revert the contents of the location bar 450 gURLBar.handleRevert(); 451 452 // If it was not requested to open directly in "edit" mode, we are done. 453 if (!showEditUI) { 454 StarUI.showConfirmation(); 455 return; 456 } 457 458 let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(info); 459 460 await StarUI.showEditBookmarkPopup(node, isNewBookmark, url); 461 }, 462 463 /** 464 * Adds a bookmark to the page targeted by a link. 465 * 466 * @param url (string) 467 * the address of the link target 468 * @param title 469 * The link text 470 */ 471 async bookmarkLink(url, title) { 472 let bm = await PlacesUtils.bookmarks.fetch({ url }); 473 if (bm) { 474 let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm); 475 await PlacesUIUtils.showBookmarkDialog( 476 { action: "edit", node }, 477 window.top 478 ); 479 return; 480 } 481 482 let parentGuid = await PlacesUIUtils.defaultParentGuid; 483 let defaultInsertionPoint = new PlacesInsertionPoint({ 484 parentGuid, 485 }); 486 await PlacesUIUtils.showBookmarkDialog( 487 { 488 action: "add", 489 type: "bookmark", 490 uri: Services.io.newURI(url), 491 title, 492 defaultInsertionPoint, 493 hiddenRows: ["location", "keyword"], 494 }, 495 window.top 496 ); 497 }, 498 499 /** 500 * Bookmarks the given tabs loaded in the current browser. 501 * 502 * @param {Array} tabs 503 * If no given tabs, bookmark all current tabs. 504 */ 505 async bookmarkTabs(tabs) { 506 tabs = tabs ?? gBrowser.visibleTabs.filter(tab => !tab.pinned); 507 let pages = PlacesCommandHook.getUniquePages(tabs).map( 508 // Bookmark exposable url. 509 page => 510 Object.assign(page, { uri: Services.io.createExposableURI(page.uri) }) 511 ); 512 await PlacesUIUtils.showBookmarkPagesDialog(pages); 513 }, 514 515 /** 516 * List of nsIURI objects characterizing tabs given in param. 517 * Duplicates are discarded. 518 */ 519 getUniquePages(tabs) { 520 let uniquePages = {}; 521 let URIs = []; 522 523 tabs.forEach(tab => { 524 let browser = tab.linkedBrowser; 525 let uri = browser.currentURI; 526 let title = browser.contentTitle || tab.label; 527 let spec = uri.spec; 528 if (!(spec in uniquePages)) { 529 uniquePages[spec] = null; 530 URIs.push({ uri, title }); 531 } 532 }); 533 return URIs; 534 }, 535 536 /** 537 * Opens the Places Organizer. 538 * 539 * @param {string} item The item to select in the organizer window, 540 * options are (case sensitive): 541 * BookmarksMenu, BookmarksToolbar, UnfiledBookmarks, 542 * AllBookmarks, History, Downloads. 543 */ 544 showPlacesOrganizer(item) { 545 var organizer = Services.wm.getMostRecentWindow("Places:Organizer"); 546 // Due to bug 528706, getMostRecentWindow can return closed windows. 547 if (!organizer || organizer.closed) { 548 // No currently open places window, so open one with the specified mode. 549 openDialog( 550 "chrome://browser/content/places/places.xhtml", 551 "", 552 "chrome,toolbar=yes,dialog=no,resizable", 553 item 554 ); 555 } else { 556 organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(item); 557 organizer.focus(); 558 } 559 }, 560 561 async searchBookmarks() { 562 let win = 563 BrowserWindowTracker.getTopWindow() ?? 564 (await BrowserWindowTracker.promiseOpenWindow()); 565 win.gURLBar.search(UrlbarTokenizer.RESTRICT.BOOKMARK, { 566 searchModeEntry: "bookmarkmenu", 567 }); 568 }, 569 570 async searchHistory() { 571 let win = 572 BrowserWindowTracker.getTopWindow() ?? 573 (await BrowserWindowTracker.promiseOpenWindow()); 574 win.gURLBar.search(UrlbarTokenizer.RESTRICT.HISTORY, { 575 searchModeEntry: "historymenu", 576 }); 577 }, 578 }; 579 580 // View for the history menu. 581 class HistoryMenu extends PlacesMenu { 582 constructor(aPopupShowingEvent) { 583 super(aPopupShowingEvent, "place:sort=4&maxResults=15"); 584 } 585 586 // Called by the base class (PlacesViewBase) so we can initialize some 587 // element references before the several superclass constructors call our 588 // methods which depend on these. 589 _init() { 590 super._init(); 591 let elements = { 592 undoTabMenu: "historyUndoMenu", 593 hiddenTabsMenu: "hiddenTabsMenu", 594 undoWindowMenu: "historyUndoWindowMenu", 595 syncTabsMenuitem: "sync-tabs-menuitem", 596 }; 597 for (let [key, elemId] of Object.entries(elements)) { 598 this[key] = document.getElementById(elemId); 599 } 600 } 601 602 toggleHiddenTabs() { 603 const isShown = 604 window.gBrowser && gBrowser.visibleTabs.length < gBrowser.tabs.length; 605 this.hiddenTabsMenu.hidden = !isShown; 606 } 607 608 toggleRecentlyClosedTabs() { 609 // enable/disable the Recently Closed Tabs sub menu 610 // no restorable tabs, so disable menu 611 if (SessionStore.getClosedTabCount() == 0) { 612 this.undoTabMenu.setAttribute("disabled", true); 613 } else { 614 this.undoTabMenu.removeAttribute("disabled"); 615 } 616 } 617 618 /** 619 * Populate when the history menu is opened 620 */ 621 populateUndoSubmenu() { 622 var undoPopup = this.undoTabMenu.menupopup; 623 624 // remove existing menu items 625 while (undoPopup.hasChildNodes()) { 626 undoPopup.firstChild.remove(); 627 } 628 629 // no restorable tabs, so make sure menu is disabled, and return 630 if (SessionStore.getClosedTabCount() == 0) { 631 this.undoTabMenu.setAttribute("disabled", true); 632 return; 633 } 634 635 // enable menu 636 this.undoTabMenu.removeAttribute("disabled"); 637 638 // populate menu 639 let tabsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getTabsFragment( 640 window, 641 "menuitem" 642 ); 643 undoPopup.appendChild(tabsFragment); 644 } 645 646 toggleRecentlyClosedWindows() { 647 // enable/disable the Recently Closed Windows sub menu 648 // no restorable windows, so disable menu 649 if (SessionStore.getClosedWindowCount() == 0) { 650 this.undoWindowMenu.setAttribute("disabled", true); 651 } else { 652 this.undoWindowMenu.removeAttribute("disabled"); 653 } 654 } 655 656 /** 657 * Populate when the history menu is opened 658 */ 659 populateUndoWindowSubmenu() { 660 let undoPopup = this.undoWindowMenu.menupopup; 661 662 // remove existing menu items 663 while (undoPopup.hasChildNodes()) { 664 undoPopup.firstChild.remove(); 665 } 666 667 // no restorable windows, so make sure menu is disabled, and return 668 if (SessionStore.getClosedWindowCount() == 0) { 669 this.undoWindowMenu.setAttribute("disabled", true); 670 return; 671 } 672 673 // enable menu 674 this.undoWindowMenu.removeAttribute("disabled"); 675 676 // populate menu 677 let windowsFragment = 678 RecentlyClosedTabsAndWindowsMenuUtils.getWindowsFragment( 679 window, 680 "menuitem", 681 /* aPrefixRestoreAll = */ false 682 ); 683 undoPopup.appendChild(windowsFragment); 684 } 685 686 toggleTabsFromOtherComputers() { 687 // Enable/disable the Tabs From Other Computers menu. Some of the menus handled 688 // by HistoryMenu do not have this menuitem. 689 if (!this.syncTabsMenuitem) { 690 return; 691 } 692 693 if (!PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem()) { 694 this.syncTabsMenuitem.hidden = true; 695 return; 696 } 697 698 this.syncTabsMenuitem.hidden = false; 699 } 700 701 _onPopupShowing(aEvent) { 702 super._onPopupShowing(aEvent); 703 704 // Don't handle events for submenus. 705 if (aEvent.target != this.rootElement) { 706 return; 707 } 708 709 this.toggleHiddenTabs(); 710 this.toggleRecentlyClosedTabs(); 711 this.toggleRecentlyClosedWindows(); 712 this.toggleTabsFromOtherComputers(); 713 } 714 715 _onCommand(aEvent) { 716 aEvent = BrowserUtils.getRootEvent(aEvent); 717 let placesNode = aEvent.target._placesNode; 718 if (placesNode) { 719 if (!PrivateBrowsingUtils.isWindowPrivate(window)) { 720 PlacesUIUtils.markPageAsTyped(placesNode.uri); 721 } 722 openUILink(placesNode.uri, aEvent, { 723 ignoreAlt: true, 724 triggeringPrincipal: 725 Services.scriptSecurityManager.getSystemPrincipal(), 726 }); 727 } 728 } 729 } 730 731 /** 732 * Functions for handling events in the Bookmarks Toolbar and menu. 733 */ 734 var BookmarksEventHandler = { 735 /** 736 * Handler for click event for an item in the bookmarks toolbar or menu. 737 * Menus and submenus from the folder buttons bubble up to this handler. 738 * Left-click is handled in the onCommand function. 739 * When items are middle-clicked (or clicked with modifier), open in tabs. 740 * If the click came through a menu, close the menu. 741 * 742 * @param aEvent 743 * DOMEvent for the click 744 * @param aView 745 * The places view which aEvent should be associated with. 746 */ 747 748 onMouseUp(aEvent) { 749 // Handles middle-click or left-click with modifier if not browser.bookmarks.openInTabClosesMenu. 750 if (aEvent.button == 2 || PlacesUIUtils.openInTabClosesMenu) { 751 return; 752 } 753 let target = aEvent.originalTarget; 754 if (target.tagName != "menuitem") { 755 return; 756 } 757 let modifKey = 758 AppConstants.platform === "macosx" ? aEvent.metaKey : aEvent.ctrlKey; 759 if (modifKey || aEvent.button == 1) { 760 target.setAttribute("closemenu", "none"); 761 var menupopup = target.parentNode; 762 menupopup.addEventListener( 763 "popuphidden", 764 () => { 765 target.removeAttribute("closemenu"); 766 }, 767 { once: true } 768 ); 769 } else { 770 // Handles edge case where same menuitem was opened previously 771 // while menu was kept open, but now menu should close. 772 target.removeAttribute("closemenu"); 773 } 774 }, 775 776 onClick: function BEH_onClick(aEvent, aView) { 777 // Only handle middle-click or left-click with modifiers. 778 let modifKey; 779 if (AppConstants.platform == "macosx") { 780 modifKey = aEvent.metaKey || aEvent.shiftKey; 781 } else { 782 modifKey = aEvent.ctrlKey || aEvent.shiftKey; 783 } 784 785 if (aEvent.button == 2 || (aEvent.button == 0 && !modifKey)) { 786 return; 787 } 788 789 var target = aEvent.originalTarget; 790 // If this event bubbled up from a menu or menuitem, 791 // close the menus if browser.bookmarks.openInTabClosesMenu. 792 var tag = target.tagName; 793 if ( 794 PlacesUIUtils.openInTabClosesMenu && 795 (tag == "menuitem" || tag == "menu") 796 ) { 797 closeMenus(aEvent.target); 798 } 799 800 if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) { 801 // Don't open the root folder in tabs when the empty area on the toolbar 802 // is middle-clicked or when a non-bookmark item (except for Open in Tabs) 803 // in a bookmarks menupopup is middle-clicked. 804 if (target.localName == "menu" || target.localName == "toolbarbutton") { 805 PlacesUIUtils.openMultipleLinksInTabs( 806 target._placesNode, 807 aEvent, 808 aView 809 ); 810 } 811 } else if (aEvent.button == 1 && !(tag == "menuitem" || tag == "menu")) { 812 // Call onCommand in the cases where it's not called automatically: 813 // Middle-clicks outside of menus. 814 this.onCommand(aEvent); 815 aEvent.preventDefault(); 816 aEvent.stopPropagation(); 817 } 818 }, 819 820 /** 821 * Handler for command event for an item in the bookmarks toolbar. 822 * Menus and submenus from the folder buttons bubble up to this handler. 823 * Opens the item. 824 * 825 * @param aEvent 826 * DOMEvent for the command 827 */ 828 onCommand: function BEH_onCommand(aEvent) { 829 var target = aEvent.originalTarget; 830 if (target._placesNode) { 831 PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent); 832 // Only record interactions through the Bookmarks Toolbar 833 if (target.closest("#PersonalToolbar")) { 834 Glean.browserEngagement.bookmarksToolbarBookmarkOpened.add(1); 835 } 836 } 837 }, 838 839 fillInBHTooltip: function BEH_fillInBHTooltip(aTooltip, aEvent) { 840 var node; 841 var cropped = false; 842 var targetURI; 843 844 if (aTooltip.triggerNode.localName == "treechildren") { 845 var tree = aTooltip.triggerNode.parentNode; 846 var cell = tree.getCellAt(aEvent.clientX, aEvent.clientY); 847 if (cell.row == -1) { 848 aEvent.preventDefault(); 849 return; 850 } 851 node = tree.view.nodeForTreeIndex(cell.row); 852 cropped = tree.isCellCropped(cell.row, cell.col); 853 } else { 854 // Check whether the tooltipNode is a Places node. 855 // In such a case use it, otherwise check for targetURI attribute. 856 var tooltipNode = aTooltip.triggerNode; 857 if (tooltipNode._placesNode) { 858 node = tooltipNode._placesNode; 859 } else { 860 // This is a static non-Places node. 861 targetURI = tooltipNode.getAttribute("targetURI"); 862 } 863 } 864 865 if (!node && !targetURI) { 866 aEvent.preventDefault(); 867 return; 868 } 869 870 // Show node.label as tooltip's title for non-Places nodes. 871 var title = node ? node.title : tooltipNode.label; 872 873 // Show URL only for Places URI-nodes or nodes with a targetURI attribute. 874 var url; 875 if (targetURI || PlacesUtils.nodeIsURI(node)) { 876 url = targetURI || node.uri; 877 } 878 879 // Show tooltip for containers only if their title is cropped. 880 if (!cropped && !url) { 881 aEvent.preventDefault(); 882 return; 883 } 884 885 let tooltipTitle = aEvent.target.querySelector(".places-tooltip-title"); 886 tooltipTitle.hidden = !title || title == url; 887 if (!tooltipTitle.hidden) { 888 tooltipTitle.textContent = title; 889 } 890 891 let tooltipUrl = aEvent.target.querySelector(".places-tooltip-uri"); 892 tooltipUrl.hidden = !url; 893 if (!tooltipUrl.hidden) { 894 // Use `value` instead of `textContent` so cropping will apply 895 tooltipUrl.value = url; 896 } 897 898 // Show tooltip. 899 }, 900 }; 901 902 // Handles special drag and drop functionality for Places menus that are not 903 // part of a Places view (e.g. the bookmarks menu in the menubar). 904 var PlacesMenuDNDHandler = { 905 _springLoadDelayMs: 350, 906 _closeDelayMs: 500, 907 _loadTimer: null, 908 _closeTimer: null, 909 _closingTimerNode: null, 910 911 /** 912 * Called when the user enters the <menu> element during a drag. 913 * 914 * @param event 915 * The DragEnter event that spawned the opening. 916 */ 917 onDragEnter: function PMDH_onDragEnter(event) { 918 // Opening menus in a Places popup is handled by the view itself. 919 if (!this._isStaticContainer(event.target)) { 920 return; 921 } 922 923 // If we re-enter the same menu or anchor before the close timer runs out, 924 // we should ensure that we do not close: 925 if (this._closeTimer && this._closingTimerNode === event.currentTarget) { 926 this._closeTimer.cancel(); 927 this._closingTimerNode = null; 928 this._closeTimer = null; 929 } 930 931 PlacesControllerDragHelper.currentDropTarget = event.target; 932 let popup = event.target.menupopup; 933 if ( 934 this._loadTimer || 935 popup.state === "showing" || 936 popup.state === "open" 937 ) { 938 return; 939 } 940 941 this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 942 this._loadTimer.initWithCallback( 943 () => { 944 this._loadTimer = null; 945 popup.setAttribute("autoopened", "true"); 946 popup.openPopup(); 947 }, 948 this._springLoadDelayMs, 949 Ci.nsITimer.TYPE_ONE_SHOT 950 ); 951 event.preventDefault(); 952 event.stopPropagation(); 953 }, 954 955 /** 956 * Handles dragleave on the <menu> element. 957 */ 958 onDragLeave: function PMDH_onDragLeave(event) { 959 // Handle menu-button separate targets. 960 if ( 961 event.relatedTarget === event.currentTarget || 962 (event.relatedTarget && 963 event.relatedTarget.parentNode === event.currentTarget) 964 ) { 965 return; 966 } 967 968 // Closing menus in a Places popup is handled by the view itself. 969 if (!this._isStaticContainer(event.target)) { 970 return; 971 } 972 973 PlacesControllerDragHelper.currentDropTarget = null; 974 let popup = event.target.menupopup; 975 976 if (this._loadTimer) { 977 this._loadTimer.cancel(); 978 this._loadTimer = null; 979 } 980 this._closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 981 this._closingTimerNode = event.currentTarget; 982 this._closeTimer.initWithCallback( 983 function () { 984 this._closeTimer = null; 985 this._closingTimerNode = null; 986 let node = PlacesControllerDragHelper.currentDropTarget; 987 let inHierarchy = false; 988 while (node && !inHierarchy) { 989 inHierarchy = node == event.target; 990 node = node.parentNode; 991 } 992 if (!inHierarchy && popup && popup.hasAttribute("autoopened")) { 993 popup.removeAttribute("autoopened"); 994 popup.hidePopup(); 995 } 996 }, 997 this._closeDelayMs, 998 Ci.nsITimer.TYPE_ONE_SHOT 999 ); 1000 }, 1001 1002 /** 1003 * Determines if a XUL element represents a static container. 1004 * 1005 * @returns true if the element is a container element (menu or 1006 *` menu-toolbarbutton), false otherwise. 1007 */ 1008 _isStaticContainer: function PMDH__isContainer(node) { 1009 let isMenu = 1010 node.localName == "menu" || 1011 (node.localName == "toolbarbutton" && 1012 node.getAttribute("type") == "menu"); 1013 let isStatic = 1014 !("_placesNode" in node) && 1015 node.menupopup && 1016 node.menupopup.hasAttribute("placespopup") && 1017 !node.parentNode.hasAttribute("placespopup"); 1018 return isMenu && isStatic; 1019 }, 1020 1021 /** 1022 * Called when the user drags over the <menu> element. 1023 * 1024 * @param event 1025 * The DragOver event. 1026 */ 1027 onDragOver: function PMDH_onDragOver(event) { 1028 PlacesControllerDragHelper.currentDropTarget = event.target; 1029 let ip = new PlacesInsertionPoint({ 1030 parentGuid: PlacesUtils.bookmarks.menuGuid, 1031 }); 1032 if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer)) { 1033 event.preventDefault(); 1034 } 1035 1036 event.stopPropagation(); 1037 }, 1038 1039 /** 1040 * Called when the user drops on the <menu> element. 1041 * 1042 * @param event 1043 * The Drop event. 1044 */ 1045 onDrop: function PMDH_onDrop(event) { 1046 // Put the item at the end of bookmark menu. 1047 let ip = new PlacesInsertionPoint({ 1048 parentGuid: PlacesUtils.bookmarks.menuGuid, 1049 }); 1050 PlacesControllerDragHelper.onDrop(ip, event.dataTransfer); 1051 PlacesControllerDragHelper.currentDropTarget = null; 1052 event.stopPropagation(); 1053 }, 1054 }; 1055 1056 /** 1057 * This object handles the initialization and uninitialization of the bookmarks 1058 * toolbar. It also has helper functions for the managed bookmarks button. 1059 */ 1060 var PlacesToolbarHelper = { 1061 get _viewElt() { 1062 return document.getElementById("PlacesToolbar"); 1063 }, 1064 1065 /** 1066 * Initialize. This will check whether we've finished startup and can 1067 * show toolbars. 1068 */ 1069 async init() { 1070 await PlacesUIUtils.canLoadToolbarContentPromise; 1071 this._realInit(); 1072 }, 1073 1074 /** 1075 * Actually initialize the places view (if needed; we might still no-op). 1076 */ 1077 _realInit() { 1078 let viewElt = this._viewElt; 1079 if (!viewElt || viewElt._placesView || window.closed) { 1080 return; 1081 } 1082 1083 // CustomizableUI.addListener is idempotent, so we can safely 1084 // call this multiple times. 1085 CustomizableUI.addListener(this); 1086 1087 if (!this._isObservingToolbars) { 1088 this._isObservingToolbars = true; 1089 window.addEventListener("toolbarvisibilitychange", this); 1090 } 1091 1092 // If the bookmarks toolbar item is: 1093 // - not in a toolbar, or; 1094 // - the toolbar is collapsed, or; 1095 // - the toolbar is hidden some other way: 1096 // don't initialize. Also, there is no need to initialize the toolbar if 1097 // customizing, because that will happen when the customization is done. 1098 let toolbar = this._getParentToolbar(viewElt); 1099 if ( 1100 !toolbar || 1101 toolbar.collapsed || 1102 this._isCustomizing || 1103 getComputedStyle(toolbar, "").display == "none" 1104 ) { 1105 return; 1106 } 1107 1108 new PlacesToolbar( 1109 `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`, 1110 document.getElementById("PlacesToolbarItems"), 1111 viewElt 1112 ); 1113 1114 if (toolbar.id == "PersonalToolbar") { 1115 // We just created a new view, thus we must check again the empty toolbar 1116 // message, regardless of "initialized". 1117 BookmarkingUI.updateEmptyToolbarMessage() 1118 .finally(() => { 1119 toolbar.toggleAttribute("initialized", true); 1120 }) 1121 .catch(console.error); 1122 } 1123 }, 1124 1125 async getIsEmpty() { 1126 if (!this._viewElt._placesView) { 1127 return true; 1128 } 1129 await this._viewElt._placesView.promiseRebuilt(); 1130 return !document.getElementById("PlacesToolbarItems").hasChildNodes(); 1131 }, 1132 1133 handleEvent(event) { 1134 switch (event.type) { 1135 case "toolbarvisibilitychange": 1136 if (event.target == this._getParentToolbar(this._viewElt)) { 1137 this._resetView(); 1138 } 1139 break; 1140 } 1141 }, 1142 1143 /** 1144 * This is a no-op if we haven't been initialized. 1145 */ 1146 uninit: function PTH_uninit() { 1147 if (this._isObservingToolbars) { 1148 delete this._isObservingToolbars; 1149 window.removeEventListener("toolbarvisibilitychange", this); 1150 } 1151 CustomizableUI.removeListener(this); 1152 }, 1153 1154 customizeStart: function PTH_customizeStart() { 1155 try { 1156 let viewElt = this._viewElt; 1157 if (viewElt && viewElt._placesView) { 1158 viewElt._placesView.uninit(); 1159 } 1160 } finally { 1161 this._isCustomizing = true; 1162 } 1163 }, 1164 1165 customizeDone: function PTH_customizeDone() { 1166 this._isCustomizing = false; 1167 this.init(); 1168 }, 1169 1170 onPlaceholderCommand() { 1171 let widgetGroup = CustomizableUI.getWidget("personal-bookmarks"); 1172 let widget = widgetGroup.forWindow(window); 1173 if ( 1174 widget.overflowed || 1175 widgetGroup.areaType == CustomizableUI.TYPE_PANEL 1176 ) { 1177 PlacesCommandHook.showPlacesOrganizer("BookmarksToolbar"); 1178 } 1179 }, 1180 1181 _getParentToolbar(element) { 1182 while (element) { 1183 if (element.localName == "toolbar") { 1184 return element; 1185 } 1186 element = element.parentNode; 1187 } 1188 return null; 1189 }, 1190 1191 onWidgetUnderflow(aNode) { 1192 // The view gets broken by being removed and reinserted by the overflowable 1193 // toolbar, so we have to force an uninit and reinit. 1194 let win = aNode.ownerGlobal; 1195 if (aNode.id == "personal-bookmarks" && win == window) { 1196 this._resetView(); 1197 } 1198 }, 1199 1200 onWidgetAdded(aWidgetId) { 1201 if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) { 1202 // It's possible (with the "Add to Menu", "Add to Toolbar" context 1203 // options) that the Places Toolbar Items have been moved without 1204 // letting us prepare and handle it with with customizeStart and 1205 // customizeDone. If that's the case, we need to reset the views 1206 // since they're probably broken from the DOM reparenting. 1207 this._resetView(); 1208 } 1209 }, 1210 1211 _resetView() { 1212 if (this._viewElt) { 1213 // It's possible that the placesView might not exist, and we need to 1214 // do a full init. This could happen if the Bookmarks Toolbar Items are 1215 // moved to the Menu Panel, and then to the toolbar with the "Add to Toolbar" 1216 // context menu option, outside of customize mode. 1217 if (this._viewElt._placesView) { 1218 this._viewElt._placesView.uninit(); 1219 } 1220 this.init(); 1221 } 1222 }, 1223 1224 async populateManagedBookmarks(popup) { 1225 if (popup.hasChildNodes()) { 1226 return; 1227 } 1228 // Show item's uri in the status bar when hovering, and clear on exit 1229 popup.addEventListener("DOMMenuItemActive", function (event) { 1230 XULBrowserWindow.setOverLink(event.target.link); 1231 }); 1232 popup.addEventListener("DOMMenuItemInactive", function () { 1233 XULBrowserWindow.setOverLink(""); 1234 }); 1235 let fragment = document.createDocumentFragment(); 1236 await this.addManagedBookmarks( 1237 fragment, 1238 Services.policies.getActivePolicies().ManagedBookmarks 1239 ); 1240 popup.appendChild(fragment); 1241 }, 1242 1243 async addManagedBookmarks(menu, children) { 1244 for (let i = 0; i < children.length; i++) { 1245 let entry = children[i]; 1246 if (entry.children) { 1247 // It's a folder. 1248 let submenu = document.createXULElement("menu"); 1249 if (entry.name) { 1250 submenu.setAttribute("label", entry.name); 1251 } else { 1252 document.l10n.setAttributes(submenu, "managed-bookmarks-subfolder"); 1253 } 1254 submenu.setAttribute("container", "true"); 1255 submenu.classList.add("menu-iconic", "bookmark-item"); 1256 let submenupopup = document.createXULElement("menupopup"); 1257 submenu.appendChild(submenupopup); 1258 menu.appendChild(submenu); 1259 this.addManagedBookmarks(submenupopup, entry.children); 1260 } else if (entry.name && entry.url) { 1261 // It's bookmark. 1262 let { preferredURI } = Services.uriFixup.getFixupURIInfo(entry.url); 1263 let menuitem = document.createXULElement("menuitem"); 1264 menuitem.setAttribute("label", entry.name); 1265 menuitem.setAttribute( 1266 "image", 1267 "page-icon:" + ChromeUtils.encodeURIForSrcset(preferredURI.spec) 1268 ); 1269 menuitem.classList.add( 1270 "menuitem-iconic", 1271 "menuitem-with-favicon", 1272 "bookmark-item" 1273 ); 1274 menuitem.link = preferredURI.spec; 1275 menu.appendChild(menuitem); 1276 } 1277 } 1278 }, 1279 1280 openManagedBookmark(event) { 1281 openUILink(event.target.link, event, { 1282 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 1283 }); 1284 }, 1285 1286 onDragStartManaged(event) { 1287 if (!event.target.link) { 1288 return; 1289 } 1290 1291 let dt = event.dataTransfer; 1292 1293 let node = {}; 1294 node.type = 0; 1295 node.title = event.target.label; 1296 node.uri = event.target.link; 1297 1298 function addData(type, index) { 1299 let wrapNode = PlacesUtils.wrapNode(node, type); 1300 dt.mozSetDataAt(type, wrapNode, index); 1301 } 1302 1303 addData(PlacesUtils.TYPE_X_MOZ_URL, 0); 1304 addData(PlacesUtils.TYPE_PLAINTEXT, 0); 1305 addData(PlacesUtils.TYPE_HTML, 0); 1306 }, 1307 }; 1308 1309 /** 1310 * Handles the bookmarks menu-button in the toolbar. 1311 */ 1312 1313 var BookmarkingUI = { 1314 STAR_ID: "star-button", 1315 STAR_BOX_ID: "star-button-box", 1316 BOOKMARK_BUTTON_ID: "bookmarks-menu-button", 1317 BOOKMARK_BUTTON_SHORTCUT: "addBookmarkAsKb", 1318 get button() { 1319 delete this.button; 1320 let widgetGroup = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID); 1321 return (this.button = widgetGroup.forWindow(window).node); 1322 }, 1323 1324 get star() { 1325 delete this.star; 1326 return (this.star = document.getElementById(this.STAR_ID)); 1327 }, 1328 1329 get starBox() { 1330 delete this.starBox; 1331 return (this.starBox = document.getElementById(this.STAR_BOX_ID)); 1332 }, 1333 1334 get anchor() { 1335 let action = PageActions.actionForID(PageActions.ACTION_ID_BOOKMARK); 1336 return BrowserPageActions.panelAnchorNodeForAction(action); 1337 }, 1338 1339 get stringbundleset() { 1340 delete this.stringbundleset; 1341 return (this.stringbundleset = document.getElementById("stringbundleset")); 1342 }, 1343 1344 get toolbar() { 1345 delete this.toolbar; 1346 return (this.toolbar = document.getElementById("PersonalToolbar")); 1347 }, 1348 1349 STATUS_UPDATING: -1, 1350 STATUS_UNSTARRED: 0, 1351 STATUS_STARRED: 1, 1352 get status() { 1353 if (this._pendingUpdate) { 1354 return this.STATUS_UPDATING; 1355 } 1356 return this.star.hasAttribute("starred") 1357 ? this.STATUS_STARRED 1358 : this.STATUS_UNSTARRED; 1359 }, 1360 1361 onPopupShowing: function BUI_onPopupShowing(event) { 1362 // Don't handle events for submenus. 1363 if (event.target.id != "BMB_bookmarksPopup") { 1364 return; 1365 } 1366 1367 // On non-photon, this code should never be reached. However, if you click 1368 // the outer button's border, some cpp code for the menu button's XBL 1369 // binding decides to open the popup even though the dropmarker is invisible. 1370 // 1371 // Separately, in Photon, if the button is in the dynamic portion of the 1372 // overflow panel, we want to show a subview instead. 1373 if ( 1374 this.button.getAttribute("cui-areatype") == CustomizableUI.TYPE_PANEL || 1375 this.button.hasAttribute("overflowedItem") 1376 ) { 1377 this._showSubView(); 1378 event.preventDefault(); 1379 event.stopPropagation(); 1380 return; 1381 } 1382 1383 let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID).forWindow( 1384 window 1385 ); 1386 if (widget.overflowed) { 1387 // Don't open a popup in the overflow popup, rather just open the Library. 1388 event.preventDefault(); 1389 widget.node.removeAttribute("closemenu"); 1390 PlacesCommandHook.showPlacesOrganizer("BookmarksMenu"); 1391 return; 1392 } 1393 1394 document.getElementById("BMB_mobileBookmarks").hidden = 1395 !SHOW_MOBILE_BOOKMARKS; 1396 1397 this.updateLabel( 1398 "BMB_viewBookmarksSidebar", 1399 SidebarController.currentID == "viewBookmarksSidebar" 1400 ); 1401 this.updateLabel("BMB_viewBookmarksToolbar", !this.toolbar.collapsed); 1402 }, 1403 1404 updateLabel(elementId, visible) { 1405 let element = PanelMultiView.getViewNode(document, elementId); 1406 let l10nID = element.getAttribute("data-l10n-id"); 1407 document.l10n.setAttributes(element, l10nID, { isVisible: !!visible }); 1408 }, 1409 1410 toggleBookmarksToolbar(reason) { 1411 let newState = this.toolbar.collapsed ? "always" : "never"; 1412 Services.prefs.setCharPref( 1413 "browser.toolbars.bookmarks.visibility", 1414 // See firefox.js for possible values 1415 newState 1416 ); 1417 1418 CustomizableUI.setToolbarVisibility(this.toolbar.id, newState, false); 1419 BrowserUsageTelemetry.recordToolbarVisibility( 1420 this.toolbar.id, 1421 newState, 1422 reason 1423 ); 1424 }, 1425 1426 isOnNewTabPage(uri) { 1427 if (!uri) { 1428 return false; 1429 } 1430 // Prevent loading AboutNewTab.sys.mjs during startup path if it 1431 // is only the newTabURL getter we are interested in. 1432 let newTabURL = Cu.isESModuleLoaded( 1433 "resource:///modules/AboutNewTab.sys.mjs" 1434 ) 1435 ? AboutNewTab.newTabURL 1436 : "about:newtab"; 1437 // Don't treat a custom "about:blank" new tab URL as the "New Tab Page" 1438 // due to about:blank being used in different contexts and the 1439 // difficulty in determining if the eventual page load is 1440 // about:blank or if the about:blank load is just temporary. 1441 if (newTabURL == "about:blank") { 1442 newTabURL = "about:newtab"; 1443 } 1444 let newTabURLs = [ 1445 newTabURL, 1446 "about:home", 1447 "chrome://browser/content/blanktab.html", 1448 // Add the "about:tor" uri. See tor-browser#41717. 1449 // NOTE: "about:newtab", "about:welcome", "about:home" and 1450 // "about:privatebrowsing" can also redirect to "about:tor". 1451 "about:tor", 1452 ]; 1453 if (PrivateBrowsingUtils.isWindowPrivate(window)) { 1454 newTabURLs.push("about:privatebrowsing"); 1455 } 1456 return newTabURLs.some(newTabUriString => 1457 this._newTabURI(newTabUriString)?.equalsExceptRef(uri) 1458 ); 1459 }, 1460 1461 _newTabURI(uriString) { 1462 let uri = this._newTabURICache.get(uriString); 1463 if (uri === undefined) { 1464 uri = Services.io.newURI(uriString); 1465 this._newTabURICache.set(uriString, uri); 1466 } 1467 return uri; 1468 }, 1469 _newTabURICache: new Map(), 1470 1471 buildBookmarksToolbarSubmenu(toolbar) { 1472 let alwaysShowMenuItem = document.createXULElement("menuitem"); 1473 let alwaysHideMenuItem = document.createXULElement("menuitem"); 1474 let showOnNewTabMenuItem = document.createXULElement("menuitem"); 1475 let menuPopup = document.createXULElement("menupopup"); 1476 menuPopup.append( 1477 alwaysShowMenuItem, 1478 showOnNewTabMenuItem, 1479 alwaysHideMenuItem 1480 ); 1481 let menu = document.createXULElement("menu"); 1482 menu.appendChild(menuPopup); 1483 1484 menu.setAttribute("label", toolbar.getAttribute("toolbarname")); 1485 menu.setAttribute("id", "toggle_" + toolbar.id); 1486 menu.setAttribute("accesskey", toolbar.getAttribute("accesskey")); 1487 menu.setAttribute("toolbarId", toolbar.id); 1488 1489 // Used by the Places context menu in the Bookmarks Toolbar 1490 // when nothing is selected 1491 menu.setAttribute("selection-type", "none|single"); 1492 1493 MozXULElement.insertFTLIfNeeded("browser/toolbarContextMenu.ftl"); 1494 let menuItems = [ 1495 [ 1496 showOnNewTabMenuItem, 1497 "toolbar-context-menu-bookmarks-toolbar-on-new-tab-2", 1498 "newtab", 1499 ], 1500 [ 1501 alwaysShowMenuItem, 1502 "toolbar-context-menu-bookmarks-toolbar-always-show-2", 1503 "always", 1504 ], 1505 [ 1506 alwaysHideMenuItem, 1507 "toolbar-context-menu-bookmarks-toolbar-never-show-2", 1508 "never", 1509 ], 1510 ]; 1511 menuItems.map(([menuItem, l10nId, visibilityEnum]) => { 1512 document.l10n.setAttributes(menuItem, l10nId); 1513 menuItem.setAttribute("type", "radio"); 1514 // The persisted state of the PersonalToolbar is stored in 1515 // "browser.toolbars.bookmarks.visibility". 1516 menuItem.toggleAttribute( 1517 "checked", 1518 gBookmarksToolbarVisibility == visibilityEnum 1519 ); 1520 // Identify these items for "onViewToolbarCommand" so 1521 // we know to check the visibilityEnum value. 1522 menuItem.dataset.bookmarksToolbarVisibility = true; 1523 menuItem.dataset.visibilityEnum = visibilityEnum; 1524 menuItem.addEventListener("command", onViewToolbarCommand); 1525 }); 1526 let menuItemForNextStateFromKbShortcut = 1527 gBookmarksToolbarVisibility == "never" 1528 ? alwaysShowMenuItem 1529 : alwaysHideMenuItem; 1530 menuItemForNextStateFromKbShortcut.setAttribute( 1531 "key", 1532 "viewBookmarksToolbarKb" 1533 ); 1534 1535 return menu; 1536 }, 1537 1538 /** 1539 * Check if we need to make the empty toolbar message `hidden`. 1540 * We'll have it unhidden during startup, to make sure the toolbar 1541 * has height, and we'll unhide it if there is nothing else on the toolbar. 1542 * We hide it in customize mode, unless there's nothing on the toolbar. 1543 */ 1544 async updateEmptyToolbarMessage() { 1545 let { initialHiddenState, checkHasBookmarks } = (() => { 1546 // Do we have visible kids? 1547 if ( 1548 this.toolbar.querySelector( 1549 `:scope > toolbarpaletteitem > toolbarbutton:not([hidden]), 1550 :scope > toolbarpaletteitem > toolbaritem:not([hidden], #personal-bookmarks), 1551 :scope > toolbarbutton:not([hidden]), 1552 :scope > toolbaritem:not([hidden], #personal-bookmarks)` 1553 ) 1554 ) { 1555 return { initialHiddenState: true, checkHasBookmarks: false }; 1556 } 1557 1558 if (this._isCustomizing) { 1559 return { initialHiddenState: true, checkHasBookmarks: false }; 1560 } 1561 1562 // If bookmarks have been moved out of the toolbar, we show the message. 1563 let bookmarksToolbarItemsPlacement = 1564 CustomizableUI.getPlacementOfWidget("personal-bookmarks"); 1565 let bookmarksItemInToolbar = 1566 bookmarksToolbarItemsPlacement?.area == CustomizableUI.AREA_BOOKMARKS; 1567 if (!bookmarksItemInToolbar) { 1568 return { initialHiddenState: false, checkHasBookmarks: false }; 1569 } 1570 1571 if (!this.toolbar.hasAttribute("initialized")) { 1572 // If the toolbar has not been initialized yet, unhide the message, it 1573 // will be made 0-width and visibility: hidden anyway, to keep the 1574 // toolbar height stable. 1575 return { initialHiddenState: false, checkHasBookmarks: true }; 1576 } 1577 1578 // Check visible bookmark nodes. 1579 if ( 1580 this.toolbar.querySelector( 1581 `#PlacesToolbarItems > toolbarseparator, 1582 #PlacesToolbarItems > toolbarbutton` 1583 ) 1584 ) { 1585 return { initialHiddenState: true, checkHasBookmarks: false }; 1586 } 1587 return { initialHiddenState: true, checkHasBookmarks: true }; 1588 })(); 1589 1590 let emptyMsg = document.getElementById("personal-toolbar-empty"); 1591 emptyMsg.hidden = initialHiddenState; 1592 if (checkHasBookmarks) { 1593 emptyMsg.hidden = !(await PlacesToolbarHelper.getIsEmpty()); 1594 } 1595 }, 1596 1597 _uninitView: function BUI__uninitView() { 1598 // When an element with a placesView attached is removed and re-inserted, 1599 // XBL reapplies the binding causing any kind of issues and possible leaks, 1600 // so kill current view and let popupshowing generate a new one. 1601 if (this.button._placesView) { 1602 this.button._placesView.uninit(); 1603 } 1604 // Also uninit the main menubar placesView, since it would have the same 1605 // issues. 1606 let menubar = document.getElementById("bookmarksMenu"); 1607 if (menubar && menubar._placesView) { 1608 menubar._placesView.uninit(); 1609 } 1610 1611 // We have to do the same thing for the "special" views underneath the 1612 // the bookmarks menu. 1613 const kSpecialViewNodeIDs = [ 1614 "BMB_bookmarksToolbar", 1615 "BMB_unsortedBookmarks", 1616 ]; 1617 for (let viewNodeID of kSpecialViewNodeIDs) { 1618 let elem = document.getElementById(viewNodeID); 1619 if (elem && elem._placesView) { 1620 elem._placesView.uninit(); 1621 } 1622 } 1623 }, 1624 1625 onCustomizeStart: function BUI_customizeStart(aWindow) { 1626 if (aWindow == window) { 1627 this._uninitView(); 1628 this._isCustomizing = true; 1629 1630 this.updateEmptyToolbarMessage().catch(console.error); 1631 1632 let isVisible = 1633 Services.prefs.getCharPref( 1634 "browser.toolbars.bookmarks.visibility", 1635 "newtab" 1636 ) != "never"; 1637 // Temporarily show the bookmarks toolbar in Customize mode if 1638 // the toolbar isn't set to Never. We don't have to worry about 1639 // hiding when leaving customize mode since the toolbar will 1640 // hide itself on location change. 1641 setToolbarVisibility(this.toolbar, isVisible, false); 1642 } 1643 }, 1644 1645 onWidgetAdded: function BUI_widgetAdded(aWidgetId, aArea) { 1646 if (aWidgetId == this.BOOKMARK_BUTTON_ID) { 1647 this._onWidgetWasMoved(); 1648 } 1649 if (aArea == CustomizableUI.AREA_BOOKMARKS) { 1650 this.updateEmptyToolbarMessage().catch(console.error); 1651 } 1652 }, 1653 1654 onWidgetRemoved: function BUI_widgetRemoved(aWidgetId, aOldArea) { 1655 if (aWidgetId == this.BOOKMARK_BUTTON_ID) { 1656 this._onWidgetWasMoved(); 1657 } 1658 if (aOldArea == CustomizableUI.AREA_BOOKMARKS) { 1659 this.updateEmptyToolbarMessage().catch(console.error); 1660 } 1661 }, 1662 1663 onWidgetReset: function BUI_widgetReset(aNode) { 1664 if (aNode == this.button) { 1665 this._onWidgetWasMoved(); 1666 } 1667 }, 1668 1669 onWidgetUndoMove: function BUI_undoWidgetUndoMove(aNode) { 1670 if (aNode == this.button) { 1671 this._onWidgetWasMoved(); 1672 } 1673 }, 1674 1675 onWidgetBeforeDOMChange: function BUI_onWidgetBeforeDOMChange( 1676 aNode, 1677 aNextNode, 1678 aContainer, 1679 aIsRemoval 1680 ) { 1681 if (aNode.id == "import-button") { 1682 this._updateImportButton(aNode, aIsRemoval ? null : aContainer); 1683 } 1684 }, 1685 1686 _updateImportButton: function BUI_updateImportButton(aNode, aContainer) { 1687 // The import button behaves like a bookmark item when in the bookmarks 1688 // toolbar, otherwise like a regular toolbar button. 1689 let isBookmarkItem = aContainer == this.toolbar; 1690 aNode.classList.toggle("toolbarbutton-1", !isBookmarkItem); 1691 aNode.classList.toggle("bookmark-item", isBookmarkItem); 1692 }, 1693 1694 _onWidgetWasMoved: function BUI_widgetWasMoved() { 1695 // If we're moved outside of customize mode, we need to uninit 1696 // our view so it gets reconstructed. 1697 if (!this._isCustomizing) { 1698 this._uninitView(); 1699 } 1700 }, 1701 1702 onCustomizeEnd: function BUI_customizeEnd(aWindow) { 1703 if (aWindow == window) { 1704 this._isCustomizing = false; 1705 this.updateEmptyToolbarMessage().catch(console.error); 1706 } 1707 }, 1708 1709 init() { 1710 CustomizableUI.addListener(this); 1711 let importButton = document.getElementById("import-button"); 1712 if (importButton) { 1713 this._updateImportButton(importButton, importButton.parentNode); 1714 } 1715 this.updateEmptyToolbarMessage().catch(console.error); 1716 }, 1717 1718 _hasBookmarksObserver: false, 1719 _itemGuids: new Set(), 1720 uninit: function BUI_uninit() { 1721 this.updateBookmarkPageMenuItem(true); 1722 CustomizableUI.removeListener(this); 1723 1724 this._uninitView(); 1725 1726 if (this._hasBookmarksObserver) { 1727 PlacesUtils.observers.removeListener( 1728 [ 1729 "bookmark-added", 1730 "bookmark-removed", 1731 "bookmark-moved", 1732 "bookmark-url-changed", 1733 ], 1734 this.handlePlacesEvents 1735 ); 1736 } 1737 1738 if (this._pendingUpdate) { 1739 delete this._pendingUpdate; 1740 } 1741 }, 1742 1743 onLocationChange: function BUI_onLocationChange() { 1744 if (this._uri && gBrowser.currentURI.equals(this._uri)) { 1745 return; 1746 } 1747 this.updateStarState(); 1748 }, 1749 1750 updateStarState: function BUI_updateStarState() { 1751 this._uri = gBrowser.currentURI; 1752 this._itemGuids.clear(); 1753 let guids = new Set(); 1754 1755 // those objects are use to check if we are in the current iteration before 1756 // returning any result. 1757 let pendingUpdate = (this._pendingUpdate = {}); 1758 1759 PlacesUtils.bookmarks 1760 .fetch({ url: this._uri }, b => guids.add(b.guid), { concurrent: true }) 1761 .catch(console.error) 1762 .then(() => { 1763 if (pendingUpdate != this._pendingUpdate) { 1764 return; 1765 } 1766 1767 // It's possible that "bookmark-added" gets called before the async statement 1768 // calls back. For such an edge case, retain all unique entries from the 1769 // array. 1770 if (this._itemGuids.size > 0) { 1771 this._itemGuids = new Set(...this._itemGuids, ...guids); 1772 } else { 1773 this._itemGuids = guids; 1774 } 1775 1776 this._updateStar(); 1777 1778 // Start observing bookmarks if needed. 1779 if (!this._hasBookmarksObserver) { 1780 try { 1781 this.handlePlacesEvents = this.handlePlacesEvents.bind(this); 1782 PlacesUtils.observers.addListener( 1783 [ 1784 "bookmark-added", 1785 "bookmark-removed", 1786 "bookmark-moved", 1787 "bookmark-url-changed", 1788 ], 1789 this.handlePlacesEvents 1790 ); 1791 this._hasBookmarksObserver = true; 1792 } catch (ex) { 1793 console.error( 1794 "BookmarkingUI failed adding a bookmarks observer: ", 1795 ex 1796 ); 1797 } 1798 } 1799 1800 delete this._pendingUpdate; 1801 }); 1802 }, 1803 1804 _updateStar: function BUI__updateStar() { 1805 let starred = this._itemGuids.size > 0; 1806 1807 // Update the image for all elements. 1808 for (let element of [ 1809 this.star, 1810 document.getElementById("context-bookmarkpage"), 1811 PanelMultiView.getViewNode(document, "panelMenuBookmarkThisPage"), 1812 document.getElementById("pageAction-panel-bookmark"), 1813 ]) { 1814 if (!element) { 1815 // The page action panel element may not have been created yet. 1816 continue; 1817 } 1818 element.toggleAttribute("starred", starred); 1819 } 1820 1821 if (!this.starBox) { 1822 // The BOOKMARK_BUTTON_SHORTCUT exists only in browser.xhtml. 1823 // Return early if we're not in this context, but still reset the 1824 // Bookmark Page items. 1825 this.updateBookmarkPageMenuItem(true); 1826 return; 1827 } 1828 1829 // Update the tooltip for elements that require it. 1830 let shortcut = document.getElementById(this.BOOKMARK_BUTTON_SHORTCUT); 1831 let l10nArgs = { 1832 shortcut: ShortcutUtils.prettifyShortcut(shortcut), 1833 }; 1834 document.l10n.setAttributes( 1835 this.starBox, 1836 starred ? "urlbar-star-edit-bookmark" : "urlbar-star-add-bookmark", 1837 l10nArgs 1838 ); 1839 1840 // Update the Bookmark Page menuitem when bookmarked state changes. 1841 this.updateBookmarkPageMenuItem(); 1842 1843 Services.obs.notifyObservers( 1844 null, 1845 "bookmark-icon-updated", 1846 starred ? "starred" : "unstarred" 1847 ); 1848 }, 1849 1850 /** 1851 * Update the "Bookmark Page…" menuitems on the menubar, panels, context 1852 * menu and page actions. 1853 * 1854 * @param {boolean} [forceReset] passed when we're destroyed and the label 1855 * should go back to the default (Bookmark Page), for MacOS. 1856 */ 1857 updateBookmarkPageMenuItem(forceReset = false) { 1858 let isStarred = !forceReset && this._itemGuids.size > 0; 1859 // Define the l10n id which will be used to localize elements 1860 // that only require a label using the menubar.ftl messages. 1861 let menuItemL10nId = isStarred ? "menu-edit-bookmark" : "menu-bookmark-tab"; 1862 let menuItem = document.getElementById("menu_bookmarkThisPage"); 1863 if (menuItem) { 1864 // Localize the menubar item. 1865 document.l10n.setAttributes(menuItem, menuItemL10nId); 1866 } 1867 1868 let panelMenuItemL10nId = isStarred 1869 ? "bookmarks-subview-edit-bookmark" 1870 : "bookmarks-subview-bookmark-tab"; 1871 let panelMenuToolbarButton = PanelMultiView.getViewNode( 1872 document, 1873 "panelMenuBookmarkThisPage" 1874 ); 1875 if (panelMenuToolbarButton) { 1876 document.l10n.setAttributes(panelMenuToolbarButton, panelMenuItemL10nId); 1877 } 1878 1879 // Localize the context menu item element. 1880 let contextItem = document.getElementById("context-bookmarkpage"); 1881 // On macOS regular menuitems are used and the shortcut isn't added 1882 if (contextItem) { 1883 if (AppConstants.platform == "macosx") { 1884 let contextItemL10nId = isStarred 1885 ? "main-context-menu-edit-bookmark-mac" 1886 : "main-context-menu-bookmark-page-mac"; 1887 document.l10n.setAttributes(contextItem, contextItemL10nId); 1888 } else { 1889 let shortcutElem = document.getElementById( 1890 this.BOOKMARK_BUTTON_SHORTCUT 1891 ); 1892 if (shortcutElem) { 1893 let shortcut = ShortcutUtils.prettifyShortcut(shortcutElem); 1894 let contextItemL10nId = isStarred 1895 ? "main-context-menu-edit-bookmark-with-shortcut" 1896 : "main-context-menu-bookmark-page-with-shortcut"; 1897 let l10nArgs = { shortcut }; 1898 document.l10n.setAttributes(contextItem, contextItemL10nId, l10nArgs); 1899 } else { 1900 let contextItemL10nId = isStarred 1901 ? "main-context-menu-edit-bookmark" 1902 : "main-context-menu-bookmark-page"; 1903 document.l10n.setAttributes(contextItem, contextItemL10nId); 1904 } 1905 } 1906 } 1907 1908 // Update Page Actions. 1909 if (document.getElementById("page-action-buttons")) { 1910 // Fetch the label attribute value of the message and 1911 // apply it on the star title. 1912 // 1913 // Note: This should be updated once bug 1608198 is fixed. 1914 this._latestMenuItemL10nId = menuItemL10nId; 1915 document.l10n.formatMessages([{ id: menuItemL10nId }]).then(l10n => { 1916 // It's possible for this promise to be scheduled multiple times. 1917 // In such a case, we'd like to avoid setting the title if there's 1918 // a newer l10n id pending to be set. 1919 if (this._latestMenuItemL10nId != menuItemL10nId) { 1920 return; 1921 } 1922 1923 // We assume that menuItemL10nId has a single attribute. 1924 let label = l10n[0].attributes[0].value; 1925 1926 // Update the label for the page action panel. 1927 let panelButton = BrowserPageActions.panelButtonNodeForActionID( 1928 PageActions.ACTION_ID_BOOKMARK 1929 ); 1930 if (panelButton) { 1931 panelButton.setAttribute("label", label); 1932 } 1933 }); 1934 } 1935 }, 1936 1937 onMainMenuPopupShowing: function BUI_onMainMenuPopupShowing(event) { 1938 // Don't handle events for submenus. 1939 if (event.target.id != "bookmarksMenuPopup") { 1940 return; 1941 } 1942 1943 document.getElementById("menu_mobileBookmarks").hidden = 1944 !SHOW_MOBILE_BOOKMARKS; 1945 }, 1946 1947 showSubView(anchor) { 1948 this._showSubView(null, anchor); 1949 }, 1950 1951 _showSubView( 1952 event, 1953 anchor = document.getElementById(this.BOOKMARK_BUTTON_ID) 1954 ) { 1955 let view = PanelMultiView.getViewNode(document, "PanelUI-bookmarks"); 1956 view.addEventListener("ViewShowing", this); 1957 view.addEventListener("ViewHiding", this); 1958 anchor.setAttribute("closemenu", "none"); 1959 this.updateLabel("panelMenu_viewBookmarksToolbar", !this.toolbar.collapsed); 1960 PanelUI.showSubView("PanelUI-bookmarks", anchor, event); 1961 }, 1962 1963 onCommand: function BUI_onCommand(aEvent) { 1964 if (aEvent.target != aEvent.currentTarget) { 1965 return; 1966 } 1967 1968 // Handle special case when the button is in the panel. 1969 if (this.button.getAttribute("cui-areatype") == CustomizableUI.TYPE_PANEL) { 1970 this._showSubView(aEvent); 1971 return; 1972 } 1973 let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID).forWindow( 1974 window 1975 ); 1976 if (widget.overflowed) { 1977 // Close the overflow panel because the Edit Bookmark panel will appear. 1978 widget.node.removeAttribute("closemenu"); 1979 } 1980 this.onStarCommand(aEvent); 1981 }, 1982 1983 onStarCommand(aEvent) { 1984 // Ignore non-left clicks on the star, or if we are updating its state. 1985 if ( 1986 !this._pendingUpdate && 1987 (aEvent.type != "click" || aEvent.button == 0) 1988 ) { 1989 PlacesCommandHook.bookmarkPage(); 1990 } 1991 }, 1992 1993 handleEvent: function BUI_handleEvent(aEvent) { 1994 switch (aEvent.type) { 1995 case "ViewShowing": 1996 this.onPanelMenuViewShowing(aEvent); 1997 break; 1998 case "ViewHiding": 1999 this.onPanelMenuViewHiding(aEvent); 2000 break; 2001 case "command": 2002 if (aEvent.target.id == "panelMenu_searchBookmarks") { 2003 PlacesCommandHook.searchBookmarks(); 2004 } else if (aEvent.target.id == "panelMenu_viewBookmarksToolbar") { 2005 this.toggleBookmarksToolbar("bookmark-tools"); 2006 } 2007 break; 2008 } 2009 }, 2010 2011 onPanelMenuViewShowing: function BUI_onViewShowing(aEvent) { 2012 let panelview = aEvent.target; 2013 2014 // Get all statically placed buttons to supply them with keyboard shortcuts. 2015 let staticButtons = panelview.getElementsByTagName("toolbarbutton"); 2016 for (let i = 0, l = staticButtons.length; i < l; ++i) { 2017 CustomizableUI.addShortcut(staticButtons[i]); 2018 } 2019 2020 // Setup the Places view. 2021 // We restrict the amount of results to 42. Not 50, but 42. Why? Because 42. 2022 let query = 2023 "place:queryType=" + 2024 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS + 2025 "&sort=" + 2026 Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING + 2027 "&maxResults=42&excludeQueries=1"; 2028 2029 this._panelMenuView = new PlacesPanelview( 2030 query, 2031 document.getElementById("panelMenu_bookmarksMenu"), 2032 panelview 2033 ); 2034 panelview.removeEventListener("ViewShowing", this); 2035 panelview.addEventListener("command", this); 2036 }, 2037 2038 onPanelMenuViewHiding: function BUI_onViewHiding(aEvent) { 2039 this._panelMenuView.uninit(); 2040 delete this._panelMenuView; 2041 let panelview = aEvent.target; 2042 panelview.removeEventListener("ViewHiding", this); 2043 panelview.removeEventListener("command", this); 2044 }, 2045 2046 handlePlacesEvents(aEvents) { 2047 let isStarUpdateNeeded = false; 2048 let affectsOtherBookmarksFolder = false; 2049 let affectsBookmarksToolbarFolder = false; 2050 2051 for (let ev of aEvents) { 2052 switch (ev.type) { 2053 case "bookmark-added": 2054 // Only need to update the UI if it wasn't marked as starred before: 2055 if (this._itemGuids.size == 0) { 2056 if (ev.url && ev.url == this._uri.spec) { 2057 // If a new bookmark has been added to the tracked uri, register it. 2058 if (!this._itemGuids.has(ev.guid)) { 2059 this._itemGuids.add(ev.guid); 2060 isStarUpdateNeeded = true; 2061 } 2062 } 2063 } 2064 2065 if (ev.parentGuid === PlacesUtils.bookmarks.toolbarGuid) { 2066 Glean.browserEngagement.bookmarksToolbarBookmarkAdded.add(1); 2067 } 2068 if (ev.parentGuid == PlacesUtils.bookmarks.tagsGuid) { 2069 StarUI.userHasTags = true; 2070 } 2071 break; 2072 case "bookmark-removed": 2073 // If one of the tracked bookmarks has been removed, unregister it. 2074 if (this._itemGuids.has(ev.guid)) { 2075 this._itemGuids.delete(ev.guid); 2076 // Only need to update the UI if the page is no longer starred 2077 if (this._itemGuids.size == 0) { 2078 isStarUpdateNeeded = true; 2079 } 2080 } 2081 2082 // Reset the default location if it is equal to the folder 2083 // being deleted. Just check the preference directly since we 2084 // do not want to do a asynchronous db lookup. 2085 PlacesUIUtils.defaultParentGuid.then(parentGuid => { 2086 if ( 2087 ev.itemType == PlacesUtils.bookmarks.TYPE_FOLDER && 2088 ev.guid == parentGuid 2089 ) { 2090 Services.prefs.setCharPref( 2091 "browser.bookmarks.defaultLocation", 2092 PlacesUtils.bookmarks.toolbarGuid 2093 ); 2094 } 2095 }); 2096 break; 2097 case "bookmark-moved": 2098 if ( 2099 ev.parentGuid === PlacesUtils.bookmarks.unfiledGuid || 2100 ev.oldParentGuid === PlacesUtils.bookmarks.unfiledGuid 2101 ) { 2102 affectsOtherBookmarksFolder = true; 2103 } 2104 2105 if ( 2106 ev.parentGuid == PlacesUtils.bookmarks.toolbarGuid || 2107 ev.oldParentGuid == PlacesUtils.bookmarks.toolbarGuid 2108 ) { 2109 affectsBookmarksToolbarFolder = true; 2110 if (ev.oldParentGuid != PlacesUtils.bookmarks.toolbarGuid) { 2111 Glean.browserEngagement.bookmarksToolbarBookmarkAdded.add(1); 2112 } 2113 } 2114 break; 2115 case "bookmark-url-changed": 2116 // If the changed bookmark was tracked, check if it is now pointing to 2117 // a different uri and unregister it. 2118 if (this._itemGuids.has(ev.guid) && ev.url != this._uri.spec) { 2119 this._itemGuids.delete(ev.guid); 2120 // Only need to update the UI if the page is no longer starred 2121 if (this._itemGuids.size == 0) { 2122 this._updateStar(); 2123 } 2124 } else if ( 2125 !this._itemGuids.has(ev.guid) && 2126 ev.url == this._uri.spec 2127 ) { 2128 // If another bookmark is now pointing to the tracked uri, register it. 2129 this._itemGuids.add(ev.guid); 2130 // Only need to update the UI if it wasn't marked as starred before: 2131 if (this._itemGuids.size == 1) { 2132 this._updateStar(); 2133 } 2134 } 2135 2136 break; 2137 } 2138 2139 if (ev.parentGuid == PlacesUtils.bookmarks.unfiledGuid) { 2140 affectsOtherBookmarksFolder = true; 2141 } else if (ev.parentGuid == PlacesUtils.bookmarks.toolbarGuid) { 2142 affectsBookmarksToolbarFolder = true; 2143 } 2144 } 2145 2146 if (isStarUpdateNeeded) { 2147 this._updateStar(); 2148 } 2149 2150 // Run after the notification has been handled by the views. 2151 Services.tm.dispatchToMainThread(() => { 2152 if (affectsOtherBookmarksFolder) { 2153 this.maybeShowOtherBookmarksFolder().catch(console.error); 2154 } 2155 if (affectsBookmarksToolbarFolder) { 2156 this.updateEmptyToolbarMessage().catch(console.error); 2157 } 2158 }); 2159 }, 2160 2161 onWidgetUnderflow(aNode) { 2162 let win = aNode.ownerGlobal; 2163 if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window) { 2164 return; 2165 } 2166 2167 // The view gets broken by being removed and reinserted. Uninit 2168 // here so popupshowing will generate a new one: 2169 this._uninitView(); 2170 }, 2171 2172 async maybeShowOtherBookmarksFolder() { 2173 // PlacesToolbar._placesView can be undefined if the toolbar isn't initialized, 2174 // collapsed, or hidden in some other way. 2175 let toolbar = document.getElementById("PlacesToolbar"); 2176 if (!toolbar?._placesView) { 2177 return; 2178 } 2179 2180 let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks"); 2181 let otherBookmarks = document.getElementById("OtherBookmarks"); 2182 if ( 2183 !SHOW_OTHER_BOOKMARKS || 2184 placement?.area != CustomizableUI.AREA_BOOKMARKS 2185 ) { 2186 if (otherBookmarks) { 2187 otherBookmarks.hidden = true; 2188 } 2189 return; 2190 } 2191 2192 let instance = (this._showOtherBookmarksInstance = {}); 2193 let unfiledGuid = PlacesUtils.bookmarks.unfiledGuid; 2194 let numberOfBookmarks = (await PlacesUtils.bookmarks.fetch(unfiledGuid)) 2195 .childCount; 2196 if (instance != this._showOtherBookmarksInstance) { 2197 return; 2198 } 2199 2200 if (numberOfBookmarks > 0) { 2201 // Build the "Other Bookmarks" button if it doesn't exist. 2202 if (!otherBookmarks) { 2203 const node = PlacesUtils.getFolderContents(unfiledGuid).root; 2204 otherBookmarks = this.buildOtherBookmarksFolder(node); 2205 } 2206 otherBookmarks.hidden = false; 2207 } else if (otherBookmarks) { 2208 otherBookmarks.hidden = true; 2209 } 2210 }, 2211 2212 buildShowOtherBookmarksMenuItem() { 2213 // Building this only if there's bookmarks in unfiled would cause 2214 // synchronous IO, thus we just add it as disabled and enable it once the 2215 // information is available. 2216 let menuItem = document.createXULElement("menuitem"); 2217 2218 menuItem.setAttribute("id", "show-other-bookmarks_PersonalToolbar"); 2219 menuItem.setAttribute("toolbarId", "PersonalToolbar"); 2220 menuItem.setAttribute("type", "checkbox"); 2221 menuItem.setAttribute("selection-type", "none|single"); 2222 menuItem.setAttribute("start-disabled", "true"); 2223 menuItem.toggleAttribute("checked", SHOW_OTHER_BOOKMARKS); 2224 2225 MozXULElement.insertFTLIfNeeded("browser/toolbarContextMenu.ftl"); 2226 document.l10n.setAttributes( 2227 menuItem, 2228 "toolbar-context-menu-bookmarks-show-other-bookmarks" 2229 ); 2230 menuItem.addEventListener("command", () => { 2231 Services.prefs.setBoolPref( 2232 "browser.toolbars.bookmarks.showOtherBookmarks", 2233 !SHOW_OTHER_BOOKMARKS 2234 ); 2235 }); 2236 // Enable the menuItem if there's unfiled bookmarks 2237 PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.unfiledGuid).then(bm => { 2238 if (bm.childCount) { 2239 menuItem.disabled = false; 2240 } 2241 }); 2242 2243 return menuItem; 2244 }, 2245 2246 buildOtherBookmarksFolder(node) { 2247 let otherBookmarksButton = document.createXULElement("toolbarbutton"); 2248 otherBookmarksButton.setAttribute("type", "menu"); 2249 otherBookmarksButton.setAttribute("container", "true"); 2250 otherBookmarksButton.id = "OtherBookmarks"; 2251 otherBookmarksButton.className = "bookmark-item"; 2252 otherBookmarksButton.hidden = "true"; 2253 otherBookmarksButton.addEventListener("popupshowing", event => 2254 document 2255 .getElementById("PlacesToolbar") 2256 ._placesView._onOtherBookmarksPopupShowing(event) 2257 ); 2258 2259 MozXULElement.insertFTLIfNeeded("browser/places.ftl"); 2260 document.l10n.setAttributes(otherBookmarksButton, "other-bookmarks-folder"); 2261 2262 let otherBookmarksPopup = document.createXULElement("menupopup", { 2263 is: "places-popup", 2264 }); 2265 otherBookmarksPopup.setAttribute("placespopup", "true"); 2266 otherBookmarksPopup.setAttribute("context", "placesContext"); 2267 otherBookmarksPopup.classList.add("toolbar-menupopup"); 2268 otherBookmarksPopup.id = "OtherBookmarksPopup"; 2269 2270 otherBookmarksPopup._placesNode = PlacesUtils.asContainer(node); 2271 otherBookmarksButton._placesNode = PlacesUtils.asContainer(node); 2272 2273 otherBookmarksButton.appendChild(otherBookmarksPopup); 2274 2275 let chevronButton = document.getElementById("PlacesChevron"); 2276 chevronButton.parentNode.append(otherBookmarksButton); 2277 2278 let placesToolbar = document.getElementById("PlacesToolbar"); 2279 placesToolbar._placesView._otherBookmarks = otherBookmarksButton; 2280 placesToolbar._placesView._otherBookmarksPopup = otherBookmarksPopup; 2281 return otherBookmarksButton; 2282 }, 2283 };