Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(111)

Side by Side Diff: chrome/browser/resources/menu.js

Issue 8687012: Moved ChromeOS-specific resources from chrome/browser/resources to chrome/browser/resources/chrom... (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src/
Patch Set: Created 9 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « chrome/browser/resources/menu.html ('k') | chrome/browser/resources/mobile_app/manifest.json » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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.
4
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 = /([^&]*)&(.)(.*)/;
15
16 var localStrings = new LocalStrings();
17
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]);
28 }
29
30 /**
31 * MenuItem class.
32 */
33 var MenuItem = cr.ui.define('div');
34
35 MenuItem.prototype = {
36 __proto__ : HTMLDivElement.prototype,
37
38 /**
39 * Decorates the menu item element.
40 */
41 decorate: function() {
42 this.className = 'menu-item';
43 },
44
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 }
73
74 menu.appendChild(this);
75 if (!attrs.visible) {
76 this.classList.add('hidden');
77 }
78 },
79
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 },
93
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 },
106
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 },
115
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');
125
126 label.className = 'menu-label';
127 this.menu_.addLabelTo(this, attrs.label, label,
128 true /* enable mnemonic */);
129
130 if (attrs.font) {
131 label.style.font = attrs.font;
132 }
133 this.appendChild(label);
134
135
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 }
143
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 },
151
152 initPadding_: function(leftIconWidth) {
153 if (leftIconWidth <= 0) {
154 this.classList.add('no-icon');
155 return;
156 }
157 this.classList.add('left-icon');
158
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 },
179 };
180
181 /**
182 * Menu class.
183 */
184 var Menu = cr.ui.define('div');
185
186 Menu.prototype = {
187 __proto__: HTMLDivElement.prototype,
188
189 /**
190 * Configuration object.
191 * @type {Object}
192 */
193 config_ : null,
194
195 /**
196 * Currently selected menu item.
197 * @type {MenuItem}
198 */
199 current_ : null,
200
201 /**
202 * Timers for opening/closing submenu.
203 * @type {number}
204 */
205 openSubmenuTimer_ : 0,
206 closeSubmenuTimer_ : 0,
207
208 /**
209 * Auto scroll timer.
210 * @type {number}
211 */
212 scrollTimer_ : 0,
213
214 /**
215 * Pointer to a submenu currently shown, if any.
216 * @type {MenuItem}
217 */
218 submenuShown_ : null,
219
220 /**
221 * True if this menu is root.
222 * @type {boolean}
223 */
224 isRoot_ : false,
225
226 /**
227 * Scrollable Viewport.
228 * @type {HTMLElement}
229 */
230 viewpotr_ : null,
231
232 /**
233 * Total hight of scroll buttons. Used to adjust the height of
234 * viewport in order to show scroll bottons without scrollbar.
235 * @type {number}
236 */
237 buttonHeight_ : 0,
238
239 /**
240 * True to enable scroll button.
241 * @type {boolean}
242 */
243 scrollEnabled : false,
244
245 /**
246 * Decorates the menu element.
247 */
248 decorate: function() {
249 this.id = 'viewport';
250 },
251
252 /**
253 * Initialize the menu.
254 * @param {Object} config Configuration parameters in JSON format.
255 * See chromeos/views/native_menu_webui.cc for details.
256 */
257 init: function(config) {
258 // List of menu items
259 this.items_ = [];
260 // Map from mnemonic character to item to activate
261 this.mnemonics_ = {};
262
263 this.config_ = config;
264 this.addEventListener('mouseout', this.onMouseout_.bind(this));
265
266 document.addEventListener('keydown', this.onKeydown_.bind(this));
267 document.addEventListener('keypress', this.onKeypress_.bind(this));
268 document.addEventListener('mousewheel', this.onMouseWheel_.bind(this));
269 window.addEventListener('resize', this.onResize_.bind(this));
270
271 // Setup scroll events.
272 var up = document.getElementById('scroll-up');
273 var down = document.getElementById('scroll-down');
274 up.addEventListener('mouseout', this.stopScroll_.bind(this));
275 down.addEventListener('mouseout', this.stopScroll_.bind(this));
276 var menu = this;
277 up.addEventListener('mouseover',
278 function() {
279 menu.autoScroll_(-SCROLL_TICK_PX);
280 });
281 down.addEventListener('mouseover',
282 function() {
283 menu.autoScroll_(SCROLL_TICK_PX);
284 });
285
286 this.buttonHeight_ =
287 up.getBoundingClientRect().height +
288 down.getBoundingClientRect().height;
289 },
290
291 /**
292 * Adds a label to {@code targetDiv}. A label may contain
293 * mnemonic key, preceded by '&'.
294 * @param {MenuItem} item The menu item to be activated by mnemonic
295 * key.
296 * @param {string} label The label string to be added to
297 * {@code targetDiv}.
298 * @param {HTMLElement} div The div element the label is added to.
299 * @param {boolean} enableMnemonic True to enable mnemonic, or false
300 * to not to interprete mnemonic key. The function removes '&'
301 * from the label in both cases.
302 */
303 addLabelTo: function(item, label, targetDiv, enableMnemonic) {
304 var mnemonic = MNEMONIC_REGEXP.exec(label);
305 if (mnemonic && enableMnemonic) {
306 var c = mnemonic[2].toLowerCase();
307 this.mnemonics_[c] = item;
308 }
309 if (!mnemonic) {
310 targetDiv.textContent = label;
311 } else if (enableMnemonic) {
312 targetDiv.appendChild(document.createTextNode(mnemonic[1]));
313 targetDiv.appendChild(document.createElement('span'));
314 targetDiv.appendChild(document.createTextNode(mnemonic[3]));
315 targetDiv.childNodes[1].className = 'mnemonic';
316 targetDiv.childNodes[1].textContent = mnemonic[2];
317 } else {
318 targetDiv.textContent = mnemonic.splice(1, 3).join('');
319 }
320 },
321
322 /**
323 * Returns the index of the {@code item}.
324 */
325 getMenuItemIndexOf: function(item) {
326 return this.items_.indexOf(item);
327 },
328
329 /**
330 * A template method to create an item object. It can be a subclass
331 * of MenuItem, or any HTMLElement that implements {@code init},
332 * {@code activate} methods as well as {@code selected} attribute.
333 * @param {Object} attrs The menu item's properties passed from C++.
334 */
335 createMenuItem: function(attrs) {
336 return new MenuItem();
337 },
338
339 /**
340 * Update and display the new model.
341 */
342 updateModel: function(model) {
343 this.isRoot = model.isRoot;
344 this.current_ = null;
345 this.items_ = [];
346 this.mnemonics_ = {};
347 this.innerHTML = ''; // remove menu items
348
349 for (var i = 0; i < model.items.length; i++) {
350 var attrs = model.items[i];
351 var item = this.createMenuItem(attrs);
352 item.init(this, attrs, model);
353 this.items_.push(item);
354 }
355 this.onResize_();
356 },
357
358 /**
359 * Highlights the currently selected item, or
360 * select the 1st selectable item if none is selected.
361 */
362 showSelection: function() {
363 if (this.current_) {
364 this.current_.selected = true;
365 } else {
366 this.findNextEnabled_(1).selected = true;
367 }
368 },
369
370 /**
371 * Add event handlers for the item.
372 */
373 addHandlers: function(item, target) {
374 var menu = this;
375 target.addEventListener('mouseover', function(event) {
376 menu.onMouseover_(event, item);
377 });
378 if (item.attrs.enabled) {
379 target.addEventListener('mouseup', function(event) {
380 menu.onClick_(event, item);
381 });
382 } else {
383 target.classList.add('disabled');
384 }
385 },
386
387 /**
388 * Set the selected item. This controls timers to open/close submenus.
389 * 1) If the selected menu is submenu, and that submenu is not yet opeend,
390 * start timer to open. This will not cancel close timer, so
391 * if there is a submenu opened, it will be closed before new submenu is
392 * open.
393 * 2) If the selected menu is submenu, and that submenu is already opened,
394 * cancel both open/close timer.
395 * 3) If the selected menu is not submenu, cancel all timers and start
396 * timer to close submenu.
397 * This prevents from opening/closing menus while you're actively
398 * navigating menus. To open submenu, you need to wait a bit, or click
399 * submenu.
400 *
401 * @param {MenuItem} item The selected item.
402 */
403 set selectedItem(item) {
404 if (this.current_ != item) {
405 if (this.current_ != null)
406 this.current_.selected = false;
407 this.current_ = item;
408 this.makeSelectedItemVisible_();
409 }
410
411 var menu = this;
412 if (item.attrs.type == 'submenu') {
413 if (this.submenuShown_ != item) {
414 this.openSubmenuTimer_ =
415 setTimeout(
416 function() {
417 menu.openSubmenu(item);
418 },
419 SUBMENU_OPEN_DELAY_MS);
420 } else {
421 this.cancelSubmenuTimer_();
422 }
423 } else if (this.submenuShown_) {
424 this.cancelSubmenuTimer_();
425 this.closeSubmenuTimer_ =
426 setTimeout(
427 function() {
428 menu.closeSubmenu_(item);
429 },
430 SUBMENU_CLOSE_DELAY_MS);
431 }
432 },
433
434 /**
435 * Open submenu {@code item}. It does nothing if the submenu is
436 * already opened.
437 * @param {MenuItem} item The submenu item to open.
438 */
439 openSubmenu: function(item) {
440 this.cancelSubmenuTimer_();
441 if (this.submenuShown_ != item) {
442 this.submenuShown_ = item;
443 item.sendOpenSubmenuCommand();
444 }
445 },
446
447 /**
448 * Handle keyboard navigation and activation.
449 * @private
450 */
451 onKeydown_: function(event) {
452 switch (event.keyIdentifier) {
453 case 'Left':
454 this.moveToParent_();
455 break;
456 case 'Right':
457 this.moveToSubmenu_();
458 break;
459 case 'Up':
460 this.classList.add('mnemonic-enabled');
461 this.findNextEnabled_(-1).selected = true;
462 break;
463 case 'Down':
464 this.classList.add('mnemonic-enabled');
465 this.findNextEnabled_(1).selected = true;
466 break;
467 case 'U+0009': // tab
468 break;
469 case 'U+001B': // escape
470 chrome.send('close_all', []);
471 break;
472 case 'Enter':
473 case 'U+0020': // space
474 if (this.current_) {
475 this.current_.activate();
476 }
477 break;
478 }
479 },
480
481 /**
482 * Handle mnemonic keys.
483 * @private
484 */
485 onKeypress_: function(event) {
486 // Handles mnemonic.
487 var c = String.fromCharCode(event.keyCode);
488 var item = this.mnemonics_[c.toLowerCase()];
489 if (item) {
490 item.selected = true;
491 item.activate();
492 }
493 },
494
495 // Mouse Event handlers
496 onClick_: function(event, item) {
497 item.activate();
498 },
499
500 onMouseover_: function(event, item) {
501 this.cancelSubmenuTimer_();
502 // Ignore false mouseover event at (0,0) which is
503 // emitted when opening submenu.
504 if (item.attrs.enabled && event.clientX != 0 && event.clientY != 0) {
505 item.selected = true;
506 }
507 },
508
509 onMouseout_: function(event) {
510 if (this.current_) {
511 this.current_.selected = false;
512 }
513 },
514
515 onResize_: function() {
516 var up = document.getElementById('scroll-up');
517 var down = document.getElementById('scroll-down');
518 // this needs to be < 2 as empty page has height of 1.
519 if (window.innerHeight < 2) {
520 // menu window is not visible yet. just hide buttons.
521 up.classList.add('hidden');
522 down.classList.add('hidden');
523 return;
524 }
525 // Do not use screen width to determin if we need scroll buttons
526 // as the max renderer hight can be shorter than actual screen size.
527 // TODO(oshima): Fix this when we implement transparent renderer.
528 if (this.scrollHeight > window.innerHeight && this.scrollEnabled) {
529 this.style.height = (window.innerHeight - this.buttonHeight_) + 'px';
530 up.classList.remove('hidden');
531 down.classList.remove('hidden');
532 } else {
533 this.style.height = '';
534 up.classList.add('hidden');
535 down.classList.add('hidden');
536 }
537 },
538
539 onMouseWheel_: function(event) {
540 var delta = event.wheelDelta / 5;
541 this.scrollTop -= delta;
542 },
543
544 /**
545 * Closes the submenu.
546 * a submenu.
547 * @private
548 */
549 closeSubmenu_: function(item) {
550 this.submenuShown_ = null;
551 this.cancelSubmenuTimer_();
552 chrome.send('close_submenu', []);
553 },
554
555 /**
556 * Move the selection to parent menu if the current menu is
557 * a submenu.
558 * @private
559 */
560 moveToParent_: function() {
561 if (!this.isRoot) {
562 if (this.current_) {
563 this.current_.selected = false;
564 }
565 chrome.send('move_to_parent', []);
566 }
567 },
568
569 /**
570 * Move the selection to submenu if the currently selected
571 * menu is a submenu.
572 * @private
573 */
574 moveToSubmenu_: function () {
575 var current = this.current_;
576 if (current && current.attrs.type == 'submenu') {
577 this.openSubmenu(current);
578 chrome.send('move_to_submenu', []);
579 }
580 },
581
582 /**
583 * Find a next selectable item. If nothing is selected, the 1st
584 * selectable item will be chosen. Returns null if nothing is
585 * selectable.
586 * @param {number} incr Specifies the direction to search, 1 to
587 * downwards and -1 for upwards.
588 * @private
589 */
590 findNextEnabled_: function(incr) {
591 var len = this.items_.length;
592 var index;
593 if (this.current_) {
594 index = this.getMenuItemIndexOf(this.current_);
595 } else {
596 index = incr > 0 ? -1 : len;
597 }
598 for (var i = 0; i < len; i++) {
599 index = (index + incr + len) % len;
600 var item = this.items_[index];
601 if (item.attrs.enabled && item.attrs.type != 'separator' &&
602 !item.classList.contains('hidden'))
603 return item;
604 }
605 return null;
606 },
607
608 /**
609 * Cancels timers to open/close submenus.
610 * @private
611 */
612 cancelSubmenuTimer_: function() {
613 clearTimeout(this.openSubmenuTimer_);
614 this.openSubmenuTimer_ = 0;
615 clearTimeout(this.closeSubmenuTimer_);
616 this.closeSubmenuTimer_ = 0;
617 },
618
619 /**
620 * Starts auto scroll.
621 * @param {number} tick The number of pixels to scroll.
622 * @private
623 */
624 autoScroll_: function(tick) {
625 var previous = this.scrollTop;
626 this.scrollTop += tick;
627 var menu = this;
628 this.scrollTimer_ = setTimeout(
629 function() {
630 menu.autoScroll_(tick);
631 },
632 SCROLL_INTERVAL_MS);
633 },
634
635 /**
636 * Stops auto scroll.
637 * @private
638 */
639 stopScroll_: function () {
640 clearTimeout(this.scrollTimer_);
641 this.scrollTimer_ = 0;
642 },
643
644 /**
645 * Scrolls the viewport to make the selected item visible.
646 * @private
647 */
648 makeSelectedItemVisible_: function(){
649 this.current_.scrollIntoViewIfNeeded(false);
650 },
651 };
652
653 /**
654 * functions to be called from C++.
655 */
656 function init(config) {
657 document.getElementById('viewport').init(config);
658 }
659
660 function selectItem() {
661 document.getElementById('viewport').showSelection();
662 }
663
664 function updateModel(model) {
665 document.getElementById('viewport').updateModel(model);
666 }
667
668 function modelUpdated() {
669 chrome.send('model_updated', []);
670 }
671
672 function enableScroll(enabled) {
673 document.getElementById('viewport').scrollEnabled = enabled;
674 }
OLDNEW
« no previous file with comments | « chrome/browser/resources/menu.html ('k') | chrome/browser/resources/mobile_app/manifest.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698