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