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 */
15base.requireStylesheet('overlay');
16base.require('ui');
17base.require('event_target');
18base.exportTo('tracing', 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 = base.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 &&
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 = base.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        base.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) {
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