Bug 1411707 - (currently on autoland) - Create a base custom element class that shares the parseXULToFragment helper;
MozReview-Commit-ID: IG84xKxO9Wc
--- 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 = [