• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview Network drop-down implementation.
7 */
8
9cr.define('cr.ui', function() {
10  /**
11   * Whether keyboard flow is in use. When setting to true, up/down arrow key
12   * will be used to move focus instead of opening the drop down.
13   */
14  var useKeyboardFlow = false;
15
16  /**
17   * Creates a new container for the drop down menu items.
18   * @constructor
19   * @extends {HTMLDivElement}
20   */
21  var DropDownContainer = cr.ui.define('div');
22
23  DropDownContainer.prototype = {
24    __proto__: HTMLDivElement.prototype,
25
26    /** @override */
27    decorate: function() {
28      this.classList.add('dropdown-container');
29      // Selected item in the menu list.
30      this.selectedItem = null;
31      // First item which could be selected.
32      this.firstItem = null;
33      this.setAttribute('role', 'menu');
34      // Whether scroll has just happened.
35      this.scrollJustHappened = false;
36    },
37
38    /**
39     * Gets scroll action to be done for the item.
40     * @param {!Object} item Menu item.
41     * @return {integer} -1 for scroll up; 0 for no action; 1 for scroll down.
42     */
43    scrollAction: function(item) {
44      var thisTop = this.scrollTop;
45      var thisBottom = thisTop + this.offsetHeight;
46      var itemTop = item.offsetTop;
47      var itemBottom = itemTop + item.offsetHeight;
48      if (itemTop <= thisTop) return -1;
49      if (itemBottom >= thisBottom) return 1;
50      return 0;
51    },
52
53    /**
54     * Selects new item.
55     * @param {!Object} selectedItem Item to be selected.
56     * @param {boolean} mouseOver Is mouseover event triggered?
57     */
58    selectItem: function(selectedItem, mouseOver) {
59      if (mouseOver && this.scrollJustHappened) {
60        this.scrollJustHappened = false;
61        return;
62      }
63      if (this.selectedItem)
64        this.selectedItem.classList.remove('hover');
65      selectedItem.classList.add('hover');
66      this.selectedItem = selectedItem;
67      if (!this.hidden) {
68        this.previousSibling.setAttribute(
69            'aria-activedescendant', selectedItem.id);
70      }
71      var action = this.scrollAction(selectedItem);
72      if (action != 0) {
73        selectedItem.scrollIntoView(action < 0);
74        this.scrollJustHappened = true;
75      }
76    }
77  };
78
79  /**
80   * Creates a new DropDown div.
81   * @constructor
82   * @extends {HTMLDivElement}
83   */
84  var DropDown = cr.ui.define('div');
85
86  DropDown.ITEM_DIVIDER_ID = -2;
87
88  DropDown.KEYCODE_DOWN = 40;
89  DropDown.KEYCODE_ENTER = 13;
90  DropDown.KEYCODE_ESC = 27;
91  DropDown.KEYCODE_SPACE = 32;
92  DropDown.KEYCODE_TAB = 9;
93  DropDown.KEYCODE_UP = 38;
94
95  DropDown.prototype = {
96    __proto__: HTMLDivElement.prototype,
97
98    /** @override */
99    decorate: function() {
100      this.appendChild(this.createOverlay_());
101      this.appendChild(this.title_ = this.createTitle_());
102      var container = new DropDownContainer();
103      container.id = this.id + '-dropdown-container';
104      this.appendChild(container);
105
106      this.addEventListener('keydown', this.keyDownHandler_);
107
108      this.title_.id = this.id + '-dropdown';
109      this.title_.setAttribute('role', 'button');
110      this.title_.setAttribute('aria-haspopup', 'true');
111      this.title_.setAttribute('aria-owns', container.id);
112    },
113
114    /**
115     * Returns true if dropdown menu is shown.
116     * @type {bool} Whether menu element is shown.
117     */
118    get isShown() {
119      return !this.container.hidden;
120    },
121
122    /**
123     * Sets dropdown menu visibility.
124     * @param {bool} show New visibility state for dropdown menu.
125     */
126    set isShown(show) {
127      this.firstElementChild.hidden = !show;
128      this.container.hidden = !show;
129      if (show) {
130        this.container.selectItem(this.container.firstItem, false);
131      } else {
132        this.title_.removeAttribute('aria-activedescendant');
133      }
134
135      // Flag for keyboard flow util to forward the up/down keys.
136      this.title_.classList.toggle('needs-up-down-keys', show);
137    },
138
139    /**
140     * Returns container of the menu items.
141     */
142    get container() {
143      return this.lastElementChild;
144    },
145
146    /**
147     * Sets title and icon.
148     * @param {string} title Text on dropdown.
149     * @param {string} icon Icon in dataURL format.
150     */
151    setTitle: function(title, icon) {
152      this.title_.firstElementChild.src = icon;
153      this.title_.lastElementChild.textContent = title;
154    },
155
156    /**
157     * Sets dropdown items.
158     * @param {Array} items Dropdown items array.
159     */
160    setItems: function(items) {
161      this.container.innerHTML = '';
162      this.container.firstItem = null;
163      this.container.selectedItem = null;
164      for (var i = 0; i < items.length; ++i) {
165        var item = items[i];
166        if ('sub' in item) {
167          // Workaround for submenus, add items on top level.
168          // TODO(altimofeev): support submenus.
169          for (var j = 0; j < item.sub.length; ++j)
170            this.createItem_(this.container, item.sub[j]);
171          continue;
172        }
173        this.createItem_(this.container, item);
174      }
175      this.container.selectItem(this.container.firstItem, false);
176    },
177
178    /**
179     * Id of the active drop-down element.
180     * @private
181     */
182    activeElementId_: '',
183
184    /**
185     * Creates dropdown item element and adds into container.
186     * @param {HTMLElement} container Container where item is added.
187     * @param {!Object} item Item to be added.
188     * @private
189     */
190    createItem_: function(container, item) {
191      var itemContentElement;
192      var className = 'dropdown-item';
193      if (item.id == DropDown.ITEM_DIVIDER_ID) {
194        className = 'dropdown-divider';
195        itemContentElement = this.ownerDocument.createElement('hr');
196      } else {
197        var span = this.ownerDocument.createElement('span');
198        itemContentElement = span;
199        span.textContent = item.label;
200        if ('bold' in item && item.bold)
201          span.classList.add('bold');
202        var image = this.ownerDocument.createElement('img');
203        image.alt = '';
204        image.classList.add('dropdown-image');
205        if (item.icon)
206          image.src = item.icon;
207      }
208
209      var itemElement = this.ownerDocument.createElement('div');
210      itemElement.classList.add(className);
211      itemElement.appendChild(itemContentElement);
212      itemElement.iid = item.id;
213      itemElement.controller = this;
214      var enabled = 'enabled' in item && item.enabled;
215      if (!enabled)
216        itemElement.classList.add('disabled-item');
217
218      if (item.id > 0) {
219        var wrapperDiv = this.ownerDocument.createElement('div');
220        wrapperDiv.setAttribute('role', 'menuitem');
221        wrapperDiv.id = this.id + item.id;
222        if (!enabled)
223          wrapperDiv.setAttribute('aria-disabled', 'true');
224        wrapperDiv.classList.add('dropdown-item-container');
225        var imageDiv = this.ownerDocument.createElement('div');
226        imageDiv.appendChild(image);
227        wrapperDiv.appendChild(imageDiv);
228        wrapperDiv.appendChild(itemElement);
229        wrapperDiv.addEventListener('click', function f(e) {
230          var item = this.lastElementChild;
231          if (item.iid < -1 || item.classList.contains('disabled-item'))
232            return;
233          item.controller.isShown = false;
234          if (item.iid >= 0)
235            chrome.send('networkItemChosen', [item.iid]);
236          this.parentNode.parentNode.title_.focus();
237        });
238        wrapperDiv.addEventListener('mouseover', function f(e) {
239          this.parentNode.selectItem(this, true);
240        });
241        itemElement = wrapperDiv;
242      }
243      container.appendChild(itemElement);
244      if (!container.firstItem && item.id >= 0) {
245        container.firstItem = itemElement;
246      }
247    },
248
249    /**
250     * Creates dropdown overlay element, which catches outside clicks.
251     * @type {HTMLElement}
252     * @private
253     */
254    createOverlay_: function() {
255      var overlay = this.ownerDocument.createElement('div');
256      overlay.classList.add('dropdown-overlay');
257      overlay.addEventListener('click', function() {
258        this.parentNode.title_.focus();
259        this.parentNode.isShown = false;
260      });
261      return overlay;
262    },
263
264    /**
265     * Creates dropdown title element.
266     * @type {HTMLElement}
267     * @private
268     */
269    createTitle_: function() {
270      var image = this.ownerDocument.createElement('img');
271      image.alt = '';
272      image.classList.add('dropdown-image');
273      var text = this.ownerDocument.createElement('div');
274
275      var el = this.ownerDocument.createElement('div');
276      el.appendChild(image);
277      el.appendChild(text);
278
279      el.tabIndex = 0;
280      el.classList.add('dropdown-title');
281      el.iid = -1;
282      el.controller = this;
283      el.inFocus = false;
284      el.opening = false;
285
286      el.addEventListener('click', function f(e) {
287        this.controller.isShown = !this.controller.isShown;
288      });
289
290      el.addEventListener('focus', function(e) {
291        this.inFocus = true;
292      });
293
294      el.addEventListener('blur', function(e) {
295        this.inFocus = false;
296      });
297
298      el.addEventListener('keydown', function f(e) {
299        if (this.inFocus && !this.controller.isShown &&
300            (e.keyCode == DropDown.KEYCODE_ENTER ||
301             e.keyCode == DropDown.KEYCODE_SPACE ||
302             (!useKeyboardFlow && (e.keyCode == DropDown.KEYCODE_UP ||
303                                   e.keyCode == DropDown.KEYCODE_DOWN)))) {
304          this.opening = true;
305          this.controller.isShown = true;
306          e.stopPropagation();
307          e.preventDefault();
308        }
309      });
310      return el;
311    },
312
313    /**
314     * Handles keydown event from the keyboard.
315     * @private
316     * @param {!Event} e Keydown event.
317     */
318    keyDownHandler_: function(e) {
319      if (!this.isShown)
320        return;
321      var selected = this.container.selectedItem;
322      var handled = false;
323      switch (e.keyCode) {
324        case DropDown.KEYCODE_UP: {
325          do {
326            selected = selected.previousSibling;
327            if (!selected)
328              selected = this.container.lastElementChild;
329          } while (selected.iid < 0);
330          this.container.selectItem(selected, false);
331          handled = true;
332          break;
333        }
334        case DropDown.KEYCODE_DOWN: {
335          do {
336            selected = selected.nextSibling;
337            if (!selected)
338              selected = this.container.firstItem;
339          } while (selected.iid < 0);
340          this.container.selectItem(selected, false);
341          handled = true;
342          break;
343        }
344        case DropDown.KEYCODE_ESC: {
345          this.isShown = false;
346          handled = true;
347          break;
348        }
349        case DropDown.KEYCODE_TAB: {
350          this.isShown = false;
351          handled = true;
352          break;
353        }
354        case DropDown.KEYCODE_ENTER: {
355          if (!this.title_.opening) {
356            this.title_.focus();
357            this.isShown = false;
358            var item =
359                this.title_.controller.container.selectedItem.lastElementChild;
360            if (item.iid >= 0 && !item.classList.contains('disabled-item'))
361              chrome.send('networkItemChosen', [item.iid]);
362          }
363          handled = true;
364          break;
365        }
366      }
367      if (handled) {
368        e.stopPropagation();
369        e.preventDefault();
370      }
371      this.title_.opening = false;
372    }
373  };
374
375  /**
376   * Updates networks list with the new data.
377   * @param {!Object} data Networks list.
378   */
379  DropDown.updateNetworks = function(data) {
380    if (DropDown.activeElementId_)
381      $(DropDown.activeElementId_).setItems(data);
382  };
383
384  /**
385   * Updates network title, which is shown by the drop-down.
386   * @param {string} title Title to be displayed.
387   * @param {!Object} icon Icon to be displayed.
388   */
389  DropDown.updateNetworkTitle = function(title, icon) {
390    if (DropDown.activeElementId_)
391      $(DropDown.activeElementId_).setTitle(title, icon);
392  };
393
394  /**
395   * Activates network drop-down. Only one network drop-down
396   * can be active at the same time. So activating new drop-down deactivates
397   * the previous one.
398   * @param {string} elementId Id of network drop-down element.
399   * @param {boolean} isOobe Whether drop-down is used by an Oobe screen.
400   */
401  DropDown.show = function(elementId, isOobe) {
402    $(elementId).isShown = false;
403    if (DropDown.activeElementId_ != elementId) {
404      DropDown.activeElementId_ = elementId;
405      chrome.send('networkDropdownShow', [elementId, isOobe]);
406    }
407  };
408
409  /**
410   * Deactivates network drop-down. Deactivating inactive drop-down does
411   * nothing.
412   * @param {string} elementId Id of network drop-down element.
413   */
414  DropDown.hide = function(elementId) {
415    if (DropDown.activeElementId_ == elementId) {
416      DropDown.activeElementId_ = '';
417      chrome.send('networkDropdownHide');
418    }
419  };
420
421  /**
422   * Refreshes network drop-down. Should be called on language change.
423   */
424  DropDown.refresh = function() {
425    chrome.send('networkDropdownRefresh');
426  };
427
428  /**
429   * Sets the keyboard flow flag.
430   */
431  DropDown.enableKeyboardFlow = function() {
432    useKeyboardFlow = true;
433  };
434
435  return {
436    DropDown: DropDown
437  };
438});
439