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