1// Copyright (c) 2011 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 */
15cr.define('gpu', function() {
16  /**
17   * Manages a full-window div that darkens the window, disables
18   * input, and hosts the currently-visible overlays. You shouldn't
19   * have to instantiate this directly --- it gets set automatically.
20   * @param {Object=} opt_propertyBag Optional properties.
21   * @constructor
22   * @extends {HTMLDivElement}
23   */
24  var OverlayRoot = cr.ui.define('div');
25  OverlayRoot.prototype = {
26    __proto__: HTMLDivElement.prototype,
27    decorate: function() {
28      this.classList.add('overlay-root');
29      this.visible = false;
30
31      this.contentHost = this.ownerDocument.createElement('div');
32      this.contentHost.classList.add('content-host');
33
34      this.tabCatcher = this.ownerDocument.createElement('span');
35      this.tabCatcher.tabIndex = 0;
36
37      this.appendChild(this.contentHost);
38
39      this.onKeydownBoundToThis_ = this.onKeydown_.bind(this);
40      this.onFocusInBoundToThis_ = this.onFocusIn_.bind(this);
41      this.addEventListener('mousedown', this.onMousedown_.bind(this));
42    },
43
44    /**
45     * Adds an overlay, attaching it to the contentHost so that it is visible.
46     */
47    showOverlay: function(overlay) {
48      // Reparent this to the overlay content host.
49      overlay.oldParent_ = overlay.parentNode;
50      this.contentHost.appendChild(overlay);
51      this.contentHost.appendChild(this.tabCatcher);
52
53      // Show the overlay root.
54      this.ownerDocument.body.classList.add('disabled-by-overlay');
55      this.visible = true;
56
57      // Bring overlay into focus.
58      overlay.tabIndex = 0;
59      overlay.focus();
60
61      // Listen to key and focus events to prevent focus from
62      // leaving the overlay.
63      this.ownerDocument.addEventListener('focusin',
64          this.onFocusInBoundToThis_, true);
65      overlay.addEventListener('keydown', this.onKeydownBoundToThis_);
66    },
67
68    /**
69     * Clicking outside of the overlay will de-focus the overlay. The
70     * next tab will look at the entire document to determine the focus.
71     * For certain documents, this can cause focus to "leak" outside of
72     * the overlay.
73     */
74    onMousedown_: function(e) {
75      if (e.target == this) {
76        e.preventDefault();
77      }
78    },
79
80    /**
81     * Prevents forward-tabbing out of the overlay
82     */
83    onFocusIn_: function(e) {
84      if (e.target == this.tabCatcher) {
85        window.setTimeout(this.focusOverlay_.bind(this), 0);
86      }
87    },
88
89    focusOverlay_: function() {
90      this.contentHost.firstChild.focus();
91    },
92
93    /**
94     * Prevent the user from shift-tabbing backwards out of the overlay.
95     */
96    onKeydown_: function(e) {
97      if (e.keyCode == 9 &&
98          e.shiftKey &&
99          e.target == this.contentHost.firstChild) {
100        e.preventDefault();
101      }
102    },
103
104    /**
105     * Hides an overlay, attaching it to its original parent if needed.
106     */
107    hideOverlay: function(overlay) {
108      // hide the overlay root
109      this.visible = false;
110      this.ownerDocument.body.classList.remove('disabled-by-overlay');
111      this.lastFocusOut_ = undefined;
112
113      // put the overlay back on its previous parent
114      overlay.parentNode.removeChild(this.tabCatcher);
115      if (overlay.oldParent_) {
116        overlay.oldParent_.appendChild(overlay);
117        delete overlay.oldParent_;
118      } else {
119        this.contentHost.removeChild(overlay);
120      }
121
122      // remove listeners
123      overlay.removeEventListener('keydown', this.onKeydownBoundToThis_);
124      this.ownerDocument.removeEventListener('focusin',
125          this.onFocusInBoundToThis_);
126    }
127  };
128
129  cr.defineProperty(OverlayRoot, 'visible', cr.PropertyKind.BOOL_ATTR);
130
131  /**
132   * Creates a new overlay element. It will not be visible until shown.
133   * @param {Object=} opt_propertyBag Optional properties.
134   * @constructor
135   * @extends {HTMLDivElement}
136   */
137  var Overlay = cr.ui.define('div');
138
139  Overlay.prototype = {
140    __proto__: HTMLDivElement.prototype,
141
142    /**
143     * Initializes the overlay element.
144     */
145    decorate: function() {
146      // create the overlay root on this document if its not present
147      if (!this.ownerDocument.querySelector('.overlay-root')) {
148        var overlayRoot = this.ownerDocument.createElement('div');
149        cr.ui.decorate(overlayRoot, OverlayRoot);
150        this.ownerDocument.body.appendChild(overlayRoot);
151      }
152
153      this.classList.add('overlay');
154      this.visible = false;
155    },
156
157    onVisibleChanged_: function() {
158      var overlayRoot = this.ownerDocument.querySelector('.overlay-root');
159      if (this.visible) {
160        overlayRoot.showOverlay(this);
161      } else {
162        overlayRoot.hideOverlay(this);
163      }
164    }
165  };
166
167  /**
168   * Shows and hides the overlay. Note that while visible == true, the overlay
169   * element will be tempoarily reparented to another place in the DOM.
170   */
171  cr.defineProperty(Overlay, 'visible', cr.PropertyKind.BOOL_ATTR,
172      Overlay.prototype.onVisibleChanged_);
173
174  return {
175    Overlay: Overlay
176  };
177});
178