Bug 605365 - Show notification for invalid form elements (r=mfinkle)
authorLucas Rocha <lucasr@mozilla.com>
Mon, 19 Sep 2011 09:46:04 -0700
changeset 77139 f17d190c8c6cee9ebb9aff8bd797ec22d4956eef
parent 77138 a9adf1767b4668df8ad0fa036db2bf19a07616d0
child 77140 b37d37febea9b1c303d979c5c990ba7f1e386412
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmfinkle
bugs605365
milestone9.0a1
Bug 605365 - Show notification for invalid form elements (r=mfinkle)
mobile/chrome/content/browser.xul
mobile/chrome/content/common-ui.js
mobile/chrome/content/content.js
mobile/chrome/content/forms.js
--- a/mobile/chrome/content/browser.xul
+++ b/mobile/chrome/content/browser.xul
@@ -543,16 +543,21 @@
       <vbox id="popup_autocomplete" class="panel-dark place-list" flex="1" onshow="BrowserUI._edit.showHistoryPopup();" hidden="true"/>
       <placelist id="bookmarks-items" class="place-list" type="bookmarks" onopen="BookmarkList.openLink(event);" onhide="BrowserUI.updateStar();" flex="1" hidden="true"/>
       <historylist id="history-items" class="place-list" onopen="HistoryList.openLink(event);" flex="1" hidden="true"/>
 #ifdef MOZ_SERVICES_SYNC
       <remotetabslist id="remotetabs-items" class="place-list" onopen="RemoteTabsList.openLink(event)" flex="1" hidden="true"/>
 #endif
     </vbox>
 
+    <!-- Form Helper form validation helper popup -->
+    <arrowbox id="form-helper-validation-container" class="arrowbox-dark" flex="1" hidden="true" offset="0" top="0" left="0">
+      <label/>
+    </arrowbox>
+
 #ifdef MOZ_SERVICES_SYNC
     <box id="syncsetup-container" class="perm-modal-block" hidden="true">
       <dialog id="syncsetup-dialog" class="content-dialog" flex="1">
         <hbox class="prompt-title">
           <description>&sync.setup.title;</description>
         </hbox>
         <separator class="prompt-line"/>
         <vbox id="syncsetup-simple" class="syncsetup-page" flex="1">
--- a/mobile/chrome/content/common-ui.js
+++ b/mobile/chrome/content/common-ui.js
@@ -666,16 +666,17 @@ var FormHelperUI = {
     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:Resize", this);
     messageManager.addMessageListener("FormAssist:AutoComplete", this);
+    messageManager.addMessageListener("FormAssist:ValidationMessage", this);
 
     // Listen for events where form assistant should be closed or updated
     let tabs = Elements.tabList;
     tabs.addEventListener("TabSelect", this, true);
     tabs.addEventListener("TabClose", this, true);
     Elements.browsers.addEventListener("URLChanged", this, true);
     Elements.browsers.addEventListener("SizeChanged", this, true);
 
@@ -734,22 +735,23 @@ var FormHelperUI = {
       id: aElement.id,
       name: aElement.name,
       title: aElement.title,
       value: aElement.value,
       maxLength: aElement.maxLength,
       type: aElement.type,
       choices: aElement.choices,
       isAutocomplete: aElement.isAutocomplete,
+      validationMessage: aElement.validationMessage,
       list: aElement.list,
     }
 
     this._updateContainerForSelect(lastElement, this._currentElement);
     this._zoom(Rect.fromRect(aElement.rect), Rect.fromRect(aElement.caretRect));
-    this._updateSuggestionsFor(this._currentElement);
+    this._updatePopupsFor(this._currentElement);
 
     // Prevent the view to scroll automatically while typing
     this._currentBrowser.scrollSync = false;
   },
 
   hide: function formHelperHide() {
     if (!this._open)
       return;
@@ -823,34 +825,35 @@ var FormHelperUI = {
           SelectHelperUI.sizeToContent();
           self._zoom(self._currentElementRect, self._currentCaretRect);
         }, 0, this);
         break;
     }
   },
 
   receiveMessage: function formHelperReceiveMessage(aMessage) {
-    let allowedMessages = ["FormAssist:Show", "FormAssist:Hide", "FormAssist:AutoComplete"];
+    let allowedMessages = ["FormAssist:Show", "FormAssist:Hide",
+                           "FormAssist:AutoComplete", "FormAssist:ValidationMessage"];
     if (!this._open && allowedMessages.indexOf(aMessage.name) == -1)
       return;
 
     let json = aMessage.json;
     switch (aMessage.name) {
       case "FormAssist:Show":
         // if the user has manually disabled the Form Assistant UI we still
         // want to show a UI for <select /> element and still want to show
         // autocomplete suggestions but not managed by FormHelperUI
         if (this.enabled) {
           this.show(json.current, json.hasPrevious, json.hasNext)
         } else if (json.current.choices) {
           SelectHelperUI.show(json.current.choices, json.current.title);
         } else {
           this._currentElementRect = Rect.fromRect(json.current.rect);
           this._currentBrowser = getBrowser();
-          this._updateSuggestionsFor(json.current);
+          this._updatePopupsFor(json.current);
         }
         break;
 
       case "FormAssist:Hide":
         if (this.enabled) {
           this.hide();
         } else {
           SelectHelperUI.hide();
@@ -861,18 +864,22 @@ var FormHelperUI = {
       case "FormAssist:Resize":
         if (!Util.isKeyboardOpened)
           return;
 
         let element = json.current;
         this._zoom(Rect.fromRect(element.rect), Rect.fromRect(element.caretRect));
         break;
 
+      case "FormAssist:ValidationMessage":
+        this._updatePopupsFor(json.current, { fromInput: true });
+        break;
+
       case "FormAssist:AutoComplete":
-        this._updateSuggestionsFor(json.current);
+        this._updatePopupsFor(json.current, { fromInput: true });
         break;
 
        case "FormAssist:Update":
         if (!Util.isKeyboardOpened)
           return;
 
         Browser.hideSidebars();
         Browser.hideTitlebar();
@@ -933,31 +940,52 @@ var FormHelperUI = {
       this._container.style.display = "";
     }
 
     let evt = document.createEvent("UIEvents");
     evt.initUIEvent("FormUI", true, true, window, aVal);
     this._container.dispatchEvent(evt);
   },
 
+  _updatePopupsFor: function _formHelperUpdatePopupsFor(aElement, options) {
+    options = options || {};
+
+    let fromInput = 'fromInput' in options && options.fromInput;
+
+    // The order of the updates matters here. If the popup update was
+    // triggered from user input (e.g. key press in an input element),
+    // we first check if there are input suggestions then check for
+    // a validation message. The idea here is that the validation message
+    // will be shown straight away once the invalid element is focused
+    // and suggestions will be shown as user inputs data. Only one popup
+    // is shown at a time. If both are not shown, then we ensure any
+    // previous popups are hidden.
+    let noPopupsShown = fromInput ?
+                        (!this._updateSuggestionsFor(aElement) &&
+                         !this._updateFormValidationFor(aElement)) :
+                        (!this._updateFormValidationFor(aElement) &&
+                         !this._updateSuggestionsFor(aElement));
+
+    if (noPopupsShown)
+      ContentPopupHelper.popup = null;
+  },
+
   _updateSuggestionsFor: function _formHelperUpdateSuggestionsFor(aElement) {
     let suggestions = this._getAutocompleteSuggestions(aElement);
-    if (!suggestions.length) {
-      ContentPopupHelper.popup = null;
-      return;
-    }
+    if (!suggestions.length)
+      return false;
 
     // the scrollX/scrollY position can change because of the animated zoom so
     // delay the suggestions positioning
     if (AnimatedZoom.isZooming()) {
       let self = this;
       this._waitForZoom(function() {
         self._updateSuggestionsFor(aElement);
       });
-      return;
+      return true;
     }
 
     // Declare which box is going to be the inside container of the content popup helper
     let suggestionsContainer = document.getElementById("form-helper-suggestions-container");
     let container = suggestionsContainer.firstChild;
     while (container.hasChildNodes())
       container.removeChild(container.lastChild);
 
@@ -969,16 +997,43 @@ var FormHelperUI = {
       button.setAttribute("data", suggestion.value);
       button.className = "form-helper-suggestions-label";
       fragment.appendChild(button);
     }
     container.appendChild(fragment);
 
     ContentPopupHelper.popup = suggestionsContainer;
     ContentPopupHelper.anchorTo(this._currentElementRect);
+
+    return true;
+  },
+
+  _updateFormValidationFor: function _formHelperUpdateFormValidationFor(aElement) {
+    if (!aElement.validationMessage)
+      return false;
+
+    // the scrollX/scrollY position can change because of the animated zoom so
+    // delay the suggestions positioning
+    if (AnimatedZoom.isZooming()) {
+      let self = this;
+      this._waitForZoom(function() {
+        self._updateFormValidationFor(aElement);
+      });
+      return true;
+    }
+
+    let validationContainer = document.getElementById("form-helper-validation-container");
+
+    // Update label with form validation message
+    validationContainer.firstChild.value = aElement.validationMessage;
+
+    ContentPopupHelper.popup = validationContainer;
+    ContentPopupHelper.anchorTo(this._currentElementRect);
+
+    return true;
   },
 
   /** Retrieve the autocomplete list from the autocomplete service for an element */
   _getAutocompleteSuggestions: function _formHelperGetAutocompleteSuggestions(aElement) {
     if (!aElement.isAutocomplete)
       return [];
 
     let suggestions = [];
--- a/mobile/chrome/content/content.js
+++ b/mobile/chrome/content/content.js
@@ -1042,16 +1042,31 @@ ContextHandler.registerType("image-loade
   }
   return false;
 });
 
 var FormSubmitObserver = {
   init: function init(){
     addMessageListener("Browser:TabOpen", this);
     addMessageListener("Browser:TabClose", this);
+
+    addEventListener("pageshow", this, false);
+
+    Services.obs.addObserver(this, "invalidformsubmit", false);
+  },
+
+  handleEvent: function handleEvent(aEvent) {
+    let target = aEvent.originalTarget;
+    let isRootDocument = (target == content.document || target.ownerDocument == content.document);
+    if (!isRootDocument)
+      return;
+
+    // Reset invalid submit state on each pageshow
+    if (aEvent.type == "pageshow")
+      Content.formAssistant.invalidSubmit = false;
   },
 
   receiveMessage: function findHandlerReceiveMessage(aMessage) {
     let json = aMessage.json;
     switch (aMessage.name) {
       case "Browser:TabOpen":
         Services.obs.addObserver(this, "formsubmit", false);
         break;
@@ -1063,16 +1078,32 @@ var FormSubmitObserver = {
 
   notify: function notify(aFormElement, aWindow, aActionURI, aCancelSubmit) {
     // Do not notify unless this is the window where the submit occurred
     if (aWindow == content)
       // We don't need to send any data along
       sendAsyncMessage("Browser:FormSubmit", {});
   },
 
+  notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
+    if (!aInvalidElements.length)
+      return;
+
+    let element = aInvalidElements.queryElementAt(0, Ci.nsISupports);
+    if (!(element instanceof HTMLInputElement ||
+          element instanceof HTMLTextAreaElement ||
+          element instanceof HTMLSelectElement ||
+          element instanceof HTMLButtonElement)) {
+      return;
+    }
+
+    Content.formAssistant.invalidSubmit = true;
+    Content.formAssistant.open(element);
+  },
+
   QueryInterface : function(aIID) {
     if (!aIID.equals(Ci.nsIFormSubmitObserver) &&
         !aIID.equals(Ci.nsISupportsWeakReference) &&
         !aIID.equals(Ci.nsISupports))
       throw Cr.NS_ERROR_NO_INTERFACE;
     return this;
   }
 };
--- a/mobile/chrome/content/forms.js
+++ b/mobile/chrome/content/forms.js
@@ -85,16 +85,18 @@ function FormAssistant() {
                     Services.prefs.getBoolPref("formhelper.enabled") : false;
 };
 
 FormAssistant.prototype = {
   _selectWrapper: null,
   _currentIndex: -1,
   _elements: [],
 
+  invalidSubmit: false,
+
   get currentElement() {
     return this._elements[this._currentIndex];
   },
 
   get currentIndex() {
     return this._currentIndex;
   },
 
@@ -348,16 +350,19 @@ FormAssistant.prototype = {
         }
 
         let focusedIndex = this._getIndexForElement(focusedElement);
         if (focusedIndex != -1 && this.currentIndex != focusedIndex)
           this.currentIndex = focusedIndex;
         break;
 
       case "text":
+        if (this._isValidatable(aEvent.target))
+          sendAsyncMessage("FormAssist:ValidationMessage", this._getJSON());
+
         if (this._isAutocomplete(aEvent.target))
           sendAsyncMessage("FormAssist:AutoComplete", this._getJSON());
         break;
 
       // key processing inside a select element are done during the keypress
       // handler, preventing this one to be fired cancel the selection change
       case "keypress":
         // There is no need to handle keys if there is not element currently
@@ -434,16 +439,19 @@ FormAssistant.prototype = {
               this.close();
             break;
 
           case aEvent.DOM_VK_ESCAPE:
           case aEvent.DOM_VK_TAB:
             break;
 
           default:
+            if (this._isValidatable(aEvent.target))
+              sendAsyncMessage("FormAssist:ValidationMessage", this._getJSON());
+
             if (this._isAutocomplete(aEvent.target))
               sendAsyncMessage("FormAssist:AutoComplete", this._getJSON());
             else if (currentElement && this._isSelectElement(currentElement))
               this.currentIndex = this.currentIndex;
             break;
         }
 
         let caretRect = this._getCaretRect();
@@ -508,16 +516,24 @@ FormAssistant.prototype = {
       // Return the container frame if we are into a nested editable frame
       if (element && element instanceof HTMLBodyElement && element.ownerDocument.defaultView != content.document.defaultView)
         return element.ownerDocument.defaultView.frameElement;
     }
 
     return aElement;
   },
 
+  _isValidatable: function(aElement) {
+    return this.invalidSubmit &&
+           (aElement instanceof HTMLInputElement ||
+            aElement instanceof HTMLTextAreaElement ||
+            aElement instanceof HTMLSelectElement ||
+            aElement instanceof HTMLButtonElement);
+  },
+
   _isAutocomplete: function formHelperIsAutocomplete(aElement) {
     if (aElement instanceof HTMLInputElement) {
       let autocomplete = aElement.getAttribute("autocomplete");
       let allowedValues = ["off", "false", "disabled"];
       if (allowedValues.indexOf(autocomplete) == -1)
         return true;
     }
 
@@ -728,16 +744,17 @@ FormAssistant.prototype = {
         id: element.id,
         name: element.name,
         title: labels.length ? labels[0].title : "",
         value: element.value,
         maxLength: element.maxLength,
         type: (element.getAttribute("type") || "").toLowerCase(),
         choices: choices,
         isAutocomplete: this._isAutocomplete(element),
+        validationMessage: this.invalidSubmit ? element.validationMessage : null,
         list: this._getListSuggestions(element),
         rect: this._getRect(),
         caretRect: this._getCaretRect(),
         editable: editable
       },
       hasPrevious: !!this._elements[this._currentIndex - 1],
       hasNext: !!this._elements[this._currentIndex + 1]
     };