- add sources.
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / menu_button.js
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   /** @const */
7   var Menu = cr.ui.Menu;
8
9   /** @const */
10   var positionPopupAroundElement = cr.ui.positionPopupAroundElement;
11
12    /**
13     * Enum for type of hide. Delayed is used when called by clicking on a
14     * checkable menu item.
15     * @enum {number}
16     */
17    var HideType = {
18      INSTANT: 0,
19      DELAYED: 1
20    };
21
22   /**
23    * Creates a new menu button element.
24    * @param {Object=} opt_propertyBag Optional properties.
25    * @constructor
26    * @extends {HTMLButtonElement}
27    */
28   var MenuButton = cr.ui.define('button');
29
30   MenuButton.prototype = {
31     __proto__: HTMLButtonElement.prototype,
32
33     /**
34      * Initializes the menu button.
35      */
36     decorate: function() {
37       this.addEventListener('mousedown', this);
38       this.addEventListener('keydown', this);
39
40       // Adding the 'custom-appearance' class prevents widgets.css from changing
41       // the appearance of this element.
42       this.classList.add('custom-appearance');
43       this.classList.add('menu-button');  // For styles in menu_button.css.
44
45       var menu;
46       if ((menu = this.getAttribute('menu')))
47         this.menu = menu;
48
49       // An event tracker for events we only connect to while the menu is
50       // displayed.
51       this.showingEvents_ = new EventTracker();
52
53       this.anchorType = cr.ui.AnchorType.BELOW;
54       this.invertLeftRight = false;
55     },
56
57     /**
58      * The menu associated with the menu button.
59      * @type {cr.ui.Menu}
60      */
61     get menu() {
62       return this.menu_;
63     },
64     set menu(menu) {
65       if (typeof menu == 'string' && menu[0] == '#') {
66         menu = this.ownerDocument.getElementById(menu.slice(1));
67         cr.ui.decorate(menu, Menu);
68       }
69
70       this.menu_ = menu;
71       if (menu) {
72         if (menu.id)
73           this.setAttribute('menu', '#' + menu.id);
74       }
75     },
76
77     /**
78      * Handles event callbacks.
79      * @param {Event} e The event object.
80      */
81     handleEvent: function(e) {
82       if (!this.menu)
83         return;
84
85       switch (e.type) {
86         case 'mousedown':
87           if (e.currentTarget == this.ownerDocument) {
88             if (!this.contains(e.target) && !this.menu.contains(e.target))
89               this.hideMenu();
90             else
91               e.preventDefault();
92           } else {
93             if (this.isMenuShown()) {
94               this.hideMenu();
95             } else if (e.button == 0) {  // Only show the menu when using left
96                                          // mouse button.
97               this.showMenu(false);
98
99               // Prevent the button from stealing focus on mousedown.
100               e.preventDefault();
101             }
102           }
103
104           // Hide the focus ring on mouse click.
105           this.classList.add('using-mouse');
106           break;
107         case 'keydown':
108           this.handleKeyDown(e);
109           // If the menu is visible we let it handle all the keyboard events.
110           if (this.isMenuShown() && e.currentTarget == this.ownerDocument) {
111             if (this.menu.handleKeyDown(e)) {
112               e.preventDefault();
113               e.stopPropagation();
114             }
115           }
116
117           // Show the focus ring on keypress.
118           this.classList.remove('using-mouse');
119           break;
120         case 'focus':
121           if (!this.contains(e.target) && !this.menu.contains(e.target)) {
122             this.hideMenu();
123             // Show the focus ring on focus - if it's come from a mouse event,
124             // the focus ring will be hidden in the mousedown event handler,
125             // executed after this.
126             this.classList.remove('using-mouse');
127           }
128           break;
129         case 'activate':
130           var hideDelayed = e.target instanceof cr.ui.MenuItem &&
131               e.target.checkable;
132           this.hideMenu(hideDelayed ? HideType.DELAYED : HideType.INSTANT);
133           break;
134         case 'scroll':
135           if (!(e.target == this.menu || this.menu.contains(e.target)))
136             this.hideMenu();
137           break;
138         case 'popstate':
139         case 'resize':
140           this.hideMenu();
141           break;
142       }
143     },
144
145     /**
146      * Shows the menu.
147      * @param {boolean} shouldSetFocus Whether to set focus on the
148      *     selected menu item.
149      */
150     showMenu: function(shouldSetFocus) {
151       this.hideMenu();
152
153       this.menu.updateCommands(this);
154
155       var event = document.createEvent('UIEvents');
156       event.initUIEvent('menushow', true, true, window, null);
157
158       if (!this.dispatchEvent(event))
159         return;
160
161       this.menu.hidden = false;
162
163       this.setAttribute('menu-shown', '');
164
165       // When the menu is shown we steal all keyboard events.
166       var doc = this.ownerDocument;
167       var win = doc.defaultView;
168       this.showingEvents_.add(doc, 'keydown', this, true);
169       this.showingEvents_.add(doc, 'mousedown', this, true);
170       this.showingEvents_.add(doc, 'focus', this, true);
171       this.showingEvents_.add(doc, 'scroll', this, true);
172       this.showingEvents_.add(win, 'popstate', this);
173       this.showingEvents_.add(win, 'resize', this);
174       this.showingEvents_.add(this.menu, 'activate', this);
175       this.positionMenu_();
176
177       if (shouldSetFocus)
178         this.menu.focusSelectedItem();
179     },
180
181     /**
182      * Hides the menu. If your menu can go out of scope, make sure to call this
183      * first.
184      * @param {HideType=} opt_hideType Type of hide.
185      *     default: HideType.INSTANT.
186      */
187     hideMenu: function(opt_hideType) {
188       if (!this.isMenuShown())
189         return;
190
191       this.removeAttribute('menu-shown');
192       if (opt_hideType == HideType.DELAYED)
193         this.menu.classList.add('hide-delayed');
194       else
195         this.menu.classList.remove('hide-delayed');
196       this.menu.hidden = true;
197
198       this.showingEvents_.removeAll();
199       this.focus();
200     },
201
202     /**
203      * Whether the menu is shown.
204      */
205     isMenuShown: function() {
206       return this.hasAttribute('menu-shown');
207     },
208
209     /**
210      * Positions the menu below the menu button. At this point we do not use any
211      * advanced positioning logic to ensure the menu fits in the viewport.
212      * @private
213      */
214     positionMenu_: function() {
215       positionPopupAroundElement(this, this.menu, this.anchorType,
216                                  this.invertLeftRight);
217     },
218
219     /**
220      * Handles the keydown event for the menu button.
221      */
222     handleKeyDown: function(e) {
223       switch (e.keyIdentifier) {
224         case 'Down':
225         case 'Up':
226         case 'Enter':
227         case 'U+0020': // Space
228           if (!this.isMenuShown())
229             this.showMenu(true);
230           e.preventDefault();
231           break;
232         case 'Esc':
233         case 'U+001B': // Maybe this is remote desktop playing a prank?
234         case 'U+0009': // Tab
235           this.hideMenu();
236           break;
237       }
238     }
239   };
240
241   /**
242    * Helper for styling a menu button with a drop-down arrow indicator.
243    * Creates a new 2D canvas context and draws a downward-facing arrow into it.
244    * @param {string} canvasName The name of the canvas. The canvas can be
245    *     addressed from CSS using -webkit-canvas(<canvasName>).
246    * @param {number} width The width of the canvas and the arrow.
247    * @param {number} height The height of the canvas and the arrow.
248    * @param {string} colorSpec The CSS color to use when drawing the arrow.
249    */
250   function createDropDownArrowCanvas(canvasName, width, height, colorSpec) {
251     var ctx = document.getCSSCanvasContext('2d', canvasName, width, height);
252     ctx.fillStyle = ctx.strokeStyle = colorSpec;
253     ctx.beginPath();
254     ctx.moveTo(0, 0);
255     ctx.lineTo(width, 0);
256     ctx.lineTo(height, height);
257     ctx.closePath();
258     ctx.fill();
259     ctx.stroke();
260   };
261
262   /** @const */ var ARROW_WIDTH = 6;
263   /** @const */ var ARROW_HEIGHT = 3;
264
265   /**
266    * Create the images used to style drop-down-style MenuButtons.
267    * This should be called before creating any MenuButtons that will have the
268    * CSS class 'drop-down'. If no colors are specified, defaults will be used.
269    * @param {=string} normalColor CSS color for the default button state.
270    * @param {=string} hoverColor CSS color for the hover button state.
271    * @param {=string} activeColor CSS color for the active button state.
272    */
273   MenuButton.createDropDownArrows = function(
274       normalColor, hoverColor, activeColor) {
275     normalColor = normalColor || 'rgb(192, 195, 198)';
276     hoverColor = hoverColor || 'rgb(48, 57, 66)';
277     activeColor = activeColor || 'white';
278
279     createDropDownArrowCanvas(
280         'drop-down-arrow', ARROW_WIDTH, ARROW_HEIGHT, normalColor);
281     createDropDownArrowCanvas(
282         'drop-down-arrow-hover', ARROW_WIDTH, ARROW_HEIGHT, hoverColor);
283     createDropDownArrowCanvas(
284         'drop-down-arrow-active', ARROW_WIDTH, ARROW_HEIGHT, activeColor);
285   };
286
287   // Export
288   return {
289     MenuButton: MenuButton,
290     HideType: HideType
291   };
292 });