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

ChromeUtils.import("resource://gre/modules/Services.jsm");
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 {

  /*
   * Implements attribute inheritance by a child element. Uses XBL @inherit
   * syntax of |to=from|.
   *
   * @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];
    }

    if (attrNewName === "text") {
      child.textContent =
        this.hasAttribute(attrName) ? this.getAttribute(attrName) : "";
    } else if (this.hasAttribute(attrName)) {
      child.setAttribute(attrNewName, this.getAttribute(attrName));
    } else {
      child.removeAttribute(attrNewName);
    }
  }

  /**
   * 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) {
      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.createElement("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;
}

MozElements.BaseControl = class BaseControl extends MozXULElement {
  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(MozElements.BaseControl,
                                       [Ci.nsIDOMXULControlElement]);

// Attach the base class to the window so other scripts can use it:
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/notificationbox.js",
    "chrome://global/content/elements/radio.js",
    "chrome://global/content/elements/textbox.js",
    "chrome://global/content/elements/tabbox.js",
    "chrome://global/content/elements/tree.js",
  ]) {
    Services.scriptloader.loadSubScript(script, window);
  }

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