browser/components/tasks/content/taskBindings.xml
author Blair McBride <bmcbride@mozilla.com>
Tue, 20 Oct 2009 12:20:45 +1300
changeset 34254 3865bf230c4917cfb03e40056edbc5ff4eed99ad
parent 33530 d8593a4771cb708584c93de36bd83c93d098481d
permissions -rw-r--r--
Merge from mozilla-central

<?xml version="1.0"?>

# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is Taskfox.
#
# The Initial Developer of the Original Code is
# Blair McBride.
# Portions created by the Initial Developer are Copyright (C) 2009
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#   Blair McBride <bmcbride@mozilla.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****

<!DOCTYPE bindings [
<!ENTITY % tasksDTD SYSTEM "chrome://browser/locale/tasks.dtd" >
%tasksDTD;
]>

<bindings id="taskBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:html="http://www.w3.org/1999/xhtml"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:xbl="http://www.mozilla.org/xbl">
  
  <binding id="verb-menulist" display="xul:menu" extends="chrome://global/content/bindings/menulist.xml#menulist">
    <content>
      <xul:hbox class="menulist-label-box" flex="1">
        <xul:image class="menulist-icon" xbl:inherits="src=image,src"/>
        <xul:label class="menulist-label" xbl:inherits="value=label,crop,accesskey" crop="right" flex="1"/>
      </xul:hbox>
      <xul:dropmarker class="verb-dropmarker" type="menu" xbl:inherits="disabled,open"/>
      <xul:menupopup anonid="popup" oncommand="this.parentNode.currentVerb = event.target.value; this.parentNode.doCommand();">
        <xul:menuseparator anonid="verbs-separator"/>
        <xul:menuitem anonid="manage-verbs" label="&manageTasks.label;" accesskey="&manageTasks.accesskey;"/>
      </xul:menupopup>
    </content>
    <implementation>
      <constructor><![CDATA[
        this.setAttribute("collapsed", true);
      ]]></constructor>
      <field name="mVerbManager" readonly="true">
        Components.classes["@mozilla.org/tasks/verbmanager;1"]
                  .getService(Components.interfaces.nsITaskVerbManager);
      </field>
      <field name="_currentVerb">null</field>
      <property name="currentVerb">
        <getter><![CDATA[
          return this._currentVerb;
        ]]></getter>
        <setter><![CDATA[
          this.ensurePopulated();
          var verb = null;
          if (typeof val == "string")
            verb = this.mVerbManager.getVerbByAlias(val);
          else if (val instanceof Components.interfaces.nsITaskVerb)
            verb = val;
          
          if (verb) {
            this.setAttribute("label", verb.name);
            this.setAttribute("image", verb.iconURI);
            this.removeAttribute("collapsed");
          } else {
            this.setAttribute("collapsed", true);
            this.setAttribute("label", "");
            this.setAttribute("image", "");
          }
          this._currentVerb = verb;
          return verb;
        ]]></setter>
      </property>
      <method name="ensurePopulated">
        <body><![CDATA[
          var popup = document.getAnonymousElementByAttribute(this,
                                                              "anonid",
                                                              "popup");
          var separator = document.getAnonymousElementByAttribute(this,
                                                                  "anonid",
                                                                  "verbs-separator");
          
          if (separator.previousSibling)
            return;
          
          var verbs = this.mVerbManager.getEnabledVerbs({});
          var menuitem = null;
          for each (let verb in verbs) {
            menuitem = document.createElement("menuitem");
            menuitem.setAttribute("class", "menuitem-iconic");
            menuitem.setAttribute("type", "radio");
            menuitem.setAttribute("name", "verb");
            menuitem.setAttribute("autocheck", true);
            menuitem.setAttribute("label", verb.name);
            menuitem.setAttribute("image", verb.iconURI);
            menuitem.setAttribute("value", verb.alias);
            //if (verb == gCurrentVerb)
            //  menuitem.setAttribute("checked", true);
            popup.insertBefore(menuitem, separator);
          }
        ]]></body>
      </method>
    </implementation>
    <handlers>
      <handler event="popupshowing">
        this.ensurePopulated();
      </handler>
    </handlers>
  </binding>
  
  <binding id="display-popup" extends="chrome://global/content/bindings/popup.xml#panel">
    <content>
      <xul:vbox flex="1">
        <xul:hbox anonid="viewport-container" flex="1">
          <xul:stack anonid="viewport" class="task-viewport" flex="1"/>
        </xul:hbox>
        <xul:hbox anonid="toolbar" class="toolbar">
          <xul:button anonid="drag" class="drag" label="&detach.label;"/>
          <xul:spacer flex="1"/>
        </xul:hbox>
      </xul:vbox>
    </content>
    <implementation implements="nsIDOMEventListener">
      <constructor><![CDATA[
        this.mViewport.mPopup = this;
        var dragNode = document.getAnonymousElementByAttribute(this,
                                                               "anonid",
                                                               "drag");
        dragNode.addEventListener("mousedown", this, true);
      ]]></constructor>
      
      <field name="_dragOffsetX">0</field>
      <field name="_dragOffsetY">0</field>
      <field name="mViewport" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "viewport");
      ]]></field>
      <field name="mViewportContainer" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "viewport-container");
      ]]></field>
      <field name="mInput">null</field>
      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.type == "mousedown")
            return this.onMouseDown(aEvent);
          if (aEvent.type == "mousemove")
            return this.onMouseMove(aEvent);
          if (aEvent.type == "mouseup")
            return this.onMouseUp(aEvent);
          return false;
        ]]></body>
      </method>
      <method name="onMouseDown">
        <parameter name="aEvent"/>
        <body><![CDATA[
          this._dragOffsetX = aEvent.screenX - this.boxObject.screenX;
          this._dragOffsetY = aEvent.screenY - this.boxObject.screenY;
          
          this.addEventListener("mousemove", this, true);
          this.addEventListener("mouseup", this, true);
        ]]></body>
      </method>
      <method name="onMouseMove">
        <parameter name="aEvent"/>
        <body><![CDATA[
          this.moveTo(aEvent.screenX - this._dragOffsetX,
                      aEvent.screenY - this._dragOffsetY);
        ]]></body>
      </method>
      <method name="onMouseUp">
        <parameter name="aEvent"/>
        <body><![CDATA[
          this.removeEventListener("mousemove", this, true);
          this.removeEventListener("mouseup", this, true);
          
          var screenPos = {
            x: this.boxObject.screenX,
            y: this.boxObject.screenY,
            height: this.clientHeight,
            width: this.clientWidth
          };
          this.hidePopup();
          window.openDialog("chrome://browser/content/taskWindow.xul",
                            null,
                            "resizable=yes",
                            this.mViewport,
                            screenPos);
        ]]></body>
      </method>
      <method name="displayTask">
        <parameter name="aQuery"/>
        <body><![CDATA[
          this.hidden = false;
          this.openPopup(this.mInput, "after_start");
          this.mViewport.displayTask(aQuery);
        ]]></body>
      </method>
      <method name="resizeToAnchor">
        <parameter name="aAnchor"/>
        <body><![CDATA[
          var rect = aAnchor.getBoundingClientRect();
          var nav = aAnchor.ownerDocument
                           .defaultView
                           .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                           .getInterface(Components.interfaces.nsIWebNavigation);
          var docShell = nav.QueryInterface(Components.interfaces.nsIDocShell);
          var docViewer = docShell.contentViewer
                                  .QueryInterface(Components.interfaces.nsIMarkupDocumentViewer);
          var width = (rect.right - rect.left) * docViewer.fullZoom;
          width = width > 100 ? width : 100;
          this.width = width;
          var viewportWidth = this.mViewportContainer.boxObject.width;
          this.mViewport.resizeTo(viewportWidth);
        ]]></body>
      </method>
    </implementation>
    <handlers>
      <handler event="popupshown"><![CDATA[
        this.mViewport.reset();
        this.resizeToAnchor(this.mInput);
      ]]></handler>
      <handler event="popuphidden"><![CDATA[
        this.mViewport.resetDetailDisplay();
      ]]></handler>
    </handlers>
  </binding>
  
  <binding id="viewport">
    <content>
        <xul:vbox anonid="display-container" class="display-container" left="0"/>
        <xul:vbox anonid="detail-container" class="detail-container" left="0">
          <xul:hbox>
            <xul:button anonid="detail-back-button" label="Go back" flex="1"
                        stylez="-moz-appearance: none; background-color: #CCC;"
                        oncommand="this.parentNode.parentNode.parentNode.hideDetailDisplay();"/>
            <xul:button label="Open in tab"/>
          </xul:hbox>
          <!-- XXX if this is in a popup, the browser won't respond to mouse
               input. See bug 130078. -->
          <xul:browser anonid="detail" type="content" src="about:blank" flex="1"/>
      </xul:vbox>
    </content>
    <implementation implements="nsIObserver">
      <field name="mPopup">null</field>
      <field name="mDisplayContainer" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "display-container");
      ]]></field>
      <field name="mDisplay">null</field>
      <field name="mDetailContainer" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "detail-container");
      ]]></field>
      <field name="mDetail"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "detail");
      ]]></field>
      
      <method name="reset">
        <body><![CDATA[
          this.mViewingDetail = false;
          this.mDisplayContainer.left = 0;
          this.mDetailContainer.left = this.mDisplayContainer.width;
        ]]></body>
      </method>
      <method name="resizeTo">
        <parameter name="aWidth"/>
        <body><![CDATA[
          this.width = aWidth;
          this.mDisplayContainer.width = aWidth;
          this.mDisplayContainer.height = this.boxObject.height;
          this.mDetailContainer.width = aWidth;
          this.mDetailContainer.height = this.boxObject.height;
          
          if (this.mViewingDetail)
            this.mDisplayContainer.left = 0 - aWidth;
          else
            this.mDetailContainer.left = aWidth;
        ]]></body>
      </method>
      <method name="displayTask">
        <parameter name="aQuery"/>
        <body><![CDATA[
          this.makeDisplay(aQuery.verb);
          
          if (this.mDisplay) {
            this.mDisplay.taskQuery = aQuery;
            this.mDisplay.mViewport = this;
          }
        ]]></body>
      </method>
      <method name="makeDisplay">
        <parameter name="aVerb"/>
        <body><![CDATA[
          while (this.mDisplayContainer.hasChildNodes())
            this.mDisplayContainer.removeChild(this.mDisplayContainer.firstChild);
          this.mDisplay = null;
          
          const nsITaskVerb = Components.interfaces.nsITaskVerb;
          if (aVerb.displayType == nsITaskVerb.DISPLAYTYPE_NONE)
            return null;
          
          
          const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
          var display = document.createElementNS(NS_XUL, "taskdisplay");
          display.setAttribute("flex", 1);
          
          if (aVerb.displayType == nsITaskVerb.DISPLAYTYPE_TEMPLATE) {
            display.setAttribute("type", "template");
            aVerb.QueryInterface(Components.interfaces.nsITaskVerbDisplayXULTemplate);
            display.setAttribute("displaytemplate", aVerb.displayTemplate);
            aVerb.QueryInterface(Components.interfaces.nsITaskVerb);
          } else if (aVerb.displayType == nsITaskVerb.DISPLAYTYPE_CONTENT) {
            display.setAttribute("type", "content");
          } else { // must be a chrome display
            display.setAttribute("type", "chrome");
          }
          
          this.mDisplay = display;
          this.mDisplayContainer.appendChild(display);
          // hack used to force a synchronous binding
          var displayWidth = display.boxObject.width;
          
          return display;
        ]]></body>
      </method>
      <field name="mAnimationTimer"><![CDATA[
        Components.classes["@mozilla.org/timer;1"]
                  .createInstance(Components.interfaces.nsITimer);
      ]]></field>
      <field name="mAnimationFunction">null</field>
      <field name="mAnimationSpeed">1</field>
      <field name="mViewingDetail">false</field>
      <method name="observe">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body><![CDATA[
          if (aTopic == "timer-callback" && aSubject == this.mAnimationTimer) {
            this.mAnimationFunction();
          }
        ]]></body>
      </method>
      <method name="_startAnimation">
        <parameter name="aTargetIsDetail"/>
        <body><![CDATA[
          var self = this;
          var lastUpdateTime = Date.now();
          this.mViewingDetail = aTargetIsDetail;
          this.mAnimationTimer.cancel();
          
          function calcTimeDiff() {
            var currentTime = Date.now();
            var timeDiff = currentTime - lastUpdateTime;
            lastUpdateTime = currentTime;
            return timeDiff;
          }
          
          function animStepToDetail(displayOffset, detailOffset) {
            var timeDiff = calcTimeDiff();
            var done = false;
            var computedDifference = 0 - (self.mAnimationSpeed * timeDiff);
            if (detailOffset < computedDifference) {
              computedDifference = 0 - detailOffset;
              done = true;
            }
            return [computedDifference, done];
          }
          
          function animStepToDisplay(displayOffset, detailOffset) {
            var timeDiff = calcTimeDiff();
            var done = false;
            var computedDifference = self.mAnimationSpeed * timeDiff;
            if (displayOffset > computedDifference) {
              computedDifference = 0 - displayOffset;
              done = true;
            }
            return [computedDifference, done];
          }
          
          var animCalc = aTargetIsDetail ? animStepToDetail : animStepToDisplay;
          
          this.mAnimationFunction = function() {
            var displayOffset = parseInt(self.mDisplayContainer.left);
            var detailOffset = parseInt(self.mDetailContainer.left);
            var [difference, done] = animCalc(displayOffset, detailOffset);
            self.mDisplayContainer.left = displayOffset + difference;
            self.mDetailContainer.left = detailOffset + difference;
            
            if (done) {
              self.mAnimationTimer.cancel();
              if (!this.mViewingDetail)
                this.resetDetailDisplay();
            }
          };
          
          const nsITimer = Components.interfaces.nsITimer;
          this.mAnimationTimer.init(this, 10, nsITimer.TYPE_REPEATING_SLACK);
        ]]></body>
      </method>
      <method name="hideDetailDisplay">
        <body><![CDATA[
          if (!this.mViewingDetail)
            return;
          
          this._startAnimation(false);
        ]]></body>
      </method>
      <method name="expandDetailDisplay">
        <parameter name="aProvider"/>
        <parameter name="aURL"/>
        <parameter name="aData"/>
        <body><![CDATA[
          if (this.mViewingDetail)
            return;
          
          aProvider.generateDisplay(this.mDetail.docShell, aURL.clone(), aData);
          
          this._startAnimation(true);
        ]]></body>
      </method>
      <method name="maybeClosePopup">
        <body><![CDATA[
          if (this.mPopup)
            this.mPopup.hidePopup();
        ]]></body>
      </method>
      <method name="migrateState">
        <parameter name="aOther"/>
        <body><![CDATA[
          var query = aOther.mDisplay.taskQuery.clone();
          this.displayTask(query);
          
          this.mDetail.swapDocShells(aOther.mDetail);
          this.mViewingDetail = aOther.mViewingDetail;
        ]]></body>
      </method>
      <method name="resetDetailDisplay">
        <body><![CDATA[
          this.mDetail.docShell.document.defaultView.location.assign("about:blank");
        ]]></body>
      </method>
      <method name="handleKeyPress">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (!this.mDisplay)
            return;
          const nsIDOMKeyEvent = Components.interfaces.nsIDOMKeyEvent;
          var isHandled = false;
          
          if (this.mDisplay.getAttribute("type") == "template") {
            switch (aEvent.keyCode) {
              case nsIDOMKeyEvent.DOM_VK_UP:
                this.mDisplay.selectPreviousResult();
                isHandled = true;
                break;
              case nsIDOMKeyEvent.DOM_VK_DOWN:
                this.mDisplay.selectNextResult();
                isHandled = true;
                break;
            }
          }
          
          if (isHandled) {
            aEvent.stopPropagation();
            aEvent.preventDefault();
          }
        ]]></body>
      </method>
    </implementation>
  </binding>
  
  
  <binding id="modifier-freeform">
    <content>
      <xul:label anonid="label" xbl:inherits="value=label"/>
      <xul:input anonid="input" xbl:inherits="value"
                onchange="this.parentNode.syncValue();"/>
    </content>
    <implementation>
      <field name="mInput" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "input");
      ]]></field>
      <property name="value">
        <getter><![CDATA[
          return this.mInput.value;
        ]]></getter>
        <setter><![CDATA[
          this.mInput.value = val;
        ]]></setter>
      </property>
      <method name="syncValue">
        <body><![CDATA[
          if (this.mController)
            this.mController.handleModifier(this.getAttribute("alias"),
                                            this.value);
        ]]></body>
      </method>
    </implementation>
  </binding>
  
  <binding id="modifier-list">
    <content>
      <xul:label anonid="label" class="label" xbl:inherits="value=label"/>
      <xul:menulist anonid="input" class="input" oncommand="this.parentNode.syncValue();">
        <xul:menupopup/>
      </xul:menulist>
    </content>
    <implementation>
      <constructor><![CDATA[
        if (this.mNountype.async) {
          Components.utils.reportError("Async nountypes not implemented yet.");
          return;
        }
        
        var values = this.mNountype.values;
        for each (let item in values) {
          let menuitem = this.mInput.appendItem(item.name, item.value);
        }
        this.mInput.selectedIndex = 0;
        this.syncValue();
      ]]></constructor>
      <field name="mInput" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "input");
      ]]></field>
      <field name="mPopup" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "popup");
      ]]></field>
      <property name="value">
        <getter><![CDATA[
          return this.mInput.value;
        ]]></getter>
        <setter><![CDATA[
          var items = this.mInput.menupopup.childNodes;
          for (var i = 0; i < items.length; i++) {
            if (items[i].value == val) {
              this.mInput.selectedIndex = i;
              return;
            }
          }
        ]]></setter>
      </property>
      <method name="syncValue">
        <body><![CDATA[
          if (this.mController)
            this.mController.handleModifier(this.getAttribute("alias"),
                                            this.value);
        ]]></body>
      </method>
    </implementation>
  </binding>
  
  <binding id="modifier-number">
    <content>
      <xul:label anonid="label" xbl:inherits="value=label"/>
      <xul:input anonid="input" type="number" xbl:inherits="value"
                 onchange="this.parentNode.syncValue();"/>
    </content>
    <implementation>
      <field name="mInput" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "input");
      ]]></field>
      <property name="value">
        <getter><![CDATA[
          return this.mInput.value;
        ]]></getter>
        <setter><![CDATA[
          this.mInput.value = val;
        ]]></setter>
      </property>
      <method name="syncValue">
        <body><![CDATA[
          if (this.mController)
            this.mController.handleModifier(this.getAttribute("alias"),
                                            this.value);
        ]]></body>
      </method>
    </implementation>
  </binding>
  
  <binding id="display-content">
    <content>
      <xul:browser anonid="browser" type="content" flex="1"/>
    </content>
    <implementation implements="nsIDOMEventListener">
      <constructor><![CDATA[
        Components.utils.import("resource://gre/modules/VerbUtils.jsm");
        this.mVerbUtils = VerbUtils;
        
        this.mBrowser.addEventListener("load", this, true);
      ]]></constructor>
      <destructor><![CDATA[
        this.mBrowser.removeEventListener("load", this, true);
        this.mBrowser.contentDocument.removeEventListener("Task:Ready", this, true);
      ]]></destructor>
      <field name="mVerbUtils">null</field>
      <field name="mViewport">null</field>
      <field name="mTaskQuery">null</field>
      <field name="mBrowser" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this, "anonid", "browser");
      ]]></field>
      <property name="taskQuery">
        <getter><![CDATA[
          return this.mTaskQuery;
        ]]></getter>
        <setter><![CDATA[
          this.mTaskQuery = val;
          var verb = this.mTaskQuery.verb;
          verb.QueryInterface(Components.interfaces.nsITaskVerbDisplayContent);
          var url = verb.generateUrl(this.mTaskQuery);
          if (url.indexOf("chrome://") == 0)
            url = this.mVerbUtils.fileToDataUri(url);
          this.mBrowser.loadURI(url, null, null);
        ]]></setter>
      </property>
      <property name="documentURI" readonly="true">
        <getter><![CDATA[
          return this.mBrowser.contentDocument.location.href;
        ]]></getter>
      </property>
      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.type == "load") {
            this.mBrowser.contentDocument.addEventListener("Task:Ready", this, true);
          } else if (aEvent.type == "Task:Ready") {
            this.mBrowser.contentDocument.removeEventListener("Task:Ready", this, true);
            var verb = this.mTaskQuery.verb;
            verb.onDisplayLoaded(this.mTaskQuery, this.mBrowser.contentWindow);
          }
        ]]></body>
      </method>
    </implementation>
  </binding>
  
  <binding id="display-chrome">
    <content>
      <xul:browser anonid="browser" flex="1"/>
    </content>
    <implementation implements="nsIDOMEventListener">
      <constructor><![CDATA[
        Components.utils.import("resource://gre/modules/VerbUtils.jsm");
        this.mVerbUtils = VerbUtils;
        
        this.mBrowser.addEventListener("load", this, true);
      ]]></constructor>
      <destructor><![CDATA[
        this.mBrowser.removeEventListener("load", this, true);
        this.mBrowser.contentDocument.removeEventListener("Task:Ready", this, true);
      ]]></destructor>
      <field name="mVerbUtils">null</field>
      <field name="mViewport">null</field>
      <field name="mTaskQuery">null</field>
      <field name="mBrowser" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this, "anonid", "browser");
      ]]></field>
      <property name="taskQuery">
        <getter><![CDATA[
          return this.mTaskQuery;
        ]]></getter>
        <setter><![CDATA[
          this.mTaskQuery = val;
          var verb = this.mTaskQuery.verb;
          verb.QueryInterface(Components.interfaces.nsITaskVerbDisplayChrome);
          var url = verb.generateUrl(this.mTaskQuery);
          this.mBrowser.loadURI(url, null, null);
        ]]></setter>
      </property>
      <property name="documentURI" readonly="true">
        <getter><![CDATA[
          return this.mBrowser.contentDocument.location.href;
        ]]></getter>
      </property>
      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.type == "load") {
            this.mBrowser.contentDocument.addEventListener("Task:Ready", this, true);
          } else if (aEvent.type == "Task:Ready") {
            this.mBrowser.contentDocument.removeEventListener("Task:Ready", this, true);
            var verb = this.mTaskQuery.verb;
            verb.onDisplayLoaded(this.mTaskQuery, this.mBrowser.contentWindow);
          }
        ]]></body>
      </method>
    </implementation>
  </binding>

  <binding id="display-template-base">
    <implementation implements="nsIXULBuilderListener">
      <constructor><![CDATA[
          if (!this.hasAttribute("displaytemplate"))
            this.setAttribute("displaytemplate", "none");
          
          Components.utils.import("resource://gre/modules/VerbUtils.jsm");
          this.mVerbUtils = VerbUtils;
        ]]></constructor>
      <field name="mVerbUtils">null</field>
      <field name="mViewport">null</field>
      <field name="templateRoot" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "template-root");
      ]]></field>
      <field name="templateQuery" readonly="true"><![CDATA[
        document.getAnonymousElementByAttribute(this,
                                                "anonid",
                                                "template-query");
      ]]></field>
      <field name="mSelectedResult">null</field> 
      <field name="mLastMoveTime">Date.now()</field> 
      <property name="taskQuery">
        <getter><![CDATA[
          return this.templateQuery.mTaskQuery;
        ]]></getter>
        <setter><![CDATA[
          this.templateQuery.mTaskQuery = val;
          // this needs to be a url (thanks to bug 321170)
          this.templateRoot.datasources = "verb:" + val.verb.alias;
        ]]></setter>
      </property>
      <property name="displayTemplate">
        <getter><![CDATA[
          return this.getAttribute("displaytemplate");
        ]]></getter>
        <setter><![CDATA[
          this.setAttribute("displaytemplate", val);
          return val;
        ]]></setter>
      </property>
      <method name="Initialize">
        <body><![CDATA[
            this.templateRoot.builder.addListener(this);
          ]]></body>
      </method>
      <method name="willRebuild">
        <parameter name="aBuilder"/>
        <body/>
      </method>
      <method name="didRebuild">
        <parameter name="aBuilder"/>
        <body><![CDATA[
          var expanders = this.templateRoot
                              .getElementsByClassName("detail-expander");
          expanders = Array.filter(expanders, function(aElem) {
            if (aElem.localName != "button")
              return false;
            if (!aElem.hasAttribute("url"))
              return false;
            var parent = aElem.parentNode;
            while (parent != this.templateRoot) {
              if (parent.localName == "action")
                return false;
              parent = parent.parentNode;
            }
            return true;
          });
          var detailManager = Components.classes["@mozilla.org/tasks/detailmanager;1"]
                                        .getService(Components.interfaces.nsITaskDetailManager);
          
          function newURL(spec) {
            const nsIStandardURL = Components.interfaces.nsIStandardURL;
            var url = Components.classes["@mozilla.org/network/standard-url;1"]
                                .createInstance(nsIStandardURL);
            url.init(nsIStandardURL.URLTYPE_STANDARD,
                     0,
                     spec,
                     null,
                     null);
            return url.QueryInterface(Components.interfaces.nsIURL);
          }
          
          
          var self = this;
          
          for each (let expander in expanders) {
            let url = newURL(expander.getAttribute("url"));
            let data = expander.getAttribute("data");
            let provider = detailManager.getProviderForURL(url, data);
            if (provider) {
              expander.setAttribute("detailprovider", provider.alias);
              expander.addEventListener("click", function(aEvent) {
                aEvent.stopPropagation();
                // XXX make sure this isn't leaking!
                self.mViewport.expandDetailDisplay(provider, url, data);
              }, false);
            }
          }
        ]]></body>
      </method>
      <method name="selectResult">
        <parameter name="aResult"/>
        <body><![CDATA[
          if (this.mSelectedResult)
            this.mSelectedResult.removeAttribute("selected");
          
          aResult.setAttribute("selected", true);
          this.mSelectedResult = aResult;
        ]]></body>
      </method>
      <method name="selectNextResult">
        <body><![CDATA[
          var nextResult = null;
          if (this.mSelectedResult)
            nextResult = this.mSelectedResult.nextSibling;
          if (!nextResult) {
            // templateRoot's first child will always be the <template> node
            nextResult = this.templateRoot.firstChild.nextSibling;
          }
          this.selectResult(nextResult);
        ]]></body>
      </method>
      <method name="selectPreviousResult">
        <body><![CDATA[
          var previousResult = null;
          if (this.mSelectedResult) {
            previousResult = this.mSelectedResult.previousSibling;
            if (previousResult.localName == "template")
              previousResult = null;
          }
          if (!previousResult)
            previousResult = this.templateRoot.lastChild;
          this.selectResult(previousResult);
        ]]></body>
      </method>
    </implementation>
    <handlers>
      <handler event="click" button="0"><![CDATA[
        var element = event.originalTarget;
        while (element && !element.hasAttribute("url")) {
          element = element.parentNode;
          if (element.localName == "taskdisplay")
            return;
        }
        if (!element)
          return;
        
        this.mVerbUtils.openUrlInBrowser(element.getAttribute("url"));
        this.mViewport.maybeClosePopup();
      ]]></handler>
      <handler event="mousemove"><![CDATA[
        var now = Date.now();
        if (now - this.mLastMoveTime > 30) {
          var item = event.originalTarget;
          
          function isResult(aNode) {
            if (aNode.className &&
                aNode.className.split(" ").indexOf("result") != -1) {
              return true;
            }
            return false;
          }
          
          while (item && item != this && !isResult(item))
            item = item.parentNode;
          if (!item || item == this)
            return;
          
          this.selectResult(item);
          
          this.mLastMoveTime = now;
        }
      ]]></handler>
    </handlers>
  </binding>
  
  <binding id="display-template-websearch" extends="chrome://browser/content/taskBindings.xml#display-template-base">
    <content>
      <xul:vbox anonid="template-root" datasources="rdf:null" ref="*"
                       querytype="tasks-view" flex="1">
        <xul:template>
          <xul:query anonid="template-query" type="task"/>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="message"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="?message"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="noresults"/>
            <xul:action>
              <xul:hbox class="msg-container" uri="?" flex="1">
                <xul:description>
                  &msg.noResultsFound.label;'<html:span style="font-weight: bold; text-decoration: underline;"><xul:textnode value="?input"/></html:span>'.
                </xul:description>
              </xul:hbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="loading"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.loading.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="error"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.error.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="result"/>
            <xul:action>
              <xul:hbox class="result clickable" url="?url" flex="1" uri="?">
                <xul:hbox class="image-container">
                  <xul:vbox>
                    <xul:image class="image" src="?image"
                               width="?image-width" height="?image-height"/>
                  </xul:vbox>
                </xul:hbox>
                <xul:vbox flex="1">
                  <xul:label class="title" value="?title" flex="1" crop="end"/>
                  <xul:hbox class="extract markup" flex="1" markup="?extract"/>
                  <xul:label class="url" value="?display-url" flex="1" crop="end"/>
                </xul:vbox>
                <xul:button class="detail-expander" url="?detail-url" data="?detail-data"/>
              </xul:hbox>
            </xul:action>
          </xul:rule>
        </xul:template>
      </xul:vbox>
    </content>
    <implementation>
      <constructor>this.Initialize();</constructor>
    </implementation>
  </binding>

  <binding id="display-template-search-tagged" extends="chrome://browser/content/taskBindings.xml#display-template-base">
    <content>
      <xul:vbox anonid="template-root" datasources="rdf:null" ref="*"
                       querytype="tasks-view" flex="1">
        <xul:template>
          <xul:query anonid="template-query" type="task"/>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="message"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="?message"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="noresults"/>
            <xul:action>
              <xul:hbox class="msg-container" uri="?" flex="1">
                <xul:description>
                  &msg.noResultsFound.label;'<html:span style="font-weight: bold; text-decoration: underline;"><xul:textnode value="?input"/></html:span>'.
                </xul:description>
              </xul:hbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="loading"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.loading.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="error"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.error.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="group"/>
            <xul:action>
              <xul:vbox class="group" url="?url" flex="1" uri="?">
                <xul:hbox class="header">
                  <xul:label class="title" flex="1" value="?name"/>
                  <xul:button class="detail-expander" url="?detail-url" data="?detail-data"/>
                </xul:hbox>
                <xul:hbox class="result" hidden="?done-loading" style="-moz-box-align: center;">
                  <xul:image src="chrome://global/skin/icons/loading_16.png"/>
                  <xul:label value="&msg.loading.label;"/>
                </xul:hbox>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="result"/>
            <xul:action>
              <xul:hbox class="result clickable" url="?url" flex="1" uri="?">
                <xul:hbox class="image-container">
                  <xul:vbox>
                    <xul:image class="image" src="?image"
                               width="?image-width" height="?image-height"/>
                  </xul:vbox>
                </xul:hbox>
                <xul:vbox flex="1">
                  <xul:label class="title" value="?title" flex="1" crop="end"/>
                  <xul:label class="sub-title" value="?sub-title" flex="1" crop="end"/>
                  <xul:description class="content" flex="1">
                    <xul:textnode value="?content"/>
                  </xul:description>
                </xul:vbox>
                <xul:vbox class="taglist" tagdata="?tags" />
                <xul:vbox hidden="?done-loading" style="-moz-box-align: center;">
                  <xul:image src="chrome://global/skin/icons/loading_16.png"/>
                </xul:vbox>
                <xul:button class="detail-expander" url="?detail-url" data="?detail-data"/>
              </xul:hbox>
            </xul:action>
          </xul:rule>
        </xul:template>
      </xul:vbox>
    </content>
    <implementation>
      <constructor>this.Initialize();</constructor>
    </implementation>
  </binding>

  <binding id="display-template-shopping" extends="chrome://browser/content/taskBindings.xml#display-template-base">
    <content>
      <xul:vbox anonid="template-root" datasources="rdf:null" ref="*"
                       querytype="tasks-view" flex="1">
        <xul:template>
          <xul:query anonid="template-query" type="task"/>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="message"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="?message"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="noresults"/>
            <xul:action>
              <xul:hbox class="msg-container" uri="?" flex="1">
                <xul:description>
                  &msg.noResultsFound.label;'<html:span style="font-weight: bold; text-decoration: underline;"><xul:textnode value="?input"/></html:span>'.
                </xul:description>
              </xul:hbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="loading"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.loading.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="error"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.error.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="item"/>
            <xul:action>
              <xul:hbox class="result clickable" url="?url" flex="1" uri="?">
                <xul:hbox class="image-container">
                  <xul:vbox>
                    <xul:image class="image" src="?image"/>
                  </xul:vbox>
                </xul:hbox>
                <xul:vbox flex="1">
                  <xul:label class="title" value="?title" flex="1" crop="end"/>
                  <xul:description class="extract" flex="1">
                    <xul:textnode value="?extract"/>
                  </xul:description>
                </xul:vbox>
                <xul:vbox class="detailbox">
                  <xul:label type="rating" rating="?rating" maxrating="?maxrating"/>
                  <xul:label class="price" value="?price"/>
                  <xul:spacer flex="1"/>
                  <xul:button label="&action.addToCart.label;" url="?addtocart-url"/>
                </xul:vbox>
                <xul:button class="detail-expander" url="?url"/>
              </xul:hbox>
            </xul:action>
          </xul:rule>
        </xul:template>
      </xul:vbox>
    </content>
    <implementation>
      <constructor>this.Initialize();</constructor>
    </implementation>
  </binding>
  
  <binding id="display-template-imagegrid" extends="chrome://browser/content/taskBindings.xml#display-template-base">
    <content>
      <xul:vbox anonid="template-root" datasources="rdf:null" ref="*"
                       querytype="tasks-view" flex="1">
        <xul:template>
          <xul:query anonid="template-query" type="task"/>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="message"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="?message"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="noresults"/>
            <xul:action>
              <xul:hbox class="msg-container" uri="?" flex="1">
                <xul:description>
                  &msg.noResultsFound.label;'<html:span style="font-weight: bold; text-decoration: underline;"><xul:textnode value="?input"/></html:span>'.
                </xul:description>
              </xul:hbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="loading"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.loading.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="error"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.error.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="result"/>
            <xul:action>
              <xul:hbox class="grid-container">
                <xul:vbox class="result clickable" url="?url" uri="?">
                  <xul:hbox class="image-container">
                    <xul:image class="image" src="?thumb"
                               width="?thumb-width" height="?thumb-height"/>
                  </xul:hbox>
                  <xul:hbox class="footer">
                    <xul:vbox flex="1" class="info-container">
                      <xul:label class="title" value="?title" flex="1" crop="end"/>
                      <xul:label class="display-url" value="?display-url" flex="1" crop="end"/>
                      <xul:label class="image-size" value="Size: ?image-width^x?image-height" flex="1" crop="end"/>
                    </xul:vbox>
                    <xul:button class="detail-expander" url="?detail-url" data="?detail-data"/>
                  </xul:hbox>
                </xul:vbox>
              </xul:hbox>
            </xul:action>
          </xul:rule>
        </xul:template>
      </xul:vbox>
    </content>
    <implementation>
      <constructor>this.Initialize();</constructor>
    </implementation>
  </binding>
  
  <binding id="display-template-translate" extends="chrome://browser/content/taskBindings.xml#display-template-base">
    <content>
      <xul:vbox anonid="template-root" datasources="rdf:null" ref="*"
                       querytype="tasks-view" flex="1">
        <xul:template>
          <xul:query anonid="template-query" type="task"/>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="message"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="?message"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="loading"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.loading.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="error"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.error.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="original-text"/>
            <xul:action>
              <xul:label value="&translate.originalText.label;"/>
              <xul:description class="original-text" uri="?"><xul:textnode value="?text"/></xul:description>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="translated-text"/>
            <xul:action>
              <xul:label value="&translate.translatedText.label;"/>
              <xul:description class="translated-text" uri="?"><xul:textnode value="?text"/></xul:description>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="lang-from"/>
            <xul:action>
              <xul:hbox class="lang-from-container">
                <xul:label value="&translate.from.label;"/>
                <xul:button class="lang" uri="?" label="?name"/>
              </xul:hbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="lang-to"/>
            <xul:action>
              <xul:hbox class="lang-to-container">
                <xul:label value="&translate.to.label;"/>
                <xul:button class="lang" uri="?" label="?name"/>
              </xul:hbox>
            </xul:action>
          </xul:rule>
        </xul:template>
      </xul:vbox>
    </content>
    <implementation>
      <constructor>this.Initialize();</constructor>
    </implementation>
  </binding>
  
  <binding id="display-template-weather" extends="chrome://browser/content/taskBindings.xml#display-template-base">
    <content>
      <xul:vbox anonid="template-root" datasources="rdf:null" ref="*"
                       querytype="tasks-view" flex="1">
        <xul:template>
          <xul:query anonid="template-query" type="task"/>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="message"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="?message"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="loading"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.loading.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="error"/>
            <xul:action>
              <xul:vbox class="msg-container" uri="?" flex="1">
                <xul:label value="&msg.error.label;"/>
              </xul:vbox>
            </xul:action>
          </xul:rule>
          <xul:rule>
            <xul:where subject="?type" rel="equals" value="location"/>
            <xul:action>
              <xul:hbox uri="?" class="forecast-container clickable" url="?now-url">
                <xul:vbox class="icon-container">
                  <xul:image src="http://icons.wunderground.com/graphics/conds/?now-icon^.GIF"/>
                </xul:vbox>
                <xul:vbox flex="1">
                  <xul:label class="location" value="?location"/>
                  <xul:hbox class="conditions">
                    <xul:label class="label" value="&weather.conditions.label;"/>
                    <xul:label class="value" value="?now-conditions"/>
                  </xul:hbox>
                  <xul:hbox class="wind">
                    <xul:label class="label" value="&weather.wind.label;"/>
                    <xul:label class="value" value="?now-wind"/>
                  </xul:hbox>
                  <xul:hbox class="temperature">
                    <xul:label class="label" value="&weather.temperature.label;"/>
                    <xul:label class="value" value="?now-temperature"/>
                  </xul:hbox>
                </xul:vbox>
                <xul:vbox>
                  <xul:label class="day" value="Tomorrow"/>
                  <xul:hbox>
                    <xul:vbox class="icon-container">
                      <xul:image src="http://icons.wunderground.com/graphics/conds/?1-icon^.GIF"/>
                    </xul:vbox>
                    <xul:vbox>
                     <xul:label class="conditions" value="?1-conditions"/>
                     <xul:hbox class="high">
                        <xul:label class="label" value="&weather.temperatureHigh.label;"/>
                        <xul:label class="value" value="?1-high"/>
                      </xul:hbox>
                      <xul:hbox class="low">
                        <xul:label class="label" value="&weather.temperatureLow.label;"/>
                        <xul:label class="value" value="?1-low"/>
                      </xul:hbox>
                    </xul:vbox>
                  </xul:hbox>
                </xul:vbox>
              </xul:hbox>
            </xul:action>
          </xul:rule>
        </xul:template>
      </xul:vbox>
    </content>
    <implementation>
      <constructor>this.Initialize();</constructor>
    </implementation>
  </binding>

  
  <binding id="helper-rating" extends="chrome://global/content/bindings/text.xml#text-label">
    <implementation>
      <constructor><![CDATA[
          // XXX this needs to be localizable
          var rating = this.hasAttribute("rating") ? this.getAttribute("rating") : null;
          if (!rating)
            return;
          var maxRating = this.hasAttribute("maxrating") ? this.getAttribute("maxrating") : null;
          var ratingText = "";
          if (maxRating)
            ratingText = rating + " out of " + maxRating;
          else
            ratingText = rating;
          this.value = "(Rating: " + ratingText + ")";
        ]]></constructor>
    </implementation>
  </binding>
  
  <binding id="helper-taglist">
    <implementation>
      <constructor><![CDATA[
        this.refreshTags();
      ]]></constructor>
      <field name="mJSON" readonly="true">
        Components.classes["@mozilla.org/dom/json;1"]
                  .createInstance(Components.interfaces.nsIJSON);
      </field>
      <method name="clearTags">
        <body><![CDATA[
          while (this.hasChildNodes())
            this.removeChild(this.firstChild);
        ]]></body>
      </method>
      <method name="refreshTags">
        <body><![CDATA[
          //this.clearTags();
          if (!this.hasAttribute("tagdata"))
            return;
          var tags = this.mJSON.decode(this.getAttribute("tagdata"));
          if (typeof tags != "object" || tags.constructor != Array)
            return;
          
          const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
          for each (let tag in tags) {
            let tagElement = document.createElementNS(XUL_NS, "label");
            tagElement.setAttribute("class", "tag");
            tagElement.setAttribute("value", tag.name);
            tagElement.setAttribute("url", tag.url);
            this.appendChild(tagElement);
          }
        ]]></body>
      </method>
    </implementation>
  </binding>
  
  <binding id="helper-markup">
    <content>
      <html:div xmlns="http://www.w3.org/1999/xhtml"
                anonid="container" flex="1">
        <children/>
      </html:div>
    </content>
    <implementation>
      <constructor><![CDATA[
        if (!this.hasAttribute("markup"))
          return;
        var markup = "<div>" + this.getAttribute("markup") + "</div>";
        var container = document.getAnonymousElementByAttribute(this,
                                                                "anonid",
                                                                "container");
        var frag = Components.classes["@mozilla.org/feed-unescapehtml;1"]
                  .getService(Components.interfaces.nsIScriptableUnescapeHTML)
                  .parseFragment(markup, false, null, container);
        // this should be done by parseFragment above, but isn't for some reason
        container.appendChild(frag);
      ]]></constructor>
    </implementation>
  </binding>

</bindings>