Bug 1517536 - Migrate popup-menu to custom element; r=mkmelin
authorArshad Khan <arshdkhn1@gmail.com>
Thu, 03 Jan 2019 23:20:00 +0530
changeset 34262 a0a854d99ee07836775ef135aab693f53ce3ccc2
parent 34261 94a2a4100e98356c27dbe3f9eeda80a20bf32135
child 34263 21ddeb46254b29eabfd6f9e4bfa8e0ef23cc59d0
push id389
push userclokep@gmail.com
push dateMon, 18 Mar 2019 19:01:53 +0000
reviewersmkmelin
bugs1517536
Bug 1517536 - Migrate popup-menu to custom element; r=mkmelin
mail/base/content/glodaFacet.js
mail/base/content/glodaFacetBindings.css
mail/base/content/glodaFacetBindings.xml
mail/base/content/glodaFacetView.js
mail/base/content/glodaFacetView.xhtml
--- a/mail/base/content/glodaFacet.js
+++ b/mail/base/content/glodaFacet.js
@@ -916,17 +916,17 @@ class MozFacetDiscrete extends HTMLEleme
       if (!(node && node.hasAttribute && node.hasAttribute("class"))) {
         return false;
       }
 
       this.currentNode = node;
       node.setAttribute("selected", "true");
 
       if (node.classList.contains("bar")) {
-        document.getElementById("popup-menu").show(event, this, node);
+        document.querySelector("facet-popup-menu").show(event, this, node);
       } else if (node.classList.contains("facet-more")) {
         this.changeMode("all");
       }
 
       return false;
     } catch (e) {
       return logException(e);
     }
@@ -952,12 +952,222 @@ class MozFacetDiscrete extends HTMLEleme
 
       return false;
     } catch (e) {
       return logException(e);
     }
   }
 }
 
+class MozFacetPopupMenu extends HTMLElement {
+  constructor() {
+    super();
+
+    this.addEventListener("keypress", (event) => {
+      switch (event.keyCode) {
+        case KeyEvent.DOM_VK_ESCAPE:
+          this.hide();
+          break;
+
+        case KeyEvent.DOM_VK_DOWN:
+          this.moveFocus(event, 1);
+          break;
+
+        case KeyEvent.DOM_VK_TAB:
+          if (event.shiftKey) {
+            this.moveFocus(event, -1);
+            break;
+          }
+
+          this.moveFocus(event, 1);
+          break;
+
+        case KeyEvent.DOM_VK_UP:
+          this.moveFocus(event, -1);
+          break;
+
+        default:
+          break;
+      }
+    });
+  }
+
+  connectedCallback() {
+    const parentDiv = document.createElement("div");
+    parentDiv.classList.add("parent");
+    parentDiv.setAttribute("tabIndex", "0");
+
+    this.includeNode = document.createElement("div");
+    this.includeNode.classList.add("popup-menuitem", "top");
+    this.includeNode.setAttribute("tabindex", "0");
+    this.includeNode.onmouseover = () => { this.focus(); };
+    this.includeNode.onkeypress = (event) => {
+      if (event.keyCode == event.DOM_VK_RETURN) {
+        this.doInclude();
+      }
+    };
+    this.includeNode.onmouseup = () => { this.doInclude(); };
+
+    this.excludeNode = document.createElement("div");
+    this.excludeNode.classList.add("popup-menuitem", "bottom");
+    this.excludeNode.setAttribute("tabindex", "0");
+    this.excludeNode.onmouseover = () => { this.focus(); };
+    this.excludeNode.onkeypress = (event) => {
+      if (event.keyCode == event.DOM_VK_RETURN) {
+        this.doExclude();
+      }
+    };
+    this.excludeNode.onmouseup = () => { this.doExclude(); };
+
+    this.undoNode = document.createElement("div");
+    this.undoNode.classList.add("popup-menuitem", "undo");
+    this.undoNode.setAttribute("tabindex", "0");
+    this.undoNode.onmouseover = () => { this.focus(); };
+    this.undoNode.onkeypress = (event) => {
+      if (event.keyCode == event.DOM_VK_RETURN) {
+        this.doUndo();
+      }
+    };
+    this.undoNode.onmouseup = () => { this.doUndo(); };
+
+    parentDiv.appendChild(this.includeNode);
+    parentDiv.appendChild(this.excludeNode);
+    parentDiv.appendChild(this.undoNode);
+
+    this.appendChild(parentDiv);
+  }
+
+  _getLabel(facetDef, facetValue, groupValue, stringName) {
+    let labelFormat;
+    if (stringName in facetDef.strings) {
+      labelFormat = facetDef.strings[stringName];
+    } else {
+      labelFormat = glodaFacetStrings.get(`glodaFacetView.facets.${stringName}.fallbackLabel`);
+    }
+
+    if (!labelFormat.includes("#1")) {
+      return labelFormat;
+    }
+
+    return labelFormat.replace("#1", facetValue);
+  }
+
+  build(facetDef, facetValue, groupValue) {
+    try {
+      if (groupValue) {
+        this.includeNode.textContent = this._getLabel(facetDef, facetValue,
+          groupValue, "mustMatchLabel");
+        this.excludeNode.textContent = this._getLabel(facetDef, facetValue,
+          groupValue, "cantMatchLabel");
+        this.undoNode.textContent = this._getLabel(facetDef, facetValue,
+          groupValue, "mayMatchLabel");
+      } else {
+        this.includeNode.textContent = this._getLabel(facetDef, facetValue,
+          groupValue, "mustMatchNoneLabel");
+        this.excludeNode.textContent = this._getLabel(facetDef, facetValue,
+          groupValue, "mustMatchSomeLabel");
+        this.undoNode.textContent = this._getLabel(facetDef, facetValue,
+          groupValue, "mayMatchAnyLabel");
+      }
+    } catch (e) {
+      logException(e);
+    }
+  }
+
+  moveFocus(event, delta) {
+    try {
+      // We probably want something quite generic in the long term, but that
+      // is way too much for now (needs to skip over invisible items, etc)
+      let focused = document.activeElement;
+      if (focused == this.includeNode) { this.excludeNode.focus(); } else if (focused == this.excludeNode) { this.includeNode.focus(); }
+      event.preventDefault();
+      event.stopPropagation();
+    } catch (e) {
+      logException(e);
+    }
+  }
+
+  selectItem(event) {
+    try {
+      let focused = document.activeElement;
+      if (focused == this.includeNode) { this.doInclude(); } else if (focused == this.excludeNode) { this.doExclude(); } else { this.doUndo(); }
+    } catch (e) {
+      logException(e);
+    }
+  }
+
+  show(event, facetNode, barNode) {
+    try {
+      this.node = barNode;
+      this.facetNode = facetNode;
+      let facetDef = facetNode.facetDef;
+      let groupValue = barNode.groupValue;
+      let variety = barNode.getAttribute("variety");
+      let label = barNode.querySelector(".bar-link").textContent;
+      this.build(facetDef, label, groupValue);
+      this.node.setAttribute("selected", "true");
+      const rtl = window.getComputedStyle(this).direction == "rtl";
+      /* We show different menus if we're on an "unselected" facet value,
+         or if we're on a preselected facet value, whether included or
+         excluded. The variety attribute handles that through CSS */
+      this.setAttribute("variety", variety);
+      let rect = barNode.getBoundingClientRect();
+      let x, y;
+      if (event.type == "click") {
+        // center the menu on the mouse click
+        if (rtl) { x = event.pageX + 10; } else { x = event.pageX - 10; }
+        y = Math.max(20, event.pageY - 15);
+      } else {
+        if (rtl) { x = rect.left + rect.width / 2 + 20; } else { x = rect.left + rect.width / 2 - 20; }
+        y = rect.top - 10;
+      }
+      if (rtl) { this.style.left = (x - this.getBoundingClientRect().width) + "px"; } else { this.style.left = x + "px"; }
+      this.style.top = y + "px";
+
+      if (variety == "remainder") {
+        // include
+        this.includeNode.focus();
+      } else {
+        // undo
+        this.undoNode.focus();
+      }
+    } catch (e) {
+      logException(e);
+    }
+  }
+
+  hide() {
+    try {
+      this.setAttribute("variety", "invisible");
+      if (this.node) {
+        this.node.removeAttribute("selected");
+        this.node.focus();
+      }
+    } catch (e) {
+      logException(e);
+    }
+  }
+
+  doInclude() {
+    try {
+      this.facetNode.includeFacet(this.node);
+      this.hide();
+    } catch (e) {
+      logException(e);
+    }
+  }
+
+  doExclude() {
+    this.facetNode.excludeFacet(this.node);
+    this.hide();
+  }
+
+  doUndo() {
+    this.facetNode.undoFacet(this.node);
+    this.hide();
+  }
+}
+
 customElements.define("facet-date", MozFacetDate);
 customElements.define("facet-boolean", MozFacetBoolean);
 customElements.define("facet-boolean-filtered", MozFacetBooleanFiltered);
 customElements.define("facet-discrete", MozFacetDiscrete);
+customElements.define("facet-popup-menu", MozFacetPopupMenu);
--- a/mail/base/content/glodaFacetBindings.css
+++ b/mail/base/content/glodaFacetBindings.css
@@ -4,12 +4,8 @@
 
 .results[type="message"] {
   -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#results-message');
 }
 
 .message {
   -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#result-message');
 }
-
-.popup-menu {
-  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#popup-menu');
-}
--- a/mail/base/content/glodaFacetBindings.xml
+++ b/mail/base/content/glodaFacetBindings.xml
@@ -6,238 +6,16 @@
 <!-- import-globals-from glodaFacetView.js -->
 
 <bindings id="glodaFacetBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:html="http://www.w3.org/1999/xhtml"
           xmlns:xbl="http://www.mozilla.org/xbl"
           xmlns:svg="http://www.w3.org/2000/svg">
-<binding id="popup-menu">
-  <content>
-    <html:div anonid="parent" class="parent"
-              tabindex="0"
-      ><html:div anonid="include-item" class="popup-menuitem top"
-        tabindex="0"
-        onmouseover="this.focus()"
-        onkeypress="if (event.keyCode == event.DOM_VK_RETURN) this.parentNode.parentNode.doInclude()"
-        onmouseup="this.parentNode.parentNode.doInclude()"></html:div
-      ><html:div anonid="exclude-item" class="popup-menuitem bottom"
-        tabindex="0"
-        onmouseover="this.focus()"
-        onkeypress="if (event.keyCode == event.DOM_VK_RETURN) this.parentNode.parentNode.doExclude()"
-        onmouseup="this.parentNode.parentNode.doExclude()"></html:div
-      ><html:div anonid="undo-item" class="popup-menuitem undo"
-        tabindex="0"
-        onmouseover="this.focus()"
-        onkeypress="if (event.keyCode == event.DOM_VK_RETURN) this.parentNode.parentNode.doUndo()"
-        onmouseup="this.parentNode.parentNode.doUndo()"></html:div
-    ></html:div>
-  </content>
-  <handlers>
-    <handler event="keypress" keycode="VK_ESCAPE"
-             action="this.hide();"/>
-    <handler event="keypress" keycode="VK_DOWN"
-             action="this.moveFocus(event, 1);"/>
-    <handler event="keypress" keycode="VK_TAB"
-             action="this.moveFocus(event, 1);"/>
-    <handler event="keypress" keycode="VK_TAB" modifiers="shift"
-             action="this.moveFocus(event, -1);"/>
-    <handler event="keypress" keycode="VK_UP"
-             action="this.moveFocus(event, -1);"/>
-  </handlers>
-  <implementation>
-    <constructor><![CDATA[
-      this.includeNode = document.getAnonymousElementByAttribute(this, "anonid",
-                                                        "include-item");
-      this.excludeNode = document.getAnonymousElementByAttribute(this, "anonid",
-                                                     "exclude-item");
-      this.undoNode = document.getAnonymousElementByAttribute(this, "anonid",
-                                                     "undo-item");
-    ]]></constructor>
-    <method name="_getLabel">
-      <parameter name="facetDef"/>
-      <parameter name="facetValue"/>
-      <parameter name="groupValue"/>
-      <parameter name="stringName"/>
-      <body><![CDATA[
-        let labelFormat;
-        if (stringName in facetDef.strings)
-          labelFormat = facetDef.strings[stringName];
-        else
-          labelFormat = glodaFacetStrings.get(
-              `glodaFacetView.facets.${stringName}.fallbackLabel`);
-        if (!labelFormat.includes("#1"))
-          return labelFormat;
-        return labelFormat.replace("#1", facetValue);
-      ]]></body>
-    </method>
-    <method name="build">
-      <parameter name="facetDef"/>
-      <parameter name="facetValue"/>
-      <parameter name="groupValue"/>
-      <body><![CDATA[
-      try {
-        if (groupValue)
-          this.includeNode.textContent = this._getLabel(facetDef, facetValue,
-                                                        groupValue, "mustMatchLabel");
-        else
-          this.includeNode.textContent = this._getLabel(facetDef, facetValue,
-                                                        groupValue, "mustMatchNoneLabel");
-        if (groupValue)
-          this.excludeNode.textContent = this._getLabel(facetDef, facetValue,
-                                                        groupValue, "cantMatchLabel");
-        else
-          this.excludeNode.textContent = this._getLabel(facetDef, facetValue,
-                                                        groupValue, "mustMatchSomeLabel");
-        if (groupValue)
-          this.undoNode.textContent = this._getLabel(facetDef, facetValue,
-                                                     groupValue, "mayMatchLabel");
-        else
-          this.undoNode.textContent = this._getLabel(facetDef, facetValue,
-                                                     groupValue, "mayMatchAnyLabel");
-      } catch (e) {
-        logException(e);
-      }
-      ]]>
-      </body>
-    </method>
-    <method name="moveFocus">
-      <parameter name="event"/>
-      <parameter name="delta"/>
-      <body><![CDATA[
-      try {
-        // We probably want something quite generic in the long term, but that
-        // is way too much for now (needs to skip over invisible items, etc)
-        let focused = document.activeElement;
-        if (focused == this.includeNode)
-          this.excludeNode.focus();
-        else if (focused == this.excludeNode)
-          this.includeNode.focus();
-        event.preventDefault();
-        event.stopPropagation();
-      } catch (e) {
-        logException(e);
-      }
-      ]]>
-      </body>
-    </method>
-    <method name="selectItem">
-      <parameter name="event"/>
-      <body><![CDATA[
-      try {
-        let focused = document.activeElement;
-        if (focused == this.includeNode)
-          this.doInclude();
-        else if (focused == this.excludeNode)
-          this.doExclude();
-        else
-          this.doUndo();
-      } catch (e) {
-        logException(e);
-      }
-      ]]>
-      </body>
-    </method>
-    <method name="show">
-      <parameter name="event"/>
-      <parameter name="facetNode"/>
-      <parameter name="barNode"/>
-      <body><![CDATA[
-        try {
-          this.node = barNode;
-          this.facetNode = facetNode;
-          let facetDef = facetNode.facetDef;
-          let groupValue = barNode.groupValue;
-          let variety = barNode.getAttribute("variety");
-          let label = barNode.querySelector(".bar-link").textContent;
-          this.build(facetDef, label, groupValue);
-          this.node.setAttribute("selected", "true");
-          var rtl = window.getComputedStyle(this).direction == "rtl";
-          /* We show different menus if we're on an "unselected" facet value,
-             or if we're on a preselected facet value, whether included or
-             excluded. The variety attribute handles that through CSS */
-          this.setAttribute("variety", variety);
-          let rect = barNode.getBoundingClientRect();
-          let X, Y;
-          if (event.type == "click") {
-            // center the menu on the mouse click
-            if (rtl)
-              X = event.pageX + 10;
-            else
-              X = event.pageX - 10;
-            Y = Math.max(20, event.pageY - 15);
-          } else {
-            if (rtl)
-              X = rect.left + rect.width / 2 + 20;
-            else
-              X = rect.left + rect.width / 2 - 20;
-            Y = rect.top - 10;
-          }
-          if (rtl)
-            this.style.left = (X - this.getBoundingClientRect().width) + "px";
-          else
-            this.style.left = X + "px";
-          this.style.top = Y + "px";
-
-          if (variety == "remainder")
-            // include
-            document.getAnonymousElementByAttribute(this,
-              "anonid", "parent").firstChild.focus();
-          else
-            // undo
-            document.getAnonymousElementByAttribute(this,
-              "anonid", "parent").lastChild.focus();
-        } catch (e) {
-          logException(e);
-        }
-      ]]>
-      </body>
-    </method>
-    <method name="hide">
-      <body><![CDATA[
-      try {
-        this.setAttribute("variety", "invisible");
-        if (this.node) {
-          this.node.removeAttribute("selected");
-          this.node.focus();
-        }
-      } catch (e) {
-        logException(e);
-      }
-      ]]>
-      </body>
-    </method>
-    <method name="doInclude">
-      <body><![CDATA[
-      try {
-        this.facetNode.includeFacet(this.node);
-        this.hide();
-      } catch (e) {
-        logException(e);
-      }
-      ]]>
-      </body>
-    </method>
-    <method name="doExclude">
-      <body><![CDATA[
-        this.facetNode.excludeFacet(this.node);
-        this.hide();
-      ]]>
-      </body>
-    </method>
-    <method name="doUndo">
-      <body><![CDATA[
-        this.facetNode.undoFacet(this.node);
-        this.hide();
-      ]]>
-      </body>
-    </method>
-  </implementation>
-</binding>
 
 <!-- ===== Results ===== -->
 
 <binding id="results-message">
   <content>
     <html:div class="results-message-header">
       <html:h2 class="results-message-count" anonid="count"></html:h2>
       <html:div class="results-message-showall">
--- a/mail/base/content/glodaFacetView.js
+++ b/mail/base/content/glodaFacetView.js
@@ -1004,12 +1004,12 @@ function reachOutAndTouchFrame() {
 
   // if it has already completed, we need to prod things
   if (aTab.query.completed && (!("IMQuery" in aTab) || aTab.IMQuery.completed))
     FacetContext.initialBuild();
 }
 
 function clickOnBody(event) {
   if (event.bubbles) {
-    document.getElementById("popup-menu").hide();
+    document.querySelector("facet-popup-menu").hide();
   }
   return 0;
 }
--- a/mail/base/content/glodaFacetView.xhtml
+++ b/mail/base/content/glodaFacetView.xhtml
@@ -37,19 +37,18 @@
   <!-- Libs -->
   <script type="application/javascript"
           src="chrome://messenger/content/protovis-r2.6-modded.js"></script>
   <!-- Facet Binding Stuff that doesn't belong in XBL -->
   <script type="application/javascript"
           src="chrome://messenger/content/glodaFacetVis.js"></script>
 </head>
 <body id="body" onload="reachOutAndTouchFrame()"
-      onkeypress="if (event.keyCode == event.DOM_VK_ESCAPE) document.getElementById('popup-menu').hide();"
       onmouseup="return clickOnBody(event)">
-  <div id="popup-menu" class="popup-menu" variety="invisible"/>
+  <facet-popup-menu class="popup-menu" variety="invisible"/>
   <div id="table">
     <div>
         <div class="facets facets-sidebar" id="facets">
           <h1 id="filter-header-label">&glodaFacetView.filters.label;</h1>
           <div>
             <facet-boolean id="facet-fromMe" type="boolean" attr="fromMe" uninitialized="true"/>
             <facet-boolean id="facet-toMe" type="boolean" attr="toMe" uninitialized="true"/>
             <facet-boolean id="facet-star" type="boolean" attr="star" uninitialized="true"/><br/>