sequence.js 12.9 KB
/*
 * ADOBE CONFIDENTIAL
 *
 * Copyright 2015 Adobe Systems Incorporated
 * All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and may be covered by U.S. and Foreign Patents,
 * patents in process, and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 */
(function($, player) {
    'use strict';

    var KNOWN_EVENTS = [
        'sequence-start',
        'sequence-stop',
        'sequence-resume',
        'sequence-pause',
        'sequence-set-strategy',
        'sequence-element-update'
    ];

    var MESSAGES = {
        SEQUENCE_INITIALIZED: 'sequence-state-initialized',
        SEQUENCE_STARTED: 'sequence-state-started',
        SEQUENCE_STOPPED: 'sequence-state-stopped',
        SEQUENCE_PAUSED: 'sequence-state-paused',
        SEQUENCE_RESUMED: 'sequence-state-resumed',
        SEQUENCE_CYCLE_COMPLETE: 'sequence-state-cycle-complete'
    };

    /**
     * The sequence manager handles showing the sequence elements following a specified strategy.
     *
     * @class
     *
     * @param {WindowIO}  windowIo                  A window io object to communicate with the parent frame
     * @param {HTMLElement|jQuery}  el              The sequence element
     * @param {Strategy|String}     [strategy]      The strategy to use to cycle through the elements
     * @param {Transition|String}   [transition]    The transition to apply between the elements
     *
     * @return {Sequence} The current sequence
     */
    var Sequence = function(windowIo, el, strategy, transition) {

        /**
         * Initialize the sequence.
         *
         * @return {Sequence} The current sequence
         */
        this._init = function() {
            this.$el = $(el);
            this.el = this.$el.get(0);
            this._wio = windowIo;
            this._wioEventHandlers = [];

            this.$el.data('screens-sequence', this);
            this.disabled = true;

            this.currentItemStartTime = 0;
            this.currentItemDuration = 0;

            // Set the strategy
            this.setStrategy(strategy || this.$el.data('strategy') || 'normal');

            // Set the transition
            this.transition = $.isFunction(transition) && transition
                || new player.transitions[transition || this.$el.data('transition') || 'normal'](this.strategy);

            // Attach events
            this._bindEvents();

            // send message to parent that sequence is initialized
            // give info on the current sequence like number of items
            this._wio.postMessage(MESSAGES.SEQUENCE_INITIALIZED, {
                items: this.strategy.items().length
            }, 'parentSequence');

            return this;
        };

        /**
         * Creates a handler for the sequence events
         * over WindowIO
         *
         * @param  {String} eventName the event name
         * @return {Function} the event handler function
         */
        this._createWioMessageHandler = function(eventName) {
            var self = this;

            return function(payload, origin) {
                // origin is ignored by now
                self.$el.trigger(eventName, payload);
            };
        };

        /**
         * Bind events to the sequence.
         */
        this._bindEvents = function() {
            var handler;

            // Start the sequence on the `sequence-start` event.
            this.$el.on('sequence-start', (function(ev, data) {
                this.start(data);
            }).bind(this));

            // Pause the sequence on the `sequence-pause` event.
            this.$el.on('sequence-pause', (function() {
                this.pause();
            }).bind(this));

            // Resume the sequence on the `sequence-resume` event.
            this.$el.on('sequence-resume', (function() {
                this.resume();
            }).bind(this));

            // Stop the sequence on the `sequence-stop` event.
            this.$el.on('sequence-stop', (function() {
                this.stop();
            }).bind(this));

            // Restart the sequence on the `sequence-element-update` event.
            this.$el.on('sequence-element-update', (function(ev) {
                if (this.strategy.update && this.strategy.update()) {
                    console.log('Strategy updated. Restarting sequence.');
                    this.stop();
                    this.start();
                }
            }).bind(this));

            this.$el.on('sequence-set-strategy', (function(ev, payload) {
                if (payload && payload.strategy) {
                    this.setStrategy(payload.strategy);
                }
            }).bind(this));

            for (var i = 0; i < KNOWN_EVENTS.length; i++) {
                handler = this._wio.on(KNOWN_EVENTS[i], this._createWioMessageHandler(KNOWN_EVENTS[i]));

                // save handler so they can be removed later
                this._wioEventHandlers.push({
                    event: KNOWN_EVENTS[i],
                    handler: handler
                });
            }
        };

        /**
         * Destroy the sequence.
         */
        this._destroy = function() {
            this.$el.off(KNOWN_EVENTS.join(' '));
            for (var i = 0; i < this._wioEventHandlers.length; i++) {
                this._wio.removeListener(this._wioEventHandlers[i].event, this._wioEventHandlers.handler);
            }
            this._wio = null;
            this.$el.removeData('screens-sequence');
            this.$el = null;
            this.el = null;
            this.transition.destroy();
            this.transition = null;
            this.strategy.destroy();
            this.strategy = null;
        };

        return this._init();
    };

    /**
     * The strategy to use to cycle through the elements
     *
     * @param {Strategy|String} strategy the strategy to use in the sequence
     */
    Sequence.prototype.setStrategy = function(strategy) {
        this.strategy =
            $.isFunction(strategy) ? strategy :
            new player.strategies[strategy](this.$el);
    };

    /**
     * Start the sequence.
     *
     * @param {Object} data The startup data that includes the duration for which this sequence is to be played
     *
     * @return {Sequence|null} The current sequence, or null if the sequence is disabled
     */
    Sequence.prototype.start = function(data) {
        // Check if we have received information on how long this sequence is to be played
        if (data) {
            this.duration = data.duration;
        }
        if (!this.disabled) {
            return null;
        }
        this.disabled = false;
        this.next();
        this._wio.postMessage(MESSAGES.SEQUENCE_STARTED, {}, 'parentSequence');
        return this;
    };

    /**
     * Pause the sequence.
     *
     * @return {Sequence|null} The current sequence, or null if the sequence is disabled
     */
    Sequence.prototype.pause = function() {
        if (this.disabled) {
            return null;
        }
        this.disabled = true;
        window.clearTimeout(this.strategy.timeout);

        this.currentItemDuration -= new Date().getTime() - this.currentItemStartTime;
        this._wio.postMessage(MESSAGES.SEQUENCE_PAUSED, {}, 'parentSequence');
        return this;
    };

    /**
     * Resume the sequence.
     *
     * @return {Sequence|null} The current sequence, or null if the sequence is disabled
     */
    Sequence.prototype.resume = function() {
        if (!this.disabled) {
            return null;
        }
        this.disabled = false;

        this.currentItemStartTime = new Date().getTime();

        var remaining = this.currentItemStartTime + this.currentItemDuration - new Date().getTime();

        if (remaining > 0) {
            this.strategy.scheduleCurrent(this.next.bind(this), remaining);
        } else {
            this.next();
        }
        this._wio.postMessage(MESSAGES.SEQUENCE_RESUMED, {}, 'parentSequence');
        return this;
    };

    /**
     * Stop the sequence.
     *
     * @return {Sequence|null} The current sequence, or null if the sequence is disabled
     */
    Sequence.prototype.stop = function() {
        if (this.$currentItem) {
            this.$currentItem.trigger('sequence-element-stop');
            this.$currentItem.trigger('sequence-element-hide');
        }
        this.$currentItem = null;
        window.clearTimeout(this.strategy.timeout);
        this._wio.postMessage(MESSAGES.SEQUENCE_STOPPED, {}, 'parentSequence');
        if (this.disabled) {
            return null;
        }
        this.disabled = true;
        return this;
    };

    /**
     * Show the next element in the sequence.
     */
    Sequence.prototype.next = function() {
        if (this.disabled) {
            return;
        }

        // checks if the previous item was the last per cycle
        if (this.strategy.isCycleComplete(this.$currentItem)) {
            // If we are a subsequence and the parent sequence is waiting for us, we inform that we are done
            // If this subsequence has an associated duration, it will continue
            // until the parent timer runs outs and informs us its time to stop
            if (this._wio.targetWindows.parentSequence) {
                this._wio.postMessage(MESSAGES.SEQUENCE_CYCLE_COMPLETE, {}, 'parentSequence'); // inform that the cycle is complete
                if (this.duration === -1) {
                    // It appears that the event sequence-element-hide does not actually hide the item
                    // Instead the hide only occurs during transition. Therefore in situations where a subsequence
                    // stops and returns a message to its parent, the last played element remains unhidden
                    // This cause a brief flash of the last item when the subsequence is transitioned back later in the loop
                    if (this.$currentItem) {
                        this.$currentItem.hide();
                    }
                    return;
                }
            }
        }

        var next = this.strategy.next(this.$currentItem);
        if (!next) {
            return;
        }

        // if the current item is not initialized, set it to the first one
        var $current = this.$currentItem ? this.$currentItem : next.$item;

        // if the next item is not the same as the current one and if it's skipped, take the next one.
        while (next && next.$item.attr('data-item-skip')) {
            next = this.strategy.next(next.$item);
            if (next && next.$item.is($current)) {
                console.warn('All items in sequence are skipped. Not able to advance.');
                return;
            }
        }
        if (!next) { // no next element available
            return;
        }

        var $nextItem = next.$item;
        var transitionType = next.transitionType;
        var transitionDuration = next.transitionDuration;
        this.currentItemDuration = this.strategy.duration($nextItem);

        this.$currentItem && this.$currentItem.trigger('sequence-element-stop');
        var transition = this.transition;
        if (transitionType) {
            transition = new player.transitions[transitionType](this.strategy);
        }

        transition.execute(this.$currentItem, $nextItem, (function() {
            if (this.disabled) { // to avoid execution if sequence is stopped
                return;
            }
            this.$currentItem && this.$currentItem.trigger('sequence-element-hide');
            this.$currentItem = $nextItem;
            if (this.currentItemDuration > -1) {
                this.$currentItem.trigger('sequence-element-show');
                this.currentItemStartTime = new Date().getTime();
                this.strategy.scheduleCurrent(this.next.bind(this), this.currentItemDuration);
            } else {
                // "infinite" case, delegate transition to the sequence element
                var self = this;

                // when sequence element is done or triggers an error it must call this callback to transition to next element
                var transitionCallback = function() {
                    window.setTimeout(self.next.bind(self), 10, $nextItem);
                };
                this.$currentItem.trigger('sequence-element-show', [transitionCallback]);
                this.$currentItem.trigger('sequence-element-delegate-transition', [transitionCallback]);
            }
        }).bind(this), transitionDuration);
    };

    /**
     * Destroy the sequence.
     */
    Sequence.prototype.destroy = function() {
        this.stop();
        this._destroy();
    };

    window.ScreensPlayer.Sequence = Sequence;

}(window.jQuery, window.ScreensPlayer));