Rewrite AndroidSyncSettings to be significantly simpler.
[chromium-blink-merge.git] / remoting / webapp / app_remoting / js / context_menu_dom.js
blob01c83b5982b27678df11c4085fccd7799b5f4871
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.
10 * To mimic the behaviour of an OS-provided context menu, the menu is dismissed
11 * in three situations:
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.
20 * TODO(jamiewalch): Fold this functionality into remoting.MenuButton.
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.
33 remoting.ContextMenuDom = function(root) {
34 /**
35 * @type {HTMLElement}
36 * @private
38 this.root_ = root;
39 /**
40 * @type {HTMLElement}
41 * @private
43 this.stub_ = /** @type {HTMLElement} */
44 (this.root_.querySelector('.context-menu-stub'));
45 /**
46 * @type {HTMLElement}
47 * @private
49 this.icon_ = /** @type {HTMLElement} */
50 (this.root_.querySelector('.context-menu-icon'));
51 /**
52 * @type {HTMLElement}
53 * @private
55 this.screen_ = /** @type {HTMLElement} */
56 (this.root_.querySelector('.context-menu-screen'));
57 /**
58 * @type {HTMLElement}
59 * @private
61 this.menu_ = /** @type {HTMLElement} */ (this.root_.querySelector('ul'));
62 /**
63 * @type {number}
64 * @private
66 this.bottom_ = 8;
67 /**
68 * @type {base.EventSourceImpl}
69 * @private
71 this.eventSource_ = new base.EventSourceImpl();
72 /**
73 * @type {string}
74 * @private
76 this.eventName_ = '_click';
77 /**
78 * Since the same element is used to lock the icon open and to drag it, we
79 * must keep track of drag events so that the corresponding click event can
80 * be ignored.
82 * @type {boolean}
83 * @private
85 this.stubDragged_ = false;
87 /**
88 * @private
90 this.dragAndDrop_ = new remoting.DragAndDrop(
91 this.stub_, this.onDragUpdate_.bind(this));
93 this.eventSource_.defineEvents([this.eventName_]);
94 this.root_.addEventListener(
95 'transitionend', this.onTransitionEnd_.bind(this), false);
96 this.stub_.addEventListener('click', this.onStubClick_.bind(this), false);
97 this.icon_.addEventListener('click', this.onIconClick_.bind(this), false);
98 this.screen_.addEventListener('click', this.onIconClick_.bind(this), false);
100 this.root_.hidden = false;
101 this.root_.style.bottom = this.bottom_ + 'px';
102 remoting.windowShape.addCallback(this);
106 * @param {Array<{left: number, top: number, width: number, height: number}>}
107 * rects List of rectangles.
109 remoting.ContextMenuDom.prototype.addToRegion = function(rects) {
110 var rect = /** @type {ClientRect} */ (this.root_.getBoundingClientRect());
111 // Clip the menu position to the main window in case the screen size has
112 // changed or a recent drag event tried to move it out of bounds.
113 if (rect.top < 0) {
114 this.bottom_ += rect.top;
115 this.root_.style.bottom = this.bottom_ + 'px';
116 rect = this.root_.getBoundingClientRect();
119 rects.push(rect);
120 if (this.root_.classList.contains('menu-opened')) {
121 var menuRect =
122 /** @type {ClientRect} */ (this.menu_.getBoundingClientRect());
123 rects.push(menuRect);
128 * @param {string} id An identifier for the menu entry.
129 * @param {string} title The text to display in the menu.
130 * @param {boolean} isCheckable True if the state of this menu entry should
131 * have a check-box and manage its toggle state automatically. Note that
132 * checkable menu entries always start off unchecked.
133 * @param {string=} opt_parentId The id of the parent menu item for submenus.
135 remoting.ContextMenuDom.prototype.create = function(
136 id, title, isCheckable, opt_parentId) {
137 var menuEntry = /** @type {HTMLElement} */ (document.createElement('li'));
138 menuEntry.innerText = title;
139 menuEntry.setAttribute('data-id', id);
140 if (isCheckable) {
141 menuEntry.setAttribute('data-checkable', true);
143 menuEntry.addEventListener('click', this.onClick_.bind(this), false);
144 /** @type {Node} */
145 var insertBefore = null;
146 if (opt_parentId) {
147 var parent = /** @type {HTMLElement} */
148 (this.menu_.querySelector('[data-id="' + opt_parentId + '"]'));
149 base.debug.assert(parent != null);
150 base.debug.assert(!parent.classList.contains('menu-group-item'));
151 parent.classList.add('menu-group-header');
152 menuEntry.classList.add('menu-group-item');
153 insertBefore = this.getInsertionPointForParent(
154 /** @type {string} */(opt_parentId));
156 this.menu_.insertBefore(menuEntry, insertBefore);
160 * @param {string} id
161 * @param {string} title
163 remoting.ContextMenuDom.prototype.updateTitle = function(id, title) {
164 var node = this.menu_.querySelector('[data-id="' + id + '"]');
165 if (node) {
166 node.innerText = title;
171 * @param {string} id
172 * @param {boolean} checked
174 remoting.ContextMenuDom.prototype.updateCheckState = function(id, checked) {
175 var node = /** @type {HTMLElement} */
176 (this.menu_.querySelector('[data-id="' + id + '"]'));
177 if (node) {
178 if (checked) {
179 node.classList.add('selected');
180 } else {
181 node.classList.remove('selected');
187 * @param {string} id
189 remoting.ContextMenuDom.prototype.remove = function(id) {
190 var node = this.menu_.querySelector('[data-id="' + id + '"]');
191 if (node) {
192 this.menu_.removeChild(node);
197 * @param {function(OnClickData=):void} listener
199 remoting.ContextMenuDom.prototype.addListener = function(listener) {
200 this.eventSource_.addEventListener(this.eventName_, listener);
204 * @param {Event} event
205 * @private
207 remoting.ContextMenuDom.prototype.onClick_ = function(event) {
208 var element = /** @type {HTMLElement} */ (event.target);
209 if (element.getAttribute('data-checkable')) {
210 element.classList.toggle('selected')
212 var clickData = {
213 menuItemId: element.getAttribute('data-id'),
214 checked: element.classList.contains('selected')
216 this.eventSource_.raiseEvent(this.eventName_, clickData);
217 this.onIconClick_();
221 * Get the insertion point for the specified sub-menu. This is the menu item
222 * immediately following the last child of that menu group, or null if there
223 * are no menu items after that group.
225 * @param {string} parentId
226 * @return {Node?}
228 remoting.ContextMenuDom.prototype.getInsertionPointForParent = function(
229 parentId) {
230 var parentNode = this.menu_.querySelector('[data-id="' + parentId + '"]');
231 base.debug.assert(parentNode != null);
232 var childNode = /** @type {HTMLElement} */ (parentNode.nextSibling);
233 while (childNode != null && childNode.classList.contains('menu-group-item')) {
234 childNode = childNode.nextSibling;
236 return childNode;
240 * Called when the CSS show/hide transition completes. Since this changes the
241 * visible dimensions of the context menu, the visible region of the window
242 * needs to be recomputed.
244 * @private
246 remoting.ContextMenuDom.prototype.onTransitionEnd_ = function() {
247 remoting.windowShape.updateClientWindowShape();
251 * Toggle the visibility of the context menu icon.
253 * @private
255 remoting.ContextMenuDom.prototype.onStubClick_ = function() {
256 if (this.stubDragged_) {
257 this.stubDragged_ = false;
258 return;
260 this.root_.classList.toggle('opened');
264 * Toggle the visibility of the context menu.
266 * @private
268 remoting.ContextMenuDom.prototype.onIconClick_ = function() {
269 this.showMenu_(!this.menu_.classList.contains('opened'));
273 * Explicitly show or hide the context menu.
275 * @param {boolean} show True to show the menu; false to hide it.
276 * @private
278 remoting.ContextMenuDom.prototype.showMenu_ = function(show) {
279 if (show) {
280 // Ensure that the menu doesn't extend off the top or bottom of the
281 // screen by aligning it to the top or bottom of the icon, depending
282 // on the latter's vertical position.
283 var menuRect =
284 /** @type {ClientRect} */ (this.menu_.getBoundingClientRect());
285 if (menuRect.bottom > window.innerHeight) {
286 this.menu_.classList.add('menu-align-bottom');
287 } else {
288 this.menu_.classList.remove('menu-align-bottom');
291 /** @type {remoting.ContextMenuDom} */
292 var that = this;
293 var onBlur = function() {
294 that.showMenu_(false);
295 window.removeEventListener('blur', onBlur, false);
297 window.addEventListener('blur', onBlur, false);
299 // Show the menu and prevent the icon from auto-hiding on mouse-out.
300 this.menu_.classList.add('opened');
301 this.root_.classList.add('menu-opened');
303 } else { // if (!show)
304 this.menu_.classList.remove('opened');
305 this.root_.classList.remove('menu-opened');
308 this.screen_.hidden = !show;
309 remoting.windowShape.updateClientWindowShape();
313 * @param {number} deltaX
314 * @param {number} deltaY
315 * @private
317 remoting.ContextMenuDom.prototype.onDragUpdate_ = function(deltaX, deltaY) {
318 this.stubDragged_ = true;
319 this.bottom_ -= deltaY;
320 this.root_.style.bottom = this.bottom_ + 'px';
321 // Deferring the window shape update until the DOM update has completed
322 // helps keep the position of the context menu consistent with the window
323 // shape (though it's still not perfect).
324 window.requestAnimationFrame(
325 function() {
326 remoting.windowShape.updateClientWindowShape();