Bumping gaia.json for 3 gaia revision(s) a=gaia-bump
[gecko.git] / browser / modules / FormSubmitObserver.jsm
blob72c5ca601a85d2f593f5763681eda4240b85f584
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /*
6  * Handles the validation callback from nsIFormFillController and
7  * the display of the help panel on invalid elements.
8  */
10 "use strict";
12 let Cc = Components.classes;
13 let Ci = Components.interfaces;
14 let Cu = Components.utils;
16 let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
17 let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
18 let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
19 let HTMLButtonElement = Ci.nsIDOMHTMLButtonElement;
21 this.EXPORTED_SYMBOLS = [ "FormSubmitObserver" ];
23 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
24 Cu.import("resource://gre/modules/Services.jsm");
25 Cu.import("resource://gre/modules/BrowserUtils.jsm");
27 function FormSubmitObserver(aWindow, aTabChildGlobal) {
28   this.init(aWindow, aTabChildGlobal);
31 FormSubmitObserver.prototype =
33   _validationMessage: "",
34   _content: null,
35   _element: null,
37   /*
38    * Public apis
39    */
41   init: function(aWindow, aTabChildGlobal)
42   {
43     this._content = aWindow;
44     this._tab = aTabChildGlobal;
45     this._mm = \r
46       this._content.QueryInterface(Ci.nsIInterfaceRequestor)\r
47                    .getInterface(Ci.nsIDocShell)\r
48                    .sameTypeRootTreeItem\r
49                    .QueryInterface(Ci.nsIDocShell)
50                    .QueryInterface(Ci.nsIInterfaceRequestor)
51                    .getInterface(Ci.nsIContentFrameMessageManager);
53     // nsIFormSubmitObserver callback about invalid forms. See HTMLFormElement
54     // for details.
55     Services.obs.addObserver(this, "invalidformsubmit", false);
56     this._tab.addEventListener("pageshow", this, false);
57     this._tab.addEventListener("unload", this, false);
58   },
60   uninit: function()
61   {
62     Services.obs.removeObserver(this, "invalidformsubmit");
63     this._content.removeEventListener("pageshow", this, false);
64     this._content.removeEventListener("unload", this, false);
65     this._mm = null;
66     this._element = null;
67     this._content = null;
68     this._tab = null;
69   },
71   /*
72    * Events
73    */
75   handleEvent: function (aEvent) {
76     switch (aEvent.type) {
77       case "pageshow":
78         if (this._isRootDocumentEvent(aEvent)) {
79           this._hidePopup();
80         }
81         break;
82       case "unload":
83         this.uninit();
84         break;
85       case "input":
86         this._onInput(aEvent);
87         break;
88       case "blur":
89         this._onBlur(aEvent);
90         break;
91     }
92   },
94   /*
95    * nsIFormSubmitObserver
96    */
98   notifyInvalidSubmit : function (aFormElement, aInvalidElements)
99   {
100     // We are going to handle invalid form submission attempt by focusing the
101     // first invalid element and show the corresponding validation message in a
102     // panel attached to the element.
103     if (!aInvalidElements.length) {
104       return;
105     }
106     
107     // Insure that this is the FormSubmitObserver associated with the form
108     // element / window this notification is about.
109     if (this._content != aFormElement.ownerDocument.defaultView.top.document.defaultView) {
110       return;
111     }
113     let element = aInvalidElements.queryElementAt(0, Ci.nsISupports);
114     if (!(element instanceof HTMLInputElement ||
115           element instanceof HTMLTextAreaElement ||
116           element instanceof HTMLSelectElement ||
117           element instanceof HTMLButtonElement)) {
118       return;
119     }
121     // Don't connect up to the same element more than once.
122     if (this._element == element) {
123       this._showPopup(element);
124       return;
125     }
126     this._element = element;
128     element.focus();
130     this._validationMessage = element.validationMessage;
132     // Watch for input changes which may change the validation message.
133     element.addEventListener("input", this, false);
135     // Watch for focus changes so we can disconnect our listeners and
136     // hide the popup.
137     element.addEventListener("blur", this, false);
139     this._showPopup(element);
140   },
142   /*
143    * Internal
144    */
145   \r
146   /*
147    * Handles input changes on the form element we've associated a popup
148    * with. Updates the validation message or closes the popup if form data
149    * becomes valid.
150    */
151   _onInput: function (aEvent) {
152     let element = aEvent.originalTarget;
154     // If the form input is now valid, hide the popup.
155     if (element.validity.valid) {
156       this._hidePopup();
157       return;
158     }
160     // If the element is still invalid for a new reason, we should update
161     // the popup error message.
162     if (this._validationMessage != element.validationMessage) {
163       this._validationMessage = element.validationMessage;
164       this._showPopup(element);
165     }
166   },
168   /*
169    * Blur event handler in which we disconnect from the form element and
170    * hide the popup.
171    */
172   _onBlur: function (aEvent) {
173     aEvent.originalTarget.removeEventListener("input", this, false);
174     aEvent.originalTarget.removeEventListener("blur", this, false);
175     this._element = null;
176     this._hidePopup();
177   },
179   /*
180    * Send the show popup message to chrome with appropriate position
181    * information. Can be called repetitively to update the currently
182    * displayed popup position and text.
183    */
184   _showPopup: function (aElement) {
185     // Collect positional information and show the popup
186     let panelData = {};
188     panelData.message = this._validationMessage;
190     // Note, this is relative to the browser and needs to be translated
191     // in chrome.
192     panelData.contentRect = this._msgRect(aElement);
194     // We want to show the popup at the middle of checkbox and radio buttons
195     // and where the content begin for the other elements.
196     let offset = 0;
197     let position = "";
199     if (aElement.tagName == 'INPUT' &&
200         (aElement.type == 'radio' || aElement.type == 'checkbox')) {
201       panelData.position = "bottomcenter topleft";
202     } else {
203       let win = aElement.ownerDocument.defaultView;
204       let style = win.getComputedStyle(aElement, null);
205       if (style.direction == 'rtl') {
206         offset = parseInt(style.paddingRight) + parseInt(style.borderRightWidth);
207       } else {
208         offset = parseInt(style.paddingLeft) + parseInt(style.borderLeftWidth);
209       }
210       let zoomFactor = this._getWindowUtils().fullZoom;
211       panelData.offset = Math.round(offset * zoomFactor);
212       panelData.position = "after_start";
213     }
214     this._mm.sendAsyncMessage("FormValidation:ShowPopup", panelData);
215   },
217   _hidePopup: function () {
218     this._mm.sendAsyncMessage("FormValidation:HidePopup", {});
219   },
221   _getWindowUtils: function () {\r
222     return this._content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);\r
223   },
225   _isRootDocumentEvent: function (aEvent) {
226     if (this._content == null) {
227       return true;
228     }
229     let target = aEvent.originalTarget;
230     return (target == this._content.document ||
231             (target.ownerDocument && target.ownerDocument == this._content.document));
232   },
234   /*
235    * Return a message manager rect for the element's bounding client rect
236    * in top level browser coords.
237    */
238   _msgRect: function (aElement) {
239     let domRect = aElement.getBoundingClientRect();
240     let zoomFactor = this._getWindowUtils().fullZoom;
241     let { offsetX, offsetY } = BrowserUtils.offsetToTopLevelWindow(this._content, aElement);
242     return {
243       left: (domRect.left + offsetX) * zoomFactor,
244       top: (domRect.top + offsetY) * zoomFactor,
245       width: domRect.width * zoomFactor,
246       height: domRect.height * zoomFactor
247     };
248   },
250   QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver])