Current File : /home/jvzmxxx/wiki1/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js
/**
 * The Core timed Text interface object
 *
 * handles class mappings for:
 * 	menu display ( jquery.ui themeable )
 * 	timed text loading request
 *  timed text edit requests
 * 	timed text search & seek interface ( version 2 )
 *
 * @author: Michael Dale
 *
 */

( function( mw, $ ) {"use strict";

	// Merge in timed text related attributes:
	mw.mergeConfig( 'EmbedPlayer.SourceAttributes', [
  	   'srclang',
  	   'kind',
	   'label'
	]);

	/**
	 * Timed Text Object
	 * @param embedPlayer Host player for timedText interfaces
	 */
	mw.TimedText = function( embedPlayer ) {
		return this.init( embedPlayer );
	};

	mw.TimedText.prototype = {

		/**
		* Preferences config order is presently:
		* 1) user cookie
		* 2) defaults provided in this config var:
		*/
		config: {
			// Layout for basic "timedText" type can be 'ontop', 'off', 'below'
			'layout' : 'ontop',

			//Set the default local ( should be grabbed from the browser )
			'userLanguage' : mw.config.get( 'wgUserLanguage' ) || 'en',

			//Set the default kind of timedText to display ( un-categorized timed-text is by default "subtitles" )
			'userKind' : 'subtitles'
		},

		// The default display mode is 'ontop'
		defaultDisplayMode : 'ontop',

		// Save last layout mode
		lastLayout : 'ontop',

		// The bind prefix:
		bindPostFix: '.timedText',

		// Default options are empty
		options: {},

		/**
		 * The list of enabled sources
		 */
		enabledSources: [],

		// First loading flag - To set the layout at first load
		firstLoad: true,

		/**
		 * The current language key
		 */
		currentLangKey : null,

		/**
		 * The direction of the current language
		 */
		currentLangDir : null,

		/**
		 * Stores the last text string per kind to avoid dom checks for updated text
		 */
		prevText: [],

		/**
		* Text sources ( a set of textSource objects )
		*/
		textSources: [],

		/**
		* Valid "Track" categories
		*/
		validCategoriesKeys: [
			"CC",
			"SUB",
			"TAD",
			"KTV",
			"TIK",
			"AR",
			"NB",
			"META",
			"TRX",
			"LRC",
			"LIN",
			"CUE"
		],

		/**
		 * @constructor
		 * @param {Object} embedPlayer Host player for timedText interfaces
		 */
		init: function( embedPlayer ) {
			var _this = this;
			mw.log("TimedText: init() ");
			this.embedPlayer = embedPlayer;
			// don't display captions on native player:
			if( embedPlayer.useNativePlayerControls() ){
				return this;
			}

			// Load user preferences config:
			var preferenceConfig = $.cookie( 'TimedText.Preferences' );
			if( preferenceConfig !== "false" && preferenceConfig != null ) {
				this.config = JSON.parse(  preferenceConfig );
			}
			// remove any old bindings on change media:
			$( this.embedPlayer ).bind( 'onChangeMedia' + this.bindPostFix , function(){
				_this.destroy();
			});

			// Remove any old bindings before we add the current bindings:
			_this.destroy();

			// Add player bindings
			_this.addPlayerBindings();
			return this;
		},
		destroy: function(){
			// remove any old player bindings;
			$( this.embedPlayer ).unbind( this.bindPostFix );
			// Clear out enabled sources:
			this.enabledSources = [];
			// Clear out text sources:
			this.textSources = [];
		},
		/**
		 * Add timed text related player bindings
		 * @return
		 */
		addPlayerBindings: function(){
			var _this = this;
			var embedPlayer = this.embedPlayer;

			// Check for timed text support:
			_this.addInterface();

			$( embedPlayer ).bind( 'timeupdate' + this.bindPostFix, function( event, jEvent, id ) {
				// regain scope
				_this = $('#' + id)[0].timedText;
				// monitor text updates
				_this.monitor();
			} );

			$( embedPlayer ).bind( 'firstPlay' + this.bindPostFix, function(event, id ) {
				// regain scope
				_this = $('#' + id)[0].timedText;
				// Will load and setup timedText sources (if not loaded already loaded )
				_this.setupTextSources();
				// Hide the caption menu if presently displayed
				$( '#textMenuContainer_' + _this.embedPlayer.id ).hide();
			} );

			// Re-Initialize when changing media
			$( embedPlayer ).bind( 'onChangeMedia' + this.bindPostFix, function() {
				_this.destroy();
				_this.updateLayout();
				_this.setupTextSources();
				$( '#textMenuContainer_' + embedPlayer.id ).hide();
			} );

			// Resize the timed text font size per window width
			$( embedPlayer ).bind( 'onCloseFullScreen' + this.bindPostFix + ' onOpenFullScreen' + this.bindPostFix, function() {
				// Check if we are in fullscreen or not, if so add an additional bottom offset of
				// double the default bottom padding.
				var textOffset = _this.embedPlayer.controlBuilder.inFullScreen ?
						mw.config.get("TimedText.BottomPadding") * 2 :
						mw.config.get("TimedText.BottomPadding");

				var textCss = _this.getInterfaceSizeTextCss({
					'width' :  embedPlayer.getInterface().width(),
					'height' : embedPlayer.getInterface().height()
				});

				mw.log( 'TimedText::set text size for: : ' + embedPlayer.getInterface().width() + ' = ' + textCss['font-size'] );
				if ( embedPlayer.controlBuilder.isOverlayControls() && !embedPlayer.getInterface().find( '.control-bar' ).is( ':hidden' ) ) {
					textOffset += _this.embedPlayer.controlBuilder.getHeight();
				}
				embedPlayer.getInterface().find( '.track' )
				.css( textCss )
				.css({
					// Get the text size scale then set it to control bar height + TimedText.BottomPadding;
					'bottom': textOffset + 'px'
				});
			});

			// Update the timed text size
			$( embedPlayer ).bind( 'updateLayout'+ this.bindPostFix, function() {
				// If the the player resize action is an animation, animate text resize,
				// else instantly adjust the css.
				var textCss = _this.getInterfaceSizeTextCss( {
					'width': embedPlayer.getPlayerWidth(),
					'height': embedPlayer.getPlayerHeight()
				});
				mw.log( 'TimedText::updateLayout: ' + textCss['font-size']);
				embedPlayer.getInterface().find( '.track' ).css( textCss );
			});

			// Setup display binding
			$( embedPlayer ).bind( 'onShowControlBar'+ this.bindPostFix, function(event, layout, id ){
				// update embedPlayer ref:
				var embedPlayer = $('#' + id )[0];
				if ( embedPlayer.controlBuilder.isOverlayControls() ) {
					// Move the text track if present
					embedPlayer.getInterface().find( '.track' )
					.stop()
					.animate( layout, 'fast' );
				}
			});

			$( embedPlayer ).bind( 'onHideControlBar' + this.bindPostFix, function(event, layout, id ){
				var embedPlayer = $('#' + id )[0];
				if ( embedPlayer.controlBuilder.isOverlayControls() ) {
					// Move the text track down if present
					embedPlayer.getInterface().find( '.track' )
					.stop()
					.animate( layout, 'fast' );
				}
			});

			$( embedPlayer ).bind( 'AdSupport_StartAdPlayback' + this.bindPostFix, function() {
				if ( $( '#textMenuContainer_' + embedPlayer.id ).length ) {
					$( '#textMenuContainer_' + embedPlayer.id ).hide();
				}
				var $textButton = embedPlayer.getInterface().find( '.timed-text' );
				if ( $textButton.length ) {
					$textButton.unbind( 'click' );
				}
				_this.lastLayout = _this.getLayoutMode();
				_this.setLayoutMode( 'off' );
			} );

			$( embedPlayer ).bind( 'AdSupport_EndAdPlayback' + this.bindPostFix, function() {
				var $textButton = embedPlayer.getInterface().find( '.timed-text' );
				if ( $textButton.length ) {
					_this.bindTextButton( $textButton );
				}
				_this.setLayoutMode( _this.lastLayout );
			} );

		},
		addInterface: function(){
			var _this = this;
			// By default we include a button in the control bar.
			$( _this.embedPlayer ).bind( 'addControlBarComponent' + this.bindPostFix, function(event, controlBar ){
				if( controlBar.supportedComponents['timedText'] !== false &&
					_this.includeCaptionButton() ) {
					controlBar.supportedComponents['timedText'] = true;
					controlBar.components['timedText'] = _this.getTimedTextButton();
				}
			});
		},
		includeCaptionButton:function(){
			return  mw.config.get( 'TimedText.ShowInterface' ) == 'always'  ||
					this.embedPlayer.getTextTracks().length;
		},
		/**
		 * Get the current language key
		 * @return
		 * @type {string}
		 */
		getCurrentLangKey: function(){
			return this.currentLangKey;
		},
		/**
		 * Get the current language direction
		 * @return
		 * @type {string}
		 */
		getCurrentLangDir: function(){
			if ( !this.currentLangDir ) {
				var source = this.getSourceByLanguage( this.getCurrentLangKey() );
				this.currentLangDir = source.dir;
			}
			return this.currentLangDir;
		},

		/**
		 * The timed text button to be added to the interface
		 */
		getTimedTextButton: function(){
			var _this = this;
			/**
			* The closed captions button
			*/
			return {
				'w': 30,
				'position': 6.9,
				'o': function( ctrlObj ) {
					var $textButton = $( '<div />' )
						.attr( 'title', mw.msg( 'mwe-embedplayer-timed_text' ) )
						.addClass( "ui-state-default ui-corner-all ui-icon_link rButton timed-text" )
						.append(
							$( '<span />' )
							.addClass( "ui-icon ui-icon-comment" )
						)
						// Captions binding:
						.buttonHover();
					_this.bindTextButton( $textButton );
					return $textButton;

				}
			};
		},
		bindTextButton: function( $textButton ){
			var _this = this;
			$textButton.unbind('click.textMenu').bind('click.textMenu', function() {
                _this.showTextMenu();
            return true;
			} );
		},

		/**
		* Get the fullscreen text css
		*/
		getInterfaceSizeTextCss: function( size ) {
			//mw.log(' win size is: ' + $( window ).width() + ' ts: ' + textSize );
			return {
				'font-size' : this.getInterfaceSizePercent( size ) + '%'
			};
		},

		/**
		* Show the text interface library and show the text interface near the player.
		*/
		showTextMenu: function() {
			var embedPlayer = this.embedPlayer;
			var loc = embedPlayer.getInterface().find( '.rButton.timed-text' ).offset();
			mw.log('TimedText::showTextMenu:: ' + embedPlayer.id + ' location: ', loc);
			// TODO: Fix menu animation
			var $menuButton = this.embedPlayer.getInterface().find( '.timed-text' );
			// Check if a menu has already been built out for the menu button:
			if ( $menuButton[0].m ) {
				$menuButton.embedMenu( 'show' );
			} else {
				// Bind the text menu:
				this.buildMenu( true );
			}
		},
		getTextMenuContainer: function(){
			var textMenuId = 'textMenuContainer_' + this.embedPlayer.id;
			if( !$( '#' + textMenuId ).length ){
				//Setup the menu:
				this.embedPlayer.getInterface().append(
					$('<div>')
						.addClass('ui-widget ui-widget-content ui-corner-all')
						.attr( 'id', textMenuId )
						.css( {
							'position' 	: 'absolute',
							'height'	: '180px',
							'width' 	: '180px',
							'font-size'	: '12px',
							'display' : 'none',
							'overflow' : 'auto'
						} )

				);
			}
			return $( '#' + textMenuId );
		},
		/**
		 * Gets a text size percent relative to about 30 columns of text for 400
		 * pixel wide player, at 100% text size.
		 *
		 * @param size {object} The size of the target player area width and height
		 */
		getInterfaceSizePercent: function( size ) {
			// This is a ugly hack we should read "original player size" and set based
			// on some standard ish normal 31 columns 15 rows
			var sizeFactor = 4;
			if( size.height / size.width < .7 ){
				sizeFactor = 6;
			}
			var textSize = size.width / sizeFactor;
			if( textSize < 95 ){
				textSize = 95;
			}
			if( textSize > 150 ){
				textSize = 150;
			}
			return textSize;
		},

		/**
		* Setups available text sources
		*   loads text sources
		* 	auto-selects a source based on the user language
		* @param {Function} callback Function to be called once text sources are setup.
		*/
		setupTextSources: function( callback ) {
			mw.log( 'TimedText::setupTextSources');
			var _this = this;
			// Load textSources
			_this.loadTextSources( function() {
				// Enable a default source and issue a request to "load it"
				_this.autoSelectSource();

				// Load and parse the text value of enabled text sources:
				_this.loadEnabledSources();

				if( callback ) {
					callback();
				}
			} );
		},

		/**
		* Binds the timed text menu
		* and updates its content from "getMainMenu"
		*
		* @param {Object} target to display the menu
		* @param {Boolean} autoShow If the menu should be displayed
		*/
		buildMenu: function( autoShow ) {
			var _this = this;
			var embedPlayer = this.embedPlayer;
			// Setup text sources ( will callback inline if already loaded )
			_this.setupTextSources( function() {
				var $menuButton = _this.embedPlayer.getInterface().find( '.timed-text' );

				var positionOpts = { };
				if( _this.embedPlayer.supports[ 'overlays' ] ){
					var positionOpts = {
						'directionV' : 'up',
						'offsetY' : _this.embedPlayer.controlBuilder.getHeight(),
						'directionH' : 'left',
						'offsetX' : -28
					};
				}

				if( !_this.embedPlayer.getInterface() ){
					mw.log("TimedText:: interface called before interface ready, just wait for interface");
					return ;
				}
				var $menuButton = _this.embedPlayer.getInterface().find( '.timed-text' );
				var ctrlObj = _this.embedPlayer.controlBuilder;
				// NOTE: Button target should be an option or config
				$menuButton.embedMenu( {
					'content'	: _this.getMainMenu(),
					'zindex' : mw.config.get( 'EmbedPlayer.FullScreenZIndex' ) + 2,
					'crumbDefaultText' : ' ',
					'autoShow': autoShow,
					'keepPosition' : true,
					'showSpeed': 0,
					'height' : 100,
					'width' : 300,
					'targetMenuContainer' : _this.getTextMenuContainer(),
					'positionOpts' : positionOpts,
					'backLinkText' : mw.msg( 'mwe-timedtext-back-btn' ),
					'createMenuCallback' : function(){
						var $interface = _this.embedPlayer.getInterface();
						var $textContainer =  _this.getTextMenuContainer();
						var textHeight = 130;
						var top = $interface.height() - textHeight - ctrlObj.getHeight() - 6;
						if( top < 0 ){
							top = 0;
						}
						// check for audio
						if(  _this.embedPlayer.isAudio() ){
							top = _this.embedPlayer.controlBuilder.getHeight() + 4;
						}
						$textContainer.css({
							'top' : top,
							'height': textHeight,
							'position' : 'absolute',
							'left': $menuButton[0].offsetLeft - 165,
							'bottom': ctrlObj.getHeight()
						})
						ctrlObj.showControlBar( true );
					},
					'closeMenuCallback' : function(){
						ctrlObj.restoreControlsHover();
					}
				});
			});
		},

		/**
		* Monitor video time and update timed text filed[s]
		*/
		monitor: function() {
			//mw.log(" timed Text monitor: " + this.enabledSources.length );
			var embedPlayer = this.embedPlayer;
			// Setup local reference to currentTime:
			var currentTime = embedPlayer.currentTime;

			// Get the text per kind
			var textCategories = [ ];

			var source = this.enabledSources[ 0 ];
			if( source ) {
				this.updateSourceDisplay( source, currentTime );
			}
		},

		/**
		 * Load all the available text sources from the inline embed
		 * @param {Function} callback Function to call once text sources are loaded
		 */
		loadTextSources: function( callback ) {
			var _this = this;
			// check if text sources are already loaded ( not em )
			if( this.textSources.length ){
				callback( this.textSources );
				return ;
			}
			this.textSources = [];
			// load inline text sources:
			$.each( this.embedPlayer.getTextTracks(), function( inx, textSource ){
				_this.textSources.push( new mw.TextSource( textSource ) );
			});
			// return the callback with sources
			callback( _this.textSources );
		},

		/**
		* Get the layout mode
		*
		* Takes into consideration:
		* 	Playback method overlays support ( have to put subtitles below video )
		*
		*/
		getLayoutMode: function() {
		 	// Re-map "ontop" to "below" if player does not support
		 	if( this.config.layout == 'ontop' && !this.embedPlayer.supports['overlays'] ) {
		 		this.config.layout = 'below';
		 	}
		 	return this.config.layout;
		},

		/**
		* Auto selects a source given the local configuration
		*
		* NOTE: presently this selects a "single" source.
		* In the future we could support multiple "enabled sources"
		*/
		autoSelectSource: function() {
			var _this = this;
			// If a source is enabled then don't auto select
			if ( this.enabledSources.length ) {
				return false;
			}
			this.enabledSources = [];

			var setDefault = false;
			// Check if any source is marked default:
			$.each( this.textSources, function(inx, source){
				if( source['default'] ){
					_this.enableSource( source );
					setDefault = true;
					return false;
				}
			});
			if ( setDefault ) {
				return true;
			}

			var setLocalPref = false;
			// Check if any source matches our "local" pref
			$.each( this.textSources, function(inx, source){
				if(	_this.config.userLanguage == source.srclang.toLowerCase()
					&&
					_this.config.userKind == source.kind
				) {
					_this.enableSource( source );
					setLocalPref = true;
					return false;
				}
			});
			if ( setLocalPref ) {
				return true;
			}

			var setEnglish = false;
			// If no userLang, source try enabling English:
			if( this.enabledSources.length == 0 ) {
				for( var i=0; i < this.textSources.length; i++ ) {
					var source = this.textSources[ i ];
					if( source.srclang.toLowerCase() == 'en' ) {
						_this.enableSource( source );
						setEnglish = true;
						return false;
					}
				}
			}
			if ( setEnglish ) {
				return true;
			}

			var setFirst = false;
			// If still no source try the first source we get;
			if( this.enabledSources.length == 0 ) {
				for( var i=0; i < this.textSources.length; i++ ) {
					var source = this.textSources[ i ];
					_this.enableSource( source );
					setFirst = true;
					return false;
				}
			}
			if ( setFirst ) {
				return true;
			}

			return false;
		},
		/**
		 * Enable a source and update the currentLangKey
		 * @param {object} source
		 * @return
		 */
		enableSource: function( source ){
			var _this = this;
			// check if we have any source set yet:
			if( !_this.enabledSources.length ){
				_this.enabledSources.push( source );
				_this.currentLangKey = source.srclang;
				_this.currentLangDir = null;
				return ;
			}
			var sourceEnabled = false;
			// Make sure the source is not already enabled
			$.each( this.enabledSources, function( inx, enabledSource ){
				if( source.id == enabledSource.id ){
					sourceEnabled = true;
				}
			});
			if ( !sourceEnabled ) {
				_this.enabledSources.push( source );
				_this.currentLangKey = source.srclang;
				_this.currentLangDir = null;
			}
		},

		/**
		 * Get the current source sub captions
		 * @param {function} callback function called once source is loaded
		 */
		loadCurrentSubSource: function( callback ){
			mw.log("loadCurrentSubSource:: enabled source:" + this.enabledSources.length);
			for( var i =0; i < this.enabledSources.length; i++ ){
				var source = this.enabledSources[i];
				if( source.kind == 'SUB' ){
					source.load( function(){
						callback( source);
						return ;
					});
				}
			}
			return false;
		},

		/**
		 * Get sub captions by language key:
		 *
		 * @param {string} langKey Key of captions to load
		 * @pram {function} callback function called once language key is loaded
		 */
		getSubCaptions: function( langKey, callback ){
			for( var i=0; i < this.textSources.length; i++ ) {
				var source = this.textSources[ i ];
				if( source.srclang.toLowerCase() === langKey ) {
					var source = this.textSources[ i ];
					source.load( function(){
						callback( source.captions );
					});
				}
			}
		},

		/**
		* Issue a request to load all enabled Sources
		*  Should be called anytime enabled Source list is updated
		*/
		loadEnabledSources: function() {
			var _this = this;
			mw.log( "TimedText:: loadEnabledSources " +  this.enabledSources.length );
			$.each( this.enabledSources, function( inx, enabledSource ) {
				// check if the source requires ovelray ( ontop ) layout mode:
				if( enabledSource.isOverlay() && _this.config.layout== 'ontop' ){
					_this.setLayoutMode( 'ontop' );
				}
				enabledSource.load(function(){
				  	// Trigger the text loading event:
				  	$( _this.embedPlayer ).trigger('loadedTextSource', enabledSource);
				});
			});
		},
		/**
		* Checks if a source is "on"
		* @return {Boolean}
		* 	true if source is on
		* 	false if source is off
		*/
		isSourceEnabled: function( source ) {
			// no source is "enabled" if subtitles are "off"
			if( this.getLayoutMode() == 'off'  ){
				return false;
			}
			var isEnabled = false;
			$.each( this.enabledSources, function( inx, enabledSource ) {
				if( source.id ) {
					if( source.id === enabledSource.id ){
						isEnabled = true;
					}
				}
				if( source.src ){
					if( source.src == enabledSource.src ){
						isEnabled = true;
					}
				}
			});
			return isEnabled;
		},

		/**
		 * Marks the active captions in the menu
		 */
		markActive: function( source ) {
			var $menu = $( '#textMenuContainer_' + this.embedPlayer.id );
			if ( $menu.length ) {
				var $captionRows = $menu.find( '.captionRow' );
				if ( $captionRows.length ) {
					$captionRows.each( function() {
						$( this ).removeClass( 'ui-icon-bullet ui-icon-radio-on' );
						var iconClass = ( $( this ).data( 'caption-id' ) === source.id ) ? 'ui-icon-bullet' : 'ui-icon-radio-on';
						$( this ).addClass( iconClass );
					} );
				}
			}
		},

		/**
		 * Marks the active layout mode in the menu
		 */
		markLayoutActive: function ( layoutMode ) {
			var $menu = $( '#textMenuContainer_' + this.embedPlayer.id );
			if ( $menu.length ) {
				var $layoutRows = $menu.find( '.layoutRow' );
				if ( $layoutRows.length ) {
					$layoutRows.each( function() {
						$( this ).removeClass( 'ui-icon-bullet ui-icon-radio-on' );
						var iconClass = ( $( this ).data( 'layoutMode' ) === layoutMode ) ? 'ui-icon-bullet' : 'ui-icon-radio-on';
						$( this ).addClass( iconClass );
					} );
				}
			}
		},

		/**
		* Get a source object by language, returns "false" if not found
		* @param {string} langKey The language key filter for selected source
		*/
		getSourceByLanguage: function ( langKey ) {
			for(var i=0; i < this.textSources.length; i++) {
				var source = this.textSources[ i ];
				if( source.srclang == langKey ){
					return source;
				}
			}
			return false;
		},

		/**
		* Builds the core timed Text menu and
		* returns the binded jquery object / dom set
		*
		* Assumes text sources have been setup: ( _this.setupTextSources() )
		*
		* calls a few sub-functions:
		* Basic menu layout:
		*		Chose Language
		*			All Subtiles here ( if we have categories list them )
		*		Layout
		*			Below video
		*			Ontop video ( only available to supported plugins )
		* TODO features:
		*		[ Search Text ]
		*			[ This video ]
		*			[ All videos ]
		*		[ Chapters ] seek to chapter
		*/
		getMainMenu: function() {
			var _this = this;

			// Set the menut to avaliable languages:
			var $menu = _this.getLanguageMenu();

			if(  _this.textSources.length == 0 ){
				$menu.append(
					$.getLineItem( mw.msg( 'mwe-timedtext-no-subs'), 'close' )
				);
			} else {
				// Layout Menu option if not in an iframe and we can expand video size:
				$menu.append(
					$.getLineItem(
						mw.msg( 'mwe-timedtext-layout-off'),
						( _this.getLayoutMode() == 'off' ) ? 'bullet' : 'radio-on',
						function() {
							_this.setLayoutMode( 'off' );
						},
						'layoutRow',
						{ 'layoutMode' : 'off' }
					)
				)
			}
			// Allow other modules to add to the timed text menu:
			$( _this.embedPlayer ).trigger( 'TimedText_BuildCCMenu', [ $menu, _this.embedPlayer.id ] ) ;

			// Test if only one menu item move its children to the top level
			if( $menu.children('li').length == 1 ){
				$menu.find('li > ul > li').detach().appendTo( $menu );
				$menu.find('li').eq(0).remove();
			}

			return $menu;
		},

		/**
		* Utility function to assist in menu build out:
		* Get menu line item (li) html: <li><a> msgKey </a></li>
		*
		* @param {String} msgKey Msg key for menu item
		*/

		/**
		* Get line item (li) from source object
		* @param {Object} source Source to get menu line item from
		*/
		getLiSource: function( source ) {
			var _this = this;
			//See if the source is currently "on"
			var sourceIcon = ( this.isSourceEnabled( source ) )? 'bullet' : 'radio-on';
			if( source.title ) {
				return $.getLineItem( source.title, sourceIcon, function() {
					_this.selectTextSource( source );
				}, 'captionRow', { 'caption-id' : source.id } );
			}
			if( source.srclang ) {
				var langKey = source.srclang.toLowerCase();
				return $.getLineItem(
					mw.msg('mwe-timedtext-key-language', langKey, _this.getLanguageName ( langKey ) ),
					sourceIcon,
					function() {
						// select the current text source:
						_this.selectTextSource( source );
					},
					'captionRow',
					{ 'caption-id' : source.id }
				);
			}
		},

		/**
	 	 * Get language name from language key
	 	 * @param {String} lang_key Language key
	 	 */
	 	getLanguageName: function( lang_key ) {
	 		if( mw.Language.names[ lang_key ]) {
	 			return mw.Language.names[ lang_key ];
	 		}
	 		return false;
	 	},


		/**
		* set the layout mode
		* @param {Object} layoutMode The selected layout mode
		*/
		setLayoutMode: function( layoutMode ) {
			var _this = this;
			mw.log("TimedText:: setLayoutMode: " + layoutMode + ' ( old mode: ' + _this.config.layout + ' )' );
			if( ( layoutMode != _this.config.layout ) || _this.firstLoad ) {
				// Update the config and redraw layout
				_this.config.layout = layoutMode;
				// Update the display:
				_this.updateLayout();
				_this.firstLoad = false;
			}
			_this.markLayoutActive( layoutMode );
		},

		toggleCaptions: function(){
			mw.log( "TimedText:: toggleCaptions was:" + this.config.layout );
			if( this.config.layout == 'off' ){
				this.setLayoutMode( this.defaultDisplayMode );
			} else {
				this.setLayoutMode( 'off' );
			}
		},
		/**
		* Updates the timed text layout ( should be called when config.layout changes )
		*/
		updateLayout: function() {
			mw.log( "TimedText:: updateLayout " );
			var $playerTarget = this.embedPlayer.getInterface();
            if( $playerTarget ) {
            	// remove any existing caption containers:
                $playerTarget.find('.captionContainer,.captionsOverlay').remove();
            }
			this.refreshDisplay();
		},

		/**
		* Select a new source
		*
		* @param {Object} source Source object selected
		*/
		selectTextSource: function( source ) {
			var _this = this;
			mw.log("TimedText:: selectTextSource: select lang: " + source.srclang );

			// enable last non-off layout:
			_this.setLayoutMode( _this.lastLayout );

			// For some reason we lose binding for the menu ~sometimes~ re-bind
			this.bindTextButton( this.embedPlayer.getInterface().find('timed-text') );

			this.currentLangKey =  source.srclang;
			this.currentLangDir = null;

			// Update the config language if the source includes language
			if( source.srclang ){
				this.config.userLanguage = source.srclang;
			}

			if( source.kind ){
				this.config.userKind = source.kind;
			}

			// (@@todo update kind & setup kind language buckets? )

			// Remove any other sources selected in sources kind
			this.enabledSources = [];

			this.enabledSources.push( source );

			// Set any existing text target to "loading"
			if( !source.loaded ) {
				var $playerTarget = this.embedPlayer.getInterface();
				$playerTarget.find('.track').text( mw.msg('mwe-timedtext-loading-text') );
				// Load the text:
				source.load( function(){
					// Refresh the interface:
					_this.refreshDisplay();
				});
			} else {
				_this.refreshDisplay();
			}

			_this.markActive( source );

			// Trigger the event
			$( this.embedPlayer ).trigger( 'TimedText_ChangeSource' );
		},

		/**
		* Refresh the display, updates the timedText layout, menu, and text display
		* also updates the cookie preference.
		*
		* Called after a user option change
		*/
		refreshDisplay: function() {
			// Update the configuration object
			$.cookie( 'TimedText.Preferences',  JSON.stringify( this.config ) );

			// Empty out previous text to force an interface update:
			this.prevText = [];

			// Refresh the Menu (if it has a target to refresh)
			mw.log( 'TimedText:: bind menu refresh display' );
			this.buildMenu();
            this.resizeInterface();

            // add an empty catption:
            this.displayTextTarget( $( '<span /> ').text( '') );

			// Issues a "monitor" command to update the timed text for the new layout
			this.monitor();
		},

		/**
		* Builds the language source list menu
		* Cehck if the "track" tags had the "kind" attribute.
		*
		* The kind attribute forms "categories" of text tracks like "subtitles",
		*  "audio description", "chapter names". We check for these categories
		*  when building out the language menu.
		*/
		getLanguageMenu: function() {
			var _this = this;

			// See if we have categories to worry about
			// associative array of SUB etc categories. Each kind contains an array of textSources.
			var categorySourceList = {};
			var sourcesWithCategoryCount = 0;

			// ( All sources should have a kind (depreciate )
			var sourcesWithoutCategory = [ ];
			for( var i=0; i < this.textSources.length; i++ ) {
				var source = this.textSources[ i ];
				if( source.kind ) {
					var categoryKey = source.kind ;
					// Init Category menu item if it does not already exist:
					if( !categorySourceList[ categoryKey ] ) {
						// Set up catList pointer:
						categorySourceList[ categoryKey ] = [];
						sourcesWithCategoryCount++;
					}
					// Append to the source kind key menu item:
					categorySourceList[ categoryKey ].push(
						_this.getLiSource( source )
					);
				}else{
					sourcesWithoutCategory.push( _this.getLiSource( source ) );
				}
			}
			var $langMenu = $('<ul>');
			// Check if we have multiple categories ( if not just list them under the parent menu item)
			if( sourcesWithCategoryCount > 1 ) {
				for(var categoryKey in categorySourceList) {
					var $catChildren = $('<ul>');
					for(var i=0; i < categorySourceList[ categoryKey ].length; i++) {
						$catChildren.append(
							categorySourceList[ categoryKey ][i]
						);
					}
					// Append a cat menu item for each kind list
					// Give grep a chance to find the usages:
					// mwe-timedtext-textcat-cc, mwe-timedtext-textcat-sub, mwe-timedtext-textcat-tad,
					// mwe-timedtext-textcat-ktv, mwe-timedtext-textcat-tik, mwe-timedtext-textcat-ar,
					// mwe-timedtext-textcat-nb, mwe-timedtext-textcat-meta, mwe-timedtext-textcat-trx,
					// mwe-timedtext-textcat-lrc, mwe-timedtext-textcat-lin, mwe-timedtext-textcat-cue
					$langMenu.append(
						$.getLineItem( mw.msg( 'mwe-timedtext-textcat-' + categoryKey.toLowerCase() ) ).append(
							$catChildren
						)
					);
				}
			} else {
				for(var categoryKey in categorySourceList) {
					for(var i=0; i < categorySourceList[ categoryKey ].length; i++) {
						$langMenu.append(
							categorySourceList[ categoryKey ][i]
						);
					}
				}
			}
			// Add any remaning sources that did nto have a category
			for(var i=0; i < sourcesWithoutCategory.length; i++) {
				$langMenu.append( sourcesWithoutCategory[i] );
			}

			return $langMenu;
		},

		/**
		 * Updates a source display in the interface for a given time
		 * @param {object} source Source to update
		 * @param {number} time Caption time used to add and remove active captions.
		 */
		updateSourceDisplay: function ( source, time ) {
			var _this = this;
			if( this.timeOffset ){
				time = time + parseInt( this.timeOffset );
			}

			// Get the source text for the requested time:
			var activeCaptions = source.getCaptionForTime( time );
			var addedCaption = false;
			// Show captions that are on:
			$.each( activeCaptions, function( capId, caption ){
				var $cap =  _this.embedPlayer.getInterface().find( '.track[data-capId="' + capId +'"]');
				if( caption.content != $cap.html() ){
					// remove old
					$cap.remove();
					// add the updated value:
					_this.addCaption( source, capId, caption );
					addedCaption = true;
				}
			});

			// hide captions that are off:
			_this.embedPlayer.getInterface().find( '.track' ).each(function( inx, caption){
				if( !activeCaptions[ $( caption ).attr('data-capId') ] ){
					if( addedCaption ){
						$( caption ).remove();
					} else {
						$( caption ).fadeOut( mw.config.get('EmbedPlayer.MonitorRate'), function(){$(this).remove();} );
					}
				}
			});
		},
		addCaption: function( source, capId, caption ){
			if( this.getLayoutMode() == 'off' ){
				return ;
			}

			// use capId as a class instead of id for easy selections and no conflicts with
			// multiple players on page.
			var $textTarget = $('<div />')
				.addClass( 'track' )
				.attr( 'data-capId', capId )
				.hide();

			// Update text ( use "html" instead of "text" so that subtitle format can
			// include html formating
			// TOOD we should scrub this for non-formating html
			$textTarget.append(
				$('<span>')
					.addClass( 'ttmlStyled' )
					.css( 'pointer-events', 'auto')
					.css( this.getCaptionCss() )
					.append(
						$('<span>')
							// Prevent background (color) overflowing TimedText
							// http://stackoverflow.com/questions/9077887/avoid-overlapping-rows-in-inline-element-with-a-background-color-applied
							.css( 'position', 'relative' )
							.html( caption.content )
					)
			);


			// Add/update the lang option
			$textTarget.attr( 'lang', source.srclang.toLowerCase() );

			// Update any links to point to a new window
			$textTarget.find( 'a' ).attr( 'target', '_blank' );

			// Add TTML or other complex text styles / layouts if we have ontop captions:
			if( this.getLayoutMode() == 'ontop' ){
				if( caption.css ){
					$textTarget.css( caption.css );
				} else {
					$textTarget.css( this.getDefaultStyle() );
				}
			}
			// Apply any custom style ( if we are ontop of the video )
			this.displayTextTarget( $textTarget );

			// apply any interface size adjustments:
			$textTarget.css( this.getInterfaceSizeTextCss({
					'width' :  this.embedPlayer.getInterface().width(),
					'height' : this.embedPlayer.getInterface().height()
				})
			);

			// Update the style of the text object if set
			if( caption.styleId ){
				var capCss = source.getStyleCssById( caption.styleId );
				$textTarget.find('span.ttmlStyled').css(
					capCss
				);
			}
			$textTarget.fadeIn('fast');
		},
		displayTextTarget: function( $textTarget ){
			var embedPlayer = this.embedPlayer;
			var $interface = embedPlayer.getInterface();
			var controlBarHeight = embedPlayer.controlBuilder.getHeight();

			if( this.getLayoutMode() == 'off' ){
				// sync player size per audio player:
				if( embedPlayer.isAudio() ){
					$interface.find( '.overlay-win' ).css( 'top', controlBarHeight  );
					$interface.css( 'height',  controlBarHeight );
				}
				return;
			}

			if( this.getLayoutMode() == 'ontop' ){
				this.addTextOverlay(
					$textTarget
				);
			} else if( this.getLayoutMode() == 'below' ){
				this.addTextBelowVideo( $textTarget );
			} else {
				mw.log("Possible Error, layout mode not recognized: " + this.getLayoutMode() );
			}

			// sync player size per audio player:
			if( embedPlayer.isAudio() && embedPlayer.getInterface().height() < 80 ){
				$interface.find( '.overlay-win' ).css( 'top', 80);
				$interface.css( 'height', 80 );

				$interface.find('.captionsOverlay' )
						.css('bottom', embedPlayer.controlBuilder.getHeight() )
			}

		},
		getDefaultStyle: function(){
			var defaultBottom = 15;
			if( this.embedPlayer.controlBuilder.isOverlayControls() && !this.embedPlayer.getInterface().find( '.control-bar' ).is( ':hidden' ) ) {
				defaultBottom += this.embedPlayer.controlBuilder.getHeight();
			}
			var baseCss =  {
					'position':'absolute',
					'bottom': defaultBottom,
					'width': '100%',
					'display': 'block',
					'opacity': .8,
					'text-align': 'center'
				};
			baseCss =$.extend( baseCss, this.getInterfaceSizeTextCss({
				'width' :  this.embedPlayer.getInterface().width(),
				'height' : this.embedPlayer.getInterface().height()
			}));
			return baseCss;
		},
		addTextOverlay: function( $textTarget ){
			var _this = this;
			var $captionsOverlayTarget = this.embedPlayer.getInterface().find('.captionsOverlay');
			var layoutCss = {
				'left': 0,
				'top': 0,
				'bottom': 0,
				'right': 0,
				'position': 'absolute',
				'direction': this.getCurrentLangDir(),
				'z-index': mw.config.get( 'EmbedPlayer.FullScreenZIndex' )
			};

			if( $captionsOverlayTarget.length == 0 ){
				// TODO make this look more like addBelowVideoCaptionsTarget
				$captionsOverlayTarget = $( '<div />' )
				 	.addClass( 'captionsOverlay' )
					.css( layoutCss )
					.css('pointer-events', 'none');
				this.embedPlayer.getVideoHolder().append( $captionsOverlayTarget );
			}
			// Append the text:
			$captionsOverlayTarget.append( $textTarget );

		},
		/**
		 * Applies the default layout for a text target
		 */
		addTextBelowVideo: function( $textTarget ) {
			var $playerTarget = this.embedPlayer.getInterface();
			// Get the relative positioned player class from the controlBuilder:
			this.embedPlayer.controlBuilder.keepControlBarOnScreen = true;
			if( !$playerTarget.find('.captionContainer').length || this.embedPlayer.useNativePlayerControls() ) {
				this.addBelowVideoCaptionContainer();
			}
			$playerTarget.find('.captionContainer').html(
				$textTarget.css( {
					'color':'white'
				} )
			);
		},
		addBelowVideoCaptionContainer: function(){
			var _this = this;
			mw.log( "TimedText:: addBelowVideoCaptionContainer" );
			var $playerTarget = this.embedPlayer.getInterface();
			if( $playerTarget.find('.captionContainer').length ) {
				return ;
			}
			// Append after video container
			this.embedPlayer.getVideoHolder().after(
				$('<div>').addClass( 'captionContainer block' )
				.css({
					'width' : '100%',
					'height' : mw.config.get( 'TimedText.BelowVideoBlackBoxHeight' ) + 'px',
					'background-color' : '#000',
					'text-align' : 'center',
					'padding-top' : '5px'
				} )
			);

			_this.embedPlayer.triggerHelper('updateLayout');
		},
        /**
         * Resize the interface for layoutMode == 'below' ( if not in full screen)
         */
        resizeInterface: function(){
            var _this = this;
            if( !_this.embedPlayer.controlBuilder ){
            	// too soon
            	return ;
            }
            if( !_this.embedPlayer.controlBuilder.inFullScreen && _this.originalPlayerHeight ){
                _this.embedPlayer.triggerHelper( 'resizeIframeContainer', [{'height' : _this.originalPlayerHeight}] );
            } else {
            	// removed resize on container content, since syncPlayerSize calls now handle keeping player aspect.
				 _this.embedPlayer.triggerHelper('updateLayout');
            }
        },
		/**
		 * Build css for caption using this.options
		 */
		getCaptionCss: function() {
			return {};
		}
	};

} )( mediaWiki, jQuery );