Backed out changeset bcbab342eed8 (bug 1889658) for causing wpt reftest failures...
[gecko.git] / toolkit / content / customElements.js
blobef58963a027ce3ed58102ce5020a642708b0c928
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 */
9 "use strict";
11 // This is loaded into chrome windows with the subscript loader. Wrap in
12 // a block to prevent accidentally leaking globals onto `window`.
13 (() => {
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) {
18     return;
19   }
21   const MozElements = {};
22   window.MozElements = MozElements;
24   const { AppConstants } = ChromeUtils.importESModule(
25     "resource://gre/modules/AppConstants.sys.mjs"
26   );
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);
38     };
39     window.addEventListener(
40       "load",
41       () => {
42         MozElements.printInstrumentation(true);
43       },
44       { once: true, capture: true }
45     );
46   }
48   MozElements.printInstrumentation = function (collapsed) {
49     let summaries = [];
50     let totalCalls = 0;
51     let totalTime = 0;
52     for (let c of instrumentedClasses) {
53       // Allow passing in something like MOZ_INSTRUMENT_CUSTOM_ELEMENTS=MozXULElement,Button to filter
54       let includeClass =
55         instrumentClasses == 1 ||
56         instrumentClasses
57           .split(",")
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;
64       }
65     }
66     if (summaries.length) {
67       let groupName = `Instrumentation data for custom elements in ${document.documentURI}`;
68       console[collapsed ? "groupCollapsed" : "group"](groupName);
69       console.log(
70         `Total function calls ${totalCalls} and total time spent inside ${totalTime.toFixed(
71           2
72         )}`
73       );
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);
78         }
79       }
80       console.groupEnd(groupName);
81     }
82   };
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);
91     while (proto) {
92       classesToInstrument.push(proto);
93       if (instrumentedBaseClasses.has(proto)) {
94         inheritsFromBase = true;
95         break;
96       }
97       proto = Object.getPrototypeOf(proto);
98     }
100     if (inheritsFromBase) {
101       for (let c of classesToInstrument.reverse()) {
102         instrumentIndividualClass(c);
103       }
104     }
105   }
107   function instrumentIndividualClass(c) {
108     if (instrumentedClasses.has(c)) {
109       return;
110     }
112     instrumentedClasses.add(c);
113     let data = { instances: 0 };
115     function wrapFunction(name, fn) {
116       return function () {
117         if (!data[name]) {
118           data[name] = { time: 0, calls: 0 };
119         }
120         data[name].calls++;
121         let n = performance.now();
122         let r = fn.apply(this, arguments);
123         data[name].time += performance.now() - n;
124         return r;
125       };
126     }
127     function wrapPropertyDescriptor(obj, name) {
128       if (name == "constructor") {
129         return;
130       }
131       let prop = Object.getOwnPropertyDescriptor(obj, name);
132       if (prop.get) {
133         prop.get = wrapFunction(`<get> ${name}`, prop.get);
134       }
135       if (prop.set) {
136         prop.set = wrapFunction(`<set> ${name}`, prop.set);
137       }
138       if (prop.writable && prop.value && prop.value.apply) {
139         prop.value = wrapFunction(name, prop.value);
140       }
141       Object.defineProperty(obj, name, prop);
142     }
144     // Handle static properties
145     for (let name of Object.getOwnPropertyNames(c)) {
146       wrapPropertyDescriptor(c, name);
147     }
149     // Handle instance properties
150     for (let name of Object.getOwnPropertyNames(c.prototype)) {
151       wrapPropertyDescriptor(c.prototype, name);
152     }
154     c.__instrumentation_data = data;
155     Object.defineProperty(c, "__instrumentation_summary", {
156       enumerable: false,
157       configurable: false,
158       get() {
159         if (data.instances == 0) {
160           return null;
161         }
163         let clonedData = JSON.parse(JSON.stringify(data));
164         delete clonedData.instances;
165         let totalCalls = 0;
166         let totalTime = 0;
167         for (let d in clonedData) {
168           let { time, calls } = clonedData[d];
169           time = parseFloat(time.toFixed(2));
170           totalCalls += calls;
171           totalTime += time;
172           clonedData[d]["time (ms)"] = time;
173           delete clonedData[d].time;
174           clonedData[d].timePerCall = parseFloat((time / calls).toFixed(4));
175         }
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}`,
185         };
186         return {
187           instances: data.instances,
188           data: clonedData,
189           name: c.name,
190           totalCalls,
191           totalTime,
192         };
193       },
194     });
195   }
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(
204     "DOMContentLoaded",
205     () => {
206       gIsDOMContentLoaded = true;
207       for (let element of gElementsPendingConnection) {
208         try {
209           if (element.isConnected) {
210             element.isRunningDelayedConnectedCallback = true;
211             element.connectedCallback();
212           }
213         } catch (ex) {
214           console.error(ex);
215         }
216         element.isRunningDelayedConnectedCallback = false;
217       }
218       gElementsPendingConnection.clear();
219     },
220     { once: true, capture: true }
221   );
223   const gXULDOMParser = new DOMParser();
224   gXULDOMParser.forceEnableXULXBL();
226   MozElements.MozElementMixin = Base => {
227     let MozElementBase = class extends Base {
228       constructor() {
229         super();
231         if (instrumentClasses) {
232           let proto = this.constructor;
233           while (proto && proto != Base) {
234             proto.__instrumentation_data.instances++;
235             proto = Object.getPrototypeOf(proto);
236           }
237         }
238       }
239       /*
240        * A declarative way to wire up attribute inheritance and automatically generate
241        * the `observedAttributes` getter.  For example, if you returned:
242        *    {
243        *      ".foo": "bar,baz=bat"
244        *    }
245        *
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.
249        *
250        * See the `inheritAttribute` function for more details on the attribute string format.
251        *
252        * @return {Object<string selector, string attributes>}
253        */
254       static get inheritedAttributes() {
255         return null;
256       }
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
264         // overridden.
265         if (!this.hasOwnProperty("_flippedInheritedAttributes")) {
266           let { inheritedAttributes } = this;
267           if (!inheritedAttributes) {
268             this._flippedInheritedAttributes = null;
269           } else {
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) {
278                   attrName = split[1];
279                   attrNewName = split[0];
280                 }
282                 if (!this._flippedInheritedAttributes[attrName]) {
283                   this._flippedInheritedAttributes[attrName] = [];
284                 }
285                 this._flippedInheritedAttributes[attrName].push([
286                   selector,
287                   attrNewName,
288                 ]);
289               }
290             }
291           }
292         }
294         return this._flippedInheritedAttributes;
295       }
296       /*
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.
299        */
300       static get observedAttributes() {
301         return Object.keys(this.flippedInheritedAttributes || {});
302       }
304       /*
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.
307        */
308       attributeChangedCallback(name, oldValue, newValue) {
309         if (oldValue === newValue || !this.initializedAttributeInheritance) {
310           return;
311         }
313         let list = this.constructor.flippedInheritedAttributes[name];
314         if (list) {
315           this.inheritAttribute(list, name);
316         }
317       }
319       /*
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.
323        *
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:
326        *
327        *   this.textContent = "";
328        *   this.append(MozXULElement.parseXULToFragment(`<label />`))
329        *   this.initializeAttributeInheritance();
330        *
331        */
332       initializeAttributeInheritance() {
333         let { flippedInheritedAttributes } = this.constructor;
334         if (!flippedInheritedAttributes) {
335           return;
336         }
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);
345           }
346         }
347       }
349       /*
350        * Implements attribute value inheritance by child elements.
351        *
352        * @param {array} list
353        *        An array of (to-element-selector, to-attr) pairs.
354        * @param {string} attr
355        *        An attribute to propagate.
356        */
357       inheritAttribute(list, attr) {
358         if (!this._inheritedElements) {
359           this._inheritedElements = {};
360         }
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);
369           }
370           let el = this._inheritedElements[selector];
371           if (el) {
372             if (newAttr == "text") {
373               el.textContent = hasAttr ? attrValue : "";
374             } else if (hasAttr) {
375               el.setAttribute(newAttr, attrValue);
376             } else {
377               el.removeAttribute(newAttr);
378             }
379           }
380         }
381       }
383       /**
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.
387        *
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.)
393        *
394        * To solve this, ElementB can override `getElementForAttrInheritance` so
395        * it queries the light DOM for some selectors as needed. For example:
396        *
397        *  class ElementA extends MozXULElement {
398        *    static get inheritedAttributes() {
399        *      return { ".one": "attr" };
400        *    }
401        *  }
402        *
403        *  class ElementB extends customElements.get("elementa") {
404        *    static get inheritedAttributes() {
405        *      return Object.assign({}, super.inheritedAttributes(), {
406        *        ".two": "attr",
407        *      });
408        *    }
409        *    getElementForAttrInheritance(selector) {
410        *      if (selector == ".two") {
411        *        return this.querySelector(selector)
412        *      } else {
413        *        return super.getElementForAttrInheritance(selector);
414        *      }
415        *    }
416        *  }
417        *
418        * @param {string} selector
419        *        A selector used to query an element.
420        *
421        * @return {Element} The element found by the selector.
422        */
423       getElementForAttrInheritance(selector) {
424         let parent = this.shadowRoot || this;
425         return parent.querySelector(selector);
426       }
428       /**
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`:
434        *
435        *    if (this.delayConnectedCallback()) { return }
436        *
437        * And this at the beginning of your `attributeChangedCallback`
438        *
439        *    if (!this.isConnectedAndReady) { return; }
440        */
441       delayConnectedCallback() {
442         if (gIsDOMContentLoaded) {
443           return false;
444         }
445         gElementsPendingConnection.add(this);
446         return true;
447       }
449       get isConnectedAndReady() {
450         return gIsDOMContentLoaded && this.isConnected;
451       }
453       /**
454        * Passes DOM events to the on_<event type> methods.
455        */
456       handleEvent(event) {
457         let methodName = "on_" + event.type;
458         if (methodName in this) {
459           this[methodName](event);
460         } else {
461           throw new Error("Unrecognized event: " + event.type);
462         }
463       }
465       /**
466        * Used by custom elements for caching fragments. We now would be
467        * caching once per class while also supporting subclasses.
468        *
469        * If available, returns the cached fragment.
470        * Otherwise, creates it.
471        *
472        * Example:
473        *
474        *  class ElementA extends MozXULElement {
475        *    static get markup() {
476        *      return `<hbox class="example"`;
477        *    }
478        *
479        *    connectedCallback() {
480        *      this.appendChild(this.constructor.fragment);
481        *    }
482        *  }
483        *
484        * @return {importedNode} The imported node that has not been
485        * inserted into document tree.
486        */
487       static get fragment() {
488         if (!this.hasOwnProperty("_fragment")) {
489           let markup = this.markup;
490           if (markup) {
491             this._fragment = MozXULElement.parseXULToFragment(
492               markup,
493               this.entities
494             );
495           } else {
496             throw new Error("Markup is null");
497           }
498         }
499         return document.importNode(this._fragment, true);
500       }
502       /**
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.
506        *
507        * This process is required instead of calling the createElement method directly
508        * because bindings get attached when:
509        *
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,
512        *
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.
518        *
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.
523        *
524        * @return {DocumentFragment} `DocumentFragment` instance containing
525        *         the corresponding element tree, including element nodes
526        *         but excluding any text node.
527        */
528       static parseXULToFragment(str, entities = []) {
529         let doc = gXULDOMParser.parseFromSafeString(
530           `
531       ${
532         entities.length
533           ? `<!DOCTYPE bindings [
534         ${entities.reduce((preamble, url, index) => {
535           return (
536             preamble +
537             `<!ENTITY % _dtd-${index} SYSTEM "${url}">
538             %_dtd-${index};
539             `
540           );
541         }, "")}
542       ]>`
543           : ""
544       }
545       <box xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
546            xmlns:html="http://www.w3.org/1999/xhtml">
547         ${str}
548       </box>
549     `,
550           "application/xml"
551         );
553         if (doc.documentElement.localName === "parsererror") {
554           throw new Error("not well-formed XML");
555         }
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();
567           }
569           currentNode = nodeIterator.nextNode();
570         }
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();
576       }
578       /**
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.
583        *
584        * @param path
585        *        The path to the FTL file
586        */
587       static insertFTLIfNeeded(path) {
588         let container = document.head || document.querySelector("linkset");
589         if (!container) {
590           if (
591             document.documentElement.namespaceURI ===
592             "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
593           ) {
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;
600           } else {
601             throw new Error(
602               "Attempt to inject localization link before document.head is available"
603             );
604           }
605         }
607         for (let link of container.querySelectorAll("link")) {
608           if (link.getAttribute("href") == path) {
609             return;
610           }
611         }
613         let link = document.createElementNS(
614           "http://www.w3.org/1999/xhtml",
615           "link"
616         );
617         link.setAttribute("rel", "localization");
618         link.setAttribute("href", path);
620         container.appendChild(link);
621       }
623       /**
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.
627        *
628        * The supplied class should implement the properties and methods of
629        * all of the interfaces that are specified.
630        *
631        * @param cls
632        *        The class that implements the interface.
633        * @param names
634        *        Array of interface names.
635        */
636       static implementCustomInterface(cls, ifaces) {
637         if (cls.prototype.customInterfaces) {
638           ifaces.push(...cls.prototype.customInterfaces);
639         }
640         cls.prototype.customInterfaces = ifaces;
642         cls.prototype.QueryInterface = ChromeUtils.generateQI(ifaces);
643         cls.prototype.getCustomInterfaceCallback =
644           function getCustomInterfaceCallback(ifaceToCheck) {
645             if (
646               cls.prototype.customInterfaces.some(iface =>
647                 iface.equals(ifaceToCheck)
648               )
649             ) {
650               return getInterfaceProxy(this);
651             }
652             return null;
653           };
654       }
655     };
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);
661     }
662     return MozElementBase;
663   };
665   const MozXULElement = MozElements.MozElementMixin(XULElement);
666   const MozHTMLElement = MozElements.MozElementMixin(HTMLElement);
668   /**
669    * Given an object, add a proxy that reflects interface implementations
670    * onto the object itself.
671    */
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);
681             }
682             return function (...args) {
683               return propOrMethod.apply(target, args);
684             };
685           }
686           return propOrMethod;
687         },
688       });
689     }
691     return obj._customInterfaceProxy;
692   }
694   MozElements.BaseControlMixin = Base => {
695     class BaseControl extends Base {
696       get disabled() {
697         return this.getAttribute("disabled") == "true";
698       }
700       set disabled(val) {
701         if (val) {
702           this.setAttribute("disabled", "true");
703         } else {
704           this.removeAttribute("disabled");
705         }
706       }
708       get tabIndex() {
709         return parseInt(this.getAttribute("tabindex")) || 0;
710       }
712       set tabIndex(val) {
713         if (val) {
714           this.setAttribute("tabindex", val);
715         } else {
716           this.removeAttribute("tabindex");
717         }
718       }
719     }
721     MozXULElement.implementCustomInterface(BaseControl, [
722       Ci.nsIDOMXULControlElement,
723     ]);
724     return BaseControl;
725   };
726   MozElements.BaseControl = MozElements.BaseControlMixin(MozXULElement);
728   const BaseTextMixin = Base =>
729     class BaseText extends MozElements.BaseControlMixin(Base) {
730       set label(val) {
731         this.setAttribute("label", val);
732       }
734       get label() {
735         return this.getAttribute("label") || "";
736       }
738       set image(val) {
739         this.setAttribute("image", val);
740       }
742       get image() {
743         return this.getAttribute("image");
744       }
746       set command(val) {
747         this.setAttribute("command", val);
748       }
750       get command() {
751         return this.getAttribute("command");
752       }
754       set accessKey(val) {
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;
761         }
762       }
764       get accessKey() {
765         return this.labelElement?.accessKey || this.getAttribute("accesskey");
766       }
767     };
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",
778       window
779     );
780   });
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"
787   );
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"],
800       [
801         "printpreview-pagination",
802         "chrome://global/content/printPreviewPagination.js",
803       ],
804       [
805         "autocomplete-input",
806         "chrome://global/content/elements/autocomplete-input.js",
807       ],
808       ["editor", "chrome://global/content/elements/editor.js"],
809     ]) {
810       customElements.setElementCreationCallback(tag, () => {
811         Services.scriptloader.loadSubScript(script, window);
812       });
813     }
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) {
818       switch (name) {
819         case "moz-button":
820           return import("chrome://global/content/elements/moz-button.mjs");
821         case "moz-button-group":
822           return import(
823             "chrome://global/content/elements/moz-button-group.mjs"
824           );
825         case "moz-message-bar":
826           return import("chrome://global/content/elements/moz-message-bar.mjs");
827         case "moz-support-link":
828           return import(
829             "chrome://global/content/elements/moz-support-link.mjs"
830           );
831         case "moz-toggle":
832           return import("chrome://global/content/elements/moz-toggle.mjs");
833         case "moz-card":
834           return import("chrome://global/content/elements/moz-card.mjs");
835       }
836       throw new Error(`Unknown custom element name (${name})`);
837     }
839     /*
840     This function explicitly returns null so that there is no confusion
841     about which custom elements from ES Modules have been loaded.
842     */
843     window.ensureCustomElements = function (...elementNames) {
844       return Promise.all(
845         elementNames
846           .filter(name => !customElements.get(name))
847           .map(name => importCustomElementFromESModule(name))
848       )
849         .then(() => null)
850         .catch(console.error);
851     };
853     // Immediately load the following elements
854     for (let script of [
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",
875     ]) {
876       Services.scriptloader.loadSubScript(script, window);
877     }
878   }
879 })();