Bug 1459556 - Part 2 - Remove the "handlers" binding. r=bgrins draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Thu, 17 May 2018 09:23:03 +0100
changeset 796210 b8d11324dcc7de96abcf830d498051afe2e9d9b7
parent 796209 3499235d4e87e2f163e97591e19fbecca8f8c397
push id110180
push userpaolo.mozmail@amadzone.org
push dateThu, 17 May 2018 08:23:50 +0000
reviewersbgrins
bugs1459556
milestone62.0a1
Bug 1459556 - Part 2 - Remove the "handlers" binding. r=bgrins MozReview-Commit-ID: GOUOKuoR1rs
browser/components/preferences/handlers.css
browser/components/preferences/handlers.xml
browser/components/preferences/in-content/main.js
browser/components/preferences/jar.mn
--- a/browser/components/preferences/handlers.css
+++ b/browser/components/preferences/handlers.css
@@ -1,16 +1,12 @@
 /* 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/. */
 
-#handlersView > richlistitem {
-  -moz-binding: url("chrome://browser/content/preferences/handlers.xml#handler");
-}
-
 #containersView > richlistitem {
   -moz-binding: none;
 }
 
 /**
  * Make the icons appear.
  * Note: we display the icon box for every item whether or not it has an icon
  * so the labels of all the items align vertically.
deleted file mode 100644
--- a/browser/components/preferences/handlers.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0"?>
-
-<!-- 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/. -->
-<!-- import-globals-from in-content/main.js -->
-
-<bindings id="handlerBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
-
-  <binding id="handler" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
-    <content>
-      <xul:hbox flex="1" equalsize="always">
-        <xul:hbox flex="1" align="center" xbl:inherits="tooltiptext=typeDescription">
-          <xul:image src="moz-icon://goat?size=16" class="typeIcon"
-                     xbl:inherits="src=typeIcon" height="16" width="16"/>
-          <xul:label flex="1" crop="end" xbl:inherits="value=typeDescription"/>
-        </xul:hbox>
-        <xul:hbox anonid="not-selected" flex="1" align="center" xbl:inherits="tooltiptext=actionDescription">
-          <xul:image xbl:inherits="src=actionIcon" height="16" width="16" class="actionIcon"/>
-          <xul:label flex="1" crop="end" xbl:inherits="value=actionDescription"/>
-        </xul:hbox>
-        <xul:hbox hidden="true" anonid="selected" flex="1">
-          <xul:menulist class="actionsMenu" flex="1" crop="end" selectedIndex="1"
-                        xbl:inherits="tooltiptext=actionDescription"
-                        oncommand="gMainPane.onSelectAction(event.originalTarget)">
-            <xul:menupopup/>
-          </xul:menulist>
-        </xul:hbox>
-      </xul:hbox>
-    </content>
-  </binding>
-
-</bindings>
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -1543,17 +1543,17 @@ var gMainPane = {
     var visibleTypes = this._visibleTypes;
 
     // If the user is filtering the list, then only show matching types.
     if (this._filter.value)
       visibleTypes = visibleTypes.filter(this._matchesFilter, this);
 
     for (let visibleType of visibleTypes) {
       let item = new HandlerListItem(visibleType);
-      this._list.appendChild(item.node);
+      item.connectAndAppendToList(this._list);
 
       if (visibleType.type === lastSelectedType) {
         this._list.selectedItem = item.node;
       }
     }
   },
 
   _matchesFilter(aType) {
@@ -1608,18 +1608,17 @@ var gMainPane = {
 
   /**
    * Rebuild the actions menu for the selected entry.  Gets called by
    * the richlistitem constructor when an entry in the list gets selected.
    */
   rebuildActionsMenu() {
     var typeItem = this._list.selectedItem;
     var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper;
-    var menu =
-      document.getAnonymousElementByAttribute(typeItem, "class", "actionsMenu");
+    var menu = typeItem.querySelector(".actionsMenu");
     var menuPopup = menu.menupopup;
 
     // Clear out existing items.
     while (menuPopup.hasChildNodes())
       menuPopup.removeChild(menuPopup.lastChild);
 
     let internalMenuItem;
     // Add the "Preview in Firefox" option for optional internal handlers.
@@ -2002,18 +2001,17 @@ var gMainPane = {
       // Rebuild the actions menu whether the user picked an app or canceled.
       // If they picked an app, we want to add the app to the menu and select it.
       // If they canceled, we want to go back to their previous selection.
       this.rebuildActionsMenu();
 
       // If the user picked a new app from the menu, select it.
       if (aHandlerApp) {
         let typeItem = this._list.selectedItem;
-        let actionsMenu =
-          document.getAnonymousElementByAttribute(typeItem, "class", "actionsMenu");
+        let actionsMenu = typeItem.querySelector(".actionsMenu");
         let menuItems = actionsMenu.menupopup.childNodes;
         for (let i = 0; i < menuItems.length; i++) {
           let menuItem = menuItems[i];
           if (menuItem.handlerApp && menuItem.handlerApp.equals(aHandlerApp)) {
             actionsMenu.selectedIndex = i;
             this.onSelectAction(menuItem);
             break;
           }
@@ -2447,59 +2445,143 @@ ArrayEnumerator.prototype = {
     return this._contents[this._index++];
   }
 };
 
 function isFeedType(t) {
   return t == TYPE_MAYBE_FEED || t == TYPE_MAYBE_VIDEO_FEED || t == TYPE_MAYBE_AUDIO_FEED;
 }
 
+var gXULDOMParser = new DOMParser();
+gXULDOMParser.forceEnableXULXBL();
+
+/**
+ * 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.
+ */
+function 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;
+  while (currentNode = nodeIterator.nextNode()) {
+    currentNode.remove();
+  }
+  // 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();
+}
+
+let gHandlerListItemFragment = parseXULToFragment(`
+  <richlistitem>
+    <hbox flex="1" equalsize="always">
+      <hbox class="typeContainer" flex="1" align="center">
+        <image class="typeIcon" width="16" height="16"
+               src="moz-icon://goat?size=16"/>
+        <label class="typeDescription" flex="1" crop="end"/>
+      </hbox>
+      <hbox class="actionContainer" flex="1" align="center">
+        <image class="actionIcon" width="16" height="16"/>
+        <label class="actionDescription" flex="1" crop="end"/>
+      </hbox>
+      <hbox class="actionsMenuContainer" flex="1">
+        <menulist class="actionsMenu" flex="1" crop="end" selectedIndex="1">
+          <menupopup/>
+        </menulist>
+      </hbox>
+    </hbox>
+  </richlistitem>
+`);
+
 /**
  * This is associated to <richlistitem> elements in the handlers view.
  */
 class HandlerListItem {
   static forNode(node) {
     return gNodeToObjectMap.get(node);
   }
 
   constructor(handlerInfoWrapper) {
     this.handlerInfoWrapper = handlerInfoWrapper;
-    this.node = document.createElement("richlistitem");
+  }
+
+  setOrRemoveAttributes(iterable) {
+    for (let [selector, name, value] of iterable) {
+      let node = selector ? this.node.querySelector(selector) : this.node;
+      if (value) {
+        node.setAttribute(name, value);
+      } else {
+        node.removeAttribute(name);
+      }
+    }
+  }
+
+  connectAndAppendToList(list) {
+    list.appendChild(document.importNode(gHandlerListItemFragment, true));
+    this.node = list.lastChild;
     gNodeToObjectMap.set(this.node, this);
 
-    this.node.setAttribute("type", this.handlerInfoWrapper.type);
-    this.node.setAttribute("typeDescription",
-                           this.handlerInfoWrapper.typeDescription);
-    if (this.handlerInfoWrapper.smallIcon) {
-      this.node.setAttribute("typeIcon", this.handlerInfoWrapper.smallIcon);
-    } else {
-      this.node.removeAttribute("typeIcon");
-    }
-
+    this.node.querySelector(".actionsMenu").addEventListener("command",
+      event => gMainPane.onSelectAction(event.originalTarget));
+
+    let typeDescription = this.handlerInfoWrapper.typeDescription;
+    this.setOrRemoveAttributes([
+      [null, "type", this.handlerInfoWrapper.type],
+      [".typeContainer", "tooltiptext", typeDescription],
+      [".typeDescription", "value", typeDescription],
+      [".typeIcon", "src", this.handlerInfoWrapper.smallIcon],
+    ]);
     this.refreshAction();
+    this.showActionsMenu = false;
   }
 
   refreshAction() {
-    this.node.setAttribute("actionDescription",
-                           this.handlerInfoWrapper.actionDescription);
-    if (this.handlerInfoWrapper.actionIconClass) {
-      this.node.setAttribute(APP_ICON_ATTR_NAME,
-                             this.handlerInfoWrapper.actionIconClass);
-      this.node.removeAttribute("actionIcon");
-    } else {
-      this.node.removeAttribute(APP_ICON_ATTR_NAME);
-      this.node.setAttribute("actionIcon", this.handlerInfoWrapper.actionIcon);
-    }
+    let actionIconClass = this.handlerInfoWrapper.actionIconClass;
+    let actionDescription = this.handlerInfoWrapper.actionDescription;
+    this.setOrRemoveAttributes([
+      [null, APP_ICON_ATTR_NAME, actionIconClass],
+      [".actionContainer", "tooltiptext", actionDescription],
+      [".actionDescription", "value", actionDescription],
+      [".actionIcon", "src", actionIconClass ? null :
+                             this.handlerInfoWrapper.actionIcon],
+    ]);
   }
 
   set showActionsMenu(value) {
-    document.getAnonymousElementByAttribute(this.node, "anonid", "selected")
-            .setAttribute("hidden", !value);
-    document.getAnonymousElementByAttribute(this.node, "anonid", "not-selected")
-            .setAttribute("hidden", !!value);
+    this.setOrRemoveAttributes([
+      [".actionContainer", "hidden", value],
+      [".actionsMenuContainer", "hidden", !value],
+    ]);
   }
 }
 
 /**
  * This object wraps nsIHandlerInfo with some additional functionality
  * the Applications prefpane needs to display and allow modification of
  * the list of handled types.
  *
--- a/browser/components/preferences/jar.mn
+++ b/browser/components/preferences/jar.mn
@@ -11,17 +11,16 @@ browser.jar:
     content/browser/preferences/clearSiteData.js
     content/browser/preferences/clearSiteData.xul
 *   content/browser/preferences/colors.xul
     content/browser/preferences/colors.js
     content/browser/preferences/connection.xul
     content/browser/preferences/connection.js
     content/browser/preferences/fonts.xul
     content/browser/preferences/fonts.js
-    content/browser/preferences/handlers.xml
     content/browser/preferences/handlers.css
     content/browser/preferences/languages.xul
     content/browser/preferences/languages.js
     content/browser/preferences/permissions.xul
     content/browser/preferences/sitePermissions.xul
     content/browser/preferences/sitePermissions.js
     content/browser/preferences/sitePermissions.css
     content/browser/preferences/containers.xul