Bug 541817 - Fennec needs find in page functionality [r=mfinkle]
authorVivien Nicolas <21@vingtetun.org>
Mon, 19 Jul 2010 16:51:03 +0200
changeset 66372 0a43dfa18cd55e973454dd7b1bd5c2cbb424846f
parent 66371 c6716dfe0737720660374ce3f44aebe422b87016
child 66373 fa60222212c217dc50574147bb2dbfed1a6b8028
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs541817
Bug 541817 - Fennec needs find in page functionality [r=mfinkle]
mobile/app/mobile.js
mobile/chrome/content/bindings.xml
mobile/chrome/content/browser-ui.js
mobile/chrome/content/browser.css
mobile/chrome/content/browser.js
mobile/chrome/content/browser.xul
mobile/chrome/content/content.js
mobile/locales/en-US/chrome/browser.dtd
mobile/locales/en-US/chrome/browser.properties
mobile/themes/core/browser.css
--- a/mobile/app/mobile.js
+++ b/mobile/app/mobile.js
@@ -137,16 +137,19 @@ pref("signon.rememberSignons", true);
 pref("signon.expireMasterPassword", false);
 pref("signon.SignonFileName", "signons.txt");
 
 /* form helper */
 pref("formhelper.enabled", true);
 pref("formhelper.autozoom", true);
 pref("formhelper.restore", false);
 
+/* find helper */
+pref("findhelper.autozoom", true);
+
 /* autocomplete */
 pref("browser.formfill.enable", true);
 
 #ifdef WINCE
 pref("layout.css.devPixelsPerPx", "1");
 #endif
 
 /* spellcheck */
--- a/mobile/chrome/content/bindings.xml
+++ b/mobile/chrome/content/bindings.xml
@@ -1072,16 +1072,95 @@
       <handler event="mousedown" phase="capturing">
         <![CDATA[
           event.stopPropagation();
         ]]>
       </handler>
     </handlers>
   </binding>
 
+  <binding id="content-navigator">
+    <content pack="end">
+      <children includes="arrowscrollbox|vbox"/>
+      <xul:hbox class="panel-dark" pack="center">
+        <children includes="textbox"/>
+        <xul:button anonid="previous-button" class="button-dark" label="&contentNavigator.previous;" xbl:inherits="command=previous"/>
+        <xul:button anonid="next-button" class="button-dark" label="&contentNavigator.next;" xbl:inherits="command=next"/>
+        <xul:spacer flex="1"/>
+        <xul:toolbarbutton class="close-button" xbl:inherits="command=close"/>
+      </xul:hbox>
+    </content>
+
+    <implementation>
+      <field name="_previousButton">
+        document.getAnonymousElementByAttribute(this, "anonid", "previous-button");
+      </field>
+
+      <field name="_nextButton">
+        document.getAnonymousElementByAttribute(this, "anonid", "next-button");
+      </field>
+
+      <field name="_spacer">
+        document.getElementById(this.getAttribute("spacer"));
+      </field>
+
+      <method name="contentHasChanged">
+        <body><![CDATA[
+          let height = Math.floor(this.getBoundingClientRect().height);
+          this.top = window.innerHeight - height;
+          this._spacer.setAttribute("height", height);
+        ]]></body>
+      </method>
+
+      <field name="_model">null</field>
+      <method name="show">
+        <parameter name="aModel" />
+        <body><![CDATA[
+          // call the hide callback of the current object if any
+          if (this._model && this._model.type != aModel.type)
+            this._model.hide();
+
+          this.setAttribute("type", aModel.type);
+          this.setAttribute("next", aModel.commands.next);
+          this.setAttribute("previous", aModel.commands.previous);
+          this.setAttribute("close", aModel.commands.close);
+
+          // buttons attributes sync with commands doesn not look updated when
+          // we dynamically switch the "command" attribute so we need to ensure
+          // the disabled state of the buttons is right when switching commands
+          this._previousButton.disabled = document.getElementById(aModel.commands.previous).disabled;
+          this._nextButton.disabled = document.getElementById(aModel.commands.next).disabled;
+
+          this._model = aModel;
+          this.contentHasChanged();
+          this._spacer.hidden = false;
+        ]]></body>
+      </method>
+
+      <method name="hide">
+        <parameter name="aModel" />
+        <body><![CDATA[
+          this.removeAttribute("next");
+          this.removeAttribute("previous");
+          this.removeAttribute("close");
+          this.removeAttribute("type");
+
+          this._model = null;
+          this._spacer.hidden = false;
+
+          // give the form spacer area back to the content
+          // XXX this should probably be removed with layers
+          Browser.forceChromeReflow();
+          Browser.contentScrollboxScroller.scrollBy(0, 0);
+          Browser._browserView.onAfterVisibleMove();
+        ]]></body>
+      </method>
+    </implementation>
+  </binding>
+
   <binding id="menulist" display="xul:box" extends="chrome://global/content/bindings/menulist.xml#menulist">
     <handlers>
       <handler event="mousedown" phase="capturing">
         <![CDATA[
           // Stop the normal menupopup from appearing
           event.stopPropagation();
         ]]>
       </handler>
--- a/mobile/chrome/content/browser-ui.js
+++ b/mobile/chrome/content/browser-ui.js
@@ -334,19 +334,19 @@ var BrowserUI = {
     document.getElementById("tabs").resize();
 
     // awesomebar
     let popup = document.getElementById("popup_autocomplete");
     popup.top = this.toolbarH;
     popup.height = windowH - this.toolbarH;
     popup.width = windowW;
 
-    // form helper
-    let formHelper = document.getElementById("form-helper-container");
-    formHelper.top = windowH - formHelper.getBoundingClientRect().height;
+    // content navigator helper
+    let contentHelper = document.getElementById("content-navigator");
+    contentHelper.top = windowH - contentHelper.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);
 
@@ -394,16 +394,17 @@ var BrowserUI = {
       PreferencesView.init();
       ConsoleView.init();
 
       // Init the sync system
       WeaveGlue.init();
     });
 
     FormHelperUI.init();
+    FindHelperUI.init();
   },
 
   uninit: function() {
     ExtensionsView.uninit();
     ConsoleView.uninit();
   },
 
   update: function(aState) {
@@ -1426,31 +1427,123 @@ var BookmarkList = {
       if (this._bookmarks.isRootFolder && this._bookmarks.items.length == 1) {
         this._manageButton.disabled = true;
         this.toggleManage();
       }
     }
   }
 };
 
+
+var FindHelperUI = {
+  type: "find",
+  commands: {
+    next: "cmd_findNext",
+    previous: "cmd_findPrevious",
+    close: "cmd_findClose"
+  },
+
+  init: function findHelperInit() {
+    this._textbox = document.getElementById("find-helper-textbox");
+    this._container = document.getElementById("content-navigator");
+
+    // Listen for form assistant messages from content
+    messageManager.addMessageListener("FindAssist:Show", this);
+
+    // Listen for events where form assistant should be closed
+    document.getElementById("tabs").addEventListener("TabSelect", this, true);
+    document.getElementById("browsers").addEventListener("URLChanged", this, true);
+  },
+
+  receiveMessage: function findHelperReceiveMessage(aMessage) {
+    let json = aMessage.json;
+    switch(aMessage.name) {
+      case "FindAssist:Show":
+        if (json.rect)
+          this._zoom(Rect.fromRect(json.rect));
+        break;
+    }
+  },
+
+  handleEvent: function findHelperHandleEvent(aEvent) {
+    if (aEvent.type == "TabSelect" || aEvent.type == "URLChanged")
+      this.hide();
+  },
+
+  show: function findHelperShow() {
+    Browser._browserView.ignorePageScroll(true);
+    Browser.selectedBrowser.messageManager.sendAsyncMessage("FindAssist:Find", { searchString: "" });
+    this._container.show(this);
+    this._textbox.focus();
+  },
+
+  hide: function findHelperHide() {
+    Browser._browserView.ignorePageScroll(false);
+    this._textbox.value = "";
+    this._container.hide(this);
+  },
+
+  goToPrevious: function findHelperGoToPrevious() {
+    Browser.selectedBrowser.messageManager.sendAsyncMessage("FindAssist:Previous", { });
+  },
+
+  goToNext: function findHelperGoToNext() {
+    Browser.selectedBrowser.messageManager.sendAsyncMessage("FindAssist:Next", { });
+  },
+
+  search: function findHelperSearch(aValue) {
+    Browser.selectedBrowser.messageManager.sendAsyncMessage("FindAssist:Find", { searchString: aValue });
+  },
+
+  updateFindInPage: function findHelperUpdateFindInPage() {
+    PageActions.removeItems("findinpage");
+    let title = Elements.browserBundle.getString("pageactions.findInPage");
+    let node = PageActions.appendItem("findinpage", title, "");
+    node.onclick = function(event) {
+      BrowserUI._hidePopup();
+      FindHelperUI.show();
+    }
+  },
+
+  _zoom: function _findHelperZoom(aElementRect) {
+    let bv = Browser._browserView;
+    let zoomRect = bv.getVisibleRect();
+
+    // Zoom to a specified Rect
+    if (aElementRect && bv.allowZoom && Services.prefs.getBoolPref("findhelper.autozoom")) {
+      let zoomLevel = Browser._getZoomLevelForRect(bv.browserToViewportRect(aElementRect.clone()));
+      zoomLevel = Math.min(Math.max(kBrowserFormZoomLevelMin, zoomLevel), kBrowserFormZoomLevelMax);
+
+      zoomRect = Browser._getZoomRectForPoint(aElementRect.center().x, aElementRect.y, zoomLevel);
+      Browser.animatedZoomTo(zoomRect);
+    }
+  }
+};
+
 /**
  * Responsible for navigating forms and filling in information.
  *  - Navigating forms is handled by next and previous commands.
  *  - When an element is focused, the browser view zooms in to the control.
  *  - The caret positionning and the view are sync to keep the type
  *    in text into view for input fields (text/textarea).
  *  - Provides autocomplete box for input fields.
  */
 var FormHelperUI = {
+  type: "form",
+  commands: {
+    next: "cmd_formNext",
+    previous: "cmd_formPrevious",
+    close: "cmd_formClose"
+  },
+
   init: function formHelperInit() {
-    this._container = document.getElementById("form-helper-container");
-    this._cmdPrevious = document.getElementById("cmd_formPrevious");
-    this._cmdNext = document.getElementById("cmd_formNext");
-    this._helperSpacer = document.getElementById("form-helper-spacer");
+    this._container = document.getElementById("content-navigator");
     this._autofillContainer = document.getElementById("form-helper-autofill");
+    this._cmdPrevious = document.getElementById(this.commands.previous);
+    this._cmdNext = document.getElementById(this.commands.next);
 
     // Listen for form assistant messages from content
     messageManager.addMessageListener("FormAssist:Show", this);
     messageManager.addMessageListener("FormAssist:Hide", this);
     messageManager.addMessageListener("FormAssist:Update", this);
     messageManager.addMessageListener("FormAssist:AutoComplete", this);
 
     // Listen for events where form assistant should be closed
@@ -1480,17 +1573,17 @@ var FormHelperUI = {
   hide: function formHelperHide() {
     if (!this._open)
       return;
 
     this._updateContainerForSelect(this._currentElement, null);
     this._open = false;
   },
 
-  handleEvent: function(aEvent) {
+  handleEvent: function formHelperHandleEvent(aEvent) {
     if (aEvent.type == "TabSelect" || aEvent.type == "URLChanged")
       this.hide();
   },
 
   receiveMessage: function formHelperReceiveMessage(aMessage) {
     let json = aMessage.json;
     switch (aMessage.name) {
       case "FormAssist:Show":
@@ -1503,17 +1596,17 @@ var FormHelperUI = {
         }
         else {
           SelectHelperUI.show(json.current.list);
         }
         break;
 
       case "FormAssist:AutoComplete":
         this._updateAutocompleteFor(json.current);
-        this._updateHelperSize();
+        this._container.contentHasChanged();
         break;
 
       case "FormAssist:Update":
         this._zoom(null, Rect.fromRect(json.caretRect));
         break;
     }
   },
 
@@ -1527,40 +1620,34 @@ var FormHelperUI = {
 
   doAutoComplete: function formHelperDoAutoComplete(aElement) {
     // Suggestions are only in <label>s. Ignore the rest.
     if (aElement instanceof Ci.nsIDOMXULLabelElement)
       Browser.selectedBrowser.messageManager.sendAsyncMessage("FormAssist:AutoComplete", { value: aElement.value });
   },
 
   get _open() {
-    return !this._container.hidden;
+    return (this._container.getAttribute("type") == this.type);
   },
 
   set _open(aVal) {
     if (aVal == this._open)
       return;
 
     let bv = Browser._browserView;
     bv.ignorePageScroll(aVal);
     this._container.hidden = !aVal;
-    this._helperSpacer.hidden = !aVal;
 
     if (aVal) {
       this._zoomStart();
+      this._container.show(this);
     } else {
-      this._currentElement = null;
       this._zoomFinish();
-
-      // give the form spacer area back to the content
-      // XXX this should probably be removed with layers
-      let bv = Browser._browserView;
-      Browser.forceChromeReflow();
-      Browser.contentScrollboxScroller.scrollBy(0, 0);
-      bv.onAfterVisibleMove();
+      this._currentElement = null;
+      this._container.hide(this);
     }
 
     let evt = document.createEvent("UIEvents");
     evt.initUIEvent("FormUI", true, true, window, aVal);
     this._container.dispatchEvent(evt);
   },
 
   _updateAutocompleteFor: function _formHelperUpdateAutocompleteFor(aElement) {
@@ -1604,25 +1691,17 @@ var FormHelperUI = {
   },
 
   /** Update the form helper container to reflect new element user is editing. */
   _updateContainer: function _formHelperUpdateContainer(aLastElement, aCurrentElement) {
     this._updateContainerForSelect(aLastElement, aCurrentElement);
 
     // Setup autofill UI
     this._updateAutocompleteFor(aCurrentElement);
-    this._updateHelperSize();
-  },
-
-  _updateHelperSize: function _formHelperUpdateHelperSize() {
-    let height = Math.floor(this._container.getBoundingClientRect().height);
-    this._container.top = window.innerHeight - height;
-
-    let containerHeight = this._container.getBoundingClientRect().height;
-    this._helperSpacer.setAttribute("height", containerHeight);
+    this._container.contentHasChanged();
   },
 
   /** Helper for _updateContainer that handles the case where the new element is a select. */
   _updateContainerForSelect: function _formHelperUpdateContainerForSelect(aLastElement, aCurrentElement) {
     let lastHasChoices = aLastElement && (aLastElement.list != null);
     let currentHasChoices = aCurrentElement && (aCurrentElement.list != null);
 
     if (!lastHasChoices && currentHasChoices) {
@@ -1632,17 +1711,17 @@ var FormHelperUI = {
       SelectHelperUI.reset();
       SelectHelperUI.show(aCurrentElement.list);
     } else if (lastHasChoices && !currentHasChoices) {
       SelectHelperUI.hide();
     }
   },
 
   /** Zoom and move viewport so that element is legible and touchable. */
-  _zoom: function formHelperZoom(aElementRect, aCaretRect) {
+  _zoom: function _formHelperZoom(aElementRect, aCaretRect) {
     let bv = Browser._browserView;
     let zoomRect = bv.getVisibleRect();
 
     // Zoom to a specified Rect
     if (aElementRect && bv.allowZoom && Services.prefs.getBoolPref("formhelper.autozoom")) {
       // Zoom to an element by keeping the caret into view
       let zoomLevel = Browser._getZoomLevelForRect(bv.browserToViewportRect(aElementRect.clone()));
       zoomLevel = Math.min(Math.max(kBrowserFormZoomLevelMin, zoomLevel), kBrowserFormZoomLevelMax);
@@ -1676,26 +1755,26 @@ var FormHelperUI = {
       zoom: Browser._browserView.getZoomLevel(),
       contentScrollOffset: Browser.getScrollboxPosition(Browser.contentScrollboxScroller),
       pageScrollOffset: Browser.getScrollboxPosition(Browser.pageScrollboxScroller)
     };
   },
 
   /** Element is no longer selected. Restore zoom level if setting is enabled. */
   _zoomFinish: function _formHelperZoomFinish() {
-    if(!gPrefService.getBoolPref("formhelper.restore"))
+    if(!Services.prefs.getBoolPref("formhelper.restore"))
       return;
 
     let restore = this._restore;
     Browser._browserView.setZoomLevel(restore.zoom);
     Browser.contentScrollboxScroller.scrollTo(restore.contentScrollOffset.x, restore.contentScrollOffset.y);
     Browser.pageScrollboxScroller.scrollTo(restore.pageScrollOffset.x, restore.pageScrollOffset.y);
   },
 
-  _getOffsetForCaret: function formHelper_getOffsetForCaret(aCaretRect, aRect) {
+  _getOffsetForCaret: function _formHelperGetOffsetForCaret(aCaretRect, aRect) {
     // Determine if we need to move left or right to bring the caret into view
     let deltaX = 0;
     if (aCaretRect.right > aRect.right)
       deltaX = aCaretRect.right - aRect.right;
     if (aCaretRect.left < aRect.left)
       deltaX = aCaretRect.left - aRect.left;
 
     // Determine if we need to move up or down to bring the caret into view
@@ -2003,20 +2082,20 @@ var ContextHelper = {
 
     container.height = height;
     container.width = width;
     container.top = (window.innerHeight - height) / 2;
     container.left = (window.innerWidth - width) / 2;
 
     BrowserUI.pushPopup(this, [container]);
   },
-  
+
   hide: function ch_hide() {
     this.popupState = null;
-    
+
     let container = document.getElementById("context-popup");
     container.hidden = true;
 
     BrowserUI.popPopup();
   }
 };
 
 var ContextCommands = {
--- a/mobile/chrome/content/browser.css
+++ b/mobile/chrome/content/browser.css
@@ -5,16 +5,20 @@ browser[remote="false"] {
 browser[remote="true"] {
   -moz-binding: url("chrome://browser/content/bindings/browser.xml#remote-browser");
 }
 
 #urlbar-edit {
   -moz-binding: url("chrome://browser/content/bindings.xml#autocomplete-aligned");
 }
 
+#content-navigator {
+  -moz-binding: url("chrome://browser/content/bindings.xml#content-navigator");
+}
+
 #tabs {
   -moz-binding: url("chrome://browser/content/tabs.xml#tablist");
 }
 
 box[type="documenttab"] {
   -moz-binding: url("chrome://browser/content/tabs.xml#documenttab");
 }
 
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -1826,16 +1826,19 @@ IdentityHandler.prototype = {
     }
 
     // Push the appropriate strings out to the UI
     this._identityPopupContentHost.textContent = host;
     this._identityPopupContentOwner.textContent = owner;
     this._identityPopupContentSupp.textContent = supplemental;
     this._identityPopupContentVerif.textContent = verifier;
 
+    // Update the find in page in-site menu
+    FindHelperUI.updateFindInPage();
+
     // Update the search engines results
     BrowserSearch.updatePageSearchEngines();
 
     // Update the per site permissions results
     PageActions.updatePagePermissions();
 
     PageActions.updatePageSaveAs();
   },
--- a/mobile/chrome/content/browser.xul
+++ b/mobile/chrome/content/browser.xul
@@ -146,16 +146,21 @@
     <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="FormHelperUI.goToPrevious();"/>
     <command id="cmd_formNext" oncommand="FormHelperUI.goToNext();"/>
     <command id="cmd_formClose" oncommand="FormHelperUI.hide();"/>
+
+    <!-- find navigation -->
+    <command id="cmd_findPrevious" oncommand="FindHelperUI.goToPrevious();"/>
+    <command id="cmd_findNext" oncommand="FindHelperUI.goToNext();"/>
+    <command id="cmd_findClose" oncommand="FindHelperUI.hide();"/>
   </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"/>
@@ -258,40 +263,34 @@
                   <html:div id="tile-container" style="overflow: hidden;" tabindex="-1">
                     <html:canvas id="content-overlay" style="display: none; position: absolute; z-index: 1000; left: 0; top: 0;">
                     </html:canvas>
                   </html:div>
                 </scrollbox>
                 <html:canvas id="view-buffer" style="display: none;" moz-opaque="true">
                 </html:canvas>
               </stack>
-              <box id="form-helper-spacer" hidden="true"/>
+              <box id="content-navigator-spacer" hidden="true"/>
             </vbox>
-
           </vbox>
         </scrollbox>
 
-        <!-- popup for form helper -->
-        <vbox id="form-helper-container" hidden="true" class="window-width" top="0" pack="end">
+        <!-- popup for content navigator helper -->
+        <vbox id="content-navigator" class="window-width" top="0" spacer="content-navigator-spacer">
           <arrowscrollbox id="form-helper-autofill" collapsed="true" align="center" flex="1" orient="horizontal"
                           onclick="FormHelperUI.doAutoComplete(event.target);"/>
-          <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"/>
-            <toolbarbutton id="form-helper-close" class="close-button" command="cmd_formClose"/>
-          </hbox>
+          <textbox id="find-helper-textbox"  oncommand="FindHelperUI.search(this.value)" type="search" flex="1"/>
         </vbox>
       </stack>
 
       <!-- Right toolbar -->
       <vbox class="panel-dark">
         <!-- Because of the stack + fixed position of the urlbar when it is in
              locked mode the event on the top-right part of the urlbar are
-              swallow by this spacer, but not with the mousethrough attribute
+             swallow by this spacer, but not with the mousethrough attribute
         -->
         <spacer class="toolbar-height" mousethrough="always"/>
 
         <vbox id="browser-controls" style="overflow: -moz-hidden-unscrollable;" class="panel-dark" flex="1">
           <toolbarbutton id="tool-star" class="browser-control-button button-image" command="cmd_star"/>
           <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/>
@@ -487,17 +486,17 @@
     </vbox>
 
     <!-- options dialog for select form field -->
     <vbox id="select-container" hidden="true" pack="center">
       <spacer flex="1000"/>
       <vbox id="select-container-inner" class="dialog-dark" flex="1">
         <scrollbox id="select-list" flex="1" orient="vertical"/>
         <hbox id="select-buttons" pack="center">
-          <button id="select-buttons-done" class="button-dark" label="&formHelper.done;" oncommand="SelectHelperUI.hide();"/>
+          <button id="select-buttons-done" class="button-dark" label="&selectHelper.done;" oncommand="SelectHelperUI.hide();"/>
         </hbox>
       </vbox>
       <spacer flex="1000"/>
     </vbox>
 
     <!-- bookmark window -->
     <vbox id="bookmarklist-container" class="panel-dark" hidden="true">
       <hbox id="bookmarklist-header">
--- a/mobile/chrome/content/content.js
+++ b/mobile/chrome/content/content.js
@@ -826,8 +826,75 @@ var FormSubmitObserver = {
         !aIID.equals(Ci.nsISupportsWeakReference) &&
         !aIID.equals(Ci.nsISupports))
       throw Components.results.NS_ERROR_NO_INTERFACE;
     return this;
   }
 };
 
 FormSubmitObserver.init();
+
+var FindHandler = {
+  get _fastFind() {
+    delete this._fastFind;
+    this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(Ci.nsITypeAheadFind);
+    this._fastFind.init(docShell);
+    return this._fastFind;
+  },
+
+  get _selectionController() {
+    delete this._selectionController;
+    return this._selectionController = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                                               .getInterface(Ci.nsISelectionDisplay)
+                                               .QueryInterface(Ci.nsISelectionController);
+  },
+
+  init: function findHandlerInit() {
+    addMessageListener("FindAssist:Find", this);
+    addMessageListener("FindAssist:Next", this);
+    addMessageListener("FindAssist:Previous", this);
+  },
+
+  receiveMessage: function findHandlerReceiveMessage(aMessage) {
+    let findResult = Ci.nsITypeAheadFind.FIND_NOTFOUND;
+    let json = aMessage.json;
+    switch (aMessage.name) {
+      case "FindAssist:Find":
+        findResult = this._fastFind.find(json.searchString, false);
+        break;
+
+      case "FindAssist:Previous":
+        findResult = this._fastFind.findAgain(true, false);
+        break;
+
+      case "FindAssist:Next":
+        findResult = this._fastFind.findAgain(false, false);
+        break;
+    }
+
+    if (findResult == Ci.nsITypeAheadFind.FIND_NOTFOUND) {
+      sendAsyncMessage("FindAssist:Show", { rect: null , result: findResult });
+      return;
+    }
+
+    let controller = this._selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
+    if (!controller.rangeCount) {
+      // The selection can be into an input or a textarea element
+      let nodes = content.document.querySelectorAll("input[type='text'], textarea");
+      for (let i = 0; i < nodes.length; i++) {
+        let node = nodes[i];
+        if (node instanceof Ci.nsIDOMNSEditableElement && node.editor) {
+          controller = node.editor.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
+          if (controller.rangeCount)
+            break;
+        }
+      }
+    }
+
+    let range = controller.getRangeAt(0);
+    let scroll = Util.getScrollOffset(content);
+    let rect = range.getBoundingClientRect();
+    rect = new Rect(scroll.x + rect.left, scroll.y + rect.top, rect.width, rect.height);
+    sendAsyncMessage("FindAssist:Show", { rect: rect.isEmpty() ? null: rect , result: findResult });
+  }
+};
+
+FindHandler.init();
--- a/mobile/locales/en-US/chrome/browser.dtd
+++ b/mobile/locales/en-US/chrome/browser.dtd
@@ -22,19 +22,20 @@
 
 <!ENTITY bookmarksHeader.label     "Bookmarks">
 <!ENTITY bookmarksManage.label     "Manage">
 
 <!ENTITY editBookmarkRemove.label  "Remove">
 <!ENTITY editBookmarkDone.label    "Done">
 <!ENTITY editBookmarkTags.label    "Add tags here">
 
-<!ENTITY formHelper.previous       "Previous">
-<!ENTITY formHelper.next           "Next">
-<!ENTITY formHelper.done           "Done">
+<!ENTITY contentNavigator.previous "Previous">
+<!ENTITY contentNavigator.next     "Next">
+
+<!ENTITY selectHelper.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">
--- a/mobile/locales/en-US/chrome/browser.properties
+++ b/mobile/locales/en-US/chrome/browser.properties
@@ -144,16 +144,17 @@ homepage.custom2=Custom Page
 pageactions.saveas.pdf=Save As PDF
 pageactions.search.addNew=Add Search Engine
 pageactions.password.forget=Forget Password
 pageactions.reset=Clear Site Preferences
 pageactions.geo=Location
 pageactions.popup=Popups
 pageactions.offline-app=Offline Storage
 pageactions.password=Password
+pageactions.findInPage=Find
 
 # Helper App Dialog (Save/Open)
 helperApp.title=Opening File
 helperApp.prompt=What would you like to do with:
 helperApp.open=Open
 helperApp.save=Save
 helperApp.nothing=Nothing
 
--- a/mobile/themes/core/browser.css
+++ b/mobile/themes/core/browser.css
@@ -201,19 +201,21 @@ toolbarbutton.urlbar-cap-button {
   background: #fff;
   margin: 0 !important;
   padding: 0 !important;
   border: none !important;
   border-top: 1px solid #262629 !important;
   border-bottom: 3px solid #262629 !important;
   -moz-border-radius: 0;
 }
+
 #urlbar-edit > hbox > .textbox-input-box {
   margin: 0;
 }
+
 #urlbar-edit > hbox > hbox > .textbox-input {
   min-height: 60px;
   text-indent: 3px;
 }
 
 #urlbar-edit:hover:active {
   background-color: #8db8d8;
 }
@@ -1091,46 +1093,60 @@ pageaction .pageaction-desc[value=""] {
   white-space: pre-wrap;
 }
 
 /* helperapp (save-as) popup ----------------------------------------------- */
 #helperapp-target {
   font-size: 18px !important;
 }
 
-/* form popup -------------------------------------------------------------- */
-#form-helper-container > #select-container > #select-container-inner {
+/* navigator popup -------------------------------------------------------------- */
+#content-navigator,
+#content-navigator #select-buttons {
+  display: none;
+}
+
+#content-navigator[type="find"],
+#content-navigator[type="form"] {
+  display: -moz-box;
+}
+
+#content-navigator:not([type="form"]) > #form-helper-autofill {
+  visibility: collapse;
+}
+
+#content-navigator:not([type="form"]) > #select-container,
+#content-navigator:not([type="find"]) > #find-helper-textbox {
+  display: none;
+}
+
+#content-navigator > #select-container > #select-container-inner {
+  border-width: 0;
   -moz-border-radius-topleft: 8px;
   -moz-border-radius-topright: 8px;
   -moz-border-radius-bottomright: 0;
   -moz-border-radius-bottomleft: 0;
   padding: 8px 4px 8px 4px;
   -moz-box-flex: 0;
 }
 
-#form-helper-container > #select-container > #select-container-inner > scrollbox {
+#content-navigator > #select-container > #select-container-inner > scrollbox {
   min-height: 70px;
 }
 
-#form-helper-container > #select-container > spacer {
+#content-navigator > #select-container > spacer {
   display: none;
 }
 
-#form-helper-container > #select-container > #select-container-inner,
-#form-buttons {
-  border: 1px solid gray;
-  border-bottom: 0;
-}
-
-#form-buttons,
+#content-navigator > hbox,
 #select-buttons {
   padding: 4px 8px; /* row size & core spacing */
 }
 
-#form-buttons > button,
+#content-navigator > hbox > button,
 #select-buttons > button {
   -moz-user-focus: ignore;
   -moz-user-select: none;
 }
 
 #form-helper-autofill {
   padding: 4px 0; /* half core spacing & none (autorepeat arrows compensate) */
   border-top: 2px solid #36373b;