CustomizeMode.sys.mjs (136189B)
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 const kPrefCustomizationDebug = "browser.uiCustomization.debug"; 6 const kPaletteId = "customization-palette"; 7 const kDragDataTypePrefix = "text/toolbarwrapper-id/"; 8 const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck"; 9 const kDrawInTitlebarPref = "browser.tabs.inTitlebar"; 10 const kCompactModeShowPref = "browser.compactmode.show"; 11 const kBookmarksToolbarPref = "browser.toolbars.bookmarks.visibility"; 12 const kKeepBroadcastAttributes = "keepbroadcastattributeswhencustomizing"; 13 14 const kPanelItemContextMenu = "customizationPanelItemContextMenu"; 15 const kPaletteItemContextMenu = "customizationPaletteItemContextMenu"; 16 17 const kDownloadAutohideCheckboxId = "downloads-button-autohide-checkbox"; 18 const kDownloadAutohidePanelId = "downloads-button-autohide-panel"; 19 const kDownloadAutoHidePref = "browser.download.autohideButton"; 20 21 import { CustomizableUI } from "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs"; 22 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 23 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 24 25 const lazy = {}; 26 27 ChromeUtils.defineESModuleGetters(lazy, { 28 BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", 29 DragPositionManager: 30 "moz-src:///browser/components/customizableui/DragPositionManager.sys.mjs", 31 URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs", 32 }); 33 ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () { 34 const kUrl = 35 "chrome://browser/locale/customizableui/customizableWidgets.properties"; 36 return Services.strings.createBundle(kUrl); 37 }); 38 XPCOMUtils.defineLazyServiceGetter( 39 lazy, 40 "gTouchBarUpdater", 41 "@mozilla.org/widget/touchbarupdater;1", 42 Ci.nsITouchBarUpdater 43 ); 44 45 let gDebug; 46 ChromeUtils.defineLazyGetter(lazy, "log", () => { 47 let { ConsoleAPI } = ChromeUtils.importESModule( 48 "resource://gre/modules/Console.sys.mjs" 49 ); 50 gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false); 51 let consoleOptions = { 52 maxLogLevel: gDebug ? "all" : "log", 53 prefix: "CustomizeMode", 54 }; 55 return new ConsoleAPI(consoleOptions); 56 }); 57 58 var gDraggingInToolbars; 59 60 var gTab; 61 62 function closeGlobalTab() { 63 let win = gTab.ownerGlobal; 64 if (win.gBrowser.browsers.length == 1) { 65 win.BrowserCommands.openTab(); 66 } 67 win.gBrowser.removeTab(gTab, { animate: true }); 68 gTab = null; 69 } 70 71 var gTabsProgressListener = { 72 onLocationChange(aBrowser, aWebProgress, aRequest, aLocation) { 73 // Tear down customize mode when the customize mode tab loads some other page. 74 // Customize mode will be re-entered if "about:blank" is loaded again, so 75 // don't tear down in this case. 76 if ( 77 !gTab || 78 gTab.linkedBrowser != aBrowser || 79 aLocation.spec == "about:blank" 80 ) { 81 return; 82 } 83 84 unregisterGlobalTab(); 85 }, 86 }; 87 88 function unregisterGlobalTab() { 89 gTab.removeEventListener("TabClose", unregisterGlobalTab); 90 let win = gTab.ownerGlobal; 91 win.removeEventListener("unload", unregisterGlobalTab); 92 win.gBrowser.removeTabsProgressListener(gTabsProgressListener); 93 94 gTab.removeAttribute("customizemode"); 95 96 gTab = null; 97 } 98 99 /** 100 * This class manages the lifetime of the CustomizeMode UI in a single browser 101 * window. There is one instance of CustomizeMode per browser window. 102 */ 103 export class CustomizeMode { 104 constructor(aWindow) { 105 this.#window = aWindow; 106 this.#document = aWindow.document; 107 this.#browser = aWindow.gBrowser; 108 this.areas = new Set(); 109 110 this.#translationObserver = new aWindow.MutationObserver(mutations => 111 this.#onTranslations(mutations) 112 ); 113 this.#ensureCustomizationPanels(); 114 115 let content = this.$("customization-content-container"); 116 if (!content) { 117 this.#window.MozXULElement.insertFTLIfNeeded("browser/customizeMode.ftl"); 118 let container = this.$("customization-container"); 119 container.replaceChild( 120 this.#window.MozXULElement.parseXULToFragment( 121 container.firstChild.data 122 ), 123 container.lastChild 124 ); 125 } 126 127 this.#attachEventListeners(); 128 129 // There are two palettes - there's the palette that can be overlayed with 130 // toolbar items in browser.xhtml. This is invisible, and never seen by the 131 // user. Then there's the visible palette, which gets populated and displayed 132 // to the user when in customizing mode. 133 this.visiblePalette = this.$(kPaletteId); 134 this.pongArena = this.$("customization-pong-arena"); 135 136 if (this.#canDrawInTitlebar()) { 137 this.#updateTitlebarCheckbox(); 138 Services.prefs.addObserver(kDrawInTitlebarPref, this); 139 } else { 140 this.$("customization-titlebar-visibility-checkbox").hidden = true; 141 } 142 143 // Observe pref changes to the bookmarks toolbar visibility, 144 // since we won't get a toolbarvisibilitychange event if the 145 // toolbar is changing from 'newtab' to 'always' in Customize mode 146 // since the toolbar is shown with the 'newtab' setting. 147 Services.prefs.addObserver(kBookmarksToolbarPref, this); 148 149 this.#window.addEventListener("unload", this); 150 } 151 152 /** 153 * True if CustomizeMode is in the process of being transitioned into or out 154 * of. 155 * 156 * @type {boolean} 157 */ 158 #transitioning = false; 159 160 /** 161 * A reference to the top-level browser window that this instance of 162 * CustomizeMode is configured to use. 163 * 164 * @type {DOMWindow} 165 */ 166 #window = null; 167 168 /** 169 * A reference to the top-level browser window document that this instance of 170 * CustomizeMode is configured to use. 171 * 172 * @type {Document} 173 */ 174 #document = null; 175 176 /** 177 * A reference to the Tabbrowser instance that belongs to the top-level 178 * browser window that CustomizeMode is configured to use. 179 * 180 * @type {Tabbrowser} 181 */ 182 #browser = null; 183 184 /** 185 * An array of customize target DOM nodes that this instance of CustomizeMode 186 * can be used to manipulate. It is assumed that when targets are in this 187 * Set, that they have drag / drop listeners attached and that their 188 * customizable children have been wrapped as toolbarpaletteitems. 189 * 190 * @type {null|Set<DOMNode>} 191 */ 192 areas = null; 193 194 /** 195 * When in customizing mode, we swap out the reference to the invisible 196 * palette in gNavToolbox.palette for our visiblePalette. This way, for the 197 * customizing browser window, when widgets are removed from customizable 198 * areas and added to the palette, they're added to the visible palette. 199 * #stowedPalette is a reference to the old invisible palette so we can 200 * restore gNavToolbox.palette to its original state after exiting 201 * customization mode. 202 * 203 * @type {null|DOMNode} 204 */ 205 #stowedPalette = null; 206 207 /** 208 * If a drag and drop operation is underway for a customizable toolbar item, 209 * this member is set to the current item being dragged over. 210 * 211 * @type {null|DOMNode} 212 */ 213 #dragOverItem = null; 214 215 /** 216 * True if we're in the state of customizing this browser window. 217 * 218 * @type {boolean} 219 */ 220 #customizing = false; 221 222 /** 223 * True if we're synthesizing drag and drop in customize mode for automated 224 * tests and want to skip the checks that ensure that the source of the 225 * drag events was this top-level browser window. This is controllable via 226 * `browser.uiCustomization.skipSourceNodeCheck`. 227 * 228 * @type {boolean} 229 */ 230 #skipSourceNodeCheck = false; 231 232 /** 233 * These are the commands we continue to leave enabled while in customize 234 * mode. All other commands are disabled, and we remove the disabled attribute 235 * when leaving customize mode. 236 * 237 * @type {Set<string>} 238 */ 239 #enabledCommands = new Set([ 240 "cmd_newNavigator", 241 "cmd_newNavigatorTab", 242 "cmd_newNavigatorTabNoEvent", 243 "cmd_close", 244 "cmd_closeWindow", 245 "cmd_maximizeWindow", 246 "cmd_minimizeWindow", 247 "cmd_restoreWindow", 248 "cmd_quitApplication", 249 "View:FullScreen", 250 "Browser:NextTab", 251 "Browser:PrevTab", 252 "Browser:NewUserContextTab", 253 "Tools:PrivateBrowsing", 254 "zoomWindow", 255 "cmd_newIdentity", 256 "cmd_newCircuit", 257 ]); 258 259 /** 260 * A MutationObserver used to hear about Fluent localization occurring for 261 * customizable items. 262 * 263 * @type {MutationObserver|null} 264 */ 265 #translationObserver = null; 266 267 /** 268 * A description of the size of a customizable item were it to be placed in a 269 * particular customizable area. 270 * 271 * @typedef {object} ItemSizeForArea 272 * @property {number} width 273 * The width of the customizable item when placed in a certain area. 274 * @property {number} height 275 * The height of the customizable item when placed in a certain area. 276 */ 277 278 /** 279 * A WeakMap mapping dragged customizable items to a WeakMap of areas that 280 * the item could be dragged into. That mapping maps to ItemSizeForArea 281 * objects that describe the width and height of the item were it to be 282 * dropped and placed within that area (since certain areas will encourage 283 * items to expand or contract). 284 * 285 * @type {WeakMap<DOMNode, WeakMap<DOMNode, DragSizeForArea>>|null} 286 */ 287 #dragSizeMap = null; 288 289 /** 290 * If this is set to true, this means that the user enabled the downloads 291 * button auto-hide feature while the button was in the palette. If so, then 292 * on exiting mode, the button is moved to its default position in the 293 * navigation toolbar. 294 */ 295 #moveDownloadsButtonToNavBar = false; 296 297 /** 298 * Returns the CustomizationHandler browser window global object. See 299 * browser-customization.js. 300 * 301 * @type {object} 302 */ 303 get #handler() { 304 return this.#window.CustomizationHandler; 305 } 306 307 /** 308 * Does cleanup of any listeners or observers when the browser window 309 * that this CustomizeMode instance is configured to use unloads. 310 */ 311 #uninit() { 312 if (this.#canDrawInTitlebar()) { 313 Services.prefs.removeObserver(kDrawInTitlebarPref, this); 314 } 315 Services.prefs.removeObserver(kBookmarksToolbarPref, this); 316 } 317 318 /** 319 * This is a shortcut for this.#document.getElementById. 320 * 321 * @param {string} id 322 * The DOM ID to return a result for. 323 * @returns {DOMNode|null} 324 */ 325 $(id) { 326 return this.#document.getElementById(id); 327 } 328 329 /** 330 * Sets the fake tab element that will be associated with being in 331 * customize mode. Customize mode looks similar to a "special kind of tab", 332 * and when that tab is closed, we exit customize mode. When that tab is 333 * switched to, we enter customize mode. If that tab is restored in the 334 * foreground, we enter customize mode. 335 * 336 * This method assigns the special <xul:tab> that will represent customize 337 * mode for this window, and sets up the relevant listeners to it. The 338 * tab will have a "customizemode" attribute set to "true" on it, as well as 339 * a special favicon. 340 * 341 * @param {MozTabbrowserTab} aTab 342 * The tab to act as the tab representation of customize mode. 343 */ 344 setTab(aTab) { 345 if (gTab == aTab) { 346 return; 347 } 348 349 if (gTab) { 350 closeGlobalTab(); 351 } 352 353 gTab = aTab; 354 355 gTab.setAttribute("customizemode", "true"); 356 357 if (gTab.linkedPanel) { 358 gTab.linkedBrowser.stop(); 359 } 360 361 let win = gTab.ownerGlobal; 362 363 win.gBrowser.setTabTitle(gTab); 364 win.gBrowser.setIcon(gTab, "chrome://browser/skin/customize.svg"); 365 366 gTab.addEventListener("TabClose", unregisterGlobalTab); 367 368 win.gBrowser.addTabsProgressListener(gTabsProgressListener); 369 370 win.addEventListener("unload", unregisterGlobalTab); 371 372 if (gTab.selected) { 373 win.gCustomizeMode.enter(); 374 } 375 } 376 377 /** 378 * Kicks off the process of entering customize mode for the window that this 379 * CustomizeMode instance was constructed with. If this window happens to be 380 * a popup window or web app window, the opener window will enter customize mode. 381 * 382 * Entering customize mode is a multistep asynchronous operation, but this 383 * method returns immediately while this operation is underway. A 384 * `customizationready` custom event is dispatched on the gNavToolbox when 385 * this asynchronous process has completed. 386 * 387 * This method will return early if customize mode is already active in this 388 * window. 389 */ 390 enter() { 391 if ( 392 !this.#window.toolbar.visible || 393 this.#window.document.documentElement.hasAttribute("taskbartab") 394 ) { 395 let w = lazy.URILoadingHelper.getTargetWindow(this.#window, { 396 skipPopups: true, 397 skipTaskbarTabs: true, 398 }); 399 if (w) { 400 w.gCustomizeMode.enter(); 401 return; 402 } 403 let obs = () => { 404 Services.obs.removeObserver(obs, "browser-delayed-startup-finished"); 405 w = lazy.URILoadingHelper.getTargetWindow(this.#window, { 406 skipPopups: true, 407 skipTaskbarTabs: true, 408 }); 409 w.gCustomizeMode.enter(); 410 }; 411 Services.obs.addObserver(obs, "browser-delayed-startup-finished"); 412 this.#window.openTrustedLinkIn("about:newtab", "window"); 413 return; 414 } 415 this._wantToBeInCustomizeMode = true; 416 417 if (this.#customizing || this.#handler.isEnteringCustomizeMode) { 418 return; 419 } 420 421 // Exiting; want to re-enter once we've done that. 422 if (this.#handler.isExitingCustomizeMode) { 423 lazy.log.debug( 424 "Attempted to enter while we're in the middle of exiting. " + 425 "We'll exit after we've entered" 426 ); 427 return; 428 } 429 430 if (!gTab) { 431 this.setTab( 432 this.#browser.addTab("about:blank", { 433 inBackground: false, 434 forceNotRemote: true, 435 skipAnimation: true, 436 triggeringPrincipal: 437 Services.scriptSecurityManager.getSystemPrincipal(), 438 }) 439 ); 440 return; 441 } 442 if (!gTab.selected) { 443 // This will force another .enter() to be called via the 444 // onlocationchange handler of the tabbrowser, so we return early. 445 gTab.ownerGlobal.gBrowser.selectedTab = gTab; 446 return; 447 } 448 gTab.ownerGlobal.focus(); 449 if (gTab.ownerDocument != this.#document) { 450 return; 451 } 452 453 let window = this.#window; 454 let document = this.#document; 455 456 this.#handler.isEnteringCustomizeMode = true; 457 458 // Always disable the reset button at the start of customize mode, it'll be re-enabled 459 // if necessary when we finish entering: 460 let resetButton = this.$("customization-reset-button"); 461 resetButton.setAttribute("disabled", "true"); 462 463 (async () => { 464 // We shouldn't start customize mode until after browser-delayed-startup has finished: 465 if (!this.#window.gBrowserInit.delayedStartupFinished) { 466 await new Promise(resolve => { 467 let delayedStartupObserver = aSubject => { 468 if (aSubject == this.#window) { 469 Services.obs.removeObserver( 470 delayedStartupObserver, 471 "browser-delayed-startup-finished" 472 ); 473 resolve(); 474 } 475 }; 476 477 Services.obs.addObserver( 478 delayedStartupObserver, 479 "browser-delayed-startup-finished" 480 ); 481 }); 482 } 483 484 CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window); 485 CustomizableUI.notifyStartCustomizing(this.#window); 486 487 // Add a keypress listener to the document so that we can quickly exit 488 // customization mode when pressing ESC. 489 document.addEventListener("keypress", this); 490 491 // Same goes for the menu button - if we're customizing, a click on the 492 // menu button means a quick exit from customization mode. 493 window.PanelUI.hide(); 494 495 let panelHolder = document.getElementById("customization-panelHolder"); 496 let panelContextMenu = document.getElementById(kPanelItemContextMenu); 497 this._previousPanelContextMenuParent = panelContextMenu.parentNode; 498 document.getElementById("mainPopupSet").appendChild(panelContextMenu); 499 panelHolder.appendChild(window.PanelUI.overflowFixedList); 500 501 window.PanelUI.overflowFixedList.toggleAttribute("customizing", true); 502 window.PanelUI.menuButton.disabled = true; 503 document.getElementById("nav-bar-overflow-button").disabled = true; 504 505 this.#transitioning = true; 506 507 let customizer = document.getElementById("customization-container"); 508 let browser = document.getElementById("browser"); 509 browser.hidden = true; 510 customizer.hidden = false; 511 512 this.#wrapAreaItemsSync(CustomizableUI.AREA_TABSTRIP); 513 514 this.#document.documentElement.toggleAttribute("customizing", true); 515 516 let customizableToolbars = document.querySelectorAll( 517 "toolbar[customizable=true]:not([autohide], [collapsed])" 518 ); 519 for (let toolbar of customizableToolbars) { 520 toolbar.toggleAttribute("customizing", true); 521 } 522 523 this.#updateOverflowPanelArrowOffset(); 524 525 // Let everybody in this window know that we're about to customize. 526 CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window); 527 528 await this.#wrapAllAreaItems(); 529 this.#populatePalette(); 530 531 this.#setupPaletteDragging(); 532 533 window.gNavToolbox.addEventListener("toolbarvisibilitychange", this); 534 535 this.#updateResetButton(); 536 this.#updateUndoResetButton(); 537 this.#updateTouchBarButton(); 538 this.#updateDensityMenu(); 539 540 this.#skipSourceNodeCheck = 541 Services.prefs.getPrefType(kSkipSourceNodePref) == 542 Ci.nsIPrefBranch.PREF_BOOL && 543 Services.prefs.getBoolPref(kSkipSourceNodePref); 544 545 CustomizableUI.addListener(this); 546 this.#customizing = true; 547 this.#transitioning = false; 548 549 // Show the palette now that the transition has finished. 550 this.visiblePalette.hidden = false; 551 window.setTimeout(() => { 552 // Force layout reflow to ensure the animation runs, 553 // and make it async so it doesn't affect the timing. 554 this.visiblePalette.clientTop; 555 this.visiblePalette.setAttribute("showing", "true"); 556 }, 0); 557 this.#updateEmptyPaletteNotice(); 558 559 this.#setupDownloadAutoHideToggle(); 560 561 this.#handler.isEnteringCustomizeMode = false; 562 563 CustomizableUI.dispatchToolboxEvent("customizationready", {}, window); 564 565 if (!this._wantToBeInCustomizeMode) { 566 this.exit(); 567 } 568 })().catch(e => { 569 lazy.log.error("Error entering customize mode", e); 570 this.#handler.isEnteringCustomizeMode = false; 571 // Exit customize mode to ensure proper clean-up when entering failed. 572 this.exit(); 573 }); 574 } 575 576 /** 577 * Exits customize mode, if we happen to be in it. This is a no-op if we 578 * are not in customize mode. 579 * 580 * This is a multi-step asynchronous operation, but this method returns 581 * synchronously after the operation begins. An `aftercustomization` 582 * custom event is dispatched on the gNavToolbox once the operation has 583 * completed. 584 */ 585 exit() { 586 this._wantToBeInCustomizeMode = false; 587 588 if (!this.#customizing || this.#handler.isExitingCustomizeMode) { 589 return; 590 } 591 592 // Entering; want to exit once we've done that. 593 if (this.#handler.isEnteringCustomizeMode) { 594 lazy.log.debug( 595 "Attempted to exit while we're in the middle of entering. " + 596 "We'll exit after we've entered" 597 ); 598 return; 599 } 600 601 if (this.resetting) { 602 lazy.log.debug( 603 "Attempted to exit while we're resetting. " + 604 "We'll exit after resetting has finished." 605 ); 606 return; 607 } 608 609 this.#handler.isExitingCustomizeMode = true; 610 611 this.#translationObserver.disconnect(); 612 613 this.#teardownDownloadAutoHideToggle(); 614 615 CustomizableUI.removeListener(this); 616 617 let window = this.#window; 618 let document = this.#document; 619 620 document.removeEventListener("keypress", this); 621 622 this.#togglePong(false); 623 624 // Disable the reset and undo reset buttons while transitioning: 625 let resetButton = this.$("customization-reset-button"); 626 let undoResetButton = this.$("customization-undo-reset-button"); 627 undoResetButton.hidden = resetButton.disabled = true; 628 629 this.#transitioning = true; 630 631 this.#depopulatePalette(); 632 633 // We need to set this.#customizing to false and remove the `customizing` 634 // attribute before removing the tab or else 635 // XULBrowserWindow.onLocationChange might think that we're still in 636 // customization mode and need to exit it for a second time. 637 this.#customizing = false; 638 document.documentElement.removeAttribute("customizing"); 639 640 if (this.#browser.selectedTab == gTab) { 641 closeGlobalTab(); 642 } 643 644 let customizer = document.getElementById("customization-container"); 645 let browser = document.getElementById("browser"); 646 customizer.hidden = true; 647 browser.hidden = false; 648 649 window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this); 650 651 this.#teardownPaletteDragging(); 652 653 (async () => { 654 await this.#unwrapAllAreaItems(); 655 656 // And drop all area references. 657 this.areas.clear(); 658 659 // Let everybody in this window know that we're starting to 660 // exit customization mode. 661 CustomizableUI.dispatchToolboxEvent("customizationending", {}, window); 662 663 window.PanelUI.menuButton.disabled = false; 664 let overflowContainer = document.getElementById( 665 "widget-overflow-mainView" 666 ).firstElementChild; 667 overflowContainer.appendChild(window.PanelUI.overflowFixedList); 668 document.getElementById("nav-bar-overflow-button").disabled = false; 669 let panelContextMenu = document.getElementById(kPanelItemContextMenu); 670 this._previousPanelContextMenuParent.appendChild(panelContextMenu); 671 672 let customizableToolbars = document.querySelectorAll( 673 "toolbar[customizable=true]:not([autohide])" 674 ); 675 for (let toolbar of customizableToolbars) { 676 toolbar.removeAttribute("customizing"); 677 } 678 679 this.#maybeMoveDownloadsButtonToNavBar(); 680 681 delete this._lastLightweightTheme; 682 this.#transitioning = false; 683 this.#handler.isExitingCustomizeMode = false; 684 CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window); 685 CustomizableUI.notifyEndCustomizing(window); 686 687 if (this._wantToBeInCustomizeMode) { 688 this.enter(); 689 } 690 })().catch(e => { 691 lazy.log.error("Error exiting customize mode", e); 692 this.#handler.isExitingCustomizeMode = false; 693 }); 694 } 695 696 /** 697 * The overflow panel in customize mode should have its arrow pointing 698 * at the overflow button. In order to do this correctly, we pass the 699 * distance between the inside of window and the middle of the button 700 * to the customize mode markup in which the arrow and panel are placed. 701 * 702 * The returned Promise resolves once the offset has been set on the panel 703 * wrapper. 704 * 705 * @returns {Promise<undefined>} 706 */ 707 async #updateOverflowPanelArrowOffset() { 708 let currentDensity = 709 this.#document.documentElement.getAttribute("uidensity"); 710 let offset = await this.#window.promiseDocumentFlushed(() => { 711 let overflowButton = this.$("nav-bar-overflow-button"); 712 let buttonRect = overflowButton.getBoundingClientRect(); 713 let endDistance; 714 if (this.#window.RTL_UI) { 715 endDistance = buttonRect.left; 716 } else { 717 endDistance = this.#window.innerWidth - buttonRect.right; 718 } 719 return endDistance + buttonRect.width / 2; 720 }); 721 if ( 722 !this.#document || 723 currentDensity != this.#document.documentElement.getAttribute("uidensity") 724 ) { 725 return; 726 } 727 this.$("customization-panelWrapper").style.setProperty( 728 "--panel-arrow-offset", 729 offset + "px" 730 ); 731 } 732 733 /** 734 * Given some DOM node, attempts to resolve to the relevant child or overflow 735 * target node that can have customizable items placed within it. It may 736 * resolve to aNode itself, if aNode can have customizable items placed 737 * directly within it. If the node does not appear to have such a child, this 738 * method will return `null`. 739 * 740 * @param {DOMNode} aNode 741 * @returns {null|DOMNode} 742 */ 743 #getCustomizableChildForNode(aNode) { 744 // NB: adjusted from #getCustomizableParent to keep that method fast 745 // (it's used during drags), and avoid multiple DOM loops 746 let areas = CustomizableUI.areas; 747 // Caching this length is important because otherwise we'll also iterate 748 // over items we add to the end from within the loop. 749 let numberOfAreas = areas.length; 750 for (let i = 0; i < numberOfAreas; i++) { 751 let area = areas[i]; 752 let areaNode = aNode.ownerDocument.getElementById(area); 753 let customizationTarget = CustomizableUI.getCustomizationTarget(areaNode); 754 if (customizationTarget && customizationTarget != areaNode) { 755 areas.push(customizationTarget.id); 756 } 757 let overflowTarget = 758 areaNode && areaNode.getAttribute("default-overflowtarget"); 759 if (overflowTarget) { 760 areas.push(overflowTarget); 761 } 762 } 763 areas.push(kPaletteId); 764 765 while (aNode && aNode.parentNode) { 766 let parent = aNode.parentNode; 767 if (areas.includes(parent.id)) { 768 return aNode; 769 } 770 aNode = parent; 771 } 772 return null; 773 } 774 775 /** 776 * Kicks off an animation for aNode that causes it to scale down and become 777 * transparent before being removed from a toolbar. This can be seen when 778 * using the context menu to remove an item from a toolbar. 779 * 780 * For nodes that are within the overflow panel, aren't 781 * toolbaritem / toolbarbuttons, or is the hidden downloads button, this 782 * returns `null` immediately and no animation is performed. If reduced 783 * motion is enabled, this returns `null` immediately and no animation is 784 * performed. 785 * 786 * @param {DOMNode} aNode 787 * The node to be removed from the toolbar. 788 * @returns {null|Promise<DOMNode>} 789 * Returns `null` if no animation is going to occur, or the DOMNode that 790 * the animation was performed on after the animation has completed. 791 */ 792 #promiseWidgetAnimationOut(aNode) { 793 if ( 794 this.#window.gReduceMotion || 795 aNode.getAttribute("cui-anchorid") == "nav-bar-overflow-button" || 796 (aNode.tagName != "toolbaritem" && aNode.tagName != "toolbarbutton") || 797 (aNode.id == "downloads-button" && aNode.hidden) 798 ) { 799 return null; 800 } 801 802 let animationNode; 803 if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) { 804 animationNode = aNode.parentNode; 805 } else { 806 animationNode = aNode; 807 } 808 return new Promise(resolve => { 809 function cleanupCustomizationExit() { 810 resolveAnimationPromise(); 811 } 812 813 function cleanupWidgetAnimationEnd(e) { 814 if ( 815 e.animationName == "widget-animate-out" && 816 e.target.id == animationNode.id 817 ) { 818 resolveAnimationPromise(); 819 } 820 } 821 822 function resolveAnimationPromise() { 823 animationNode.removeEventListener( 824 "animationend", 825 cleanupWidgetAnimationEnd 826 ); 827 animationNode.removeEventListener( 828 "customizationending", 829 cleanupCustomizationExit 830 ); 831 resolve(animationNode); 832 } 833 834 // Wait until the next frame before setting the class to ensure 835 // we do start the animation. 836 this.#window.requestAnimationFrame(() => { 837 this.#window.requestAnimationFrame(() => { 838 animationNode.classList.add("animate-out"); 839 animationNode.ownerGlobal.gNavToolbox.addEventListener( 840 "customizationending", 841 cleanupCustomizationExit 842 ); 843 animationNode.addEventListener( 844 "animationend", 845 cleanupWidgetAnimationEnd 846 ); 847 }); 848 }); 849 }); 850 } 851 852 /** 853 * Moves a customizable item to the end of the navbar. This is used primarily 854 * by the toolbar item context menus regardless of whether or not we're in 855 * customize mode. If this item is within a toolbar, this may kick off an 856 * animation that shrinks the item icon and causes it to become transparent 857 * before the move occurs. The returned Promise resolves once the animation 858 * and the move has completed. 859 * 860 * @param {DOMNode} aNode 861 * The node to be moved to the end of the navbar area. 862 * @returns {Promise<undefined>} 863 * Resolves once the move has completed. 864 */ 865 async addToToolbar(aNode) { 866 aNode = this.#getCustomizableChildForNode(aNode); 867 if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) { 868 aNode = aNode.firstElementChild; 869 } 870 let widgetAnimationPromise = this.#promiseWidgetAnimationOut(aNode); 871 let animationNode; 872 if (widgetAnimationPromise) { 873 animationNode = await widgetAnimationPromise; 874 } 875 876 let widgetToAdd = aNode.id; 877 if ( 878 CustomizableUI.isSpecialWidget(widgetToAdd) && 879 aNode.closest("#customization-palette") 880 ) { 881 widgetToAdd = widgetToAdd.match( 882 /^customizableui-special-(spring|spacer|separator)/ 883 )[1]; 884 } 885 886 CustomizableUI.addWidgetToArea(widgetToAdd, CustomizableUI.AREA_NAVBAR); 887 lazy.BrowserUsageTelemetry.recordWidgetChange( 888 widgetToAdd, 889 CustomizableUI.AREA_NAVBAR 890 ); 891 if (!this.#customizing) { 892 CustomizableUI.dispatchToolboxEvent("customizationchange"); 893 } 894 895 // If the user explicitly moves this item, turn off autohide. 896 if (aNode.id == "downloads-button") { 897 Services.prefs.setBoolPref(kDownloadAutoHidePref, false); 898 if (this.#customizing) { 899 this.#showDownloadsAutoHidePanel(); 900 } 901 } 902 903 if (animationNode) { 904 animationNode.classList.remove("animate-out"); 905 } 906 } 907 908 /** 909 * Pins a customizable widget to the overflow panel. This is used by the 910 * toolbar item context menus regardless of whether or not we're in customize 911 * mode. It is also on the widget context menu within the overflow panel when 912 * the widget is placed there temporarily during a toolbar overflow. If this 913 * item is within a toolbar, this may kick off an* animation that shrinks the 914 * item icon and causes it to become transparent before the move occurs. The 915 * returned Promise resolves once the animation and the move has completed. 916 * 917 * @param {DOMNode} aNode 918 * The node to be moved to the overflow panel. 919 * @param {string} aReason 920 * A string to describe why the item is being moved to the overflow panel. 921 * This is passed along to BrowserUsageTelemetry.recordWidgetChange, and 922 * is dash-delimited. 923 * 924 * Examples: 925 * 926 * "toolbar-context-menu": the reason is that the user chose to do this via 927 * the toolbar context menu. 928 * 929 * "panelitem-context": the reason is that the user chose to do this via 930 * the overflow panel item context menu when the item was moved there 931 * temporarily during a toolbar overflow. 932 * @returns {Promise<undefined>} 933 * Resolves once the move has completed. 934 */ 935 async addToPanel(aNode, aReason) { 936 aNode = this.#getCustomizableChildForNode(aNode); 937 if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) { 938 aNode = aNode.firstElementChild; 939 } 940 let widgetAnimationPromise = this.#promiseWidgetAnimationOut(aNode); 941 let animationNode; 942 if (widgetAnimationPromise) { 943 animationNode = await widgetAnimationPromise; 944 } 945 946 let panel = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL; 947 CustomizableUI.addWidgetToArea(aNode.id, panel); 948 lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, panel, aReason); 949 if (!this.#customizing) { 950 CustomizableUI.dispatchToolboxEvent("customizationchange"); 951 } 952 953 // If the user explicitly moves this item, turn off autohide. 954 if (aNode.id == "downloads-button") { 955 Services.prefs.setBoolPref(kDownloadAutoHidePref, false); 956 if (this.#customizing) { 957 this.#showDownloadsAutoHidePanel(); 958 } 959 } 960 961 if (animationNode) { 962 animationNode.classList.remove("animate-out"); 963 } 964 if (!this.#window.gReduceMotion) { 965 let overflowButton = this.$("nav-bar-overflow-button"); 966 overflowButton.setAttribute("animate", "true"); 967 overflowButton.addEventListener( 968 "animationend", 969 function onAnimationEnd(event) { 970 if (event.animationName.startsWith("overflow-animation")) { 971 this.removeEventListener("animationend", onAnimationEnd); 972 this.removeAttribute("animate"); 973 } 974 } 975 ); 976 } 977 } 978 979 /** 980 * Removes a customizable item from its area and puts it in the palette. This 981 * is used by the toolbar item context menus regardless of whether or not 982 * we're in customize mode. It is also on the widget context menu within the 983 * overflow panel when the widget is placed there temporarily during a toolbar 984 * overflow. If this item is within a toolbar, this may kick off an animation 985 * that shrinks the item icon and causes it to become transparent before the 986 * removal occurs. The returned Promise resolves once the animation and the 987 * removal has completed. 988 * 989 * @param {DOMNode} aNode 990 * The node to be removed and placed into the palette. 991 * @param {string} aReason 992 * A string to describe why the item is being removed. 993 * This is passed along to BrowserUsageTelemetry.recordWidgetChange, and 994 * is dash-delimited. 995 * 996 * Examples: 997 * 998 * "toolbar-context-menu": the reason is that the user chose to do this via 999 * the toolbar context menu. 1000 * 1001 * "panelitem-context": the reason is that the user chose to do this via 1002 * the overflow panel item context menu when the item was moved there 1003 * temporarily during a toolbar overflow. 1004 * @returns {Promise<undefined>} 1005 * Resolves once the removal has completed. 1006 */ 1007 async removeFromArea(aNode, aReason) { 1008 aNode = this.#getCustomizableChildForNode(aNode); 1009 if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) { 1010 aNode = aNode.firstElementChild; 1011 } 1012 let widgetAnimationPromise = this.#promiseWidgetAnimationOut(aNode); 1013 let animationNode; 1014 if (widgetAnimationPromise) { 1015 animationNode = await widgetAnimationPromise; 1016 } 1017 1018 CustomizableUI.removeWidgetFromArea(aNode.id); 1019 lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, null, aReason); 1020 if (!this.#customizing) { 1021 CustomizableUI.dispatchToolboxEvent("customizationchange"); 1022 } 1023 1024 // If the user explicitly removes this item, turn off autohide. 1025 if (aNode.id == "downloads-button") { 1026 Services.prefs.setBoolPref(kDownloadAutoHidePref, false); 1027 if (this.#customizing) { 1028 this.#showDownloadsAutoHidePanel(); 1029 } 1030 } 1031 if (animationNode) { 1032 animationNode.classList.remove("animate-out"); 1033 } 1034 } 1035 1036 /** 1037 * Populates the visible palette seen in the content area when entering 1038 * customize mode. This moves items from the normal "hidden" palette that 1039 * belongs to the toolbox, and then temporarily overrides the toolbox 1040 * with the visible palette until we exit customize mode. 1041 */ 1042 #populatePalette() { 1043 let fragment = this.#document.createDocumentFragment(); 1044 let toolboxPalette = this.#window.gNavToolbox.palette; 1045 1046 try { 1047 let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette); 1048 for (let widget of unusedWidgets) { 1049 let paletteItem = this.#makePaletteItem(widget); 1050 if (!paletteItem) { 1051 continue; 1052 } 1053 fragment.appendChild(paletteItem); 1054 } 1055 1056 let flexSpace = CustomizableUI.createSpecialWidget( 1057 "spring", 1058 this.#document 1059 ); 1060 fragment.appendChild(this.wrapToolbarItem(flexSpace, "palette")); 1061 1062 this.visiblePalette.appendChild(fragment); 1063 this.#stowedPalette = this.#window.gNavToolbox.palette; 1064 this.#window.gNavToolbox.palette = this.visiblePalette; 1065 1066 // Now that the palette items are all here, disable all commands. 1067 // We do this here rather than directly in `enter` because we 1068 // need to do/undo this when we're called from reset(), too. 1069 this.#updateCommandsDisabledState(true); 1070 } catch (ex) { 1071 lazy.log.error(ex); 1072 } 1073 } 1074 1075 /** 1076 * For a given widget, finds the associated node within this window and then 1077 * creates / updates a toolbarpaletteitem that will wrap that node to make 1078 * drag and drop operations on that node work while in customize mode. 1079 * 1080 * @param {WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget 1081 * @returns {DOMNode} 1082 */ 1083 #makePaletteItem(aWidget) { 1084 let widgetNode = aWidget.forWindow(this.#window).node; 1085 if (!widgetNode) { 1086 lazy.log.error( 1087 "Widget with id " + aWidget.id + " does not return a valid node" 1088 ); 1089 return null; 1090 } 1091 // Do not build a palette item for hidden widgets; there's not much to show. 1092 if (widgetNode.hidden) { 1093 return null; 1094 } 1095 1096 let wrapper = this.createOrUpdateWrapper(widgetNode, "palette"); 1097 wrapper.appendChild(widgetNode); 1098 return wrapper; 1099 } 1100 1101 /** 1102 * Unwraps and moves items from the visible palette back to the hidden palette 1103 * when exiting customize mode. This also reassigns the toolbox's palette 1104 * property to point back at the default hidden palette, as this was 1105 * overridden to be the visible palette in #populatePalette. 1106 */ 1107 #depopulatePalette() { 1108 // Quick, undo the command disabling before we depopulate completely: 1109 this.#updateCommandsDisabledState(false); 1110 1111 this.visiblePalette.hidden = true; 1112 let paletteChild = this.visiblePalette.firstElementChild; 1113 let nextChild; 1114 while (paletteChild) { 1115 nextChild = paletteChild.nextElementSibling; 1116 let itemId = paletteChild.firstElementChild.id; 1117 if (CustomizableUI.isSpecialWidget(itemId)) { 1118 this.visiblePalette.removeChild(paletteChild); 1119 } else { 1120 // XXXunf Currently this doesn't destroy the (now unused) node in the 1121 // API provider case. It would be good to do so, but we need to 1122 // keep strong refs to it in CustomizableUI (can't iterate of 1123 // WeakMaps), and there's the question of what behavior 1124 // wrappers should have if consumers keep hold of them. 1125 let unwrappedPaletteItem = this.unwrapToolbarItem(paletteChild); 1126 this.#stowedPalette.appendChild(unwrappedPaletteItem); 1127 } 1128 1129 paletteChild = nextChild; 1130 } 1131 this.visiblePalette.hidden = false; 1132 this.#window.gNavToolbox.palette = this.#stowedPalette; 1133 } 1134 1135 /** 1136 * For all <command> elements in the window document that have no ID, or 1137 * are not in the #enabledCommands set, puts them in the "disabled" state if 1138 * `shouldBeDisabled` is true. For any command that was already disabled, adds 1139 * a "wasdisabled" attribute to the command. 1140 * 1141 * If `shouldBeDisabled` is false, removes the "wasdisabled" attribute from 1142 * any command nodes that have them, and for those that don't, removes the 1143 * "disabled" attribute. 1144 * 1145 * @param {boolean} shouldBeDisabled 1146 * True if all <command> elements not in #enabledCommands should be 1147 * disabled. False otherwise. 1148 */ 1149 #updateCommandsDisabledState(shouldBeDisabled) { 1150 for (let command of this.#document.querySelectorAll("command")) { 1151 if (!command.id || !this.#enabledCommands.has(command.id)) { 1152 if (shouldBeDisabled) { 1153 if (!command.hasAttribute("disabled")) { 1154 command.setAttribute("disabled", true); 1155 } else { 1156 command.setAttribute("wasdisabled", true); 1157 } 1158 } else if (command.getAttribute("wasdisabled") != "true") { 1159 command.removeAttribute("disabled"); 1160 } else { 1161 command.removeAttribute("wasdisabled"); 1162 } 1163 } 1164 } 1165 } 1166 1167 /** 1168 * Checks if the passed in DOM node is one that can represent a 1169 * customizable widget. 1170 * 1171 * @param {DOMNode} aNode 1172 * The node to check to see if it's a customizable widget node. 1173 * @returns {boolean} 1174 * `true` if the passed in DOM node is a type that can be used for 1175 * customizable widgets. 1176 */ 1177 #isCustomizableItem(aNode) { 1178 return ( 1179 aNode.localName == "toolbarbutton" || 1180 aNode.localName == "toolbaritem" || 1181 aNode.localName == "toolbarseparator" || 1182 aNode.localName == "toolbarspring" || 1183 aNode.localName == "toolbarspacer" 1184 ); 1185 } 1186 1187 /** 1188 * Checks if the passed in DOM node is a toolbarpaletteitem wrapper. 1189 * 1190 * @param {DOMNode} aNode 1191 * The node to check for being wrapped. 1192 * @returns {boolean} 1193 * `true` if the passed in DOM node is a toolbarpaletteitem, meaning 1194 * that it was wrapped via createOrUpdateWrapper. 1195 */ 1196 isWrappedToolbarItem(aNode) { 1197 return aNode.localName == "toolbarpaletteitem"; 1198 } 1199 1200 /** 1201 * Queues a function for the main thread to execute soon that will wrap 1202 * aNode in a toolbarpaletteitem (or update the wrapper if it already exists). 1203 * 1204 * @param {DOMNode} aNode 1205 * The node to wrap in a toolbarpaletteitem. 1206 * @param {string} aPlace 1207 * The string to set as the "place" attribute on the node when it is 1208 * wrapped. This is expected to be one of the strings returned by 1209 * CustomizableUI.getPlaceForItem. 1210 * @returns {Promise<DOMNode>} 1211 * Resolves with the wrapper node, or the node itself if the node is not 1212 * a customizable item. 1213 */ 1214 #deferredWrapToolbarItem(aNode, aPlace) { 1215 return new Promise(resolve => { 1216 Services.tm.dispatchToMainThread(() => { 1217 let wrapper = this.wrapToolbarItem(aNode, aPlace); 1218 resolve(wrapper); 1219 }); 1220 }); 1221 } 1222 1223 /** 1224 * Creates or updates a wrapping toolbarpaletteitem around aNode, presuming 1225 * the node is a customizable item. 1226 * 1227 * @param {DOMNode} aNode 1228 * The node to wrap, or update the wrapper for. 1229 * @param {string} aPlace 1230 * The string to set as the "place" attribute on the node when it is 1231 * wrapped. This is expected to be one of the strings returned by 1232 * CustomizableUI.getPlaceForItem. 1233 * @returns {DOMNode} 1234 * The toolbarbpaletteitem wrapper, in the event that aNode is a 1235 * customizable item. Otherwise, returns aNode. 1236 */ 1237 wrapToolbarItem(aNode, aPlace) { 1238 if (!this.#isCustomizableItem(aNode)) { 1239 return aNode; 1240 } 1241 let wrapper = this.createOrUpdateWrapper(aNode, aPlace); 1242 1243 // It's possible that this toolbar node is "mid-flight" and doesn't have 1244 // a parent, in which case we skip replacing it. This can happen if a 1245 // toolbar item has been dragged into the palette. In that case, we tell 1246 // CustomizableUI to remove the widget from its area before putting the 1247 // widget in the palette - so the node will have no parent. 1248 if (aNode.parentNode) { 1249 aNode = aNode.parentNode.replaceChild(wrapper, aNode); 1250 } 1251 wrapper.appendChild(aNode); 1252 return wrapper; 1253 } 1254 1255 /** 1256 * Helper to set the title and tooltiptext on a toolbarpaletteitem wrapper 1257 * based on the wrapped node - either by reading the label/title attributes 1258 * of aNode, or (in the event of delayed Fluent localization) setting up a 1259 * mutation observer on aNode such that when the label and/or title do 1260 * get set, we re-enter #updateWrapperLabel to update the toolbarpaletteitem. 1261 * 1262 * @param {DOMNode} aNode 1263 * The wrapped customizable item to update the wrapper for. 1264 * @param {boolean} aIsUpdate 1265 * True if the node already has a pre-existing wrapper that is being 1266 * updated rather than created. 1267 * @param {DOMNode} [aWrapper=aNode.parentElement] 1268 * The toolbarpaletteitem wrapper, in the event that it's not the 1269 * immediate ancestor of aNode for some reason. 1270 */ 1271 #updateWrapperLabel(aNode, aIsUpdate, aWrapper = aNode.parentElement) { 1272 if (aNode.hasAttribute("label")) { 1273 aWrapper.setAttribute("title", aNode.getAttribute("label")); 1274 aWrapper.setAttribute("tooltiptext", aNode.getAttribute("label")); 1275 } else if (aNode.hasAttribute("title")) { 1276 aWrapper.setAttribute("title", aNode.getAttribute("title")); 1277 aWrapper.setAttribute("tooltiptext", aNode.getAttribute("title")); 1278 } else if (aNode.hasAttribute("data-l10n-id") && !aIsUpdate) { 1279 this.#translationObserver.observe(aNode, { 1280 attributes: true, 1281 attributeFilter: ["label", "title"], 1282 }); 1283 } 1284 } 1285 1286 /** 1287 * Called when a node without a label or title has those attributes updated. 1288 * 1289 * @param {MutationRecord[]} aMutations 1290 * The list of mutations for the label/title attributes of the nodes that 1291 * had neither of those attributes set when wrapping them. 1292 */ 1293 #onTranslations(aMutations) { 1294 for (let mut of aMutations) { 1295 let { target } = mut; 1296 if ( 1297 target.parentElement?.localName == "toolbarpaletteitem" && 1298 (target.hasAttribute("label") || mut.target.hasAttribute("title")) 1299 ) { 1300 this.#updateWrapperLabel(target, true); 1301 } 1302 } 1303 } 1304 1305 /** 1306 * Creates or updates a toolbarpaletteitem to wrap a customizable item. This 1307 * wrapper makes it possible to click and drag these customizable items around 1308 * in the DOM without the underlying item having its event handlers invoked. 1309 * 1310 * @param {DOMNode} aNode 1311 * The node to create or update the toolbarpaletteitem wrapper for. 1312 * @param {string} aPlace 1313 * The string to set as the "place" attribute on the node when it is 1314 * wrapped. This is expected to be one of the strings returned by 1315 * CustomizableUI.getPlaceForItem. 1316 * @param {boolean} aIsUpdate 1317 * True if it is expected that aNode is already wrapped and that we're 1318 * updating the wrapper rather than creating it. 1319 * @returns {DOMNode} 1320 * Returns the created or updated toolbarpaletteitem wrapper. 1321 */ 1322 createOrUpdateWrapper(aNode, aPlace, aIsUpdate) { 1323 let wrapper; 1324 if ( 1325 aIsUpdate && 1326 aNode.parentNode && 1327 aNode.parentNode.localName == "toolbarpaletteitem" 1328 ) { 1329 wrapper = aNode.parentNode; 1330 aPlace = wrapper.getAttribute("place"); 1331 } else { 1332 wrapper = this.#document.createXULElement("toolbarpaletteitem"); 1333 // "place" is used to show the label when it's sitting in the palette. 1334 wrapper.setAttribute("place", aPlace); 1335 } 1336 1337 // Ensure the wrapped item doesn't look like it's in any special state, and 1338 // can't be interactved with when in the customization palette. 1339 // Note that some buttons opt out of this with the 1340 // keepbroadcastattributeswhencustomizing attribute. 1341 if ( 1342 aNode.hasAttribute("command") && 1343 aNode.getAttribute(kKeepBroadcastAttributes) != "true" 1344 ) { 1345 wrapper.setAttribute("itemcommand", aNode.getAttribute("command")); 1346 aNode.removeAttribute("command"); 1347 } 1348 1349 if ( 1350 aNode.hasAttribute("observes") && 1351 aNode.getAttribute(kKeepBroadcastAttributes) != "true" 1352 ) { 1353 wrapper.setAttribute("itemobserves", aNode.getAttribute("observes")); 1354 aNode.removeAttribute("observes"); 1355 } 1356 1357 if (aNode.hasAttribute("checked")) { 1358 wrapper.setAttribute("itemchecked", "true"); 1359 aNode.removeAttribute("checked"); 1360 } 1361 1362 if (aNode.hasAttribute("id")) { 1363 wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id")); 1364 } 1365 1366 this.#updateWrapperLabel(aNode, aIsUpdate, wrapper); 1367 1368 if (aNode.hasAttribute("flex")) { 1369 wrapper.setAttribute("flex", aNode.getAttribute("flex")); 1370 } 1371 1372 let removable = 1373 aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode); 1374 wrapper.setAttribute("removable", removable); 1375 1376 // Allow touch events to initiate dragging in customize mode. 1377 // This is only supported on Windows for now. 1378 wrapper.setAttribute("touchdownstartsdrag", "true"); 1379 1380 let contextMenuAttrName = ""; 1381 if (aNode.getAttribute("context")) { 1382 contextMenuAttrName = "context"; 1383 } else if (aNode.getAttribute("contextmenu")) { 1384 contextMenuAttrName = "contextmenu"; 1385 } 1386 let currentContextMenu = aNode.getAttribute(contextMenuAttrName); 1387 let contextMenuForPlace = 1388 aPlace == "panel" ? kPanelItemContextMenu : kPaletteItemContextMenu; 1389 if (aPlace != "toolbar") { 1390 wrapper.setAttribute("context", contextMenuForPlace); 1391 } 1392 // Only keep track of the menu if it is non-default. 1393 if (currentContextMenu && currentContextMenu != contextMenuForPlace) { 1394 aNode.setAttribute("wrapped-context", currentContextMenu); 1395 aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName); 1396 aNode.removeAttribute(contextMenuAttrName); 1397 } else if (currentContextMenu == contextMenuForPlace) { 1398 aNode.removeAttribute(contextMenuAttrName); 1399 } 1400 1401 // Only add listeners for newly created wrappers: 1402 if (!aIsUpdate) { 1403 wrapper.addEventListener("mousedown", this); 1404 wrapper.addEventListener("mouseup", this); 1405 } 1406 1407 if (CustomizableUI.isSpecialWidget(aNode.id)) { 1408 wrapper.setAttribute( 1409 "title", 1410 lazy.gWidgetsBundle.GetStringFromName(aNode.nodeName + ".label") 1411 ); 1412 } 1413 1414 return wrapper; 1415 } 1416 1417 /** 1418 * Queues a function for the main thread to execute soon that will unwrap 1419 * the node wrapped with aWrapper, which should be a toolbarpaletteitem. 1420 * 1421 * @param {DOMNode} aWrapper 1422 * The toolbarpaletteitem wrapper around a node to unwrap. 1423 * @returns {Promise<DOMNode|null>} 1424 * Resolves with the unwrapped node, or null in the event that some 1425 * problem occurred while unwrapping (which will be logged). 1426 */ 1427 #deferredUnwrapToolbarItem(aWrapper) { 1428 return new Promise(resolve => { 1429 Services.tm.dispatchToMainThread(() => { 1430 let item = null; 1431 try { 1432 item = this.unwrapToolbarItem(aWrapper); 1433 } catch (ex) { 1434 console.error(ex); 1435 } 1436 resolve(item); 1437 }); 1438 }); 1439 } 1440 1441 /** 1442 * Unwraps a customizable item wrapped with a toolbarpaletteitem. If the 1443 * passed in aWrapper is not a toolbarpaletteitem, this just returns 1444 * aWrapper. 1445 * 1446 * @param {DOMNode} aWrapper 1447 * The toolbarpaletteitem wrapper around a node to be unwrapped. 1448 * @returns {DOMNode|null} 1449 * Returns the unwrapped customizable item, or null if something went wrong 1450 * while unwrapping. 1451 */ 1452 unwrapToolbarItem(aWrapper) { 1453 if (aWrapper.nodeName != "toolbarpaletteitem") { 1454 return aWrapper; 1455 } 1456 aWrapper.removeEventListener("mousedown", this); 1457 aWrapper.removeEventListener("mouseup", this); 1458 1459 let place = aWrapper.getAttribute("place"); 1460 1461 let toolbarItem = aWrapper.firstElementChild; 1462 if (!toolbarItem) { 1463 lazy.log.error( 1464 "no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id 1465 ); 1466 aWrapper.remove(); 1467 return null; 1468 } 1469 1470 if (aWrapper.hasAttribute("itemobserves")) { 1471 toolbarItem.setAttribute( 1472 "observes", 1473 aWrapper.getAttribute("itemobserves") 1474 ); 1475 } 1476 1477 if (aWrapper.hasAttribute("itemchecked")) { 1478 toolbarItem.checked = true; 1479 } 1480 1481 if (aWrapper.hasAttribute("itemcommand")) { 1482 let commandID = aWrapper.getAttribute("itemcommand"); 1483 toolbarItem.setAttribute("command", commandID); 1484 1485 // XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing 1486 let command = this.$(commandID); 1487 if (command?.hasAttribute("disabled")) { 1488 toolbarItem.setAttribute("disabled", command.getAttribute("disabled")); 1489 } 1490 } 1491 1492 let wrappedContext = toolbarItem.getAttribute("wrapped-context"); 1493 if (wrappedContext) { 1494 let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName"); 1495 toolbarItem.setAttribute(contextAttrName, wrappedContext); 1496 toolbarItem.removeAttribute("wrapped-contextAttrName"); 1497 toolbarItem.removeAttribute("wrapped-context"); 1498 } else if (place == "panel") { 1499 toolbarItem.setAttribute("context", kPanelItemContextMenu); 1500 } 1501 1502 if (aWrapper.parentNode) { 1503 aWrapper.parentNode.replaceChild(toolbarItem, aWrapper); 1504 } 1505 return toolbarItem; 1506 } 1507 1508 /** 1509 * For a given area, iterates the children within its customize target, 1510 * and asynchronously wraps each customizable child in a 1511 * toolbarpaletteitem. This also adds drag and drop handlers to the 1512 * customize target for the area. 1513 * 1514 * @param {string} aArea 1515 * The ID of the area to wrap the children for in the window that this 1516 * CustomizeMode was instantiated for. 1517 * @returns {Promise<DOMNode|null>} 1518 * Resolves with the customize target DOMNode for aArea for the window that 1519 * this CustomizeMode was instantiated for - or null if the customize target 1520 * cannot be found or is unknown to CustomizeMode. 1521 */ 1522 async #wrapAreaItems(aArea) { 1523 let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.#window); 1524 if (!target || this.areas.has(target)) { 1525 return null; 1526 } 1527 1528 this.#addCustomizeTargetDragAndDropHandlers(target); 1529 for (let child of target.children) { 1530 if ( 1531 this.#isCustomizableItem(child) && 1532 !this.isWrappedToolbarItem(child) 1533 ) { 1534 await this.#deferredWrapToolbarItem( 1535 child, 1536 CustomizableUI.getPlaceForItem(child) 1537 ).catch(lazy.log.error); 1538 } 1539 } 1540 this.areas.add(target); 1541 return target; 1542 } 1543 1544 /** 1545 * A synchronous version of #wrapAreaItems that will wrap all of the 1546 * customizable children of aArea's customize target in toolbarpaletteitems. 1547 * 1548 * @param {string} aArea 1549 * The ID of the area to wrap the children for in the window that this 1550 * CustomizeMode was instantiated for. 1551 * @returns {DOMNode|null} 1552 * Returns the customize target DOMNode for aArea for the window that 1553 * this CustomizeMode was instantiated for - or null if the customize target 1554 * cannot be found or is unknown to CustomizeMode. 1555 */ 1556 #wrapAreaItemsSync(aArea) { 1557 let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.#window); 1558 if (!target || this.areas.has(target)) { 1559 return null; 1560 } 1561 1562 this.#addCustomizeTargetDragAndDropHandlers(target); 1563 try { 1564 for (let child of target.children) { 1565 if ( 1566 this.#isCustomizableItem(child) && 1567 !this.isWrappedToolbarItem(child) 1568 ) { 1569 this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)); 1570 } 1571 } 1572 } catch (ex) { 1573 lazy.log.error(ex, ex.stack); 1574 } 1575 1576 this.areas.add(target); 1577 return target; 1578 } 1579 1580 /** 1581 * Iterates all areas and asynchronously wraps their customize target children 1582 * with toolbarpaletteitems in the window that this CustomizeMode was 1583 * constructed for. 1584 * 1585 * @returns {Promise<undefined>} 1586 * Resolves when wrapping has completed. 1587 */ 1588 async #wrapAllAreaItems() { 1589 for (let area of CustomizableUI.areas) { 1590 await this.#wrapAreaItems(area); 1591 } 1592 } 1593 1594 /** 1595 * Adds capturing drag and drop handlers for some customize target for some 1596 * customizable area. These handlers delegate event handling to the 1597 * handleEvent method. 1598 * 1599 * @param {DOMNode} aTarget 1600 * The customize target node to add drag and drop handlers for. 1601 */ 1602 #addCustomizeTargetDragAndDropHandlers(aTarget) { 1603 // Allow dropping on the padding of the arrow panel. 1604 if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) { 1605 aTarget = this.$("customization-panelHolder"); 1606 } 1607 aTarget.addEventListener("dragstart", this, true); 1608 aTarget.addEventListener("dragover", this, true); 1609 aTarget.addEventListener("dragleave", this, true); 1610 aTarget.addEventListener("drop", this, true); 1611 aTarget.addEventListener("dragend", this, true); 1612 } 1613 1614 /** 1615 * Iterates all of the customizable item children of a customize target within 1616 * a particular area, and attempts to wrap them in toolbarpaletteitems. 1617 * 1618 * @param {DOMNode} target 1619 * The customize target for some customizable area. 1620 */ 1621 #wrapItemsInArea(target) { 1622 for (let child of target.children) { 1623 if (this.#isCustomizableItem(child)) { 1624 this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)); 1625 } 1626 } 1627 } 1628 1629 /** 1630 * Removes capturing drag and drop handlers for some customize target for some 1631 * customizable area added via #addCustomizeTargetDragAndDropHandlers. 1632 * 1633 * @param {DOMNode} aTarget 1634 * The customize target node to remove drag and drop handlers for. 1635 */ 1636 #removeCustomizeTargetDragAndDropHandlers(aTarget) { 1637 // Remove handler from different target if it was added to 1638 // allow dropping on the padding of the arrow panel. 1639 if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) { 1640 aTarget = this.$("customization-panelHolder"); 1641 } 1642 aTarget.removeEventListener("dragstart", this, true); 1643 aTarget.removeEventListener("dragover", this, true); 1644 aTarget.removeEventListener("dragleave", this, true); 1645 aTarget.removeEventListener("drop", this, true); 1646 aTarget.removeEventListener("dragend", this, true); 1647 } 1648 1649 /** 1650 * Iterates all of the customizable item children of a customize target within 1651 * a particular area that have been wrapped in toolbarpaletteitems, and 1652 * attemps to unwrap them. 1653 * 1654 * @param {DOMNode} target 1655 * The customize target for some customizable area. 1656 */ 1657 #unwrapItemsInArea(target) { 1658 for (let toolbarItem of target.children) { 1659 if (this.isWrappedToolbarItem(toolbarItem)) { 1660 this.unwrapToolbarItem(toolbarItem); 1661 } 1662 } 1663 } 1664 1665 /** 1666 * Iterates all areas and asynchronously unwraps their customize target 1667 * children that had been previously wrapped in toolbarpaletteitems in the 1668 * window that this CustomizeMode was constructed for. This also removes 1669 * the drag and drop handlers for each area. 1670 * 1671 * @returns {Promise<undefined>} 1672 * Resolves when unwrapping has completed. 1673 */ 1674 #unwrapAllAreaItems() { 1675 return (async () => { 1676 for (let target of this.areas) { 1677 for (let toolbarItem of target.children) { 1678 if (this.isWrappedToolbarItem(toolbarItem)) { 1679 await this.#deferredUnwrapToolbarItem(toolbarItem); 1680 } 1681 } 1682 this.#removeCustomizeTargetDragAndDropHandlers(target); 1683 } 1684 this.areas.clear(); 1685 })().catch(lazy.log.error); 1686 } 1687 1688 /** 1689 * Resets the customization state of the browser across all windows to the 1690 * default settings. 1691 * 1692 * @returns {Promise<undefined>} 1693 * Resolves once resetting the customization state has completed. 1694 */ 1695 reset() { 1696 this.resetting = true; 1697 // Disable the reset button temporarily while resetting: 1698 let btn = this.$("customization-reset-button"); 1699 btn.disabled = true; 1700 return (async () => { 1701 this.#depopulatePalette(); 1702 await this.#unwrapAllAreaItems(); 1703 1704 CustomizableUI.reset(); 1705 1706 await this.#wrapAllAreaItems(); 1707 this.#populatePalette(); 1708 1709 this.#updateResetButton(); 1710 this.#updateUndoResetButton(); 1711 this.#updateEmptyPaletteNotice(); 1712 this.#moveDownloadsButtonToNavBar = false; 1713 this.resetting = false; 1714 if (!this._wantToBeInCustomizeMode) { 1715 this.exit(); 1716 } 1717 })().catch(lazy.log.error); 1718 } 1719 1720 /** 1721 * Reverts a reset operation back to the prior customization state. 1722 * 1723 * @see CustomizeMode.reset() 1724 * @returns {Promise<undefined>} 1725 */ 1726 undoReset() { 1727 this.resetting = true; 1728 1729 return (async () => { 1730 this.#depopulatePalette(); 1731 await this.#unwrapAllAreaItems(); 1732 1733 CustomizableUI.undoReset(); 1734 1735 await this.#wrapAllAreaItems(); 1736 this.#populatePalette(); 1737 1738 this.#updateResetButton(); 1739 this.#updateUndoResetButton(); 1740 this.#updateEmptyPaletteNotice(); 1741 this.#moveDownloadsButtonToNavBar = false; 1742 this.resetting = false; 1743 })().catch(lazy.log.error); 1744 } 1745 1746 /** 1747 * Handler for toolbarvisibilitychange events that fire within the window 1748 * that this CustomizeMode was constructed for. 1749 * 1750 * @param {CustomEvent} aEvent 1751 * The toolbarvisibilitychange event that was fired. 1752 */ 1753 #onToolbarVisibilityChange(aEvent) { 1754 let toolbar = aEvent.target; 1755 toolbar.toggleAttribute( 1756 "customizing", 1757 aEvent.detail.visible && toolbar.getAttribute("customizable") == "true" 1758 ); 1759 this.#onUIChange(); 1760 } 1761 1762 /** 1763 * The callback called by CustomizableUI when a widget moves. 1764 */ 1765 onWidgetMoved() { 1766 this.#onUIChange(); 1767 } 1768 1769 /** 1770 * The callback called by CustomizableUI when a widget is added to an area. 1771 */ 1772 onWidgetAdded() { 1773 this.#onUIChange(); 1774 } 1775 1776 /** 1777 * The callback called by CustomizableUI when a widget is removed from an 1778 * area. 1779 */ 1780 onWidgetRemoved() { 1781 this.#onUIChange(); 1782 } 1783 1784 /** 1785 * The callback called by CustomizableUI *before* a widget's DOM node is acted 1786 * upon by CustomizableUI (to add, move or remove it). 1787 * 1788 * @param {Element} aNodeToChange 1789 * The DOM node being acted upon. 1790 * @param {Element|null} aSecondaryNode 1791 * The DOM node (if any) before which a widget will be inserted. 1792 * @param {Element} aContainer 1793 * The *actual* DOM container for the widget (could be an overflow panel in 1794 * case of an overflowable toolbar). 1795 */ 1796 onWidgetBeforeDOMChange(aNodeToChange, aSecondaryNode, aContainer) { 1797 if (aContainer.ownerGlobal != this.#window || this.resetting) { 1798 return; 1799 } 1800 // If we get called for widgets that aren't in the window yet, they might not have 1801 // a parentNode at all. 1802 if (aNodeToChange.parentNode) { 1803 this.unwrapToolbarItem(aNodeToChange.parentNode); 1804 } 1805 if (aSecondaryNode) { 1806 this.unwrapToolbarItem(aSecondaryNode.parentNode); 1807 } 1808 } 1809 1810 /** 1811 * The callback called by CustomizableUI *after* a widget's DOM node is acted 1812 * upon by CustomizableUI (to add, move or remove it). 1813 * 1814 * @param {Element} aNodeToChange 1815 * The DOM node that was acted upon. 1816 * @param {Element|null} aSecondaryNode 1817 * The DOM node (if any) that the widget was inserted before. 1818 * @param {Element} aContainer 1819 * The *actual* DOM container for the widget (could be an overflow panel in 1820 * case of an overflowable toolbar). 1821 */ 1822 onWidgetAfterDOMChange(aNodeToChange, aSecondaryNode, aContainer) { 1823 if (aContainer.ownerGlobal != this.#window || this.resetting) { 1824 return; 1825 } 1826 // If the node is still attached to the container, wrap it again: 1827 if (aNodeToChange.parentNode) { 1828 let place = CustomizableUI.getPlaceForItem(aNodeToChange); 1829 this.wrapToolbarItem(aNodeToChange, place); 1830 if (aSecondaryNode) { 1831 this.wrapToolbarItem(aSecondaryNode, place); 1832 } 1833 } else { 1834 // If not, it got removed. 1835 1836 // If an API-based widget is removed while customizing, append it to the palette. 1837 // The #applyDrop code itself will take care of positioning it correctly, if 1838 // applicable. We need the code to be here so removing widgets using CustomizableUI's 1839 // API also does the right thing (and adds it to the palette) 1840 let widgetId = aNodeToChange.id; 1841 let widget = CustomizableUI.getWidget(widgetId); 1842 if (widget.provider == CustomizableUI.PROVIDER_API) { 1843 let paletteItem = this.#makePaletteItem(widget); 1844 this.visiblePalette.appendChild(paletteItem); 1845 } 1846 } 1847 } 1848 1849 /** 1850 * The callback called by CustomizableUI when a widget is destroyed. Only 1851 * fired for API-based widgets. 1852 * 1853 * @param {string} aWidgetId 1854 * The ID of the widget that was destroyed. 1855 */ 1856 onWidgetDestroyed(aWidgetId) { 1857 let wrapper = this.$("wrapper-" + aWidgetId); 1858 if (wrapper) { 1859 wrapper.remove(); 1860 } 1861 } 1862 1863 /** 1864 * The callback called by CustomizableUI after a widget with id aWidgetId has 1865 * been created, and has been added to either its default area or the area in 1866 * which it was placed previously. If the widget has no default area and/or it 1867 * has never been placed anywhere, aArea may be null. Only fired for API-based 1868 * widgets. 1869 * 1870 * @param {string} aWidgetId 1871 * The ID of the widget that was just created. 1872 * @param {string|null} aArea 1873 * The ID of the area that the widget was placed in, or null if it is 1874 * now in the customization palette. 1875 */ 1876 onWidgetAfterCreation(aWidgetId, aArea) { 1877 // If the node was added to an area, we would have gotten an onWidgetAdded notification, 1878 // plus associated DOM change notifications, so only do stuff for the palette: 1879 if (!aArea) { 1880 let widgetNode = this.$(aWidgetId); 1881 if (widgetNode) { 1882 this.wrapToolbarItem(widgetNode, "palette"); 1883 } else { 1884 let widget = CustomizableUI.getWidget(aWidgetId); 1885 this.visiblePalette.appendChild(this.#makePaletteItem(widget)); 1886 } 1887 } 1888 } 1889 1890 /** 1891 * Called by CustomizableUI after an area node is first built when it is 1892 * registered. 1893 * 1894 * @param {string} aArea 1895 * The ID for the area that was just registered. 1896 * @param {DOMNode} aContainer 1897 * The DOM node for the customizable area. 1898 */ 1899 onAreaNodeRegistered(aArea, aContainer) { 1900 if (aContainer.ownerDocument == this.#document) { 1901 this.#wrapItemsInArea(aContainer); 1902 this.#addCustomizeTargetDragAndDropHandlers(aContainer); 1903 this.areas.add(aContainer); 1904 } 1905 } 1906 1907 /** 1908 * Called by CustomizableUI after an area node is unregistered and no longer 1909 * available in this window. 1910 * 1911 * @param {string} aArea 1912 * The ID for the area that was just registered. 1913 * @param {DOMNode} aContainer 1914 * The DOM node for the customizable area. 1915 * @param {string} aReason 1916 * One of the CustomizableUI.REASON_* constants to describe the reason 1917 * that the area was unregistered for. 1918 */ 1919 onAreaNodeUnregistered(aArea, aContainer, aReason) { 1920 if ( 1921 aContainer.ownerDocument == this.#document && 1922 aReason == CustomizableUI.REASON_AREA_UNREGISTERED 1923 ) { 1924 this.#unwrapItemsInArea(aContainer); 1925 this.#removeCustomizeTargetDragAndDropHandlers(aContainer); 1926 this.areas.delete(aContainer); 1927 } 1928 } 1929 1930 /** 1931 * Opens about:addons in a new tab, showing the themes list. 1932 */ 1933 #openAddonsManagerThemes() { 1934 this.#window.BrowserAddonUI.openAddonsMgr("addons://list/theme"); 1935 } 1936 1937 /** 1938 * Temporarily updates the density of the browser UI to suit the passed in 1939 * mode. This is used to preview the density of the browser while the user 1940 * hovers the various density options, and is reset when the user stops 1941 * hovering the options. 1942 * 1943 * @param {number|null} mode 1944 * One of the density mode constants from gUIDensity - for example, 1945 * gUIDensity.MODE_TOUCH. 1946 */ 1947 #previewUIDensity(mode) { 1948 this.#window.gUIDensity.update(mode); 1949 this.#updateOverflowPanelArrowOffset(); 1950 } 1951 1952 /** 1953 * Resets the current UI density to the currently configured density. This 1954 * is used after temporarily previewing a density. 1955 */ 1956 #resetUIDensity() { 1957 this.#window.gUIDensity.update(); 1958 this.#updateOverflowPanelArrowOffset(); 1959 } 1960 1961 /** 1962 * Sets a UI density mode as the configured density. 1963 * 1964 * @param {number|null} mode 1965 * One of the density mode constants from gUIDensity - for example, 1966 * gUIDensity.MODE_TOUCH. 1967 */ 1968 setUIDensity(mode) { 1969 let win = this.#window; 1970 let gUIDensity = win.gUIDensity; 1971 let currentDensity = gUIDensity.getCurrentDensity(); 1972 let panel = win.document.getElementById("customization-uidensity-menu"); 1973 1974 Services.prefs.setIntPref(gUIDensity.uiDensityPref, mode); 1975 1976 // If the user is choosing a different UI density mode while 1977 // the mode is overriden to Touch, remove the override. 1978 if (currentDensity.overridden) { 1979 Services.prefs.setBoolPref(gUIDensity.autoTouchModePref, false); 1980 } 1981 1982 this.#onUIChange(); 1983 panel.hidePopup(); 1984 this.#updateOverflowPanelArrowOffset(); 1985 } 1986 1987 /** 1988 * Updates the state of the UI density menupopup to correctly reflect the 1989 * current configured density and to list the available alternative densities. 1990 */ 1991 #onUIDensityMenuShowing() { 1992 let win = this.#window; 1993 let doc = win.document; 1994 let gUIDensity = win.gUIDensity; 1995 let currentDensity = gUIDensity.getCurrentDensity(); 1996 1997 let normalItem = doc.getElementById( 1998 "customization-uidensity-menuitem-normal" 1999 ); 2000 normalItem.mode = gUIDensity.MODE_NORMAL; 2001 2002 let items = [normalItem]; 2003 2004 let compactItem = doc.getElementById( 2005 "customization-uidensity-menuitem-compact" 2006 ); 2007 compactItem.mode = gUIDensity.MODE_COMPACT; 2008 2009 if (Services.prefs.getBoolPref(kCompactModeShowPref)) { 2010 compactItem.hidden = false; 2011 items.push(compactItem); 2012 } else { 2013 compactItem.hidden = true; 2014 } 2015 2016 let touchItem = doc.getElementById( 2017 "customization-uidensity-menuitem-touch" 2018 ); 2019 // Touch mode can not be enabled in OSX right now. 2020 if (touchItem) { 2021 touchItem.mode = gUIDensity.MODE_TOUCH; 2022 items.push(touchItem); 2023 } 2024 2025 // Mark the active mode menuitem. 2026 for (let item of items) { 2027 if (item.mode == currentDensity.mode) { 2028 item.setAttribute("aria-checked", "true"); 2029 item.setAttribute("active", "true"); 2030 } else { 2031 item.removeAttribute("aria-checked"); 2032 item.removeAttribute("active"); 2033 } 2034 } 2035 2036 // Add menu items for automatically switching to Touch mode in Windows Tablet Mode. 2037 if (AppConstants.platform == "win") { 2038 let spacer = doc.getElementById("customization-uidensity-touch-spacer"); 2039 let checkbox = doc.getElementById( 2040 "customization-uidensity-autotouchmode-checkbox" 2041 ); 2042 spacer.removeAttribute("hidden"); 2043 checkbox.removeAttribute("hidden"); 2044 2045 // Show a hint that the UI density was overridden automatically. 2046 if (currentDensity.overridden) { 2047 let sb = Services.strings.createBundle( 2048 "chrome://browser/locale/uiDensity.properties" 2049 ); 2050 touchItem.setAttribute( 2051 "acceltext", 2052 sb.GetStringFromName("uiDensity.menuitem-touch.acceltext") 2053 ); 2054 } else { 2055 touchItem.removeAttribute("acceltext"); 2056 } 2057 2058 let autoTouchMode = Services.prefs.getBoolPref( 2059 win.gUIDensity.autoTouchModePref 2060 ); 2061 if (autoTouchMode) { 2062 checkbox.setAttribute("checked", "true"); 2063 } else { 2064 checkbox.removeAttribute("checked"); 2065 } 2066 } 2067 } 2068 2069 /** 2070 * Sets "automatic" touch mode to enabled or disabled. Automatic touch mode 2071 * means that touch density is used automatically if the device has switched 2072 * into a tablet mode. 2073 * 2074 * @param {boolean} checked 2075 * True if automatic touch mode should be enabled. 2076 */ 2077 #updateAutoTouchMode(checked) { 2078 Services.prefs.setBoolPref("browser.touchmode.auto", checked); 2079 // Re-render the menu items since the active mode might have 2080 // change because of this. 2081 this.#onUIDensityMenuShowing(); 2082 this.#onUIChange(); 2083 } 2084 2085 /** 2086 * Called anytime the UI configuration has changed in such a way that we need 2087 * to update the state and appearance of customize mode. 2088 */ 2089 #onUIChange() { 2090 if (!this.resetting) { 2091 this.#updateResetButton(); 2092 this.#updateUndoResetButton(); 2093 this.#updateEmptyPaletteNotice(); 2094 } 2095 CustomizableUI.dispatchToolboxEvent("customizationchange"); 2096 } 2097 2098 /** 2099 * Handles updating the state of customize mode if the palette has been 2100 * emptied such that only the special "spring" element remains (as this 2101 * cannot be removed from the palette). 2102 */ 2103 #updateEmptyPaletteNotice() { 2104 let paletteItems = 2105 this.visiblePalette.getElementsByTagName("toolbarpaletteitem"); 2106 let whimsyButton = this.$("whimsy-button"); 2107 2108 if ( 2109 paletteItems.length == 1 && 2110 paletteItems[0].id.includes("wrapper-customizableui-special-spring") 2111 ) { 2112 whimsyButton.hidden = false; 2113 } else { 2114 this.#togglePong(false); 2115 whimsyButton.hidden = true; 2116 } 2117 } 2118 2119 /** 2120 * Updates the enabled / disabled state of the Restore Defaults button based 2121 * on whether or not we're already in the default state. 2122 */ 2123 #updateResetButton() { 2124 let btn = this.$("customization-reset-button"); 2125 btn.disabled = CustomizableUI.inDefaultState; 2126 } 2127 2128 /** 2129 * Updates the hidden / visible state of the "undo reset" button based on 2130 * whether or not we've just performed a reset that can be undone. 2131 */ 2132 #updateUndoResetButton() { 2133 let undoResetButton = this.$("customization-undo-reset-button"); 2134 undoResetButton.hidden = !CustomizableUI.canUndoReset; 2135 } 2136 2137 /** 2138 * On macOS, if a touch bar is available on the device, updates the 2139 * hidden / visible state of the Customize Touch Bar button and spacer. 2140 */ 2141 #updateTouchBarButton() { 2142 if (AppConstants.platform != "macosx") { 2143 return; 2144 } 2145 let touchBarButton = this.$("customization-touchbar-button"); 2146 let touchBarSpacer = this.$("customization-touchbar-spacer"); 2147 2148 let isTouchBarInitialized = lazy.gTouchBarUpdater.isTouchBarInitialized(); 2149 touchBarButton.hidden = !isTouchBarInitialized; 2150 touchBarSpacer.hidden = !isTouchBarInitialized; 2151 } 2152 2153 /** 2154 * Updates the hidden / visible state of the UI density button. 2155 */ 2156 #updateDensityMenu() { 2157 // If we're entering Customize Mode, and we're using compact mode, 2158 // then show the button after that. 2159 let gUIDensity = this.#window.gUIDensity; 2160 if (gUIDensity.getCurrentDensity().mode == gUIDensity.MODE_COMPACT) { 2161 Services.prefs.setBoolPref(kCompactModeShowPref, true); 2162 } 2163 2164 let button = this.#document.getElementById( 2165 "customization-uidensity-button" 2166 ); 2167 button.hidden = 2168 !Services.prefs.getBoolPref(kCompactModeShowPref) && 2169 !button.querySelector("#customization-uidensity-menuitem-touch"); 2170 } 2171 2172 /** 2173 * Generic event handler used throughout most of the Customize Mode UI. This 2174 * is mainly used to dispatch events to more specific handlers based on the 2175 * event type. 2176 * 2177 * @param {Event} aEvent 2178 * The event being handled. 2179 */ 2180 handleEvent(aEvent) { 2181 switch (aEvent.type) { 2182 case "toolbarvisibilitychange": 2183 this.#onToolbarVisibilityChange(aEvent); 2184 break; 2185 case "dragstart": 2186 this.#onDragStart(aEvent); 2187 break; 2188 case "dragover": 2189 this.#onDragOver(aEvent); 2190 break; 2191 case "drop": 2192 this.#onDragDrop(aEvent); 2193 break; 2194 case "dragleave": 2195 this.#onDragLeave(aEvent); 2196 break; 2197 case "dragend": 2198 this.#onDragEnd(aEvent); 2199 break; 2200 case "mousedown": 2201 this.#onMouseDown(aEvent); 2202 break; 2203 case "mouseup": 2204 this.#onMouseUp(aEvent); 2205 break; 2206 case "keypress": 2207 if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { 2208 this.exit(); 2209 } 2210 break; 2211 case "unload": 2212 this.#uninit(); 2213 break; 2214 } 2215 } 2216 2217 /** 2218 * Sets up the dragover/drop handlers on the visible palette. We handle 2219 * dragover/drop on the outer palette separately to avoid overlap with other 2220 * drag/drop handlers. 2221 */ 2222 #setupPaletteDragging() { 2223 this.#addCustomizeTargetDragAndDropHandlers(this.visiblePalette); 2224 2225 this.paletteDragHandler = aEvent => { 2226 let originalTarget = aEvent.originalTarget; 2227 if ( 2228 this.#isUnwantedDragDrop(aEvent) || 2229 this.visiblePalette.contains(originalTarget) || 2230 this.$("customization-panelHolder").contains(originalTarget) 2231 ) { 2232 return; 2233 } 2234 // We have a dragover/drop on the palette. 2235 if (aEvent.type == "dragover") { 2236 this.#onDragOver(aEvent, this.visiblePalette); 2237 } else { 2238 this.#onDragDrop(aEvent, this.visiblePalette); 2239 } 2240 }; 2241 let contentContainer = this.$("customization-content-container"); 2242 contentContainer.addEventListener( 2243 "dragover", 2244 this.paletteDragHandler, 2245 true 2246 ); 2247 contentContainer.addEventListener("drop", this.paletteDragHandler, true); 2248 } 2249 2250 /** 2251 * Tears down the dragover/drop handlers on the visible palette added by 2252 * #setupPaletteDragging. 2253 */ 2254 #teardownPaletteDragging() { 2255 lazy.DragPositionManager.stop(); 2256 this.#removeCustomizeTargetDragAndDropHandlers(this.visiblePalette); 2257 2258 let contentContainer = this.$("customization-content-container"); 2259 contentContainer.removeEventListener( 2260 "dragover", 2261 this.paletteDragHandler, 2262 true 2263 ); 2264 contentContainer.removeEventListener("drop", this.paletteDragHandler, true); 2265 delete this.paletteDragHandler; 2266 } 2267 2268 /** 2269 * Implements nsIObserver. This is mainly to observe for preference changes. 2270 * 2271 * @param {nsISupports} aSubject 2272 * The nsISupports subject for the notification topic that is being 2273 * observed. 2274 * @param {string} aTopic 2275 * The notification topic that is being observed. 2276 */ 2277 observe(aSubject, aTopic) { 2278 switch (aTopic) { 2279 case "nsPref:changed": 2280 this.#updateResetButton(); 2281 this.#updateUndoResetButton(); 2282 if (this.#canDrawInTitlebar()) { 2283 this.#updateTitlebarCheckbox(); 2284 } 2285 break; 2286 } 2287 } 2288 2289 /** 2290 * Returns true if the current platform and configuration allows us to draw in 2291 * the window titlebar. 2292 * 2293 * @returns {boolean} 2294 */ 2295 #canDrawInTitlebar() { 2296 return this.#window.CustomTitlebar.systemSupported; 2297 } 2298 2299 /** 2300 * De-lazifies the customization panel and the menupopup / panel template 2301 * holding various DOM nodes for customize mode. These things are lazily 2302 * added to the DOM to avoid polluting the browser window DOM with things 2303 * that only Customize Mode cares about. 2304 */ 2305 #ensureCustomizationPanels() { 2306 let template = this.$("customizationPanel"); 2307 template.replaceWith(template.content); 2308 2309 let wrapper = this.$("customModeWrapper"); 2310 wrapper.replaceWith(wrapper.content); 2311 } 2312 2313 /** 2314 * Adds event listeners for all of the interactive elements in the window that 2315 * this Customize Mode instance was constructed with. 2316 */ 2317 #attachEventListeners() { 2318 let container = this.$("customization-container"); 2319 2320 container.addEventListener("command", event => { 2321 switch (event.target.id) { 2322 case "customization-titlebar-visibility-checkbox": 2323 // NB: because command fires after click, by the time we've fired, the checkbox binding 2324 // will already have switched the button's state, so this is correct: 2325 this.#toggleTitlebar(event.target.checked); 2326 break; 2327 case "customization-uidensity-menuitem-compact": 2328 case "customization-uidensity-menuitem-normal": 2329 case "customization-uidensity-menuitem-touch": 2330 this.setUIDensity(event.target.mode); 2331 break; 2332 case "customization-uidensity-autotouchmode-checkbox": 2333 this.#updateAutoTouchMode(event.target.checked); 2334 break; 2335 case "whimsy-button": 2336 this.#togglePong(event.target.checked); 2337 break; 2338 case "customization-touchbar-button": 2339 this.#customizeTouchBar(); 2340 break; 2341 case "customization-undo-reset-button": 2342 this.undoReset(); 2343 break; 2344 case "customization-reset-button": 2345 this.reset(); 2346 break; 2347 case "customization-done-button": 2348 this.exit(); 2349 break; 2350 } 2351 }); 2352 2353 container.addEventListener("popupshowing", event => { 2354 switch (event.target.id) { 2355 case "customization-toolbar-menu": 2356 this.#window.ToolbarContextMenu.onViewToolbarsPopupShowing(event); 2357 break; 2358 case "customization-uidensity-menu": 2359 this.#onUIDensityMenuShowing(); 2360 break; 2361 } 2362 }); 2363 2364 let updateDensity = event => { 2365 switch (event.target.id) { 2366 case "customization-uidensity-menuitem-compact": 2367 case "customization-uidensity-menuitem-normal": 2368 case "customization-uidensity-menuitem-touch": 2369 this.#previewUIDensity(event.target.mode); 2370 } 2371 }; 2372 let densityMenu = this.#document.getElementById( 2373 "customization-uidensity-menu" 2374 ); 2375 densityMenu.addEventListener("focus", updateDensity); 2376 densityMenu.addEventListener("mouseover", updateDensity); 2377 2378 let resetDensity = event => { 2379 switch (event.target.id) { 2380 case "customization-uidensity-menuitem-compact": 2381 case "customization-uidensity-menuitem-normal": 2382 case "customization-uidensity-menuitem-touch": 2383 this.#resetUIDensity(); 2384 } 2385 }; 2386 densityMenu.addEventListener("blur", resetDensity); 2387 densityMenu.addEventListener("mouseout", resetDensity); 2388 2389 this.$("customization-lwtheme-link").addEventListener("click", () => { 2390 this.#openAddonsManagerThemes(); 2391 }); 2392 2393 this.$(kPaletteItemContextMenu).addEventListener("popupshowing", event => { 2394 this.#onPaletteContextMenuShowing(event); 2395 }); 2396 2397 this.$(kPaletteItemContextMenu).addEventListener("command", event => { 2398 switch (event.target.id) { 2399 case "customizationPaletteItemContextMenuAddToToolbar": 2400 this.addToToolbar( 2401 event.target.parentNode.triggerNode, 2402 "palette-context" 2403 ); 2404 break; 2405 case "customizationPaletteItemContextMenuAddToPanel": 2406 this.addToPanel( 2407 event.target.parentNode.triggerNode, 2408 "palette-context" 2409 ); 2410 break; 2411 } 2412 }); 2413 2414 let autohidePanel = this.$(kDownloadAutohidePanelId); 2415 autohidePanel.addEventListener("popupshown", event => { 2416 this._downloadPanelAutoHideTimeout = this.#window.setTimeout( 2417 () => event.target.hidePopup(), 2418 4000 2419 ); 2420 }); 2421 autohidePanel.addEventListener("mouseover", () => { 2422 this.#window.clearTimeout(this._downloadPanelAutoHideTimeout); 2423 }); 2424 autohidePanel.addEventListener("mouseout", event => { 2425 this._downloadPanelAutoHideTimeout = this.#window.setTimeout( 2426 () => event.target.hidePopup(), 2427 2000 2428 ); 2429 }); 2430 autohidePanel.addEventListener("popuphidden", () => { 2431 this.#window.clearTimeout(this._downloadPanelAutoHideTimeout); 2432 }); 2433 2434 this.$(kDownloadAutohideCheckboxId).addEventListener("command", event => { 2435 this.#onDownloadsAutoHideChange(event); 2436 }); 2437 } 2438 2439 /** 2440 * Updates the checked / unchecked state of the Titlebar checkbox, to 2441 * reflect whether or not we're currently configured to show the native 2442 * titlebar or not. 2443 */ 2444 #updateTitlebarCheckbox() { 2445 let drawInTitlebar = Services.appinfo.drawInTitlebar; 2446 let checkbox = this.$("customization-titlebar-visibility-checkbox"); 2447 // Drawing in the titlebar means 'hiding' the titlebar. 2448 // We use the attribute rather than a property because if we're not in 2449 // customize mode the button is hidden and properties don't work. 2450 if (drawInTitlebar) { 2451 checkbox.removeAttribute("checked"); 2452 } else { 2453 checkbox.setAttribute("checked", "true"); 2454 } 2455 } 2456 2457 /** 2458 * Configures whether or not we should show the native titlebar. 2459 * 2460 * @param {boolean} aShouldShowTitlebar 2461 * True if we should show the native titlebar. False to draw the browser 2462 * UI into the titlebar instead. 2463 */ 2464 #toggleTitlebar(aShouldShowTitlebar) { 2465 // Drawing in the titlebar means not showing the titlebar, hence the negation: 2466 Services.prefs.setIntPref(kDrawInTitlebarPref, !aShouldShowTitlebar); 2467 } 2468 2469 /** 2470 * A convenient shortcut to calling getBoundsWithoutFlushing on this windows' 2471 * nsIDOMWindowUtils. 2472 * 2473 * @param {DOMNode} element 2474 * An element for which to try to get the bounding client rect, but without 2475 * flushing styles or layout. 2476 * @returns {DOMRect} 2477 */ 2478 #getBoundsWithoutFlushing(element) { 2479 return this.#window.windowUtils.getBoundsWithoutFlushing(element); 2480 } 2481 2482 /** 2483 * Handles the dragstart event on any customizable item in one of the 2484 * customizable areas. 2485 * 2486 * @param {DragEvent} aEvent 2487 * The dragstart event being handled. 2488 */ 2489 #onDragStart(aEvent) { 2490 __dumpDragData(aEvent); 2491 let item = aEvent.target; 2492 while (item && item.localName != "toolbarpaletteitem") { 2493 if ( 2494 item.localName == "toolbar" || 2495 item.id == kPaletteId || 2496 item.id == "customization-panelHolder" 2497 ) { 2498 return; 2499 } 2500 item = item.parentNode; 2501 } 2502 2503 let draggedItem = item.firstElementChild; 2504 let placeForItem = CustomizableUI.getPlaceForItem(item); 2505 2506 let dt = aEvent.dataTransfer; 2507 let documentId = aEvent.target.ownerDocument.documentElement.id; 2508 2509 dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0); 2510 dt.effectAllowed = "move"; 2511 2512 let itemRect = this.#getBoundsWithoutFlushing(draggedItem); 2513 let itemCenter = { 2514 x: itemRect.left + itemRect.width / 2, 2515 y: itemRect.top + itemRect.height / 2, 2516 }; 2517 this._dragOffset = { 2518 x: aEvent.clientX - itemCenter.x, 2519 y: aEvent.clientY - itemCenter.y, 2520 }; 2521 2522 let toolbarParent = draggedItem.closest("toolbar"); 2523 if (toolbarParent) { 2524 let toolbarRect = this.#getBoundsWithoutFlushing(toolbarParent); 2525 toolbarParent.style.minHeight = toolbarRect.height + "px"; 2526 } 2527 2528 gDraggingInToolbars = new Set(); 2529 2530 // Hack needed so that the dragimage will still show the 2531 // item as it appeared before it was hidden. 2532 this._initializeDragAfterMove = () => { 2533 // For automated tests, we sometimes start exiting customization mode 2534 // before this fires, which leaves us with placeholders inserted after 2535 // we've exited. So we need to check that we are indeed customizing. 2536 if (this.#customizing && !this.#transitioning) { 2537 item.hidden = true; 2538 lazy.DragPositionManager.start(this.#window); 2539 let canUsePrevSibling = 2540 placeForItem == "toolbar" || placeForItem == "panel"; 2541 if (item.nextElementSibling) { 2542 this.#setDragActive( 2543 item.nextElementSibling, 2544 "before", 2545 draggedItem.id, 2546 placeForItem 2547 ); 2548 this.#dragOverItem = item.nextElementSibling; 2549 } else if (canUsePrevSibling && item.previousElementSibling) { 2550 this.#setDragActive( 2551 item.previousElementSibling, 2552 "after", 2553 draggedItem.id, 2554 placeForItem 2555 ); 2556 this.#dragOverItem = item.previousElementSibling; 2557 } 2558 let currentArea = this.#getCustomizableParent(item); 2559 currentArea.setAttribute("draggingover", "true"); 2560 } 2561 this._initializeDragAfterMove = null; 2562 this.#window.clearTimeout(this._dragInitializeTimeout); 2563 }; 2564 this._dragInitializeTimeout = this.#window.setTimeout( 2565 this._initializeDragAfterMove, 2566 0 2567 ); 2568 } 2569 2570 /** 2571 * Handles the dragover event for any customizable area. 2572 * 2573 * @param {DragEvent} aEvent 2574 * The dragover event being handled. 2575 * @param {DOMNode} [aOverrideTarget=undefined] 2576 * Optional argument that allows callers to override the dragover target to 2577 * be something other than the dragover event current target. 2578 */ 2579 #onDragOver(aEvent, aOverrideTarget) { 2580 if (this.#isUnwantedDragDrop(aEvent)) { 2581 return; 2582 } 2583 if (this._initializeDragAfterMove) { 2584 this._initializeDragAfterMove(); 2585 } 2586 2587 __dumpDragData(aEvent); 2588 2589 let document = aEvent.target.ownerDocument; 2590 let documentId = document.documentElement.id; 2591 if (!aEvent.dataTransfer.mozTypesAt(0).length) { 2592 return; 2593 } 2594 2595 let draggedItemId = aEvent.dataTransfer.mozGetDataAt( 2596 kDragDataTypePrefix + documentId, 2597 0 2598 ); 2599 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); 2600 let targetArea = this.#getCustomizableParent( 2601 aOverrideTarget || aEvent.currentTarget 2602 ); 2603 let originArea = this.#getCustomizableParent(draggedWrapper); 2604 2605 // Do nothing if the target or origin are not customizable. 2606 if (!targetArea || !originArea) { 2607 return; 2608 } 2609 2610 // Do nothing if the widget is not allowed to be removed. 2611 if ( 2612 targetArea.id == kPaletteId && 2613 !CustomizableUI.isWidgetRemovable(draggedItemId) 2614 ) { 2615 return; 2616 } 2617 2618 // Do nothing if the widget is not allowed to move to the target area. 2619 if (!CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) { 2620 return; 2621 } 2622 2623 let targetAreaType = CustomizableUI.getPlaceForItem(targetArea); 2624 let targetNode = this.#getDragOverNode( 2625 aEvent, 2626 targetArea, 2627 targetAreaType, 2628 draggedItemId 2629 ); 2630 2631 // We need to determine the place that the widget is being dropped in 2632 // the target. 2633 let dragOverItem, dragValue; 2634 if (targetNode == CustomizableUI.getCustomizationTarget(targetArea)) { 2635 // We'll assume if the user is dragging directly over the target, that 2636 // they're attempting to append a child to that target. 2637 dragOverItem = 2638 (targetAreaType == "toolbar" 2639 ? this.#findVisiblePreviousSiblingNode(targetNode.lastElementChild) 2640 : targetNode.lastElementChild) || targetNode; 2641 dragValue = "after"; 2642 } else { 2643 let targetParent = targetNode.parentNode; 2644 let position = Array.prototype.indexOf.call( 2645 targetParent.children, 2646 targetNode 2647 ); 2648 if (position == -1) { 2649 dragOverItem = 2650 targetAreaType == "toolbar" 2651 ? this.#findVisiblePreviousSiblingNode(targetNode.lastElementChild) 2652 : targetNode.lastElementChild; 2653 dragValue = "after"; 2654 } else { 2655 dragOverItem = targetParent.children[position]; 2656 if (targetAreaType == "toolbar") { 2657 // Check if the aDraggedItem is hovered past the first half of dragOverItem 2658 let itemRect = this.#getBoundsWithoutFlushing(dragOverItem); 2659 let dropTargetCenter = itemRect.left + itemRect.width / 2; 2660 let existingDir = dragOverItem.getAttribute("dragover"); 2661 let dirFactor = this.#window.RTL_UI ? -1 : 1; 2662 if (existingDir == "before") { 2663 dropTargetCenter += 2664 ((parseInt(dragOverItem.style.borderInlineStartWidth) || 0) / 2) * 2665 dirFactor; 2666 } else { 2667 dropTargetCenter -= 2668 ((parseInt(dragOverItem.style.borderInlineEndWidth) || 0) / 2) * 2669 dirFactor; 2670 } 2671 let before = this.#window.RTL_UI 2672 ? aEvent.clientX > dropTargetCenter 2673 : aEvent.clientX < dropTargetCenter; 2674 dragValue = before ? "before" : "after"; 2675 } else if (targetAreaType == "panel") { 2676 let itemRect = this.#getBoundsWithoutFlushing(dragOverItem); 2677 let dropTargetCenter = itemRect.top + itemRect.height / 2; 2678 let existingDir = dragOverItem.getAttribute("dragover"); 2679 if (existingDir == "before") { 2680 dropTargetCenter += 2681 (parseInt(dragOverItem.style.borderBlockStartWidth) || 0) / 2; 2682 } else { 2683 dropTargetCenter -= 2684 (parseInt(dragOverItem.style.borderBlockEndWidth) || 0) / 2; 2685 } 2686 dragValue = aEvent.clientY < dropTargetCenter ? "before" : "after"; 2687 } else { 2688 dragValue = "before"; 2689 } 2690 } 2691 } 2692 2693 if (this.#dragOverItem && dragOverItem != this.#dragOverItem) { 2694 this.#cancelDragActive(this.#dragOverItem, dragOverItem); 2695 } 2696 2697 if ( 2698 dragOverItem != this.#dragOverItem || 2699 dragValue != dragOverItem.getAttribute("dragover") 2700 ) { 2701 if (dragOverItem != CustomizableUI.getCustomizationTarget(targetArea)) { 2702 this.#setDragActive( 2703 dragOverItem, 2704 dragValue, 2705 draggedItemId, 2706 targetAreaType 2707 ); 2708 } 2709 this.#dragOverItem = dragOverItem; 2710 targetArea.setAttribute("draggingover", "true"); 2711 } 2712 2713 aEvent.preventDefault(); 2714 aEvent.stopPropagation(); 2715 } 2716 2717 /** 2718 * Handles the drop event on any customizable area. 2719 * 2720 * @param {DragEvent} aEvent 2721 * The drop event being handled. 2722 * @param {DOMNode} [aOverrideTarget=undefined] 2723 * Optional argument that allows callers to override the drop target to 2724 * be something other than the drop event current target. 2725 */ 2726 #onDragDrop(aEvent, aOverrideTarget) { 2727 if (this.#isUnwantedDragDrop(aEvent)) { 2728 return; 2729 } 2730 2731 __dumpDragData(aEvent); 2732 this._initializeDragAfterMove = null; 2733 this.#window.clearTimeout(this._dragInitializeTimeout); 2734 2735 let targetArea = this.#getCustomizableParent( 2736 aOverrideTarget || aEvent.currentTarget 2737 ); 2738 let document = aEvent.target.ownerDocument; 2739 let documentId = document.documentElement.id; 2740 let draggedItemId = aEvent.dataTransfer.mozGetDataAt( 2741 kDragDataTypePrefix + documentId, 2742 0 2743 ); 2744 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); 2745 let originArea = this.#getCustomizableParent(draggedWrapper); 2746 if (this.#dragSizeMap) { 2747 this.#dragSizeMap = new WeakMap(); 2748 } 2749 // Do nothing if the target area or origin area are not customizable. 2750 if (!targetArea || !originArea) { 2751 return; 2752 } 2753 let targetNode = this.#dragOverItem; 2754 let dropDir = targetNode.getAttribute("dragover"); 2755 // Need to insert *after* this node if we promised the user that: 2756 if (targetNode != targetArea && dropDir == "after") { 2757 if (targetNode.nextElementSibling) { 2758 targetNode = targetNode.nextElementSibling; 2759 } else { 2760 targetNode = targetArea; 2761 } 2762 } 2763 if (targetNode.tagName == "toolbarpaletteitem") { 2764 targetNode = targetNode.firstElementChild; 2765 } 2766 2767 this.#cancelDragActive(this.#dragOverItem, null, true); 2768 2769 try { 2770 this.#applyDrop( 2771 aEvent, 2772 targetArea, 2773 originArea, 2774 draggedItemId, 2775 targetNode 2776 ); 2777 } catch (ex) { 2778 lazy.log.error(ex, ex.stack); 2779 } 2780 2781 // If the user explicitly moves this item, turn off autohide. 2782 if (draggedItemId == "downloads-button") { 2783 Services.prefs.setBoolPref(kDownloadAutoHidePref, false); 2784 this.#showDownloadsAutoHidePanel(); 2785 } 2786 } 2787 2788 /** 2789 * A helper method for #onDragDrop that applies the changes to the browser 2790 * UI from the drop event on either a customizable item, or an area. 2791 * 2792 * @param {DragEvent} aEvent 2793 * The drop event being handled. 2794 * @param {DOMNode} aTargetArea 2795 * The target area node being dropped on. 2796 * @param {DOMNode} aOriginArea 2797 * The origin area node that the dropped item originally came from. 2798 * @param {string} aDroppedItemId 2799 * The ID value of the customizable item being dropped. 2800 * @param {DOMNode} aTargetNode 2801 * The customizable item (or area) node being dropped on. 2802 */ 2803 #applyDrop(aEvent, aTargetArea, aOriginArea, aDroppedItemId, aTargetNode) { 2804 let document = aEvent.target.ownerDocument; 2805 let draggedItem = document.getElementById(aDroppedItemId); 2806 draggedItem.hidden = false; 2807 draggedItem.removeAttribute("mousedown"); 2808 2809 let toolbarParent = draggedItem.closest("toolbar"); 2810 if (toolbarParent) { 2811 toolbarParent.style.removeProperty("min-height"); 2812 } 2813 2814 // Do nothing if the target was dropped onto itself (ie, no change in area 2815 // or position). 2816 if (draggedItem == aTargetNode) { 2817 return; 2818 } 2819 2820 if (!CustomizableUI.canWidgetMoveToArea(aDroppedItemId, aTargetArea.id)) { 2821 return; 2822 } 2823 2824 // Is the target area the customization palette? 2825 if (aTargetArea.id == kPaletteId) { 2826 // Did we drag from outside the palette? 2827 if (aOriginArea.id !== kPaletteId) { 2828 if (!CustomizableUI.isWidgetRemovable(aDroppedItemId)) { 2829 return; 2830 } 2831 2832 CustomizableUI.removeWidgetFromArea(aDroppedItemId, "drag"); 2833 lazy.BrowserUsageTelemetry.recordWidgetChange( 2834 aDroppedItemId, 2835 null, 2836 "drag" 2837 ); 2838 // Special widgets are removed outright, we can return here: 2839 if (CustomizableUI.isSpecialWidget(aDroppedItemId)) { 2840 return; 2841 } 2842 } 2843 draggedItem = draggedItem.parentNode; 2844 2845 // If the target node is the palette itself, just append 2846 if (aTargetNode == this.visiblePalette) { 2847 this.visiblePalette.appendChild(draggedItem); 2848 } else { 2849 // The items in the palette are wrapped, so we need the target node's parent here: 2850 this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode); 2851 } 2852 this.#onDragEnd(aEvent); 2853 return; 2854 } 2855 2856 // Skipintoolbarset items won't really be moved: 2857 let areaCustomizationTarget = 2858 CustomizableUI.getCustomizationTarget(aTargetArea); 2859 if (draggedItem.getAttribute("skipintoolbarset") == "true") { 2860 // These items should never leave their area: 2861 if (aTargetArea != aOriginArea) { 2862 return; 2863 } 2864 let place = draggedItem.parentNode.getAttribute("place"); 2865 this.unwrapToolbarItem(draggedItem.parentNode); 2866 if (aTargetNode == areaCustomizationTarget) { 2867 areaCustomizationTarget.appendChild(draggedItem); 2868 } else { 2869 this.unwrapToolbarItem(aTargetNode.parentNode); 2870 areaCustomizationTarget.insertBefore(draggedItem, aTargetNode); 2871 this.wrapToolbarItem(aTargetNode, place); 2872 } 2873 this.wrapToolbarItem(draggedItem, place); 2874 return; 2875 } 2876 2877 // Force creating a new spacer/spring/separator if dragging from the palette 2878 if ( 2879 CustomizableUI.isSpecialWidget(aDroppedItemId) && 2880 aOriginArea.id == kPaletteId 2881 ) { 2882 aDroppedItemId = aDroppedItemId.match( 2883 /^customizableui-special-(spring|spacer|separator)/ 2884 )[1]; 2885 } 2886 2887 // Is the target the customization area itself? If so, we just add the 2888 // widget to the end of the area. 2889 if (aTargetNode == areaCustomizationTarget) { 2890 CustomizableUI.addWidgetToArea(aDroppedItemId, aTargetArea.id); 2891 lazy.BrowserUsageTelemetry.recordWidgetChange( 2892 aDroppedItemId, 2893 aTargetArea.id, 2894 "drag" 2895 ); 2896 this.#onDragEnd(aEvent); 2897 return; 2898 } 2899 2900 // We need to determine the place that the widget is being dropped in 2901 // the target. 2902 let placement; 2903 let itemForPlacement = aTargetNode; 2904 // Skip the skipintoolbarset items when determining the place of the item: 2905 while ( 2906 itemForPlacement && 2907 itemForPlacement.getAttribute("skipintoolbarset") == "true" && 2908 itemForPlacement.parentNode && 2909 itemForPlacement.parentNode.nodeName == "toolbarpaletteitem" 2910 ) { 2911 itemForPlacement = itemForPlacement.parentNode.nextElementSibling; 2912 if ( 2913 itemForPlacement && 2914 itemForPlacement.nodeName == "toolbarpaletteitem" 2915 ) { 2916 itemForPlacement = itemForPlacement.firstElementChild; 2917 } 2918 } 2919 if (itemForPlacement) { 2920 let targetNodeId = 2921 itemForPlacement.nodeName == "toolbarpaletteitem" 2922 ? itemForPlacement.firstElementChild && 2923 itemForPlacement.firstElementChild.id 2924 : itemForPlacement.id; 2925 placement = CustomizableUI.getPlacementOfWidget(targetNodeId); 2926 } 2927 if (!placement) { 2928 lazy.log.debug( 2929 "Could not get a position for " + 2930 aTargetNode.nodeName + 2931 "#" + 2932 aTargetNode.id + 2933 "." + 2934 aTargetNode.className 2935 ); 2936 } 2937 let position = placement ? placement.position : null; 2938 2939 // Is the target area the same as the origin? Since we've already handled 2940 // the possibility that the target is the customization palette, we know 2941 // that the widget is moving within a customizable area. 2942 if (aTargetArea == aOriginArea) { 2943 CustomizableUI.moveWidgetWithinArea(aDroppedItemId, position); 2944 lazy.BrowserUsageTelemetry.recordWidgetChange( 2945 aDroppedItemId, 2946 aTargetArea.id, 2947 "drag" 2948 ); 2949 } else { 2950 CustomizableUI.addWidgetToArea(aDroppedItemId, aTargetArea.id, position); 2951 lazy.BrowserUsageTelemetry.recordWidgetChange( 2952 aDroppedItemId, 2953 aTargetArea.id, 2954 "drag" 2955 ); 2956 } 2957 2958 this.#onDragEnd(aEvent); 2959 2960 // If we dropped onto a skipintoolbarset item, manually correct the drop location: 2961 if (aTargetNode != itemForPlacement) { 2962 let draggedWrapper = draggedItem.parentNode; 2963 let container = draggedWrapper.parentNode; 2964 container.insertBefore(draggedWrapper, aTargetNode.parentNode); 2965 } 2966 } 2967 2968 /** 2969 * Handles the dragleave event on any customizable item or area. 2970 * 2971 * @param {DragEvent} aEvent 2972 * The dragleave event being handled. 2973 */ 2974 #onDragLeave(aEvent) { 2975 if (this.#isUnwantedDragDrop(aEvent)) { 2976 return; 2977 } 2978 2979 __dumpDragData(aEvent); 2980 2981 // When leaving customization areas, cancel the drag on the last dragover item 2982 // We've attached the listener to areas, so aEvent.currentTarget will be the area. 2983 // We don't care about dragleave events fired on descendants of the area, 2984 // so we check that the event's target is the same as the area to which the listener 2985 // was attached. 2986 if (this.#dragOverItem && aEvent.target == aEvent.currentTarget) { 2987 this.#cancelDragActive(this.#dragOverItem); 2988 this.#dragOverItem = null; 2989 } 2990 } 2991 2992 /** 2993 * Handles the dragleave event on any customizable item being dragged. 2994 * 2995 * @param {DragEvent} aEvent 2996 * The dragleave event being handled. 2997 */ 2998 #onDragEnd(aEvent) { 2999 // To workaround bug 460801 we manually forward the drop event here when 3000 // dragend wouldn't be fired. 3001 // 3002 // Note that that means that this function may be called multiple times by a 3003 // single drag operation. 3004 if (this.#isUnwantedDragDrop(aEvent)) { 3005 return; 3006 } 3007 this._initializeDragAfterMove = null; 3008 this.#window.clearTimeout(this._dragInitializeTimeout); 3009 __dumpDragData(aEvent, "#onDragEnd"); 3010 3011 let document = aEvent.target.ownerDocument; 3012 document.documentElement.removeAttribute("customizing-movingItem"); 3013 3014 let documentId = document.documentElement.id; 3015 if (!aEvent.dataTransfer.mozTypesAt(0)) { 3016 return; 3017 } 3018 3019 let draggedItemId = aEvent.dataTransfer.mozGetDataAt( 3020 kDragDataTypePrefix + documentId, 3021 0 3022 ); 3023 3024 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); 3025 3026 // DraggedWrapper might no longer available if a widget node is 3027 // destroyed after starting (but before stopping) a drag. 3028 if (draggedWrapper) { 3029 draggedWrapper.hidden = false; 3030 draggedWrapper.removeAttribute("mousedown"); 3031 3032 let toolbarParent = draggedWrapper.closest("toolbar"); 3033 if (toolbarParent) { 3034 toolbarParent.style.removeProperty("min-height"); 3035 } 3036 } 3037 3038 if (this.#dragOverItem) { 3039 this.#cancelDragActive(this.#dragOverItem); 3040 this.#dragOverItem = null; 3041 } 3042 lazy.DragPositionManager.stop(); 3043 } 3044 3045 /** 3046 * True if the drag/drop event comes from a source other than one of our 3047 * browser windows. This check can be overridden for testing by setting 3048 * `browser.uiCustomization.skipSourceNodeCheck` to `true`. 3049 * 3050 * @param {DragEvent} aEvent 3051 * A drag/drop event. 3052 * @returns {boolean} 3053 * True if the event should be ignored. 3054 */ 3055 #isUnwantedDragDrop(aEvent) { 3056 // The synthesized events for tests generated by synthesizePlainDragAndDrop 3057 // and synthesizeDrop in mochitests are used only for testing whether the 3058 // right data is being put into the dataTransfer. Neither cause a real drop 3059 // to occur, so they don't set the source node. There isn't a means of 3060 // testing real drag and drops, so this pref skips the check but it should 3061 // only be set by test code. 3062 if (this.#skipSourceNodeCheck) { 3063 return false; 3064 } 3065 3066 /* Discard drag events that originated from a separate window to 3067 prevent content->chrome privilege escalations. */ 3068 let mozSourceNode = aEvent.dataTransfer.mozSourceNode; 3069 // mozSourceNode is null in the dragStart event handler or if 3070 // the drag event originated in an external application. 3071 return !mozSourceNode || mozSourceNode.ownerGlobal != this.#window; 3072 } 3073 3074 /** 3075 * Handles applying drag preview effects to a customizable item or area while 3076 * a drag and drop operation is underway. 3077 * 3078 * @param {DOMNode} aDraggedOverItem 3079 * A customizable item being dragged over within a customizable area, or 3080 * a customizable area node. 3081 * @param {string} aValue 3082 * A string to set as the "dragover" attribute on the dragged item to 3083 * indicate which direction (before or after) the placeholder for the 3084 * drag operation should go relative to aItem. This is either the 3085 * string "before" or the string "after". 3086 * @param {string} aDraggedItemId 3087 * The ID of the customizable item being dragged. 3088 * @param {string} aPlace 3089 * The place string associated with the customizable area being dragged 3090 * over. This is expected to be one of the strings returned by 3091 * CustomizableUI.getPlaceForItem. 3092 */ 3093 #setDragActive(aDraggedOverItem, aValue, aDraggedItemId, aPlace) { 3094 if (!aDraggedOverItem) { 3095 return; 3096 } 3097 3098 if (aDraggedOverItem.getAttribute("dragover") != aValue) { 3099 aDraggedOverItem.setAttribute("dragover", aValue); 3100 3101 let window = aDraggedOverItem.ownerGlobal; 3102 let draggedItem = window.document.getElementById(aDraggedItemId); 3103 if (aPlace == "palette") { 3104 // We mostly delegate the complexity of grid placeholder effects to 3105 // DragPositionManager by way of #setGridDragActive. 3106 this.#setGridDragActive(aDraggedOverItem, draggedItem, aValue); 3107 } else { 3108 let targetArea = this.#getCustomizableParent(aDraggedOverItem); 3109 let makeSpaceImmediately = false; 3110 if (!gDraggingInToolbars.has(targetArea.id)) { 3111 gDraggingInToolbars.add(targetArea.id); 3112 let draggedWrapper = this.$("wrapper-" + aDraggedItemId); 3113 let originArea = this.#getCustomizableParent(draggedWrapper); 3114 makeSpaceImmediately = originArea == targetArea; 3115 } 3116 let propertyToMeasure = aPlace == "toolbar" ? "width" : "height"; 3117 // Calculate width/height of the item when it'd be dropped in this position. 3118 let borderWidth = this.#getDragItemSize(aDraggedOverItem, draggedItem)[ 3119 propertyToMeasure 3120 ]; 3121 let layoutSide = aPlace == "toolbar" ? "Inline" : "Block"; 3122 let prop, otherProp; 3123 if (aValue == "before") { 3124 prop = "border" + layoutSide + "StartWidth"; 3125 otherProp = "border-" + layoutSide.toLowerCase() + "-end-width"; 3126 } else { 3127 prop = "border" + layoutSide + "EndWidth"; 3128 otherProp = "border-" + layoutSide.toLowerCase() + "-start-width"; 3129 } 3130 if (makeSpaceImmediately) { 3131 aDraggedOverItem.setAttribute("notransition", "true"); 3132 } 3133 aDraggedOverItem.style[prop] = borderWidth + "px"; 3134 aDraggedOverItem.style.removeProperty(otherProp); 3135 if (makeSpaceImmediately) { 3136 // Force a layout flush: 3137 aDraggedOverItem.getBoundingClientRect(); 3138 aDraggedOverItem.removeAttribute("notransition"); 3139 } 3140 } 3141 } 3142 } 3143 3144 /** 3145 * Reverts drag preview effects applied via #setDragActive from a customizable 3146 * item or area when a drag and drop operation ends. 3147 * 3148 * @param {DOMNode} aDraggedOverItem 3149 * The customizable item or area that was being dragged over. 3150 * @param {DOMNode} aNextDraggedOverItem 3151 * If non-null, this is the customizable item or area that is being 3152 * dragged over now instead of aDraggedOverItem. 3153 * @param {boolean} aNoTransition 3154 * True if the reversion of the drag preview effect should occur without 3155 * a transition (for example, on a drop). 3156 */ 3157 #cancelDragActive(aDraggedOverItem, aNextDraggedOverItem, aNoTransition) { 3158 let currentArea = this.#getCustomizableParent(aDraggedOverItem); 3159 if (!currentArea) { 3160 return; 3161 } 3162 let nextArea = aNextDraggedOverItem 3163 ? this.#getCustomizableParent(aNextDraggedOverItem) 3164 : null; 3165 if (currentArea != nextArea) { 3166 currentArea.removeAttribute("draggingover"); 3167 } 3168 let areaType = CustomizableUI.getAreaType(currentArea.id); 3169 if (areaType) { 3170 if (aNoTransition) { 3171 aDraggedOverItem.setAttribute("notransition", "true"); 3172 } 3173 aDraggedOverItem.removeAttribute("dragover"); 3174 // Remove all property values in the case that the end padding 3175 // had been set. 3176 aDraggedOverItem.style.removeProperty("border-inline-start-width"); 3177 aDraggedOverItem.style.removeProperty("border-inline-end-width"); 3178 aDraggedOverItem.style.removeProperty("border-block-start-width"); 3179 aDraggedOverItem.style.removeProperty("border-block-end-width"); 3180 if (aNoTransition) { 3181 // Force a layout flush: 3182 aDraggedOverItem.getBoundingClientRect(); 3183 aDraggedOverItem.removeAttribute("notransition"); 3184 } 3185 } else { 3186 aDraggedOverItem.removeAttribute("dragover"); 3187 if (aNextDraggedOverItem) { 3188 if (nextArea == currentArea) { 3189 // No need to do anything if we're still dragging in this area: 3190 return; 3191 } 3192 } 3193 // Otherwise, clear everything out: 3194 let positionManager = 3195 lazy.DragPositionManager.getManagerForArea(currentArea); 3196 positionManager.clearPlaceholders(currentArea, aNoTransition); 3197 } 3198 } 3199 3200 /** 3201 * Handles applying drag preview effects to the customization palette grid. 3202 * 3203 * @param {DOMNode} aDragOverNode 3204 * A customizable item being dragged over within the palette, or 3205 * the palette node itself. 3206 * @param {DOMNode} aDraggedItem 3207 * The customizable item being dragged. 3208 */ 3209 #setGridDragActive(aDragOverNode, aDraggedItem) { 3210 let targetArea = this.#getCustomizableParent(aDragOverNode); 3211 let draggedWrapper = this.$("wrapper-" + aDraggedItem.id); 3212 let originArea = this.#getCustomizableParent(draggedWrapper); 3213 let positionManager = 3214 lazy.DragPositionManager.getManagerForArea(targetArea); 3215 let draggedSize = this.#getDragItemSize(aDragOverNode, aDraggedItem); 3216 positionManager.insertPlaceholder( 3217 targetArea, 3218 aDragOverNode, 3219 draggedSize, 3220 originArea == targetArea 3221 ); 3222 } 3223 3224 /** 3225 * Given a customizable item being dragged, and a DOMNode being dragged over, 3226 * returns the size of the dragged item were it to be placed within the area 3227 * associated with aDragOverNode. 3228 * 3229 * @param {DOMNode} aDragOverNode 3230 * The node currently being dragged over. 3231 * @param {DOMNode} aDraggedItem 3232 * The customizable item node currently being dragged. 3233 * @returns {ItemSizeForArea} 3234 */ 3235 #getDragItemSize(aDragOverNode, aDraggedItem) { 3236 // Cache it good, cache it real good. 3237 if (!this.#dragSizeMap) { 3238 this.#dragSizeMap = new WeakMap(); 3239 } 3240 if (!this.#dragSizeMap.has(aDraggedItem)) { 3241 this.#dragSizeMap.set(aDraggedItem, new WeakMap()); 3242 } 3243 let itemMap = this.#dragSizeMap.get(aDraggedItem); 3244 let targetArea = this.#getCustomizableParent(aDragOverNode); 3245 let currentArea = this.#getCustomizableParent(aDraggedItem); 3246 // Return the size for this target from cache, if it exists. 3247 let size = itemMap.get(targetArea); 3248 if (size) { 3249 return size; 3250 } 3251 3252 // Calculate size of the item when it'd be dropped in this position. 3253 let currentParent = aDraggedItem.parentNode; 3254 let currentSibling = aDraggedItem.nextElementSibling; 3255 const kAreaType = "cui-areatype"; 3256 let areaType, currentType; 3257 3258 if (targetArea != currentArea) { 3259 // Move the widget temporarily next to the placeholder. 3260 aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode); 3261 // Update the node's areaType. 3262 areaType = CustomizableUI.getAreaType(targetArea.id); 3263 currentType = 3264 aDraggedItem.hasAttribute(kAreaType) && 3265 aDraggedItem.getAttribute(kAreaType); 3266 if (areaType) { 3267 aDraggedItem.setAttribute(kAreaType, areaType); 3268 } 3269 this.wrapToolbarItem(aDraggedItem, areaType || "palette"); 3270 CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id); 3271 } else { 3272 aDraggedItem.parentNode.hidden = false; 3273 } 3274 3275 // Fetch the new size. 3276 let rect = aDraggedItem.parentNode.getBoundingClientRect(); 3277 size = { width: rect.width, height: rect.height }; 3278 // Cache the found value of size for this target. 3279 itemMap.set(targetArea, size); 3280 3281 if (targetArea != currentArea) { 3282 this.unwrapToolbarItem(aDraggedItem.parentNode); 3283 // Put the item back into its previous position. 3284 currentParent.insertBefore(aDraggedItem, currentSibling); 3285 // restore the areaType 3286 if (areaType) { 3287 if (currentType === false) { 3288 aDraggedItem.removeAttribute(kAreaType); 3289 } else { 3290 aDraggedItem.setAttribute(kAreaType, currentType); 3291 } 3292 } 3293 this.createOrUpdateWrapper(aDraggedItem, null, true); 3294 CustomizableUI.onWidgetDrag(aDraggedItem.id); 3295 } else { 3296 aDraggedItem.parentNode.hidden = true; 3297 } 3298 return size; 3299 } 3300 3301 /** 3302 * Walks the ancestry of a DOMNode element and finds the first customizable 3303 * area node in that ancestry, or null if no such customizable area node 3304 * can be found. 3305 * 3306 * @param {DOMNode} aElement 3307 * The DOMNode for which to find the customizable area parent. 3308 * @returns {DOMNode} 3309 * The customizable area parent of aElement. 3310 */ 3311 #getCustomizableParent(aElement) { 3312 if (aElement) { 3313 // Deal with drag/drop on the padding of the panel. 3314 let containingPanelHolder = aElement.closest( 3315 "#customization-panelHolder" 3316 ); 3317 if (containingPanelHolder) { 3318 return containingPanelHolder.querySelector( 3319 "#widget-overflow-fixed-list" 3320 ); 3321 } 3322 } 3323 3324 let areas = CustomizableUI.areas; 3325 areas.push(kPaletteId); 3326 return aElement.closest(areas.map(a => "#" + CSS.escape(a)).join(",")); 3327 } 3328 3329 /** 3330 * During a drag operation of a customizable item over a customizable area, 3331 * returns the node within that customizable area that the item is being 3332 * dragged over. 3333 * 3334 * @param {DragEvent} aEvent 3335 * The dragover event being handled. 3336 * @param {DOMNode} aAreaElement 3337 * The customizable area element that we should consider the dragover 3338 * operation to be occurring on. This might actually be different from the 3339 * target of the dragover event if we've retargeted the drag (see bug 3340 * 1396423 for an example of where we retarget a dragover area to the 3341 * palette rather than the overflow panel). 3342 * @param {string} aPlace 3343 * The place string associated with the customizable area being dragged 3344 * over. This is expected to be one of the strings returned by 3345 * CustomizableUI.getPlaceForItem. 3346 * @returns {DOMNode} 3347 * The node within aAreaElement that we should assume the dragged item is 3348 * being dragged over. If we cannot resolve this to a target, this falls 3349 * back to just being the aEvent.target. 3350 */ 3351 #getDragOverNode(aEvent, aAreaElement, aPlace) { 3352 let expectedParent = 3353 CustomizableUI.getCustomizationTarget(aAreaElement) || aAreaElement; 3354 if (!expectedParent.contains(aEvent.target)) { 3355 return expectedParent; 3356 } 3357 // Offset the drag event's position with the offset to the center of 3358 // the thing we're dragging 3359 let dragX = aEvent.clientX - this._dragOffset.x; 3360 let dragY = aEvent.clientY - this._dragOffset.y; 3361 3362 // Ensure this is within the container 3363 let boundsContainer = expectedParent; 3364 let bounds = this.#getBoundsWithoutFlushing(boundsContainer); 3365 dragX = Math.min(bounds.right, Math.max(dragX, bounds.left)); 3366 dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top)); 3367 3368 let targetNode; 3369 if (aPlace == "toolbar" || aPlace == "panel") { 3370 targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY); 3371 while (targetNode && targetNode.parentNode != expectedParent) { 3372 targetNode = targetNode.parentNode; 3373 } 3374 } else { 3375 let positionManager = 3376 lazy.DragPositionManager.getManagerForArea(aAreaElement); 3377 // Make it relative to the container: 3378 dragX -= bounds.left; 3379 dragY -= bounds.top; 3380 // Find the closest node: 3381 targetNode = positionManager.find(aAreaElement, dragX, dragY); 3382 } 3383 return targetNode || aEvent.target; 3384 } 3385 3386 /** 3387 * Handler for mousedown events in the customize mode UI for the primary 3388 * button. If the mousedown event is being fired on a customizable item, it 3389 * will have a "mousedown" attribute set to "true" on it. A 3390 * "customizing-movingItem" attribute is also set to "true" on the 3391 * document element. 3392 * 3393 * @param {MouseEvent} aEvent 3394 * The mousedown event being handled. 3395 */ 3396 #onMouseDown(aEvent) { 3397 lazy.log.debug("#onMouseDown"); 3398 if (aEvent.button != 0) { 3399 return; 3400 } 3401 let doc = aEvent.target.ownerDocument; 3402 doc.documentElement.setAttribute("customizing-movingItem", true); 3403 let item = this.#getWrapper(aEvent.target); 3404 if (item) { 3405 item.toggleAttribute("mousedown", true); 3406 } 3407 } 3408 3409 /** 3410 * Handler for mouseup events in the customize mode UI for the primary 3411 * button. If the mouseup event is being fired on a customizable item, it 3412 * will have the "mousedown" attribute added in #onMouseDown removed. This 3413 * will also remove the "customizing-movingItem" attribute on the document 3414 * element. 3415 * 3416 * @param {MouseEvent} aEvent 3417 * The mouseup event being handled. 3418 */ 3419 #onMouseUp(aEvent) { 3420 lazy.log.debug("#onMouseUp"); 3421 if (aEvent.button != 0) { 3422 return; 3423 } 3424 let doc = aEvent.target.ownerDocument; 3425 doc.documentElement.removeAttribute("customizing-movingItem"); 3426 let item = this.#getWrapper(aEvent.target); 3427 if (item) { 3428 item.removeAttribute("mousedown"); 3429 } 3430 } 3431 3432 /** 3433 * Given a customizable item (or one of its descendants), returns the 3434 * toolbarpaletteitem wrapper node ancestor. If no such wrapper can be found, 3435 * this returns null. 3436 * 3437 * @param {DOMNode} aElement 3438 * The customizable item node (or one of its descendants) to get the 3439 * toolbarpaletteitem wrapper node ancestor for. 3440 * @returns {DOMNode|null} 3441 * The toolbarpaletteitem wrapper node, or null if one cannot be found. 3442 */ 3443 #getWrapper(aElement) { 3444 while (aElement && aElement.localName != "toolbarpaletteitem") { 3445 if (aElement.localName == "toolbar") { 3446 return null; 3447 } 3448 aElement = aElement.parentNode; 3449 } 3450 return aElement; 3451 } 3452 3453 /** 3454 * Given some toolbarpaletteitem wrapper, walks the prior sibling elements 3455 * until it finds one that either isn't a toolbarpaletteitem, or doesn't have 3456 * it's first element hidden. Returns null if no such prior sibling element 3457 * can be found. 3458 * 3459 * @param {DOMNode} aReferenceNode 3460 * The toolbarpaletteitem node to check the prior siblings for visibilty. 3461 * If aReferenceNode is not a toolbarpaletteitem, this just returns the 3462 * aReferenceNode immediately. 3463 * @returns {DOMNode|null} 3464 * The first prior sibling with a visible first element child, or the 3465 * first non-toolbarpaletteitem prior sibling, or null if no such item can 3466 * be found. 3467 */ 3468 #findVisiblePreviousSiblingNode(aReferenceNode) { 3469 while ( 3470 aReferenceNode && 3471 aReferenceNode.localName == "toolbarpaletteitem" && 3472 aReferenceNode.firstElementChild.hidden 3473 ) { 3474 aReferenceNode = aReferenceNode.previousElementSibling; 3475 } 3476 return aReferenceNode; 3477 } 3478 3479 /** 3480 * The popupshowing event handler for the context menu on the customization 3481 * palette. 3482 * 3483 * @param {WidgetMouseEvent} event 3484 * The popupshowing event being fired for the context menu. 3485 */ 3486 #onPaletteContextMenuShowing(event) { 3487 let isFlexibleSpace = event.target.triggerNode.id.includes( 3488 "wrapper-customizableui-special-spring" 3489 ); 3490 event.target.querySelector(".customize-context-addToPanel").disabled = 3491 isFlexibleSpace; 3492 } 3493 3494 /** 3495 * The popupshowing event handler for the context menu for items in the 3496 * overflow panel while in customize mode. This is currently public due to 3497 * bug 1378427 (also see bug 1747945). 3498 * 3499 * @param {WidgetMouseEvent} event 3500 * The popupshowing event being fired for the context menu. 3501 */ 3502 onPanelContextMenuShowing(event) { 3503 let inPermanentArea = !!event.target.triggerNode.closest( 3504 "#widget-overflow-fixed-list" 3505 ); 3506 let doc = event.target.ownerDocument; 3507 doc.getElementById("customizationPanelItemContextMenuUnpin").hidden = 3508 !inPermanentArea; 3509 doc.getElementById("customizationPanelItemContextMenuPin").hidden = 3510 inPermanentArea; 3511 3512 doc.ownerGlobal.MozXULElement.insertFTLIfNeeded( 3513 "browser/toolbarContextMenu.ftl" 3514 ); 3515 event.target.querySelectorAll("[data-lazy-l10n-id]").forEach(el => { 3516 el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id")); 3517 el.removeAttribute("data-lazy-l10n-id"); 3518 }); 3519 } 3520 3521 /** 3522 * A window click event handler that checks to see if the item being clicked 3523 * in the window is the downloads button wrapper, with the primary button. 3524 * This is used to show the downloads button autohide panel. 3525 * 3526 * @param {MouseEvent} event 3527 * The click event on the window. 3528 */ 3529 #checkForDownloadsClick(event) { 3530 if ( 3531 event.target.closest("#wrapper-downloads-button") && 3532 event.button == 0 3533 ) { 3534 event.view.gCustomizeMode.#showDownloadsAutoHidePanel(); 3535 } 3536 } 3537 3538 /** 3539 * Adds a click event listener to the top-level window to check to see if the 3540 * downloads button is ever clicked while in customize mode. Callers should 3541 * ensure that #teardownDownloadAutoHideToggle is called when exiting 3542 * customize mode. 3543 */ 3544 #setupDownloadAutoHideToggle() { 3545 this.#window.addEventListener("click", this.#checkForDownloadsClick, true); 3546 } 3547 3548 /** 3549 * Removes the click event listener on the top-level window that was added by 3550 * #setupDownloadAutoHideToggle. 3551 */ 3552 #teardownDownloadAutoHideToggle() { 3553 this.#window.removeEventListener( 3554 "click", 3555 this.#checkForDownloadsClick, 3556 true 3557 ); 3558 this.$(kDownloadAutohidePanelId).hidePopup(); 3559 } 3560 3561 /** 3562 * Attempts to move the downloads button to the navigation toolbar in the 3563 * event that they've turned on the autohide feature for the button while 3564 * the button is in the palette. 3565 */ 3566 #maybeMoveDownloadsButtonToNavBar() { 3567 // If the user toggled the autohide checkbox while the item was in the 3568 // palette, and hasn't moved it since, move the item to the default 3569 // location in the navbar for them. 3570 if ( 3571 !CustomizableUI.getPlacementOfWidget("downloads-button") && 3572 this.#moveDownloadsButtonToNavBar && 3573 this.#window.DownloadsButton.autoHideDownloadsButton 3574 ) { 3575 let navbarPlacements = CustomizableUI.getWidgetIdsInArea("nav-bar"); 3576 let insertionPoint = navbarPlacements.indexOf("urlbar-container"); 3577 while (++insertionPoint < navbarPlacements.length) { 3578 let widget = navbarPlacements[insertionPoint]; 3579 // If we find a non-searchbar, non-spacer node, break out of the loop: 3580 if ( 3581 widget != "search-container" && 3582 !(CustomizableUI.isSpecialWidget(widget) && widget.includes("spring")) 3583 ) { 3584 break; 3585 } 3586 } 3587 CustomizableUI.addWidgetToArea( 3588 "downloads-button", 3589 "nav-bar", 3590 insertionPoint 3591 ); 3592 lazy.BrowserUsageTelemetry.recordWidgetChange( 3593 "downloads-button", 3594 "nav-bar", 3595 "move-downloads" 3596 ); 3597 } 3598 } 3599 3600 /** 3601 * Opens the panel that shows the toggle for auto-hiding the downloads button 3602 * when there are no downloads underway. If that panel is already open, it 3603 * is first closed. This panel is not shown if the downloads button is in 3604 * the overflow panel (since when the button is there, it does not autohide). 3605 * 3606 * @returns {Promise<undefined>} 3607 */ 3608 async #showDownloadsAutoHidePanel() { 3609 let doc = this.#document; 3610 let panel = doc.getElementById(kDownloadAutohidePanelId); 3611 panel.hidePopup(); 3612 let button = doc.getElementById("downloads-button"); 3613 // We don't show the tooltip if the button is in the panel. 3614 if (button.closest("#widget-overflow-fixed-list")) { 3615 return; 3616 } 3617 3618 let offsetX = 0, 3619 offsetY = 0; 3620 let panelOnTheLeft = false; 3621 let toolbarContainer = button.closest("toolbar"); 3622 if (toolbarContainer && toolbarContainer.id == "nav-bar") { 3623 let navbarWidgets = CustomizableUI.getWidgetIdsInArea("nav-bar"); 3624 if ( 3625 navbarWidgets.indexOf("urlbar-container") <= 3626 navbarWidgets.indexOf("downloads-button") 3627 ) { 3628 panelOnTheLeft = true; 3629 } 3630 } else { 3631 await this.#window.promiseDocumentFlushed(() => {}); 3632 3633 if (!this.#customizing || !this._wantToBeInCustomizeMode) { 3634 return; 3635 } 3636 let buttonBounds = this.#getBoundsWithoutFlushing(button); 3637 let windowBounds = this.#getBoundsWithoutFlushing(doc.documentElement); 3638 panelOnTheLeft = 3639 buttonBounds.left + buttonBounds.width / 2 > windowBounds.width / 2; 3640 } 3641 let position; 3642 if (panelOnTheLeft) { 3643 // Tested in RTL, these get inverted automatically, so this does the 3644 // right thing without taking RTL into account explicitly. 3645 position = "topleft topright"; 3646 if (toolbarContainer) { 3647 offsetX = 8; 3648 } 3649 } else { 3650 position = "topright topleft"; 3651 if (toolbarContainer) { 3652 offsetX = -8; 3653 } 3654 } 3655 3656 let checkbox = doc.getElementById(kDownloadAutohideCheckboxId); 3657 if (this.#window.DownloadsButton.autoHideDownloadsButton) { 3658 checkbox.setAttribute("checked", "true"); 3659 } else { 3660 checkbox.removeAttribute("checked"); 3661 } 3662 3663 // We don't use the icon to anchor because it might be resizing because of 3664 // the animations for drag/drop. Hence the use of offsets. 3665 panel.openPopup(button, position, offsetX, offsetY); 3666 } 3667 3668 /** 3669 * Called when the downloads button auto-hide toggle changes value. 3670 * 3671 * @param {CommandEvent} event 3672 * The event that caused the toggle change. 3673 */ 3674 #onDownloadsAutoHideChange(event) { 3675 let checkbox = event.target.ownerDocument.getElementById( 3676 kDownloadAutohideCheckboxId 3677 ); 3678 Services.prefs.setBoolPref(kDownloadAutoHidePref, checkbox.checked); 3679 // Ensure we move the button (back) after the user leaves customize mode. 3680 event.view.gCustomizeMode.#moveDownloadsButtonToNavBar = checkbox.checked; 3681 } 3682 3683 /** 3684 * Called when the button to customize the macOS touchbar is clicked. 3685 */ 3686 #customizeTouchBar() { 3687 let updater = Cc["@mozilla.org/widget/touchbarupdater;1"].getService( 3688 Ci.nsITouchBarUpdater 3689 ); 3690 updater.enterCustomizeMode(); 3691 } 3692 3693 /** 3694 * This is a method to toggle pong on or off in customize mode. You heard me. 3695 * 3696 * @param {boolean} enabled 3697 * True if pong should be launched, or false if it should be torn down. 3698 */ 3699 #togglePong(enabled) { 3700 // It's possible we're toggling for a reason other than hitting 3701 // the button (we might be exiting, for example), so make sure that 3702 // the state and checkbox are in sync. 3703 let whimsyButton = this.$("whimsy-button"); 3704 whimsyButton.checked = enabled; 3705 3706 if (enabled) { 3707 this.visiblePalette.setAttribute("whimsypong", "true"); 3708 this.pongArena.hidden = false; 3709 if (!this.uninitWhimsy) { 3710 this.uninitWhimsy = this.#whimsypong(); 3711 } 3712 } else { 3713 this.visiblePalette.removeAttribute("whimsypong"); 3714 if (this.uninitWhimsy) { 3715 this.uninitWhimsy(); 3716 this.uninitWhimsy = null; 3717 } 3718 this.pongArena.hidden = true; 3719 } 3720 } 3721 3722 /** 3723 * This method contains a very simple implementation of a pong-like game. 3724 * Calling this method presumes that the pongArea element is visible. 3725 * 3726 * @returns {Function} 3727 * Returns a clean-up function which tears down the launched pong game. 3728 */ 3729 #whimsypong() { 3730 function update() { 3731 updateBall(); 3732 updatePlayers(); 3733 } 3734 3735 function updateBall() { 3736 if (ball[1] <= 0 || ball[1] >= gameSide) { 3737 if ( 3738 (ball[1] <= 0 && (ball[0] < p1 || ball[0] > p1 + paddleWidth)) || 3739 (ball[1] >= gameSide && (ball[0] < p2 || ball[0] > p2 + paddleWidth)) 3740 ) { 3741 updateScore(ball[1] <= 0 ? 0 : 1); 3742 } else { 3743 if ( 3744 (ball[1] <= 0 && 3745 (ball[0] - p1 < paddleEdge || 3746 p1 + paddleWidth - ball[0] < paddleEdge)) || 3747 (ball[1] >= gameSide && 3748 (ball[0] - p2 < paddleEdge || 3749 p2 + paddleWidth - ball[0] < paddleEdge)) 3750 ) { 3751 ballDxDy[0] *= Math.random() + 1.3; 3752 ballDxDy[0] = Math.max(Math.min(ballDxDy[0], 6), -6); 3753 if (Math.abs(ballDxDy[0]) == 6) { 3754 ballDxDy[0] += Math.sign(ballDxDy[0]) * Math.random(); 3755 } 3756 } else { 3757 ballDxDy[0] /= 1.1; 3758 } 3759 ballDxDy[1] *= -1; 3760 ball[1] = ball[1] <= 0 ? 0 : gameSide; 3761 } 3762 } 3763 ball = [ 3764 Math.max(Math.min(ball[0] + ballDxDy[0], gameSide), 0), 3765 Math.max(Math.min(ball[1] + ballDxDy[1], gameSide), 0), 3766 ]; 3767 if (ball[0] <= 0 || ball[0] >= gameSide) { 3768 ballDxDy[0] *= -1; 3769 } 3770 } 3771 3772 function updatePlayers() { 3773 if (keydown) { 3774 let p1Adj = 1; 3775 if ( 3776 (keydown == 37 && !window.RTL_UI) || 3777 (keydown == 39 && window.RTL_UI) 3778 ) { 3779 p1Adj = -1; 3780 } 3781 p1 += p1Adj * 10 * keydownAdj; 3782 } 3783 3784 let sign = Math.sign(ballDxDy[0]); 3785 if ( 3786 (sign > 0 && ball[0] > p2 + paddleWidth / 2) || 3787 (sign < 0 && ball[0] < p2 + paddleWidth / 2) 3788 ) { 3789 p2 += sign * 3; 3790 } else if ( 3791 (sign > 0 && ball[0] > p2 + paddleWidth / 1.1) || 3792 (sign < 0 && ball[0] < p2 + paddleWidth / 1.1) 3793 ) { 3794 p2 += sign * 9; 3795 } 3796 3797 if (score >= winScore) { 3798 p1 = ball[0]; 3799 p2 = ball[0]; 3800 } 3801 p1 = Math.max(Math.min(p1, gameSide - paddleWidth), 0); 3802 p2 = Math.max(Math.min(p2, gameSide - paddleWidth), 0); 3803 } 3804 3805 function updateScore(adj) { 3806 if (adj) { 3807 score += adj; 3808 } else if (--lives == 0) { 3809 quit = true; 3810 } 3811 ball = ballDef.slice(); 3812 ballDxDy = ballDxDyDef.slice(); 3813 ballDxDy[1] *= score / winScore + 1; 3814 } 3815 3816 function draw() { 3817 let xAdj = window.RTL_UI ? -1 : 1; 3818 elements["wp-player1"].style.transform = 3819 "translate(" + xAdj * p1 + "px, -37px)"; 3820 elements["wp-player2"].style.transform = 3821 "translate(" + xAdj * p2 + "px, " + gameSide + "px)"; 3822 elements["wp-ball"].style.transform = 3823 "translate(" + xAdj * ball[0] + "px, " + ball[1] + "px)"; 3824 elements["wp-score"].textContent = score; 3825 elements["wp-lives"].setAttribute("lives", lives); 3826 if (score >= winScore) { 3827 let arena = elements.arena; 3828 let image = "url(chrome://browser/skin/customizableui/whimsy.png)"; 3829 let position = `${ 3830 (window.RTL_UI ? gameSide : 0) + xAdj * ball[0] - 10 3831 }px ${ball[1] - 10}px`; 3832 let repeat = "no-repeat"; 3833 let size = "20px"; 3834 if (arena.style.backgroundImage) { 3835 if (arena.style.backgroundImage.split(",").length >= 160) { 3836 quit = true; 3837 } 3838 3839 image += ", " + arena.style.backgroundImage; 3840 position += ", " + arena.style.backgroundPosition; 3841 repeat += ", " + arena.style.backgroundRepeat; 3842 size += ", " + arena.style.backgroundSize; 3843 } 3844 arena.style.backgroundImage = image; 3845 arena.style.backgroundPosition = position; 3846 arena.style.backgroundRepeat = repeat; 3847 arena.style.backgroundSize = size; 3848 } 3849 } 3850 3851 function onkeydown(event) { 3852 keys.push(event.which); 3853 if (keys.length > 10) { 3854 keys.shift(); 3855 let codeEntered = true; 3856 for (let i = 0; i < keys.length; i++) { 3857 if (keys[i] != keysCode[i]) { 3858 codeEntered = false; 3859 break; 3860 } 3861 } 3862 if (codeEntered) { 3863 elements.arena.setAttribute("kcode", "true"); 3864 let spacer = document.querySelector( 3865 "#customization-palette > toolbarpaletteitem" 3866 ); 3867 spacer.setAttribute("kcode", "true"); 3868 } 3869 } 3870 if (event.which == 37 /* left */ || event.which == 39 /* right */) { 3871 keydown = event.which; 3872 keydownAdj *= 1.05; 3873 } 3874 } 3875 3876 function onkeyup(event) { 3877 if (event.which == 37 || event.which == 39) { 3878 keydownAdj = 1; 3879 keydown = 0; 3880 } 3881 } 3882 3883 function uninit() { 3884 document.removeEventListener("keydown", onkeydown); 3885 document.removeEventListener("keyup", onkeyup); 3886 if (rAFHandle) { 3887 window.cancelAnimationFrame(rAFHandle); 3888 } 3889 let arena = elements.arena; 3890 while (arena.firstChild) { 3891 arena.firstChild.remove(); 3892 } 3893 arena.removeAttribute("score"); 3894 arena.removeAttribute("lives"); 3895 arena.removeAttribute("kcode"); 3896 arena.style.removeProperty("background-image"); 3897 arena.style.removeProperty("background-position"); 3898 arena.style.removeProperty("background-repeat"); 3899 arena.style.removeProperty("background-size"); 3900 let spacer = document.querySelector( 3901 "#customization-palette > toolbarpaletteitem" 3902 ); 3903 spacer.removeAttribute("kcode"); 3904 elements = null; 3905 document = null; 3906 quit = true; 3907 } 3908 3909 if (this.uninitWhimsy) { 3910 return this.uninitWhimsy; 3911 } 3912 3913 let ballDef = [10, 10]; 3914 let ball = [10, 10]; 3915 let ballDxDyDef = [2, 2]; 3916 let ballDxDy = [2, 2]; 3917 let score = 0; 3918 let p1 = 0; 3919 let p2 = 10; 3920 let gameSide = 300; 3921 let paddleEdge = 30; 3922 let paddleWidth = 84; 3923 let keydownAdj = 1; 3924 let keydown = 0; 3925 let keys = []; 3926 let keysCode = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]; 3927 let lives = 5; 3928 let winScore = 11; 3929 let quit = false; 3930 let document = this.#document; 3931 let rAFHandle = 0; 3932 let elements = { 3933 arena: document.getElementById("customization-pong-arena"), 3934 }; 3935 3936 document.addEventListener("keydown", onkeydown); 3937 document.addEventListener("keyup", onkeyup); 3938 3939 for (let id of ["player1", "player2", "ball", "score", "lives"]) { 3940 let el = document.createXULElement("box"); 3941 el.id = "wp-" + id; 3942 elements[el.id] = elements.arena.appendChild(el); 3943 } 3944 3945 let spacer = this.visiblePalette.querySelector("toolbarpaletteitem"); 3946 for (let player of ["#wp-player1", "#wp-player2"]) { 3947 let val = "-moz-element(#" + spacer.id + ") no-repeat"; 3948 elements.arena.querySelector(player).style.background = val; 3949 } 3950 3951 let window = this.#window; 3952 rAFHandle = window.requestAnimationFrame(function animate() { 3953 update(); 3954 draw(); 3955 if (quit) { 3956 elements["wp-score"].textContent = score; 3957 elements["wp-lives"] && 3958 elements["wp-lives"].setAttribute("lives", lives); 3959 elements.arena.setAttribute("score", score); 3960 elements.arena.setAttribute("lives", lives); 3961 } else { 3962 rAFHandle = window.requestAnimationFrame(animate); 3963 } 3964 }); 3965 3966 return uninit; 3967 } 3968 } 3969 3970 /** 3971 * A utility function that, when in debug mode, can emit drag data through the 3972 * debug logging mechanism for various drag and drop events. 3973 * 3974 * @param {DragEvent} aEvent 3975 * The DragEvent to dump debug information to the log for. 3976 * @param {string|null} caller 3977 * An optional string to indicate the caller of the log message. 3978 */ 3979 function __dumpDragData(aEvent, caller) { 3980 if (!gDebug) { 3981 return; 3982 } 3983 let str = 3984 "Dumping drag data (" + 3985 (caller ? caller + " in " : "") + 3986 "CustomizeMode.sys.mjs) {\n"; 3987 str += " type: " + aEvent.type + "\n"; 3988 for (let el of ["target", "currentTarget", "relatedTarget"]) { 3989 if (aEvent[el]) { 3990 str += 3991 " " + 3992 el + 3993 ": " + 3994 aEvent[el] + 3995 "(localName=" + 3996 aEvent[el].localName + 3997 "; id=" + 3998 aEvent[el].id + 3999 ")\n"; 4000 } 4001 } 4002 for (let prop in aEvent.dataTransfer) { 4003 if (typeof aEvent.dataTransfer[prop] != "function") { 4004 str += 4005 " dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n"; 4006 } 4007 } 4008 str += "}"; 4009 lazy.log.debug(str); 4010 }