/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // This file is a copy of a file with the same name in Firefox. Only the // pieces we're using, and a few pieces the devtools rely on such as the // constants, remain. const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs", }); /** * gPanelsForWindow is a list of known panels in a window which we may need to close * should command events fire which target them. */ var gPanelsForWindow = new WeakMap(); var CustomizableUIInternal = { addPanelCloseListeners(aPanel) { Services.els.addSystemEventListener(aPanel, "click", this, false); Services.els.addSystemEventListener(aPanel, "keypress", this, false); let win = aPanel.ownerGlobal; if (!gPanelsForWindow.has(win)) { gPanelsForWindow.set(win, new Set()); } gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel)); }, removePanelCloseListeners(aPanel) { Services.els.removeSystemEventListener(aPanel, "click", this, false); Services.els.removeSystemEventListener(aPanel, "keypress", this, false); let win = aPanel.ownerGlobal; let panels = gPanelsForWindow.get(win); if (panels) { panels.delete(this._getPanelForNode(aPanel)); } }, handleEvent(aEvent) { switch (aEvent.type) { case "click": case "keypress": this.maybeAutoHidePanel(aEvent); break; } }, _getPanelForNode(aNode) { return aNode.closest("panel"); }, /* * If people put things in the panel which need more than single-click interaction, * we don't want to close it. Right now we check for text inputs and menu buttons. * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank * part of the menu. */ _isOnInteractiveElement(aEvent) { function getMenuPopupForDescendant(aNode) { let lastPopup = null; while ( aNode && aNode.parentNode && aNode.parentNode.localName.startsWith("menu") ) { lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup; aNode = aNode.parentNode; } return lastPopup; } let target = aEvent.target; let panel = this._getPanelForNode(aEvent.currentTarget); // This can happen in e.g. customize mode. If there's no panel, // there's clearly nothing for us to close; pretend we're interactive. if (!panel) { return true; } // We keep track of: // whether we're in an input container (text field) let inInput = false; // whether we're in a popup/context menu let inMenu = false; // whether we're in a toolbarbutton/toolbaritem let inItem = false; // whether the current menuitem has a valid closemenu attribute let menuitemCloseMenu = "auto"; // While keeping track of that, we go from the original target back up, // to the panel if we have to. We bail as soon as we find an input, // a toolbarbutton/item, or the panel: while (target) { // Skip out of iframes etc: if (target.nodeType == target.DOCUMENT_NODE) { if (!target.defaultView) { // Err, we're done. break; } // Find containing browser or iframe element in the parent doc. target = target.defaultView.docShell.chromeEventHandler; if (!target) { break; } } let tagName = target.localName; inInput = tagName == "input"; inItem = tagName == "toolbaritem" || tagName == "toolbarbutton"; let isMenuItem = tagName == "menuitem"; inMenu = inMenu || isMenuItem; if (isMenuItem && target.hasAttribute("closemenu")) { let closemenuVal = target.getAttribute("closemenu"); menuitemCloseMenu = closemenuVal == "single" || closemenuVal == "none" ? closemenuVal : "auto"; } // Keep the menu open and break out of the loop if the click happened on // the ShadowRoot or a disabled menu item. if ( target.nodeType == target.DOCUMENT_FRAGMENT_NODE || target.getAttribute("disabled") == "true" ) { return true; } // This isn't in the loop condition because we want to break before // changing |target| if any of these conditions are true if (inInput || inItem || target == panel) { break; } // We need specific code for popups: the item on which they were invoked // isn't necessarily in their parentNode chain: if (isMenuItem) { let topmostMenuPopup = getMenuPopupForDescendant(target); target = (topmostMenuPopup && topmostMenuPopup.triggerNode) || target.parentNode; } else { target = target.parentNode; } } // If the user clicked a menu item... if (inMenu) { // We care if we're in an input also, // or if the user specified closemenu!="auto": if (inInput || menuitemCloseMenu != "auto") { return true; } // Otherwise, we're probably fine to close the panel return false; } // If we're not in a menu, and we *are* in a type="menu" toolbarbutton, // we'll now interact with the menu if (inItem && target.getAttribute("type") == "menu") { return true; } return inInput || !inItem; }, hidePanelForNode(aNode) { let panel = this._getPanelForNode(aNode); if (panel) { lazy.PanelMultiView.hidePopup(panel); } }, maybeAutoHidePanel(aEvent) { let eventType = aEvent.type; if (eventType == "keypress" && aEvent.keyCode != aEvent.DOM_VK_RETURN) { return; } if (eventType == "click" && aEvent.button != 0) { return; } // We don't check preventDefault - it makes sense that this was prevented, // but we probably still want to close the panel. If consumers don't want // this to happen, they should specify the closemenu attribute. if (eventType != "command" && this._isOnInteractiveElement(aEvent)) { return; } // We can't use event.target because we might have passed an anonymous // content boundary as well, and so target points to the outer element in // that case. Unfortunately, this means we get anonymous child nodes instead // of the real ones, so looking for the 'stoooop, don't close me' attributes // is more involved. let target = aEvent.originalTarget; while (target.parentNode && target.localName != "panel") { if ( target.getAttribute("closemenu") == "none" || target.getAttribute("widget-type") == "view" || target.getAttribute("widget-type") == "button-and-view" ) { return; } target = target.parentNode; } // If we get here, we can actually hide the popup: this.hidePanelForNode(aEvent.target); }, }; Object.freeze(CustomizableUIInternal); export var CustomizableUI = { /** * Constant reference to the ID of the navigation toolbar. */ AREA_NAVBAR: "nav-bar", /** * Constant reference to the ID of the menubar's toolbar. */ AREA_MENUBAR: "toolbar-menubar", /** * Constant reference to the ID of the tabstrip toolbar. */ AREA_TABSTRIP: "TabsToolbar", /** * Constant reference to the ID of the bookmarks toolbar. */ AREA_BOOKMARKS: "PersonalToolbar", /** * Constant reference to the ID of the non-dymanic (fixed) list in the overflow panel. */ AREA_FIXED_OVERFLOW_PANEL: "widget-overflow-fixed-list", /** * Constant indicating the area is a menu panel. */ TYPE_MENU_PANEL: "menu-panel", /** * Constant indicating the area is a toolbar. */ TYPE_TOOLBAR: "toolbar", /** * Constant indicating a XUL-type provider. */ PROVIDER_XUL: "xul", /** * Constant indicating an API-type provider. */ PROVIDER_API: "api", /** * Constant indicating dynamic (special) widgets: spring, spacer, and separator. */ PROVIDER_SPECIAL: "special", /** * Constant indicating the widget is built-in */ SOURCE_BUILTIN: "builtin", /** * Constant indicating the widget is externally provided * (e.g. by add-ons or other items not part of the builtin widget set). */ SOURCE_EXTERNAL: "external", /** * Constant indicating the reason the event was fired was a window closing */ REASON_WINDOW_CLOSED: "window-closed", /** * Constant indicating the reason the event was fired was an area being * unregistered separately from window closing mechanics. */ REASON_AREA_UNREGISTERED: "area-unregistered", /** * Add a widget to an area. * If the area to which you try to add is not known to CustomizableUI, * this will throw. * If the area to which you try to add is the same as the area in which * the widget is currently placed, this will do the same as * moveWidgetWithinArea. * If the widget cannot be removed from its original location, this will * no-op. * * This will fire an onWidgetAdded notification, * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification * for each window CustomizableUI knows about. * * @param aWidgetId the ID of the widget to add * @param aArea the ID of the area to add the widget to * @param aPosition the position at which to add the widget. If you do not * pass a position, the widget will be added to the end * of the area. */ addWidgetToArea(aWidgetId, aArea, aPosition) {}, /** * Remove a widget from its area. If the widget cannot be removed from its * area, or is not in any area, this will no-op. Otherwise, this will fire an * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and * onWidgetAfterDOMChange notification for each window CustomizableUI knows * about. * * @param aWidgetId the ID of the widget to remove */ removeWidgetFromArea(aWidgetId) {}, /** * Get the placement of a widget. This is by far the best way to obtain * information about what the state of your widget is. The internals of * this call are cheap (no DOM necessary) and you will know where the user * has put your widget. * * @param aWidgetId the ID of the widget whose placement you want to know * @returns * { * area: "somearea", // The ID of the area where the widget is placed * position: 42 // the index in the placements array corresponding to * // your widget. * } * * OR * * null // if the widget is not placed anywhere (ie in the palette) */ getPlacementOfWidget(aWidgetId, aOnlyRegistered = true, aDeadAreas = false) { return null; }, /** * Add listeners to a panel that will close it. For use from the menu panel * and overflowable toolbar implementations, unlikely to be useful for * consumers. * * @param aPanel the panel to which listeners should be attached. */ addPanelCloseListeners(aPanel) { CustomizableUIInternal.addPanelCloseListeners(aPanel); }, /** * Remove close listeners that have been added to a panel with * addPanelCloseListeners. For use from the menu panel and overflowable * toolbar implementations, unlikely to be useful for consumers. * * @param aPanel the panel from which listeners should be removed. */ removePanelCloseListeners(aPanel) { CustomizableUIInternal.removePanelCloseListeners(aPanel); }, /** * Notify toolbox(es) of a particular event. If you don't pass aWindow, * all toolboxes will be notified. For use from Customize Mode only, * do not use otherwise. * * @param aEvent the name of the event to send. * @param aDetails optional, the details of the event. * @param aWindow optional, the window in which to send the event. */ dispatchToolboxEvent(aEvent, aDetails = {}, aWindow = null) {}, }; Object.freeze(CustomizableUI);