Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / menu.js
blob598e58bfdc3b34bd3ede4eb9548dab0c49b8e853
1 // Copyright (c) 2011 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 // How long to wait to open submenu when mouse hovers.
6 var SUBMENU_OPEN_DELAY_MS = 200;
7 // How long to wait to close submenu when mouse left.
8 var SUBMENU_CLOSE_DELAY_MS = 500;
9 // Scroll repeat interval.
10 var SCROLL_INTERVAL_MS = 20;
11 // Scrolling amount in pixel.
12 var SCROLL_TICK_PX = 4;
13 // Regular expression to match/find mnemonic key.
14 var MNEMONIC_REGEXP = /([^&]*)&(.)(.*)/;
16 var localStrings = new LocalStrings();
18 /**
19  * Sends 'activate' WebUI message.
20  * @param {number} index The index of menu item to activate in menu model.
21  * @param {string} mode The activation mode, one of 'close_and_activate', or
22  *    'activate_no_close'.
23  * TODO(oshima): change these string to enum numbers once it becomes possible
24  * to pass number to C++.
25  */
26 function sendActivate(index, mode) {
27   chrome.send('activate', [String(index), mode]);
30 /**
31  * MenuItem class.
32  */
33 var MenuItem = cr.ui.define('div');
35 MenuItem.prototype = {
36   __proto__: HTMLDivElement.prototype,
38   /**
39    * Decorates the menu item element.
40    */
41   decorate: function() {
42     this.className = 'menu-item';
43   },
45   /**
46    * Initialize the MenuItem.
47    * @param {Menu} menu A {@code Menu} object to which this menu item
48    *    will be added to.
49    * @param {Object} attrs JSON object that represents this menu items
50    *    properties.  This is created from menu model in C code.  See
51    *    chromeos/views/native_menu_webui.cc.
52    * @param {Object} model The model object.
53    */
54   init: function(menu, attrs, model) {
55     // The left icon's width. 0 if no icon.
56     var leftIconWidth = model.maxIconWidth;
57     this.menu_ = menu;
58     this.attrs = attrs;
59     var attrs = this.attrs;
60     if (attrs.type == 'separator') {
61       this.className = 'separator';
62     } else if (attrs.type == 'command' ||
63                attrs.type == 'submenu' ||
64                attrs.type == 'check' ||
65                attrs.type == 'radio') {
66       this.initMenuItem_();
67       this.initPadding_(leftIconWidth);
68     } else {
69       // This should not happend.
70       this.classList.add('disabled');
71       this.textContent = 'unknown';
72     }
74     menu.appendChild(this);
75     if (!attrs.visible) {
76       this.classList.add('hidden');
77     }
78   },
80   /**
81    * Changes the selection state of the menu item.
82    * @param {boolean} selected True to set the selection, or false
83    *     otherwise.
84    */
85   set selected(selected) {
86     if (selected) {
87       this.classList.add('selected');
88       this.menu_.selectedItem = this;
89     } else {
90       this.classList.remove('selected');
91     }
92   },
94   /**
95    * Activate the menu item.
96    */
97   activate: function() {
98     if (this.attrs.type == 'submenu') {
99       this.menu_.openSubmenu(this);
100     } else if (this.attrs.type != 'separator' &&
101                this.className.indexOf('selected') >= 0) {
102       sendActivate(this.menu_.getMenuItemIndexOf(this),
103                    'close_and_activate');
104     }
105   },
107   /**
108    * Sends open_submenu WebUI message.
109    */
110   sendOpenSubmenuCommand: function() {
111     chrome.send('open_submenu',
112                 [String(this.menu_.getMenuItemIndexOf(this)),
113                  String(this.getBoundingClientRect().top)]);
114   },
116   /**
117    * Internal method to initiailze the MenuItem.
118    * @private
119    */
120   initMenuItem_: function() {
121     var attrs = this.attrs;
122     this.className = 'menu-item ' + attrs.type;
123     this.menu_.addHandlers(this, this);
124     var label = document.createElement('div');
126     label.className = 'menu-label';
127     this.menu_.addLabelTo(this, attrs.label, label,
128                           true /* enable mnemonic */);
130     if (attrs.font) {
131       label.style.font = attrs.font;
132     }
133     this.appendChild(label);
136     if (attrs.accel) {
137       var accel = document.createElement('div');
138       accel.className = 'accelerator';
139       accel.textContent = attrs.accel;
140       accel.style.font = attrs.font;
141       this.appendChild(accel);
142     }
144     if (attrs.type == 'submenu') {
145       // This overrides left-icon's position, but it's OK as submenu
146       // shoudln't have left-icon.
147       this.classList.add('right-icon');
148       this.style.backgroundImage = 'url(' + this.menu_.config_.arrowUrl + ')';
149     }
150   },
152   initPadding_: function(leftIconWidth) {
153     if (leftIconWidth <= 0) {
154       this.classList.add('no-icon');
155       return;
156     }
157     this.classList.add('left-icon');
159     var url;
160     var attrs = this.attrs;
161     if (attrs.type == 'radio') {
162       url = attrs.checked ?
163           this.menu_.config_.radioOnUrl :
164           this.menu_.config_.radioOffUrl;
165     } else if (attrs.icon) {
166       url = attrs.icon;
167     } else if (attrs.type == 'check' && attrs.checked) {
168       url = this.menu_.config_.checkUrl;
169     }
170     if (url) {
171       this.style.backgroundImage = 'url(' + url + ')';
172     }
173     // TODO(oshima): figure out how to update left padding in rule.
174     // 4 is the padding on left side of icon.
175     var padding =
176         4 + leftIconWidth + this.menu_.config_.icon_to_label_padding;
177     this.style.WebkitPaddingStart = padding + 'px';
178   },
182  * Menu class.
183  */
184 var Menu = cr.ui.define('div');
186 Menu.prototype = {
187   __proto__: HTMLDivElement.prototype,
189   /**
190    * Configuration object.
191    * @type {Object}
192    */
193   config_: null,
195   /**
196    * Currently selected menu item.
197    * @type {MenuItem}
198    */
199   current_: null,
201   /**
202    * Timers for opening/closing submenu.
203    * @type {number}
204    */
205   openSubmenuTimer_: 0,
206   closeSubmenuTimer_: 0,
208   /**
209    * Auto scroll timer.
210    * @type {number}
211    */
212   scrollTimer_: 0,
214   /**
215    * Pointer to a submenu currently shown, if any.
216    * @type {MenuItem}
217    */
218   submenuShown_: null,
220   /**
221    * True if this menu is root.
222    * @type {boolean}
223    */
224   isRoot_: false,
226   /**
227    * Total hight of scroll buttons. Used to adjust the height of
228    * viewport in order to show scroll bottons without scrollbar.
229    * @type {number}
230    */
231   buttonHeight_: 0,
233   /**
234    * True to enable scroll button.
235    * @type {boolean}
236    */
237   scrollEnabled: false,
239   /**
240    * Decorates the menu element.
241    */
242   decorate: function() {
243     this.id = 'viewport';
244   },
246   /**
247    * Initialize the menu.
248    * @param {Object} config Configuration parameters in JSON format.
249    *  See chromeos/views/native_menu_webui.cc for details.
250    */
251   init: function(config) {
252     // List of menu items
253     this.items_ = [];
254     // Map from mnemonic character to item to activate
255     this.mnemonics_ = {};
257     this.config_ = config;
258     this.addEventListener('mouseout', this.onMouseout_.bind(this));
260     document.addEventListener('keydown', this.onKeydown_.bind(this));
261     document.addEventListener('keypress', this.onKeypress_.bind(this));
262     document.addEventListener('mousewheel', this.onMouseWheel_.bind(this));
263     window.addEventListener('resize', this.onResize_.bind(this));
265     // Setup scroll events.
266     var up = $('scroll-up');
267     var down = $('scroll-down');
268     up.addEventListener('mouseout', this.stopScroll_.bind(this));
269     down.addEventListener('mouseout', this.stopScroll_.bind(this));
270     var menu = this;
271     up.addEventListener('mouseover',
272                         function() {
273                           menu.autoScroll_(-SCROLL_TICK_PX);
274                         });
275     down.addEventListener('mouseover',
276                           function() {
277                             menu.autoScroll_(SCROLL_TICK_PX);
278                           });
280     this.buttonHeight_ =
281         up.getBoundingClientRect().height +
282         down.getBoundingClientRect().height;
283   },
285   /**
286    * Adds a label to {@code targetDiv}. A label may contain
287    * mnemonic key, preceded by '&'.
288    * @param {MenuItem} item The menu item to be activated by mnemonic
289    *    key.
290    * @param {string} label The label string to be added to
291    *    {@code targetDiv}.
292    * @param {HTMLElement} div The div element the label is added to.
293    * @param {boolean} enableMnemonic True to enable mnemonic, or false
294    *    to not to interprete mnemonic key. The function removes '&'
295    *    from the label in both cases.
296    */
297   addLabelTo: function(item, label, targetDiv, enableMnemonic) {
298     var mnemonic = MNEMONIC_REGEXP.exec(label);
299     if (mnemonic && enableMnemonic) {
300       var c = mnemonic[2].toLowerCase();
301       this.mnemonics_[c] = item;
302     }
303     if (!mnemonic) {
304       targetDiv.textContent = label;
305     } else if (enableMnemonic) {
306       targetDiv.appendChild(document.createTextNode(mnemonic[1]));
307       targetDiv.appendChild(document.createElement('span'));
308       targetDiv.appendChild(document.createTextNode(mnemonic[3]));
309       targetDiv.childNodes[1].className = 'mnemonic';
310       targetDiv.childNodes[1].textContent = mnemonic[2];
311     } else {
312       targetDiv.textContent = mnemonic.splice(1, 3).join('');
313     }
314   },
316   /**
317    * @return {number} The index of the {@code item}.
318    */
319   getMenuItemIndexOf: function(item) {
320     return this.items_.indexOf(item);
321   },
323   /**
324    * A template method to create an item object. It can be a subclass
325    * of MenuItem, or any HTMLElement that implements {@code init},
326    * {@code activate} methods as well as {@code selected} attribute.
327    * @param {Object} attrs The menu item's properties passed from C++.
328    * @return {MenuItem} The created menu item.
329    */
330   createMenuItem: function(attrs) {
331     return new MenuItem();
332   },
334   /**
335    * Update and display the new model.
336    */
337   updateModel: function(model) {
338     this.isRoot = model.isRoot;
339     this.current_ = null;
340     this.items_ = [];
341     this.mnemonics_ = {};
342     this.innerHTML = '';  // remove menu items
344     for (var i = 0; i < model.items.length; i++) {
345       var attrs = model.items[i];
346       var item = this.createMenuItem(attrs);
347       item.init(this, attrs, model);
348       this.items_.push(item);
349     }
350     this.onResize_();
351   },
353   /**
354    * Highlights the currently selected item, or
355    * select the 1st selectable item if none is selected.
356    */
357   showSelection: function() {
358     if (this.current_) {
359       this.current_.selected = true;
360     } else {
361       this.findNextEnabled_(1).selected = true;
362     }
363   },
365   /**
366    * Add event handlers for the item.
367    */
368   addHandlers: function(item, target) {
369     var menu = this;
370     target.addEventListener('mouseover', function(event) {
371       menu.onMouseover_(event, item);
372     });
373     if (item.attrs.enabled) {
374       target.addEventListener('mouseup', function(event) {
375         menu.onClick_(event, item);
376       });
377     } else {
378       target.classList.add('disabled');
379     }
380   },
382   /**
383    * Set the selected item. This controls timers to open/close submenus.
384    * 1) If the selected menu is submenu, and that submenu is not yet opeend,
385    *    start timer to open. This will not cancel close timer, so
386    *    if there is a submenu opened, it will be closed before new submenu is
387    *    open.
388    * 2) If the selected menu is submenu, and that submenu is already opened,
389    *    cancel both open/close timer.
390    * 3) If the selected menu is not submenu, cancel all timers and start
391    *    timer to close submenu.
392    * This prevents from opening/closing menus while you're actively
393    * navigating menus. To open submenu, you need to wait a bit, or click
394    * submenu.
395    *
396    * @param {MenuItem} item The selected item.
397    */
398   set selectedItem(item) {
399     if (this.current_ != item) {
400       if (this.current_ != null)
401         this.current_.selected = false;
402       this.current_ = item;
403       this.makeSelectedItemVisible_();
404     }
406     var menu = this;
407     if (item.attrs.type == 'submenu') {
408       if (this.submenuShown_ != item) {
409         this.openSubmenuTimer_ =
410             setTimeout(
411                 function() {
412                   menu.openSubmenu(item);
413                 },
414                 SUBMENU_OPEN_DELAY_MS);
415       } else {
416         this.cancelSubmenuTimer_();
417       }
418     } else if (this.submenuShown_) {
419       this.cancelSubmenuTimer_();
420       this.closeSubmenuTimer_ =
421           setTimeout(
422               function() {
423                 menu.closeSubmenu_(item);
424               },
425               SUBMENU_CLOSE_DELAY_MS);
426     }
427   },
429   /**
430    * Open submenu {@code item}. It does nothing if the submenu is
431    * already opened.
432    * @param {MenuItem} item The submenu item to open.
433    */
434   openSubmenu: function(item) {
435     this.cancelSubmenuTimer_();
436     if (this.submenuShown_ != item) {
437       this.submenuShown_ = item;
438       item.sendOpenSubmenuCommand();
439     }
440   },
442   /**
443    * Handle keyboard navigation and activation.
444    * @private
445    */
446   onKeydown_: function(event) {
447     switch (event.keyIdentifier) {
448       case 'Left':
449         this.moveToParent_();
450         break;
451       case 'Right':
452         this.moveToSubmenu_();
453         break;
454       case 'Up':
455         this.classList.add('mnemonic-enabled');
456         this.findNextEnabled_(-1).selected = true;
457       break;
458       case 'Down':
459         this.classList.add('mnemonic-enabled');
460         this.findNextEnabled_(1).selected = true;
461         break;
462       case 'U+0009':  // tab
463          break;
464       case 'U+001B':  // escape
465         chrome.send('close_all');
466         break;
467       case 'Enter':
468       case 'U+0020':  // space
469         if (this.current_) {
470           this.current_.activate();
471         }
472         break;
473     }
474   },
476   /**
477    * Handle mnemonic keys.
478    * @private
479    */
480   onKeypress_: function(event) {
481     // Handles mnemonic.
482     var c = String.fromCharCode(event.keyCode);
483     var item = this.mnemonics_[c.toLowerCase()];
484     if (item) {
485       item.selected = true;
486       item.activate();
487     }
488   },
490   // Mouse Event handlers
491   onClick_: function(event, item) {
492     item.activate();
493   },
495   onMouseover_: function(event, item) {
496     this.cancelSubmenuTimer_();
497     // Ignore false mouseover event at (0,0) which is
498     // emitted when opening submenu.
499     if (item.attrs.enabled && event.clientX != 0 && event.clientY != 0) {
500       item.selected = true;
501     }
502   },
504   onMouseout_: function(event) {
505     if (this.current_) {
506       this.current_.selected = false;
507     }
508   },
510   onResize_: function() {
511     var up = $('scroll-up');
512     var down = $('scroll-down');
513     // this needs to be < 2 as empty page has height of 1.
514     if (window.innerHeight < 2) {
515       // menu window is not visible yet. just hide buttons.
516       up.classList.add('hidden');
517       down.classList.add('hidden');
518       return;
519     }
520     // Do not use screen width to determin if we need scroll buttons
521     // as the max renderer hight can be shorter than actual screen size.
522     // TODO(oshima): Fix this when we implement transparent renderer.
523     if (this.scrollHeight > window.innerHeight && this.scrollEnabled) {
524       this.style.height = (window.innerHeight - this.buttonHeight_) + 'px';
525       up.classList.remove('hidden');
526       down.classList.remove('hidden');
527     } else {
528       this.style.height = '';
529       up.classList.add('hidden');
530       down.classList.add('hidden');
531     }
532   },
534   onMouseWheel_: function(event) {
535     var delta = event.wheelDelta / 5;
536     this.scrollTop -= delta;
537   },
539   /**
540    * Closes the submenu.
541    * a submenu.
542    * @private
543    */
544   closeSubmenu_: function(item) {
545     this.submenuShown_ = null;
546     this.cancelSubmenuTimer_();
547     chrome.send('close_submenu');
548   },
550   /**
551    * Move the selection to parent menu if the current menu is
552    * a submenu.
553    * @private
554    */
555   moveToParent_: function() {
556     if (!this.isRoot) {
557       if (this.current_) {
558         this.current_.selected = false;
559       }
560       chrome.send('move_to_parent');
561     }
562   },
564   /**
565    * Move the selection to submenu if the currently selected
566    * menu is a submenu.
567    * @private
568    */
569   moveToSubmenu_: function() {
570     var current = this.current_;
571     if (current && current.attrs.type == 'submenu') {
572       this.openSubmenu(current);
573       chrome.send('move_to_submenu');
574     }
575   },
577   /**
578    * Finds the next selectable item. If nothing is selected, the first
579    * selectable item will be chosen. Returns null if nothing is selectable.
580    * @param {number} incr Specifies the direction to search, 1 to
581    *     downwards and -1 for upwards.
582    * @private
583    * @return {MenuItem} The next selectable item.
584    */
585   findNextEnabled_: function(incr) {
586     var len = this.items_.length;
587     var index;
588     if (this.current_) {
589       index = this.getMenuItemIndexOf(this.current_);
590     } else {
591       index = incr > 0 ? -1 : len;
592     }
593     for (var i = 0; i < len; i++) {
594       index = (index + incr + len) % len;
595       var item = this.items_[index];
596       if (item.attrs.enabled && item.attrs.type != 'separator' &&
597           !item.classList.contains('hidden'))
598         return item;
599     }
600     return null;
601   },
603   /**
604    * Cancels timers to open/close submenus.
605    * @private
606    */
607   cancelSubmenuTimer_: function() {
608     clearTimeout(this.openSubmenuTimer_);
609     this.openSubmenuTimer_ = 0;
610     clearTimeout(this.closeSubmenuTimer_);
611     this.closeSubmenuTimer_ = 0;
612   },
614   /**
615    * Starts auto scroll.
616    * @param {number} tick The number of pixels to scroll.
617    * @private
618    */
619   autoScroll_: function(tick) {
620     var previous = this.scrollTop;
621     this.scrollTop += tick;
622     var menu = this;
623     this.scrollTimer_ = setTimeout(
624         function() {
625           menu.autoScroll_(tick);
626         },
627         SCROLL_INTERVAL_MS);
628   },
630   /**
631    * Stops auto scroll.
632    * @private
633    */
634   stopScroll_: function() {
635     clearTimeout(this.scrollTimer_);
636     this.scrollTimer_ = 0;
637   },
639   /**
640    * Scrolls the viewport to make the selected item visible.
641    * @private
642    */
643   makeSelectedItemVisible_: function() {
644     this.current_.scrollIntoViewIfNeeded(false);
645   },
649  * functions to be called from C++.
650  * @param {Object} config The viewport configuration.
651  */
652 function init(config) {
653   $('viewport').init(config);
656 function selectItem() {
657   $('viewport').showSelection();
660 function updateModel(model) {
661   $('viewport').updateModel(model);
664 function modelUpdated() {
665   chrome.send('model_updated');
668 function enableScroll(enabled) {
669   $('viewport').scrollEnabled = enabled;