FormValidationChild.sys.mjs (5842B)
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/. */ 4 5 /** 6 * Handles the validation callback from nsIFormFillController and 7 * the display of the help panel on invalid elements. 8 */ 9 10 import { LayoutUtils } from "resource://gre/modules/LayoutUtils.sys.mjs"; 11 12 export class FormValidationChild extends JSWindowActorChild { 13 constructor() { 14 super(); 15 this._validationMessage = ""; 16 this._element = null; 17 } 18 19 /* 20 * Events 21 */ 22 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 } 47 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 } 56 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.isFormAssociatedCustomElement 64 ) 65 ) { 66 continue; 67 } 68 69 let validationMessage = element.isFormAssociatedCustomElement 70 ? element.internals.validationMessage 71 : element.validationMessage; 72 73 if (element.isFormAssociatedCustomElement) { 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 } 82 83 if (!element || !Services.focus.elementIsFocusable(element, 0)) { 84 continue; 85 } 86 87 // Update validation message before showing notification 88 this._validationMessage = validationMessage; 89 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; 96 97 element.focus(); 98 99 // Watch for input changes which may change the validation message. 100 element.addEventListener("input", this); 101 102 // Watch for focus changes so we can disconnect our listeners and 103 // hide the popup. 104 element.addEventListener("blur", this); 105 106 this._showPopup(element); 107 break; 108 } 109 } 110 111 /* 112 * Internal 113 */ 114 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; 122 123 // If the form input is now valid, hide the popup. 124 if (element.validity.valid) { 125 this._hidePopup(); 126 return; 127 } 128 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 } 136 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 } 149 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 = {}; 158 159 panelData.message = this._validationMessage; 160 161 panelData.screenRect = LayoutUtils.getElementBoundingScreenRect(aElement); 162 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); 174 175 aElement.ownerGlobal.addEventListener("pagehide", this, { 176 mozSystemGroup: true, 177 }); 178 } 179 180 _hidePopup() { 181 this.sendAsyncMessage("FormValidation:HidePopup", {}); 182 this._element.ownerGlobal.removeEventListener("pagehide", this, { 183 mozSystemGroup: true, 184 }); 185 } 186 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 } 197 }