no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / browser / actors / FormValidationChild.sys.mjs
blobbb67f1f1f48a3848284d052a49123f6f9edf6946
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 import { LayoutUtils } from "resource://gre/modules/LayoutUtils.sys.mjs";
12 export class FormValidationChild extends JSWindowActorChild {
13   constructor() {
14     super();
15     this._validationMessage = "";
16     this._element = null;
17   }
19   /*
20    * Events
21    */
23   handleEvent(aEvent) {
24     switch (aEvent.type) {
25       case "MozInvalidForm":
26         aEvent.preventDefault();
27         this.notifyInvalidSubmit(aEvent.detail);
28         break;
29       case "pageshow":
30         if (this._isRootDocumentEvent(aEvent)) {
31           this._hidePopup();
32         }
33         break;
34       case "pagehide":
35         // Act as if the element is being blurred. This will remove any
36         // listeners and hide the popup.
37         this._onBlur();
38         break;
39       case "input":
40         this._onInput(aEvent);
41         break;
42       case "blur":
43         this._onBlur(aEvent);
44         break;
45     }
46   }
48   notifyInvalidSubmit(aInvalidElements) {
49     // Show a validation message on the first focusable element.
50     for (let element of aInvalidElements) {
51       // Insure that this is the FormSubmitObserver associated with the
52       // element / window this notification is about.
53       if (this.contentWindow != element.ownerGlobal.document.defaultView) {
54         return;
55       }
57       if (
58         !(
59           ChromeUtils.getClassName(element) === "HTMLInputElement" ||
60           ChromeUtils.getClassName(element) === "HTMLTextAreaElement" ||
61           ChromeUtils.getClassName(element) === "HTMLSelectElement" ||
62           ChromeUtils.getClassName(element) === "HTMLButtonElement" ||
63           element.isFormAssociatedCustomElements
64         )
65       ) {
66         continue;
67       }
69       let validationMessage = element.isFormAssociatedCustomElements
70         ? element.internals.validationMessage
71         : element.validationMessage;
73       if (element.isFormAssociatedCustomElements) {
74         // For element that are form-associated custom elements, user agents
75         // should use their validation anchor instead.
76         // It is not clear how constraint validation should work for FACE in
77         // spec if the validation anchor is null, see
78         // https://github.com/whatwg/html/issues/10155. Blink seems fallback to
79         // FACE itself when validation anchor is null, which looks reasonable.
80         element = element.internals.validationAnchor || element;
81       }
83       if (!element || !Services.focus.elementIsFocusable(element, 0)) {
84         continue;
85       }
87       // Update validation message before showing notification
88       this._validationMessage = validationMessage;
90       // Don't connect up to the same element more than once.
91       if (this._element == element) {
92         this._showPopup(element);
93         break;
94       }
95       this._element = element;
97       element.focus();
99       // Watch for input changes which may change the validation message.
100       element.addEventListener("input", this);
102       // Watch for focus changes so we can disconnect our listeners and
103       // hide the popup.
104       element.addEventListener("blur", this);
106       this._showPopup(element);
107       break;
108     }
109   }
111   /*
112    * Internal
113    */
115   /*
116    * Handles input changes on the form element we've associated a popup
117    * with. Updates the validation message or closes the popup if form data
118    * becomes valid.
119    */
120   _onInput(aEvent) {
121     let element = aEvent.originalTarget;
123     // If the form input is now valid, hide the popup.
124     if (element.validity.valid) {
125       this._hidePopup();
126       return;
127     }
129     // If the element is still invalid for a new reason, we should update
130     // the popup error message.
131     if (this._validationMessage != element.validationMessage) {
132       this._validationMessage = element.validationMessage;
133       this._showPopup(element);
134     }
135   }
137   /*
138    * Blur event handler in which we disconnect from the form element and
139    * hide the popup.
140    */
141   _onBlur() {
142     if (this._element) {
143       this._element.removeEventListener("input", this);
144       this._element.removeEventListener("blur", this);
145     }
146     this._hidePopup();
147     this._element = null;
148   }
150   /*
151    * Send the show popup message to chrome with appropriate position
152    * information. Can be called repetitively to update the currently
153    * displayed popup position and text.
154    */
155   _showPopup(aElement) {
156     // Collect positional information and show the popup
157     let panelData = {};
159     panelData.message = this._validationMessage;
161     panelData.screenRect = LayoutUtils.getElementBoundingScreenRect(aElement);
163     // We want to show the popup at the middle of checkbox and radio buttons
164     // and where the content begin for the other elements.
165     if (
166       aElement.tagName == "INPUT" &&
167       (aElement.type == "radio" || aElement.type == "checkbox")
168     ) {
169       panelData.position = "bottomcenter topleft";
170     } else {
171       panelData.position = "after_start";
172     }
173     this.sendAsyncMessage("FormValidation:ShowPopup", panelData);
175     aElement.ownerGlobal.addEventListener("pagehide", this, {
176       mozSystemGroup: true,
177     });
178   }
180   _hidePopup() {
181     this.sendAsyncMessage("FormValidation:HidePopup", {});
182     this._element.ownerGlobal.removeEventListener("pagehide", this, {
183       mozSystemGroup: true,
184     });
185   }
187   _isRootDocumentEvent(aEvent) {
188     if (this.contentWindow == null) {
189       return true;
190     }
191     let target = aEvent.originalTarget;
192     return (
193       target == this.document ||
194       (target.ownerDocument && target.ownerDocument == this.document)
195     );
196   }