File:ContainerClientPlugin.js

/**
 * @module Container Client
 * @namespace springroll
 */
(function(undefined)
{
	// Include classes
	var ApplicationPlugin = include('springroll.ApplicationPlugin'),
		PageVisibility = include('springroll.PageVisibility'),
		UserData = include('springroll.UserData'),
		Bellhop = include('Bellhop');

	/**
	 * @class Application
	 */
	var plugin = new ApplicationPlugin(200);

	// Init the animator
	plugin.setup = function()
	{
		var options = this.options;

		/**
		 * Send a message to let the site know that this has
		 * been loaded, if the site is there
		 * @property {Bellhop} container
		 */
		var container = this.container = new Bellhop();
		container.connect();

		/**
		 * The API for saving user data, default is to save
		 * data to the container, if not connected, it will
		 * save user data to local cookies
		 * @property {springroll.UserData} userData
		 */
		this.userData = new UserData(container);

		/**
		 * This option tells the container to always keep focus on the iframe even
		 * when the focus is lost. This is useful mostly if your Application
		 * requires keyboard input.
		 * @property {Boolean} options.keepFocus
		 */
		options.add('keepFocus', false)
			.on('keepFocus', function(data)
			{
				container.send('keepFocus', data);
			});

		// Pass along preloading progress
		this.on('progress', function(e)
		{
			this.container.send('progress', e);
		});

		// When the preloading is done
		this.once('beforeInit', function()
		{
			container.send('loaded');
		});

		// Send the first event
		container.send('loading');

		/**
		 * The default play-mode for the application is continuous, if the application is
		 * running as part of a sequence is it considered in "single play" mode
		 * and the application will therefore close itself.
		 * @property {Boolean} singlePlay
		 * @readOnly
		 * @default false
		 */
		this.singlePlay = false;

		/**
		 * The optional play options to use if the application is played in "single play"
		 * mode. These options are passed from the application container to specify
		 * options that are used for this single play session. For instance,
		 * if you want the single play to focus on a certain level or curriculum
		 * such as `{ "shape": "square" }`
		 * @property {Object} playOptions
		 * @readOnly
		 */
		this.playOptions = {};

		/**
		 * When a application is in singlePlay mode it will end.
		 * It's unnecessary to check `if (this.singlePlay)` just
		 * call the method and it will end the application if it can.
		 * @method singlePlayEnd
		 * @return {Boolean} If endGame is called
		 */
		this.singlePlayEnd = function()
		{
			if (this.singlePlay)
			{
				this.endGame();
				return true;
			}
			return false;
		};

		/**
		 * Manually close the application, this can happen when playing through once
		 * @method endGame
		 * @param {String} [exitType='game_completed'] The type of exit
		 */
		this.endGame = function(exitType)
		{
			this.trigger('endGame', exitType || 'game_completed');
			this.destroy();
		};

		// Dispatch the features
		this.once('beforeInit', function()
		{
			var hasSound = !!this.sound;

			// Add the features that are enabled
			this.container.send('features',
			{
				sound: hasSound,
				hints: !!this.hints,
				music: hasSound && this.sound.contextExists('music'),
				vo: hasSound && this.sound.contextExists('vo'),
				sfx: hasSound && this.sound.contextExists('sfx'),
				captions: !!this.captions,
				disablePause: !!this.options.disablePause
			});
		});

		if (container.supported)
		{
			container.fetch('singlePlay', onSinglePlay.bind(this));
			container.fetch('playOptions', onPlayOptions.bind(this));
		}

		// Handle errors gracefully
		window.onerror = onWindowError.bind(this);

		// Listen when the browser closes or redirects
		window.onunload = window.onbeforeunload = onWindowUnload.bind(this);
	};

	/**
	 * Handler for when a window is unloaded
	 * @method  onWindowUnload
	 * @private
	 */
	var onWindowUnload = function()
	{
		// Remove listener to not trigger twice
		window.onunload = window.onbeforeunload = null;
		this.endGame('left_site');
		return undefined;
	};

	/**
	 * Handle the window uncaught errors with the container
	 * @method  onWindowError
	 * @private
	 * @param  {Error} error Uncaught Error
	 */
	var onWindowError = function(error)
	{
		// If the container is supported
		// then handle the errors and pass to the container
		if (this.container.supported)
		{
			if (DEBUG && window.console) console.error(error);
			this.container.send('localError', String(error));
			return RELEASE; // handle gracefully in release mode
		}
	};

	// Check for application name
	plugin.preload = function(done)
	{
		if (!this.name)
		{
			if (DEBUG)
			{
				throw "Application name is empty, please add a Application option of 'name'";
			}
			else
			{
				throw "Application name is empty";
			}
		}

		// Connect the user data to container
		this.userData.id = this.name;

		// Merge the container options with the current
		// application options
		if (this.container.supported)
		{
			//Setup the container listeners for site soundMute and captionsMute events
			this.container.on(
			{
				soundMuted: onSoundMuted.bind(this),
				captionsMuted: onCaptionsMuted.bind(this),
				musicMuted: onContextMuted.bind(this, 'music'),
				voMuted: onContextMuted.bind(this, 'vo'),
				sfxMuted: onContextMuted.bind(this, 'sfx'),
				captionsStyles: onCaptionsStyles.bind(this),
				pause: onPause.bind(this),
				close: onClose.bind(this)
			});

			// Turn off the page hide and show auto pausing the App
			this.options.autoPause = false;

			//handle detecting and sending blur/focus events
			var container = this.container;
			this._pageVisibility = new PageVisibility(
				container.send.bind(container, 'focus', true),
				container.send.bind(container, 'focus', false)
			);
		}
		done();
	};

	/**
	 * When the container pauses the application
	 * @method onPause
	 * @private
	 * @param {event} e The Bellhop event
	 */
	var onPause = function(e)
	{
		var paused = !!e.data;
		// container pause events are also considered "autoPause" events
		// event if the event was fired by the container's pauseButton
		this.autoPaused = paused;
		this.enabled = !paused;
	};

	/**
	 * Handler when the sound is muted
	 * @method onSoundMuted
	 * @private
	 * @param {Event} e The bellhop event
	 */
	var onSoundMuted = function(e)
	{
		if (this.sound)
		{
			this.sound.muteAll = !!e.data;
		}
	};

	/**
	 * Handler when the captions are muted
	 * @method onCaptionsMuted
	 * @private
	 * @param {Event} e The bellhop event
	 */
	var onCaptionsMuted = function(e)
	{
		if (this.captions)
		{
			this.captions.mute = !!e.data;
		}
	};

	/**
	 * Handler when the context is muted
	 * @method onContextMuted
	 * @private
	 * @param {String} context The name of the sound context
	 * @param {Event} e The bellhop event
	 */
	var onContextMuted = function(context, e)
	{
		if (this.sound)
		{
			this.sound.setContextMute(context, !!e.data);
		}
	};

	/**
	 * The captions style is being set
	 * @method onCaptionsStyles
	 * @private
	 * @param {Event} e The bellhop event
	 */
	var onCaptionsStyles = function(e)
	{
		var styles = e.data;
		var captions = this.captions ||
		{};
		var textField = captions.textField || null;

		// Make sure we have a text field and a DOM object
		if (textField && textField.nodeName)
		{
			textField.className = "size-" + styles.size + " " +
				"bg-" + styles.background + " " +
				"color-" + styles.color + " " +
				"edge-" + styles.edge + " " +
				"font-" + styles.font + " " +
				"align-" + styles.align;
		}
	};

	/**
	 * Handler when a application enters single play mode
	 * @method onPlayOptions
	 * @private
	 * @param {event} e The Bellhop event
	 */
	var onPlayOptions = function(e)
	{
		Object.merge(this.playOptions, e.data ||
		{});
	};

	/**
	 * Handler when a application enters single play mode
	 * @method onSinglePlay
	 * @private
	 * @param {event} e The Bellhop event
	 */
	var onSinglePlay = function(e)
	{
		this.singlePlay = !!e.data;
	};

	/**
	 * Game container requests closing the application
	 * @method onClose
	 * @private
	 */
	var onClose = function()
	{
		this.endGame('closed_container');
	};

	// Destroy the animator
	plugin.teardown = function()
	{
		window.onerror = null;

		if (this._pageVisibility)
		{
			this._pageVisibility.destroy();
			this._pageVisibility = null;
		}

		if (this.userData)
		{
			this.userData.destroy();
			this.userData = null;
		}

		// Send the end application event to the container
		if (this.container)
		{
			this.container.send('endGame');
			this.container.destroy();
			this.container = null;
		}
	};

}());