/* 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.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const lazy = {}; XPCOMUtils.defineLazyPreferenceGetter( lazy, "DOM_FORMS_SELECTSEARCH", "dom.forms.selectSearch", false ); XPCOMUtils.defineLazyPreferenceGetter( lazy, "CUSTOM_STYLING_ENABLED", "dom.forms.select.customstyling", false ); // 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", ]; var SelectParentHelper = { /** * `populate` takes the `menulist` element and a list of `items` and generates * a popup list of options. * * If `CUSTOM_STYLING_ENABLED` 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 (selectBackgroundSet) { // We intentionally use the parsed color to prevent color // values like `url(..)` being injected into the // `background-image` property. let parsedColor = menupopup.style.backgroundColor; menupopup.style.setProperty( "--content-select-background-image", `linear-gradient(${parsedColor}, ${parsedColor})` ); // Always drop the background color to avoid messing with the custom // shadow on Windows 10 styling. menupopup.style.backgroundColor = ""; // If the background is set, we also make sure we set the color, to // prevent contrast issues. menupopup.style.setProperty("--panel-color", selectStyle.color); sheet.insertRule( `#ContentSelectDropdown > menupopup > :is(menuitem, menucaption):not([_moz-menuactive="true"]) { color: inherit; }`, 0 ); } } for (let i = 0, len = uniqueItemStyles.length; i < len; ++i) { sheet.insertRule( `#ContentSelectDropdown .ContentSelectDropdown-item-${i} {}`, 0 ); let style = uniqueItemStyles[i]; let rule = sheet.cssRules[0].style; rule.direction = style.direction; rule.fontSize = zoom * parseFloat(style["font-size"], 10) + "px"; if (!custom) { continue; } let optionBackgroundIsTransparent = style["background-color"] == "rgba(0, 0, 0, 0)"; let optionBackgroundSet = !optionBackgroundIsTransparent || style.color != selectStyle.color; if (optionBackgroundIsTransparent && style.color != selectStyle.color) { style["background-color"] = selectStyle["background-color"]; } if (style.color == style["background-color"]) { style.color = selectStyle.color; } let inactiveRule = null; for (const property of SUPPORTED_OPTION_OPTGROUP_PROPERTIES) { let shouldSkip = (function() { if (property == "direction" || property == "font-size") { // Handled elsewhere. return true; } if (!style[property]) { return true; } if (property == "background-color" || property == "color") { // This also depends on whether "color" is set. return !optionBackgroundSet; } return style[property] == selectStyle[property]; })(); if (shouldSkip) { continue; } if (PROPERTIES_RESET_WHEN_ACTIVE.includes(property)) { if (!inactiveRule) { sheet.insertRule( `#ContentSelectDropdown .ContentSelectDropdown-item-${i}:not([_moz-menuactive="true"]) {}`, 0 ); inactiveRule = sheet.cssRules[0].style; } inactiveRule[property] = style[property]; } else { rule[property] = style[property]; } } style.customStyling = selectBackgroundSet || optionBackgroundSet; } // 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 (custom && selectBackgroundSet) { menulist.menupopup.setAttribute("customoptionstyling", "true"); } else { menulist.menupopup.removeAttribute("customoptionstyling"); } this._currentZoom = zoom; this._currentMenulist = menulist; this.populateChildren(menulist, items, uniqueItemStyles, selectedIndex); }, 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": if ( !event.relatedTarget || !this._currentMenulist.contains(event.relatedTarget) ) { this._actor.sendAsyncMessage("Forms:MouseOver", {}); } break; case "mouseout": if ( !event.relatedTarget || !this._currentMenulist.contains(event.relatedTarget) ) { 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.custom && lazy.CUSTOM_STYLING_ENABLED, msg.data.isDarkBackground, 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