Javascript  |  281行  |  8.32 KB

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * @fileoverview Implements an element that is hidden by default, but
 * when shown, dims and (attempts to) disable the main document.
 *
 * You can turn any div into an overlay. Note that while an
 * overlay element is shown, its parent is changed. Hiding the overlay
 * restores its original parentage.
 *
 */
base.requireStylesheet('ui.overlay');

base.require('base.properties');
base.require('base.events');
base.require('ui');

base.exportTo('ui', function() {
  /**
   * Manages a full-window div that darkens the window, disables
   * input, and hosts the currently-visible overlays. You shouldn't
   * have to instantiate this directly --- it gets set automatically.
   * @param {Object=} opt_propertyBag Optional properties.
   * @constructor
   * @extends {HTMLDivElement}
   */
  var OverlayRoot = ui.define('div');
  OverlayRoot.prototype = {
    __proto__: HTMLDivElement.prototype,
    decorate: function() {
      this.classList.add('overlay-root');


      this.createToolBar_();

      this.contentHost = this.ownerDocument.createElement('div');
      this.contentHost.classList.add('content-host');

      this.tabCatcher = this.ownerDocument.createElement('span');
      this.tabCatcher.tabIndex = 0;

      this.appendChild(this.contentHost);

      this.onKeydown_ = this.onKeydown_.bind(this);
      this.onFocusIn_ = this.onFocusIn_.bind(this);
      this.addEventListener('mousedown', this.onMousedown_.bind(this));
    },

    toggleToolbar: function(show) {
      if (show) {
        if (this.contentHost.firstChild)
          this.contentHost.insertBefore(this.contentHost.firstChild,
                                        this.toolbar_);
        else
          this.contentHost.appendChild(this.toolbar_);
      } else {
        if (this.toolbar_.parentElement)
          this.contentHost.removeChild(this.toolbar_);
      }
    },

    createToolBar_: function() {
      this.toolbar_ = this.ownerDocument.createElement('div');
      this.toolbar_.className = 'tool-bar';
      this.exitButton_ = this.ownerDocument.createElement('span');
      this.exitButton_.className = 'exit-button';
      this.exitButton_.textContent = 'x';
      this.exitButton_.title = 'Close Overlay (esc)';
      this.toolbar_.appendChild(this.exitButton_);
    },

    /**
     * Adds an overlay, attaching it to the contentHost so that it is visible.
     */
    showOverlay: function(overlay) {
      // Reparent this to the overlay content host.
      overlay.oldParent_ = overlay.parentNode;
      this.contentHost.appendChild(overlay);
      this.contentHost.appendChild(this.tabCatcher);

      // Show the overlay root.
      this.ownerDocument.body.classList.add('disabled-by-overlay');

      // Bring overlay into focus.
      overlay.tabIndex = 0;
      var focusElement =
          overlay.querySelector('button, input, list, select, a');
      if (!focusElement) {
        focusElement = overlay;
      }
      focusElement.focus();

      // Listen to key and focus events to prevent focus from
      // leaving the overlay.
      this.ownerDocument.addEventListener('focusin', this.onFocusIn_, true);
      overlay.addEventListener('keydown', this.onKeydown_);
    },

    /**
     * Clicking outside of the overlay will de-focus the overlay. The
     * next tab will look at the entire document to determine the focus.
     * For certain documents, this can cause focus to "leak" outside of
     * the overlay.
     */
    onMousedown_: function(e) {
      if (e.target == this) {
        e.preventDefault();
      }
    },

    /**
     * Prevents forward-tabbing out of the overlay
     */
    onFocusIn_: function(e) {
      if (e.target == this.tabCatcher) {
        window.setTimeout(this.focusOverlay_.bind(this), 0);
      }
    },

    focusOverlay_: function() {
      this.contentHost.firstChild.focus();
    },

    /**
     * Prevent the user from shift-tabbing backwards out of the overlay.
     */
    onKeydown_: function(e) {
      if (e.keyCode == 9 &&  // tab
          e.shiftKey &&
          e.target == this.contentHost.firstChild) {
        e.preventDefault();
      }
    },

    /**
     * Hides an overlay, attaching it to its original parent if needed.
     */
    hideOverlay: function(overlay) {
      // hide the overlay root
      this.visible = false;
      this.ownerDocument.body.classList.remove('disabled-by-overlay');
      this.lastFocusOut_ = undefined;

      // put the overlay back on its previous parent
      overlay.parentNode.removeChild(this.tabCatcher);
      if (overlay.oldParent_) {
        overlay.oldParent_.appendChild(overlay);
        delete overlay.oldParent_;
      } else {
        this.contentHost.removeChild(overlay);
      }

      // remove listeners
      overlay.removeEventListener('keydown', this.onKeydown_);
      this.ownerDocument.removeEventListener('focusin', this.onFocusIn_);
    }
  };

  /**
   * Creates a new overlay element. It will not be visible until shown.
   * @param {Object=} opt_propertyBag Optional properties.
   * @constructor
   * @extends {HTMLDivElement}
   */
  var Overlay = ui.define('div');

  Overlay.prototype = {
    __proto__: HTMLDivElement.prototype,

    /**
     * Initializes the overlay element.
     */
    decorate: function() {
      // create the overlay root on this document if its not present
      if (!this.ownerDocument.querySelector('.overlay-root')) {
        var overlayRoot = this.ownerDocument.createElement('div');
        ui.decorate(overlayRoot, OverlayRoot);
        this.ownerDocument.body.appendChild(overlayRoot);
      }

      this.classList.add('overlay');
      this.visible_ = false;
      this.obeyCloseEvents = false;
      this.additionalCloseKeyCodes = [];
      this.onKeyDown = this.onKeyDown.bind(this);
      this.onKeyPress = this.onKeyPress.bind(this);
      this.onDocumentClick = this.onDocumentClick.bind(this);
      this.addEventListener('visibleChange',
          Overlay.prototype.onVisibleChange_.bind(this), true);
      this.obeyCloseEvents = true;
    },

    get visible() {
      return this.visible_;
    },

    set visible(newValue) {
      base.setPropertyAndDispatchChange(this, 'visible', newValue);
    },

    get obeyCloseEvents() {
      return this.obeyCloseEvents_;
    },

    set obeyCloseEvents(newValue) {
      base.setPropertyAndDispatchChange(this, 'obeyCloseEvents', newValue);
      var overlayRoot = this.ownerDocument.querySelector('.overlay-root');
      // Currently the toolbar only has the close button.
      overlayRoot.toggleToolbar(newValue);
    },

    get toolbar() {
      return this.ownerDocument.querySelector('.overlay-root .tool-bar');
    },

    onVisibleChange_: function() {
      var overlayRoot = this.ownerDocument.querySelector('.overlay-root');
      if (this.visible) {
        overlayRoot.setAttribute('visible', 'visible');
        overlayRoot.showOverlay(this);
        document.addEventListener('keydown', this.onKeyDown, true);
        document.addEventListener('keypress', this.onKeyPress, true);
        document.addEventListener('click', this.onDocumentClick, true);
      } else {
        overlayRoot.removeAttribute('visible');
        document.removeEventListener('keydown', this.onKeyDown, true);
        document.removeEventListener('keypress', this.onKeyPress, true);
        document.removeEventListener('click', this.onDocumentClick, true);
        overlayRoot.hideOverlay(this);
      }
    },

    onKeyDown: function(e) {
      if (!this.obeyCloseEvents)
        return;

      if (e.keyCode == 27) {  // escape
        this.visible = false;
        e.preventDefault();
        return;
      }
    },

    onKeyPress: function(e) {
      if (!this.obeyCloseEvents)
        return;

      for (var i = 0; i < this.additionalCloseKeyCodes.length; i++) {
        if (e.keyCode == this.additionalCloseKeyCodes[i]) {
          this.visible = false;
          e.preventDefault();
          return;
        }
      }
    },

    onDocumentClick: function(e) {
      if (!this.obeyCloseEvents)
        return;
      var target = e.target;
      while (target !== null) {
        if (target === this)
          return;
        target = target.parentNode;
      }
      this.visible = false;
      e.preventDefault();
      return;
    }

  };

  return {
    Overlay: Overlay
  };
});