diff options
Diffstat (limited to 'comm/mail/components/customizableui/CustomizableUI.sys.mjs')
-rw-r--r-- | comm/mail/components/customizableui/CustomizableUI.sys.mjs | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/comm/mail/components/customizableui/CustomizableUI.sys.mjs b/comm/mail/components/customizableui/CustomizableUI.sys.mjs new file mode 100644 index 0000000000..2628bd6109 --- /dev/null +++ b/comm/mail/components/customizableui/CustomizableUI.sys.mjs @@ -0,0 +1,360 @@ +/* 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); |