FormValidationParent.sys.mjs (5851B)
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 * Chrome side handling of form validation popup. 7 */ 8 9 const lazy = {}; 10 11 ChromeUtils.defineESModuleGetters(lazy, { 12 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 13 }); 14 15 class PopupShownObserver { 16 _weakContext = null; 17 18 constructor(browsingContext) { 19 this._weakContext = Cu.getWeakReference(browsingContext); 20 } 21 22 observe(subject, topic) { 23 let ctxt = this._weakContext.get(); 24 let actor = ctxt.currentWindowGlobal?.getExistingActor("FormValidation"); 25 if (!actor) { 26 Services.obs.removeObserver(this, "popup-shown"); 27 return; 28 } 29 // If any panel besides ourselves shows, hide ourselves again. 30 if (topic == "popup-shown" && subject != actor._panel) { 31 actor._hidePopup(); 32 } 33 } 34 35 QueryInterface = ChromeUtils.generateQI([ 36 Ci.nsIObserver, 37 Ci.nsISupportsWeakReference, 38 ]); 39 } 40 41 export class FormValidationParent extends JSWindowActorParent { 42 constructor() { 43 super(); 44 45 this._panel = null; 46 this._obs = null; 47 } 48 49 static hasOpenPopups(ownPanel = null) { 50 for (let win of lazy.BrowserWindowTracker.orderedWindows) { 51 let popups = win.document.querySelectorAll("panel,menupopup"); 52 for (let popup of popups) { 53 if (popup == ownPanel) { 54 continue; // Skip our own panel if provided. 55 } 56 let { state } = popup; 57 if (state == "open" || state == "showing") { 58 return true; 59 } 60 } 61 } 62 return false; 63 } 64 65 /* 66 * Public apis 67 */ 68 69 uninit() { 70 this._panel = null; 71 this._obs = null; 72 } 73 74 hidePopup() { 75 this._hidePopup(); 76 } 77 78 /* 79 * Events 80 */ 81 82 receiveMessage(aMessage) { 83 switch (aMessage.name) { 84 case "FormValidation:ShowPopup": { 85 let browser = this.browsingContext.top.embedderElement; 86 let window = browser.ownerGlobal; 87 let data = aMessage.data; 88 let tabBrowser = window.gBrowser; 89 90 // target is the <browser>, make sure we're receiving a message 91 // from the foreground tab. 92 if (tabBrowser && browser != tabBrowser.selectedBrowser) { 93 return; 94 } 95 96 // If any other popups are open, we don't show the form validation 97 // popup. We have to fall through for our own popup to make sure the 98 // popup is updated if we are asked to reshow it with a different 99 // message or for a different element. 100 if (FormValidationParent.hasOpenPopups(this._panel)) { 101 return; 102 } 103 104 this._showPopup(browser, data); 105 break; 106 } 107 case "FormValidation:HidePopup": 108 this._hidePopup(); 109 break; 110 } 111 } 112 113 handleEvent(aEvent) { 114 switch (aEvent.type) { 115 case "FullZoomChange": 116 case "TextZoomChange": 117 case "scroll": 118 this._hidePopup(); 119 break; 120 case "popuphidden": 121 this._onPopupHidden(aEvent); 122 break; 123 } 124 } 125 126 /* 127 * Internal 128 */ 129 130 _onPopupHidden(aEvent) { 131 aEvent.originalTarget.removeEventListener("popuphidden", this, true); 132 Services.obs.removeObserver(this._obs, "popup-shown"); 133 let tabBrowser = aEvent.originalTarget.ownerGlobal.gBrowser; 134 tabBrowser.selectedBrowser.removeEventListener("scroll", this, true); 135 tabBrowser.selectedBrowser.removeEventListener("FullZoomChange", this); 136 tabBrowser.selectedBrowser.removeEventListener("TextZoomChange", this); 137 138 this._obs = null; 139 this._panel = null; 140 } 141 142 /** 143 * Shows the form validation popup at a specified position or updates the 144 * messaging and position if the popup is already displayed. 145 * 146 * @param {MozBrowser} aBrowser - Browser element that requests the popup. 147 * @param {object} aPanelData - Object that contains popup information 148 * aPanelData stucture detail: 149 * screenRect - the screen rect of the target element. 150 * position - popup positional string constants. 151 * message - the form element validation message text. 152 */ 153 _showPopup(aBrowser, aPanelData) { 154 let previouslyShown = !!this._panel; 155 this._panel = this._getAndMaybeCreatePanel(); 156 this._panel.firstChild.textContent = aPanelData.message; 157 158 // Display the panel if it isn't already visible. 159 if (previouslyShown) { 160 return; 161 } 162 // Cleanup after the popup is hidden 163 this._panel.addEventListener("popuphidden", this, true); 164 // Hide ourselves if other popups shown 165 this._obs = new PopupShownObserver(this.browsingContext); 166 Services.obs.addObserver(this._obs, "popup-shown", true); 167 168 // Hide if the user scrolls the page 169 aBrowser.addEventListener("scroll", this, true); 170 aBrowser.addEventListener("FullZoomChange", this); 171 aBrowser.addEventListener("TextZoomChange", this); 172 173 aBrowser.constrainPopup(this._panel); 174 175 // Open the popup 176 let rect = aPanelData.screenRect; 177 this._panel.openPopupAtScreenRect( 178 aPanelData.position, 179 rect.left, 180 rect.top, 181 rect.width, 182 rect.height, 183 false, 184 false 185 ); 186 } 187 188 /* 189 * Hide the popup if currently displayed. Will fire an event to onPopupHiding 190 * above if visible. 191 */ 192 _hidePopup() { 193 this._panel?.hidePopup(); 194 } 195 196 _getAndMaybeCreatePanel() { 197 // Lazy load the invalid form popup the first time we need to display it. 198 if (!this._panel) { 199 let browser = this.browsingContext.top.embedderElement; 200 let window = browser.ownerGlobal; 201 let template = window.document.getElementById("invalidFormTemplate"); 202 if (template) { 203 template.replaceWith(template.content); 204 } 205 this._panel = window.document.getElementById("invalid-form-popup"); 206 } 207 208 return this._panel; 209 } 210 }