Home | History | Annotate | Download | only in login
      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 
      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;
     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       var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping(
    178           this.container);
    179       if (maxHeight < this.container.offsetHeight)
    180         this.container.style.maxHeight = maxHeight + 'px';
    181     },
    182 
    183     /**
    184      * Id of the active drop-down element.
    185      * @private
    186      */
    187     activeElementId_: '',
    188 
    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       }
    213 
    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');
    222 
    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     },
    253 
    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     },
    268 
    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');
    279 
    280       var el = this.ownerDocument.createElement('div');
    281       el.appendChild(image);
    282       el.appendChild(text);
    283 
    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;
    290 
    291       el.addEventListener('click', function f(e) {
    292         this.controller.isShown = !this.controller.isShown;
    293       });
    294 
    295       el.addEventListener('focus', function(e) {
    296         this.inFocus = true;
    297       });
    298 
    299       el.addEventListener('blur', function(e) {
    300         this.inFocus = false;
    301       });
    302 
    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     },
    317 
    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   };
    379 
    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   };
    388 
    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   };
    398 
    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   };
    413 
    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   };
    425 
    426   /**
    427    * Refreshes network drop-down. Should be called on language change.
    428    */
    429   DropDown.refresh = function() {
    430     chrome.send('networkDropdownRefresh');
    431   };
    432 
    433   /**
    434    * Sets the keyboard flow flag.
    435    */
    436   DropDown.enableKeyboardFlow = function() {
    437     useKeyboardFlow = true;
    438   };
    439 
    440   return {
    441     DropDown: DropDown
    442   };
    443 });
    444