1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7this.EXPORTED_SYMBOLS = [ 8 "SelectParentHelper" 9]; 10 11// Maximum number of rows to display in the select dropdown. 12const MAX_ROWS = 20; 13 14var currentBrowser = null; 15var currentMenulist = null; 16var currentZoom = 1; 17var closedWithEnter = false; 18 19this.SelectParentHelper = { 20 populate: function(menulist, items, selectedIndex, zoom) { 21 // Clear the current contents of the popup 22 menulist.menupopup.textContent = ""; 23 currentZoom = zoom; 24 currentMenulist = menulist; 25 populateChildren(menulist, items, selectedIndex, zoom); 26 }, 27 28 open: function(browser, menulist, rect, isOpenedViaTouch) { 29 menulist.hidden = false; 30 currentBrowser = browser; 31 closedWithEnter = false; 32 this._registerListeners(browser, menulist.menupopup); 33 34 let win = browser.ownerDocument.defaultView; 35 36 // Set the maximum height to show exactly MAX_ROWS items. 37 let menupopup = menulist.menupopup; 38 let firstItem = menupopup.firstChild; 39 while (firstItem && firstItem.hidden) { 40 firstItem = firstItem.nextSibling; 41 } 42 43 if (firstItem) { 44 let itemHeight = firstItem.getBoundingClientRect().height; 45 46 // Include the padding and border on the popup. 47 let cs = win.getComputedStyle(menupopup); 48 let bpHeight = parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) + 49 parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom); 50 menupopup.style.maxHeight = (itemHeight * MAX_ROWS + bpHeight) + "px"; 51 } 52 53 menupopup.classList.toggle("isOpenedViaTouch", isOpenedViaTouch); 54 55 let constraintRect = browser.getBoundingClientRect(); 56 constraintRect = new win.DOMRect(constraintRect.left + win.mozInnerScreenX, 57 constraintRect.top + win.mozInnerScreenY, 58 constraintRect.width, constraintRect.height); 59 menupopup.setConstraintRect(constraintRect); 60 menupopup.openPopupAtScreenRect("after_start", rect.left, rect.top, rect.width, rect.height, false, false); 61 }, 62 63 hide: function(menulist, browser) { 64 if (currentBrowser == browser) { 65 menulist.menupopup.hidePopup(); 66 } 67 }, 68 69 handleEvent: function(event) { 70 switch (event.type) { 71 case "mouseover": 72 currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOver", {}); 73 break; 74 75 case "mouseout": 76 currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOut", {}); 77 break; 78 79 case "keydown": 80 if (event.keyCode == event.DOM_VK_RETURN) { 81 closedWithEnter = true; 82 } 83 break; 84 85 case "command": 86 if (event.target.hasAttribute("value")) { 87 let win = currentBrowser.ownerDocument.defaultView; 88 89 currentBrowser.messageManager.sendAsyncMessage("Forms:SelectDropDownItem", { 90 value: event.target.value, 91 closedWithEnter: closedWithEnter 92 }); 93 } 94 break; 95 96 case "fullscreen": 97 if (currentMenulist) { 98 currentMenulist.menupopup.hidePopup(); 99 } 100 break; 101 102 case "popuphidden": 103 currentBrowser.messageManager.sendAsyncMessage("Forms:DismissedDropDown", {}); 104 let popup = event.target; 105 this._unregisterListeners(currentBrowser, popup); 106 popup.parentNode.hidden = true; 107 currentBrowser = null; 108 currentMenulist = null; 109 currentZoom = 1; 110 break; 111 } 112 }, 113 114 receiveMessage(msg) { 115 if (msg.name == "Forms:UpdateDropDown") { 116 // Sanity check - we'd better know what the currently 117 // opened menulist is, and what browser it belongs to... 118 if (!currentMenulist || !currentBrowser) { 119 return; 120 } 121 122 let options = msg.data.options; 123 let selectedIndex = msg.data.selectedIndex; 124 this.populate(currentMenulist, options, selectedIndex, currentZoom); 125 } 126 }, 127 128 _registerListeners: function(browser, popup) { 129 popup.addEventListener("command", this); 130 popup.addEventListener("popuphidden", this); 131 popup.addEventListener("mouseover", this); 132 popup.addEventListener("mouseout", this); 133 browser.ownerDocument.defaultView.addEventListener("keydown", this, true); 134 browser.ownerDocument.defaultView.addEventListener("fullscreen", this, true); 135 browser.messageManager.addMessageListener("Forms:UpdateDropDown", this); 136 }, 137 138 _unregisterListeners: function(browser, popup) { 139 popup.removeEventListener("command", this); 140 popup.removeEventListener("popuphidden", this); 141 popup.removeEventListener("mouseover", this); 142 popup.removeEventListener("mouseout", this); 143 browser.ownerDocument.defaultView.removeEventListener("keydown", this, true); 144 browser.ownerDocument.defaultView.removeEventListener("fullscreen", this, true); 145 browser.messageManager.removeMessageListener("Forms:UpdateDropDown", this); 146 }, 147 148}; 149 150function populateChildren(menulist, options, selectedIndex, zoom, 151 parentElement = null, isGroupDisabled = false, adjustedTextSize = -1) { 152 let element = menulist.menupopup; 153 154 // -1 just means we haven't calculated it yet. When we recurse through this function 155 // we will pass in adjustedTextSize to save on recalculations. 156 if (adjustedTextSize == -1) { 157 let win = element.ownerDocument.defaultView; 158 159 // Grab the computed text size and multiply it by the remote browser's fullZoom to ensure 160 // the popup's text size is matched with the content's. We can't just apply a CSS transform 161 // here as the popup's preferred size is calculated pre-transform. 162 let textSize = win.getComputedStyle(element).getPropertyValue("font-size"); 163 adjustedTextSize = (zoom * parseFloat(textSize, 10)) + "px"; 164 } 165 166 for (let option of options) { 167 let isOptGroup = (option.tagName == 'OPTGROUP'); 168 let item = element.ownerDocument.createElement(isOptGroup ? "menucaption" : "menuitem"); 169 170 item.setAttribute("label", option.textContent); 171 item.style.direction = option.textDirection; 172 item.style.fontSize = adjustedTextSize; 173 item.hidden = option.display == "none" || (parentElement && parentElement.hidden); 174 item.setAttribute("tooltiptext", option.tooltip); 175 176 element.appendChild(item); 177 178 // A disabled optgroup disables all of its child options. 179 let isDisabled = isGroupDisabled || option.disabled; 180 if (isDisabled) { 181 item.setAttribute("disabled", "true"); 182 } 183 184 if (isOptGroup) { 185 populateChildren(menulist, option.children, selectedIndex, zoom, 186 item, isDisabled, adjustedTextSize); 187 } else { 188 if (option.index == selectedIndex) { 189 // We expect the parent element of the popup to be a <xul:menulist> that 190 // has the popuponly attribute set to "true". This is necessary in order 191 // for a <xul:menupopup> to act like a proper <html:select> dropdown, as 192 // the <xul:menulist> does things like remember state and set the 193 // _moz-menuactive attribute on the selected <xul:menuitem>. 194 menulist.selectedItem = item; 195 196 // It's hack time. In the event that we've re-populated the menulist due 197 // to a mutation in the <select> in content, that means that the -moz_activemenu 198 // may have been removed from the selected item. Since that's normally only 199 // set for the initially selected on popupshowing for the menulist, and we 200 // don't want to close and re-open the popup, we manually set it here. 201 menulist.menuBoxObject.activeChild = item; 202 } 203 204 item.setAttribute("value", option.index); 205 206 if (parentElement) { 207 item.classList.add("contentSelectDropdown-ingroup") 208 } 209 } 210 } 211} 212