diff options
Diffstat (limited to 'toolkit/content/widgets/panel.js')
-rw-r--r-- | toolkit/content/widgets/panel.js | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/toolkit/content/widgets/panel.js b/toolkit/content/widgets/panel.js new file mode 100644 index 0000000000..a301ecb1f2 --- /dev/null +++ b/toolkit/content/widgets/panel.js @@ -0,0 +1,293 @@ +/* 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/. */ + +"use strict"; + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + class MozPanel extends MozElements.MozElementMixin(XULPopupElement) { + static get markup() { + return `<html:slot part="content" style="display: none !important"/>`; + } + constructor() { + super(); + + this._prevFocus = 0; + this._fadeTimer = null; + + this.attachShadow({ mode: "open" }); + + this.addEventListener("popupshowing", this); + this.addEventListener("popupshown", this); + this.addEventListener("popuphiding", this); + this.addEventListener("popuphidden", this); + this.addEventListener("popuppositioned", this); + } + + connectedCallback() { + // Create shadow DOM lazily if a panel is hidden. It helps to reduce + // cycles on startup. + if (!this.hidden) { + this.ensureInitialized(); + } + + if (this.isArrowPanel) { + if (!this.hasAttribute("flip")) { + this.setAttribute("flip", "both"); + } + if (!this.hasAttribute("side")) { + this.setAttribute("side", "top"); + } + if (!this.hasAttribute("position")) { + this.setAttribute("position", "bottomleft topleft"); + } + if (!this.hasAttribute("consumeoutsideclicks")) { + this.setAttribute("consumeoutsideclicks", "false"); + } + } + } + + ensureInitialized() { + // As an optimization, we don't slot contents if the panel is [hidden] in + // connectedCallback this means we can avoid running this code at startup + // and only need to do it when a panel is about to be shown. We then + // override the `hidden` setter and `removeAttribute` and call this + // function if the node is about to be shown. + if (this.shadowRoot.firstChild) { + return; + } + + this.shadowRoot.appendChild(this.constructor.fragment); + if (this.hasAttribute("neverhidden")) { + this.panelContent.style.display = ""; + } + } + + get panelContent() { + return this.shadowRoot.querySelector("[part=content]"); + } + + get hidden() { + return super.hidden; + } + + set hidden(v) { + if (!v) { + this.ensureInitialized(); + } + super.hidden = v; + } + + removeAttribute(name) { + if (name == "hidden") { + this.ensureInitialized(); + } + super.removeAttribute(name); + } + + get isArrowPanel() { + return this.getAttribute("type") == "arrow"; + } + + get noOpenOnAnchor() { + return this.hasAttribute("no-open-on-anchor"); + } + + _setSideAttribute(event) { + if (!this.isArrowPanel || !event.isAnchored) { + return; + } + + let position = event.alignmentPosition; + if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) { + // The assigned side stays the same regardless of direction. + let isRTL = window.getComputedStyle(this).direction == "rtl"; + + if (position.indexOf("start_") == 0) { + this.setAttribute("side", isRTL ? "left" : "right"); + } else { + this.setAttribute("side", isRTL ? "right" : "left"); + } + } else if ( + position.indexOf("before_") == 0 || + position.indexOf("after_") == 0 + ) { + if (position.indexOf("before_") == 0) { + this.setAttribute("side", "bottom"); + } else { + this.setAttribute("side", "top"); + } + } + + // This method isn't implemented by panel.js, but it can be added to + // individual instances that need to show an arrow. + this.setArrowPosition?.(event); + } + + on_popupshowing(event) { + if (event.target == this) { + this.panelContent.style.display = ""; + } + if (this.isArrowPanel && event.target == this) { + if (this.anchorNode && !this.noOpenOnAnchor) { + let anchorRoot = + this.anchorNode.closest("toolbarbutton, .anchor-root") || + this.anchorNode; + anchorRoot.setAttribute("open", "true"); + } + + if (this.getAttribute("animate") != "false") { + this.setAttribute("animate", "open"); + // the animating attribute prevents user interaction during transition + // it is removed when popupshown fires + this.setAttribute("animating", "true"); + } + + // set fading + var fade = this.getAttribute("fade"); + var fadeDelay = 0; + if (fade == "fast") { + fadeDelay = 1; + } else if (fade == "slow") { + fadeDelay = 4000; + } + + if (fadeDelay != 0) { + this._fadeTimer = setTimeout( + () => this.hidePopup(true), + fadeDelay, + this + ); + } + } + + // Capture the previous focus before has a chance to get set inside the panel + try { + this._prevFocus = Cu.getWeakReference( + document.commandDispatcher.focusedElement + ); + if (!this._prevFocus.get()) { + this._prevFocus = Cu.getWeakReference(document.activeElement); + } + } catch (ex) { + this._prevFocus = Cu.getWeakReference(document.activeElement); + } + } + + on_popupshown(event) { + if (this.isArrowPanel && event.target == this) { + this.removeAttribute("animating"); + this.setAttribute("panelopen", "true"); + } + + // Fire event for accessibility APIs + let alertEvent = document.createEvent("Events"); + alertEvent.initEvent("AlertActive", true, true); + this.dispatchEvent(alertEvent); + } + + on_popuphiding(event) { + if (this.isArrowPanel && event.target == this) { + let animate = this.getAttribute("animate") != "false"; + + if (this._fadeTimer) { + clearTimeout(this._fadeTimer); + if (animate) { + this.setAttribute("animate", "fade"); + } + } else if (animate) { + this.setAttribute("animate", "cancel"); + } + + if (this.anchorNode && !this.noOpenOnAnchor) { + let anchorRoot = + this.anchorNode.closest("toolbarbutton, .anchor-root") || + this.anchorNode; + anchorRoot.removeAttribute("open"); + } + } + + try { + this._currentFocus = document.commandDispatcher.focusedElement; + } catch (e) { + this._currentFocus = document.activeElement; + } + } + + on_popuphidden(event) { + if (event.target == this && !this.hasAttribute("neverhidden")) { + this.panelContent.style.setProperty("display", "none", "important"); + } + if (this.isArrowPanel && event.target == this) { + this.removeAttribute("panelopen"); + if (this.getAttribute("animate") != "false") { + this.removeAttribute("animate"); + } + } + + function doFocus() { + // Focus was set on an element inside this panel, + // so we need to move it back to where it was previously. + // Elements can use refocused-by-panel to change their focus behaviour + // when re-focused by a panel hiding. + prevFocus.setAttribute("refocused-by-panel", true); + try { + let fm = Services.focus; + fm.setFocus(prevFocus, fm.FLAG_NOSCROLL); + } catch (e) { + prevFocus.focus(); + } + prevFocus.removeAttribute("refocused-by-panel"); + } + var currentFocus = this._currentFocus; + var prevFocus = this._prevFocus ? this._prevFocus.get() : null; + this._currentFocus = null; + this._prevFocus = null; + + // Avoid changing focus if focus changed while we hide the popup + // (This can happen e.g. if the popup is hiding as a result of a + // click/keypress that focused something) + let nowFocus; + try { + nowFocus = document.commandDispatcher.focusedElement; + } catch (e) { + nowFocus = document.activeElement; + } + if (nowFocus && nowFocus != currentFocus) { + return; + } + + if (prevFocus && this.getAttribute("norestorefocus") != "true") { + // Try to restore focus + try { + if (document.commandDispatcher.focusedWindow != window) { + // Focus has already been set to a window outside of this panel + return; + } + } catch (ex) {} + + if (!currentFocus) { + doFocus(); + return; + } + while (currentFocus) { + if (currentFocus == this) { + doFocus(); + return; + } + currentFocus = currentFocus.parentNode; + } + } + } + + on_popuppositioned(event) { + if (event.target == this) { + this._setSideAttribute(event); + } + } + } + + customElements.define("panel", MozPanel); +} |