/* 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 ``; } 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); }