From 9e3c08db40b8916968b9f30096c7be3f00ce9647 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 21 Apr 2024 13:44:51 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- toolkit/actors/SelectParent.sys.mjs | 798 ++++++++++++++++++++++++++++++++++++ 1 file changed, 798 insertions(+) create mode 100644 toolkit/actors/SelectParent.sys.mjs (limited to 'toolkit/actors/SelectParent.sys.mjs') diff --git a/toolkit/actors/SelectParent.sys.mjs b/toolkit/actors/SelectParent.sys.mjs new file mode 100644 index 0000000000..6c41903994 --- /dev/null +++ b/toolkit/actors/SelectParent.sys.mjs @@ -0,0 +1,798 @@ +/* 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/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { XPCOMUtils } from "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", + "text-transform", + "font-family", + "font-weight", + "font-size", + "font-style", +]; + +const SUPPORTED_SELECT_PROPERTIES = [ + ...SUPPORTED_OPTION_OPTGROUP_PROPERTIES, + "scrollbar-width", + "scrollbar-color", +]; + +export 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(menulist.menupopup); + + // 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; + } + + let win = menulist.ownerGlobal; + 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) { + browser.constrainPopup(menupopup); + } 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(popup); + popup.parentNode.hidden = true; + this._currentBrowser = null; + this._currentMenulist = null; + this._selectRect = null; + this._currentZoom = 1; + this._actor = null; + break; + } + }, + + receiveMessage(browser, msg) { + // Sanity check - we'd better know what the currently opened menulist is, + // and what browser it belongs to... + if (!this._currentMenulist || this._currentBrowser != browser) { + return; + } + + if (msg.name == "Forms:UpdateDropDown") { + 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(popup) { + popup.addEventListener("command", this); + popup.addEventListener("popuphidden", this); + popup.addEventListener("mouseover", this); + popup.addEventListener("mouseout", this); + popup.ownerGlobal.addEventListener("mouseup", this, true); + popup.ownerGlobal.addEventListener("keydown", this, true); + popup.ownerGlobal.addEventListener("fullscreen", this, true); + }, + + _unregisterListeners(popup) { + popup.removeEventListener("command", this); + popup.removeEventListener("popuphidden", this); + popup.removeEventListener("mouseover", this); + popup.removeEventListener("mouseout", this); + popup.ownerGlobal.removeEventListener("mouseup", this, true); + popup.ownerGlobal.removeEventListener("keydown", this, true); + popup.ownerGlobal.removeEventListener("fullscreen", this, true); + }, + + /** + * `populateChildren` creates all elements for the popup menu + * based on the list of