forked from mirrors/gecko-dev
		
	 43ebe0ce80
			
		
	
	
		43ebe0ce80
		
	
	
	
	
		
			
			This change allows us to refactor the shopping-card to have its ratings case without needing to duplicate the card widget itself. Additionally, this will allow us to use the card widget for the translations case in about:preferences. The button styles in moz-card.css will need to be de-duplicated in the future, once we figure out the moz-button widget. Differential Revision: https://phabricator.services.mozilla.com/D187712
		
			
				
	
	
		
			879 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			879 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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 defines these globals on the window object.
 | |
| // Define them here so that ESLint can find them:
 | |
| /* globals MozXULElement, MozHTMLElement, MozElements */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| // This is loaded into chrome windows with the subscript loader. Wrap in
 | |
| // a block to prevent accidentally leaking globals onto `window`.
 | |
| (() => {
 | |
|   // Handle customElements.js being loaded as a script in addition to the subscriptLoader
 | |
|   // from MainProcessSingleton, to handle pages that can open both before and after
 | |
|   // MainProcessSingleton starts. See Bug 1501845.
 | |
|   if (window.MozXULElement) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   const MozElements = {};
 | |
|   window.MozElements = MozElements;
 | |
| 
 | |
|   const { AppConstants } = ChromeUtils.importESModule(
 | |
|     "resource://gre/modules/AppConstants.sys.mjs"
 | |
|   );
 | |
|   const instrumentClasses = Services.env.get("MOZ_INSTRUMENT_CUSTOM_ELEMENTS");
 | |
|   const instrumentedClasses = instrumentClasses ? new Set() : null;
 | |
|   const instrumentedBaseClasses = instrumentClasses ? new WeakSet() : null;
 | |
| 
 | |
|   // If requested, wrap the normal customElements.define to give us a chance
 | |
|   // to modify the class so we can instrument function calls in local development:
 | |
|   if (instrumentClasses) {
 | |
|     let define = window.customElements.define;
 | |
|     window.customElements.define = function (name, c, opts) {
 | |
|       instrumentCustomElementClass(c);
 | |
|       return define.call(this, name, c, opts);
 | |
|     };
 | |
|     window.addEventListener(
 | |
|       "load",
 | |
|       () => {
 | |
|         MozElements.printInstrumentation(true);
 | |
|       },
 | |
|       { once: true, capture: true }
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   MozElements.printInstrumentation = function (collapsed) {
 | |
|     let summaries = [];
 | |
|     let totalCalls = 0;
 | |
|     let totalTime = 0;
 | |
|     for (let c of instrumentedClasses) {
 | |
|       // Allow passing in something like MOZ_INSTRUMENT_CUSTOM_ELEMENTS=MozXULElement,Button to filter
 | |
|       let includeClass =
 | |
|         instrumentClasses == 1 ||
 | |
|         instrumentClasses
 | |
|           .split(",")
 | |
|           .some(n => c.name.toLowerCase().includes(n.toLowerCase()));
 | |
|       let summary = c.__instrumentation_summary;
 | |
|       if (includeClass && summary) {
 | |
|         summaries.push(summary);
 | |
|         totalCalls += summary.totalCalls;
 | |
|         totalTime += summary.totalTime;
 | |
|       }
 | |
|     }
 | |
|     if (summaries.length) {
 | |
|       let groupName = `Instrumentation data for custom elements in ${document.documentURI}`;
 | |
|       console[collapsed ? "groupCollapsed" : "group"](groupName);
 | |
|       console.log(
 | |
|         `Total function calls ${totalCalls} and total time spent inside ${totalTime.toFixed(
 | |
|           2
 | |
|         )}`
 | |
|       );
 | |
|       for (let summary of summaries) {
 | |
|         console.log(`${summary.name} (# instances: ${summary.instances})`);
 | |
|         if (Object.keys(summary.data).length > 1) {
 | |
|           console.table(summary.data);
 | |
|         }
 | |
|       }
 | |
|       console.groupEnd(groupName);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   function instrumentCustomElementClass(c) {
 | |
|     // Climb up prototype chain to see if we inherit from a MozElement.
 | |
|     // Keep track of classes to instrument, for example:
 | |
|     //   MozMenuCaption->MozMenuBase->BaseText->BaseControl->MozXULElement
 | |
|     let inheritsFromBase = instrumentedBaseClasses.has(c);
 | |
|     let classesToInstrument = [c];
 | |
|     let proto = Object.getPrototypeOf(c);
 | |
|     while (proto) {
 | |
|       classesToInstrument.push(proto);
 | |
|       if (instrumentedBaseClasses.has(proto)) {
 | |
|         inheritsFromBase = true;
 | |
|         break;
 | |
|       }
 | |
|       proto = Object.getPrototypeOf(proto);
 | |
|     }
 | |
| 
 | |
|     if (inheritsFromBase) {
 | |
|       for (let c of classesToInstrument.reverse()) {
 | |
|         instrumentIndividualClass(c);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function instrumentIndividualClass(c) {
 | |
|     if (instrumentedClasses.has(c)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     instrumentedClasses.add(c);
 | |
|     let data = { instances: 0 };
 | |
| 
 | |
|     function wrapFunction(name, fn) {
 | |
|       return function () {
 | |
|         if (!data[name]) {
 | |
|           data[name] = { time: 0, calls: 0 };
 | |
|         }
 | |
|         data[name].calls++;
 | |
|         let n = performance.now();
 | |
|         let r = fn.apply(this, arguments);
 | |
|         data[name].time += performance.now() - n;
 | |
|         return r;
 | |
|       };
 | |
|     }
 | |
|     function wrapPropertyDescriptor(obj, name) {
 | |
|       if (name == "constructor") {
 | |
|         return;
 | |
|       }
 | |
|       let prop = Object.getOwnPropertyDescriptor(obj, name);
 | |
|       if (prop.get) {
 | |
|         prop.get = wrapFunction(`<get> ${name}`, prop.get);
 | |
|       }
 | |
|       if (prop.set) {
 | |
|         prop.set = wrapFunction(`<set> ${name}`, prop.set);
 | |
|       }
 | |
|       if (prop.writable && prop.value && prop.value.apply) {
 | |
|         prop.value = wrapFunction(name, prop.value);
 | |
|       }
 | |
|       Object.defineProperty(obj, name, prop);
 | |
|     }
 | |
| 
 | |
|     // Handle static properties
 | |
|     for (let name of Object.getOwnPropertyNames(c)) {
 | |
|       wrapPropertyDescriptor(c, name);
 | |
|     }
 | |
| 
 | |
|     // Handle instance properties
 | |
|     for (let name of Object.getOwnPropertyNames(c.prototype)) {
 | |
|       wrapPropertyDescriptor(c.prototype, name);
 | |
|     }
 | |
| 
 | |
|     c.__instrumentation_data = data;
 | |
|     Object.defineProperty(c, "__instrumentation_summary", {
 | |
|       enumerable: false,
 | |
|       configurable: false,
 | |
|       get() {
 | |
|         if (data.instances == 0) {
 | |
|           return null;
 | |
|         }
 | |
| 
 | |
|         let clonedData = JSON.parse(JSON.stringify(data));
 | |
|         delete clonedData.instances;
 | |
|         let totalCalls = 0;
 | |
|         let totalTime = 0;
 | |
|         for (let d in clonedData) {
 | |
|           let { time, calls } = clonedData[d];
 | |
|           time = parseFloat(time.toFixed(2));
 | |
|           totalCalls += calls;
 | |
|           totalTime += time;
 | |
|           clonedData[d]["time (ms)"] = time;
 | |
|           delete clonedData[d].time;
 | |
|           clonedData[d].timePerCall = parseFloat((time / calls).toFixed(4));
 | |
|         }
 | |
| 
 | |
|         let timePerCall = parseFloat((totalTime / totalCalls).toFixed(4));
 | |
|         totalTime = parseFloat(totalTime.toFixed(2));
 | |
| 
 | |
|         // Add a spaced-out final row with summed up totals
 | |
|         clonedData["\ntotals"] = {
 | |
|           "time (ms)": `\n${totalTime}`,
 | |
|           calls: `\n${totalCalls}`,
 | |
|           timePerCall: `\n${timePerCall}`,
 | |
|         };
 | |
|         return {
 | |
|           instances: data.instances,
 | |
|           data: clonedData,
 | |
|           name: c.name,
 | |
|           totalCalls,
 | |
|           totalTime,
 | |
|         };
 | |
|       },
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   // The listener of DOMContentLoaded must be set on window, rather than
 | |
|   // document, because the window can go away before the event is fired.
 | |
|   // In that case, we don't want to initialize anything, otherwise we
 | |
|   // may be leaking things because they will never be destroyed after.
 | |
|   let gIsDOMContentLoaded = false;
 | |
|   const gElementsPendingConnection = new Set();
 | |
|   window.addEventListener(
 | |
|     "DOMContentLoaded",
 | |
|     () => {
 | |
|       gIsDOMContentLoaded = true;
 | |
|       for (let element of gElementsPendingConnection) {
 | |
|         try {
 | |
|           if (element.isConnected) {
 | |
|             element.isRunningDelayedConnectedCallback = true;
 | |
|             element.connectedCallback();
 | |
|           }
 | |
|         } catch (ex) {
 | |
|           console.error(ex);
 | |
|         }
 | |
|         element.isRunningDelayedConnectedCallback = false;
 | |
|       }
 | |
|       gElementsPendingConnection.clear();
 | |
|     },
 | |
|     { once: true, capture: true }
 | |
|   );
 | |
| 
 | |
|   const gXULDOMParser = new DOMParser();
 | |
|   gXULDOMParser.forceEnableXULXBL();
 | |
| 
 | |
|   MozElements.MozElementMixin = Base => {
 | |
|     let MozElementBase = class extends Base {
 | |
|       constructor() {
 | |
|         super();
 | |
| 
 | |
|         if (instrumentClasses) {
 | |
|           let proto = this.constructor;
 | |
|           while (proto && proto != Base) {
 | |
|             proto.__instrumentation_data.instances++;
 | |
|             proto = Object.getPrototypeOf(proto);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       /*
 | |
|        * A declarative way to wire up attribute inheritance and automatically generate
 | |
|        * the `observedAttributes` getter.  For example, if you returned:
 | |
|        *    {
 | |
|        *      ".foo": "bar,baz=bat"
 | |
|        *    }
 | |
|        *
 | |
|        * Then the base class will automatically return ["bar", "bat"] from `observedAttributes`,
 | |
|        * and set up an `attributeChangedCallback` to pass those attributes down onto an element
 | |
|        * matching the ".foo" selector.
 | |
|        *
 | |
|        * See the `inheritAttribute` function for more details on the attribute string format.
 | |
|        *
 | |
|        * @return {Object<string selector, string attributes>}
 | |
|        */
 | |
|       static get inheritedAttributes() {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       static get flippedInheritedAttributes() {
 | |
|         // Have to be careful here, if a subclass overrides inheritedAttributes
 | |
|         // and its parent class is instantiated first, then reading
 | |
|         // this._flippedInheritedAttributes on the child class will return the
 | |
|         // computed value from the parent.  We store it separately on each class
 | |
|         // to ensure everything works correctly when inheritedAttributes is
 | |
|         // overridden.
 | |
|         if (!this.hasOwnProperty("_flippedInheritedAttributes")) {
 | |
|           let { inheritedAttributes } = this;
 | |
|           if (!inheritedAttributes) {
 | |
|             this._flippedInheritedAttributes = null;
 | |
|           } else {
 | |
|             this._flippedInheritedAttributes = {};
 | |
|             for (let selector in inheritedAttributes) {
 | |
|               let attrRules = inheritedAttributes[selector].split(",");
 | |
|               for (let attrRule of attrRules) {
 | |
|                 let attrName = attrRule;
 | |
|                 let attrNewName = attrRule;
 | |
|                 let split = attrName.split("=");
 | |
|                 if (split.length == 2) {
 | |
|                   attrName = split[1];
 | |
|                   attrNewName = split[0];
 | |
|                 }
 | |
| 
 | |
|                 if (!this._flippedInheritedAttributes[attrName]) {
 | |
|                   this._flippedInheritedAttributes[attrName] = [];
 | |
|                 }
 | |
|                 this._flippedInheritedAttributes[attrName].push([
 | |
|                   selector,
 | |
|                   attrNewName,
 | |
|                 ]);
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         return this._flippedInheritedAttributes;
 | |
|       }
 | |
|       /*
 | |
|        * Generate this array based on `inheritedAttributes`, if any. A class is free to override
 | |
|        * this if it needs to do something more complex or wants to opt out of this behavior.
 | |
|        */
 | |
|       static get observedAttributes() {
 | |
|         return Object.keys(this.flippedInheritedAttributes || {});
 | |
|       }
 | |
| 
 | |
|       /*
 | |
|        * Provide default lifecycle callback for attribute changes that will inherit attributes
 | |
|        * based on the static `inheritedAttributes` Object. This can be overridden by callers.
 | |
|        */
 | |
|       attributeChangedCallback(name, oldValue, newValue) {
 | |
|         if (oldValue === newValue || !this.initializedAttributeInheritance) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         let list = this.constructor.flippedInheritedAttributes[name];
 | |
|         if (list) {
 | |
|           this.inheritAttribute(list, name);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       /*
 | |
|        * After setting content, calling this will cache the elements from selectors in the
 | |
|        * static `inheritedAttributes` Object. It'll also do an initial call to `this.inheritAttributes()`,
 | |
|        * so in the simple case, this is the only function you need to call.
 | |
|        *
 | |
|        * This should be called any time the children that are inheriting attributes changes. For instance,
 | |
|        * it's common in a connectedCallback to do something like:
 | |
|        *
 | |
|        *   this.textContent = "";
 | |
|        *   this.append(MozXULElement.parseXULToFragment(`<label />`))
 | |
|        *   this.initializeAttributeInheritance();
 | |
|        *
 | |
|        */
 | |
|       initializeAttributeInheritance() {
 | |
|         let { flippedInheritedAttributes } = this.constructor;
 | |
|         if (!flippedInheritedAttributes) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         // Clear out any existing cached elements:
 | |
|         this._inheritedElements = null;
 | |
| 
 | |
|         this.initializedAttributeInheritance = true;
 | |
|         for (let attr in flippedInheritedAttributes) {
 | |
|           if (this.hasAttribute(attr)) {
 | |
|             this.inheritAttribute(flippedInheritedAttributes[attr], attr);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       /*
 | |
|        * Implements attribute value inheritance by child elements.
 | |
|        *
 | |
|        * @param {array} list
 | |
|        *        An array of (to-element-selector, to-attr) pairs.
 | |
|        * @param {string} attr
 | |
|        *        An attribute to propagate.
 | |
|        */
 | |
|       inheritAttribute(list, attr) {
 | |
|         if (!this._inheritedElements) {
 | |
|           this._inheritedElements = {};
 | |
|         }
 | |
| 
 | |
|         let hasAttr = this.hasAttribute(attr);
 | |
|         let attrValue = this.getAttribute(attr);
 | |
| 
 | |
|         for (let [selector, newAttr] of list) {
 | |
|           if (!(selector in this._inheritedElements)) {
 | |
|             this._inheritedElements[selector] =
 | |
|               this.getElementForAttrInheritance(selector);
 | |
|           }
 | |
|           let el = this._inheritedElements[selector];
 | |
|           if (el) {
 | |
|             if (newAttr == "text") {
 | |
|               el.textContent = hasAttr ? attrValue : "";
 | |
|             } else if (hasAttr) {
 | |
|               el.setAttribute(newAttr, attrValue);
 | |
|             } else {
 | |
|               el.removeAttribute(newAttr);
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       /**
 | |
|        * Used in setting up attribute inheritance. Takes a selector and returns
 | |
|        * an element for that selector from shadow DOM if there is a shadowRoot,
 | |
|        * or from the light DOM if not.
 | |
|        *
 | |
|        * Here's one problem this solves. ElementB extends ElementA which extends
 | |
|        * MozXULElement. ElementA has a shadowRoot. ElementB tries to inherit
 | |
|        * attributes in light DOM by calling `initializeAttributeInheritance`
 | |
|        * but that fails because it defaults to inheriting from the shadow DOM
 | |
|        * and not the light DOM. (See bug 1545824.)
 | |
|        *
 | |
|        * To solve this, ElementB can override `getElementForAttrInheritance` so
 | |
|        * it queries the light DOM for some selectors as needed. For example:
 | |
|        *
 | |
|        *  class ElementA extends MozXULElement {
 | |
|        *    static get inheritedAttributes() {
 | |
|        *      return { ".one": "attr" };
 | |
|        *    }
 | |
|        *  }
 | |
|        *
 | |
|        *  class ElementB extends customElements.get("elementa") {
 | |
|        *    static get inheritedAttributes() {
 | |
|        *      return Object.assign({}, super.inheritedAttributes(), {
 | |
|        *        ".two": "attr",
 | |
|        *      });
 | |
|        *    }
 | |
|        *    getElementForAttrInheritance(selector) {
 | |
|        *      if (selector == ".two") {
 | |
|        *        return this.querySelector(selector)
 | |
|        *      } else {
 | |
|        *        return super.getElementForAttrInheritance(selector);
 | |
|        *      }
 | |
|        *    }
 | |
|        *  }
 | |
|        *
 | |
|        * @param {string} selector
 | |
|        *        A selector used to query an element.
 | |
|        *
 | |
|        * @return {Element} The element found by the selector.
 | |
|        */
 | |
|       getElementForAttrInheritance(selector) {
 | |
|         let parent = this.shadowRoot || this;
 | |
|         return parent.querySelector(selector);
 | |
|       }
 | |
| 
 | |
|       /**
 | |
|        * Sometimes an element may not want to run connectedCallback logic during
 | |
|        * parse. This could be because we don't want to initialize the element before
 | |
|        * the element's contents have been fully parsed, or for performance reasons.
 | |
|        * If you'd like to opt-in to this, then add this to the beginning of your
 | |
|        * `connectedCallback` and `disconnectedCallback`:
 | |
|        *
 | |
|        *    if (this.delayConnectedCallback()) { return }
 | |
|        *
 | |
|        * And this at the beginning of your `attributeChangedCallback`
 | |
|        *
 | |
|        *    if (!this.isConnectedAndReady) { return; }
 | |
|        */
 | |
|       delayConnectedCallback() {
 | |
|         if (gIsDOMContentLoaded) {
 | |
|           return false;
 | |
|         }
 | |
|         gElementsPendingConnection.add(this);
 | |
|         return true;
 | |
|       }
 | |
| 
 | |
|       get isConnectedAndReady() {
 | |
|         return gIsDOMContentLoaded && this.isConnected;
 | |
|       }
 | |
| 
 | |
|       /**
 | |
|        * Passes DOM events to the on_<event type> methods.
 | |
|        */
 | |
|       handleEvent(event) {
 | |
|         let methodName = "on_" + event.type;
 | |
|         if (methodName in this) {
 | |
|           this[methodName](event);
 | |
|         } else {
 | |
|           throw new Error("Unrecognized event: " + event.type);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       /**
 | |
|        * Used by custom elements for caching fragments. We now would be
 | |
|        * caching once per class while also supporting subclasses.
 | |
|        *
 | |
|        * If available, returns the cached fragment.
 | |
|        * Otherwise, creates it.
 | |
|        *
 | |
|        * Example:
 | |
|        *
 | |
|        *  class ElementA extends MozXULElement {
 | |
|        *    static get markup() {
 | |
|        *      return `<hbox class="example"`;
 | |
|        *    }
 | |
|        *
 | |
|        *    connectedCallback() {
 | |
|        *      this.appendChild(this.constructor.fragment);
 | |
|        *    }
 | |
|        *  }
 | |
|        *
 | |
|        * @return {importedNode} The imported node that has not been
 | |
|        * inserted into document tree.
 | |
|        */
 | |
|       static get fragment() {
 | |
|         if (!this.hasOwnProperty("_fragment")) {
 | |
|           let markup = this.markup;
 | |
|           if (markup) {
 | |
|             this._fragment = MozXULElement.parseXULToFragment(
 | |
|               markup,
 | |
|               this.entities
 | |
|             );
 | |
|           } else {
 | |
|             throw new Error("Markup is null");
 | |
|           }
 | |
|         }
 | |
|         return document.importNode(this._fragment, true);
 | |
|       }
 | |
| 
 | |
|       /**
 | |
|        * Allows eager deterministic construction of XUL elements with XBL attached, by
 | |
|        * parsing an element tree and returning a DOM fragment to be inserted in the
 | |
|        * document before any of the inner elements is referenced by JavaScript.
 | |
|        *
 | |
|        * This process is required instead of calling the createElement method directly
 | |
|        * because bindings get attached when:
 | |
|        *
 | |
|        * 1. the node gets a layout frame constructed, or
 | |
|        * 2. the node gets its JavaScript reflector created, if it's in the document,
 | |
|        *
 | |
|        * whichever happens first. The createElement method would return a JavaScript
 | |
|        * reflector, but the element wouldn't be in the document, so the node wouldn't
 | |
|        * get XBL attached. After that point, even if the node is inserted into a
 | |
|        * document, it won't get XBL attached until either the frame is constructed or
 | |
|        * the reflector is garbage collected and the element is touched again.
 | |
|        *
 | |
|        * @param {string} str
 | |
|        *        String with the XML representation of XUL elements.
 | |
|        * @param {string[]} [entities]
 | |
|        *        An array of DTD URLs containing entity definitions.
 | |
|        *
 | |
|        * @return {DocumentFragment} `DocumentFragment` instance containing
 | |
|        *         the corresponding element tree, including element nodes
 | |
|        *         but excluding any text node.
 | |
|        */
 | |
|       static parseXULToFragment(str, entities = []) {
 | |
|         let doc = gXULDOMParser.parseFromSafeString(
 | |
|           `
 | |
|       ${
 | |
|         entities.length
 | |
|           ? `<!DOCTYPE bindings [
 | |
|         ${entities.reduce((preamble, url, index) => {
 | |
|           return (
 | |
|             preamble +
 | |
|             `<!ENTITY % _dtd-${index} SYSTEM "${url}">
 | |
|             %_dtd-${index};
 | |
|             `
 | |
|           );
 | |
|         }, "")}
 | |
|       ]>`
 | |
|           : ""
 | |
|       }
 | |
|       <box xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
 | |
|            xmlns:html="http://www.w3.org/1999/xhtml">
 | |
|         ${str}
 | |
|       </box>
 | |
|     `,
 | |
|           "application/xml"
 | |
|         );
 | |
| 
 | |
|         if (doc.documentElement.localName === "parsererror") {
 | |
|           throw new Error("not well-formed XML");
 | |
|         }
 | |
| 
 | |
|         // The XUL/XBL parser is set to ignore all-whitespace nodes, whereas (X)HTML
 | |
|         // does not do this. Most XUL code assumes that the whitespace has been
 | |
|         // stripped out, so we simply remove all text nodes after using the parser.
 | |
|         let nodeIterator = doc.createNodeIterator(doc, NodeFilter.SHOW_TEXT);
 | |
|         let currentNode = nodeIterator.nextNode();
 | |
|         while (currentNode) {
 | |
|           // Remove whitespace-only nodes. Regex is taken from:
 | |
|           // https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace_in_the_DOM
 | |
|           if (!/[^\t\n\r ]/.test(currentNode.textContent)) {
 | |
|             currentNode.remove();
 | |
|           }
 | |
| 
 | |
|           currentNode = nodeIterator.nextNode();
 | |
|         }
 | |
|         // We use a range here so that we don't access the inner DOM elements from
 | |
|         // JavaScript before they are imported and inserted into a document.
 | |
|         let range = doc.createRange();
 | |
|         range.selectNodeContents(doc.querySelector("box"));
 | |
|         return range.extractContents();
 | |
|       }
 | |
| 
 | |
|       /**
 | |
|        * Insert a localization link to an FTL file. This is used so that
 | |
|        * a Custom Element can wait to inject the link until it's connected,
 | |
|        * and so that consuming documents don't require the correct <link>
 | |
|        * present in the markup.
 | |
|        *
 | |
|        * @param path
 | |
|        *        The path to the FTL file
 | |
|        */
 | |
|       static insertFTLIfNeeded(path) {
 | |
|         let container = document.head || document.querySelector("linkset");
 | |
|         if (!container) {
 | |
|           if (
 | |
|             document.documentElement.namespaceURI ===
 | |
|             "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
 | |
|           ) {
 | |
|             container = document.createXULElement("linkset");
 | |
|             document.documentElement.appendChild(container);
 | |
|           } else if (document.documentURI == AppConstants.BROWSER_CHROME_URL) {
 | |
|             // Special case for browser.xhtml. Here `document.head` is null, so
 | |
|             // just insert the link at the end of the window.
 | |
|             container = document.documentElement;
 | |
|           } else {
 | |
|             throw new Error(
 | |
|               "Attempt to inject localization link before document.head is available"
 | |
|             );
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         for (let link of container.querySelectorAll("link")) {
 | |
|           if (link.getAttribute("href") == path) {
 | |
|             return;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         let link = document.createElementNS(
 | |
|           "http://www.w3.org/1999/xhtml",
 | |
|           "link"
 | |
|         );
 | |
|         link.setAttribute("rel", "localization");
 | |
|         link.setAttribute("href", path);
 | |
| 
 | |
|         container.appendChild(link);
 | |
|       }
 | |
| 
 | |
|       /**
 | |
|        * Indicate that a class defining a XUL element implements one or more
 | |
|        * XPCOM interfaces by adding a getCustomInterface implementation to it,
 | |
|        * as well as an implementation of QueryInterface.
 | |
|        *
 | |
|        * The supplied class should implement the properties and methods of
 | |
|        * all of the interfaces that are specified.
 | |
|        *
 | |
|        * @param cls
 | |
|        *        The class that implements the interface.
 | |
|        * @param names
 | |
|        *        Array of interface names.
 | |
|        */
 | |
|       static implementCustomInterface(cls, ifaces) {
 | |
|         if (cls.prototype.customInterfaces) {
 | |
|           ifaces.push(...cls.prototype.customInterfaces);
 | |
|         }
 | |
|         cls.prototype.customInterfaces = ifaces;
 | |
| 
 | |
|         cls.prototype.QueryInterface = ChromeUtils.generateQI(ifaces);
 | |
|         cls.prototype.getCustomInterfaceCallback =
 | |
|           function getCustomInterfaceCallback(ifaceToCheck) {
 | |
|             if (
 | |
|               cls.prototype.customInterfaces.some(iface =>
 | |
|                 iface.equals(ifaceToCheck)
 | |
|               )
 | |
|             ) {
 | |
|               return getInterfaceProxy(this);
 | |
|             }
 | |
|             return null;
 | |
|           };
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     // Rename the class so we can distinguish between MozXULElement and MozXULPopupElement, for example.
 | |
|     Object.defineProperty(MozElementBase, "name", { value: `Moz${Base.name}` });
 | |
|     if (instrumentedBaseClasses) {
 | |
|       instrumentedBaseClasses.add(MozElementBase);
 | |
|     }
 | |
|     return MozElementBase;
 | |
|   };
 | |
| 
 | |
|   const MozXULElement = MozElements.MozElementMixin(XULElement);
 | |
|   const MozHTMLElement = MozElements.MozElementMixin(HTMLElement);
 | |
| 
 | |
|   /**
 | |
|    * Given an object, add a proxy that reflects interface implementations
 | |
|    * onto the object itself.
 | |
|    */
 | |
|   function getInterfaceProxy(obj) {
 | |
|     /* globals MozQueryInterface */
 | |
|     if (!obj._customInterfaceProxy) {
 | |
|       obj._customInterfaceProxy = new Proxy(obj, {
 | |
|         get(target, prop, receiver) {
 | |
|           let propOrMethod = target[prop];
 | |
|           if (typeof propOrMethod == "function") {
 | |
|             if (MozQueryInterface.isInstance(propOrMethod)) {
 | |
|               return Reflect.get(target, prop, receiver);
 | |
|             }
 | |
|             return function (...args) {
 | |
|               return propOrMethod.apply(target, args);
 | |
|             };
 | |
|           }
 | |
|           return propOrMethod;
 | |
|         },
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     return obj._customInterfaceProxy;
 | |
|   }
 | |
| 
 | |
|   MozElements.BaseControlMixin = Base => {
 | |
|     class BaseControl extends Base {
 | |
|       get disabled() {
 | |
|         return this.getAttribute("disabled") == "true";
 | |
|       }
 | |
| 
 | |
|       set disabled(val) {
 | |
|         if (val) {
 | |
|           this.setAttribute("disabled", "true");
 | |
|         } else {
 | |
|           this.removeAttribute("disabled");
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       get tabIndex() {
 | |
|         return parseInt(this.getAttribute("tabindex")) || 0;
 | |
|       }
 | |
| 
 | |
|       set tabIndex(val) {
 | |
|         if (val) {
 | |
|           this.setAttribute("tabindex", val);
 | |
|         } else {
 | |
|           this.removeAttribute("tabindex");
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     MozXULElement.implementCustomInterface(BaseControl, [
 | |
|       Ci.nsIDOMXULControlElement,
 | |
|     ]);
 | |
|     return BaseControl;
 | |
|   };
 | |
|   MozElements.BaseControl = MozElements.BaseControlMixin(MozXULElement);
 | |
| 
 | |
|   const BaseTextMixin = Base =>
 | |
|     class BaseText extends MozElements.BaseControlMixin(Base) {
 | |
|       set label(val) {
 | |
|         this.setAttribute("label", val);
 | |
|       }
 | |
| 
 | |
|       get label() {
 | |
|         return this.getAttribute("label");
 | |
|       }
 | |
| 
 | |
|       set image(val) {
 | |
|         this.setAttribute("image", val);
 | |
|       }
 | |
| 
 | |
|       get image() {
 | |
|         return this.getAttribute("image");
 | |
|       }
 | |
| 
 | |
|       set command(val) {
 | |
|         this.setAttribute("command", val);
 | |
|       }
 | |
| 
 | |
|       get command() {
 | |
|         return this.getAttribute("command");
 | |
|       }
 | |
| 
 | |
|       set accessKey(val) {
 | |
|         // Always store on the control
 | |
|         this.setAttribute("accesskey", val);
 | |
|         // If there is a label, change the accesskey on the labelElement
 | |
|         // if it's also set there
 | |
|         if (this.labelElement) {
 | |
|           this.labelElement.accessKey = val;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       get accessKey() {
 | |
|         return this.labelElement
 | |
|           ? this.labelElement.accessKey
 | |
|           : this.getAttribute("accesskey");
 | |
|       }
 | |
|     };
 | |
|   MozElements.BaseTextMixin = BaseTextMixin;
 | |
|   MozElements.BaseText = BaseTextMixin(MozXULElement);
 | |
| 
 | |
|   // Attach the base class to the window so other scripts can use it:
 | |
|   window.MozXULElement = MozXULElement;
 | |
|   window.MozHTMLElement = MozHTMLElement;
 | |
| 
 | |
|   customElements.setElementCreationCallback("browser", () => {
 | |
|     Services.scriptloader.loadSubScript(
 | |
|       "chrome://global/content/elements/browser-custom-element.js",
 | |
|       window
 | |
|     );
 | |
|   });
 | |
| 
 | |
|   // Skip loading any extra custom elements in the extension dummy document
 | |
|   // and GeckoView windows.
 | |
|   const loadExtraCustomElements = !(
 | |
|     document.documentURI == "chrome://extensions/content/dummy.xhtml" ||
 | |
|     document.documentURI == "chrome://geckoview/content/geckoview.xhtml"
 | |
|   );
 | |
|   if (loadExtraCustomElements) {
 | |
|     // Lazily load the following elements
 | |
|     for (let [tag, script] of [
 | |
|       ["button-group", "chrome://global/content/elements/named-deck.js"],
 | |
|       ["findbar", "chrome://global/content/elements/findbar.js"],
 | |
|       ["menulist", "chrome://global/content/elements/menulist.js"],
 | |
|       ["message-bar", "chrome://global/content/elements/message-bar.js"],
 | |
|       ["named-deck", "chrome://global/content/elements/named-deck.js"],
 | |
|       ["named-deck-button", "chrome://global/content/elements/named-deck.js"],
 | |
|       ["panel-list", "chrome://global/content/elements/panel-list.js"],
 | |
|       ["search-textbox", "chrome://global/content/elements/search-textbox.js"],
 | |
|       ["stringbundle", "chrome://global/content/elements/stringbundle.js"],
 | |
|       [
 | |
|         "printpreview-pagination",
 | |
|         "chrome://global/content/printPreviewPagination.js",
 | |
|       ],
 | |
|       [
 | |
|         "autocomplete-input",
 | |
|         "chrome://global/content/elements/autocomplete-input.js",
 | |
|       ],
 | |
|       ["editor", "chrome://global/content/elements/editor.js"],
 | |
|     ]) {
 | |
|       customElements.setElementCreationCallback(tag, () => {
 | |
|         Services.scriptloader.loadSubScript(script, window);
 | |
|       });
 | |
|     }
 | |
|     // Bug 1813077: This is a workaround until Bug 1803810 lands
 | |
|     // which will give us the ability to load ESMs synchronously
 | |
|     // like the previous Services.scriptloader.loadSubscript() function
 | |
|     function importCustomElementFromESModule(name) {
 | |
|       switch (name) {
 | |
|         case "moz-button-group":
 | |
|           return import(
 | |
|             "chrome://global/content/elements/moz-button-group.mjs"
 | |
|           );
 | |
|         case "moz-message-bar":
 | |
|           return import("chrome://global/content/elements/moz-message-bar.mjs");
 | |
|         case "moz-support-link":
 | |
|           return import(
 | |
|             "chrome://global/content/elements/moz-support-link.mjs"
 | |
|           );
 | |
|         case "moz-toggle":
 | |
|           return import("chrome://global/content/elements/moz-toggle.mjs");
 | |
|         case "moz-card":
 | |
|           return import("chrome://global/content/elements/moz-card.mjs");
 | |
|       }
 | |
|       throw new Error(`Unknown custom element name (${name})`);
 | |
|     }
 | |
| 
 | |
|     /*
 | |
|     This function explicitly returns null so that there is no confusion
 | |
|     about which custom elements from ES Modules have been loaded.
 | |
|     */
 | |
|     window.ensureCustomElements = function (...elementNames) {
 | |
|       return Promise.all(
 | |
|         elementNames
 | |
|           .filter(name => !customElements.get(name))
 | |
|           .map(name => importCustomElementFromESModule(name))
 | |
|       )
 | |
|         .then(() => null)
 | |
|         .catch(console.error);
 | |
|     };
 | |
| 
 | |
|     // Immediately load the following elements
 | |
|     for (let script of [
 | |
|       "chrome://global/content/elements/arrowscrollbox.js",
 | |
|       "chrome://global/content/elements/dialog.js",
 | |
|       "chrome://global/content/elements/general.js",
 | |
|       "chrome://global/content/elements/button.js",
 | |
|       "chrome://global/content/elements/checkbox.js",
 | |
|       "chrome://global/content/elements/menu.js",
 | |
|       "chrome://global/content/elements/menupopup.js",
 | |
|       "chrome://global/content/elements/moz-input-box.js",
 | |
|       "chrome://global/content/elements/notificationbox.js",
 | |
|       "chrome://global/content/elements/panel.js",
 | |
|       "chrome://global/content/elements/popupnotification.js",
 | |
|       "chrome://global/content/elements/radio.js",
 | |
|       "chrome://global/content/elements/richlistbox.js",
 | |
|       "chrome://global/content/elements/autocomplete-popup.js",
 | |
|       "chrome://global/content/elements/autocomplete-richlistitem.js",
 | |
|       "chrome://global/content/elements/tabbox.js",
 | |
|       "chrome://global/content/elements/text.js",
 | |
|       "chrome://global/content/elements/toolbarbutton.js",
 | |
|       "chrome://global/content/elements/tree.js",
 | |
|       "chrome://global/content/elements/wizard.js",
 | |
|     ]) {
 | |
|       Services.scriptloader.loadSubScript(script, window);
 | |
|     }
 | |
|   }
 | |
| })();
 |