Option search, with select highlighting draft
authorTylerM <maklebus@msu.edu>
Wed, 05 Oct 2016 18:50:56 -0400
changeset 424924 7a699d16c98d34e9fcc28bd088012e3bbcc48572
parent 421258 e8fa13708c070d1fadf488ed9d951464745b4e17
child 424925 3ce9cd2afc0feec705b0b966b13f1c3796e1532b
push id32293
push userbmo:maklebus@msu.edu
push dateThu, 13 Oct 2016 20:34:31 +0000
milestone52.0a1
Option search, with select highlighting MozReview-Commit-ID: 2onR2P9Z6d0
toolkit/content/widgets/menu.xml
toolkit/modules/SelectParentHelper.jsm
--- a/toolkit/content/widgets/menu.xml
+++ b/toolkit/content/widgets/menu.xml
@@ -225,17 +225,17 @@
   </binding>
 
   <binding id="menuitem-iconic-noaccel" extends="chrome://global/content/bindings/menu.xml#menuitem">
     <content>
       <xul:hbox class="menu-iconic-left" align="center" pack="center"
                 xbl:inherits="selected,disabled,checked">
         <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
       </xul:hbox>
-      <xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/>
+      <xul:label class="menu-iconic-text" flex="1" xbl:inherits="xbl:text=label,accesskey,crop" crop="right"/>
     </content>
   </binding>
 
   <binding id="menucaption-inmenulist" extends="chrome://global/content/bindings/menu.xml#menucaption">
     <content>
       <xul:hbox class="menu-iconic-left" align="center" pack="center"
                 xbl:inherits="selected,disabled,checked">
         <xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
@@ -266,21 +266,21 @@
         <xul:label class="menu-iconic-accel" xbl:inherits="value=acceltext"/>
       </xul:hbox>
       <xul:hbox align="center" class="menu-right" xbl:inherits="_moz-menuactive,disabled">
         <xul:image/>
       </xul:hbox>
       <children includes="menupopup|template"/>
     </content>
   </binding>
-  
+
   <binding id="menubutton-item" extends="chrome://global/content/bindings/menu.xml#menuitem-base">
     <content>
       <xul:label class="menubutton-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/>
       <children includes="menupopup"/>
     </content>
-  </binding>  
-  
+  </binding>
+
   <binding id="menuseparator" role="xul:menuseparator"
            extends="chrome://global/content/bindings/menu.xml#menuitem-base">
   </binding>
 
 </bindings>
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -3,30 +3,39 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "SelectParentHelper"
 ];
 
+
+const {classes: Cc, utils: Cu, interfaces: Ci} = Components;
+
+const domUtil = Cc["@mozilla.org/inspector/dom-utils;1"]
+                  .getService(Ci.inIDOMUtils);
+
+
 // Maximum number of rows to display in the select dropdown.
 const MAX_ROWS = 20;
 
 var currentBrowser = null;
 var currentMenulist = null;
 var currentZoom = 1;
 var closedWithEnter = false;
 
 this.SelectParentHelper = {
+
   populate: function(menulist, items, selectedIndex, zoom) {
     // Clear the current contents of the popup
     menulist.menupopup.textContent = "";
     currentZoom = zoom;
     currentMenulist = menulist;
+
     populateChildren(menulist, items, selectedIndex, zoom);
   },
 
   open: function(browser, menulist, rect) {
     menulist.hidden = false;
     currentBrowser = browser;
     closedWithEnter = false;
     this._registerListeners(browser, menulist.menupopup);
@@ -140,30 +149,32 @@ this.SelectParentHelper = {
     popup.removeEventListener("mouseout", this);
     browser.ownerDocument.defaultView.removeEventListener("keydown", this, true);
     browser.ownerDocument.defaultView.removeEventListener("fullscreen", this, true);
     browser.messageManager.removeMessageListener("Forms:UpdateDropDown", this);
   },
 
 };
 
+// CPST - added first search parameter
 function populateChildren(menulist, options, selectedIndex, zoom,
-                          parentElement = null, isGroupDisabled = false, adjustedTextSize = -1) {
+                          parentElement = null, isGroupDisabled = false, adjustedTextSize = -1, firstSearch = true) {
   let element = menulist.menupopup;
 
   // -1 just means we haven't calculated it yet. When we recurse through this function
   // we will pass in adjustedTextSize to save on recalculations.
   if (adjustedTextSize == -1) {
     let win = element.ownerDocument.defaultView;
 
     // Grab the computed text size and multiply it by the remote browser's fullZoom to ensure
     // the popup's text size is matched with the content's. We can't just apply a CSS transform
     // here as the popup's preferred size is calculated pre-transform.
     let textSize = win.getComputedStyle(element).getPropertyValue("font-size");
     adjustedTextSize = (zoom * parseFloat(textSize, 10)) + "px";
+
   }
 
   for (let option of options) {
     let isOptGroup = (option.tagName == 'OPTGROUP');
     let item = element.ownerDocument.createElement(isOptGroup ? "menucaption" : "menuitem");
 
     item.setAttribute("label", option.textContent);
     item.style.direction = option.textDirection;
@@ -174,19 +185,20 @@ function populateChildren(menulist, opti
     element.appendChild(item);
 
     // A disabled optgroup disables all of its child options.
     let isDisabled = isGroupDisabled || option.disabled;
     if (isDisabled) {
       item.setAttribute("disabled", "true");
     }
 
+    // CPST - added false argument
     if (isOptGroup) {
       populateChildren(menulist, option.children, selectedIndex, zoom,
-                       item, isDisabled, adjustedTextSize);
+                       item, isDisabled, adjustedTextSize, false);
     } else {
       if (option.index == selectedIndex) {
         // We expect the parent element of the popup to be a <xul:menulist> that
         // has the popuponly attribute set to "true". This is necessary in order
         // for a <xul:menupopup> to act like a proper <html:select> dropdown, as
         // the <xul:menulist> does things like remember state and set the
         // _moz-menuactive attribute on the selected <xul:menuitem>.
         menulist.selectedItem = item;
@@ -201,9 +213,125 @@ function populateChildren(menulist, opti
 
       item.setAttribute("value", option.index);
 
       if (parentElement) {
         item.classList.add("contentSelectDropdown-ingroup")
       }
     }
   }
+
+  // CPST Check if first iteration through list and if list is long enough for
+  //  a search element to be added
+  if(firstSearch && element.childElementCount > 40){
+
+    // CPST Add a search field to top of list
+    let searchbox = element.ownerDocument.createElement('textbox');
+    searchbox.setAttribute("type", "search");
+
+    // CPST add focus event listener to search box
+    searchbox.addEventListener("focus", onSearchFocus);
+    // CPST add blur event listener to search box
+    searchbox.addEventListener("blur", onSearchBlur);
+    // CPST add input event listener to search box
+    searchbox.addEventListener("input", onSearchInput);
+
+    // CPST insert searchbox element at top of dropdown
+    element.insertBefore(searchbox, element.childNodes[0]);
+
+    // CPST ignore key list navigation so text goes to search
+    element.setAttribute("ignorekeys", "true");
+  }
+
 }
+
+// CPST - Search focus event
+function onSearchFocus(){
+  dump("CPST - Search Focus\n");
+  let searchObj = this;
+  let menupopup = searchObj.parentElement;
+  menupopup.setAttribute("ignorekeys", "true");
+  dump("CPST - Ignore keys true");
+}
+
+// CPST - Search blur event
+function onSearchBlur(){
+  dump("CPST - Search Blur\n");
+  let searchObj = this;
+  let menupopup = searchObj.parentElement;
+  menupopup.setAttribute("ignorekeys", "false");
+  dump("CPST - Ignore keys false");
+}
+
+// CPST - Search input event
+function onSearchInput(){
+  let doc = this.ownerDocument;
+  let win = doc.defaultView;
+  let selection = doc.defaultView.getSelection();
+  selection.removeAllRanges();
+
+  let searchObj = this;
+  // Get input from search field, set to all lower case for comparison
+  let input = searchObj.value.toLowerCase();
+
+  let menupopup = searchObj.parentElement;
+  let menuItems = menupopup.querySelectorAll("menuitem, menucaption");
+
+  let allHidden = true;
+  let prevCaption = null;
+
+  // Iterate through options to show/hide
+  for (let currentItem of menuItems) {
+
+    // Get label and tooltip (title) from option and change to
+    //  lower case for comparison
+    let itemLabel = currentItem.getAttribute("label").toLowerCase();
+    let itemTooltip = currentItem.getAttribute("title").toLowerCase();
+
+    if(input==""){
+      currentItem.setAttribute("hidden", "false");
+
+    } else if(currentItem.localName=="menucaption"){
+
+      if(prevCaption!=null){
+          prevCaption.setAttribute("hidden", (allHidden ? "true" : "false"));
+      }
+      prevCaption = currentItem;
+      allHidden = true;
+
+    } else{
+
+      if(!currentItem.classList.contains("contentSelectDropdown-ingroup") && currentItem.previousSibling.classList.contains("contentSelectDropdown-ingroup")){
+        if(prevCaption!=null){
+            prevCaption.setAttribute("hidden", (allHidden ? "true" : "false"));
+        }
+        prevCaption = null;
+        allHidden = true;
+      }
+
+      if(itemLabel.includes(input) || itemTooltip.includes(input)){
+        currentItem.setAttribute("hidden", "false");
+
+        let start = itemLabel.indexOf(input);
+        if(start!=-1){
+          // Getting label this way does not work on OSX? Find better way
+          let label = currentItem.boxObject.firstChild;
+          let textNode = label.firstChild;
+          let range = new win.Range();
+          range.setStart(textNode, start);
+          range.setEnd(textNode, (start+input.length));
+          let selection = doc.defaultView.getSelection();
+          selection.addRange(range);
+        }
+
+        allHidden = false;
+      } else{
+        currentItem.setAttribute("hidden", "true");
+      }
+    }
+
+  }
+
+  if(prevCaption!=null && allHidden){
+    prevCaption.setAttribute("hidden", (allHidden ? "true" : "false"));
+  }
+
+}