--- 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.
*