1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // This file defines these globals on the window object.
6 // Define them here so that ESLint can find them:
7 /* globals MozXULElement, MozHTMLElement, MozElements */
11 // This is loaded into chrome windows with the subscript loader. Wrap in
12 // a block to prevent accidentally leaking globals onto `window`.
14 // Handle customElements.js being loaded as a script in addition to the subscriptLoader
15 // from MainProcessSingleton, to handle pages that can open both before and after
16 // MainProcessSingleton starts. See Bug 1501845.
17 if (window.MozXULElement) {
21 const MozElements = {};
22 window.MozElements = MozElements;
24 const { AppConstants } = ChromeUtils.importESModule(
25 "resource://gre/modules/AppConstants.sys.mjs"
27 const instrumentClasses = Services.env.get("MOZ_INSTRUMENT_CUSTOM_ELEMENTS");
28 const instrumentedClasses = instrumentClasses ? new Set() : null;
29 const instrumentedBaseClasses = instrumentClasses ? new WeakSet() : null;
31 // If requested, wrap the normal customElements.define to give us a chance
32 // to modify the class so we can instrument function calls in local development:
33 if (instrumentClasses) {
34 let define = window.customElements.define;
35 window.customElements.define = function (name, c, opts) {
36 instrumentCustomElementClass(c);
37 return define.call(this, name, c, opts);
39 window.addEventListener(
42 MozElements.printInstrumentation(true);
44 { once: true, capture: true }
48 MozElements.printInstrumentation = function (collapsed) {
52 for (let c of instrumentedClasses) {
53 // Allow passing in something like MOZ_INSTRUMENT_CUSTOM_ELEMENTS=MozXULElement,Button to filter
55 instrumentClasses == 1 ||
58 .some(n => c.name.toLowerCase().includes(n.toLowerCase()));
59 let summary = c.__instrumentation_summary;
60 if (includeClass && summary) {
61 summaries.push(summary);
62 totalCalls += summary.totalCalls;
63 totalTime += summary.totalTime;
66 if (summaries.length) {
67 let groupName = `Instrumentation data for custom elements in ${document.documentURI}`;
68 console[collapsed ? "groupCollapsed" : "group"](groupName);
70 `Total function calls ${totalCalls} and total time spent inside ${totalTime.toFixed(
74 for (let summary of summaries) {
75 console.log(`${summary.name} (# instances: ${summary.instances})`);
76 if (Object.keys(summary.data).length > 1) {
77 console.table(summary.data);
80 console.groupEnd(groupName);
84 function instrumentCustomElementClass(c) {
85 // Climb up prototype chain to see if we inherit from a MozElement.
86 // Keep track of classes to instrument, for example:
87 // MozMenuCaption->MozMenuBase->BaseText->BaseControl->MozXULElement
88 let inheritsFromBase = instrumentedBaseClasses.has(c);
89 let classesToInstrument = [c];
90 let proto = Object.getPrototypeOf(c);
92 classesToInstrument.push(proto);
93 if (instrumentedBaseClasses.has(proto)) {
94 inheritsFromBase = true;
97 proto = Object.getPrototypeOf(proto);
100 if (inheritsFromBase) {
101 for (let c of classesToInstrument.reverse()) {
102 instrumentIndividualClass(c);
107 function instrumentIndividualClass(c) {
108 if (instrumentedClasses.has(c)) {
112 instrumentedClasses.add(c);
113 let data = { instances: 0 };
115 function wrapFunction(name, fn) {
118 data[name] = { time: 0, calls: 0 };
121 let n = performance.now();
122 let r = fn.apply(this, arguments);
123 data[name].time += performance.now() - n;
127 function wrapPropertyDescriptor(obj, name) {
128 if (name == "constructor") {
131 let prop = Object.getOwnPropertyDescriptor(obj, name);
133 prop.get = wrapFunction(`<get> ${name}`, prop.get);
136 prop.set = wrapFunction(`<set> ${name}`, prop.set);
138 if (prop.writable && prop.value && prop.value.apply) {
139 prop.value = wrapFunction(name, prop.value);
141 Object.defineProperty(obj, name, prop);
144 // Handle static properties
145 for (let name of Object.getOwnPropertyNames(c)) {
146 wrapPropertyDescriptor(c, name);
149 // Handle instance properties
150 for (let name of Object.getOwnPropertyNames(c.prototype)) {
151 wrapPropertyDescriptor(c.prototype, name);
154 c.__instrumentation_data = data;
155 Object.defineProperty(c, "__instrumentation_summary", {
159 if (data.instances == 0) {
163 let clonedData = JSON.parse(JSON.stringify(data));
164 delete clonedData.instances;
167 for (let d in clonedData) {
168 let { time, calls } = clonedData[d];
169 time = parseFloat(time.toFixed(2));
172 clonedData[d]["time (ms)"] = time;
173 delete clonedData[d].time;
174 clonedData[d].timePerCall = parseFloat((time / calls).toFixed(4));
177 let timePerCall = parseFloat((totalTime / totalCalls).toFixed(4));
178 totalTime = parseFloat(totalTime.toFixed(2));
180 // Add a spaced-out final row with summed up totals
181 clonedData["\ntotals"] = {
182 "time (ms)": `\n${totalTime}`,
183 calls: `\n${totalCalls}`,
184 timePerCall: `\n${timePerCall}`,
187 instances: data.instances,
197 // The listener of DOMContentLoaded must be set on window, rather than
198 // document, because the window can go away before the event is fired.
199 // In that case, we don't want to initialize anything, otherwise we
200 // may be leaking things because they will never be destroyed after.
201 let gIsDOMContentLoaded = false;
202 const gElementsPendingConnection = new Set();
203 window.addEventListener(
206 gIsDOMContentLoaded = true;
207 for (let element of gElementsPendingConnection) {
209 if (element.isConnected) {
210 element.isRunningDelayedConnectedCallback = true;
211 element.connectedCallback();
216 element.isRunningDelayedConnectedCallback = false;
218 gElementsPendingConnection.clear();
220 { once: true, capture: true }
223 const gXULDOMParser = new DOMParser();
224 gXULDOMParser.forceEnableXULXBL();
226 MozElements.MozElementMixin = Base => {
227 let MozElementBase = class extends Base {
231 if (instrumentClasses) {
232 let proto = this.constructor;
233 while (proto && proto != Base) {
234 proto.__instrumentation_data.instances++;
235 proto = Object.getPrototypeOf(proto);
240 * A declarative way to wire up attribute inheritance and automatically generate
241 * the `observedAttributes` getter. For example, if you returned:
243 * ".foo": "bar,baz=bat"
246 * Then the base class will automatically return ["bar", "bat"] from `observedAttributes`,
247 * and set up an `attributeChangedCallback` to pass those attributes down onto an element
248 * matching the ".foo" selector.
250 * See the `inheritAttribute` function for more details on the attribute string format.
252 * @return {Object<string selector, string attributes>}
254 static get inheritedAttributes() {
258 static get flippedInheritedAttributes() {
259 // Have to be careful here, if a subclass overrides inheritedAttributes
260 // and its parent class is instantiated first, then reading
261 // this._flippedInheritedAttributes on the child class will return the
262 // computed value from the parent. We store it separately on each class
263 // to ensure everything works correctly when inheritedAttributes is
265 if (!this.hasOwnProperty("_flippedInheritedAttributes")) {
266 let { inheritedAttributes } = this;
267 if (!inheritedAttributes) {
268 this._flippedInheritedAttributes = null;
270 this._flippedInheritedAttributes = {};
271 for (let selector in inheritedAttributes) {
272 let attrRules = inheritedAttributes[selector].split(",");
273 for (let attrRule of attrRules) {
274 let attrName = attrRule;
275 let attrNewName = attrRule;
276 let split = attrName.split("=");
277 if (split.length == 2) {
279 attrNewName = split[0];
282 if (!this._flippedInheritedAttributes[attrName]) {
283 this._flippedInheritedAttributes[attrName] = [];
285 this._flippedInheritedAttributes[attrName].push([
294 return this._flippedInheritedAttributes;
297 * Generate this array based on `inheritedAttributes`, if any. A class is free to override
298 * this if it needs to do something more complex or wants to opt out of this behavior.
300 static get observedAttributes() {
301 return Object.keys(this.flippedInheritedAttributes || {});
305 * Provide default lifecycle callback for attribute changes that will inherit attributes
306 * based on the static `inheritedAttributes` Object. This can be overridden by callers.
308 attributeChangedCallback(name, oldValue, newValue) {
309 if (oldValue === newValue || !this.initializedAttributeInheritance) {
313 let list = this.constructor.flippedInheritedAttributes[name];
315 this.inheritAttribute(list, name);
320 * After setting content, calling this will cache the elements from selectors in the
321 * static `inheritedAttributes` Object. It'll also do an initial call to `this.inheritAttributes()`,
322 * so in the simple case, this is the only function you need to call.
324 * This should be called any time the children that are inheriting attributes changes. For instance,
325 * it's common in a connectedCallback to do something like:
327 * this.textContent = "";
328 * this.append(MozXULElement.parseXULToFragment(`<label />`))
329 * this.initializeAttributeInheritance();
332 initializeAttributeInheritance() {
333 let { flippedInheritedAttributes } = this.constructor;
334 if (!flippedInheritedAttributes) {
338 // Clear out any existing cached elements:
339 this._inheritedElements = null;
341 this.initializedAttributeInheritance = true;
342 for (let attr in flippedInheritedAttributes) {
343 if (this.hasAttribute(attr)) {
344 this.inheritAttribute(flippedInheritedAttributes[attr], attr);
350 * Implements attribute value inheritance by child elements.
352 * @param {array} list
353 * An array of (to-element-selector, to-attr) pairs.
354 * @param {string} attr
355 * An attribute to propagate.
357 inheritAttribute(list, attr) {
358 if (!this._inheritedElements) {
359 this._inheritedElements = {};
362 let hasAttr = this.hasAttribute(attr);
363 let attrValue = this.getAttribute(attr);
365 for (let [selector, newAttr] of list) {
366 if (!(selector in this._inheritedElements)) {
367 this._inheritedElements[selector] =
368 this.getElementForAttrInheritance(selector);
370 let el = this._inheritedElements[selector];
372 if (newAttr == "text") {
373 el.textContent = hasAttr ? attrValue : "";
374 } else if (hasAttr) {
375 el.setAttribute(newAttr, attrValue);
377 el.removeAttribute(newAttr);
384 * Used in setting up attribute inheritance. Takes a selector and returns
385 * an element for that selector from shadow DOM if there is a shadowRoot,
386 * or from the light DOM if not.
388 * Here's one problem this solves. ElementB extends ElementA which extends
389 * MozXULElement. ElementA has a shadowRoot. ElementB tries to inherit
390 * attributes in light DOM by calling `initializeAttributeInheritance`
391 * but that fails because it defaults to inheriting from the shadow DOM
392 * and not the light DOM. (See bug 1545824.)
394 * To solve this, ElementB can override `getElementForAttrInheritance` so
395 * it queries the light DOM for some selectors as needed. For example:
397 * class ElementA extends MozXULElement {
398 * static get inheritedAttributes() {
399 * return { ".one": "attr" };
403 * class ElementB extends customElements.get("elementa") {
404 * static get inheritedAttributes() {
405 * return Object.assign({}, super.inheritedAttributes(), {
409 * getElementForAttrInheritance(selector) {
410 * if (selector == ".two") {
411 * return this.querySelector(selector)
413 * return super.getElementForAttrInheritance(selector);
418 * @param {string} selector
419 * A selector used to query an element.
421 * @return {Element} The element found by the selector.
423 getElementForAttrInheritance(selector) {
424 let parent = this.shadowRoot || this;
425 return parent.querySelector(selector);
429 * Sometimes an element may not want to run connectedCallback logic during
430 * parse. This could be because we don't want to initialize the element before
431 * the element's contents have been fully parsed, or for performance reasons.
432 * If you'd like to opt-in to this, then add this to the beginning of your
433 * `connectedCallback` and `disconnectedCallback`:
435 * if (this.delayConnectedCallback()) { return }
437 * And this at the beginning of your `attributeChangedCallback`
439 * if (!this.isConnectedAndReady) { return; }
441 delayConnectedCallback() {
442 if (gIsDOMContentLoaded) {
445 gElementsPendingConnection.add(this);
449 get isConnectedAndReady() {
450 return gIsDOMContentLoaded && this.isConnected;
454 * Passes DOM events to the on_<event type> methods.
457 let methodName = "on_" + event.type;
458 if (methodName in this) {
459 this[methodName](event);
461 throw new Error("Unrecognized event: " + event.type);
466 * Used by custom elements for caching fragments. We now would be
467 * caching once per class while also supporting subclasses.
469 * If available, returns the cached fragment.
470 * Otherwise, creates it.
474 * class ElementA extends MozXULElement {
475 * static get markup() {
476 * return `<hbox class="example"`;
479 * connectedCallback() {
480 * this.appendChild(this.constructor.fragment);
484 * @return {importedNode} The imported node that has not been
485 * inserted into document tree.
487 static get fragment() {
488 if (!this.hasOwnProperty("_fragment")) {
489 let markup = this.markup;
491 this._fragment = MozXULElement.parseXULToFragment(
496 throw new Error("Markup is null");
499 return document.importNode(this._fragment, true);
503 * Allows eager deterministic construction of XUL elements with XBL attached, by
504 * parsing an element tree and returning a DOM fragment to be inserted in the
505 * document before any of the inner elements is referenced by JavaScript.
507 * This process is required instead of calling the createElement method directly
508 * because bindings get attached when:
510 * 1. the node gets a layout frame constructed, or
511 * 2. the node gets its JavaScript reflector created, if it's in the document,
513 * whichever happens first. The createElement method would return a JavaScript
514 * reflector, but the element wouldn't be in the document, so the node wouldn't
515 * get XBL attached. After that point, even if the node is inserted into a
516 * document, it won't get XBL attached until either the frame is constructed or
517 * the reflector is garbage collected and the element is touched again.
519 * @param {string} str
520 * String with the XML representation of XUL elements.
521 * @param {string[]} [entities]
522 * An array of DTD URLs containing entity definitions.
524 * @return {DocumentFragment} `DocumentFragment` instance containing
525 * the corresponding element tree, including element nodes
526 * but excluding any text node.
528 static parseXULToFragment(str, entities = []) {
529 let doc = gXULDOMParser.parseFromSafeString(
533 ? `<!DOCTYPE bindings [
534 ${entities.reduce((preamble, url, index) => {
537 `<!ENTITY % _dtd-${index} SYSTEM "${url}">
545 <box xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
546 xmlns:html="http://www.w3.org/1999/xhtml">
553 if (doc.documentElement.localName === "parsererror") {
554 throw new Error("not well-formed XML");
557 // The XUL/XBL parser is set to ignore all-whitespace nodes, whereas (X)HTML
558 // does not do this. Most XUL code assumes that the whitespace has been
559 // stripped out, so we simply remove all text nodes after using the parser.
560 let nodeIterator = doc.createNodeIterator(doc, NodeFilter.SHOW_TEXT);
561 let currentNode = nodeIterator.nextNode();
562 while (currentNode) {
563 // Remove whitespace-only nodes. Regex is taken from:
564 // https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace_in_the_DOM
565 if (!/[^\t\n\r ]/.test(currentNode.textContent)) {
566 currentNode.remove();
569 currentNode = nodeIterator.nextNode();
571 // We use a range here so that we don't access the inner DOM elements from
572 // JavaScript before they are imported and inserted into a document.
573 let range = doc.createRange();
574 range.selectNodeContents(doc.querySelector("box"));
575 return range.extractContents();
579 * Insert a localization link to an FTL file. This is used so that
580 * a Custom Element can wait to inject the link until it's connected,
581 * and so that consuming documents don't require the correct <link>
582 * present in the markup.
585 * The path to the FTL file
587 static insertFTLIfNeeded(path) {
588 let container = document.head || document.querySelector("linkset");
591 document.documentElement.namespaceURI ===
592 "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
594 container = document.createXULElement("linkset");
595 document.documentElement.appendChild(container);
596 } else if (document.documentURI == AppConstants.BROWSER_CHROME_URL) {
597 // Special case for browser.xhtml. Here `document.head` is null, so
598 // just insert the link at the end of the window.
599 container = document.documentElement;
602 "Attempt to inject localization link before document.head is available"
607 for (let link of container.querySelectorAll("link")) {
608 if (link.getAttribute("href") == path) {
613 let link = document.createElementNS(
614 "http://www.w3.org/1999/xhtml",
617 link.setAttribute("rel", "localization");
618 link.setAttribute("href", path);
620 container.appendChild(link);
624 * Indicate that a class defining a XUL element implements one or more
625 * XPCOM interfaces by adding a getCustomInterface implementation to it,
626 * as well as an implementation of QueryInterface.
628 * The supplied class should implement the properties and methods of
629 * all of the interfaces that are specified.
632 * The class that implements the interface.
634 * Array of interface names.
636 static implementCustomInterface(cls, ifaces) {
637 if (cls.prototype.customInterfaces) {
638 ifaces.push(...cls.prototype.customInterfaces);
640 cls.prototype.customInterfaces = ifaces;
642 cls.prototype.QueryInterface = ChromeUtils.generateQI(ifaces);
643 cls.prototype.getCustomInterfaceCallback =
644 function getCustomInterfaceCallback(ifaceToCheck) {
646 cls.prototype.customInterfaces.some(iface =>
647 iface.equals(ifaceToCheck)
650 return getInterfaceProxy(this);
657 // Rename the class so we can distinguish between MozXULElement and MozXULPopupElement, for example.
658 Object.defineProperty(MozElementBase, "name", { value: `Moz${Base.name}` });
659 if (instrumentedBaseClasses) {
660 instrumentedBaseClasses.add(MozElementBase);
662 return MozElementBase;
665 const MozXULElement = MozElements.MozElementMixin(XULElement);
666 const MozHTMLElement = MozElements.MozElementMixin(HTMLElement);
669 * Given an object, add a proxy that reflects interface implementations
670 * onto the object itself.
672 function getInterfaceProxy(obj) {
673 /* globals MozQueryInterface */
674 if (!obj._customInterfaceProxy) {
675 obj._customInterfaceProxy = new Proxy(obj, {
676 get(target, prop, receiver) {
677 let propOrMethod = target[prop];
678 if (typeof propOrMethod == "function") {
679 if (MozQueryInterface.isInstance(propOrMethod)) {
680 return Reflect.get(target, prop, receiver);
682 return function (...args) {
683 return propOrMethod.apply(target, args);
691 return obj._customInterfaceProxy;
694 MozElements.BaseControlMixin = Base => {
695 class BaseControl extends Base {
697 return this.getAttribute("disabled") == "true";
702 this.setAttribute("disabled", "true");
704 this.removeAttribute("disabled");
709 return parseInt(this.getAttribute("tabindex")) || 0;
714 this.setAttribute("tabindex", val);
716 this.removeAttribute("tabindex");
721 MozXULElement.implementCustomInterface(BaseControl, [
722 Ci.nsIDOMXULControlElement,
726 MozElements.BaseControl = MozElements.BaseControlMixin(MozXULElement);
728 const BaseTextMixin = Base =>
729 class BaseText extends MozElements.BaseControlMixin(Base) {
731 this.setAttribute("label", val);
735 return this.getAttribute("label") || "";
739 this.setAttribute("image", val);
743 return this.getAttribute("image");
747 this.setAttribute("command", val);
751 return this.getAttribute("command");
755 // Always store on the control
756 this.setAttribute("accesskey", val);
757 // If there is a label, change the accesskey on the labelElement
758 // if it's also set there
759 if (this.labelElement) {
760 this.labelElement.accessKey = val;
765 return this.labelElement?.accessKey || this.getAttribute("accesskey");
768 MozElements.BaseTextMixin = BaseTextMixin;
769 MozElements.BaseText = BaseTextMixin(MozXULElement);
771 // Attach the base class to the window so other scripts can use it:
772 window.MozXULElement = MozXULElement;
773 window.MozHTMLElement = MozHTMLElement;
775 customElements.setElementCreationCallback("browser", () => {
776 Services.scriptloader.loadSubScript(
777 "chrome://global/content/elements/browser-custom-element.js",
782 // Skip loading any extra custom elements in the extension dummy document
783 // and GeckoView windows.
784 const loadExtraCustomElements = !(
785 document.documentURI == "chrome://extensions/content/dummy.xhtml" ||
786 document.documentURI == "chrome://geckoview/content/geckoview.xhtml"
788 if (loadExtraCustomElements) {
789 // Lazily load the following elements
790 for (let [tag, script] of [
791 ["button-group", "chrome://global/content/elements/named-deck.js"],
792 ["findbar", "chrome://global/content/elements/findbar.js"],
793 ["menulist", "chrome://global/content/elements/menulist.js"],
794 ["message-bar", "chrome://global/content/elements/message-bar.js"],
795 ["named-deck", "chrome://global/content/elements/named-deck.js"],
796 ["named-deck-button", "chrome://global/content/elements/named-deck.js"],
797 ["panel-list", "chrome://global/content/elements/panel-list.js"],
798 ["search-textbox", "chrome://global/content/elements/search-textbox.js"],
799 ["stringbundle", "chrome://global/content/elements/stringbundle.js"],
801 "printpreview-pagination",
802 "chrome://global/content/printPreviewPagination.js",
805 "autocomplete-input",
806 "chrome://global/content/elements/autocomplete-input.js",
808 ["editor", "chrome://global/content/elements/editor.js"],
810 customElements.setElementCreationCallback(tag, () => {
811 Services.scriptloader.loadSubScript(script, window);
814 // Bug 1813077: This is a workaround until Bug 1803810 lands
815 // which will give us the ability to load ESMs synchronously
816 // like the previous Services.scriptloader.loadSubscript() function
817 function importCustomElementFromESModule(name) {
820 return import("chrome://global/content/elements/moz-button.mjs");
821 case "moz-button-group":
823 "chrome://global/content/elements/moz-button-group.mjs"
825 case "moz-message-bar":
826 return import("chrome://global/content/elements/moz-message-bar.mjs");
827 case "moz-support-link":
829 "chrome://global/content/elements/moz-support-link.mjs"
832 return import("chrome://global/content/elements/moz-toggle.mjs");
834 return import("chrome://global/content/elements/moz-card.mjs");
836 throw new Error(`Unknown custom element name (${name})`);
840 This function explicitly returns null so that there is no confusion
841 about which custom elements from ES Modules have been loaded.
843 window.ensureCustomElements = function (...elementNames) {
846 .filter(name => !customElements.get(name))
847 .map(name => importCustomElementFromESModule(name))
850 .catch(console.error);
853 // Immediately load the following elements
855 "chrome://global/content/elements/arrowscrollbox.js",
856 "chrome://global/content/elements/dialog.js",
857 "chrome://global/content/elements/general.js",
858 "chrome://global/content/elements/button.js",
859 "chrome://global/content/elements/checkbox.js",
860 "chrome://global/content/elements/menu.js",
861 "chrome://global/content/elements/menupopup.js",
862 "chrome://global/content/elements/moz-input-box.js",
863 "chrome://global/content/elements/notificationbox.js",
864 "chrome://global/content/elements/panel.js",
865 "chrome://global/content/elements/popupnotification.js",
866 "chrome://global/content/elements/radio.js",
867 "chrome://global/content/elements/richlistbox.js",
868 "chrome://global/content/elements/autocomplete-popup.js",
869 "chrome://global/content/elements/autocomplete-richlistitem.js",
870 "chrome://global/content/elements/tabbox.js",
871 "chrome://global/content/elements/text.js",
872 "chrome://global/content/elements/toolbarbutton.js",
873 "chrome://global/content/elements/tree.js",
874 "chrome://global/content/elements/wizard.js",
876 Services.scriptloader.loadSubScript(script, window);