API Docs for: 0.5.1
Show:

File: addon/components/exp-player/component.js

import Ember from 'ember';
import layout from './template';

import FullScreen from '../../mixins/full-screen';
import ExperimentParser from '../../utils/parse-experiment';

let {
    $
} = Ember;

/**
 * @module exp-player
 * @submodule components
 */

/**
 * Experiment player: a component that renders a series of frames that define an experiment
 *
 * Sample usage:
 * ```handlebars
 * {{exp-player
 *   experiment=experiment
 *   session=session
 *   pastSessions=pastSessions
 *   saveHandler=(action 'saveSession')
 *   frameIndex=0
 *   fullScreenElementId='expContainer'}}
 * ```
 * @class ExpPlayer
 */
export default Ember.Component.extend(FullScreen, {
    layout: layout,

    experiment: null, // Experiment model
    session: null,
    pastSessions: null,
    frames: null,
    conditions: null,

    frameIndex: 0, // Index of the currently active frame
    framePage: 0, // Index of the currently visible page within a frame

    displayFullscreen: false,
    fullScreenElementId: 'experiment-player',

    allowExit: false,
    hasAttemptedExit: false,

    // Any additional properties we might wish to pass from the player to individual frames. Allows passing of arbitrary config
    // by individual consuming applications to suit custom needs.
    extra: {},

    /**
     * The message to display in the early exit modal. Newer browsers may not respect this message.
     * @property {String|null} messageEarlyExitModal
     */
    messageEarlyExitModal: 'Are you sure you want to leave this page? You may lose unsaved data.',

    /**
     * Customize what happens when the user exits the page
     * @method beforeUnload
     * @param {event} event The event to be handled
     * @return {String|null} If string is provided, triggers a modal to confirm user wants to leave page
     */
    beforeUnload(event) {
        if (!this.get('allowExit')) {
            this.set('hasAttemptedExit', true);
            this.send('exitFullscreen');

            // Log that the user attempted to leave early, via browser navigation.
            // There is no guarantee that the server request to save this event will finish before exit completed;
            //   we are limited in our ability to prevent willful exits
            this.send('setGlobalTimeEvent', 'exitEarly', {
                exitType: 'browserNavigationAttempt', // Page navigation, closed browser, etc
                lastPageSeen: this.get('frameIndex')
            });
            //Ensure sync - try to force save to finish before exit
            Ember.run(() => this.get('session').save());

            // Then attempt to warn the user and exit
            // Newer browsers will ignore the custom message below. See https://bugs.chromium.org/p/chromium/issues/detail?id=587940
            const message = this.get('messageEarlyExitModal');
            event.returnValue = message;
            return message;
        }
        return null;
    },

    _registerHandlers() {
        $(window).on('beforeunload', this.beforeUnload.bind(this));
    },
    _removeHandlers() {
        $(window).off('beforeunload');
    },
    onFrameIndexChange: Ember.observer('frameIndex', function() {
        var max = this.get('frames.length') - 1;
        var frameIndex = this.get('frameIndex');
        if (frameIndex === max) {
            this._removeHandlers();
        }
    }),
    willDestroy() {
        this._super(...arguments);
        this._removeHandlers();
    },

    init: function() {
        this._super(...arguments);
        this._registerHandlers();

        var parser = new ExperimentParser({
            structure: this.get('experiment.structure'),
            pastSessions: this.get('pastSessions').toArray()
        });
        var [frameConfigs, conditions] = parser.parse();
        this.set('frames', frameConfigs); // When player loads, convert structure to list of frames
        this.set('displayFullscreen', this.get('experiment.displayFullscreen') || false); // Choose whether to display this experiment fullscreen (default false)

        var session = this.get('session');
        session.set('conditions', conditions);
        session.save();
    },

    currentFrameConfig: Ember.computed('frames', 'frameIndex', function() {
        var frames = this.get('frames') || [];
        var frameIndex = this.get('frameIndex');
        return frames[frameIndex];
    }),

    _currentFrameTemplate: null,
    currentFrameTemplate: Ember.computed('currentFrameConfig', '_currentFrameTemplate', function() {
        var currentFrameTemplate = this.get('_currentFrameTemplate');
        if (currentFrameTemplate) {
            return currentFrameTemplate;
        }

        var currentFrameConfig = this.get('currentFrameConfig');
        var componentName = `${currentFrameConfig.kind}`;

        if (!Ember.getOwner(this).lookup(`component:${componentName}`)) {
            console.warn(`No component named ${componentName} is registered.`);
        }
        return componentName;
    }),

    currentFrameContext: Ember.computed('pastSessions', function() {
        return {
            pastSessions: this.get('pastSessions')
        };
    }),

    _transition() {
        Ember.run(() => {
            this.set('_currentFrameTemplate', 'exp-blank');
        });
        this.set('_currentFrameTemplate', null);
    },
    _exit() {
        this.send('sessionCompleted');
        this.get('session').save().then(() => window.location = this.get('experiment.exitUrl') || '/');
    },

    actions: {
        sessionCompleted() {
            this.get('session').set('completed', true);
        },

        setGlobalTimeEvent(eventName, extra) {
            // Set a timing event not tied to any one frame
            let curTime = new Date();
            let eventData = {
                eventType: eventName,
                timestamp: curTime.toISOString()
            };
            Ember.merge(eventData, extra || {});
            let session = this.get('session');
            session.get('globalEventTimings').pushObject(eventData);
        },

        saveFrame(frameId, frameData) {
            // Save the data from a completed frame to the session data item
            this.get('session.sequence').push(frameId);
            this.get('session.expData')[frameId] = frameData;
            return this.get('session').save();
        },

        next() {
            var frameIndex = this.get('frameIndex');
            if (frameIndex < (this.get('frames').length - 1)) {
                this._transition();
                this.set('frameIndex', frameIndex + 1);
                this.set('framePage', 0);
                return;
            }
            this._exit();
        },

        skipone() {
            var frameIndex = this.get('frameIndex');
            if (frameIndex < (this.get('frames').length - 2)) {
                this._transition();
                this.set('frameIndex', frameIndex + 2);
                return;
            }
            this._exit();
        },

        previous() {
            var frameIndex = this.get('frameIndex');
            if (frameIndex !== 0) {
                this._transition();
                this.set('frameIndex', frameIndex - 1);
            }
        },

        closeExitWarning() {
            this.set('hasAttemptedExit', false);
        },

        updateFramePage(framePage) {
            this.set('framePage', framePage);
        }
    }
});