summaryrefslogtreecommitdiff
path: root/toolkit/modules/SelectParentHelper.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/SelectParentHelper.jsm')
-rw-r--r--toolkit/modules/SelectParentHelper.jsm211
1 files changed, 211 insertions, 0 deletions
diff --git a/toolkit/modules/SelectParentHelper.jsm b/toolkit/modules/SelectParentHelper.jsm
new file mode 100644
index 0000000000..2d37cdd5ee
--- /dev/null
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -0,0 +1,211 @@
+/* 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"
+];
+
+// Maximum number of rows to display in the select dropdown.
+const MAX_ROWS = 20;
+
+var currentBrowser = null;
+var currentMenulist = null;
+var currentZoom = 1;
+var closedWithEnter = false;
+
+this.SelectParentHelper = {
+ populate: function(menulist, items, selectedIndex, zoom) {
+ // Clear the current contents of the popup
+ menulist.menupopup.textContent = "";
+ currentZoom = zoom;
+ currentMenulist = menulist;
+ populateChildren(menulist, items, selectedIndex, zoom);
+ },
+
+ open: function(browser, menulist, rect, isOpenedViaTouch) {
+ menulist.hidden = false;
+ currentBrowser = browser;
+ closedWithEnter = false;
+ this._registerListeners(browser, menulist.menupopup);
+
+ let win = browser.ownerDocument.defaultView;
+
+ // 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("after_start", rect.left, rect.top, rect.width, rect.height, false, false);
+ },
+
+ hide: function(menulist, browser) {
+ if (currentBrowser == browser) {
+ menulist.menupopup.hidePopup();
+ }
+ },
+
+ handleEvent: function(event) {
+ switch (event.type) {
+ 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")) {
+ let win = currentBrowser.ownerDocument.defaultView;
+
+ currentBrowser.messageManager.sendAsyncMessage("Forms:SelectDropDownItem", {
+ value: event.target.value,
+ closedWithEnter: 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;
+ this.populate(currentMenulist, options, selectedIndex, currentZoom);
+ }
+ },
+
+ _registerListeners: function(browser, popup) {
+ popup.addEventListener("command", this);
+ popup.addEventListener("popuphidden", this);
+ popup.addEventListener("mouseover", this);
+ popup.addEventListener("mouseout", this);
+ browser.ownerDocument.defaultView.addEventListener("keydown", this, true);
+ browser.ownerDocument.defaultView.addEventListener("fullscreen", this, true);
+ browser.messageManager.addMessageListener("Forms:UpdateDropDown", this);
+ },
+
+ _unregisterListeners: function(browser, popup) {
+ popup.removeEventListener("command", this);
+ popup.removeEventListener("popuphidden", this);
+ popup.removeEventListener("mouseover", this);
+ popup.removeEventListener("mouseout", this);
+ browser.ownerDocument.defaultView.removeEventListener("keydown", this, true);
+ browser.ownerDocument.defaultView.removeEventListener("fullscreen", this, true);
+ browser.messageManager.removeMessageListener("Forms:UpdateDropDown", this);
+ },
+
+};
+
+function populateChildren(menulist, options, selectedIndex, zoom,
+ parentElement = null, isGroupDisabled = false, adjustedTextSize = -1) {
+ let element = menulist.menupopup;
+
+ // -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) {
+ let win = element.ownerDocument.defaultView;
+
+ // 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);
+ item.setAttribute("tooltiptext", option.tooltip);
+
+ element.appendChild(item);
+
+ // A disabled optgroup disables all of its child options.
+ let isDisabled = isGroupDisabled || option.disabled;
+ if (isDisabled) {
+ item.setAttribute("disabled", "true");
+ }
+
+ if (isOptGroup) {
+ populateChildren(menulist, option.children, selectedIndex, zoom,
+ item, isDisabled, adjustedTextSize);
+ } else {
+ if (option.index == selectedIndex) {
+ // We expect the parent element of the popup to be a <xul:menulist> that
+ // has the popuponly attribute set to "true". This is necessary in order
+ // for a <xul:menupopup> to act like a proper <html:select> dropdown, as
+ // the <xul:menulist> does things like remember state and set the
+ // _moz-menuactive attribute on the selected <xul:menuitem>.
+ menulist.selectedItem = item;
+
+ // It's hack time. In the event that we've re-populated the menulist due
+ // to a mutation in the <select> in content, that means that the -moz_activemenu
+ // may have been removed from the selected item. Since that's normally only
+ // set for the initially selected on popupshowing for the menulist, and we
+ // don't want to close and re-open the popup, we manually set it here.
+ menulist.menuBoxObject.activeChild = item;
+ }
+
+ item.setAttribute("value", option.index);
+
+ if (parentElement) {
+ item.classList.add("contentSelectDropdown-ingroup")
+ }
+ }
+ }
+}