1 /* vim: set ts=2 sw=2 sts=2 et tw=80: */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
10 ChromeUtils.defineESModuleGetters(lazy, {
11 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
12 LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs",
15 const kStateActive = 0x00000001; // ElementState::ACTIVE
16 const kStateHover = 0x00000004; // ElementState::HOVER
18 // Duplicated in SelectParent.jsm
19 // Please keep these lists in sync.
20 const SUPPORTED_OPTION_OPTGROUP_PROPERTIES = [
32 const SUPPORTED_SELECT_PROPERTIES = [
33 ...SUPPORTED_OPTION_OPTGROUP_PROPERTIES,
38 // A process global state for whether or not content thinks
39 // that a <select> dropdown is open or not. This is managed
40 // entirely within this module, and is read-only accessible
41 // via SelectContentHelper.open.
44 export var SelectContentHelper = function(aElement, aOptions, aActor) {
45 this.element = aElement;
46 this.initialSelection = aElement[aElement.selectedIndex] || null;
48 this.closedWithClickOn = false;
49 this.isOpenedViaTouch = aOptions.isOpenedViaTouch;
50 this._closeAfterBlur = true;
51 this._pseudoStylesSetup = false;
52 this._lockedDescendants = null;
55 this._updateTimer = new lazy.DeferredTask(this._update.bind(this), 0);
58 Object.defineProperty(SelectContentHelper, "open", {
64 SelectContentHelper.prototype = {
66 let win = this.element.ownerGlobal;
67 win.addEventListener("pagehide", this, { mozSystemGroup: true });
68 this.element.addEventListener("blur", this, { mozSystemGroup: true });
69 this.element.addEventListener("transitionend", this, {
72 let MutationObserver = this.element.ownerGlobal.MutationObserver;
73 this.mut = new MutationObserver(mutations => {
74 // Something changed the <select> while it was open, so
75 // we'll poke a DeferredTask to update the parent sometime
76 // in the very near future.
77 this._updateTimer.arm();
79 this.mut.observe(this.element, {
85 XPCOMUtils.defineLazyPreferenceGetter(
87 "disablePopupAutohide",
88 "ui.popup.disable_autohide",
94 this.element.openInParentProcess = false;
95 let win = this.element.ownerGlobal;
96 win.removeEventListener("pagehide", this, { mozSystemGroup: true });
97 this.element.removeEventListener("blur", this, { mozSystemGroup: true });
98 this.element.removeEventListener("transitionend", this, {
103 this.mut.disconnect();
104 this._updateTimer.disarm();
105 this._updateTimer = null;
110 this.element.openInParentProcess = true;
111 this._setupPseudoClassStyles();
112 let rect = this._getBoundingContentRect();
113 let computedStyles = getComputedStyles(this.element);
114 let options = this._buildOptionList();
115 let defaultStyles = this.element.ownerGlobal.getDefaultComputedStyle(
118 this.actor.sendAsyncMessage("Forms:ShowDropDown", {
119 isOpenedViaTouch: this.isOpenedViaTouch,
122 custom: !this.element.nodePrincipal.isSystemPrincipal,
123 selectedIndex: this.element.selectedIndex,
124 isDarkBackground: ChromeUtils.isDarkBackground(this.element),
125 style: supportedStyles(computedStyles, SUPPORTED_SELECT_PROPERTIES),
126 defaultStyle: supportedStyles(defaultStyles, SUPPORTED_SELECT_PROPERTIES),
128 this._clearPseudoClassStyles();
132 _setupPseudoClassStyles() {
133 if (this._pseudoStylesSetup) {
134 throw new Error("pseudo styles must not be set up yet");
136 // Do all of the things that change style at once, before we read
138 this._pseudoStylesSetup = true;
139 InspectorUtils.addPseudoClassLock(this.element, ":focus");
140 let lockedDescendants = (this._lockedDescendants = this.element.querySelectorAll(
143 for (let child of lockedDescendants) {
144 // Selected options have the :checked pseudo-class, which
145 // we want to disable before calculating the computed
146 // styles since the user agent styles alter the styling
147 // based on :checked.
148 InspectorUtils.addPseudoClassLock(child, ":checked", false);
152 _clearPseudoClassStyles() {
153 if (!this._pseudoStylesSetup) {
154 throw new Error("pseudo styles must be set up already");
156 // Undo all of the things that change style at once, after we're
157 // done reading styles.
158 InspectorUtils.clearPseudoClassLocks(this.element);
159 let lockedDescendants = this._lockedDescendants;
160 for (let child of lockedDescendants) {
161 InspectorUtils.clearPseudoClassLocks(child);
163 this._lockedDescendants = null;
164 this._pseudoStylesSetup = false;
167 _getBoundingContentRect() {
168 return lazy.LayoutUtils.getElementBoundingScreenRect(this.element);
172 if (!this._pseudoStylesSetup) {
173 throw new Error("pseudo styles must be set up");
175 let uniqueStyles = [];
176 let options = buildOptionListForChildren(this.element, uniqueStyles);
177 return { options, uniqueStyles };
181 // The <select> was updated while the dropdown was open.
182 // Let's send up a new list of options.
183 // Technically we might not need to set this pseudo-class
184 // during _update() since the element should organically
185 // have :focus, though it is here for belt-and-suspenders.
186 this._setupPseudoClassStyles();
187 let computedStyles = getComputedStyles(this.element);
188 let defaultStyles = this.element.ownerGlobal.getDefaultComputedStyle(
191 this.actor.sendAsyncMessage("Forms:UpdateDropDown", {
192 options: this._buildOptionList(),
193 custom: !this.element.nodePrincipal.isSystemPrincipal,
194 selectedIndex: this.element.selectedIndex,
195 isDarkBackground: ChromeUtils.isDarkBackground(this.element),
196 style: supportedStyles(computedStyles, SUPPORTED_SELECT_PROPERTIES),
197 defaultStyle: supportedStyles(defaultStyles, SUPPORTED_SELECT_PROPERTIES),
199 this._clearPseudoClassStyles();
202 dispatchMouseEvent(win, target, eventName) {
203 let mouseEvent = new win.MouseEvent(eventName, {
209 target.dispatchEvent(mouseEvent);
212 receiveMessage(message) {
213 switch (message.name) {
214 case "Forms:SelectDropDownItem":
215 this.element.selectedIndex = message.data.value;
216 this.closedWithClickOn = !message.data.closedWithEnter;
219 case "Forms:DismissedDropDown": {
224 let win = this.element.ownerGlobal;
226 // Running arbitrary script below (dispatching events for example) can
227 // close us, but we should still send events consistently.
228 let element = this.element;
230 let selectedOption = element.item(element.selectedIndex);
232 // For ordering of events, we're using non-e10s as our guide here,
233 // since the spec isn't exactly clear. In non-e10s:
234 // - If the user clicks on an element in the dropdown, we fire
235 // mousedown, mouseup, input, change, and click events.
236 // - If the user uses the keyboard to select an element in the
237 // dropdown, we only fire input and change events.
238 // - If the user pressed ESC key or clicks outside the dropdown,
239 // we fire nothing as the selected option is unchanged.
240 if (this.closedWithClickOn) {
241 this.dispatchMouseEvent(win, selectedOption, "mousedown");
242 this.dispatchMouseEvent(win, selectedOption, "mouseup");
245 // Clear active document no matter user selects via keyboard or mouse
246 InspectorUtils.removeContentState(
249 /* aClearActiveDocument */ true
252 // Fire input and change events when selected option changes
253 if (this.initialSelection !== selectedOption) {
254 let inputEvent = new win.Event("input", {
259 let changeEvent = new win.Event("change", {
263 let handlingUserInput = win.windowUtils.setHandlingUserInput(true);
265 element.dispatchEvent(inputEvent);
266 element.dispatchEvent(changeEvent);
268 handlingUserInput.destruct();
273 if (this.closedWithClickOn) {
274 this.dispatchMouseEvent(win, selectedOption, "click");
281 case "Forms:MouseOver":
282 InspectorUtils.setContentState(this.element, kStateHover);
285 case "Forms:MouseOut":
286 InspectorUtils.removeContentState(this.element, kStateHover);
289 case "Forms:MouseUp":
290 let win = this.element.ownerGlobal;
291 if (message.data.onAnchor) {
292 this.dispatchMouseEvent(win, this.element, "mouseup");
294 InspectorUtils.removeContentState(this.element, kStateActive);
295 if (message.data.onAnchor) {
296 this.dispatchMouseEvent(win, this.element, "click");
300 case "Forms:SearchFocused":
301 this._closeAfterBlur = false;
304 case "Forms:BlurDropDown-Pong":
305 if (!this._closeAfterBlur || !gOpen) {
308 this.actor.sendAsyncMessage("Forms:HideDropDown", {});
315 switch (event.type) {
317 if (this.element.ownerDocument === event.target) {
318 this.actor.sendAsyncMessage("Forms:HideDropDown", {});
323 if (this.element !== event.target || this.disablePopupAutohide) {
326 this._closeAfterBlur = true;
327 // Send a ping-pong message to make sure that we wait for
328 // enough cycles to pass from the potential focusing of the
329 // search box to disable closing-after-blur.
330 this.actor.sendAsyncMessage("Forms:BlurDropDown-Ping", {});
333 case "mozhidedropdown":
334 if (this.element === event.target) {
335 this.actor.sendAsyncMessage("Forms:HideDropDown", {});
339 case "transitionend":
341 this.element === event.target &&
342 SUPPORTED_SELECT_PROPERTIES.includes(event.propertyName)
344 this._updateTimer.arm();
351 function getComputedStyles(element) {
352 return element.ownerGlobal.getComputedStyle(element);
355 function supportedStyles(cs, supportedProps) {
357 for (let property of supportedProps) {
358 styles[property] = cs.getPropertyValue(property);
363 function supportedStylesEqual(styles, otherStyles) {
364 for (let property in styles) {
365 if (styles[property] !== otherStyles[property]) {
372 function uniqueStylesIndex(cs, uniqueStyles) {
373 let styles = supportedStyles(cs, SUPPORTED_OPTION_OPTGROUP_PROPERTIES);
374 for (let i = uniqueStyles.length; i--; ) {
375 if (supportedStylesEqual(uniqueStyles[i], styles)) {
379 uniqueStyles.push(styles);
380 return uniqueStyles.length - 1;
383 function buildOptionListForChildren(node, uniqueStyles) {
386 for (let child of node.children) {
387 let className = ChromeUtils.getClassName(child);
388 let isOption = className == "HTMLOptionElement";
389 let isOptGroup = className == "HTMLOptGroupElement";
390 if (!isOption && !isOptGroup) {
397 // The option code-path should match HTMLOptionElement::GetRenderedLabel.
398 let textContent = isOptGroup
399 ? child.getAttribute("label")
400 : child.label || child.text;
401 if (textContent == null) {
405 let cs = getComputedStyles(child);
410 disabled: child.disabled,
412 tooltip: child.title,
414 ? buildOptionListForChildren(child, uniqueStyles)
416 // Most options have the same style. In order to reduce the size of the
417 // IPC message, coalesce them in uniqueStyles.
418 styleIndex: uniqueStylesIndex(cs, uniqueStyles),
425 // Hold the instance of SelectContentHelper created
426 // when the dropdown list is opened. This variable helps
427 // re-route the received message from SelectChild to SelectContentHelper object.
428 let currentSelectContentHelper = new WeakMap();
430 export class SelectChild extends JSWindowActorChild {
432 if (SelectContentHelper.open) {
433 // The SelectContentHelper object handles captured
434 // events when the <select> popup is open.
435 let contentHelper = currentSelectContentHelper.get(this);
437 contentHelper.handleEvent(event);
442 switch (event.type) {
443 case "mozshowdropdown": {
444 let contentHelper = new SelectContentHelper(
446 { isOpenedViaTouch: false },
449 currentSelectContentHelper.set(this, contentHelper);
453 case "mozshowdropdown-sourcetouch": {
454 let contentHelper = new SelectContentHelper(
456 { isOpenedViaTouch: true },
459 currentSelectContentHelper.set(this, contentHelper);
465 receiveMessage(message) {
466 let contentHelper = currentSelectContentHelper.get(this);
468 contentHelper.receiveMessage(message);