blob: 1a911a83911cbeb931fd43bd0756553c6acbc7da [file] [log] [blame]
Jamie Gennis01058352012-05-06 12:48:05 -07001// 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 */
15cr.define('tracing', 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 var focusElement =
60 overlay.querySelector('button, input, list, select, a');
61 if (!focusElement) {
62 focusElement = overlay;
63 }
64 focusElement.focus();
65
66 // Listen to key and focus events to prevent focus from
67 // leaving the overlay.
68 this.ownerDocument.addEventListener('focusin',
69 this.onFocusInBoundToThis_, true);
70 overlay.addEventListener('keydown', this.onKeydownBoundToThis_);
71 },
72
73 /**
74 * Clicking outside of the overlay will de-focus the overlay. The
75 * next tab will look at the entire document to determine the focus.
76 * For certain documents, this can cause focus to "leak" outside of
77 * the overlay.
78 */
79 onMousedown_: function(e) {
80 if (e.target == this) {
81 e.preventDefault();
82 }
83 },
84
85 /**
86 * Prevents forward-tabbing out of the overlay
87 */
88 onFocusIn_: function(e) {
89 if (e.target == this.tabCatcher) {
90 window.setTimeout(this.focusOverlay_.bind(this), 0);
91 }
92 },
93
94 focusOverlay_: function() {
95 this.contentHost.firstChild.focus();
96 },
97
98 /**
99 * Prevent the user from shift-tabbing backwards out of the overlay.
100 */
101 onKeydown_: function(e) {
102 if (e.keyCode == 9 &&
103 e.shiftKey &&
104 e.target == this.contentHost.firstChild) {
105 e.preventDefault();
106 }
107 },
108
109 /**
110 * Hides an overlay, attaching it to its original parent if needed.
111 */
112 hideOverlay: function(overlay) {
113 // hide the overlay root
114 this.visible = false;
115 this.ownerDocument.body.classList.remove('disabled-by-overlay');
116 this.lastFocusOut_ = undefined;
117
118 // put the overlay back on its previous parent
119 overlay.parentNode.removeChild(this.tabCatcher);
120 if (overlay.oldParent_) {
121 overlay.oldParent_.appendChild(overlay);
122 delete overlay.oldParent_;
123 } else {
124 this.contentHost.removeChild(overlay);
125 }
126
127 // remove listeners
128 overlay.removeEventListener('keydown', this.onKeydownBoundToThis_);
129 this.ownerDocument.removeEventListener('focusin',
130 this.onFocusInBoundToThis_);
131 }
132 };
133
134 cr.defineProperty(OverlayRoot, 'visible', cr.PropertyKind.BOOL_ATTR);
135
136 /**
137 * Creates a new overlay element. It will not be visible until shown.
138 * @param {Object=} opt_propertyBag Optional properties.
139 * @constructor
140 * @extends {HTMLDivElement}
141 */
142 var Overlay = cr.ui.define('div');
143
144 Overlay.prototype = {
145 __proto__: HTMLDivElement.prototype,
146
147 /**
148 * Initializes the overlay element.
149 */
150 decorate: function() {
151 // create the overlay root on this document if its not present
152 if (!this.ownerDocument.querySelector('.overlay-root')) {
153 var overlayRoot = this.ownerDocument.createElement('div');
154 cr.ui.decorate(overlayRoot, OverlayRoot);
155 this.ownerDocument.body.appendChild(overlayRoot);
156 }
157
158 this.classList.add('overlay');
159 this.visible = false;
160 },
161
162 onVisibleChanged_: function() {
163 var overlayRoot = this.ownerDocument.querySelector('.overlay-root');
164 if (this.visible) {
165 overlayRoot.showOverlay(this);
166 } else {
167 overlayRoot.hideOverlay(this);
168 }
169 }
170 };
171
172 /**
173 * Shows and hides the overlay. Note that while visible == true, the overlay
174 * element will be tempoarily reparented to another place in the DOM.
175 */
176 cr.defineProperty(Overlay, 'visible', cr.PropertyKind.BOOL_ATTR,
177 Overlay.prototype.onVisibleChanged_);
178
179 return {
180 Overlay: Overlay
181 };
182});