• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
16base.requireStylesheet('ui.overlay');
17
18base.require('base.properties');
19base.require('base.events');
20base.require('ui');
21
22base.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