- add sources.
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / context_menu_handler.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 // require: event_target.js
6
7 cr.define('cr.ui', function() {
8   /** @const */ var EventTarget = cr.EventTarget;
9   /** @const */ var Menu = cr.ui.Menu;
10
11   /**
12    * Handles context menus.
13    * @constructor
14    * @extends {EventTarget}
15    */
16   function ContextMenuHandler() {
17     this.showingEvents_ = new EventTracker();
18   }
19
20   ContextMenuHandler.prototype = {
21     __proto__: EventTarget.prototype,
22
23     /**
24      * The menu that we are currently showing.
25      * @type {cr.ui.Menu}
26      */
27     menu_: null,
28     get menu() {
29       return this.menu_;
30     },
31
32     /**
33      * Shows a menu as a context menu.
34      * @param {!Event} e The event triggering the show (usually a contextmenu
35      *     event).
36      * @param {!cr.ui.Menu} menu The menu to show.
37      */
38     showMenu: function(e, menu) {
39       menu.updateCommands(e.currentTarget);
40       if (!menu.hasVisibleItems())
41         return;
42
43       this.menu_ = menu;
44       menu.classList.remove('hide-delayed');
45       menu.hidden = false;
46       menu.contextElement = e.currentTarget;
47
48       // When the menu is shown we steal a lot of events.
49       var doc = menu.ownerDocument;
50       var win = doc.defaultView;
51       this.showingEvents_.add(doc, 'keydown', this, true);
52       this.showingEvents_.add(doc, 'mousedown', this, true);
53       this.showingEvents_.add(doc, 'focus', this);
54       this.showingEvents_.add(win, 'popstate', this);
55       this.showingEvents_.add(win, 'resize', this);
56       this.showingEvents_.add(win, 'blur', this);
57       this.showingEvents_.add(menu, 'contextmenu', this);
58       this.showingEvents_.add(menu, 'activate', this);
59       this.positionMenu_(e, menu);
60
61       var ev = new Event('show');
62       ev.element = menu.contextElement;
63       ev.menu = menu;
64       this.dispatchEvent(ev);
65     },
66
67     /**
68      * Hide the currently shown menu.
69      * @param {HideType=} opt_hideType Type of hide.
70      *     default: cr.ui.HideType.INSTANT.
71      */
72     hideMenu: function(opt_hideType) {
73       var menu = this.menu;
74       if (!menu)
75         return;
76
77       if (opt_hideType == cr.ui.HideType.DELAYED)
78         menu.classList.add('hide-delayed');
79       else
80         menu.classList.remove('hide-delayed');
81       menu.hidden = true;
82       var originalContextElement = menu.contextElement;
83       menu.contextElement = null;
84       this.showingEvents_.removeAll();
85       menu.selectedIndex = -1;
86       this.menu_ = null;
87
88       // On windows we might hide the menu in a right mouse button up and if
89       // that is the case we wait some short period before we allow the menu
90       // to be shown again.
91       this.hideTimestamp_ = cr.isWindows ? Date.now() : 0;
92
93       var ev = new Event('hide');
94       ev.element = menu.contextElement;
95       ev.menu = menu;
96       this.dispatchEvent(ev);
97     },
98
99     /**
100      * Positions the menu
101      * @param {!Event} e The event object triggering the showing.
102      * @param {!cr.ui.Menu} menu The menu to position.
103      * @private
104      */
105     positionMenu_: function(e, menu) {
106       // TODO(arv): Handle scrolled documents when needed.
107
108       var element = e.currentTarget;
109       var x, y;
110       // When the user presses the context menu key (on the keyboard) we need
111       // to detect this.
112       if (this.keyIsDown_) {
113         var rect = element.getRectForContextMenu ?
114                        element.getRectForContextMenu() :
115                        element.getBoundingClientRect();
116         var offset = Math.min(rect.width, rect.height) / 2;
117         x = rect.left + offset;
118         y = rect.top + offset;
119       } else {
120         x = e.clientX;
121         y = e.clientY;
122       }
123
124       cr.ui.positionPopupAtPoint(x, y, menu);
125     },
126
127     /**
128      * Handles event callbacks.
129      * @param {!Event} e The event object.
130      */
131     handleEvent: function(e) {
132       // Keep track of keydown state so that we can use that to determine the
133       // reason for the contextmenu event.
134       switch (e.type) {
135         case 'keydown':
136           this.keyIsDown_ = !e.ctrlKey && !e.altKey &&
137               // context menu key or Shift-F10
138               (e.keyCode == 93 && !e.shiftKey ||
139                e.keyIdentifier == 'F10' && e.shiftKey);
140           break;
141
142         case 'keyup':
143           this.keyIsDown_ = false;
144           break;
145       }
146
147       // Context menu is handled even when we have no menu.
148       if (e.type != 'contextmenu' && !this.menu)
149         return;
150
151       switch (e.type) {
152         case 'mousedown':
153           if (!this.menu.contains(e.target))
154             this.hideMenu();
155           else
156             e.preventDefault();
157           break;
158         case 'keydown':
159           // keyIdentifier does not report 'Esc' correctly
160           if (e.keyCode == 27 /* Esc */) {
161             this.hideMenu();
162             e.stopPropagation();
163             e.preventDefault();
164
165           // If the menu is visible we let it handle all the keyboard events.
166           } else if (this.menu) {
167             this.menu.handleKeyDown(e);
168             e.preventDefault();
169             e.stopPropagation();
170           }
171           break;
172
173         case 'activate':
174           var hideDelayed = e.target instanceof cr.ui.MenuItem &&
175               e.target.checkable;
176           this.hideMenu(hideDelayed ? cr.ui.HideType.DELAYED :
177                                       cr.ui.HideType.INSTANT);
178           break;
179
180         case 'focus':
181           if (!this.menu.contains(e.target))
182             this.hideMenu();
183           break;
184
185         case 'blur':
186           this.hideMenu();
187           break;
188
189         case 'popstate':
190         case 'resize':
191           this.hideMenu();
192           break;
193
194         case 'contextmenu':
195           if ((!this.menu || !this.menu.contains(e.target)) &&
196               (!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50))
197             this.showMenu(e, e.currentTarget.contextMenu);
198           e.preventDefault();
199           // Don't allow elements further up in the DOM to show their menus.
200           e.stopPropagation();
201           break;
202       }
203     },
204
205     /**
206      * Adds a contextMenu property to an element or element class.
207      * @param {!Element|!Function} element The element or class to add the
208      *     contextMenu property to.
209      */
210     addContextMenuProperty: function(element) {
211       if (typeof element == 'function')
212         element = element.prototype;
213
214       element.__defineGetter__('contextMenu', function() {
215         return this.contextMenu_;
216       });
217       element.__defineSetter__('contextMenu', function(menu) {
218         var oldContextMenu = this.contextMenu;
219
220         if (typeof menu == 'string' && menu[0] == '#') {
221           menu = this.ownerDocument.getElementById(menu.slice(1));
222           cr.ui.decorate(menu, Menu);
223         }
224
225         if (menu === oldContextMenu)
226           return;
227
228         if (oldContextMenu && !menu) {
229           this.removeEventListener('contextmenu', contextMenuHandler);
230           this.removeEventListener('keydown', contextMenuHandler);
231           this.removeEventListener('keyup', contextMenuHandler);
232         }
233         if (menu && !oldContextMenu) {
234           this.addEventListener('contextmenu', contextMenuHandler);
235           this.addEventListener('keydown', contextMenuHandler);
236           this.addEventListener('keyup', contextMenuHandler);
237         }
238
239         this.contextMenu_ = menu;
240
241         if (menu && menu.id)
242           this.setAttribute('contextmenu', '#' + menu.id);
243
244         cr.dispatchPropertyChange(this, 'contextMenu', menu, oldContextMenu);
245       });
246
247       if (!element.getRectForContextMenu) {
248         /**
249          * @return {!ClientRect} The rect to use for positioning the context
250          *     menu when the context menu is not opened using a mouse position.
251          */
252         element.getRectForContextMenu = function() {
253           return this.getBoundingClientRect();
254         };
255       }
256     },
257
258     /**
259      * Sets the given contextMenu to the given element. A contextMenu property
260      * would be added if necessary.
261      * @param {!Element} element The element or class to set the contextMenu to.
262      * @param {!cr.ui.Menu} contextMenu The contextMenu property to be set.
263      */
264     setContextMenu: function(element, contextMenu) {
265       if (!element.contextMenu)
266         this.addContextMenuProperty(element);
267       element.contextMenu = contextMenu;
268     }
269   };
270
271   /**
272    * The singleton context menu handler.
273    * @type {!ContextMenuHandler}
274    */
275   var contextMenuHandler = new ContextMenuHandler;
276
277   // Export
278   return {
279     contextMenuHandler: contextMenuHandler,
280   };
281 });