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();
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++.
26 function sendActivate(index
, mode
) {
27 chrome
.send('activate', [String(index
), mode
]);
33 var MenuItem
= cr
.ui
.define('div');
35 MenuItem
.prototype = {
36 __proto__
: HTMLDivElement
.prototype,
39 * Decorates the menu item element.
41 decorate: function() {
42 this.className
= 'menu-item';
46 * Initialize the MenuItem.
47 * @param {Menu} menu A {@code Menu} object to which this menu item
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.
54 init: function(menu
, attrs
, model
) {
55 // The left icon's width. 0 if no icon.
56 var leftIconWidth
= model
.maxIconWidth
;
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') {
67 this.initPadding_(leftIconWidth
);
69 // This should not happend.
70 this.classList
.add('disabled');
71 this.textContent
= 'unknown';
74 menu
.appendChild(this);
76 this.classList
.add('hidden');
81 * Changes the selection state of the menu item.
82 * @param {boolean} selected True to set the selection, or false
85 set selected(selected
) {
87 this.classList
.add('selected');
88 this.menu_
.selectedItem
= this;
90 this.classList
.remove('selected');
95 * Activate the menu item.
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');
108 * Sends open_submenu WebUI message.
110 sendOpenSubmenuCommand: function() {
111 chrome
.send('open_submenu',
112 [String(this.menu_
.getMenuItemIndexOf(this)),
113 String(this.getBoundingClientRect().top
)]);
117 * Internal method to initiailze the MenuItem.
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 */);
131 label
.style
.font
= attrs
.font
;
133 this.appendChild(label
);
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
);
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
+ ')';
152 initPadding_: function(leftIconWidth
) {
153 if (leftIconWidth
<= 0) {
154 this.classList
.add('no-icon');
157 this.classList
.add('left-icon');
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
) {
167 } else if (attrs
.type
== 'check' && attrs
.checked
) {
168 url
= this.menu_
.config_
.checkUrl
;
171 this.style
.backgroundImage
= 'url(' + url
+ ')';
173 // TODO(oshima): figure out how to update left padding in rule.
174 // 4 is the padding on left side of icon.
176 4 + leftIconWidth
+ this.menu_
.config_
.icon_to_label_padding
;
177 this.style
.WebkitPaddingStart
= padding
+ 'px';
184 var Menu
= cr
.ui
.define('div');
187 __proto__
: HTMLDivElement
.prototype,
190 * Configuration object.
196 * Currently selected menu item.
202 * Timers for opening/closing submenu.
205 openSubmenuTimer_
: 0,
206 closeSubmenuTimer_
: 0,
215 * Pointer to a submenu currently shown, if any.
221 * True if this menu is root.
227 * Total hight of scroll buttons. Used to adjust the height of
228 * viewport in order to show scroll bottons without scrollbar.
234 * True to enable scroll button.
237 scrollEnabled
: false,
240 * Decorates the menu element.
242 decorate: function() {
243 this.id
= 'viewport';
247 * Initialize the menu.
248 * @param {Object} config Configuration parameters in JSON format.
249 * See chromeos/views/native_menu_webui.cc for details.
251 init: function(config
) {
252 // List of menu 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));
271 up
.addEventListener('mouseover',
273 menu
.autoScroll_(-SCROLL_TICK_PX
);
275 down
.addEventListener('mouseover',
277 menu
.autoScroll_(SCROLL_TICK_PX
);
281 up
.getBoundingClientRect().height
+
282 down
.getBoundingClientRect().height
;
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
290 * @param {string} label The label string to be added to
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.
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
;
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];
312 targetDiv
.textContent
= mnemonic
.splice(1, 3).join('');
317 * @return {number} The index of the {@code item}.
319 getMenuItemIndexOf: function(item
) {
320 return this.items_
.indexOf(item
);
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.
330 createMenuItem: function(attrs
) {
331 return new MenuItem();
335 * Update and display the new model.
337 updateModel: function(model
) {
338 this.isRoot
= model
.isRoot
;
339 this.current_
= null;
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
);
354 * Highlights the currently selected item, or
355 * select the 1st selectable item if none is selected.
357 showSelection: function() {
359 this.current_
.selected
= true;
361 this.findNextEnabled_(1).selected
= true;
366 * Add event handlers for the item.
368 addHandlers: function(item
, target
) {
370 target
.addEventListener('mouseover', function(event
) {
371 menu
.onMouseover_(event
, item
);
373 if (item
.attrs
.enabled
) {
374 target
.addEventListener('mouseup', function(event
) {
375 menu
.onClick_(event
, item
);
378 target
.classList
.add('disabled');
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
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
396 * @param {MenuItem} item The selected item.
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_();
407 if (item
.attrs
.type
== 'submenu') {
408 if (this.submenuShown_
!= item
) {
409 this.openSubmenuTimer_
=
412 menu
.openSubmenu(item
);
414 SUBMENU_OPEN_DELAY_MS
);
416 this.cancelSubmenuTimer_();
418 } else if (this.submenuShown_
) {
419 this.cancelSubmenuTimer_();
420 this.closeSubmenuTimer_
=
423 menu
.closeSubmenu_(item
);
425 SUBMENU_CLOSE_DELAY_MS
);
430 * Open submenu {@code item}. It does nothing if the submenu is
432 * @param {MenuItem} item The submenu item to open.
434 openSubmenu: function(item
) {
435 this.cancelSubmenuTimer_();
436 if (this.submenuShown_
!= item
) {
437 this.submenuShown_
= item
;
438 item
.sendOpenSubmenuCommand();
443 * Handle keyboard navigation and activation.
446 onKeydown_: function(event
) {
447 switch (event
.keyIdentifier
) {
449 this.moveToParent_();
452 this.moveToSubmenu_();
455 this.classList
.add('mnemonic-enabled');
456 this.findNextEnabled_(-1).selected
= true;
459 this.classList
.add('mnemonic-enabled');
460 this.findNextEnabled_(1).selected
= true;
462 case 'U+0009': // tab
464 case 'U+001B': // escape
465 chrome
.send('close_all');
468 case 'U+0020': // space
470 this.current_
.activate();
477 * Handle mnemonic keys.
480 onKeypress_: function(event
) {
482 var c
= String
.fromCharCode(event
.keyCode
);
483 var item
= this.mnemonics_
[c
.toLowerCase()];
485 item
.selected
= true;
490 // Mouse Event handlers
491 onClick_: function(event
, item
) {
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;
504 onMouseout_: function(event
) {
506 this.current_
.selected
= false;
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');
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');
528 this.style
.height
= '';
529 up
.classList
.add('hidden');
530 down
.classList
.add('hidden');
534 onMouseWheel_: function(event
) {
535 var delta
= event
.wheelDelta
/ 5;
536 this.scrollTop
-= delta
;
540 * Closes the submenu.
544 closeSubmenu_: function(item
) {
545 this.submenuShown_
= null;
546 this.cancelSubmenuTimer_();
547 chrome
.send('close_submenu');
551 * Move the selection to parent menu if the current menu is
555 moveToParent_: function() {
558 this.current_
.selected
= false;
560 chrome
.send('move_to_parent');
565 * Move the selection to submenu if the currently selected
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');
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.
583 * @return {MenuItem} The next selectable item.
585 findNextEnabled_: function(incr
) {
586 var len
= this.items_
.length
;
589 index
= this.getMenuItemIndexOf(this.current_
);
591 index
= incr
> 0 ? -1 : len
;
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'))
604 * Cancels timers to open/close submenus.
607 cancelSubmenuTimer_: function() {
608 clearTimeout(this.openSubmenuTimer_
);
609 this.openSubmenuTimer_
= 0;
610 clearTimeout(this.closeSubmenuTimer_
);
611 this.closeSubmenuTimer_
= 0;
615 * Starts auto scroll.
616 * @param {number} tick The number of pixels to scroll.
619 autoScroll_: function(tick
) {
620 var previous
= this.scrollTop
;
621 this.scrollTop
+= tick
;
623 this.scrollTimer_
= setTimeout(
625 menu
.autoScroll_(tick
);
634 stopScroll_: function() {
635 clearTimeout(this.scrollTimer_
);
636 this.scrollTimer_
= 0;
640 * Scrolls the viewport to make the selected item visible.
643 makeSelectedItemVisible_: function() {
644 this.current_
.scrollIntoViewIfNeeded(false);
649 * functions to be called from C++.
650 * @param {Object} config The viewport configuration.
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
;