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;