Source: ui/overlay.reel/overlay.js

/*global require, exports, console, MontageElement */

/**
 * @module "ui/overlay.reel"
 */
var Component = require("../component").Component,
    KeyComposer = require("../../composer/key-composer").KeyComposer,
    PressComposer = require("../../composer/press-composer").PressComposer,
    defaultEventManager = require("../../core/event/event-manager").defaultEventManager;

var CLASS_PREFIX = "montage-Overlay",
    VISIBLE_CLASS_NAME = CLASS_PREFIX + "--visible";

/**
 *
 * @class Overlay
 * @extends Component
 */
var Overlay = exports.Overlay = Component.specialize( /** @lends Overlay.prototype # */ {

    /**
     * Dispatched when the user dismiss the overlay by clicking outside of it.
     * @event dismiss
     * @memberof Overlay
     * @param {Event} event
     */
    __pressComposer: {
        value: null
    },

    _pressComposer: {
        get: function () {
            if (!this.__pressComposer) {
                this.__pressComposer = new PressComposer();
                this.__pressComposer.delegate = this;
                this.addComposerForElement(this._pressComposer, this.element.ownerDocument);
            }

            return this.__pressComposer;
        }
    },

    __keyComposer: {
        value: null
    },

    _keyComposer: {
        get: function () {
            if (!this.__keyComposer) {
                this.__keyComposer = new KeyComposer();
                this.__keyComposer.keys = "escape";
                this.__keyComposer.identifier = "escape";
                this.addComposerForElement(this.__keyComposer, this.element.ownerDocument.defaultView);
            }

            return this.__keyComposer;
        }
    },

    _anchor: {
        value: null
    },

    /**
     * The anchor element or component to display the overlay next to.
     */
    anchor: {
        set: function (value) {
            this._anchor = value;
            this.needsDraw = true;
        },
        get: function () {
            return this._anchor;
        }
    },

    _position: {
        value: null
    },

    /**
     * Where to position the overlay in the screen.
     * @type {{left: number, top: number}}
     */
    position: {
        set: function (value) {
            this._position = value;
            this.needsDraw = true;
        },
        get: function () {
            return this._position;
        }
    },

    // Value used to store the position where the overlay will be drawn.
    // This position is calculated at willDraw time and used at draw.
    _drawPosition: {
        value: null
    },

    _isShown: {
        value: false
    },

    isShown: {
        get: function () {
            return this._isShown;
        }
    },

    _resizeHandlerTimeout: {
        value: null
    },

    _previousActiveTarget: {
        value: null
    },

    /**
     * A delegate that can implement `willPositionOverlay` and/or
     * `shouldDismissOverlay`.
     *
     * - `willPositionOverlay(overlay, calculatedPosition)` is called when the
     *   overlay is being shown, and should return an object with `top` and
     *   `left` properties.
     * - `shouldDismissOverlay(overlay, target, event)` is called when the user
     *   clicks outside of the overlay or presses escape inside the overlay.
     *   Usually this will hide the overlay. Return `true` to hide the overlay,
     *   or `false` to leave the overlay visible.
     * @type {Object}
     */
    delegate: {
        value: null
    },

    _dismissOnExternalInteraction: {
        value: true
    },

    dismissOnExternalInteraction: {
        set: function (value) {
            if (value !== this._dismissOnExternalInteraction) {
                this._dismissOnExternalInteraction = value;

                if (value) {
                    this._pressComposer.addEventListener("pressStart", this, false);
                } else {
                    this._pressComposer.removeEventListener("pressStart", this, false);
                }
            }
        },
        get: function () {
            return this._dismissOnExternalInteraction;
        }
    },

    enterDocument: {
        value: function (firstTime) {
            if (firstTime) {
                // Need to move the element to be a child of the document to
                // escape possible offset parent container.
                this.element.ownerDocument.body.appendChild(this.element);
                this.attachToParentComponent();
            }
        }
    },
    

    /**
     * Show the overlay. The overlay is displayed at the position determined by
     * the following conditions:
     *
     * 1. If a delegate is provided and the willPositionOverlay function is
     *    implemented, the position is always determined by the delegate.
     * 2. If "position" is set, the overlay is always displayed at this
     *    location.
     * 3. If an anchor is set, the overlay is displayed below the anchor.
     * 4. If no positional hints are provided, the overlay is displayed at the
     *    center of the screen.
     *
     * FIXME: We have to add key events on both this component and the keyComposer
     * because of a bug in KeyComposer.
     */
    show: {
        value: function () {
            if (!this._isShown) {
                if (this.isModal) {
                    this._previousActiveTarget = defaultEventManager.activeTarget;
                    defaultEventManager.activeTarget = this;
                    if (defaultEventManager.activeTarget !== this) {
                        console.warn("Overlay " + this.identifier + " can't become the active target because ", defaultEventManager.activeTarget, " didn't surrender it.");
                        return;
                    }
                }

                this.attachToParentComponent();
                this.classList.add(VISIBLE_CLASS_NAME);
                this.loadComposer(this._pressComposer);
                this.loadComposer(this._keyComposer);
                this._isShown = true;
                this.needsDraw = true;

                this._keyComposer.addEventListener("keyPress", this, false);
                this.element.ownerDocument.defaultView.addEventListener("resize", this);

                if (this._dismissOnExternalInteraction) {
                    this._pressComposer.addEventListener("pressStart", this, false);
                }
            }
        }
    },

    hide: {
        value: function () {
            if (this._isShown) {
                // detachFromParentComponent happens at didDraw
                this.classList.remove(VISIBLE_CLASS_NAME);
                this.unloadComposer(this._pressComposer);
                this.unloadComposer(this._keyComposer);
                this._isShown = false;
                this.needsDraw = true;

                if (this.isModal) {
                    defaultEventManager.activeTarget = this._previousActiveTarget;
                }

                this._keyComposer.removeEventListener("keyPress", this, false);
                this.element.ownerDocument.defaultView.removeEventListener("resize", this);

                if (this._dismissOnExternalInteraction) {
                    this._pressComposer.removeEventListener("pressStart", this, false);
                }
            }
        }
    },

    isModal: {
        value: true
    },

    shouldComposerSurrenderPointerToComponent: {
        value: function (composer, pointer, component) {
            if (component && component.element && !this.element.contains(component.element)) {
                this.hide();
            }

            return true;
        }
    },

    /**
     * The overlay should only surrender focus if it is hidden, non-modal, or
     * if the other component is one of its descendants.
     */
    surrendersActiveTarget: {
        value: function (candidateActiveTarget) {
            var response = !(this.isModal && this.isShown),
                delegateResponse;

            if (!response && candidateActiveTarget && candidateActiveTarget.element) {
                response = this.element.contains(candidateActiveTarget.element);
            }

            delegateResponse = this.callDelegateMethod(
                "overlayShouldDismissOnSurrenderActiveTarget", this, candidateActiveTarget, response
            );

            return delegateResponse !== void 0 ? delegateResponse : response;
        }
    },

    // Event handlers

    handleResize: {
        value: function () {
            if (this.isShown) {
                this.needsDraw = true;
            }
        }
    },

    handlePressStart: {
        value: function (event) {
            if (!this.element.contains(event.targetElement)) {
                this.dismissOverlay(event);
            }
        }
    },

    handleKeyPress: {
        value: function (event) {
            if (event.identifier === "escape") {
                this.dismissOverlay(event);
            }
        }
    },

    /**
     * User event has requested that we dismiss the overlay. Give the delegate
     * an opportunity to prevent it. Returns whether the overlay was hidden.
     */
    dismissOverlay: {
        value: function (event) {
            var shouldDismissOverlay = false;
            if (this._isShown) {
                shouldDismissOverlay = this.callDelegateMethod("shouldDismissOverlay", this, event.targetElement, event.type);

                if (shouldDismissOverlay === void 0 || shouldDismissOverlay) {
                    this.hide();
                    this._dispatchDismissEvent();
                }
            }

            return shouldDismissOverlay;
        }
    },

    // Draw

    willDraw: {
        value: function () {
            if (this._isShown) {
                this._calculatePosition();

            } else {
                this.callDelegateMethod("didHideOverlay", this);
            }
        }
    },

    draw: {
        value: function () {
            if (this._isShown) {
                var position = this._drawPosition;

                this.element.style.top = position.top + "px";
                this.element.style.left = position.left + "px";
                this.element.style.visibility = "visible";

                this.callDelegateMethod("didShowOverlay", this);

            } else {
                this.element.style.visibility = "hidden";
            }
        }
    },

    didDraw: {
        value: function () {
            if (!this._isShown) {
                this.detachFromParentComponent();
            }
        }
    },

    _calculatePosition: {
        value: function () {
            var position,
                delegatePosition;

            if (this.position) {
                position = this.position;
            } else if (this.anchor) {
                position = this._calculateAnchorPosition();
            } else {
                position = this._calculateCenteredPosition();
            }

            delegatePosition = this.callDelegateMethod("willPositionOverlay", this, position);

            if (delegatePosition) {
                position = delegatePosition;
            }

            this._drawPosition = position;
        }
    },

    _calculateAnchorPosition: {
        value: function () {
            var anchor = this.anchor,
                width = this.element.offsetWidth,
                anchorPosition = anchor.getBoundingClientRect(),
                anchorHeight = anchor.offsetHeight || 0,
                anchorWidth = anchor.offsetWidth || 0,
                position;

            position = {
                top: anchorPosition.top + anchorHeight,
                left: anchorPosition.left + (anchorWidth / 2) - (width / 2)
            };

            if (position.left < 0) {
                position.left = 0;
            }

            return position;
        }
    },

    _calculateCenteredPosition: {
        value: function () {
            var defaultView = this.element.ownerDocument.defaultView,
                viewportHeight = defaultView.innerHeight,
                viewportWidth = defaultView.innerWidth,
                height = this.element.offsetHeight,
                width = this.element.offsetWidth;

            return {
                top: (viewportHeight / 2 - (height / 2)),
                left: (viewportWidth / 2 - (width / 2))
            };
        }
    },

    _dispatchDismissEvent: {
        value: function () {
            var dismissEvent = document.createEvent("CustomEvent");

            dismissEvent.initCustomEvent("dismiss", true, true, null);

            this.dispatchEvent(dismissEvent);
        }
    }

});

if (window.MontageElement) {
    MontageElement.define("montage-overlay", Overlay);
}