Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / login / network_dropdown.js
blob846183d13ff20055c9068ec1f349fd2e2fe1f94c
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.
5 /**
6  * @fileoverview Network drop-down implementation.
7  */
9 cr.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;
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');
23   DropDownContainer.prototype = {
24     __proto__: HTMLDivElement.prototype,
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     },
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     },
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   };
79   /**
80    * Creates a new DropDown div.
81    * @constructor
82    * @extends {HTMLDivElement}
83    */
84   var DropDown = cr.ui.define('div');
86   DropDown.ITEM_DIVIDER_ID = -2;
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;
95   DropDown.prototype = {
96     __proto__: HTMLDivElement.prototype,
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);
106       this.addEventListener('keydown', this.keyDownHandler_);
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     },
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     },
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       }
135       // Flag for keyboard flow util to forward the up/down keys.
136       this.title_.classList.toggle('needs-up-down-keys', show);
137     },
139     /**
140      * Returns container of the menu items.
141      */
142     get container() {
143       return this.lastElementChild;
144     },
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     },
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);
177       var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping(
178           this.container);
179       if (maxHeight < this.container.offsetHeight)
180         this.container.style.maxHeight = maxHeight + 'px';
181     },
183     /**
184      * Id of the active drop-down element.
185      * @private
186      */
187     activeElementId_: '',
189     /**
190      * Creates dropdown item element and adds into container.
191      * @param {HTMLElement} container Container where item is added.
192      * @param {!Object} item Item to be added.
193      * @private
194      */
195     createItem_: function(container, item) {
196       var itemContentElement;
197       var className = 'dropdown-item';
198       if (item.id == DropDown.ITEM_DIVIDER_ID) {
199         className = 'dropdown-divider';
200         itemContentElement = this.ownerDocument.createElement('hr');
201       } else {
202         var span = this.ownerDocument.createElement('span');
203         itemContentElement = span;
204         span.textContent = item.label;
205         if ('bold' in item && item.bold)
206           span.classList.add('bold');
207         var image = this.ownerDocument.createElement('img');
208         image.alt = '';
209         image.classList.add('dropdown-image');
210         if (item.icon)
211           image.src = item.icon;
212       }
214       var itemElement = this.ownerDocument.createElement('div');
215       itemElement.classList.add(className);
216       itemElement.appendChild(itemContentElement);
217       itemElement.iid = item.id;
218       itemElement.controller = this;
219       var enabled = 'enabled' in item && item.enabled;
220       if (!enabled)
221         itemElement.classList.add('disabled-item');
223       if (item.id > 0) {
224         var wrapperDiv = this.ownerDocument.createElement('div');
225         wrapperDiv.setAttribute('role', 'menuitem');
226         wrapperDiv.id = this.id + item.id;
227         if (!enabled)
228           wrapperDiv.setAttribute('aria-disabled', 'true');
229         wrapperDiv.classList.add('dropdown-item-container');
230         var imageDiv = this.ownerDocument.createElement('div');
231         imageDiv.appendChild(image);
232         wrapperDiv.appendChild(imageDiv);
233         wrapperDiv.appendChild(itemElement);
234         wrapperDiv.addEventListener('click', function f(e) {
235           var item = this.lastElementChild;
236           if (item.iid < -1 || item.classList.contains('disabled-item'))
237             return;
238           item.controller.isShown = false;
239           if (item.iid >= 0)
240             chrome.send('networkItemChosen', [item.iid]);
241           this.parentNode.parentNode.title_.focus();
242         });
243         wrapperDiv.addEventListener('mouseover', function f(e) {
244           this.parentNode.selectItem(this, true);
245         });
246         itemElement = wrapperDiv;
247       }
248       container.appendChild(itemElement);
249       if (!container.firstItem && item.id >= 0) {
250         container.firstItem = itemElement;
251       }
252     },
254     /**
255      * Creates dropdown overlay element, which catches outside clicks.
256      * @type {HTMLElement}
257      * @private
258      */
259     createOverlay_: function() {
260       var overlay = this.ownerDocument.createElement('div');
261       overlay.classList.add('dropdown-overlay');
262       overlay.addEventListener('click', function() {
263         this.parentNode.title_.focus();
264         this.parentNode.isShown = false;
265       });
266       return overlay;
267     },
269     /**
270      * Creates dropdown title element.
271      * @type {HTMLElement}
272      * @private
273      */
274     createTitle_: function() {
275       var image = this.ownerDocument.createElement('img');
276       image.alt = '';
277       image.classList.add('dropdown-image');
278       var text = this.ownerDocument.createElement('div');
280       var el = this.ownerDocument.createElement('div');
281       el.appendChild(image);
282       el.appendChild(text);
284       el.tabIndex = 0;
285       el.classList.add('dropdown-title');
286       el.iid = -1;
287       el.controller = this;
288       el.inFocus = false;
289       el.opening = false;
291       el.addEventListener('click', function f(e) {
292         this.controller.isShown = !this.controller.isShown;
293       });
295       el.addEventListener('focus', function(e) {
296         this.inFocus = true;
297       });
299       el.addEventListener('blur', function(e) {
300         this.inFocus = false;
301       });
303       el.addEventListener('keydown', function f(e) {
304         if (this.inFocus && !this.controller.isShown &&
305             (e.keyCode == DropDown.KEYCODE_ENTER ||
306              e.keyCode == DropDown.KEYCODE_SPACE ||
307              (!useKeyboardFlow && (e.keyCode == DropDown.KEYCODE_UP ||
308                                    e.keyCode == DropDown.KEYCODE_DOWN)))) {
309           this.opening = true;
310           this.controller.isShown = true;
311           e.stopPropagation();
312           e.preventDefault();
313         }
314       });
315       return el;
316     },
318     /**
319      * Handles keydown event from the keyboard.
320      * @private
321      * @param {!Event} e Keydown event.
322      */
323     keyDownHandler_: function(e) {
324       if (!this.isShown)
325         return;
326       var selected = this.container.selectedItem;
327       var handled = false;
328       switch (e.keyCode) {
329         case DropDown.KEYCODE_UP: {
330           do {
331             selected = selected.previousSibling;
332             if (!selected)
333               selected = this.container.lastElementChild;
334           } while (selected.iid < 0);
335           this.container.selectItem(selected, false);
336           handled = true;
337           break;
338         }
339         case DropDown.KEYCODE_DOWN: {
340           do {
341             selected = selected.nextSibling;
342             if (!selected)
343               selected = this.container.firstItem;
344           } while (selected.iid < 0);
345           this.container.selectItem(selected, false);
346           handled = true;
347           break;
348         }
349         case DropDown.KEYCODE_ESC: {
350           this.isShown = false;
351           handled = true;
352           break;
353         }
354         case DropDown.KEYCODE_TAB: {
355           this.isShown = false;
356           handled = true;
357           break;
358         }
359         case DropDown.KEYCODE_ENTER: {
360           if (!this.title_.opening) {
361             this.title_.focus();
362             this.isShown = false;
363             var item =
364                 this.title_.controller.container.selectedItem.lastElementChild;
365             if (item.iid >= 0 && !item.classList.contains('disabled-item'))
366               chrome.send('networkItemChosen', [item.iid]);
367           }
368           handled = true;
369           break;
370         }
371       }
372       if (handled) {
373         e.stopPropagation();
374         e.preventDefault();
375       }
376       this.title_.opening = false;
377     }
378   };
380   /**
381    * Updates networks list with the new data.
382    * @param {!Object} data Networks list.
383    */
384   DropDown.updateNetworks = function(data) {
385     if (DropDown.activeElementId_)
386       $(DropDown.activeElementId_).setItems(data);
387   };
389   /**
390    * Updates network title, which is shown by the drop-down.
391    * @param {string} title Title to be displayed.
392    * @param {!Object} icon Icon to be displayed.
393    */
394   DropDown.updateNetworkTitle = function(title, icon) {
395     if (DropDown.activeElementId_)
396       $(DropDown.activeElementId_).setTitle(title, icon);
397   };
399   /**
400    * Activates network drop-down. Only one network drop-down
401    * can be active at the same time. So activating new drop-down deactivates
402    * the previous one.
403    * @param {string} elementId Id of network drop-down element.
404    * @param {boolean} isOobe Whether drop-down is used by an Oobe screen.
405    */
406   DropDown.show = function(elementId, isOobe) {
407     $(elementId).isShown = false;
408     if (DropDown.activeElementId_ != elementId) {
409       DropDown.activeElementId_ = elementId;
410       chrome.send('networkDropdownShow', [elementId, isOobe]);
411     }
412   };
414   /**
415    * Deactivates network drop-down. Deactivating inactive drop-down does
416    * nothing.
417    * @param {string} elementId Id of network drop-down element.
418    */
419   DropDown.hide = function(elementId) {
420     if (DropDown.activeElementId_ == elementId) {
421       DropDown.activeElementId_ = '';
422       chrome.send('networkDropdownHide');
423     }
424   };
426   /**
427    * Refreshes network drop-down. Should be called on language change.
428    */
429   DropDown.refresh = function() {
430     chrome.send('networkDropdownRefresh');
431   };
433   /**
434    * Sets the keyboard flow flag.
435    */
436   DropDown.enableKeyboardFlow = function() {
437     useKeyboardFlow = true;
438   };
440   return {
441     DropDown: DropDown
442   };