/* 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"; this.EXPORTED_SYMBOLS = [ "SelectParentHelper" ]; const {utils: Cu} = Components; const {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm", {}); const {Services} = Cu.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; // Make sure to clear these objects when the popup closes to avoid leaking. var currentBrowser = null; var currentMenulist = null; var selectRect = null; var currentZoom = 1; var closedWithEnter = false; var customStylingEnabled = Services.prefs.getBoolPref("dom.forms.select.customstyling"); this.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 alse * 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. if (customStylingEnabled && selectBackgroundColor != uaSelectBackgroundColor && selectBackgroundColor != "rgba(0, 0, 0, 0)") { ruleBody = `background-image: linear-gradient(${selectBackgroundColor}, ${selectBackgroundColor});`; usedSelectBackgroundColor = selectBackgroundColor; selectBackgroundSet = true; } else { usedSelectBackgroundColor = uaSelectBackgroundColor; } if (customStylingEnabled && selectColor != uaSelectColor && selectColor != usedSelectBackgroundColor) { ruleBody += `color: ${selectColor};`; usedSelectColor = selectColor; } else { usedSelectColor = uaColor; } if (customStylingEnabled && selectTextShadow != "none") { ruleBody += `text-shadow: ${selectTextShadow};`; sheet.insertRule(`#ContentSelectDropdown > menupopup > [_moz-menuactive="true"] { text-shadow: none; }`, 0); } if (ruleBody) { sheet.insertRule(`#ContentSelectDropdown > menupopup { ${ruleBody} }`, 0); 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"); } currentZoom = zoom; currentMenulist = menulist; populateChildren(menulist, items, selectedIndex, zoom, usedSelectBackgroundColor, usedSelectColor, selectTextShadow, selectBackgroundSet, sheet); }, open(browser, menulist, rect, isOpenedViaTouch) { menulist.hidden = false; currentBrowser = browser; closedWithEnter = false; 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.firstChild; while (firstItem && firstItem.hidden) { firstItem = firstItem.nextSibling; } 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 (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(currentMenulist.menupopup.getOuterScreenRect(), x, y) && inRect(selectRect, x, y) && currentMenulist.menupopup.state == "open"; currentBrowser.messageManager.sendAsyncMessage("Forms:MouseUp", { onAnchor }); break; case "mouseover": currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOver", {}); break; case "mouseout": currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOut", {}); break; case "keydown": if (event.keyCode == event.DOM_VK_RETURN) { closedWithEnter = true; } break; case "command": if (event.target.hasAttribute("value")) { currentBrowser.messageManager.sendAsyncMessage("Forms:SelectDropDownItem", { value: event.target.value, closedWithEnter }); } break; case "fullscreen": if (currentMenulist) { currentMenulist.menupopup.hidePopup(); } break; case "popuphidden": currentBrowser.messageManager.sendAsyncMessage("Forms:DismissedDropDown", {}); let popup = event.target; this._unregisterListeners(currentBrowser, popup); popup.parentNode.hidden = true; currentBrowser = null; currentMenulist = null; selectRect = null; currentZoom = 1; break; } }, receiveMessage(msg) { if (!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 (!currentMenulist) { return; } let scrollBox = currentMenulist.menupopup.scrollBox; let scrollTop = scrollBox.scrollTop; let options = msg.data.options; let selectedIndex = msg.data.selectedIndex; let uaBackgroundColor = msg.data.uaBackgroundColor; let uaColor = msg.data.uaColor; let uaSelectBackgroundColor = msg.data.uaSelectBackgroundColor; let uaSelectColor = msg.data.uaSelectColor; let selectBackgroundColor = msg.data.selectBackgroundColor; let selectColor = msg.data.selectColor; let selectTextShadow = msg.data.selectTextShadow; this.populate(currentMenulist, options, selectedIndex, currentZoom, uaBackgroundColor, uaColor, uaSelectBackgroundColor, uaSelectColor, selectBackgroundColor, selectColor, selectTextShadow); // Restore scroll position to what it was prior to the update. scrollBox.scrollTop = scrollTop; } else if (msg.name == "Forms:BlurDropDown-Ping") { currentBrowser.messageManager.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); browser.messageManager.addMessageListener("Forms:UpdateDropDown", this); browser.messageManager.addMessageListener("Forms:BlurDropDown-Ping", this); }, _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); browser.messageManager.removeMessageListener("Forms:UpdateDropDown", this); browser.messageManager.removeMessageListener("Forms:BlurDropDown-Ping", this); }, }; /** * `populateChildren` creates all elements for the popup menu * based on the list of