Bug 704879 - (5/6) Add form validation messages. r=lucasr
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Tue, 06 Mar 2012 11:56:44 -0800
changeset 88398 a979f7ffdba1926e38467b7a79c9cf216cfee0b6
parent 88397 c251670745e0935339b1df94903d6c34485da872
child 88399 f0d2d132bb7e71ede804b6aeb6099e36663fe111
push id22194
push usermak77@bonardo.net
push dateWed, 07 Mar 2012 09:33:54 +0000
treeherdermozilla-central@8ef88a69f861 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr
bugs704879
milestone13.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 704879 - (5/6) Add form validation messages. r=lucasr
mobile/android/base/FormAssistPopup.java
mobile/android/chrome/content/browser.js
--- a/mobile/android/base/FormAssistPopup.java
+++ b/mobile/android/base/FormAssistPopup.java
@@ -69,84 +69,122 @@ public class FormAssistPopup extends Lis
 
     private static final String LOGTAG = "FormAssistPopup";
 
     private static int sMinWidth = 0;
     private static int sRowHeight = 0;
     private static final int POPUP_MIN_WIDTH_IN_DPI = 200;
     private static final int POPUP_ROW_HEIGHT_IN_DPI = 32;
 
+    private static enum PopupType { NONE, AUTOCOMPLETE, VALIDATION };
+
+    // Keep track of the type of popup we're currently showing
+    private PopupType mTypeShowing = PopupType.NONE;
+
     public FormAssistPopup(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
 
         mAnimation = AnimationUtils.loadAnimation(context, R.anim.grow_fade_in);
         mAnimation.setDuration(75);
 
         setFocusable(false);
 
         setOnItemClickListener(new OnItemClickListener() {
             public void onItemClick(AdapterView<?> parentView, View view, int position, long id) {
-                String value = ((TextView) view).getText().toString();
-                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FormAssist:AutoComplete", value));
-                hide();
+                if (mTypeShowing.equals(PopupType.AUTOCOMPLETE)) {
+                    TextView textView = (TextView) view;
+                    String value = textView.getText().toString();
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FormAssist:AutoComplete", value));
+                    hide();
+                }
             }
         });
 
         GeckoAppShell.registerGeckoEventListener("FormAssist:AutoComplete", this);
+        GeckoAppShell.registerGeckoEventListener("FormAssist:ValidationMessage", this);
         GeckoAppShell.registerGeckoEventListener("FormAssist:Hide", this);
     }
 
     public void handleMessage(String event, JSONObject message) {
         try {
             if (event.equals("FormAssist:AutoComplete")) {
                 handleAutoCompleteMessage(message);
+            } else if (event.equals("FormAssist:ValidationMessage")) {
+                handleValidationMessage(message);
             } else if (event.equals("FormAssist:Hide")) {
                 handleHideMessage(message);
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     private void handleAutoCompleteMessage(JSONObject message) throws JSONException  {
         final JSONArray suggestions = message.getJSONArray("suggestions");
         final JSONArray rect = message.getJSONArray("rect");
         final double zoom = message.getDouble("zoom");
         GeckoApp.mAppContext.mMainHandler.post(new Runnable() {
             public void run() {
-                // Don't show autocomplete popup when using fullscreen VKB
-                InputMethodManager imm =
-                        (InputMethodManager) GeckoApp.mAppContext.getSystemService(Context.INPUT_METHOD_SERVICE);
-                if (!imm.isFullscreenMode())
-                    show(suggestions, rect, zoom);
+                showAutoCompleteSuggestions(suggestions, rect, zoom);
             }
         });
     }
 
+    private void handleValidationMessage(JSONObject message) throws JSONException {
+        final String validationMessage = message.getString("validationMessage");
+        final JSONArray rect = message.getJSONArray("rect");
+        final double zoom = message.getDouble("zoom");
+        GeckoApp.mAppContext.mMainHandler.post(new Runnable() {
+            public void run() {
+                showValidationMessage(validationMessage, rect, zoom);
+            }
+        });
+    }
+    
     private void handleHideMessage(JSONObject message) {
         GeckoApp.mAppContext.mMainHandler.post(new Runnable() {
             public void run() {
                 hide();
             }
         });
     }
 
-    public void show(JSONArray suggestions, JSONArray rect, double zoom) {
+    private void showAutoCompleteSuggestions(JSONArray suggestions, JSONArray rect, double zoom) {
         ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext, R.layout.autocomplete_list_item);
-        for (int i = 0; i < suggestions.length(); i++) {
-            try {
+        try {
+            for (int i = 0; i < suggestions.length(); i++)
                 adapter.add(suggestions.get(i).toString());
-            } catch (JSONException e) {
-                Log.i(LOGTAG, "JSONException: " + e);
-            }
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "JSONException: " + e);
         }
+        setAdapter(adapter);
 
+        if (positionAndShowPopup(rect, zoom))
+            mTypeShowing = PopupType.AUTOCOMPLETE;
+    }
+
+    // TODO: style the validation message popup differently (bug 731654)
+    private void showValidationMessage(String validationMessage, JSONArray rect, double zoom) {
+        ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext, R.layout.autocomplete_list_item);
+        adapter.add(validationMessage);
         setAdapter(adapter);
 
+        if (positionAndShowPopup(rect, zoom))
+            mTypeShowing = PopupType.VALIDATION;
+    }
+
+    // Returns true if the popup is successfully shown, false otherwise
+    public boolean positionAndShowPopup(JSONArray rect, double zoom) {
+        // Don't show the form assist popup when using fullscreen VKB
+        InputMethodManager imm =
+                (InputMethodManager) GeckoApp.mAppContext.getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (imm.isFullscreenMode())
+            return false;
+
         if (!isShown()) {
             setVisibility(View.VISIBLE);
             startAnimation(mAnimation);
         }
 
         if (mLayout == null) {
             mLayout = (RelativeLayout.LayoutParams) getLayoutParams();
             mWidth = mLayout.width;
@@ -188,17 +226,17 @@ public class FormAssistPopup extends Lis
         // listWidth can be negative if it is a constant - FILL_PARENT or MATCH_PARENT
         if (listWidth >= 0 && listWidth < sMinWidth) {
             listWidth = sMinWidth;
 
             if ((listLeft + listWidth) > viewport.width)
                 listLeft = (int) (viewport.width - listWidth);
         }
 
-        listHeight = sRowHeight * adapter.getCount();
+        listHeight = sRowHeight * getAdapter().getCount();
 
         // The text box doesnt fit below
         if ((listTop + listHeight) > viewport.height) {
             // Find where the maximum space is, and fit it there
             if ((viewport.height - listTop) > top) {
                 // Shrink the height to fit it below the text-box
                 listHeight = (int) (viewport.height - listTop);
             } else {
@@ -212,17 +250,20 @@ public class FormAssistPopup extends Lis
                 }
            }
         }
 
         mLayout = new RelativeLayout.LayoutParams(listWidth, listHeight);
         mLayout.setMargins(listLeft, listTop, 0, 0);
         setLayoutParams(mLayout);
         requestLayout();
+
+        return true;
     }
 
     public void hide() {
         if (isShown()) {
             setVisibility(View.GONE);
+            mTypeShowing = PopupType.NONE;
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FormAssist:Hidden", null));
         }
     }
 }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -2827,30 +2827,41 @@ var ErrorPageEventHandler = {
         }
         break;
       }
     }
   }
 };
 
 var FormAssistant = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
+
   // Used to keep track of the element that corresponds to the current
   // autocomplete suggestions
   _currentInputElement: null,
 
+  // Keep track of whether or not an invalid form has been submitted
+  _invalidSubmit: false,
+
   init: function() {
     Services.obs.addObserver(this, "FormAssist:AutoComplete", false);
     Services.obs.addObserver(this, "FormAssist:Hidden", false);
+    Services.obs.addObserver(this, "invalidformsubmit", false);
 
     BrowserApp.deck.addEventListener("input", this, false);
+    BrowserApp.deck.addEventListener("pageshow", this, false);
   },
 
   uninit: function() {
     Services.obs.removeObserver(this, "FormAssist:AutoComplete");
     Services.obs.removeObserver(this, "FormAssist:Hidden");
+    Services.obs.removeObserver(this, "invalidformsubmit");
+
+    BrowserApp.deck.removeEventListener("input", this);
+    BrowserApp.deck.removeEventListener("pageshow", this);
   },
 
   observe: function(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "FormAssist:AutoComplete":
         if (!this._currentInputElement)
           break;
 
@@ -2860,25 +2871,53 @@ var FormAssistant = {
         break;
 
       case "FormAssist:Hidden":
         this._currentInputElement = null;
         break;
     }
   },
 
+  notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
+    if (!aInvalidElements.length)
+      return;
+
+    // Ignore this notificaiton if the current tab doesn't contain the invalid form
+    if (BrowserApp.selectedBrowser.contentDocument !=
+        aFormElement.ownerDocument.defaultView.top.document)
+      return;
+
+    this._invalidSubmit = true;
+
+    let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
+    if (this._showValidationMessage(currentElement))
+      currentElement.focus();
+  },
+
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "input":
         let currentElement = aEvent.target;
+
+        // Since we can only show one popup at a time, prioritze autocomplete
+        // suggestions over a form validation message
         if (this._showAutoCompleteSuggestions(currentElement))
           break;
+        if (this._showValidationMessage(currentElement))
+          break;
 
         // If we're not showing autocomplete suggestions, hide the form assist popup
         this._hideFormAssistPopup();
+        break;
+
+      // Reset invalid submit state on each pageshow
+      case "pageshow":
+        let target = aEvent.originalTarget;
+        if (target == content.document || target.ownerDocument == content.document)
+          this._invalidSubmit = false;
     }
   },
 
   // We only want to show autocomplete suggestions for certain elements
   _isAutoComplete: function _isAutoComplete(aElement) {
     if (!(aElement instanceof HTMLInputElement) ||
         (aElement.getAttribute("type") == "password") ||
         (aElement.hasAttribute("autocomplete") &&
@@ -2948,16 +2987,49 @@ var FormAssistant = {
 
     // Keep track of input element so we can fill it in if the user
     // selects an autocomplete suggestion
     this._currentInputElement = aElement;
 
     return true;
   },
 
+  // Only show a validation message if the user submitted an invalid form,
+  // there's a non-empty message string, and the element is the correct type
+  _isValidateable: function _isValidateable(aElement) {
+    if (!this._invalidSubmit ||
+        !aElement.validationMessage ||
+        !(aElement instanceof HTMLInputElement ||
+          aElement instanceof HTMLTextAreaElement ||
+          aElement instanceof HTMLSelectElement ||
+          aElement instanceof HTMLButtonElement))
+      return false;
+
+    return true;
+  },
+
+  // Sends a validation message and position data for an element to the Java UI.
+  // Returns true if there's a validation message to show, false otherwise.
+  _showValidationMessage: function _sendValidationMessage(aElement) {
+    if (!this._isValidateable(aElement))
+      return false;
+
+    let positionData = this._getElementPositionData(aElement);
+    sendMessageToJava({
+      gecko: {
+        type: "FormAssist:ValidationMessage",
+        validationMessage: aElement.validationMessage,
+        rect: positionData.rect,
+        zoom: positionData.zoom
+      }
+    });
+
+    return true;
+  },
+
   _hideFormAssistPopup: function _hideFormAssistPopup() {
     sendMessageToJava({
       gecko: { type:  "FormAssist:Hide" }
     });
   }
 };
 
 var XPInstallObserver = {