Bug 486184: make filling in forms easier [r=webapps r=mark.finkle]
authorVivien Nicolas <21@vingtetun.org>
Sat, 24 Oct 2009 00:04:51 -0400
changeset 1017 f8894cddcdc13780b5677a1ec7b51552a94e1107
parent 1016 b224b864866d1339589d7a223f9208f28289975f
child 1018 42151c6f63491c123b92188f8b1328f19702f747
push id887
push usermfinkle@mozilla.com
push dateSat, 24 Oct 2009 04:09:16 +0000
reviewerswebapps, mark
bugs486184
Bug 486184: make filling in forms easier [r=webapps r=mark.finkle]
chrome/content/BrowserView.js
chrome/content/bindings.xml
chrome/content/browser-ui.js
chrome/content/browser.js
chrome/content/browser.xul
chrome/content/content.css
locales/en-US/chrome/browser.dtd
themes/hildon/browser.css
themes/wince/browser.css
--- a/chrome/content/BrowserView.js
+++ b/chrome/content/BrowserView.js
@@ -37,16 +37,18 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 let Ci = Components.interfaces;
 
 const kBrowserViewZoomLevelMin = 0.2;
 const kBrowserViewZoomLevelMax = 4.0;
+const kBrowserFormZoomLevelMin = 1.0;
+const kBrowserFormZoomLevelMax = 2.0;
 const kBrowserViewZoomLevelPrecision = 10000;
 const kBrowserViewPrefetchBeginIdleWait = 1;    // seconds
 const kBrowserViewCacheSize = 15;
 
 /**
  * A BrowserView maintains state of the viewport (browser, zoom level,
  * dimensions) and the visible rectangle into the viewport, for every
  * browser it is given (cf setBrowser()).  In updates to the viewport state,
--- a/chrome/content/bindings.xml
+++ b/chrome/content/bindings.xml
@@ -925,16 +925,31 @@
       
           this.focus();
           SelectHelper.show(this);
         ]]>
       </handler>
     </handlers>
   </binding>
 
+  <binding id="chrome-input">
+    <content>
+      <children />
+    </content>
+    <handlers>
+      <handler event="click" button="0">
+        <![CDATA[
+          var showEvent = document.createEvent("Events");
+          showEvent.initEvent("UIShowForm", true, false);
+          this.dispatchEvent(showEvent);
+        ]]>
+      </handler>
+    </handlers>
+  </binding>
+
   <binding id="chrome-select">
     <content>
       <children />
     </content>
 
     <implementation>
       <property name="selectElement"
                 onget="return this.QueryInterface(Components.interfaces.nsISelectElement);"
@@ -950,20 +965,18 @@
       </handler>
 
       <handler event="click" button="0">
         <![CDATA[
           let options = this.options;
           if (options.length == 0)
             return;
       
-          this.focus();
-
           var showEvent = document.createEvent("Events");
-          showEvent.initEvent("UIShowSelect", true, false);
+          showEvent.initEvent("UIShowForm", true, false);
           this.dispatchEvent(showEvent);
         ]]>
       </handler>
     </handlers>
   </binding>
 
   <binding id="chrome-select-option">
     <content orient="horizontal" flex="1">
--- a/chrome/content/browser-ui.js
+++ b/chrome/content/browser-ui.js
@@ -337,16 +337,20 @@ var BrowserUI = {
     let bookmarkPopup = document.getElementById("bookmark-popup");
     let bookmarkPopupW = windowW / 4;
     bookmarkPopup.height = windowH / 4;
     bookmarkPopup.width = bookmarkPopupW;
     let starRect = this.starButton.getBoundingClientRect();
     const popupMargin = 10;
     bookmarkPopup.top = Math.round(starRect.top) + popupMargin;
     bookmarkPopup.left = windowW - this.sidebarW - bookmarkPopupW - popupMargin;
+
+    // form helper
+    let formHelper = document.getElementById("form-helper-container");
+    formHelper.top = windowH - formHelper.getBoundingClientRect().height;
   },
 
   init : function() {
     this._edit = document.getElementById("urlbar-edit");
     this._throbber = document.getElementById("urlbar-throbber");
     this._favicon = document.getElementById("urlbar-favicon");
     this._favicon.addEventListener("error", this, false);
 
@@ -357,17 +361,17 @@ var BrowserUI = {
     document.getElementById("toolbar-main").ignoreDrag = true;
 
     let tabs = document.getElementById("tabs");
     tabs.addEventListener("TabSelect", this, true);
     tabs.addEventListener("TabOpen", this, true);
 
     let browsers = document.getElementById("browsers");
     browsers.addEventListener("DOMWindowClose", this, true);
-    browsers.addEventListener("UIShowSelect", this, false, true);
+    browsers.addEventListener("UIShowForm", this, false, true);
 
     // XXX these really want to listen to only the current browser
     browsers.addEventListener("DOMTitleChanged", this, true);
     browsers.addEventListener("DOMLinkAdded", this, true);
     browsers.addEventListener("DOMWillOpenModalDialog", this, true);
     
     // listening mousedown for automatically dismiss some popups (e.g. larry)
     window.addEventListener("mousedown", this, true);
@@ -441,16 +445,19 @@ var BrowserUI = {
 
     // FIXME: deckbrowser should not fire TabSelect on the initial tab (bug 454028)
     if (!browser.currentURI)
       return;
 
     // Update the navigation buttons
     this._updateButtons(browser);
 
+    // Close the forms assistant
+    FormHelper.close();
+
     // Check for a bookmarked page
     this.updateStar();
 
     var urlString = this.getDisplayURI(browser);
     if (urlString == "about:blank")
       urlString = "";
 
     this._setURI(urlString);
@@ -585,18 +592,18 @@ var BrowserUI = {
         this._titleChanged(aEvent.target);
         break;
       case "DOMLinkAdded":
         this._linkAdded(aEvent);
         break;
       case "DOMWindowClose":
         this._domWindowClose(aEvent);
         break;
-      case "UIShowSelect":
-        SelectHelper.show(aEvent.target);
+      case "UIShowForm":
+        FormHelper.open(aEvent.target);
         break;
       case "TabSelect":
         this._tabSelect(aEvent);
         break;
       case "TabOpen":
         if (!this.isTabsVisible() && 
             Browser.selectedTab.chromeTab != aEvent.target)
           NewTabPopup.show(aEvent.target);
@@ -948,16 +955,235 @@ var BookmarkList = {
       if (this._bookmarks.isRootFolder && this._bookmarks.items.length == 1) {
         this._manageButton.disabled = true;
         this.toggleManage();
       }
     }
   }
 };
 
+var FormHelper = {
+  _nodes: null,
+  get _container() {
+    delete this._container;
+    return this._container = document.getElementById("form-helper-container");
+  },
+
+  get _helperSpacer() {
+    delete this._helperSpacer;
+    return this._helperSpacer = document.getElementById("form-helper-spacer");
+  },
+
+  get _selectContainer() {
+    delete this._selectContainer;
+    return this._selectContainer = document.getElementById("select-container");
+  },
+
+  _getRectForElement: function formHelper_getRectForElement(aElement) {
+    let elRect = Browser.getBoundingContentRect(aElement);
+    let bv = Browser._browserView;
+
+    let labels = this.getLabelsFor(aElement);
+    for (let i=0; i<labels.length; i++) {
+      let labelRect = Browser.getBoundingContentRect(labels[i]);
+      if (labelRect.left < elRect.left) {
+        let width = labelRect.width + elRect.width + (elRect.x - labelRect.x - labelRect.width);
+        return new Rect(labelRect.x, labelRect.y, width, elRect.height).expandToIntegers();
+      }
+    }
+    return elRect;
+  },
+
+  _update: function(aPreviousElement, aNewElement) {
+    this._updateSelect(aPreviousElement, aNewElement);
+
+    let height = Math.floor(this._container.getBoundingClientRect().height);
+    this._container.top = window.innerHeight - height;
+
+    document.getElementById("form-helper-previous").disabled = this._getPrevious() ? false : true;
+    document.getElementById("form-helper-next").disabled = this._getNext() ? false : true;
+  },
+
+  _updateSelect: function(aPreviousElement, aNewElement) {
+    let previousIsSelect = this._isValidSelectElement(aPreviousElement);
+    let currentIsSelect = this._isValidSelectElement(aNewElement);
+    
+    if (currentIsSelect && !previousIsSelect) {
+      this._selectContainer.height = window.innerHeight / 1.8;
+
+      let rootNode = this._container;
+      rootNode.insertBefore(this._selectContainer, rootNode.lastChild);
+
+      SelectHelper.show(aNewElement);
+    }
+    else if (currentIsSelect && previousIsSelect) {
+      SelectHelper.reset();
+      SelectHelper.show(aNewElement);
+    }
+    else if (!currentIsSelect && previousIsSelect) {
+      let rootNode = this._container.parentNode;
+      rootNode.insertBefore(this._selectContainer, rootNode.lastChild);
+
+      SelectHelper.close();
+    }
+  },
+
+  _isValidElement: function(aElement) {
+    if (aElement.disabled)
+      return false;
+
+    if (aElement instanceof HTMLSelectElement || aElement instanceof HTMLTextAreaElement) {
+      let rect = aElement.getBoundingClientRect();
+      let isVisible = (rect.height != 0 || rect.width != 0);
+      return isVisible;
+    }
+    
+    if (aElement instanceof HTMLInputElement) {
+      let ignoreInputElements = ["checkbox", "radio", "hidden", "reset", "button"];
+      let isValidElement = (ignoreInputElements.indexOf(aElement.type) == -1);
+      if (!isValidElement)
+       return false;
+ 
+      let rect = aElement.getBoundingClientRect();
+      let isVisible = (rect.height != 0 || rect.width != 0);
+      return isVisible;
+    }
+
+    return false;
+  },
+
+  _isValidSelectElement: function(aElement) {
+    return (aElement instanceof HTMLSelectElement) || (aElement instanceof Ci.nsIDOMXULMenuListElement);
+  },
+
+  _getAll: function() {
+    let doc = getBrowser().contentDocument;
+    let nodes = doc.evaluate("//input|//select",
+                             doc,
+                             null,
+                             XPathResult.ORDERED_NODE_ITERATOR_TYPE,
+                             null);
+
+    let elements = [];
+    let node = nodes.iterateNext();
+    while (node) {
+      if (this._isValidElement(node)) 
+        elements.push(node);
+      node = nodes.iterateNext();
+    }
+
+    function orderByTabIndex(a, b) {
+      return a.tabIndex - b.tabIndex;
+    }
+    return elements.sort(orderByTabIndex);
+  },
+
+  _getPrevious: function() {
+    let elements = this._nodes;
+    for (let i = elements.length; i>0; --i) {
+      if (elements[i] == this._currentElement)
+        return elements[--i];
+   }
+   return null;
+  },
+
+  _getNext: function() {
+    let elements = this._nodes;
+    for (let i = 0; i<elements.length; i++) {
+      if (elements[i] == this._currentElement)
+        return elements[++i];
+    }
+    return null;
+  },
+
+  getLabelsFor: function(aElement) {
+    let associatedLabels = [];
+    if (this._isValidElement(aElement)) {
+      let labels = aElement.ownerDocument.getElementsByTagName("label");
+      for (let i=0; i<labels.length; i++) {
+        if (labels[i].getAttribute("for") == aElement.id)
+          associatedLabels.push(labels[i]);
+      }
+    }
+
+    if (aElement.parentNode instanceof HTMLLabelElement)
+      associatedLabels.push(aElement.parentNode);
+
+    return associatedLabels;
+  },
+
+  _currentElement: null,
+  getCurrentElement: function() {
+    return this._currentElement;
+  },
+
+  setCurrentElement: function(aElement) {
+    if (!aElement)
+      return;
+
+    let previousElement = this._currentElement;
+    this._currentElement = aElement;
+    this._update(previousElement, aElement);
+    
+    let containerHeight = this._container.getBoundingClientRect().height;
+    this._helperSpacer.setAttribute("height", containerHeight);
+
+    this.zoom(aElement);
+    gFocusManager.setFocus(aElement, Ci.nsIFocusManager.FLAG_NOSCROLL);
+  },
+
+  goToPrevious: function formHelperGoToPrevious() {
+    let previous = this._getPrevious();
+    this.setCurrentElement(previous);
+  },
+
+  goToNext: function formHelperGoToNext() {
+    let next = this._getNext();
+    this.setCurrentElement(next);
+  },
+
+  open: function formHelperOpen(aElement) {
+    this._open = true;
+
+    this._container.hidden = false;
+    this._helperSpacer.hidden = false;
+
+    this._nodes = this._getAll();
+    this.setCurrentElement(aElement);
+  },
+
+  close: function formHelperHide() {
+    if (!this._open)
+      return;
+
+    this._updateSelect(this._currentElement, null);
+
+    this._helperSpacer.hidden = true;
+    // give the form spacer area back to the content
+    let bv = Browser._browserView;
+    bv.onBeforeVisibleMove(0, 0);
+    Browser.contentScrollboxScroller.scrollBy(0, 0);
+    bv.onAfterVisibleMove();
+
+    this._container.hidden = true;
+    this._currentElement = null;
+    this._open = false;
+  },
+
+  zoom: function formHelperZoom(aElement) {
+    let zoomLevel = Browser._getZoomLevelForElement(aElement);
+    zoomLevel = Math.min(Math.max(kBrowserFormZoomLevelMin, zoomLevel), kBrowserFormZoomLevelMax);
+
+    let elRect = this._getRectForElement(aElement);
+    let zoomRect = Browser._getZoomRectForPoint(elRect.center().x, elRect.y, zoomLevel);
+
+    Browser.setVisibleRect(zoomRect);
+  }
+};
+
 function SelectWrapper(aControl) {
   this._control = aControl;
 }
 
 SelectWrapper.prototype = {
   get selectedIndex() { return this._control.selectedIndex; },
   get multiple() { return this._control.multiple; },
   get options() { return this._control.options; },
@@ -1084,17 +1310,16 @@ var SelectHelper = {
       }
     }
 
     this._panel = document.getElementById("select-container");
     this._panel.hidden = false;
 
     this._scrollElementIntoView(firstSelected);
 
-    this._list.focus();
     this._list.addEventListener("click", this, false);
   },
 
   _scrollElementIntoView: function(aElement) {
     if (!aElement)
       return;
 
     let index = -1;
@@ -1139,28 +1364,29 @@ var SelectHelper = {
         }
       }
     }
 
     if (!isIdentical) 
       this._control.fireOnChange();
   },
 
+  reset: function() {
+    let empty = this._list.cloneNode(false);
+    this._list.parentNode.replaceChild(empty, this._list);
+    this._list = empty;
+  },
+
   close: function() {
     this._updateControl();
 
     this._list.removeEventListener("click", this, false);
     this._panel.hidden = true;
-
-    // Clear out the list for the next show
-    let empty = this._list.cloneNode(false);
-    this._list.parentNode.replaceChild(empty, this._list);
-    this._list = empty;
-
-    this._control.focus();
+    
+    this.reset();
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "click":
         let item = aEvent.target;
         if (item && item.hasOwnProperty("optionIndex")) {
           if (this._control.multiple) {
--- a/chrome/content/browser.js
+++ b/chrome/content/browser.js
@@ -978,62 +978,42 @@ var Browser = {
                                     // sometimes doesn't cause the reflow that
                                     // resizes the parent right away, so we
                                     // can preempt it here.  Not needed anywhere
                                     // currently but if this becomes API or is
                                     // needed then uncomment this line.
   },
 
   /**
-   * Returns a good zoom rectangle for given element.
-   * @param y Where the user clicked in browser coordinates. For long elements
-   *          (like news columns), this keeps the clicked spot in the viewport.
-   * @return Rectangle in current viewport coordinates, null if nothing works.
+   * Find the needed zoom level for zooming on an element
    */
-  _getZoomRectForElement: function _getZoomRectForElement(element, y) {
-    if (element == null)
-      return null;
-
+  _getZoomLevelForElement: function _getZoomLevelForElement(element) {
     const margin = 15;
-    let bv = Browser._browserView;
-    let vis = bv.getVisibleRect();
+
+    let bv = this._browserView;
     let elRect = bv.browserToViewportRect(Browser.getBoundingContentRect(element));
-    y = bv.browserToViewport(y);
-
-    let zoomLevel = BrowserView.Util.clampZoomLevel(bv.getZoomLevel() * vis.width / (elRect.width + margin * 2));
-    let zoomRatio = bv.getZoomLevel() / zoomLevel;
-
-    // Don't zoom in a marginal amount
-    // > 2/3 means operation increases the zoom level by less than 1.5
-    if (zoomRatio >= .6666)
-       return null;
-
-    let newVisW = vis.width * zoomRatio, newVisH = vis.height * zoomRatio;
-    let result = new Rect(elRect.center().x - newVisW / 2, y - newVisH / 2, newVisW, newVisH).expandToIntegers();
-
-    // Make sure rectangle doesn't poke out of viewport
-    return result.translateInside(bv._browserViewportState.viewportRect);
+
+    let vis = bv.getVisibleRect();
+    return BrowserView.Util.clampZoomLevel(bv.getZoomLevel() * vis.width / (elRect.width + margin * 2));
   },
 
   /**
    * Find a good zoom rectangle for point specified in browser coordinates.
-   * @return Point in viewport coordinates, null if the zoom is too big.
+   * @return Point in viewport coordinates
    */
   _getZoomRectForPoint: function _getZoomRectForPoint(x, y, zoomLevel) {
     let bv = Browser._browserView;
     let vis = bv.getVisibleRect();
     x = bv.browserToViewport(x);
     y = bv.browserToViewport(y);
 
-    if (zoomLevel >= 4)
-      return null;
-
+    zoomLevel = Math.min(kBrowserViewZoomLevelMax, zoomLevel);
     let zoomRatio = zoomLevel / bv.getZoomLevel();
     let newVisW = vis.width / zoomRatio, newVisH = vis.height / zoomRatio;
-    let result = new Rect(x - newVisW / 2, y - newVisH / 2, newVisW, newVisH);
+    let result = new Rect(x - newVisW / 2, y - newVisH / 2, newVisW, newVisH).expandToIntegers();
 
     // Make sure rectangle doesn't poke out of viewport
     return result.translateInside(bv._browserViewportState.viewportRect);
   },
 
   setVisibleRect: function setVisibleRect(rect) {
     let bv = Browser._browserView;
     let vis = bv.getVisibleRect();
@@ -1050,25 +1030,33 @@ var Browser = {
     Browser.contentScrollboxScroller.scrollTo(scrollX, scrollY);
     bv.onAfterVisibleMove();
     bv.renderNow();
 
     bv.commitOffscreenOperation();
   },
 
   zoomToPoint: function zoomToPoint(cX, cY) {
-    const margin = 15;
-
     let [elementX, elementY] = Browser.transformClientToBrowser(cX, cY);
+
     let element = Browser.elementFromPoint(elementX, elementY);
-    let zoomRect = this._getZoomRectForElement(element, elementY);
-
-    if (zoomRect == null)
+    if (!element)
       return false;
 
+    let zoomLevel = this._getZoomLevelForElement(element);
+
+    // Don't zoom in a marginal amount
+    // > 2/3 means operation increases the zoom level by less than 1.5
+    let zoomRatio = this._browserView.getZoomLevel() / zoomLevel;
+    if (zoomRatio >= .6666)
+       return false;
+
+    let elRect = Browser.getBoundingContentRect(element);
+    let zoomRect = this._getZoomRectForPoint(elRect.center().x, elementY, zoomLevel);
+
     this.setVisibleRect(zoomRect);
     return true;
   },
 
   zoomFromPoint: function zoomFromPoint(cX, cY) {
     let bv = this._browserView;
     
     let zoomLevel = bv.getZoomForPage();
@@ -1167,23 +1155,24 @@ var Browser = {
     return elem;
   },
 
   /**
    * Return the visible rect in coordinates with origin at the (left, top) of
    * the tile container, i.e. BrowserView coordinates.
    */
   getVisibleRect: function getVisibleRect() {
+    let stack = document.getElementById("tile-stack");
     let container = document.getElementById("tile-container");
     let containerBCR = container.getBoundingClientRect();
 
     let x = Math.round(-containerBCR.left);
     let y = Math.round(-containerBCR.top);
     let w = window.innerWidth;
-    let h = window.innerHeight;
+    let h = stack.getBoundingClientRect().height;
 
     return new Rect(x, y, w, h);
   },
 
   /**
    * Convenience function for getting the scrollbox position off of a
    * scrollBoxObject interface.  Returns the actual values instead of the
    * wrapping objects.
@@ -1272,19 +1261,20 @@ Browser.MainDragger.prototype = {
     let x = 0, y = 0, rect;
 
     rect = Rect.fromRect(Browser.pageScrollbox.getBoundingClientRect()).map(Math.round);
     if (doffset.x < 0 && rect.right < window.innerWidth)
       x = Math.max(doffset.x, rect.right - window.innerWidth);
     if (doffset.x > 0 && rect.left > 0)
       x = Math.min(doffset.x, rect.left);
 
+    let height = document.getElementById("tile-stack").getBoundingClientRect().height;
     rect = Rect.fromRect(Browser.contentScrollbox.getBoundingClientRect()).map(Math.round);
-    if (doffset.y < 0 && rect.bottom < window.innerHeight)
-      y = Math.max(doffset.y, rect.bottom - window.innerHeight);
+    if (doffset.y < 0 && rect.bottom < height)
+      y = Math.max(doffset.y, rect.bottom - height);
     if (doffset.y > 0 && rect.top > 0)
       y = Math.min(doffset.y, rect.top);
 
     doffset.subtract(x, y);
     return new Point(x, y);
   },
 
   /** Pan scroller by the given amount. Updates doffset with leftovers. */
--- a/chrome/content/browser.xul
+++ b/chrome/content/browser.xul
@@ -122,16 +122,21 @@
 
     <!-- editing -->
     <command id="cmd_cut" label="&cut.label;" oncommand="CommandUpdater.doCommand(this.id);"/>
     <command id="cmd_copy" label="&copy.label;" oncommand="CommandUpdater.doCommand(this.id);"/>
     <command id="cmd_copylink" label="&copylink.label;" oncommand="CommandUpdater.doCommand(this.id);"/>
     <command id="cmd_paste" label="&paste.label;" oncommand="CommandUpdater.doCommand(this.id);"/>
     <command id="cmd_delete" label="&delete.label;" oncommand="CommandUpdater.doCommand(this.id);"/>
     <command id="cmd_selectAll" label="&selectAll.label;" oncommand="CommandUpdater.doCommand(this.id);"/>
+
+    <!-- forms navigation -->
+    <command id="cmd_formPrevious" oncommand="FormHelper.goToPrevious();"/>
+    <command id="cmd_formNext" oncommand="FormHelper.goToNext();"/>
+    <command id="cmd_formClose" oncommand="FormHelper.close();"/>
   </commandset>
 
   <keyset id="mainKeyset">
     <!-- basic navigation -->
     <key id="key_back" keycode="VK_LEFT" command="cmd_back" modifiers="control"/>
     <key id="key_forward" keycode="VK_RIGHT" command="cmd_forward" modifiers="control"/>
     <key id="key_back2" keycode="VK_BACK" command="cmd_back"/>
     <key id="key_forward2" keycode="VK_BACK" command="cmd_forward" modifiers="shift"/>
@@ -223,25 +228,29 @@
                 <toolbarbutton id="tool-app-close" class="urlbar-button button-image" command="cmd_close"/>
               </toolbar>
             </box>
           </box>
 
           <notificationbox id="notifications"/>
 
           <!-- Content viewport -->
-          <stack id="tile-stack" class="window-width window-height">
-            <scrollbox id="content-scrollbox" style="overflow: hidden;" class="window-width window-height">
-              <!-- Content viewport -->
-              <html:div id="tile-container" style="overflow: hidden;"/>
-            </scrollbox>
+          <vbox class="window-width window-height">
+            <stack id="tile-stack" class="window-width" flex="1">
+              <scrollbox id="content-scrollbox" style="overflow: hidden;" class="window-width" flex="1">
+                <!-- Content viewport -->
+                <html:div id="tile-container" style="overflow: hidden;"/>
+              </scrollbox>
 
-            <html:canvas id="view-buffer" style="display: none;" moz-opaque="true">
-            </html:canvas>
-          </stack>
+              <html:canvas id="view-buffer" style="display: none;" moz-opaque="true">
+              </html:canvas>
+            </stack>
+            <box id="form-helper-spacer" hidden="true"/>
+          </vbox>
+
         </vbox>
       </scrollbox>
 
       <!-- Right toolbar -->
       <vbox class="panel-dark">
         <spacer class="toolbar-height"/>
 
         <vbox id="browser-controls" style="overflow: -moz-hidden-unscrollable;" class="panel-dark" flex="1">
@@ -249,16 +258,26 @@
           <toolbarbutton id="tool-back" class="browser-control-button button-image" command="cmd_back"/>
           <toolbarbutton id="tool-forward" class="browser-control-button button-image" command="cmd_forward"/>
           <toolbarspring/>
           <toolbarbutton id="tool-panel-open" class="page-button button-image" command="cmd_panel"/>
         </vbox>
       </vbox>
     </scrollbox>
 
+    <!-- popup for form helper -->
+    <vbox id="form-helper-container" class="window-width" hidden="true" top="0" pack="end">
+      <hbox id="form-buttons" class="panel-dark" pack="center">
+        <button id="form-helper-previous" class="button-dark" label="&formHelper.previous;" command="cmd_formPrevious"/>
+        <button id="form-helper-next" class="button-dark" label="&formHelper.next;" command="cmd_formNext"/>
+        <spacer flex="1"/>
+        <button id="form-helper-close" class="button-dark" label="&formHelper.done;" command="cmd_formClose"/>
+      </hbox>
+    </vbox>
+
     <!-- popup for site identity information -->
     <vbox id="identity-container" hidden="true" class="panel-dark window-width" mode="unknownIdentity">
       <hbox id="identity-popup-container" flex="1" align="top">
         <image id="identity-popup-icon"/>
         <vbox id="identity-popup-content-box" flex="1">
           <hbox flex="1">
             <label id="identity-popup-connectedToLabel" value="&identity.connectedTo2;"/>
             <label id="identity-popup-connectedToLabel2" flex="1">&identity.unverifiedsite2;</label>
@@ -420,18 +439,18 @@
         </radiogroup>
       </arrowscrollbox>
     </vbox>
 
     <!-- options dialog for select form field -->
     <vbox id="select-container" hidden="true">
       <vbox id="select-container-inner" class="panel-dark" flex="1">
         <scrollbox id="select-list" flex="1" orient="vertical"/>
-        <hbox id="select-buttons">
-          <button id="select-buttons-done" class="button-dark" label="&selectListDone.label;" oncommand="SelectHelper.close();"/>
+        <hbox id="select-buttons" pack="center">
+          <button id="select-buttons-done" class="button-dark" label="&formHelper.done;" oncommand="SelectHelper.close();"/>
         </hbox>
       </vbox>
     </vbox>
 
     <!-- bookmark window -->
     <vbox id="bookmarklist-container" class="panel-dark" hidden="true">
       <hbox id="bookmarklist-header">
         <description flex="1">&bookmarksHeader.label;</description>
--- a/chrome/content/content.css
+++ b/chrome/content/content.css
@@ -49,16 +49,25 @@ scrollbarbutton {
 thumb {
   min-width: 10px !important;
   -moz-appearance: none !important;
   background-color: gray !important;
   border: 1px solid gray !important;
   -moz-border-radius: 4px !important;
 }
 
+textarea,
+input:not(type),
+input[type=""],
+input[type="file"],
+input[type="password"],
+input[type="text"] {
+  -moz-binding: url("chrome://browser/content/bindings.xml#chrome-input");
+}
+
 select {
   -moz-binding: url("chrome://browser/content/bindings.xml#chrome-select");
 }
 
 select:not([size]) > scrollbar,
 select[size="1"] > scrollbar,
 select:not([size]) scrollbarbutton,
 select[size="1"] scrollbarbutton {
--- a/locales/en-US/chrome/browser.dtd
+++ b/locales/en-US/chrome/browser.dtd
@@ -26,16 +26,20 @@
 <!ENTITY editBookmarkDone.label    "Done">
 <!ENTITY editBookmarkTags.label    "Add tags here">
 
 <!ENTITY helperApp.prompt          "What would you like to do with">
 <!ENTITY helperApp.open            "Open">
 <!ENTITY helperApp.save            "Save">
 <!ENTITY helperApp.nothing         "Nothing">
 
+<!ENTITY formHelper.previous       "Previous">
+<!ENTITY formHelper.next           "Next">
+<!ENTITY formHelper.done           "Done">
+
 <!ENTITY addonsHeader.label        "Add-ons">
 <!ENTITY addonsLocal.label         "Your Add-ons">
 <!ENTITY addonsUpdate.label        "Update">
 <!ENTITY addonsRepo.label          "Get Add-ons">
 <!ENTITY addonsRecommended.label   "Recommended">
 <!ENTITY addonsSearch.label        "Search">
 <!ENTITY addonsSearch2.emptytext   "Search Catalog">
 <!ENTITY addonsSearch.recommended  "Recommended">
@@ -84,10 +88,8 @@
 <!ENTITY consoleWarnings.label     "Warnings">
 <!ENTITY consoleMessages.label     "Messages">
 <!ENTITY consoleCodeEval.label     "Code:">
 <!ENTITY consoleClear.label        "Clear">
 <!ENTITY consoleEvaluate.label     "…">
 <!ENTITY consoleErrFile.label      "Source File:">
 <!ENTITY consoleErrLine.label      "Line:">
 <!ENTITY consoleErrColumn.label    "Column:">
-
-<!ENTITY selectListDone.label      "Done">
--- a/themes/hildon/browser.css
+++ b/themes/hildon/browser.css
@@ -862,31 +862,67 @@ settings .settings-title {
   white-space: pre-wrap;
 }
 
 /* helperapp (save-as) popup ----------------------------------------------- */
 #helperapp-target {
   font-size: 2.4mm !important;
 }
 
-/* select popup ------------------------------------------------------------ */
-#select-container {
-  padding: 3mm; /* half row size */
+/* form popup -------------------------------------------------------------- */
+#form-helper-container > #select-container > #select-container-inner {
+  -moz-border-radius-topleft: 1mm;
+  -moz-border-radius-topright: 1mm;
+  padding: 1mm 0.5mm 1mm 0.5mm;
+}
+
+#form-helper-container > #select-container > #select-container-inner,
+#form-buttons {
+  border: 0.1mm solid gray;
+  border-bottom: 0;
+}
+
+#form-buttons,
+#select-buttons {
+  padding: 0.5mm 1mm; /* row size & core spacing */
 }
 
-#select-container-inner {
+#form-buttons > button,
+#select-buttons > button {
+  -moz-user-focus: ignore;
+  -moz-user-select: none;
+}
+ 
+#form-helper-container #select-buttons {
+  display: none;
+}
+
+#select-container:not([hidden=true]) + #form-buttons {
+  border-top: 0;
+}
+
+/* select popup ------------------------------------------------------------ */
+#stack > #select-container {
+  padding: 3mm;
+}
+
+#stack > #select-container > #select-container-inner {
   border: 0.5mm solid #36373b;
   -moz-border-top-colors: #fff #36373b;
   -moz-border-right-colors: #fff #36373b;
   -moz-border-left-colors: #fff #36373b;
   -moz-border-bottom-colors: #fff #36373b;
   -moz-border-radius: 1mm;
   padding-top: 1mm; /* core spacing */
 }
 
+#select-list {
+  border: 0.1mm solid gray;
+}
+
 #select-list > option {
   color: #000;
   background-color: #fff;
   padding: 5px;
   border-bottom: 1px solid rgb(207,207,207);
   min-height: 6mm; /* row size */
   -moz-box-align: center;
 }
@@ -904,21 +940,16 @@ settings .settings-title {
 #select-list > option.optgroup > image {
   display: none;
 }
 
 #select-list > option.in-optgroup {
   -moz-padding-start: 2.2mm;
 }
 
-#select-buttons {
-  padding: 0.5mm 1mm; /* row size & core spacing */
-  -moz-box-pack: center;
-}
-
 #select-list > option > image {
   min-width: 30px;
 }
 
 #select-list > option[selected="true"] > image {
   list-style-image: url("chrome://browser/skin/images/check-30.png");
 }
 
--- a/themes/wince/browser.css
+++ b/themes/wince/browser.css
@@ -564,21 +564,83 @@ settings .settings-title {
   white-space: pre-wrap;
 }
 
 /* helperapp (save-as) popup ----------------------------------------------- */
 #helperapp-target {
   font-size: 8pt !important;
 }
 
+/* forms popup ------------------------------------------------------------- */
+ #forms-buttons {
+  border: 0.25mm solid black;
+  border-bottom: 0;
+  padding: 0.55mm 1.1mm; /* row size & core spacing */
+  -moz-box-pack: center;
+}
+ 
+#select-container:not([hidden=true]) + #forms-buttons {
+  border-top: 0;
+}
+
+#forms-buttons > button {
+  -moz-user-focus: ignore;
+  -moz-user-select: none;
+}
+
+/* form popup -------------------------------------------------------------- */
+#form-helper-container > #select-container > #select-container-inner {
+  -moz-border-radius-topleft: 1mm;
+  -moz-border-radius-topright: 1mm;
+  padding: 0.75mm 0.25mm 0.75mm 0.25mm;
+  border-bottom: 0;
+}
+
+#form-helper-container > #select-container > #select-container-inner,
+#form-buttons {
+  border: 0.25mm solid gray;
+  border-bottom: 0;
+}
+
+#form-buttons,
+#select-buttons {
+  padding: 0.25mm 0.5mm; /* row size & core spacing */
+}
+
+#form-buttons > button,
+#select-buttons > button {
+  -moz-user-focus: ignore;
+  -moz-user-select: none;
+}
+
+#form-helper-container #select-buttons {
+  display: none;
+}
+
+#select-container:not([hidden=true]) + #form-buttons {
+  border-top: 0;
+}
+
 /* select popup ------------------------------------------------------------ */
-#select-container {
-  border: 0.5mm solid #36373b;
-  -moz-border-radius: 1.0mm;
-  padding-top: 1.1mm; /* core spacing */
+#stack > #select-container {
+  padding: 1.5mm;
+}
+
+#stack > #select-container > #select-container-inner {
+  border: 0.25mm solid #36373b;
+  -moz-border-top-colors: #fff #36373b;
+  -moz-border-right-colors: #fff #36373b;
+  -moz-border-left-colors: #fff #36373b;
+  -moz-border-bottom-colors: #fff #36373b;
+  -moz-border-radius: 0.5mm;
+  padding-top: 0.5mm; /* core spacing */
+}
+
+#select-list {
+  border: 0.1mm solid gray;
 }
 
 #select-list > option {
   color: #000;
   background-color: #fff;
   padding: 5px;
   border-bottom: 1px solid rgb(207,207,207);
   min-height: 7.2mm; /* row size */
@@ -598,21 +660,16 @@ settings .settings-title {
 #select-list > option.optgroup > image {
   display: none;
 }
 
 #select-list > option.in-optgroup {
   -moz-padding-start: 4.4mm;
 }
 
-#select-buttons {
-  padding: 0.55mm 1.1mm; /* row size & core spacing */
-  -moz-box-pack: center;
-}
-
 #select-list > option > image {
   min-width: 30px;
 }
 
 .modal-block {
   -moz-box-align: center;
   -moz-box-pack: center;
   background-color: rgba(128, 128, 128, 0.5);