Bug 1462798 - Create a base custom element class that shares the parseXULToFragment helper;r=paolo draft
authorBrian Grinstead <bgrinstead@mozilla.com>
Sat, 19 May 2018 13:56:06 -0700
changeset 797496 9b6aaf8758f250b13aae35398627eb11df2f20f0
parent 797378 e1fafe357b36270b835acf86f17aef855ce76fa5
push id110499
push userbgrinstead@mozilla.com
push dateSat, 19 May 2018 20:56:22 +0000
reviewerspaolo
bugs1462798
milestone62.0a1
Bug 1462798 - Create a base custom element class that shares the parseXULToFragment helper;r=paolo MozReview-Commit-ID: IG84xKxO9Wc
toolkit/components/processsingleton/MainProcessSingleton.js
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/tests/chrome/chrome.ini
toolkit/content/tests/chrome/test_custom_element_base.xul
toolkit/content/widgets/general.js
toolkit/content/widgets/stringbundle.js
toolkit/profile/content/createProfileWizard.xul
toolkit/profile/content/profileSelection.xul
tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
--- a/toolkit/components/processsingleton/MainProcessSingleton.js
+++ b/toolkit/components/processsingleton/MainProcessSingleton.js
@@ -79,22 +79,18 @@ MainProcessSingleton.prototype = {
     case "document-element-inserted":
       // Set up Custom Elements for XUL windows before anything else happens
       // in the document. Anything loaded here should be considered part of
       // core XUL functionality. Any window-specific elements can be registered
       // via <script> tags at the top of individual documents.
       const doc = subject;
       if (doc.nodePrincipal.isSystemPrincipal &&
           doc.contentType == "application/vnd.mozilla.xul+xml") {
-        for (let script of [
-          "chrome://global/content/elements/stringbundle.js",
-          "chrome://global/content/elements/general.js",
-        ]) {
-          Services.scriptloader.loadSubScript(script, doc.ownerGlobal);
-        }
+        Services.scriptloader.loadSubScript(
+          "chrome://global/content/customElements.js", doc.ownerGlobal);
       }
       break;
 
     case "xpcom-shutdown":
       Services.mm.removeMessageListener("Search:AddEngine", this.addSearchEngine);
       Services.obs.removeObserver(this, "document-element-inserted");
       break;
     }
new file mode 100644
--- /dev/null
+++ b/toolkit/content/customElements.js
@@ -0,0 +1,73 @@
+/* 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/. */
+
+"use strict";
+
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const gXULDOMParser = new DOMParser();
+gXULDOMParser.forceEnableXULXBL();
+
+class MozXULElement extends XULElement {
+  /**
+   * 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 str
+   *        String with the XML representation of XUL elements.
+   *
+   * @return DocumentFragment containing the corresponding element tree, including
+   *         element nodes but excluding any text node.
+   */
+  static parseXULToFragment(str) {
+    let doc = gXULDOMParser.parseFromString(`
+      <box xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+        ${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.firstChild);
+    return range.extractContents();
+  }
+}
+
+// Attach the base class to the window so other scripts can use it:
+window.MozXULElement = MozXULElement;
+
+for (let script of [
+  "chrome://global/content/elements/stringbundle.js",
+  "chrome://global/content/elements/general.js",
+]) {
+  Services.scriptloader.loadSubScript(script, window);
+}
+
+}
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -41,16 +41,17 @@ toolkit.jar:
 *   content/global/buildconfig.html
    content/global/buildconfig.css
    content/global/contentAreaUtils.js
    content/global/datepicker.xhtml
 #ifndef MOZ_FENNEC
    content/global/editMenuOverlay.js
 #endif
    content/global/filepicker.properties
+   content/global/customElements.js
    content/global/globalOverlay.js
    content/global/mozilla.xhtml
    content/global/aboutMozilla.css
    content/global/preferencesBindings.js
    content/global/process-content.js
    content/global/resetProfile.css
    content/global/resetProfile.js
    content/global/resetProfile.xul
--- a/toolkit/content/tests/chrome/chrome.ini
+++ b/toolkit/content/tests/chrome/chrome.ini
@@ -96,16 +96,17 @@ support-files = bug451540_window.xul
 skip-if = (os == 'mac' && os_version == '10.10') # Unexpectedly perma-passes on OSX 10.10
 [test_bug792324.xul]
 [test_bug1048178.xul]
 skip-if = toolkit == "cocoa"
 [test_button.xul]
 [test_closemenu_attribute.xul]
 [test_colorpicker_popup.xul]
 [test_contextmenu_list.xul]
+[test_custom_element_base.xul]
 [test_deck.xul]
 [test_dialogfocus.xul]
 [test_findbar.xul]
 subsuite = clipboard
 [test_findbar_entireword.xul]
 [test_findbar_events.xul]
 [test_focus_anons.xul]
 [test_hiddenitems.xul]
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/test_custom_element_base.xul
@@ -0,0 +1,45 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Custom Element Base Class Tests"
+  onload="runTests();"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
+
+  <!-- test code goes here -->
+  <script type="application/javascript"><![CDATA[
+
+  SimpleTest.waitForExplicitFinish();
+
+  function runTests() {
+    ok(MozXULElement, "MozXULElement defined on the window");
+    testParseXULToFragment();
+    SimpleTest.finish();
+  }
+
+  function testParseXULToFragment() {
+    ok(MozXULElement.parseXULToFragment, "parseXULToFragment helper exists");
+
+    let frag = MozXULElement.parseXULToFragment(`<deck id='foo' />`);
+    ok(frag instanceof DocumentFragment);
+
+    document.documentElement.appendChild(frag);
+
+    let deck = document.documentElement.lastChild;
+    ok(deck instanceof MozXULElement, "instance of MozXULElement");
+    ok(deck instanceof XULElement, "instance of XULElement");
+    is(deck.id, "foo", "attribute set");
+    is(deck.selectedIndex, "0", "Custom Element is property attached");
+    deck.remove();
+  }
+
+  ]]>
+  </script>
+</window>
+
--- a/toolkit/content/widgets/general.js
+++ b/toolkit/content/widgets/general.js
@@ -1,17 +1,17 @@
 /* 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/. */
 
 "use strict";
 
 {
 
-class MozDeck extends XULElement {
+class MozDeck extends MozXULElement {
   set selectedIndex(val) {
     if (this.selectedIndex == val) return val;
     this.setAttribute("selectedIndex", val);
     var event = document.createEvent("Events");
     event.initEvent("select", true, true);
     this.dispatchEvent(event);
     return val;
   }
@@ -30,17 +30,17 @@ class MozDeck extends XULElement {
 
   get selectedPanel() {
     return this.childNodes[this.selectedIndex];
   }
 }
 
 customElements.define("deck", MozDeck);
 
-class MozDropmarker extends XULElement {
+class MozDropmarker extends MozXULElement {
   connectedCallback() {
     // Only create the image the first time we are connected
     if (!this.firstChild) {
       let image = document.createElement("image");
       image.classList.add("dropmarker-icon");
       this.appendChild(image);
     }
   }
--- a/toolkit/content/widgets/stringbundle.js
+++ b/toolkit/content/widgets/stringbundle.js
@@ -5,17 +5,17 @@
 "use strict";
 
 // This is loaded into all XUL windows. Wrap in a block to prevent
 // leaking to window scope.
 {
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-class MozStringbundle extends XULElement {
+class MozStringbundle extends MozXULElement {
   get stringBundle() {
     if (!this._bundle) {
       try {
         this._bundle = Services.strings.createBundle(this.src);
       } catch (e) {
         dump("Failed to get stringbundle:\n");
         dump(e + "\n");
       }
--- a/toolkit/profile/content/createProfileWizard.xul
+++ b/toolkit/profile/content/createProfileWizard.xul
@@ -16,17 +16,17 @@
         title="&newprofile.title;"
         xmlns:html="http://www.w3.org/1999/xhtml"
         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onwizardfinish="return onFinish();"
         onload="initWizard();"
         style="&window.size;">
   <script type="application/javascript"
-          src="chrome://global/content/elements/stringbundle.js"/>
+          src="chrome://global/content/customElements.js"/>
 
   <stringbundle id="bundle_profileManager"
                 src="chrome://mozapps/locale/profile/profileSelection.properties"/>
 
   <script type="application/javascript" src="chrome://mozapps/content/profile/createProfileWizard.js"/>
 
   <wizardpage id="explanation" onpageshow="enableNextButton();">
     <description>&profileCreationExplanation_1.text;</description>
--- a/toolkit/profile/content/profileSelection.xul
+++ b/toolkit/profile/content/profileSelection.xul
@@ -25,17 +25,17 @@
   style="width: 30em;"
   onload="startup();"
   ondialogaccept="return acceptDialog()"
   ondialogcancel="return exitDialog()"
   buttonlabelaccept="&start.label;"
   buttonlabelcancel="&exit.label;">
 
   <script type="application/javascript"
-          src="chrome://global/content/elements/stringbundle.js"/>
+          src="chrome://global/content/customElements.js"/>
 
   <stringbundle id="bundle_profileManager"
                 src="chrome://mozapps/locale/profile/profileSelection.properties"/>
   <stringbundle id="bundle_brand"
                 src="chrome://branding/locale/brand.properties"/>
 
   <script type="application/javascript" src="chrome://mozapps/content/profile/profileSelection.js"/>
 
--- a/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
@@ -23,16 +23,17 @@ const rootDir = helpers.rootDir;
 // When updating EXTRA_SCRIPTS or MAPPINGS, be sure to also update the
 // 'support-files' config in `tools/lint/eslint.yml`.
 
 // These are scripts not included in global-scripts.inc, but which are loaded
 // via overlays.
 const EXTRA_SCRIPTS = [
   "browser/base/content/nsContextMenu.js",
   "toolkit/content/contentAreaUtils.js",
+  "toolkit/content/customElements.js",
   "browser/components/places/content/editBookmark.js",
   "browser/components/downloads/content/downloads.js",
   "browser/components/downloads/content/indicator.js",
   // Via editMenuCommands.inc.xul
   "toolkit/content/editMenuOverlay.js"
 ];
 
 const extraDefinitions = [