Don't show supervised user as "already on this device" while they're being imported.
[chromium-blink-merge.git] / remoting / webapp / app_remoting / js / context_menu_dom.js
blob48791b7f3b20852e62cc33339a36114fbeb61c08
1 // Copyright 2014 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 /**
6  * @fileoverview
7  * Provide an alternative location for the application's context menu items
8  * on platforms that don't provide it.
9  *
10  * To mimic the behaviour of an OS-provided context menu, the menu is dismissed
11  * in three situations:
12  *
13  * 1. When the window loses focus (i.e, the user has clicked on another window
14  *    or on the desktop).
15  * 2. When the user selects an option from the menu.
16  * 3. When the user clicks on another part of the same window; this is achieved
17  *    using an invisible screen element behind the menu, but in front of all
18  *    other DOM.
19  *
20  * TODO(jamiewalch): Fold this functionality into remoting.MenuButton.
21  */
22 'use strict';
24 /** @suppress {duplicate} */
25 var remoting = remoting || {};
27 /**
28  * @constructor
29  * @implements {remoting.WindowShape.ClientUI}
30  * @implements {remoting.ContextMenuAdapter}
31  * @param {HTMLElement} root The root of the context menu DOM.
32  */
33 remoting.ContextMenuDom = function(root) {
34   /** @private {HTMLElement} */
35   this.root_ = root;
36   /** @private {HTMLElement} */
37   this.stub_ = /** @type {HTMLElement} */
38       (this.root_.querySelector('.context-menu-stub'));
39   /** @private {HTMLElement} */
40   this.icon_ = /** @type {HTMLElement} */
41       (this.root_.querySelector('.context-menu-icon'));
42   /** @private {HTMLElement} */
43   this.screen_ = /** @type {HTMLElement} */
44       (this.root_.querySelector('.context-menu-screen'));
45   /** @private {HTMLElement} */
46   this.menu_ = /** @type {HTMLElement} */ (this.root_.querySelector('ul'));
47   /** @private {number} */
48   this.bottom_ = 8;
49   /** @private {base.EventSourceImpl} */
50   this.eventSource_ = new base.EventSourceImpl();
51   /** @private {string} */
52   this.eventName_ = '_click';
53   /**
54    * Since the same element is used to lock the icon open and to drag it, we
55    * must keep track of drag events so that the corresponding click event can
56    * be ignored.
57    *
58    * @private {boolean}
59    */
60   this.stubDragged_ = false;
62   /**
63    * @private
64    */
65   this.dragAndDrop_ = new remoting.DragAndDrop(
66       this.stub_, this.onDragUpdate_.bind(this));
68   this.eventSource_.defineEvents([this.eventName_]);
69   this.root_.addEventListener(
70       'transitionend', this.onTransitionEnd_.bind(this), false);
71   this.stub_.addEventListener('click', this.onStubClick_.bind(this), false);
72   this.icon_.addEventListener('click', this.onIconClick_.bind(this), false);
73   this.screen_.addEventListener('click', this.onIconClick_.bind(this), false);
75   this.root_.hidden = false;
76   this.root_.style.bottom = this.bottom_ + 'px';
77   remoting.windowShape.registerClientUI(this);
80 remoting.ContextMenuDom.prototype.dispose = function() {
81   remoting.windowShape.unregisterClientUI(this);
84 /**
85  * @param {Array<{left: number, top: number, width: number, height: number}>}
86  *     rects List of rectangles.
87  */
88 remoting.ContextMenuDom.prototype.addToRegion = function(rects) {
89   var rect = /** @type {ClientRect} */ (this.root_.getBoundingClientRect());
90   // Clip the menu position to the main window in case the screen size has
91   // changed or a recent drag event tried to move it out of bounds.
92   if (rect.top < 0) {
93     this.bottom_ += rect.top;
94     this.root_.style.bottom = this.bottom_ + 'px';
95     rect = this.root_.getBoundingClientRect();
96   }
98   rects.push(rect);
99   if (this.root_.classList.contains('menu-opened')) {
100     var menuRect =
101         /** @type {ClientRect} */ (this.menu_.getBoundingClientRect());
102     rects.push(menuRect);
103   }
107  * @param {string} id An identifier for the menu entry.
108  * @param {string} title The text to display in the menu.
109  * @param {boolean} isCheckable True if the state of this menu entry should
110  *     have a check-box and manage its toggle state automatically. Note that
111  *     checkable menu entries always start off unchecked.
112  * @param {string=} opt_parentId The id of the parent menu item for submenus.
113  */
114 remoting.ContextMenuDom.prototype.create = function(
115     id, title, isCheckable, opt_parentId) {
116   var menuEntry = /** @type {HTMLElement} */ (document.createElement('li'));
117   menuEntry.innerText = title;
118   menuEntry.setAttribute('data-id', id);
119   if (isCheckable) {
120     menuEntry.setAttribute('data-checkable', true);
121   }
122   menuEntry.addEventListener('click', this.onClick_.bind(this), false);
123   /** @type {Node} */
124   var insertBefore = null;
125   if (opt_parentId) {
126     var parent = /** @type {HTMLElement} */
127         (this.menu_.querySelector('[data-id="' + opt_parentId + '"]'));
128     base.debug.assert(parent != null);
129     base.debug.assert(!parent.classList.contains('menu-group-item'));
130     parent.classList.add('menu-group-header');
131     menuEntry.classList.add('menu-group-item');
132     insertBefore = this.getInsertionPointForParent(
133         /** @type {string} */(opt_parentId));
134   }
135   this.menu_.insertBefore(menuEntry, insertBefore);
139  * @param {string} id
140  * @param {string} title
141  */
142 remoting.ContextMenuDom.prototype.updateTitle = function(id, title) {
143   var node = this.menu_.querySelector('[data-id="' + id + '"]');
144   if (node) {
145     node.innerText = title;
146   }
150  * @param {string} id
151  * @param {boolean} checked
152  */
153 remoting.ContextMenuDom.prototype.updateCheckState = function(id, checked) {
154   var node = /** @type {HTMLElement} */
155       (this.menu_.querySelector('[data-id="' + id + '"]'));
156   if (node) {
157     if (checked) {
158       node.classList.add('selected');
159     } else {
160       node.classList.remove('selected');
161     }
162   }
166  * @param {string} id
167  */
168 remoting.ContextMenuDom.prototype.remove = function(id) {
169   var node = this.menu_.querySelector('[data-id="' + id + '"]');
170   if (node) {
171     this.menu_.removeChild(node);
172   }
176  * @param {function(OnClickData=):void} listener
177  */
178 remoting.ContextMenuDom.prototype.addListener = function(listener) {
179   this.eventSource_.addEventListener(this.eventName_, listener);
183  * @param {Event} event
184  * @private
185  */
186 remoting.ContextMenuDom.prototype.onClick_ = function(event) {
187   var element = /** @type {HTMLElement} */ (event.target);
188   if (element.getAttribute('data-checkable')) {
189     element.classList.toggle('selected')
190   }
191   var clickData = {
192     menuItemId: element.getAttribute('data-id'),
193     checked: element.classList.contains('selected')
194   };
195   this.eventSource_.raiseEvent(this.eventName_, clickData);
196   this.onIconClick_();
200  * Get the insertion point for the specified sub-menu. This is the menu item
201  * immediately following the last child of that menu group, or null if there
202  * are no menu items after that group.
204  * @param {string} parentId
205  * @return {Node?}
206  */
207 remoting.ContextMenuDom.prototype.getInsertionPointForParent = function(
208     parentId) {
209   var parentNode = this.menu_.querySelector('[data-id="' + parentId + '"]');
210   base.debug.assert(parentNode != null);
211   var childNode = /** @type {HTMLElement} */ (parentNode.nextSibling);
212   while (childNode != null && childNode.classList.contains('menu-group-item')) {
213     childNode = childNode.nextSibling;
214   }
215   return childNode;
219  * Called when the CSS show/hide transition completes. Since this changes the
220  * visible dimensions of the context menu, the visible region of the window
221  * needs to be recomputed.
223  * @private
224  */
225 remoting.ContextMenuDom.prototype.onTransitionEnd_ = function() {
226   remoting.windowShape.updateClientWindowShape();
230  * Toggle the visibility of the context menu icon.
232  * @private
233  */
234 remoting.ContextMenuDom.prototype.onStubClick_ = function() {
235   if (this.stubDragged_) {
236     this.stubDragged_ = false;
237     return;
238   }
239   this.root_.classList.toggle('opened');
243  * Toggle the visibility of the context menu.
245  * @private
246  */
247 remoting.ContextMenuDom.prototype.onIconClick_ = function() {
248   this.showMenu_(!this.menu_.classList.contains('opened'));
252  * Explicitly show or hide the context menu.
254  * @param {boolean} show True to show the menu; false to hide it.
255  * @private
256  */
257 remoting.ContextMenuDom.prototype.showMenu_ = function(show) {
258   if (show) {
259     // Ensure that the menu doesn't extend off the top or bottom of the
260     // screen by aligning it to the top or bottom of the icon, depending
261     // on the latter's vertical position.
262     var menuRect =
263         /** @type {ClientRect} */ (this.menu_.getBoundingClientRect());
264     if (menuRect.bottom > window.innerHeight) {
265       this.menu_.classList.add('menu-align-bottom');
266     } else {
267       this.menu_.classList.remove('menu-align-bottom');
268     }
270     /** @type {remoting.ContextMenuDom} */
271     var that = this;
272     var onBlur = function() {
273       that.showMenu_(false);
274       window.removeEventListener('blur', onBlur, false);
275     };
276     window.addEventListener('blur', onBlur, false);
278     // Show the menu and prevent the icon from auto-hiding on mouse-out.
279     this.menu_.classList.add('opened');
280     this.root_.classList.add('menu-opened');
282   } else {  // if (!show)
283     this.menu_.classList.remove('opened');
284     this.root_.classList.remove('menu-opened');
285   }
287   this.screen_.hidden = !show;
288   remoting.windowShape.updateClientWindowShape();
292  * @param {number} deltaX
293  * @param {number} deltaY
294  * @private
295  */
296 remoting.ContextMenuDom.prototype.onDragUpdate_ = function(deltaX, deltaY) {
297   this.stubDragged_ = true;
298   this.bottom_ -= deltaY;
299   this.root_.style.bottom = this.bottom_ + 'px';
300   // Deferring the window shape update until the DOM update has completed
301   // helps keep the position of the context menu consistent with the window
302   // shape (though it's still not perfect).
303   window.requestAnimationFrame(
304       function() {
305         remoting.windowShape.updateClientWindowShape();
306       });