michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["CustomizeMode"]; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: const kPrefCustomizationDebug = "browser.uiCustomization.debug"; michael@0: const kPrefCustomizationAnimation = "browser.uiCustomization.disableAnimation"; michael@0: const kPaletteId = "customization-palette"; michael@0: const kAboutURI = "about:customizing"; michael@0: const kDragDataTypePrefix = "text/toolbarwrapper-id/"; michael@0: const kPlaceholderClass = "panel-customization-placeholder"; michael@0: const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck"; michael@0: const kToolbarVisibilityBtn = "customization-toolbar-visibility-button"; michael@0: const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar"; michael@0: const kMaxTransitionDurationMs = 2000; michael@0: michael@0: const kPanelItemContextMenu = "customizationPanelItemContextMenu"; michael@0: const kPaletteItemContextMenu = "customizationPaletteItemContextMenu"; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource:///modules/CustomizableUI.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DragPositionManager", michael@0: "resource:///modules/DragPositionManager.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", michael@0: "resource:///modules/BrowserUITelemetry.jsm"); michael@0: michael@0: let gModuleName = "[CustomizeMode]"; michael@0: #include logging.js michael@0: michael@0: let gDisableAnimation = null; michael@0: michael@0: let gDraggingInToolbars; michael@0: michael@0: function CustomizeMode(aWindow) { michael@0: if (gDisableAnimation === null) { michael@0: gDisableAnimation = Services.prefs.getPrefType(kPrefCustomizationAnimation) == Ci.nsIPrefBranch.PREF_BOOL && michael@0: Services.prefs.getBoolPref(kPrefCustomizationAnimation); michael@0: } michael@0: this.window = aWindow; michael@0: this.document = aWindow.document; michael@0: this.browser = aWindow.gBrowser; michael@0: michael@0: // There are two palettes - there's the palette that can be overlayed with michael@0: // toolbar items in browser.xul. This is invisible, and never seen by the michael@0: // user. Then there's the visible palette, which gets populated and displayed michael@0: // to the user when in customizing mode. michael@0: this.visiblePalette = this.document.getElementById(kPaletteId); michael@0: this.paletteEmptyNotice = this.document.getElementById("customization-empty"); michael@0: this.paletteSpacer = this.document.getElementById("customization-spacer"); michael@0: this.tipPanel = this.document.getElementById("customization-tipPanel"); michael@0: #ifdef CAN_DRAW_IN_TITLEBAR michael@0: this._updateTitlebarButton(); michael@0: Services.prefs.addObserver(kDrawInTitlebarPref, this, false); michael@0: this.window.addEventListener("unload", this); michael@0: #endif michael@0: }; michael@0: michael@0: CustomizeMode.prototype = { michael@0: _changed: false, michael@0: _transitioning: false, michael@0: window: null, michael@0: document: null, michael@0: // areas is used to cache the customizable areas when in customization mode. michael@0: areas: null, michael@0: // When in customizing mode, we swap out the reference to the invisible michael@0: // palette in gNavToolbox.palette for our visiblePalette. This way, for the michael@0: // customizing browser window, when widgets are removed from customizable michael@0: // areas and added to the palette, they're added to the visible palette. michael@0: // _stowedPalette is a reference to the old invisible palette so we can michael@0: // restore gNavToolbox.palette to its original state after exiting michael@0: // customization mode. michael@0: _stowedPalette: null, michael@0: _dragOverItem: null, michael@0: _customizing: false, michael@0: _skipSourceNodeCheck: null, michael@0: _mainViewContext: null, michael@0: michael@0: get panelUIContents() { michael@0: return this.document.getElementById("PanelUI-contents"); michael@0: }, michael@0: michael@0: get _handler() { michael@0: return this.window.CustomizationHandler; michael@0: }, michael@0: michael@0: uninit: function() { michael@0: #ifdef CAN_DRAW_IN_TITLEBAR michael@0: Services.prefs.removeObserver(kDrawInTitlebarPref, this); michael@0: #endif michael@0: }, michael@0: michael@0: toggle: function() { michael@0: if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) { michael@0: this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode; michael@0: return; michael@0: } michael@0: if (this._customizing) { michael@0: this.exit(); michael@0: } else { michael@0: this.enter(); michael@0: } michael@0: }, michael@0: michael@0: enter: function() { michael@0: this._wantToBeInCustomizeMode = true; michael@0: michael@0: if (this._customizing || this._handler.isEnteringCustomizeMode) { michael@0: return; michael@0: } michael@0: michael@0: // Exiting; want to re-enter once we've done that. michael@0: if (this._handler.isExitingCustomizeMode) { michael@0: LOG("Attempted to enter while we're in the middle of exiting. " + michael@0: "We'll exit after we've entered"); michael@0: return; michael@0: } michael@0: michael@0: michael@0: // We don't need to switch to kAboutURI, or open a new tab at michael@0: // kAboutURI if we're already on it. michael@0: if (this.browser.selectedBrowser.currentURI.spec != kAboutURI) { michael@0: this.window.switchToTabHavingURI(kAboutURI, true, { michael@0: skipTabAnimation: true, michael@0: }); michael@0: return; michael@0: } michael@0: michael@0: let window = this.window; michael@0: let document = this.document; michael@0: michael@0: this._handler.isEnteringCustomizeMode = true; michael@0: michael@0: // Always disable the reset button at the start of customize mode, it'll be re-enabled michael@0: // if necessary when we finish entering: michael@0: let resetButton = this.document.getElementById("customization-reset-button"); michael@0: resetButton.setAttribute("disabled", "true"); michael@0: michael@0: Task.spawn(function() { michael@0: // We shouldn't start customize mode until after browser-delayed-startup has finished: michael@0: if (!this.window.gBrowserInit.delayedStartupFinished) { michael@0: let delayedStartupDeferred = Promise.defer(); michael@0: let delayedStartupObserver = function(aSubject) { michael@0: if (aSubject == this.window) { michael@0: Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished"); michael@0: delayedStartupDeferred.resolve(); michael@0: } michael@0: }.bind(this); michael@0: Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false); michael@0: yield delayedStartupDeferred.promise; michael@0: } michael@0: michael@0: let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn); michael@0: let togglableToolbars = window.getTogglableToolbars(); michael@0: let bookmarksToolbar = document.getElementById("PersonalToolbar"); michael@0: if (togglableToolbars.length == 0) { michael@0: toolbarVisibilityBtn.setAttribute("hidden", "true"); michael@0: } else { michael@0: toolbarVisibilityBtn.removeAttribute("hidden"); michael@0: } michael@0: michael@0: // Disable lightweight themes while in customization mode since michael@0: // they don't have large enough images to pad the full browser window. michael@0: if (this.document.documentElement._lightweightTheme) michael@0: this.document.documentElement._lightweightTheme.disable(); michael@0: michael@0: CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window); michael@0: CustomizableUI.notifyStartCustomizing(this.window); michael@0: michael@0: // Add a keypress listener to the document so that we can quickly exit michael@0: // customization mode when pressing ESC. michael@0: document.addEventListener("keypress", this); michael@0: michael@0: // Same goes for the menu button - if we're customizing, a click on the michael@0: // menu button means a quick exit from customization mode. michael@0: window.PanelUI.hide(); michael@0: window.PanelUI.menuButton.addEventListener("command", this); michael@0: window.PanelUI.menuButton.open = true; michael@0: window.PanelUI.beginBatchUpdate(); michael@0: michael@0: // The menu panel is lazy, and registers itself when the popup shows. We michael@0: // need to force the menu panel to register itself, or else customization michael@0: // is really not going to work. We pass "true" to ensureReady to michael@0: // indicate that we're handling calling startBatchUpdate and michael@0: // endBatchUpdate. michael@0: if (!window.PanelUI.isReady()) { michael@0: yield window.PanelUI.ensureReady(true); michael@0: } michael@0: michael@0: // Hide the palette before starting the transition for increased perf. michael@0: this.visiblePalette.hidden = true; michael@0: this.visiblePalette.removeAttribute("showing"); michael@0: michael@0: // Disable the button-text fade-out mask michael@0: // during the transition for increased perf. michael@0: let panelContents = window.PanelUI.contents; michael@0: panelContents.setAttribute("customize-transitioning", "true"); michael@0: michael@0: // Move the mainView in the panel to the holder so that we can see it michael@0: // while customizing. michael@0: let mainView = window.PanelUI.mainView; michael@0: let panelHolder = document.getElementById("customization-panelHolder"); michael@0: panelHolder.appendChild(mainView); michael@0: michael@0: let customizeButton = document.getElementById("PanelUI-customize"); michael@0: customizeButton.setAttribute("enterLabel", customizeButton.getAttribute("label")); michael@0: customizeButton.setAttribute("label", customizeButton.getAttribute("exitLabel")); michael@0: customizeButton.setAttribute("enterTooltiptext", customizeButton.getAttribute("tooltiptext")); michael@0: customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("exitTooltiptext")); michael@0: michael@0: this._transitioning = true; michael@0: michael@0: let customizer = document.getElementById("customization-container"); michael@0: customizer.parentNode.selectedPanel = customizer; michael@0: customizer.hidden = false; michael@0: michael@0: yield this._doTransition(true); michael@0: michael@0: // Let everybody in this window know that we're about to customize. michael@0: CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window); michael@0: michael@0: this._mainViewContext = mainView.getAttribute("context"); michael@0: if (this._mainViewContext) { michael@0: mainView.removeAttribute("context"); michael@0: } michael@0: michael@0: this._showPanelCustomizationPlaceholders(); michael@0: michael@0: yield this._wrapToolbarItems(); michael@0: this.populatePalette(); michael@0: michael@0: this._addDragHandlers(this.visiblePalette); michael@0: michael@0: window.gNavToolbox.addEventListener("toolbarvisibilitychange", this); michael@0: michael@0: document.getElementById("PanelUI-help").setAttribute("disabled", true); michael@0: document.getElementById("PanelUI-quit").setAttribute("disabled", true); michael@0: michael@0: this._updateResetButton(); michael@0: this._updateUndoResetButton(); michael@0: michael@0: this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL && michael@0: Services.prefs.getBoolPref(kSkipSourceNodePref); michael@0: michael@0: let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])"); michael@0: for (let toolbar of customizableToolbars) michael@0: toolbar.setAttribute("customizing", true); michael@0: michael@0: CustomizableUI.addListener(this); michael@0: window.PanelUI.endBatchUpdate(); michael@0: this._customizing = true; michael@0: this._transitioning = false; michael@0: michael@0: // Show the palette now that the transition has finished. michael@0: this.visiblePalette.hidden = false; michael@0: window.setTimeout(() => { michael@0: // Force layout reflow to ensure the animation runs, michael@0: // and make it async so it doesn't affect the timing. michael@0: this.visiblePalette.clientTop; michael@0: this.visiblePalette.setAttribute("showing", "true"); michael@0: }, 0); michael@0: this.paletteSpacer.hidden = true; michael@0: this._updateEmptyPaletteNotice(); michael@0: michael@0: this.maybeShowTip(panelHolder); michael@0: michael@0: this._handler.isEnteringCustomizeMode = false; michael@0: panelContents.removeAttribute("customize-transitioning"); michael@0: michael@0: CustomizableUI.dispatchToolboxEvent("customizationready", {}, window); michael@0: this._enableOutlinesTimeout = window.setTimeout(() => { michael@0: this.document.getElementById("nav-bar").setAttribute("showoutline", "true"); michael@0: this.panelUIContents.setAttribute("showoutline", "true"); michael@0: delete this._enableOutlinesTimeout; michael@0: }, 0); michael@0: michael@0: // It's possible that we didn't enter customize mode via the menu panel, michael@0: // meaning we didn't kick off about:customizing preloading. If that's michael@0: // the case, let's kick it off for the next time we load this mode. michael@0: window.gCustomizationTabPreloader.ensurePreloading(); michael@0: if (!this._wantToBeInCustomizeMode) { michael@0: this.exit(); michael@0: } michael@0: }.bind(this)).then(null, function(e) { michael@0: ERROR(e); michael@0: // We should ensure this has been called, and calling it again doesn't hurt: michael@0: window.PanelUI.endBatchUpdate(); michael@0: this._handler.isEnteringCustomizeMode = false; michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: exit: function() { michael@0: this._wantToBeInCustomizeMode = false; michael@0: michael@0: if (!this._customizing || this._handler.isExitingCustomizeMode) { michael@0: return; michael@0: } michael@0: michael@0: // Entering; want to exit once we've done that. michael@0: if (this._handler.isEnteringCustomizeMode) { michael@0: LOG("Attempted to exit while we're in the middle of entering. " + michael@0: "We'll exit after we've entered"); michael@0: return; michael@0: } michael@0: michael@0: if (this.resetting) { michael@0: LOG("Attempted to exit while we're resetting. " + michael@0: "We'll exit after resetting has finished."); michael@0: return; michael@0: } michael@0: michael@0: this.hideTip(); michael@0: michael@0: this._handler.isExitingCustomizeMode = true; michael@0: michael@0: if (this._enableOutlinesTimeout) { michael@0: this.window.clearTimeout(this._enableOutlinesTimeout); michael@0: } else { michael@0: this.document.getElementById("nav-bar").removeAttribute("showoutline"); michael@0: this.panelUIContents.removeAttribute("showoutline"); michael@0: } michael@0: michael@0: this._removeExtraToolbarsIfEmpty(); michael@0: michael@0: CustomizableUI.removeListener(this); michael@0: michael@0: this.document.removeEventListener("keypress", this); michael@0: this.window.PanelUI.menuButton.removeEventListener("command", this); michael@0: this.window.PanelUI.menuButton.open = false; michael@0: michael@0: this.window.PanelUI.beginBatchUpdate(); michael@0: michael@0: this._removePanelCustomizationPlaceholders(); michael@0: michael@0: let window = this.window; michael@0: let document = this.document; michael@0: let documentElement = document.documentElement; michael@0: michael@0: // Hide the palette before starting the transition for increased perf. michael@0: this.paletteSpacer.hidden = false; michael@0: this.visiblePalette.hidden = true; michael@0: this.visiblePalette.removeAttribute("showing"); michael@0: this.paletteEmptyNotice.hidden = true; michael@0: michael@0: // Disable the button-text fade-out mask michael@0: // during the transition for increased perf. michael@0: let panelContents = window.PanelUI.contents; michael@0: panelContents.setAttribute("customize-transitioning", "true"); michael@0: michael@0: // Disable the reset and undo reset buttons while transitioning: michael@0: let resetButton = this.document.getElementById("customization-reset-button"); michael@0: let undoResetButton = this.document.getElementById("customization-undo-reset-button"); michael@0: undoResetButton.hidden = resetButton.disabled = true; michael@0: michael@0: this._transitioning = true; michael@0: michael@0: Task.spawn(function() { michael@0: yield this.depopulatePalette(); michael@0: michael@0: yield this._doTransition(false); michael@0: michael@0: let browser = document.getElementById("browser"); michael@0: if (this.browser.selectedBrowser.currentURI.spec == kAboutURI) { michael@0: let custBrowser = this.browser.selectedBrowser; michael@0: if (custBrowser.canGoBack) { michael@0: // If there's history to this tab, just go back. michael@0: // Note that this throws an exception if the previous document has a michael@0: // problematic URL (e.g. about:idontexist) michael@0: try { michael@0: custBrowser.goBack(); michael@0: } catch (ex) { michael@0: ERROR(ex); michael@0: } michael@0: } else { michael@0: // If we can't go back, we're removing the about:customization tab. michael@0: // We only do this if we're the top window for this window (so not michael@0: // a dialog window, for example). michael@0: if (window.getTopWin(true) == window) { michael@0: let customizationTab = this.browser.selectedTab; michael@0: if (this.browser.browsers.length == 1) { michael@0: window.BrowserOpenTab(); michael@0: } michael@0: this.browser.removeTab(customizationTab); michael@0: } michael@0: } michael@0: } michael@0: browser.parentNode.selectedPanel = browser; michael@0: let customizer = document.getElementById("customization-container"); michael@0: customizer.hidden = true; michael@0: michael@0: window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this); michael@0: michael@0: DragPositionManager.stop(); michael@0: this._removeDragHandlers(this.visiblePalette); michael@0: michael@0: yield this._unwrapToolbarItems(); michael@0: michael@0: if (this._changed) { michael@0: // XXXmconley: At first, it seems strange to also persist the old way with michael@0: // currentset - but this might actually be useful for switching michael@0: // to old builds. We might want to keep this around for a little michael@0: // bit. michael@0: this.persistCurrentSets(); michael@0: } michael@0: michael@0: // And drop all area references. michael@0: this.areas = []; michael@0: michael@0: // Let everybody in this window know that we're starting to michael@0: // exit customization mode. michael@0: CustomizableUI.dispatchToolboxEvent("customizationending", {}, window); michael@0: michael@0: window.PanelUI.setMainView(window.PanelUI.mainView); michael@0: window.PanelUI.menuButton.disabled = false; michael@0: michael@0: let customizeButton = document.getElementById("PanelUI-customize"); michael@0: customizeButton.setAttribute("exitLabel", customizeButton.getAttribute("label")); michael@0: customizeButton.setAttribute("label", customizeButton.getAttribute("enterLabel")); michael@0: customizeButton.setAttribute("exitTooltiptext", customizeButton.getAttribute("tooltiptext")); michael@0: customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("enterTooltiptext")); michael@0: michael@0: // We have to use setAttribute/removeAttribute here instead of the michael@0: // property because the XBL property will be set later, and right michael@0: // now we'd be setting an expando, which breaks the XBL property. michael@0: document.getElementById("PanelUI-help").removeAttribute("disabled"); michael@0: document.getElementById("PanelUI-quit").removeAttribute("disabled"); michael@0: michael@0: panelContents.removeAttribute("customize-transitioning"); michael@0: michael@0: // We need to set this._customizing to false before removing the tab michael@0: // or the TabSelect event handler will think that we are exiting michael@0: // customization mode for a second time. michael@0: this._customizing = false; michael@0: michael@0: let mainView = window.PanelUI.mainView; michael@0: if (this._mainViewContext) { michael@0: mainView.setAttribute("context", this._mainViewContext); michael@0: } michael@0: michael@0: if (this.document.documentElement._lightweightTheme) michael@0: this.document.documentElement._lightweightTheme.enable(); michael@0: michael@0: let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])"); michael@0: for (let toolbar of customizableToolbars) michael@0: toolbar.removeAttribute("customizing"); michael@0: michael@0: this.window.PanelUI.endBatchUpdate(); michael@0: this._changed = false; michael@0: this._transitioning = false; michael@0: this._handler.isExitingCustomizeMode = false; michael@0: CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window); michael@0: CustomizableUI.notifyEndCustomizing(window); michael@0: michael@0: if (this._wantToBeInCustomizeMode) { michael@0: this.enter(); michael@0: } michael@0: }.bind(this)).then(null, function(e) { michael@0: ERROR(e); michael@0: // We should ensure this has been called, and calling it again doesn't hurt: michael@0: window.PanelUI.endBatchUpdate(); michael@0: this._handler.isExitingCustomizeMode = false; michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * The customize mode transition has 3 phases when entering: michael@0: * 1) Pre-customization mode michael@0: * This is the starting phase of the browser. michael@0: * 2) customize-entering michael@0: * This phase is a transition, optimized for smoothness. michael@0: * 3) customize-entered michael@0: * After the transition completes, this phase draws all of the michael@0: * expensive detail that isn't necessary during the second phase. michael@0: * michael@0: * Exiting customization mode has a similar set of phases, but in reverse michael@0: * order - customize-entered, customize-exiting, pre-customization mode. michael@0: * michael@0: * When in the customize-entering, customize-entered, or customize-exiting michael@0: * phases, there is a "customizing" attribute set on the main-window to simplify michael@0: * excluding certain styles while in any phase of customize mode. michael@0: */ michael@0: _doTransition: function(aEntering) { michael@0: let deferred = Promise.defer(); michael@0: let deck = this.document.getElementById("content-deck"); michael@0: michael@0: let customizeTransitionEnd = function(aEvent) { michael@0: if (aEvent != "timedout" && michael@0: (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) { michael@0: return; michael@0: } michael@0: this.window.clearTimeout(catchAllTimeout); michael@0: // Bug 962677: We let the event loop breathe for before we do the final michael@0: // stage of the transition to improve perceived performance. michael@0: this.window.setTimeout(function () { michael@0: deck.removeEventListener("transitionend", customizeTransitionEnd); michael@0: michael@0: if (!aEntering) { michael@0: this.document.documentElement.removeAttribute("customize-exiting"); michael@0: this.document.documentElement.removeAttribute("customizing"); michael@0: } else { michael@0: this.document.documentElement.setAttribute("customize-entered", true); michael@0: this.document.documentElement.removeAttribute("customize-entering"); michael@0: } michael@0: CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window); michael@0: michael@0: deferred.resolve(); michael@0: }.bind(this), 0); michael@0: }.bind(this); michael@0: deck.addEventListener("transitionend", customizeTransitionEnd); michael@0: michael@0: if (gDisableAnimation) { michael@0: this.document.getElementById("tab-view-deck").setAttribute("fastcustomizeanimation", true); michael@0: } michael@0: if (aEntering) { michael@0: this.document.documentElement.setAttribute("customizing", true); michael@0: this.document.documentElement.setAttribute("customize-entering", true); michael@0: } else { michael@0: this.document.documentElement.setAttribute("customize-exiting", true); michael@0: this.document.documentElement.removeAttribute("customize-entered"); michael@0: } michael@0: michael@0: let catchAll = () => customizeTransitionEnd("timedout"); michael@0: let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: maybeShowTip: function(aAnchor) { michael@0: let shown = false; michael@0: const kShownPref = "browser.customizemode.tip0.shown"; michael@0: try { michael@0: shown = Services.prefs.getBoolPref(kShownPref); michael@0: } catch (ex) {} michael@0: if (shown) michael@0: return; michael@0: michael@0: let anchorNode = aAnchor || this.document.getElementById("customization-panelHolder"); michael@0: let messageNode = this.tipPanel.querySelector(".customization-tipPanel-contentMessage"); michael@0: if (!messageNode.childElementCount) { michael@0: // Put the tip contents in the popup. michael@0: let bundle = this.document.getElementById("bundle_browser"); michael@0: const kLabelClass = "customization-tipPanel-link"; michael@0: messageNode.innerHTML = bundle.getFormattedString("customizeTips.tip0", [ michael@0: "