/* vim: set ts=2 sw=2 sts=2 et tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; var EXPORTED_SYMBOLS = ["SelectParent", "SelectParentHelper"]; const { AppConstants } = ChromeUtils.import( "resource://gre/modules/AppConstants.jsm" ); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); // Maximum number of rows to display in the select dropdown. const MAX_ROWS = 20; // Minimum elements required to show select search const SEARCH_MINIMUM_ELEMENTS = 40; // The properties that we should respect only when the item is not active. const PROPERTIES_RESET_WHEN_ACTIVE = [ "color", "background-color", "text-shadow", ]; // Duplicated in SelectChild.jsm // Please keep these lists in sync. const SUPPORTED_OPTION_OPTGROUP_PROPERTIES = [ "direction", "color", "background-color", "text-shadow", "font-family", "font-weight", "font-size", "font-style", ]; const SUPPORTED_SELECT_PROPERTIES = [ ...SUPPORTED_OPTION_OPTGROUP_PROPERTIES, "scrollbar-width", "scrollbar-color", ]; const customStylingEnabled = Services.prefs.getBoolPref( "dom.forms.select.customstyling" ); var SelectParentHelper = { /** * `populate` takes the `menulist` element and a list of `items` and generates * a popup list of options. * * If `customStylingEnabled` is set to `true`, the function will also * style the select and its popup trying to prevent the text * and background to end up in the same color. * * All `ua*` variables represent the color values for the default colors * for their respective form elements used by the user agent. * The `select*` variables represent the color values defined for the * particular backgroundColor to transparent, // but they don't intend to change the popup to transparent. // So we remove the backgroundColor and turn it into an image instead. if (selectStyle["background-color"] != uaStyle["background-color"]) { // We intentionally use the parsed color to prevent color // values like `url(..)` being injected into the // `background-image` property. let parsedColor = sheet.cssRules[0].style["background-color"]; // The background color gets dropped unconditionally below. sheet.cssRules[0].style.setProperty( "--content-select-background-image", `linear-gradient(${parsedColor}, ${parsedColor})` ); selectBackgroundSet = true; } if (addedRule) { // Always drop the background color to avoid messing with the custom // shadow on Windows 10 styling. sheet.cssRules[0].style["background-color"] = ""; } if (addedRule) { sheet.insertRule( `#ContentSelectDropdown > menupopup > :not([_moz-menuactive="true"]) { color: inherit; }`, 0 ); } } // We only set the `customoptionstyling` if the background has been // manually set. This prevents the overlap between moz-appearance and // background-color. `color` and `text-shadow` do not interfere with it. if (selectBackgroundSet) { menulist.menupopup.setAttribute("customoptionstyling", "true"); } else { menulist.menupopup.removeAttribute("customoptionstyling"); } this._currentZoom = zoom; this._currentMenulist = menulist; this.populateChildren( menulist, items, uniqueItemStyles, selectedIndex, zoom, selectStyle, selectBackgroundSet, sheet ); }, open(browser, menulist, rect, isOpenedViaTouch, selectParentActor) { this._actor = selectParentActor; menulist.hidden = false; this._currentBrowser = browser; this._closedWithEnter = false; this._selectRect = rect; this._registerListeners(browser, menulist.menupopup); let win = browser.ownerGlobal; // Set the maximum height to show exactly MAX_ROWS items. let menupopup = menulist.menupopup; let firstItem = menupopup.firstElementChild; while (firstItem && firstItem.hidden) { firstItem = firstItem.nextElementSibling; } if (firstItem) { let itemHeight = firstItem.getBoundingClientRect().height; // Include the padding and border on the popup. let cs = win.getComputedStyle(menupopup); let bpHeight = parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) + parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom); menupopup.style.maxHeight = itemHeight * MAX_ROWS + bpHeight + "px"; } menupopup.classList.toggle("isOpenedViaTouch", isOpenedViaTouch); if (browser.getAttribute("selectmenuconstrained") != "false") { let constraintRect = browser.getBoundingClientRect(); constraintRect = new win.DOMRect( constraintRect.left + win.mozInnerScreenX, constraintRect.top + win.mozInnerScreenY, constraintRect.width, constraintRect.height ); menupopup.setConstraintRect(constraintRect); } else { menupopup.setConstraintRect(new win.DOMRect(0, 0, 0, 0)); } menupopup.openPopupAtScreenRect( AppConstants.platform == "macosx" ? "selection" : "after_start", rect.left, rect.top, rect.width, rect.height, false, false ); }, hide(menulist, browser) { if (this._currentBrowser == browser) { menulist.menupopup.hidePopup(); } }, handleEvent(event) { switch (event.type) { case "mouseup": function inRect(rect, x, y) { return ( x >= rect.left && x <= rect.left + rect.width && y >= rect.top && y <= rect.top + rect.height ); } let x = event.screenX, y = event.screenY; let onAnchor = !inRect(this._currentMenulist.menupopup.getOuterScreenRect(), x, y) && inRect(this._selectRect, x, y) && this._currentMenulist.menupopup.state == "open"; this._actor.sendAsyncMessage("Forms:MouseUp", { onAnchor }); break; case "mouseover": this._actor.sendAsyncMessage("Forms:MouseOver", {}); break; case "mouseout": this._actor.sendAsyncMessage("Forms:MouseOut", {}); break; case "keydown": if (event.keyCode == event.DOM_VK_RETURN) { this._closedWithEnter = true; } break; case "command": if (event.target.hasAttribute("value")) { this._actor.sendAsyncMessage("Forms:SelectDropDownItem", { value: event.target.value, closedWithEnter: this._closedWithEnter, }); } break; case "fullscreen": if (this._currentMenulist) { this._currentMenulist.menupopup.hidePopup(); } break; case "popuphidden": this._actor.sendAsyncMessage("Forms:DismissedDropDown", {}); let popup = event.target; this._unregisterListeners(this._currentBrowser, popup); popup.parentNode.hidden = true; this._currentBrowser = null; this._currentMenulist = null; this._selectRect = null; this._currentZoom = 1; this._actor = null; break; } }, receiveMessage(msg) { if (!this._currentBrowser) { return; } if (msg.name == "Forms:UpdateDropDown") { // Sanity check - we'd better know what the currently // opened menulist is, and what browser it belongs to... if (!this._currentMenulist) { return; } let scrollBox = this._currentMenulist.menupopup.scrollBox.scrollbox; let scrollTop = scrollBox.scrollTop; let options = msg.data.options; let selectedIndex = msg.data.selectedIndex; this.populate( this._currentMenulist, options.options, options.uniqueStyles, selectedIndex, this._currentZoom, msg.data.defaultStyle, msg.data.style ); // Restore scroll position to what it was prior to the update. scrollBox.scrollTop = scrollTop; } else if (msg.name == "Forms:BlurDropDown-Ping") { this._actor.sendAsyncMessage("Forms:BlurDropDown-Pong", {}); } }, _registerListeners(browser, popup) { popup.addEventListener("command", this); popup.addEventListener("popuphidden", this); popup.addEventListener("mouseover", this); popup.addEventListener("mouseout", this); browser.ownerGlobal.addEventListener("mouseup", this, true); browser.ownerGlobal.addEventListener("keydown", this, true); browser.ownerGlobal.addEventListener("fullscreen", this, true); }, _unregisterListeners(browser, popup) { popup.removeEventListener("command", this); popup.removeEventListener("popuphidden", this); popup.removeEventListener("mouseover", this); popup.removeEventListener("mouseout", this); browser.ownerGlobal.removeEventListener("mouseup", this, true); browser.ownerGlobal.removeEventListener("keydown", this, true); browser.ownerGlobal.removeEventListener("fullscreen", this, true); }, /** * `populateChildren` creates all elements for the popup menu * based on the list of