Home | History | Annotate | Download | only in ui
      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 cr.define('cr.ui', function() {
      6 
      7   /** @const */ var MenuItem = cr.ui.MenuItem;
      8 
      9   /**
     10    * Creates a new menu element. Menu dispatches all commands on the element it
     11    * was shown for.
     12    *
     13    * @param {Object=} opt_propertyBag Optional properties.
     14    * @constructor
     15    * @extends {HTMLMenuElement}
     16    */
     17   var Menu = cr.ui.define('menu');
     18 
     19   Menu.prototype = {
     20     __proto__: HTMLMenuElement.prototype,
     21 
     22     selectedIndex_: -1,
     23 
     24     /**
     25      * Element for which menu is being shown.
     26      */
     27     contextElement: null,
     28 
     29     /**
     30      * Selector for children which are menu items.
     31      */
     32     menuItemSelector: '*',
     33 
     34     /**
     35      * Initializes the menu element.
     36      */
     37     decorate: function() {
     38       this.addEventListener('mouseover', this.handleMouseOver_);
     39       this.addEventListener('mouseout', this.handleMouseOut_);
     40 
     41       this.classList.add('decorated');
     42       this.setAttribute('role', 'menu');
     43       this.hidden = true;  // Hide the menu by default.
     44 
     45       // Decorate the children as menu items.
     46       var menuItems = this.menuItems;
     47       for (var i = 0, menuItem; menuItem = menuItems[i]; i++) {
     48         cr.ui.decorate(menuItem, MenuItem);
     49       }
     50     },
     51 
     52     /**
     53      * Adds menu item at the end of the list.
     54      * @param {Object} item Menu item properties.
     55      * @return {cr.ui.MenuItem} The created menu item.
     56      */
     57     addMenuItem: function(item) {
     58       var menuItem = this.ownerDocument.createElement('menuitem');
     59       this.appendChild(menuItem);
     60 
     61       cr.ui.decorate(menuItem, MenuItem);
     62 
     63       if (item.label)
     64         menuItem.label = item.label;
     65 
     66       if (item.iconUrl)
     67         menuItem.iconUrl = item.iconUrl;
     68 
     69       return menuItem;
     70     },
     71 
     72     /**
     73      * Adds separator at the end of the list.
     74      */
     75     addSeparator: function() {
     76       var separator = this.ownerDocument.createElement('hr');
     77       cr.ui.decorate(separator, MenuItem);
     78       this.appendChild(separator);
     79     },
     80 
     81     /**
     82      * Clears menu.
     83      */
     84     clear: function() {
     85       this.textContent = '';
     86     },
     87 
     88     /**
     89      * Walks up the ancestors of |el| until a menu item belonging to this menu
     90      * is found.
     91      * @param {Element} el The element to start searching from.
     92      * @return {cr.ui.MenuItem} The found menu item or null.
     93      * @private
     94      */
     95     findMenuItem_: function(el) {
     96       while (el && el.parentNode != this) {
     97         el = el.parentNode;
     98       }
     99       return el;
    100     },
    101 
    102     /**
    103      * Handles mouseover events and selects the hovered item.
    104      * @param {Event} e The mouseover event.
    105      * @private
    106      */
    107     handleMouseOver_: function(e) {
    108       var overItem = this.findMenuItem_(e.target);
    109       this.selectedItem = overItem;
    110     },
    111 
    112     /**
    113      * Handles mouseout events and deselects any selected item.
    114      * @param {Event} e The mouseout event.
    115      * @private
    116      */
    117     handleMouseOut_: function(e) {
    118       this.selectedItem = null;
    119     },
    120 
    121     get menuItems() {
    122       return this.querySelectorAll(this.menuItemSelector);
    123     },
    124 
    125     /**
    126      * The selected menu item or null if none.
    127      * @type {cr.ui.MenuItem}
    128      */
    129     get selectedItem() {
    130       return this.menuItems[this.selectedIndex];
    131     },
    132     set selectedItem(item) {
    133       var index = Array.prototype.indexOf.call(this.menuItems, item);
    134       this.selectedIndex = index;
    135     },
    136 
    137     /**
    138      * Focuses the selected item. If selectedIndex is invalid, set it to 0
    139      * first.
    140      */
    141     focusSelectedItem: function() {
    142       if (this.selectedIndex < 0 ||
    143           this.selectedIndex > this.menuItems.length) {
    144         this.selectedIndex = 0;
    145       }
    146 
    147       if (this.selectedItem) {
    148         this.selectedItem.focus();
    149         this.setAttribute('aria-activedescendant', this.selectedItem.id);
    150       }
    151     },
    152 
    153     /**
    154      * Menu length
    155      */
    156     get length() {
    157       return this.menuItems.length;
    158     },
    159 
    160     /**
    161      * Returns if the menu has any visible item.
    162      * @return {boolean} True if the menu has visible item. Otherwise, false.
    163      */
    164     hasVisibleItems: function() {
    165       var menuItems = this.menuItems;  // Cache.
    166       for (var i = 0, menuItem; menuItem = menuItems[i]; i++) {
    167         if (!menuItem.hidden)
    168           return true;
    169       }
    170       return false;
    171     },
    172 
    173     /**
    174      * This is the function that handles keyboard navigation. This is usually
    175      * called by the element responsible for managing the menu.
    176      * @param {Event} e The keydown event object.
    177      * @return {boolean} Whether the event was handled be the menu.
    178      */
    179     handleKeyDown: function(e) {
    180       var item = this.selectedItem;
    181 
    182       var self = this;
    183       function selectNextAvailable(m) {
    184         var menuItems = self.menuItems;
    185         var len = menuItems.length;
    186         if (!len) {
    187           // Edge case when there are no items.
    188           return;
    189         }
    190         var i = self.selectedIndex;
    191         if (i == -1 && m == -1) {
    192           // Edge case when needed to go the last item first.
    193           i = 0;
    194         }
    195 
    196         // "i" may be negative(-1), so modulus operation and cycle below
    197         // wouldn't work as assumed. This trick makes startPosition positive
    198         // without altering it's modulo.
    199         var startPosition = (i + len) % len;
    200 
    201         while (true) {
    202           i = (i + m + len) % len;
    203 
    204           // Check not to enter into infinite loop if all items are hidden or
    205           // disabled.
    206           if (i == startPosition)
    207             break;
    208 
    209           item = menuItems[i];
    210           if (item && !item.isSeparator() && !item.hidden && !item.disabled)
    211             break;
    212         }
    213         if (item && !item.disabled)
    214           self.selectedIndex = i;
    215       }
    216 
    217       switch (e.keyIdentifier) {
    218         case 'Down':
    219           selectNextAvailable(1);
    220           this.focusSelectedItem();
    221           return true;
    222         case 'Up':
    223           selectNextAvailable(-1);
    224           this.focusSelectedItem();
    225           return true;
    226         case 'Enter':
    227         case 'U+0020': // Space
    228           if (item) {
    229             var activationEvent = cr.doc.createEvent('Event');
    230             activationEvent.initEvent('activate', true, true);
    231             activationEvent.originalEvent = e;
    232             if (item.dispatchEvent(activationEvent)) {
    233               if (item.command)
    234                 item.command.execute();
    235             }
    236           }
    237           return true;
    238       }
    239 
    240       return false;
    241     },
    242 
    243     /**
    244      * Updates menu items command according to context.
    245      * @param {Node=} node Node for which to actuate commands state.
    246      */
    247     updateCommands: function(node) {
    248       var menuItems = this.menuItems;
    249 
    250       for (var i = 0, menuItem; menuItem = menuItems[i]; i++) {
    251         if (!menuItem.isSeparator())
    252           menuItem.updateCommand(node);
    253       }
    254     }
    255   };
    256 
    257   function selectedIndexChanged(selectedIndex, oldSelectedIndex) {
    258     var oldSelectedItem = this.menuItems[oldSelectedIndex];
    259     if (oldSelectedItem) {
    260       oldSelectedItem.selected = false;
    261       oldSelectedItem.blur();
    262     }
    263     var item = this.selectedItem;
    264     if (item)
    265       item.selected = true;
    266   }
    267 
    268   /**
    269    * The selected menu item.
    270    * @type {number}
    271    */
    272   cr.defineProperty(Menu, 'selectedIndex', cr.PropertyKind.JS,
    273       selectedIndexChanged);
    274 
    275   // Export
    276   return {
    277     Menu: Menu
    278   };
    279 });
    280