/* 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; var currentBrowser = null; var currentMenulist = null; var currentZoom = 1; var closedWithEnter = false; var selectRect; this.SelectParentHelper = { populate(menulist, items, selectedIndex, zoom, uaBackgroundColor, uaColor) { // Clear the current contents of the popup menulist.menupopup.textContent = ""; let stylesheet = menulist.querySelector("#ContentSelectDropdownScopedStylesheet"); if (stylesheet) { stylesheet.remove(); } currentZoom = zoom; currentMenulist = menulist; populateChildren(menulist, items, selectedIndex, zoom, uaBackgroundColor, uaColor); }, 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); let constraintRect = browser.getBoundingClientRect(); constraintRect = new win.DOMRect(constraintRect.left + win.mozInnerScreenX, constraintRect.top + win.mozInnerScreenY, constraintRect.width, constraintRect.height); menupopup.setConstraintRect(constraintRect); 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; currentZoom = 1; break; } }, receiveMessage(msg) { 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 || !currentBrowser) { return; } let options = msg.data.options; let selectedIndex = msg.data.selectedIndex; let uaBackgroundColor = msg.data.uaBackgroundColor; let uaColor = msg.data.uaColor; this.populate(currentMenulist, options, selectedIndex, currentZoom, uaBackgroundColor, uaColor); } }, _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); }, _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); }, }; function populateChildren(menulist, options, selectedIndex, zoom, uaBackgroundColor, uaColor, parentElement = null, isGroupDisabled = false, adjustedTextSize = -1, addSearch = true, nthChildIndex = 1) { let element = menulist.menupopup; let win = element.ownerGlobal; let scopedStyleSheet = menulist.querySelector("#ContentSelectDropdownScopedStylesheet"); if (!scopedStyleSheet) { let doc = element.ownerDocument; scopedStyleSheet = doc.createElementNS("http://www.w3.org/1999/xhtml", "style"); scopedStyleSheet.setAttribute("id", "ContentSelectDropdownScopedStylesheet"); scopedStyleSheet.scoped = true; scopedStyleSheet.hidden = true; scopedStyleSheet = menulist.appendChild(scopedStyleSheet); } // -1 just means we haven't calculated it yet. When we recurse through this function // we will pass in adjustedTextSize to save on recalculations. if (adjustedTextSize == -1) { // Grab the computed text size and multiply it by the remote browser's fullZoom to ensure // the popup's text size is matched with the content's. We can't just apply a CSS transform // here as the popup's preferred size is calculated pre-transform. let textSize = win.getComputedStyle(element).getPropertyValue("font-size"); adjustedTextSize = (zoom * parseFloat(textSize, 10)) + "px"; } for (let option of options) { let isOptGroup = (option.tagName == "OPTGROUP"); let item = element.ownerDocument.createElement(isOptGroup ? "menucaption" : "menuitem"); item.setAttribute("label", option.textContent); item.style.direction = option.textDirection; item.style.fontSize = adjustedTextSize; item.hidden = option.display == "none" || (parentElement && parentElement.hidden); // Keep track of which options are hidden by page content, so we can avoid showing // them on search input item.hiddenByContent = item.hidden; item.setAttribute("tooltiptext", option.tooltip); let ruleBody = ""; if (option.backgroundColor && option.backgroundColor != "transparent" && option.backgroundColor != uaBackgroundColor) { ruleBody = `background-color: ${option.backgroundColor};`; } if (option.color && option.color != uaColor) { ruleBody += `color: ${option.color};`; } if (ruleBody) { let sheet = scopedStyleSheet.sheet; sheet.insertRule(`${item.localName}:nth-child(${nthChildIndex}):not([_moz-menuactive="true"]) { ${ruleBody} }`, 0); item.setAttribute("customoptionstyling", "true"); } else { item.removeAttribute("customoptionstyling"); } element.appendChild(item); nthChildIndex++; // A disabled optgroup disables all of its child options. let isDisabled = isGroupDisabled || option.disabled; if (isDisabled) { item.setAttribute("disabled", "true"); } if (isOptGroup) { nthChildIndex = populateChildren(menulist, option.children, selectedIndex, zoom, uaBackgroundColor, uaColor, item, isDisabled, adjustedTextSize, false); } else { if (option.index == selectedIndex) { // We expect the parent element of the popup to be a that // has the popuponly attribute set to "true". This is necessary in order // for a to act like a proper dropdown, as // the does things like remember state and set the // _moz-menuactive attribute on the selected . menulist.selectedItem = item; // It's hack time. In the event that we've re-populated the menulist due // to a mutation in the