Home | History | Annotate | Download | only in src
      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 
      6 /**
      7  * @fileoverview Implements an element that is hidden by default, but
      8  * when shown, dims and (attempts to) disable the main document.
      9  *
     10  * You can turn any div into an overlay. Note that while an
     11  * overlay element is shown, its parent is changed. Hiding the overlay
     12  * restores its original parentage.
     13  *
     14  */
     15 base.requireStylesheet('overlay');
     16 base.require('ui');
     17 base.require('event_target');
     18 base.exportTo('tracing.ui', function() {
     19   /**
     20    * Manages a full-window div that darkens the window, disables
     21    * input, and hosts the currently-visible overlays. You shouldn't
     22    * have to instantiate this directly --- it gets set automatically.
     23    * @param {Object=} opt_propertyBag Optional properties.
     24    * @constructor
     25    * @extends {HTMLDivElement}
     26    */
     27   var OverlayRoot = tracing.ui.define('div');
     28   OverlayRoot.prototype = {
     29     __proto__: HTMLDivElement.prototype,
     30     decorate: function() {
     31       this.classList.add('overlay-root');
     32       this.visible = false;
     33 
     34       this.contentHost = this.ownerDocument.createElement('div');
     35       this.contentHost.classList.add('content-host');
     36 
     37       this.tabCatcher = this.ownerDocument.createElement('span');
     38       this.tabCatcher.tabIndex = 0;
     39 
     40       this.appendChild(this.contentHost);
     41 
     42       this.onKeydownBoundToThis_ = this.onKeydown_.bind(this);
     43       this.onFocusInBoundToThis_ = this.onFocusIn_.bind(this);
     44       this.addEventListener('mousedown', this.onMousedown_.bind(this));
     45     },
     46 
     47     /**
     48      * Adds an overlay, attaching it to the contentHost so that it is visible.
     49      */
     50     showOverlay: function(overlay) {
     51       // Reparent this to the overlay content host.
     52       overlay.oldParent_ = overlay.parentNode;
     53       this.contentHost.appendChild(overlay);
     54       this.contentHost.appendChild(this.tabCatcher);
     55 
     56       // Show the overlay root.
     57       this.ownerDocument.body.classList.add('disabled-by-overlay');
     58       this.visible = true;
     59 
     60       // Bring overlay into focus.
     61       overlay.tabIndex = 0;
     62       var focusElement =
     63           overlay.querySelector('button, input, list, select, a');
     64       if (!focusElement) {
     65         focusElement = overlay;
     66       }
     67       focusElement.focus();
     68 
     69       // Listen to key and focus events to prevent focus from
     70       // leaving the overlay.
     71       this.ownerDocument.addEventListener('focusin',
     72           this.onFocusInBoundToThis_, true);
     73       overlay.addEventListener('keydown', this.onKeydownBoundToThis_);
     74     },
     75 
     76     /**
     77      * Clicking outside of the overlay will de-focus the overlay. The
     78      * next tab will look at the entire document to determine the focus.
     79      * For certain documents, this can cause focus to "leak" outside of
     80      * the overlay.
     81      */
     82     onMousedown_: function(e) {
     83       if (e.target == this) {
     84         e.preventDefault();
     85       }
     86     },
     87 
     88     /**
     89      * Prevents forward-tabbing out of the overlay
     90      */
     91     onFocusIn_: function(e) {
     92       if (e.target == this.tabCatcher) {
     93         window.setTimeout(this.focusOverlay_.bind(this), 0);
     94       }
     95     },
     96 
     97     focusOverlay_: function() {
     98       this.contentHost.firstChild.focus();
     99     },
    100 
    101     /**
    102      * Prevent the user from shift-tabbing backwards out of the overlay.
    103      */
    104     onKeydown_: function(e) {
    105       if (e.keyCode == 9 &&  // tab
    106           e.shiftKey &&
    107           e.target == this.contentHost.firstChild) {
    108         e.preventDefault();
    109       }
    110     },
    111 
    112     /**
    113      * Hides an overlay, attaching it to its original parent if needed.
    114      */
    115     hideOverlay: function(overlay) {
    116       // hide the overlay root
    117       this.visible = false;
    118       this.ownerDocument.body.classList.remove('disabled-by-overlay');
    119       this.lastFocusOut_ = undefined;
    120 
    121       // put the overlay back on its previous parent
    122       overlay.parentNode.removeChild(this.tabCatcher);
    123       if (overlay.oldParent_) {
    124         overlay.oldParent_.appendChild(overlay);
    125         delete overlay.oldParent_;
    126       } else {
    127         this.contentHost.removeChild(overlay);
    128       }
    129 
    130       // remove listeners
    131       overlay.removeEventListener('keydown', this.onKeydownBoundToThis_);
    132       this.ownerDocument.removeEventListener('focusin',
    133           this.onFocusInBoundToThis_);
    134     }
    135   };
    136 
    137   base.defineProperty(OverlayRoot, 'visible', base.PropertyKind.BOOL_ATTR);
    138 
    139   /**
    140    * Creates a new overlay element. It will not be visible until shown.
    141    * @param {Object=} opt_propertyBag Optional properties.
    142    * @constructor
    143    * @extends {HTMLDivElement}
    144    */
    145   var Overlay = tracing.ui.define('div');
    146 
    147   Overlay.prototype = {
    148     __proto__: HTMLDivElement.prototype,
    149 
    150     /**
    151      * Initializes the overlay element.
    152      */
    153     decorate: function() {
    154       // create the overlay root on this document if its not present
    155       if (!this.ownerDocument.querySelector('.overlay-root')) {
    156         var overlayRoot = this.ownerDocument.createElement('div');
    157         tracing.ui.decorate(overlayRoot, OverlayRoot);
    158         this.ownerDocument.body.appendChild(overlayRoot);
    159       }
    160 
    161       this.classList.add('overlay');
    162       this.visible = false;
    163       this.defaultClickShouldClose = true;
    164       this.autoClose = false;
    165       this.additionalCloseKeyCodes = [];
    166       this.onKeyDown = this.onKeyDown.bind(this);
    167       this.onKeyPress = this.onKeyPress.bind(this);
    168       this.onDocumentClick = this.onDocumentClick.bind(this);
    169     },
    170 
    171     onVisibleChanged_: function() {
    172       var overlayRoot = this.ownerDocument.querySelector('.overlay-root');
    173       base.dispatchSimpleEvent(this, 'visibleChange');
    174       if (this.visible) {
    175         overlayRoot.showOverlay(this);
    176         document.addEventListener('keydown', this.onKeyDown, true);
    177         document.addEventListener('keypress', this.onKeyPress, true);
    178         document.addEventListener('click', this.onDocumentClick, true);
    179       } else {
    180         document.removeEventListener('keydown', this.onKeyDown, true);
    181         document.removeEventListener('keypress', this.onKeyPress, true);
    182         document.removeEventListener('click', this.onDocumentClick, true);
    183         overlayRoot.hideOverlay(this);
    184       }
    185     },
    186 
    187     onKeyDown: function(e) {
    188       if (!this.autoClose)
    189         return;
    190 
    191       if (e.keyCode == 27) {  // escape
    192         this.visible = false;
    193         e.preventDefault();
    194         return;
    195       }
    196     },
    197 
    198     onKeyPress: function(e) {
    199       if (!this.autoClose)
    200         return;
    201 
    202       for (var i = 0; i < this.additionalCloseKeyCodes.length; i++) {
    203         if (e.keyCode == this.additionalCloseKeyCodes[i]) {
    204           this.visible = false;
    205           e.preventDefault();
    206           return;
    207         }
    208       }
    209     },
    210 
    211     onDocumentClick: function(e) {
    212       if (!this.defaultClickShouldClose)
    213         return;
    214       var target = e.target;
    215       while (target !== null) {
    216         if (target === this)
    217           return;
    218         target = target.parentNode;
    219       }
    220       this.visible = false;
    221       e.preventDefault();
    222       return;
    223     }
    224 
    225   };
    226 
    227   /**
    228    * Shows and hides the overlay. Note that while visible == true, the overlay
    229    * element will be tempoarily reparented to another place in the DOM.
    230    */
    231   base.defineProperty(Overlay, 'visible', base.PropertyKind.BOOL_ATTR,
    232       Overlay.prototype.onVisibleChanged_);
    233   base.defineProperty(Overlay, 'defaultClickShouldClose',
    234       base.PropertyKind.BOOL_ATTR);
    235 
    236   return {
    237     Overlay: Overlay
    238   };
    239 });
    240