Source code

Revision control

Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
/* 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 BaseControlMixin, MozElementMixin, MozXULElement, 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 {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");

// 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();

const MozElements = {};

const MozElementMixin = Base => class MozElement extends Base {
  /*
   * 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;
  }

  /*
   * 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() {
    let {inheritedAttributes} = this;
    if (!inheritedAttributes) {
      return [];
    }

    let allAttributes = new Set();
    for (let sel in inheritedAttributes) {
      for (let attrName of inheritedAttributes[sel].split(",")) {
        allAttributes.add(attrName.split("=").pop());
      }
    }
    return [...allAttributes];
  }

  /*
   * 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 (!this.isConnectedAndReady || oldValue === newValue || !this.inheritedAttributesCache) {
      return;
    }

    this.inheritAttributes();
  }

  /*
  * 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 {inheritedAttributes} = this.constructor;
    if (!inheritedAttributes) {
      return;
    }
    this._inheritedAttributesValuesCache = null;
    this.inheritedAttributesCache = new Map();
    for (let selector in inheritedAttributes) {
      let parent = this.shadowRoot || this;
      let el = parent.querySelector(selector);
      // Skip unmatched selectors in case an element omits some elements in certain cases:
      if (!el) {
        continue;
      }
      if (this.inheritedAttributesCache.has(el)) {
        console.error(`Error: duplicate element encountered with ${selector}`);
      }

      this.inheritedAttributesCache.set(el, inheritedAttributes[selector]);
    }
    this.inheritAttributes();
  }

  /*
   * Loop through the static `inheritedAttributes` Map and inherit attributes to child elements.
   *
   * This usually won't need to be called directly - `this.initializeAttributeInheritance()` and
   * `this.attributeChangedCallback` will call it for you when appropriate.
   */
  inheritAttributes() {
    let {inheritedAttributes} = this.constructor;
    if (!inheritedAttributes) {
      return;
    }

    if (!this.inheritedAttributesCache) {
     console.error(`You must call this.initializeAttributeInheritance() for ${this.tagName}`);
     return;
    }

    for (let [ el, attrs ] of this.inheritedAttributesCache.entries()) {
      for (let attr of attrs.split(",")) {
        this.inheritAttribute(el, attr);
      }
    }
  }

  /*
   * Implements attribute inheritance by a child element. Uses XBL @inherit
   * syntax of |to=from|. This can be used directly, but for simple cases
   * you should use the inheritedAttributes getter and let the base class
   * handle this for you.
   *
   * @param {element} child
   *        A child element that inherits an attribute.
   * @param {string} attr
   *        An attribute to inherit. Optionally in the form of |to=from|, where
   *        |to| is an attribute defined on custom element, whose value will be
   *        inherited to |from| attribute, defined a child element. Note |from| may
   *        take a special value of "text" to propogate attribute value as
   *        a child's text.
   */
  inheritAttribute(child, attr) {
    let attrName = attr;
    let attrNewName = attr;
    let split = attrName.split("=");
    if (split.length == 2) {
      attrName = split[1];
      attrNewName = split[0];
    }
    let hasAttr = this.hasAttribute(attrName);
    let attrValue = this.getAttribute(attrName);

    // If our attribute hasn't changed since we last inherited, we don't want to
    // propagate it down to the child. This prevents overriding an attribute that's
    // been changed on the child (for instance, [checked]).
    if (!this._inheritedAttributesValuesCache) {
      this._inheritedAttributesValuesCache = new WeakMap();
    }
    if (!this._inheritedAttributesValuesCache.has(child)) {
      this._inheritedAttributesValuesCache.set(child, {});
    }
    let lastInheritedAttributes = this._inheritedAttributesValuesCache.get(child);

    if ((hasAttr && attrValue === lastInheritedAttributes[attrName]) ||
        (!hasAttr && !lastInheritedAttributes.hasOwnProperty(attrName))) {
      // We got a request to inherit an unchanged attribute - bail.
      return;
    }

    // Store the value we're about to pass down to the child.
    if (hasAttr) {
      lastInheritedAttributes[attrName] = attrValue;
    } else {
      delete lastInheritedAttributes[attrName];
    }

    // Actually set the attribute.
    if (attrNewName === "text") {
      child.textContent = hasAttr ? attrValue : "";
    } else if (hasAttr) {
      child.setAttribute(attrNewName, attrValue);
    } else {
      child.removeAttribute(attrNewName);
    }

    if (attrNewName == "accesskey" && child.formatAccessKey) {
      child.formatAccessKey(false);
    }
  }

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

  /**
   * 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.parseFromString(`
      ${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");
    // 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.contentType == "application/vnd.mozilla.xul+xml") {
        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;
    };
  }
};

const MozXULElement = MozElementMixin(XULElement);

/**
 * 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 (propOrMethod instanceof MozQueryInterface) {
            return Reflect.get(target, prop, receiver);
          }
          return function(...args) {
            return propOrMethod.apply(target, args);
          };
        }
        return propOrMethod;
      },
    });
  }

  return obj._customInterfaceProxy;
}

const 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");
      }
    }
  }

  Base.implementCustomInterface(BaseControl,
                                [Ci.nsIDOMXULControlElement]);
  return BaseControl;
};
MozElements.BaseControl = BaseControlMixin(MozXULElement);

const BaseTextMixin = Base => class extends BaseControlMixin(Base) {
  set label(val) {
    this.setAttribute("label", val);
    return val;
  }

  get label() {
    return this.getAttribute("label");
  }

  set crop(val) {
    this.setAttribute("crop", val);
    return val;
  }

  get crop() {
    return this.getAttribute("crop");
  }

  set image(val) {
    this.setAttribute("image", val);
    return val;
  }

  get image() {
    return this.getAttribute("image");
  }

  set command(val) {
    this.setAttribute("command", val);
    return 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;
    }
    return val;
  }

  get accessKey() {
    return this.labelElement ? this.labelElement.accessKey : this.getAttribute("accesskey");
  }
};
MozElements.BaseText = BaseTextMixin(MozXULElement);

// Attach the base class to the window so other scripts can use it:
window.BaseControlMixin = BaseControlMixin;
window.MozElementMixin = MozElementMixin;
window.MozXULElement = MozXULElement;
window.MozElements = MozElements;

customElements.setElementCreationCallback("browser", () => {
  Services.scriptloader.loadSubScript("chrome://global/content/elements/browser-custom-element.js", window);
});

// For now, don't load any elements in the extension dummy document.
// We will want to load <browser> when that's migrated (bug 1441935).
const isDummyDocument = document.documentURI == "chrome://extensions/content/dummy.xul";
if (!isDummyDocument) {
  for (let script of [
    "chrome://global/content/elements/general.js",
    "chrome://global/content/elements/checkbox.js",
    "chrome://global/content/elements/menu.js",
    "chrome://global/content/elements/notificationbox.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/textbox.js",
    "chrome://global/content/elements/tabbox.js",
    "chrome://global/content/elements/tree.js",
    "chrome://global/content/elements/wizard.js",
  ]) {
    Services.scriptloader.loadSubScript(script, window);
  }

  for (let [tag, script] of [
    ["findbar", "chrome://global/content/elements/findbar.js"],
    ["menulist", "chrome://global/content/elements/menulist.js"],
    ["stringbundle", "chrome://global/content/elements/stringbundle.js"],
    ["printpreview-toolbar", "chrome://global/content/printPreviewToolbar.js"],
    ["editor", "chrome://global/content/elements/editor.js"],
    ["text-link", "chrome://global/content/elements/text.js"],
  ]) {
    customElements.setElementCreationCallback(tag, () => {
      Services.scriptloader.loadSubScript(script, window);
    });
  }
}
})();