Source: public/modules/core/controllers/master.client.controller.js

'use strict';

/**
 * In the demo application, the `MasterController` is persistent throughout
 * an experiment session and provides a number of 'session-wide' facilities.
 * These include restarting a session, exposing the {@link
 * Angular.TrialData|TrialData} service, changing the language, configuring
 * hot keys, showing alerts, and blacking out the screen.
 *
 * The hot keys are configured using the
 * [angular-hotkeys](https://github.com/chieffancypants/angular-hotkeys)
 * module. In particular, pressing `'d'` twice will toggle debugging mode. When
 * in debugging mode, the right arrow key will allow the user to advance to
 * the next slide. Also, in debugging mode, nothing blocks the user from
 * advancing through slides (e.g., the user can advance irrespective of
 * whether or not Max is ready).
 *
 * @class Angular.MasterController
 */

angular.module('core').controller(
    'MasterController',
    [
        '$scope',
        'TrialData',
        'hotkeys',
        'ExperimentManager',
        'gettextCatalog',
        '$state',
        '$timeout',
        '$log',
        '$http',
        '$rootScope',
        'OSC',  // Note that though OSC is not used here, requiring it is
                // necessary in order to globally handle messages to which no
                // one is subscribed
        function($scope, TrialData, hotkeys, ExperimentManager, gettextCatalog,
                 $state, $timeout, $log, $http, $rootScope, OSC) {

            $log.debug('Loading MasterController.');

            var thisController = this;


            /***** $scope Members *****/


            /**
             * The `MasterController`'s `$scope` object. All properties on
             * `$scope` are available to the view.
             *
             * @name $scope
             * @namespace
             * @instance
             * @memberof Angular.MasterController
             * @type {{}}
             */

            /**
             * Expose {@link Angular.TrialData#toJson|TrialData#toJson} on
             * `$scope`. Calling this method returns the result of calling
             * {@link Angular.TrialData#toJson|TrialData#toJson}.
             *
             * @function trialDataJson
             * @memberof Angular.MasterController#$scope
             * @instance
             * @type {{}}
             */
            $scope.trialDataJson = function() {
                return TrialData.toJson();
            };

            /**
             * Set the current language used for displaying content. Ensure
             * that you have translations available and compiled (see the
             * [README](index.html)).
             *
             * @function setLanguage
             * @memberof Angular.MasterController#$scope
             * @instance
             * @param {string} language The language code of the language to use
             * @return {undefined}
             */
            $scope.setLanguage = function(language) {

                $log.debug('MasterController setting language to \'' +
                    language + '\'.');

                gettextCatalog.setCurrentLanguage(language);
                TrialData.language(language);
            };

            /**
             * Return to the `'home'` state.
             *
             * @function startOver
             * @memberof Angular.MasterController#$scope
             * @instance
             * @return {undefined}
             */
            $scope.startOver = function() {

                $log.debug('MasterController starting over.');

                $state.go('home', {}, {reload: true});

                $scope.debugMode = false;

                $scope.alerts = [];

                thisController.setLanguageToDefault();
            };

            /**
             * Array for holding alert messages.
             *
             * @name alerts
             * @type {string[]}
             * @memberof Angular.MasterController#$scope
             * @instance
             */
            $scope.alerts = [];

            /**
             * Adds an alert to {@link
             * Angular.MasterController#$scope#alerts|$scope#alerts}, unless
             * it is already present.
             *
             * @function addAlert
             * @memberof Angular.MasterController#$scope
             * @instance
             * @param {{}} alert This object should have two properties:
             * `msg` and `type`. The value of the `msg` property should be
             * the alert `string`, and the value of the `type` property should
             * be a `string` indicating the alert type. Acceptable values
             * for alert types are: `'success'`, `'info'`, `'warning'`, and
             * `'danger'`.
             * @return {undefined}
             */
            $scope.addAlert = function(alert) {

                var errorExists = false;
                for (var i = 0; i < $scope.alerts.length; i++) {
                    if ($scope.alerts[i].msg === alert.msg &&
                        $scope.alerts[i].type === alert.type) {

                        $log.debug('Not adding ' + alert.msg + ' to visible' +
                            ' alerts, as it already exists in $scope.alerts.');
                        errorExists = true;
                        break;
                    }
                }

                if (!errorExists) {
                    $log.debug('Adding \'' + alert.msg + '\' to' +
                        ' $scope.alerts.');
                    $scope.alerts.push(alert);
                }
            };

            /**
             * Closes the alert in {@link
             * Angular.MasterController#$scope#alerts|$scope#alerts} at the
             * specified index.
             *
             * @function closeAlert
             * @memberof Angular.MasterController#$scope
             * @instance
             * @param {number} index The index in the {@link
                * Angular.MasterController#$scope#alerts|$scope#alerts} array
             * of the alert to close
             * @return {undefined}
             */
            $scope.closeAlert = function(index) {
                $log.debug('Closing alert at index ' + index + ': \'' +
                    $scope.alerts[index].msg + '\'');
                $scope.alerts.splice(index, 1);
            };

            /**
             * Adds a generic error alert to {@link
             * Angular.MasterController#$scope#alerts|$scope#alerts}. The
             * alert is a `'danger'`-type alert, and in the demo app
             * displays the message `'There seems to be a problem. Please
             * contact a mediator for assistance.'`
             *
             * @function addGenericErrorAlert
             * @memberof Angular.MasterController#$scope
             * @instance
             * @return {undefined}
             */
            $scope.addGenericErrorAlert = function() {
                $log.debug('Adding generic error alert.');
                $scope.addAlert({
                    type: 'danger',
                    msg: 'There seems to be a problem. Please contact a ' +
                    'mediator for assistance.'
                });
            };

            /**
             * A flag indicating whether or not the 'blackout' class should
             * be added to the body of the visible page. When this is
             * `true`, the page is blacked out.
             *
             * @name blackoutClass
             * @memberof Angular.MasterController#$scope
             * @instance
             * @type {boolean}
             */
            $scope.blackoutClass = false;

            /**
             * A convenience method for setting {@link
             * Angular.MasterController#$scope#blackoutClass|$scope#blackoutClass}
             * to `true` (and blacking out the page).
             *
             * @function hideBody
             * @memberof Angular.MasterController#$scope
             * @instance
             * @return {undefined}
             */
            $scope.hideBody = function() {
                $log.debug('Hiding body.');
                $scope.blackoutClass = true;
            };

            /**
             * A convenience method for setting {@link
             * Angular.MasterController#$scope#blackoutClass|$scope#blackoutClass}
             * to `false` (and reversing the blacking out of the page).
             *
             * @function showBody
             * @memberof Angular.MasterController#$scope
             * @instance
             * @return {undefined}
             */
            $scope.showBody = function() {
                $log.debug('Showing body.');
                $scope.blackoutClass = false;
            };

            /**
             * A convenience method for toggling the value of {@link
             * Angular.MasterController#$scope#blackoutClass|$scope#blackoutClass}.
             *
             * @function toggleBodyVisibility
             * @memberof Angular.MasterController#$scope
             * @instance
             * @return {undefined}
             */
            $scope.toggleBodyVisibility = function() {
                $log.debug('Toggling body visibility.');
                $scope.blackoutClass = !$scope.blackoutClass;
            };

            /**
             * Resets the inactivity timeout, advance the slide by calling
             * {@link
             * Angular.ExperimentManager#advanceSlide|ExperimentManager#advanceSlide}.,
             * and clear all current alerts.
             *
             * @function advanceSlide
             * @memberof Angular.MasterController#$scope
             * @instance
             * @return {undefined}
             */
            $scope.advanceSlide = function() {

                $log.debug('MasterController advancing slide.');

                // Reset the inactivity timeout
                thisController.resetInactivityTimeout();

                // Clear all current alerts
                $scope.alerts = [];

                // Wrap ExperimentManager's advance slide
                ExperimentManager.advanceSlide();
            };

            /**
             * Calls {@link
             * Angular.ExperimentManager#advanceSlide|ExperimentManager#advanceSlide}
             * if {@link Angular.ExperimentManager#$scope#debugMode} is `true`.
             *
             * @function handleRightArrow
             * @memberof Angular.MasterController#$scope
             * @instance
             * @return {undefined}
             */
            $scope.handleRightArrow = function handleRightArrowFn() {
                if ($scope.debugMode) {
                    $scope.advanceSlide();
                }
            };

            /**
             * Indicates whether or not debugging mode is enabled.
             *
             * @name debugMode
             * @memberof Angular.MasterController#$scope
             * @instance
             * @type {boolean}
             */
            $scope.debugMode = false;

            /**
             * Toggles debugging mode. If debugging mode is enabled, an
             * alert is posted to the view.
             *
             * @function toggleDebugMode
             * @instance
             * @memberof Angular.MasterController#$scope
             * @return {undefined}
             */
            $scope.toggleDebugMode = function() {

                $scope.debugMode = !$scope.debugMode;
                var alertMessage = 'Debug mode has been ';
                if ($scope.debugMode) {
                    $log.info('Debug mode enabled.');
                    alertMessage += 'enabled.';
                } else {
                    $log.info('Debug mode disabled.');
                    alertMessage += 'disabled.';
                }

                $scope.addAlert({type: 'info', msg: alertMessage});
            };


            /***** Inner Members *****/


            /**
             * Sets the language to the default language as specified in the
             * {@link Node.module:CustomConfiguration~customConfiguration~defaultLanguage|CustomConfiguration}
             * module.
             *
             * @function setLanguageToDefault
             * @memberof Angular.MasterController
             * @instance
             * @return {undefined}
             */
            this.setLanguageToDefault = function() {
                $http.get('/api/config')
                    .then(function(response) {

                        if (response.data.hasOwnProperty('defaultLanguage')) {
                            $scope.setLanguage(response.data.defaultLanguage);
                        } else {
                            var message = 'A default language was not ' +
                                'provided by the server.';
                            $log.error(message, response);
                            throw new Error(message);
                        }
                    })
                    .catch(function(response) {
                        var message = 'There was a problem retrieving the' +
                            ' configuration from the server.';
                        $log.error(message, response);
                        throw new Error(message);
                    });
            };

            /**
             * Resets the inactivity timeout.
             *
             * The demo application ships with an inactivity timeout. If
             * after five minutes no interaction with the experiment has
             * occurred, this timeout fires and calls {@link
             * Angular.MasterController#$scope#startOver|$scope#startOver}
             * method.
             *
             * @function resetInactivityTimeout
             * @memberof Angular.MasterController
             * @instance
             * @return {undefined}
             */
            this.resetInactivityTimeout = function() {

                $log.debug('MasterController resetting inactivity timeout.');

                $timeout.cancel(thisController.inactivityTimeout);
                thisController.inactivityTimeout = $timeout(
                    thisController.startOver,
                    5 * 60 * 1000
                );
            };


            /***** Initialization Logic *****/


            // Setup hot keys
            $log.debug('Setting up hot keys.');

            hotkeys.add({
                combo: 'd d',
                description: 'Toggle debug mode',
                callback: $scope.toggleDebugMode
            });

            hotkeys.add({
                combo: 'right',
                description: 'Advance slide',
                callback: $scope.handleRightArrow
            });

            // Scroll to the top of the page on new state
            $rootScope.$on('$stateChangeSuccess', function() {
                document.body.scrollTop =
                    document.documentElement.scrollTop = 0;
            });

            this.setLanguageToDefault();
            this.resetInactivityTimeout();
        }
    ]);