| OLD | NEW | 
 | (Empty) | 
|    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. |  | 
|    4  |  | 
|    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'; |  | 
|   23  |  | 
|   24 /** @suppress {duplicate} */ |  | 
|   25 var remoting = remoting || {}; |  | 
|   26  |  | 
|   27 /** |  | 
|   28  * @constructor |  | 
|   29  * @implements {remoting.WindowShape.ClientUI} |  | 
|   30  * @implements {remoting.ContextMenuAdapter} |  | 
|   31  * @param {HTMLElement} root The root of the context menu DOM. |  | 
|   32  * @param {remoting.WindowShape} windowShape |  | 
|   33  */ |  | 
|   34 remoting.ContextMenuDom = function(root, windowShape) { |  | 
|   35   /** @private {HTMLElement} */ |  | 
|   36   this.root_ = root; |  | 
|   37   /** @private {HTMLElement} */ |  | 
|   38   this.stub_ = /** @type {HTMLElement} */ |  | 
|   39       (this.root_.querySelector('.context-menu-stub')); |  | 
|   40   /** @private {HTMLElement} */ |  | 
|   41   this.icon_ = /** @type {HTMLElement} */ |  | 
|   42       (this.root_.querySelector('.context-menu-icon')); |  | 
|   43   /** @private {HTMLElement} */ |  | 
|   44   this.screen_ = /** @type {HTMLElement} */ |  | 
|   45       (this.root_.querySelector('.context-menu-screen')); |  | 
|   46   /** @private {HTMLElement} */ |  | 
|   47   this.menu_ = /** @type {HTMLElement} */ (this.root_.querySelector('ul')); |  | 
|   48   /** @private {number} */ |  | 
|   49   this.bottom_ = 8; |  | 
|   50   /** @private {base.EventSourceImpl} */ |  | 
|   51   this.eventSource_ = new base.EventSourceImpl(); |  | 
|   52   /** @private {string} */ |  | 
|   53   this.eventName_ = '_click'; |  | 
|   54   /** |  | 
|   55    * Since the same element is used to lock the icon open and to drag it, we |  | 
|   56    * must keep track of drag events so that the corresponding click event can |  | 
|   57    * be ignored. |  | 
|   58    * |  | 
|   59    * @private {boolean} |  | 
|   60    */ |  | 
|   61   this.stubDragged_ = false; |  | 
|   62  |  | 
|   63   /** @private */ |  | 
|   64   this.windowShape_ = windowShape; |  | 
|   65  |  | 
|   66   /** |  | 
|   67    * @private |  | 
|   68    */ |  | 
|   69   this.dragAndDrop_ = new remoting.DragAndDrop( |  | 
|   70       this.stub_, this.onDragUpdate_.bind(this)); |  | 
|   71  |  | 
|   72   this.eventSource_.defineEvents([this.eventName_]); |  | 
|   73   this.root_.addEventListener( |  | 
|   74       'transitionend', this.onTransitionEnd_.bind(this), false); |  | 
|   75   this.stub_.addEventListener('click', this.onStubClick_.bind(this), false); |  | 
|   76   this.icon_.addEventListener('click', this.onIconClick_.bind(this), false); |  | 
|   77   this.screen_.addEventListener('click', this.onIconClick_.bind(this), false); |  | 
|   78  |  | 
|   79   this.root_.hidden = false; |  | 
|   80   this.root_.style.bottom = this.bottom_ + 'px'; |  | 
|   81   this.windowShape_.registerClientUI(this); |  | 
|   82 }; |  | 
|   83  |  | 
|   84 remoting.ContextMenuDom.prototype.dispose = function() { |  | 
|   85   this.windowShape_.unregisterClientUI(this); |  | 
|   86 }; |  | 
|   87  |  | 
|   88 /** |  | 
|   89  * @param {Array<{left: number, top: number, width: number, height: number}>} |  | 
|   90  *     rects List of rectangles. |  | 
|   91  */ |  | 
|   92 remoting.ContextMenuDom.prototype.addToRegion = function(rects) { |  | 
|   93   var rect = /** @type {ClientRect} */ (this.root_.getBoundingClientRect()); |  | 
|   94   // Clip the menu position to the main window in case the screen size has |  | 
|   95   // changed or a recent drag event tried to move it out of bounds. |  | 
|   96   if (rect.top < 0) { |  | 
|   97     this.bottom_ += rect.top; |  | 
|   98     this.root_.style.bottom = this.bottom_ + 'px'; |  | 
|   99     rect = this.root_.getBoundingClientRect(); |  | 
|  100   } |  | 
|  101  |  | 
|  102   rects.push(rect); |  | 
|  103   if (this.root_.classList.contains('menu-opened')) { |  | 
|  104     var menuRect = this.menu_.getBoundingClientRect(); |  | 
|  105     rects.push(menuRect); |  | 
|  106   } |  | 
|  107 }; |  | 
|  108  |  | 
|  109 /** |  | 
|  110  * @param {string} id An identifier for the menu entry. |  | 
|  111  * @param {string} title The text to display in the menu. |  | 
|  112  * @param {boolean} isCheckable True if the state of this menu entry should |  | 
|  113  *     have a check-box and manage its toggle state automatically. Note that |  | 
|  114  *     checkable menu entries always start off unchecked. |  | 
|  115  * @param {string=} opt_parentId The id of the parent menu item for submenus. |  | 
|  116  */ |  | 
|  117 remoting.ContextMenuDom.prototype.create = function( |  | 
|  118     id, title, isCheckable, opt_parentId) { |  | 
|  119   var menuEntry = /** @type {HTMLElement} */ (document.createElement('li')); |  | 
|  120   menuEntry.innerText = title; |  | 
|  121   menuEntry.setAttribute('data-id', id); |  | 
|  122   if (isCheckable) { |  | 
|  123     menuEntry.setAttribute('data-checkable', true); |  | 
|  124   } |  | 
|  125   menuEntry.addEventListener('click', this.onClick_.bind(this), false); |  | 
|  126   /** @type {Node} */ |  | 
|  127   var insertBefore = null; |  | 
|  128   if (opt_parentId) { |  | 
|  129     var parent = /** @type {HTMLElement} */ |  | 
|  130         (this.menu_.querySelector('[data-id="' + opt_parentId + '"]')); |  | 
|  131     console.assert( |  | 
|  132         parent != null, |  | 
|  133         'No parent match for [data-id="' + /** @type {string} */(opt_parentId) + |  | 
|  134         '"] in create().'); |  | 
|  135     console.assert(!parent.classList.contains('menu-group-item'), |  | 
|  136                    'Nested sub-menus are not supported.'); |  | 
|  137     parent.classList.add('menu-group-header'); |  | 
|  138     menuEntry.classList.add('menu-group-item'); |  | 
|  139     insertBefore = this.getInsertionPointForParent( |  | 
|  140         /** @type {string} */(opt_parentId)); |  | 
|  141   } |  | 
|  142   this.menu_.insertBefore(menuEntry, insertBefore); |  | 
|  143 }; |  | 
|  144  |  | 
|  145 /** |  | 
|  146  * @param {string} id |  | 
|  147  * @param {string} title |  | 
|  148  */ |  | 
|  149 remoting.ContextMenuDom.prototype.updateTitle = function(id, title) { |  | 
|  150   var node = this.menu_.querySelector('[data-id="' + id + '"]'); |  | 
|  151   if (node) { |  | 
|  152     node.innerText = title; |  | 
|  153   } |  | 
|  154 }; |  | 
|  155  |  | 
|  156 /** |  | 
|  157  * @param {string} id |  | 
|  158  * @param {boolean} checked |  | 
|  159  */ |  | 
|  160 remoting.ContextMenuDom.prototype.updateCheckState = function(id, checked) { |  | 
|  161   var node = /** @type {HTMLElement} */ |  | 
|  162       (this.menu_.querySelector('[data-id="' + id + '"]')); |  | 
|  163   if (node) { |  | 
|  164     if (checked) { |  | 
|  165       node.classList.add('selected'); |  | 
|  166     } else { |  | 
|  167       node.classList.remove('selected'); |  | 
|  168     } |  | 
|  169   } |  | 
|  170 }; |  | 
|  171  |  | 
|  172 /** |  | 
|  173  * @param {string} id |  | 
|  174  */ |  | 
|  175 remoting.ContextMenuDom.prototype.remove = function(id) { |  | 
|  176   var node = this.menu_.querySelector('[data-id="' + id + '"]'); |  | 
|  177   if (node) { |  | 
|  178     this.menu_.removeChild(node); |  | 
|  179   } |  | 
|  180 }; |  | 
|  181  |  | 
|  182 /** |  | 
|  183  * @param {function(OnClickData=):void} listener |  | 
|  184  */ |  | 
|  185 remoting.ContextMenuDom.prototype.addListener = function(listener) { |  | 
|  186   this.eventSource_.addEventListener(this.eventName_, listener); |  | 
|  187 }; |  | 
|  188  |  | 
|  189 /** |  | 
|  190  * @param {Event} event |  | 
|  191  * @private |  | 
|  192  */ |  | 
|  193 remoting.ContextMenuDom.prototype.onClick_ = function(event) { |  | 
|  194   var element = /** @type {HTMLElement} */ (event.target); |  | 
|  195   if (element.getAttribute('data-checkable')) { |  | 
|  196     element.classList.toggle('selected') |  | 
|  197   } |  | 
|  198   var clickData = { |  | 
|  199     menuItemId: element.getAttribute('data-id'), |  | 
|  200     checked: element.classList.contains('selected') |  | 
|  201   }; |  | 
|  202   this.eventSource_.raiseEvent(this.eventName_, clickData); |  | 
|  203   this.onIconClick_(); |  | 
|  204 }; |  | 
|  205  |  | 
|  206 /** |  | 
|  207  * Get the insertion point for the specified sub-menu. This is the menu item |  | 
|  208  * immediately following the last child of that menu group, or null if there |  | 
|  209  * are no menu items after that group. |  | 
|  210  * |  | 
|  211  * @param {string} parentId |  | 
|  212  * @return {Node?} |  | 
|  213  */ |  | 
|  214 remoting.ContextMenuDom.prototype.getInsertionPointForParent = function( |  | 
|  215     parentId) { |  | 
|  216   var parentNode = this.menu_.querySelector('[data-id="' + parentId + '"]'); |  | 
|  217   console.assert(parentNode != null, |  | 
|  218                  'No parent match for [data-id="' + parentId + |  | 
|  219                  '"] in getInsertionPointForParent().'); |  | 
|  220   var childNode = /** @type {HTMLElement} */ (parentNode.nextSibling); |  | 
|  221   while (childNode != null && childNode.classList.contains('menu-group-item')) { |  | 
|  222     childNode = childNode.nextSibling; |  | 
|  223   } |  | 
|  224   return childNode; |  | 
|  225 }; |  | 
|  226  |  | 
|  227 /** |  | 
|  228  * Called when the CSS show/hide transition completes. Since this changes the |  | 
|  229  * visible dimensions of the context menu, the visible region of the window |  | 
|  230  * needs to be recomputed. |  | 
|  231  * |  | 
|  232  * @private |  | 
|  233  */ |  | 
|  234 remoting.ContextMenuDom.prototype.onTransitionEnd_ = function() { |  | 
|  235   this.windowShape_.updateClientWindowShape(); |  | 
|  236 }; |  | 
|  237  |  | 
|  238 /** |  | 
|  239  * Toggle the visibility of the context menu icon. |  | 
|  240  * |  | 
|  241  * @private |  | 
|  242  */ |  | 
|  243 remoting.ContextMenuDom.prototype.onStubClick_ = function() { |  | 
|  244   if (this.stubDragged_) { |  | 
|  245     this.stubDragged_ = false; |  | 
|  246     return; |  | 
|  247   } |  | 
|  248   this.root_.classList.toggle('opened'); |  | 
|  249 }; |  | 
|  250  |  | 
|  251 /** |  | 
|  252  * Toggle the visibility of the context menu. |  | 
|  253  * |  | 
|  254  * @private |  | 
|  255  */ |  | 
|  256 remoting.ContextMenuDom.prototype.onIconClick_ = function() { |  | 
|  257   this.showMenu_(!this.menu_.classList.contains('opened')); |  | 
|  258 }; |  | 
|  259  |  | 
|  260 /** |  | 
|  261  * Explicitly show or hide the context menu. |  | 
|  262  * |  | 
|  263  * @param {boolean} show True to show the menu; false to hide it. |  | 
|  264  * @private |  | 
|  265  */ |  | 
|  266 remoting.ContextMenuDom.prototype.showMenu_ = function(show) { |  | 
|  267   if (show) { |  | 
|  268     // Ensure that the menu doesn't extend off the top or bottom of the |  | 
|  269     // screen by aligning it to the top or bottom of the icon, depending |  | 
|  270     // on the latter's vertical position. |  | 
|  271     var menuRect = |  | 
|  272         /** @type {ClientRect} */ (this.menu_.getBoundingClientRect()); |  | 
|  273     if (menuRect.bottom > window.innerHeight) { |  | 
|  274       this.menu_.classList.add('menu-align-bottom'); |  | 
|  275     } else { |  | 
|  276       this.menu_.classList.remove('menu-align-bottom'); |  | 
|  277     } |  | 
|  278  |  | 
|  279     /** @type {remoting.ContextMenuDom} */ |  | 
|  280     var that = this; |  | 
|  281     var onBlur = function() { |  | 
|  282       that.showMenu_(false); |  | 
|  283       window.removeEventListener('blur', onBlur, false); |  | 
|  284     }; |  | 
|  285     window.addEventListener('blur', onBlur, false); |  | 
|  286  |  | 
|  287     // Show the menu and prevent the icon from auto-hiding on mouse-out. |  | 
|  288     this.menu_.classList.add('opened'); |  | 
|  289     this.root_.classList.add('menu-opened'); |  | 
|  290  |  | 
|  291   } else {  // if (!show) |  | 
|  292     this.menu_.classList.remove('opened'); |  | 
|  293     this.root_.classList.remove('menu-opened'); |  | 
|  294   } |  | 
|  295  |  | 
|  296   this.screen_.hidden = !show; |  | 
|  297   this.windowShape_.updateClientWindowShape(); |  | 
|  298 }; |  | 
|  299  |  | 
|  300 /** |  | 
|  301  * @param {number} deltaX |  | 
|  302  * @param {number} deltaY |  | 
|  303  * @private |  | 
|  304  */ |  | 
|  305 remoting.ContextMenuDom.prototype.onDragUpdate_ = function(deltaX, deltaY) { |  | 
|  306   this.stubDragged_ = true; |  | 
|  307   this.bottom_ -= deltaY; |  | 
|  308   this.root_.style.bottom = this.bottom_ + 'px'; |  | 
|  309   // Deferring the window shape update until the DOM update has completed |  | 
|  310   // helps keep the position of the context menu consistent with the window |  | 
|  311   // shape (though it's still not perfect). |  | 
|  312   window.requestAnimationFrame( |  | 
|  313       this.windowShape_.updateClientWindowShape.bind(this.windowShape_)); |  | 
|  314 }; |  | 
| OLD | NEW |