<?xml version="1.0" encoding="UTF-8"?>
<javascript app="core">
 <file javascript_app="global" javascript_location="library" javascript_path="" javascript_name="app.js" javascript_type="framework" javascript_version="107643" javascript_position="300"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * app.js - Our main app script
 *
 * Author: Rikki Tissier
 */

// Our namespace
var ips = ips || {};

jQuery.migrateMute = true;

( function($, _, undefined){
	"use strict";

	ips = ( function () {

		var _settings,
			_strings = {},
			_graphData = {},
			uid = 1,
			urand = Math.ceil( Math.random() * 10000 ),
			elemContainer,
			_location = 'front';

		/**
		 * Boot methods - sets up the app
		 *
		 * @returns 	{void}
		 */ 
		var boot = function (config) {
			_settings = config;

			// Set our ajax handler default
			_setAjaxConfig();

			// Add a little jQuery plugin that allows us to add callbacks
			// to CSS animations
			$.fn.animationComplete = function (callback) {
				return $( this ).one('webkitAnimationEnd animationend', function (e) {
					// Important fix: ignore bubbled transition events
					if( e.target == this ){
						callback.apply( this );
					}
				});
			};

			// In case we already have ips_uid elements on the page, start at the highest found

			// jQuery plugin that adds a unique id to an element if an ID doesn't
			// already exist. Based on jQueryUI method.
			$.fn.identify = function () {
				return this.each( function () {
					if( !this.id ) {
						this.id = 'ips_uid_' + urand + '_' + (++uid);
					}
				});
			};

			// Redefine .html() so that we can observe lazy-loaded content when we update elements
			var oldHtml = $.fn.html;
			var newHtml = function (value) {
				var toReturn = oldHtml.apply(this, arguments);
				var elem = $( this );

				// If we've updated with a string...
				if( typeof value === "string" ){
					var itemsToLoad = elem.find( ips.utils.lazyLoad.contentSelector );
					var lazyLoadWrapper = elem.closest('[data-controller^="core.front.core.lightboxedImages"]');

					if( itemsToLoad.length ){
						try {
							// See if we're inserting into an element that already handles lazy loading...
							if( lazyLoadWrapper.length ){
								lazyLoadWrapper.trigger('refreshContent');
							} else {
								// If not, just observe manually
								Debug.log("Updated with `html()` and found content to lazyLoad");
								if( ips.getSetting('lazyLoadEnabled') ){
									ips.utils.lazyLoad.observe(this);
								} else {
									ips.utils.lazyLoad.loadContent(this); // load immediately
								}
							}
						} catch (err) { }
					}
				}

				return toReturn;
			}
			$.fn.html = newHtml;

			// Redefine .prop() so that we can observe events when properties are changed
			// See: http://stackoverflow.com/questions/16336473/add-event-handler-when-checkbox-becomes-disabled
			var oldProp = $.fn.prop;
			var newProp = function () {
				var retFunc = oldProp.apply( this, arguments );
				this.trigger( 'propChanged', this );
				return retFunc;
			};

			$.fn.prop = newProp;

			// Add a utility function for easily adding
			// methods to a prototype. 
			// From "Javascript: The Good Parts" by Douglas Crockford
			Function.prototype.method = function (name, func) {
				this.prototype[name] = func;
				return this;
			};

			// Set up mustache-style templates for language interpolation in underscore
			_.templateSettings = {
				interpolate: /\{\{(.+?)\}\}/g
			};
											
			// Warn users about pasting stuff into the console
			if( !Debug.isEnabled() && window.console ){
				window.console.log("%cThis is a browser feature intended for developers. Do not paste any code here given to you by someone else. It may compromise your account or have other negative side effects.", "font-weight: bold; font-size: 14px;");
			}

			// Signal that we're ready to begin
			$( document )
				.trigger('doneBooting')
				.ready( function () {
					// Set our location
					_location = $( 'body' ).attr('data-pageLocation') || 'front';
					_preloadLoader();
				});
		},

		/**
		 * Allows us to use mock ajax objects if necessary
		 *
		 * @returns 	{object} 	Ajax object (jQuery's $.ajax by default)
		 */ 
		getAjax = function () {
			if( getSetting('mock_ajax') ){
				return getSetting('mock_ajax');
			}

			return $.ajax;
		},

		/**
		 * Returns our main wrapper (body by default)
		 * With custom skins, sometimes inserting into the body can cause styling issues.
		 * With the container setting, we can choose to insert them somewhere else.
		 *
		 * @returns 	{element} 	The container
		 */ 
		getContainer = function () {
			var tryThis = $( getSetting('container') );
			return ( tryThis.length ) ? tryThis : $('body');
		},

		/**
		 * Loading spinners are contained in a different font file, so there's a FOUT
		 * when they are first shown. We'll create an invisible element so the browser
		 * loads them.
		 *
		 * @returns 	{void}
		 */ 
		_preloadLoader = function () {
			var elem = $('<span/>').css({
				visibility: 'hidden',
				position: 'absolute',
				top: '-300px',
				width: '1px',
				height: '1px',
				overflow: 'hidden'
			});

			if( $('html').attr('dir') == 'rtl' ){
				elem.css({ right: '-300px' });
			} else {
				elem.css({ left: '-300px' });
			}

			elem.append( $('<span/>').addClass('ipsLoading ipsLoading_noAnim').css({
				display: 'block'
			}) );

			$('body').append( elem );
		},

		/**
		 * Sets up our global ajax handlers
		 *
		 * @returns 	{void}
		 */ 
		_setAjaxConfig = function () {
			var data = {
				csrfKey: ips.getSetting('csrfKey')
			};

			$.ajaxSetup({
				data: data,
				cache: true
			});

			// Add global loading indicator ability
			var count = 0;

			$( document )
				.ajaxSend( function (event, request, settings) {
					if( !_.isUndefined( settings ) && settings.showLoading === true ){
						if( !$('#elAjaxLoading').length ){
							getContainer().append( templates.render('core.general.ajax') );
						}

						count++;
						ips.utils.anim.go( 'fadeIn fast', $('#elAjaxLoading') );
					}
				})
				.ajaxComplete( function (event, request, settings) {
					if( !_.isUndefined( settings ) && settings.showLoading === true ){
						count--;

						if( count === 0 ){
							ips.utils.anim.go( 'fadeOut fast', $('#elAjaxLoading') );
						}
					}

					// Check for redirect response
					if( !_.isUndefined( settings ) && !settings.bypassRedirect ){
						var responseJson = null;

						if( !_.isUndefined( request.responseJSON ) && !_.isUndefined( request.responseJSON.redirect ) )
						{
							responseJson = request.responseJSON;
						}
						else if( !_.isUndefined( request.responseText ) )
						{
							try
							{
								var jsonResponse = $.parseJSON( request.responseText );

								if( jsonResponse && !_.isUndefined( jsonResponse.redirect ) )
								{
									responseJson = jsonResponse;
								}
							}
							catch( err ){}
						}

						if( responseJson ){
							// Do we have a flash message to show?
							if( !_.isUndefined( responseJson.message ) && responseJson.message != '' ){
								ips.utils.cookie.set( 'flmsg', responseJson.message );
							}
							
							if ( responseJson.redirect.match( /#/ ) ) {
								window.location.href = responseJson.redirect;
								window.location.reload();
							} else {
								window.location = responseJson.redirect;
							}
						}
					}

					// Re-parse cookies
					ips.utils.cookie.init();
				});
		},

		/**
		 * Config getter
		 *
		 * @param 	{string} 	key 	Setting key to return
		 * @returns {mixed} 	Config setting, or undefined if it doesn't exist
		 */ 
		getSetting = function (key) {
			return _settings[ key ];
		},

		/**
		 * Return full settings object
		 *
		 * @returns 	{object} 	Settings object
		 */
		getAllSettings = function () {
			return _settings;
		},

		/**
		 * Config setter
		 *
		 * @param 	{string} 	key 	Key to set
		 * @param 	{mixed} 	value	Setting value
		 * @returns {void}
		 */
		setSetting = function (key, value) {
			_settings[ key ] = value;
		},

		/**
		 * Adds strings to our language object
		 *
		 * @param 	{mixed} 	strings 	Either an {object} of key/values, or a {string} as a key
		 * @param 	{string} 	[...]		If strings is a string, this param is the value
		 * @returns {void}
		 */
		setString = function (strings) {

			if( _.isString( strings ) && arguments.length == 2 && _.isString( arguments[1] ) ){
				strings = {};
				strings[ arguments[0] ] = arguments[1];
			} else if( !_.isObject( strings ) ){
				Debug.warn("Invalid strings object passed to addString");
				return;
			}

			$.each( strings, function (key, value ){
				_strings[ key ] = value;
			});
		},
		
		/**
		 * Detect if a language string exists
		 *
		 * @param 	{mixed} 	key		The key
		 * @returns {bool}
		 */	
		haveString = function (key) {
			return !_.isUndefined( _strings[ key ] );
		},

		/**
		 * Retrieves a string from storage, and interpolates values if needed
		 *
		 * @param 	{mixed} 	strings 	Either an {object} of key/values, or a {string} as a key
		 * @param 	{string} 	[...]		If strings is a string, this param is the value
		 * @returns {string}	The interpolated string (empty if the key does not exist)
		 */	
		getString = function (key, values) {

			if( _.isUndefined( _strings[ key ] ) ){
				Debug.warn("The string '" + key + "' doesn't exist");
				return '';
			}

			var thisString = _strings[ key ],
				values = values || {};

			// Do we have special values to parse?
			if( !_.indexOf( thisString, '{{' ) ){
				return thisString;
			}

			// Add some vars into the values
			_.extend( values, {
				baseURL: ips.getSetting('baseURL')
			});

			try {
				return _.template( thisString )( values );
			} catch (err) {
				return ( Debug.isEnabled() ) ? "[Error using language string " + key + "]" : "";
			}
		},

		/**
		* GraphQL Response setter
		*
		* @param 	{string} 	key 	Key to set
		* @param 	{mixed} 	value	Response value
		* @returns {void}
		*/
		setGraphQlData = function (key, value) {
			_graphData[ key ] = value;
		},

		/**
		 * GraphQL Responses getter
		 *
		 * @param 	{string} 	key 	Key to return
		 * @returns {mixed} 	Data, or undefined if it doesn't exist
		 */
		getGraphQlData = function (key) {
			return _graphData[ key ];
		},

		/**
		 * Returns the location in which we're running
		 *
		 * @returns {string} 	Location key
		 */
		getLocation = function () {
			return _location;
		},

		/**
		 * Create a module, checking that each namespace is ready to accept
		 * modules. If the init method exists on the given module, it is added
		 * to the document.ready queue.
		 *
		 * @param	{string} 	name 	The full module path to create
		 * @param 	{function} 	fn 		The module definition
		 * @returns {void}
		 */
		createModule = function (name, fn) {
			
			var bits = name.split('.'),
				currentPath = window;

			var tmpName = [];

			// Loop through the path pieces and ensure they exist
			if( bits.length ){
				for( var i = 0; i < bits.length; i++ ){

					if( _.isUndefined( currentPath[ bits[i] ] ) ){
						currentPath[ bits[i] ] = {};
					}

					currentPath = currentPath[ bits[i] ];
				}
			} else {
				return false;
			}

			// Assign our module to the path
			currentPath = _.extend( currentPath, fn.call( currentPath ) );

			// Set up init if it exists
			if( _.isFunction( currentPath.init ) ){
				// support external loading. If the script is embedded in another webpage after it loads, the document is already 'ready'. We will instead listen for a 'ipsScriptsReady' event
				if (window.ipsScriptsExternallyLoaded) {
					$(document.body).on('ipsScriptsReady', function () {
						currentPath.init.call(currentPath);
					})
				} else {
					$(document).ready(function () {
						currentPath.init.call(currentPath);
					});
				}
			}

			$( document ).trigger( 'moduleCreated', [ name ] );
		},

		/**
		 * Provides a pluralized version of a string based on the supplied value
		 *
		 * @param	{string} 	stringKey 	The key of the language string containing the pluralization tag
		 * @param 	{number} 	value 		The value to be pluralized
		 * @returns {void}
		 */
		pluralize = function (stringKey, params) {
			// Get the string we'll work with
			var word = stringKey;

			// Get the pluralization tags from it
			var i = 0;

			if( !_.isArray( params ) ){
				params = [ params ];
			}
			
			word = word.replace( /\{(!|\d+?)?#(.*?)\}/g, function (a,b,c,d) {
				// {# [1:count][?:counts]}
				if( !b || b == '!' ){
					b = i;
					i++;
				}

				var value;
				var fallback;
				var output = '';
				var replacement = params[ b ] + '';

				c.replace( /\[(.+?):(.+?)\]/g, function (w,x,y,z) {
					var xLen = x.length * -1;
					
					if( x == '?' ){
						fallback = y.replace( '#', replacement );
					} 
					else if( x.charAt(0) == '%' && x.substring( 1 ) == replacement.substring( 0, x.substring( 1 ).length ) ){
						value = y.replace( '#', replacement );
					}
					else if( x.charAt(0) == '*' && x.substring( 1 ) == replacement.substr( -x.substring( 1 ).length ) ){
						value = y.replace( '#', replacement );	
					}
					else if( x == replacement ) {
						value = y.replace( '#', replacement );
					}
				});

				output = a.replace( /^\{/, '' ).replace(/\}$/, '' ).replace( '!#', '' );
				output = output.replace( b + '#', replacement ).replace( '#', replacement );
				output = output.replace( /\[.+\]/, value == null ? fallback : value ).trim();

				return output;
			});

			return word;
		},

		testConsole = function () {
			if( window.atob && window.console ){
				console.log( window.atob("ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgd293ICAgICAgICAgICAgICAgICAgICAgICAgICAgI\
CAgIAogICAgICAgICAgICAgICAgICAgICAgICBzdWNoIGZvcnVtICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAga\
G93IGFqYXggICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICB2ZXJ5IGNvbW11bml0eSAgICAgIC\
AgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtdWNoIG1lbWJlcgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg\
ICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCg==")
				);
			}

			return '';
		};

		var templates = function () {
			var _templateStore = {};

			/**
			 * Sets a mustache template
			 *
			 * @param	{string} 	key 		Key name for this template
			 * @param 	{mixed} 	template 	Template, as string or function which returns a string
			 * @returns {void}
			 */
			var set = function (key, template) {
				_templateStore[ key ] = template;
			},

			/**
			 * Return a mustache template
			 *
			 * @param	{string} 	key 		Key name for the template to retrieve
			 * @returns {string}	Template contents
			 */
			get = function (key) {
				if( _templateStore[ key ] ){
					if( _.isFunction( _templateStore[ key ] ) ){
						return _templateStore[ key ]();
					} else {
						return _templateStore[ key ];
					}
				}

				return '';
			},

			/**
			 * Renders a mustache template
			 *
			 * @param	{string} 	key 		Key name for this template
			 * @param 	{object} 	obj 		Object of values with which to render the template
			 * @returns {string} 	The rendered contents
			 */
			render = function (key, obj) {
				// Add some common vars
				obj = _.extend( obj || {}, {
					baseURL: ips.getSetting('baseURL'),
					lang: _lang,
					blankImg: ips.getSetting('blankImg') || ''
				});

				return Mustache.render( get( key ), obj );
			},

			/**
			 * Returns a compile mustache template ready for us
			 *
			 * @param	{string} 	key 		Key name for this template
			 * @returns {function} 	A compiled template function
			 */
			compile = function (key) {
				if( _templateStore[ key ] ){
					return Mustache.parse( get( key ) );
				}

				return $.noop;
			},

			/**
			 * Returns a function that Mustache can use to swap out language strings
			 * Allows {{#lang}}key{{/lang}} to be used in the templates
			 *
			 * @returns {function} 	A closure
			 */
			_lang = function () {
				return function (text, render) {
					return render( ips.getString( text ) );
				}
			};

			return {
				set: set,
				get: get,
				render: render,
				compile: compile
			};
		}();

		return {
			boot: boot,
			createModule: createModule,
			getSetting: getSetting,
			getAllSettings: getAllSettings,
			setSetting: setSetting,
			getGraphQlData: getGraphQlData,
			setGraphQlData: setGraphQlData,
			getAjax: getAjax,
			getContainer: getContainer,
			setString: setString,
			haveString: haveString,
			getString: getString,
			pluralize: pluralize,
			getLocation: getLocation,
			templates: templates,
			testConsole: testConsole
		};

	}());
	
	ips.boot( ipsSettings );
	
	// Extend some core objects with useful methods
	String.prototype.startsWith = function (pattern) {
		return this.lastIndexOf(pattern, 0) === 0;
  	};

}(jQuery, _));

// Polyfill for event.submitter not existing in safari
// Taken from https://gist.github.com/nuxodin/3ae174f2a6a112df3ccad22459237a91
!function(){
	var lastBtn = null
	document.addEventListener('click',function(e){
		if (!e.target.closest) return;
		lastBtn = e.target.closest('button, input[type=submit]');
	}, true);
	document.addEventListener('submit',function(e){
		if ('submitter' in e) return;
		var canditates = [document.activeElement, lastBtn];
		lastBtn = null;
		for (var i=0; i < canditates.length; i++) {
			var candidate = canditates[i];
			if (!candidate) continue;
			if (!candidate.form) continue;
			if (!candidate.matches('button, input[type=button], input[type=image]')) continue;
			e.submitter = candidate;
			return;
		}
		e.submitter = e.target.querySelector('button, input[type=button], input[type=image]');
	}, true);
}();]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="" javascript_name="Debug.js" javascript_type="framework" javascript_version="107643" javascript_position="250"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * Debug.js - A simple logging module. Allows adapters to be passed in to enable
 * alternatives to simple window.console logging.
 *
 * Author: Rikki Tissier
 */

var Debug = Debug || {};

;( function($, _, undefined){
	"use strict";

	Debug = function () {

		var options = {
			enabled: false,
			level: 1,
			adapters: [ ]
		};

		var LEVEL = {
			DEBUG: 1,
			INFO: 2,
			WARN: 3,
			ERROR: 4
		};

		/**
		 * Logs a Debug message
		 *
		 * @param 	{string}	msg 	Message to log
		 * @returns {object} 	Returns Debug
		 */
		var debug = function (msg) {
			logMessage( LEVEL.DEBUG, msg);
			return Debug;
		},

		/**
		 * Logs an Info message
		 *
		 * @param 	{string}	msg 	Message to log
		 * @returns {object} 	Returns Debug
		 */
		info = function (msg) {
			logMessage( LEVEL.INFO, msg );
			return Debug;
		},

		/**
		 * Logs a Warn message
		 *
		 * @param 	{string}	msg 	Message to log
		 * @returns {object} 	Returns Debug
		 */
		warn = function (msg) {
			logMessage( LEVEL.WARN, msg );
			return Debug;
		},

		/**
		 * Logs an Error message
		 *
		 * @param 	{string}	msg 	Message to log
		 * @returns {object} 	Returns Debug
		 */
		error = function (msg) {
			logMessage( LEVEL.ERROR, msg );
			return Debug;
		},

		/**
		 * Checks our debugging level is met, and passes the message off to the
		 * adapter being used
		 *
		 * @param 	{string}	msg 	Message to log
		 * @returns {object} 	Returns Debug
		 */
		logMessage = function (level, message) {

			if( options.enabled && level >= options.level && options.adapters.length ){
				for( var i = 0; i < options.adapters.length; i++ ){
					options.adapters[ i ].write( level, message );
				}
			}

			return Debug;
		},

		/**
		 * Sets the enabled/disabled status of logging
		 *
		 * @param	{boolean}	enabled 	Whether logging should be enabled
		 * @returns {object} 	Returns Debug
		 */
		setEnabled = function (enabled) {
			options.enabled = ( enabled === false ) ? false : true;
			return Debug;
		},

		/**
		 * See whether debugging is enabled
		 *
		 * @returns 	{boolean}
		 */
		isEnabled = function () {
			return options.enabled;
		},

		/**
		 * Sets the debugging severity threshold
		 *
		 * @param 	{number} 	level 	Level to set as minimum threshold
		 * @returns {object} 	Returns Debug
		 */
		setLevel = function (level) {
			if( LEVEL[ level ] ){
				options.level = LEVEL[ level ];
			}

			return Debug;
		},

		/**
		 * Adds an adapter to use for logging
		 *
		 * @param 	{number} 	level 	Level to set as minimum threshold
		 * @returns {object} 	Returns Debug
		 */
		addAdapter = function (adapter) {

			if( _.isObject( adapter ) ){
				options.adapters.push( adapter );
			}

			return Debug;
		},

		/**
		 * Clears all adapters
		 *
		 * @returns 	{object} 	Returns Debug
		 */
		clearAdapters = function () {
			options.adapters = [];
			return Debug;
		};

		return {
			// logging methods
			debug: debug,
			log: debug, // Alias for matt
			info: info,
			warn: warn,
			error: error,

			// other methods
			setEnabled: setEnabled,
			setLevel: setLevel,
			addAdapter: addAdapter,
			clearAdapters: clearAdapters,
			isEnabled: isEnabled
		};
	}();

	/** Default Console adapter	 */
	var Console = function() {};

	Console.prototype.write = function ( level, msg ){
		
		if( window.console ){
			switch( level ){
				case 1:
					if( _.isObject( msg ) ){
						console.dir( msg );
					} else {
						console.log( msg );
					}
					break;
				case 2:
					console.info( msg );
					break;
				case 3:
					console.warn( msg );
					break;
				case 4:
					console.error( msg );
					break;
			}
		}
	};

	Debug.addAdapter( new Console );

	if ( ipsDebug || ( !_.isUndefined( window.localStorage ) && 'localStorage' in window && window['localStorage'] !== null && window.localStorage.getItem('DEBUG') ) ) {
		Debug.setEnabled( true ).setLevel( 'DEBUG' ).info("Enabled logging");
	}
	
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common" javascript_name="ips.controller.js" javascript_type="framework" javascript_version="107643" javascript_position="250"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.controller.js - Base controller handling
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.controller', function(){

		var _controllers = {},
			_autoMixins = {},
			_manualMixins = {},
			_mixins = {},
			_beingLoaded = [],
			_queue = {},
			_prototypes = {},
			instanceID = 1,
			_controllerCaseMap = {
				'core.front.core.autosizeiframe': 'core.front.core.autoSizeIframe'
			};

		/**
		 * Registers a controller
		 *
		 * @param	{string} 	id 			ID of this controller; used to auto-init on dom nodes
		 * @param	{object} 	definition 	Object containing the methods for the controller
		 * @returns {void}
		 */
		var register = function (id, definition) {
			_controllers[ id ] = definition;
			_checkQueue( id );
		},

		/**
		 * Returns boolean denoting whether a controller has been registered
		 *
		 * @param	{string} 	id 		ID of controller to check
		 * @returns 	{boolean}
		 */
		isRegistered = function (id) {
			return !_.isUndefined( _controllers[ id ] );
		},

		/**
		 * Initializes controllers by looking at the dom tree for data-controller attributess
		 *
		 * @param	{element} 	node 		Root node to search in (defaults to document)
		 * @returns {void}
		 */
		init = function () {
			// And also listen for the contentChange event
			$( document ).on('contentChange', function(e, newNode){
				initializeControllers( newNode );
			});

			// Do our initial search
			initializeControllers();
		},

		/**
		 * Registers a controller mixin
		 *
		 * @param 		{string} 	Controller ID this mixin works with
		 * @param 		{boolean} 	Automatically apply mixin to all instances of controller?
		 * @param 		{function} 	Function definition to apply
		 * @returns 	{void}
		 */
		mixin = function (mixinName, controller, auto, mixinFunc) {
			if( _.isFunction( auto ) ){
				mixinFunc = auto;
				auto = false;
			}

			var obj = ( auto ) ? _autoMixins : _manualMixins;
			
			if( _.isUndefined( obj[ controller ] ) ){
				obj[ controller ] = {};
			}

			obj[ controller ][ mixinName ] = mixinFunc;
		},

		/**
		 * Given a node, will find all controllers on the node, initialize the ones that are
		 * available, and instruct the others to be loaded remotely
		 *
		 * @param 		{element} 	Optional node to initialize on
		 * @returns 	{void}
		 */
		initializeControllers = function (node) {
			var controllers = _findControllers( node );
			var needsLoading = {};

			for( var controller in controllers ){

				// If the controller is already registered, we'll init it immediately
				if( isRegistered( controller ) ){
					for( var i = 0; i < controllers[ controller ].length; i++ ){
						
						var elem = controllers[ controller ][i]['elem'];
						var mixins = controllers[ controller ][i]['mixins'];

						initControllerOnElem( elem, controller, mixins );
					}
				// If not, we'll load it then init
				} else {
					needsLoading[ controller ] = controllers[ controller ];
				}
			}
			
			if( _.size( needsLoading ) ){
				_loadControllers( needsLoading )
					.done( function () {
						// No need to initialize here - the register call in the controller itself will trigger a queue check
					});
			}
		},

		/**
		 * Checks queued controllers to see if the given controller ID is needed
		 *
		 * @param 		{string} 	id 		Controller ID being checked
		 * @returns 	{void}
		 */
		_checkQueue = function (id) {

			if( _queue[ id ] && _queue[ id ].length ){
				for( var i = 0; i < _queue[ id ].length; i++ ){
					initControllerOnElem( _queue[ id ][ i ]['elem'], id, _queue[ id ][ i ]['mixins'] );
				} 

				delete _queue[ id ];
			}

			if( _.indexOf( _beingLoaded, id ) ){
				delete _beingLoaded[ _.indexOf( _beingLoaded, id ) ];
			}
		},

		/**
		 * Loads the specified controllers, providing they aren't already being loaded
		 *
		 * @param 		{object} 	needsLoading 	Object of key/value pairs of controllers to load 
		 * @returns 	{void}
		 */
		_loadControllers = function (needsLoading) {
			// Build include paths
			var filePaths = [];
			var deferred = $.Deferred();

			// CHeck whether our controllers are already being loaded
			for( var controller in needsLoading ){
				if( _.indexOf( _beingLoaded, controller ) !== -1 ){
					delete needsLoading[ controller ];
					continue;
				}

				_beingLoaded.push( controller );
				filePaths.push( _buildFilePath( controller ) );
			}

			if( !_.size( needsLoading ) ){
				// All are being loaded, so we're done here
				deferred.resolve();
				return deferred.promise();
			}

			// Add to the queue
			_.extend( _queue, needsLoading );

			ips.loader.get( filePaths ).then( function () {
				deferred.resolve();
			});

			return deferred.promise();
		},

		/**
		 * Builds a controller file path from the provided controller ID
		 *
		 * @param 		{string} 	controllerName 	 Controller ID
		 * @returns 	{string}	File path
		 */
		_buildFilePath = function (controllerName) {
			var bits = controllerName.split('.');

			// Get the URL for this controller
			// The URL will vary depending on whether we're in_dev or not.
			if( ips.getSetting('useCompiledFiles') === false ){
				// If we're in_dev, we can build the URL simply by appending the pieces of the controller ID
				return bits[0] + '/' + bits[1] + '/controllers/' + bits[2] +
						'/ips.' + bits[2] + '.' + bits[3] + '.js';
			} else {
				// If we're not indev, we need to locate the bundle the controller exists in
				try {
					var url = ipsJavascriptMap[ bits[0] ][ bits[1] + '_' + bits[2] ];
					
					if( url.indexOf('?') != -1 ){
						return url + '&v=' + ips.getSetting('jsAntiCache');
					} else {
						return url + '?v=' + ips.getSetting('jsAntiCache');
					}
				} catch (err) {
					return '';
				}
			}
		},

		/**
		 * Searches the provided node for any controllers specified on elements
		 *
		 * @param 		{element} 	node 	Optional node to search on. Defaults to document.
		 * @returns 	{object}	Found controllers, with the key being controller ID, value being array of elements
		 */
		_findControllers = function (node) {
			// 02/03/16 - Allow either dom nodes or jquery objects here.
			// Previously only dom elements were allowed, which meant in most cases
			// we reverted to checking the whole document again, causing some odd behavior.
			if( !_.isElement( node ) && !( node instanceof jQuery ) ){
				node = document;
			}

			var controllersToLoad = {};
				
			$( node ).find('[data-controller]').addBack().each( function (idx, elem){

				if( !$( elem ).data('_controllers') ){
					$( elem ).data('_controllers', []);
				}

				var controllerString = $( elem ).data('controller'),
					controllerList = $( elem ).data('_controllers');

				if( controllerString )
				{
					_getControllersAndMixins( controllerString );

					var controllers = _getControllersAndMixins( controllerString );

					// Loop through each controller on this element
					if( _.size( controllers ) ){
						_.each( controllers, function (val, key) {

							if( controllerList.length && _.indexOf( controllerList, key ) !== -1 ){
								// Already initialized on this element
								return;
							}

							if( controllersToLoad[ key ] ){
								controllersToLoad[ key ].push( { elem: elem, mixins: val } );
							} else {
								controllersToLoad[ key ] = [ { elem: elem, mixins: val } ];
							}
						});
					}
				}
			});
			
			return controllersToLoad;
		},

		/**
		 * Returns controllers and mixins found in the string
		 * Given <pre>controllerOne( mixin1; mixin2 ), controllerTwo, controllerThree</pre>, returns:
		 * <pre>
		 * {
		 * 	controllerOne: [ mixin1, mixin2 ],
		 *	controllerTwo: [],
		 * 	controllerThree: []
		 * }
		 * </pre>
		 *
		 * @returns 	{string}
		 */
		_getControllersAndMixins = function (controllerString) {
			var controllers = {};
			var pieces = controllerString.split(',');

			for( var i = 0; i < pieces.length; i++ ){
				
				pieces[i] = pieces[i].trim();

				// Fix case issues on user-submitted content
				if( !_.isUndefined( _controllerCaseMap[ pieces[i] ] ) ){
					pieces[i] = _controllerCaseMap[ pieces[i] ];
				}

				if( pieces[i].indexOf('(') === -1 ){
					controllers[ pieces[i] ] = [];
					continue;
				}

				var p = pieces[i].match( /([a-zA-Z0-9.]+)\((.+?)\)/i );
				var mixinPieces = [];

				_.each( p[2].split(';'), function (val) {
					mixinPieces.push( val.trim() );
				});

				controllers[ p[1] ] = mixinPieces;
			}

			return controllers;
		},

		/**
		 * Returns an incremental controller ID
		 * Controller IDs are used to enable controllers to identify events that they
		 * emitted themselves.
		 *
		 * @returns 	{string}
		 */
		getInstanceID = function () {
			return 'ipscontroller' + (++instanceID);
		},

		/**
		 * Allows an element to be cleaned externally. If the element passed is not a controller scope,
		 * it'll search down one level of the DOM to find controllers.
		 *
		 * @param 		{element} 	elem 	The element to clean
		 * @returns 	{string}
		 */
		cleanContentsOf = function (elem) {
			Debug.log('Cleaning contents of controller');
			
			$( elem ).find('[data-controller]')
				.each( function () {
					var loopController = $( this );
					var controllers = loopController.data( '_controllerObjs' ) || [];

					if( controllers.length ){
						loopController.data('_controllerObjs', []);
						
						for( var i = 0; i < controllers.length; i++ ){
							controllers[i]._destroy.apply( controllers[i] );
							delete controllers[i];
						}
					}
				});

			// Remove any widgets that exist in this elem
			ips.ui.destructAllWidgets( $( elem ) );
		},

		/**
		 * Initializes a controller instance by creating a new function, extending it with
		 * the controller methods then initializing it on the relevant dom node
		 *
		 * @param	{element} 	elem 			The element that will form the scope of this controller
		 * @param	{string} 	controllerID 	ID of this controller
		 * @returns {void}
		 */
		initControllerOnElem = function (elem, controllerID, mixins) {

			if( !_controllers[ controllerID ] ){
				Debug.error("Controller '" + controllerID + "' has not been registered");
				return;
			}

			if( _.isUndefined( $( elem ).data('_controllers') )){
				$( elem ).data('_controllers', []);
			}

			$( elem ).data('_controllers').push( controllerID );

			if( _.isUndefined( _prototypes[ controllerID ] ) ){
				// Fetch our controller prototype
				_prototypes[ controllerID ] = getBaseController();
				// Extend with our specific controller methods
				$.extend( true, _prototypes[ controllerID ].prototype, _controllers[ controllerID ] );	
			}
			
			// And init
			if( _.isUndefined( $( elem ).data( '_controllerObjs' ) ) ){
				$( elem ).data( '_controllerObjs', [] );
			}

			var controllers = $( elem ).data( '_controllerObjs' );
			var obj = new _prototypes[ controllerID ](elem, controllerID);
			controllers.push( obj );

			// Any mixins?
			// Auto mixins first
			if( !_.isUndefined( _autoMixins[ controllerID ] ) && _.size( _autoMixins[ controllerID ] ) ){	
				_.each( _autoMixins[ controllerID ], function (val, key) {
					_autoMixins[ controllerID ][ key ].call( obj );
				});
			}

			// Then the manually-specified ones
			if( mixins.length ){
				for( var i = 0; i < mixins.length; i++ ){
					if( !_.isUndefined( _manualMixins[ controllerID ] ) && !_.isUndefined( _manualMixins[ controllerID ][ mixins[i] ] ) ){
						_manualMixins[ controllerID ][ mixins[i] ].call( obj );
					}
				}
			}

			if( _.isFunction( obj.initialize ) ){
				obj.initialize.call( obj );
			}

			$( elem ).removeData( '_controller' + controllerID );
			
			$( document ).trigger( 'controllerReady', {
				controllerID: obj.controllerID,
				controllerType: obj.controllerType,
				controllerElem: elem
			});
		},

		/**
		 * Finds controllers within a node that have an ID matching the provided name
		 * Wildcard character * supported at the front or end of the controller parameter
		 *
		 * @param 		{string} 		controller 		Controller name to find
		 * @param 		{element} 		node 			Optional node to search in (document by default)
		 * @returns 	{function}
		 */
		_findSubControllers = function (controller, node) {
			var results = [];

			node = ( node && ( _.isElement( node ) || node.jquery ) ) ? node : document;

			if( controller.indexOf('*') === -1 ){
				results = $( node ).find('[data-controller*="' + controller + '"]');
			} else {
				var pieces = controller.split('.');

				if( pieces[0] == '*' ){
					pieces.shift();
					results = $( node ).find('[data-controller$="' + pieces.join('.') + '"]');
				} else if( pieces[ pieces.length - 1 ] == '*' ){
					pieces.pop();
					results = $( node ).find('[data-controller^="' + pieces.join('.') + '"]');
				}
			}

			return results;
		},

		/**
		 * Returns a new function that will form our controller prototype
		 *
		 * @returns 	{function}
		 */
		getBaseController = function () {

			/** Base controller definition */
			var baseController = function (scope, type) {
				this.controllerType = type;
				this.controllerID = getInstanceID();
				this.scope = $( scope );
				this._eventListeners = [];

				var self = this;

				// Advice methods - inspired by http://javascriptweblog.wordpress.com/2011/05/31/a-fresh-look-at-javascript-mixins/
				// and Twitter Flight
				var adviceFuncs = {
					before: function (baseFn, newFn) {
						return function () {
							newFn.apply( this, arguments );
							return baseFn.apply( this, arguments );
						};
					},
					after: function (baseFn, newFn) {
						return function () {
							var toReturn = baseFn.apply( this, arguments );
							newFn.apply( this, arguments );
							return toReturn;
						}
					},
					around: function (baseFn, newFn) {
						return function () {
							var args = ips.utils.argsToArray( arguments );
							args.unshift( baseFn.bind( this ) );
							return newFn.apply( this, args );
						}
					}
				}

				_.each( ['before', 'after', 'around'], _.bind( function (type) {
					this[ type ] = function (base, fn) {
						if( _.isUndefined( this[ base ] ) || !_.isFunction( this[ base ] ) ){
							Debug.log( "Method '" + base + '" is not present in controller ' + this.controllerID );
							return;
						}

						// Replace our base method with a wrapped version
						this[ base ] = adviceFuncs[ type ]( this[ base ], fn );
					};		

				}, this ) );

				//Debug.info("Initialized " + this.controllerID + " of type " + this.controllerType);
			};

			baseController.method('_destroy', function () {

				Debug.log( 'Destroyed instance ' + this.controllerID + ' of ' + this.controllerType );

				// Remove each event listener that was created in this controller
				if( this._eventListeners.length ){
					for( var i = 0; i < this._eventListeners.length; i++ ){
						var data = this._eventListeners[i];

						if( data['delegate'] ){
							data['elem'].off( data['ev'], data['delegate'], data['fn'] );
						} else {
							data['elem'].off( data['ev'], data['fn'] );	
						}
					}
				}

				if( _.isFunction( this.destroy ) ){
					this.destroy.call( this );
				}

				// Remove reference to scope so that GC can do its thing
				this.scope = null;
			});

			// Searches for controllers within the current, triggers a destroy event and deletes the controller objs
			baseController.method('cleanContents', function () {
				Debug.log('Cleaning contents of controller');
				
				this.scope.find('[data-controller]')
					.each( function () {
						var loopController = $( this );
						var controllers = loopController.data( '_controllerObjs' ) || [];

						if( controllers.length ){
							loopController.data('_controllerObjs', []);
							
							for( var i = 0; i < controllers.length; i++ ){
								controllers[i]._destroy.apply( controllers[i] );
								delete controllers[i];
							}
						}
					});

				// Remove any widgets that exist in this elem
				ips.ui.destructAllWidgets( this.scope );
			});

			baseController.method('trigger', function (elem, ev, data) {

				// Convert silly arguments object to an array
				var args = ips.utils.argsToArray( arguments );

				elem = ( !_.isElement( elem ) && !elem.jquery ) ? this.scope : $( args.shift() );
				ev = args[0];
				data = args[1] || {};

				// Add our origin to the event
				if( !data.stack ){
					data.stack = [];
				} 

				data.stack.push( 'controllers.' + this.controllerType + '.' + this.controllerID );

				elem.trigger( ev, data );

			});

			baseController.method('on', function (elem, ev, delegate, fn) {
				
				// Convert silly arguments object to an array
				var args = ips.utils.argsToArray( arguments );

				// Reconfigure our args as necessary
				elem = ( !_.isElement( elem ) && elem != document && elem != window ) ? this.scope : $( args.shift() );
				ev = args[0];
				fn = ( args.length == 3 ) ? args[2] : args[1];				
				delegate = ( args.length == 3 ) ? args[1] : undefined;

				if( !_.isFunction( fn ) ){
					Debug.warn("Callback function for " + ev + " doesn't exist in " + this.controllerType 
						+ " (" + this.controllerID + ")");
					return;
				}

				// Bind our callback to the controller
				fn = _.bind( fn, this );

				// Set up the event
				if( delegate ){
					elem.on( ev, delegate, fn );
					this._eventListeners.push({
						elem: elem,
						event: ev,
						delegate: delegate,
						fn: fn
					});
				} else {
					elem.on( ev, fn );
					this._eventListeners.push({
						elem: elem,
						event: ev,
						fn: fn
					});
				}
			});

			baseController.method('triggerOn', function (controller, ev, data) {
				var toTrigger = _findSubControllers( controller, this.scope );

				if( !toTrigger.length ){
					return;
				}

				data = data || {};

				// Add our origin to the event
				if( !data.stack ){
					data.stack = [];
				} 

				data.stack.push( 'controllers.' + this.controllerType + '.' + this.controllerID );

				toTrigger.trigger( ev, data );
			});			

			return baseController;
		};

		return {
			initControllerOnElem: initControllerOnElem,
			register: register,
			mixin: mixin,
			isRegistered: isRegistered,
			init: init,
			cleanContentsOf: cleanContentsOf
		};
	});

}( jQuery, _ ));
]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common" javascript_name="ips.loader.js" javascript_type="framework" javascript_version="107643" javascript_position="250"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.loader.js - Loader module
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.loader', function(){

		var _loadedScripts = [],
			_loadingScripts = [];

		/**
		 * Figures out the scripts that have already been inserted into the page
		 *
		 * @returns {void}
		 */
		var init = function () {
			var scripts = $('script[type="text/javascript"][src][data-ips]');

			scripts.each( function () {
				var scriptInfo = ips.utils.url.getURIObject( $( this ).attr('src') );

				if ( scriptInfo.queryKey.src ){
					var paths = _getPathScripts( scriptInfo.queryKey.src );

					_.each( paths, function (value){
						_loadedScripts.push( value );
					});
				} else if( scriptInfo.path.indexOf('interface/') !== -1 ) {
					var interfaces = _getInterfaceScript( scriptInfo.path );

					if( interfaces ){
						_loadedScripts.push( interfaces );
					}	
				} else {
					var other = _getOtherScript( scriptInfo.source );

					if( other ){
						_loadedScripts.push( other );
					}
				}
			});
		},

		/**
		 * Parses a script URL. If the script is local, returns the path from /applications/, otherwise, the whole url.
		 *
		 * @returns {string} 
		 */
		_getOtherScript = function (path) {
			path = path.replace( ips.getSetting('baseURL'), '' );

			if( path.startsWith('/') ){
				path = path.substring(1);
			}

			if( path.startsWith('applications/') ){
				path = path.replace(/^applications\//i, '')
			}

			return path;
		},

		/**
		 * Parses a script URL for the relative path to an interface script
		 *
		 * @returns {mixed} 	Path as a string if an interface file, or false if not
		 */
		_getInterfaceScript = function (path) {
			// Split the path
			var pieces = _.compact( path.split('/').reverse() );
			var path = [];

			for( var i = 0; i < pieces.length; i++ ){
				if( pieces[i] == 'interface' ){
					path.push('interface');
					path.push( pieces[ i+1 ] );
					break;
				}

				path.push( pieces[i] );
			}

			if( _.indexOf( path, 'interface' ) !== -1 ){
				return path.reverse().join('/');
			}

			return false;
		},

		/**
		 * Splits a comma-separated list of paths into individual paths
		 *
		 * @returns {array}
		 */
		_getPathScripts = function (src) {
			return _.compact( src.split(',') );
		},

		/**
		 * Loads a script file. Calls the internal _doLoad method, wrapped in jQuery's when method for deferred
		 *
		 * @param 	{array} 	filePaths 	Array of relative file paths to load
		 * @returns {void}
		 */
		get = function (toLoad) {
			return $.when( _doLoad( _.compact( _.uniq( toLoad ) ) ) );
		},

		/**
		 * Loads a script file remotely
		 *
		 * @param 	{array} 	filePaths 	Array of relative file paths to load
		 * @returns {void}
		 */
		_doLoad = function (filePaths) {
			var deferred = $.Deferred();

			if( !_.isArray( filePaths ) ){
				filePaths = [ filePaths ];
			}

			var done = [];
			var loading = [];
			var toLoad = [];

			// Step 1: Sort each file into done, loading or toLoad
			for( var i = 0; i < filePaths.length; i++ ){
				if( _.indexOf( _loadedScripts, filePaths[ i ] ) !== -1 ){
					done.push( filePaths[ i ] );
					continue;
				}

				if( _.indexOf( _loadingScripts, filePaths[ i ] ) !== -1 ){
					loading.push( filePaths[ i ] );
					continue;
				}

				toLoad.push( filePaths[ i ] );
			}

			// Step 2: If we've already loaded everything, short circuit and resolve the deferred
			if( done.length === filePaths.length ){
				deferred.resolve();
				return deferred.promise();
			}

			// Step 3: If we've got any files to watch (either loading, or toLoad), set an event handler
			if( loading.length || toLoad.length ){
				$( document ).on( 'scriptLoaded', function (e, files) {

					for( var i = 0; i < files.length; i++ ){
						if( _.indexOf( filePaths, files[ i ] ) === -1 ){
							continue;
						}

						done.push( files[ i ] );
					}

					if( done.length === filePaths.length ){
						setTimeout( function () {
							deferred.resolve();	
						}, 100);						
					}
				});
			}

			// Step 4: Load the files that haven't been loaded yet
			// Split them into local and global files and do separate requests for each
			if( toLoad.length ){
				var localFiles = [];
				var remoteFiles = []

				for( var i = 0; i < toLoad.length; i++ ){
					if( toLoad[ i ].match( /^(http|\/\/)/i ) ){
						remoteFiles.push( toLoad[ i ] );
					} else {
						localFiles.push( toLoad[ i ] );
					}
				}

				if( localFiles.length ){
					_insertScript( localFiles );	
				}

				if( remoteFiles.length ){
					for( var i = 0; i < remoteFiles.length; i++ ){
						_insertScript( [ remoteFiles[ i ] ] );
					}
				}				
			}

			return deferred.promise();
		},

		/**
		 * Loads a script file via ajax
		 *
		 * @param 	{array} 	filePaths 	Array of file paths to load
		 * @param 	{boolean} 	[cached]	Whether to allow the browser to use a cached file (default: true)
		 * @returns {void}
		 */
		_insertScript = function (toLoad, cached) {

			// Add URLs to the loading array
			for( var i = 0; i < toLoad.length; i++ ){
				_loadingScripts.push( toLoad[ i ] );
			}

			Debug.log( "Loading: " + toLoad.join(', ') );

			// Figure out the URL
			var url = '';

			if( toLoad[0].match( /^(http|\/\/)/i ) ){
				url = toLoad[0].match( /^http/ ) ? toLoad[0].replace( /^.+?\/\/(.*)$/, '//$1' ) : toLoad[0];
			} else {
				url = ips.getSetting('jsURL') + '?src=' + encodeURIComponent( toLoad.join(',') );
			}

			// Now fetch the script(s) by calling our JS url.
			// On success, we add the script(s) to the _loadedScripts array, and trigger an event so that other methods are aware
			// And we always remove it from the _loadingScripts array when the ajax finishes.
			// In a settimeout so that this happens on the next tick.
			setTimeout( function () {
				$.ajax( {
					dataType: 'script',
					cache: ( _.isUndefined( cached ) ) ? true : cached,
					url: url,
					data: {
						antiCache: ips.getSetting('jsAntiCache'),
						csrfKey: null, // Ensure the CSRF key is empty when fetching JS
					}
				})
					.fail( function (jqXHR, textStatus, errorThrown) {
						Debug.error( "Failed to load: " + toLoad.join(', ') );
						Debug.log( textStatus );
					})
					.always( function () {
						for( var i = 0; i < toLoad.length; i++ ){
							var index = _.indexOf( _loadingScripts, toLoad[ i ] );

							if( index !== -1 ){
								_loadingScripts.splice( index, 1 );
							}
						}
					})
					.done( function () {
						
						// Remove from loading, add to loaded
						for( var i = 0; i < toLoad.length; i++ ){
							_loadedScripts.push( toLoad[ i ] );
						}

						// Trigger event to let observers know
						$( document ).trigger( 'scriptLoaded', [ toLoad ] );
						Debug.log( "Loaded: " + toLoad.join(', ') );
					})
			}, 500);
			
		};

		return {
			init: init,
			get: get
		}
	});
}( jQuery, _ )); ]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common" javascript_name="ips.model.js" javascript_type="framework" javascript_version="107643" javascript_position="250"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.model.js - Base model handling
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.model', function(){

		var _models = {};

		/**
		 * Registers a model. Models are initialized immediately.
		 *
		 * @param	{string} 	id 			ID of this model
		 * @param	{object} 	definition 	Object containing the methods for the model
		 * @returns {void}
		 */
		var register = function (id, definition) {
			//_models[ id ] = definition;

			var Base = getBaseModel();

			// Extend with our specific controller methods
			$.extend( Base.prototype, definition );

			// And init
			var obj = new Base(id);

			if( _.isFunction( obj.initialize ) ){
				obj.initialize.call( obj );
			}
		},

		/**
		 * Returns a new function that will form our model prototype
		 *
		 * @returns 	{function}
		 */
		getBaseModel = function () {

			/*
			 * Base model definition
			 */
			var baseModel = function (id) {
				this.modelID = id;
				//Debug.info("Initialized model " + this.modelID);
			};

			baseModel.method('trigger', function (elem, ev, data) {

				// Convert silly arguments object to an array
				var args = ips.utils.argsToArray( arguments );

				elem = ( !_.isElement( elem ) ) ? $(document) : $( args.shift() );
				ev = args[0];
				data = args[1] || {};

				// Add our origin to the event
				if( !data.stack ){
					data.stack = [];
				} 

				data.stack.push( 'models.' + this.modelID );

				elem.trigger( ev, data );
			});

			baseModel.method('on', function (elem, ev, delegate, fn) {

				// Convert silly arguments object to an array
				var args = ips.utils.argsToArray( arguments );

				// Reconfigure our args as necessary
				elem = ( !_.isElement( elem ) && elem != document ) ? $(document) : $( args.shift() );
				ev = args[0];
				fn = ( args.length == 3 ) ? args[2] : args[1];
				delegate = ( args.length == 3 ) ? args[1] : undefined;

				if( !_.isFunction( fn ) ){
					Debug.warn("Callback function for " + ev + " doesn't exist in " + this.modelID);
					return;
				}

				// Bind our callback to the model
				fn = _.bind( fn, this );

				// Set up the event
				if( delegate ){
					elem.on( ev, delegate, fn );
				} else {
					elem.on( ev, fn );
				}
			});

			baseModel.method('getData', function (data, eventData) {
				var self = this;
				var ajaxObj = ips.getAjax();

				// If this appears to be a local URL, prefix with the baseURL
				if( !data.url.startsWith('http') ){
					data.url = ips.getSetting('baseURL') + 'index.php?' + data.url;
				}

				// See if we're specifying events manually
				// If not, build some event names to use
				if( data.events && _.isString( data.events ) ){
					data.events = {
						loading: data.events + 'Loading',
						done: data.events + 'Done',
						fail: data.events + 'Error',
						always: data.events + 'Finished'
					};
				}

				// Check if namespace exists, add a period if neessary
				if( data.namespace && !data.namespace.startsWith('.') ){
					data.namespace = '.' + data.namespace;
				}

				// Do the loading
				if( data.events.loading ){
					this.trigger( data.events.loading + ( data.namespace || '' ), eventData || {} );
				}

				ajaxObj( data.url, {
					data: data.data || {},
					dataType: data.dataType || 'html',
					type: data.type || 'get'
				})
					.done( function (response) {
						if( data.events.done ){
							if( data.dataType == 'json' ){
								var doneData = _.extend( eventData || {}, response );
							} else {
								var doneData = _.extend( eventData || {}, { response: response } );
							}

							self.trigger( data.events.done + ( data.namespace || '' ), doneData );
						}
					})
					.fail( function (jqXHR) {
						if( data.events.fail ){
							try {
								if( data.dataType == 'json' ){
									var doneData = _.extend( eventData || {}, $.parseJSON( jqXHR.responseText ) );
								} else {
									var doneData = _.extend( eventData || {}, { response: jqXHR.responseText } );
								}
							} catch (err) {}

							self.trigger( data.events.fail + ( data.namespace || '' ), doneData )
						}
					})
					.always( function () {
						if( data.events.always ){
							self.trigger( data.events.always + ( data.namespace || '' ) );
						}
					});
			});

			return baseModel;
		};

		return {
			register: register
		};
	});

}( jQuery, _ ));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.addressForm.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.addressForm.js - Address form element widget
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.addressForm', function(){
		
		var defaults = {
			minimize: false,
			country: "",
			requireFullAddress: true
		};

		var respond = function (elem, options, e) {
			// Don't reinitialize an existing address field
			if( !_.isUndefined( elem.data('initialized') ) ){
				return;
			}

			options = _.defaults( options, defaults );

			if( options.minimize ){
				minimizeAddress( elem, options );
			} else {
				init( elem, options, e );
			}

			elem.data('initialized', true);
		};
			
		var init = function (elem, options, e) {						
			// Watch for country changes (so we can change state/region to a select box if appropriate
			elem.on( 'change', '[data-role="countrySelect"]', _.bind( countryChange, e, elem, options ) );
			$( elem ).find('[data-role="countrySelect"]').change();
			
			// Add a + button for address lines
			recalculateAddAddressLineButton( elem );
		};
		
		var googlePlacesCallback = function(){
			$( window ).trigger( 'googlePlacesLoaded' );
		};

		var minimizeAddress = function (elem, options) {
			var tempInput = $('<input/>')
								.attr( 'type', 'text' )
								.attr( 'data-role', 'minimizedVersion' )
								.attr( 'placeholder', ips.getString('specifyLocation') )
								.on( 'focus', function (e) {
									// Hide the minimized version
									$( this ).hide();

									// Set country if applicable
									if( options.country ){
										$( elem ).find('[data-role="countrySelect"]').val( options.country );
									}
									
									// Init 
									init( elem, options, e );

									// Show the main address fields
									elem.show().find('input').first().focus();
								});

			var value = [];

			// Build the existing value
			elem.find('input, select').each( function (addressPart) {
				if( $( this ).val() ){
					if( $( this ).is('select') ){
						value.push( $( this ).find('option[value="' + $( this ).val() + '"]').text().trim() );
					} else {
						value.push( $( this ).val().trim() );
					}
				}
				
			});

			if( value.length ){
				tempInput.val( value.join(', ') );
			}

			elem
				.hide()
				.after( tempInput );
		};

		var countryChange = function(elem, options, e) {
			ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=ajax&do=states&country=' + $( e.target ).val() )
				.done( function (response) {
					if( response.length ) {
						if( !$( elem ).find('[data-role="regionSelect"]').length )
						{
							var regionText = $( elem ).find('[data-role="regionText"]');
						}
						else
						{
							var regionText = $( elem ).find('[data-role="regionSelect"]');
						}
						
						var regionSelect = $('<select data-role="regionSelect" />');

						regionSelect.attr( 'name', regionText.attr('name') );
						
						if ( !options.requireFullAddress ) {
							regionSelect.append( $('<option />').attr( 'value', '' ).html( $( elem ).find('[data-role="regionText"]').attr('placeholder') ) );
						}

						for( var i = 0; i < response.length; i++ ){
							regionSelect.append( $('<option />').attr( 'value', response[i] ).html( response[i] ) );

							if( response[i].toLowerCase() == regionText.val().toLowerCase() ){
								regionSelect.val( response[i] );
							}
						}

						regionText.replaceWith( regionSelect );
					} else {
						if( !$( elem ).find('[data-role="regionText"]').length ){
							var regionSelect = $( elem ).find('[data-role="regionSelect"]');
							var regionText = $('<input type="text" data-role="regionText" placeholder="' + ips.getString('address_region') + '" />');

							regionText.attr( 'name', regionSelect.attr('name') ).val( "" );
							regionSelect.replaceWith( regionText );
						}
					}
				} );

				if ( typeof elem.attr('data-ipsAddressForm-googlePlaces') !== typeof undefined && elem.attr('data-ipsAddressForm-googlePlaces') !== false ) {
					if ( elem.attr( 'data-ipsAddressForm-googlePlaces' ) === 'loaded' ) {
						googlePlacesInit( elem );
					} else {
						if ( typeof google === 'undefined' ) {
							ips.loader.get( [ 'https://maps.googleapis.com/maps/api/js?key=' + elem.attr('data-ipsAddressForm-googleApiKey') + '&libraries=places&sensor=false&callback=ips.ui.addressForm.googlePlacesCallback' ] );
							$( window ).on( 'googlePlacesLoaded', function(){
								elem.attr( 'data-ipsAddressForm-googlePlaces', 'loaded' );
								googlePlacesInit( elem );
							});
						} else {
							elem.attr( 'data-ipsAddressForm-googlePlaces', 'loaded' );
							googlePlacesInit( elem );
						}
					}
				}
		};
		
		var addAddressLine = function (elem, value) {
			var lastLine = elem.find('[data-role="addressLine"]').closest('li').last();
			var newLine = lastLine.clone();

			if( value ) {
				newLine.find('input').focus().val( value );
			}

			lastLine.after( newLine );
		};
		
		var recalculateAddAddressLineButton = function (elem) {
			elem.find( '[data-role="addAddressLine"]' ).remove();
			var button = $('<i class="fa fa-plus" style="cursor:pointer; margin-left: 4px" data-role="addAddressLine">');

			button.click( function () {
				addAddressLine(elem, '');
				recalculateAddAddressLineButton(elem);
			});

			elem.find('[data-role="addressLine"]').last().after( button );
		};
		
		var googlePlacesInit = function (elem) {
			var googlePlacesInput = $(elem).find('[data-role="addressLine"]').first();
			var options = {
				types: [ 'geocode' ],
				componentRestrictions: { country: $(elem).find('[data-role="countrySelect"]').val() }
			};
			var autocomplete = new google.maps.places.Autocomplete( googlePlacesInput.get(0), options );

			googlePlacesInput.on( 'focus', function () {
				if( navigator.geolocation ){
					navigator.geolocation.getCurrentPosition( function (position) {
						var geolocation = new google.maps.LatLng( position.coords.latitude,position.coords.longitude );
						autocomplete.setBounds( new google.maps.LatLngBounds( geolocation, geolocation ) );
					});
				}
			} );
			googlePlacesInput.on( 'keypress', function(e) {
				if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
					return false;
				}
			} );

			google.maps.event.addListener( autocomplete, 'place_changed', function () {
				var place = autocomplete.getPlace();
				
				for( var i = 0; i < $( elem ).find('[data-role="addressLine"]').length; i++ ){
					$( elem ).find('[data-role="addressLine"]').val( '' );
				}
				var parsedAddress = $( '<div>' + place.adr_address + '</div>' );
				
				var addressLines = [];
				var existingAddressLines = $( elem ).find('[data-role="addressLine"]').length;
				if ( parsedAddress.find('.post-office-box').length ) {
					addressLines.push( parsedAddress.find('.post-office-box').text() );
				}
				if ( parsedAddress.find('.street-address').length ) {
					addressLines.push( parsedAddress.find('.street-address').text() );
				}
				if ( parsedAddress.find('.extended-address').length ) {
					addressLines.push( parsedAddress.find('.extended-address').text() );
				}
				for( var i = 0; i < addressLines.length; i++ ){
					if( existingAddressLines ){
						$( elem ).find('[data-role="addressLine"]').slice( i, 1 ).focus().val( addressLines[i] );
						existingAddressLines--;
					} else {
						addAddressLine(elem, addressLines[i]);
					}
				}
				
				if ( parsedAddress.find('.locality') ) {
					elem.find('[data-role="city"]').focus().val( parsedAddress.find('.locality').text() );
				}
				if ( parsedAddress.find('.region') ) {
					elem.find('[data-role="regionText"]').focus().val( parsedAddress.find('.region').text() );
					
					if ( elem.find('[data-role="regionSelect"] option[value="' + parsedAddress.find('.region').text() + '"]').length ) {
						elem.find('[data-role="regionSelect"]').val( parsedAddress.find('.region').text() );
					} else {
						var i;
						for( i in place.address_components ) {
							if ( place.address_components[i].types[0] === 'administrative_area_level_1' || place.address_components[i].types[0] === 'administrative_area_level_2' ) {
								if ( elem.find('[data-role="regionSelect"] option[value="' + place.address_components[i].long_name + '"]').length ) {
									elem.find('[data-role="regionSelect"]').val( place.address_components[i].long_name );
									break;
								} else if ( elem.find('[data-role="regionSelect"] option[value="' + place.address_components[i].short_name + '"]').length ) {
									elem.find('[data-role="regionSelect"]').val( place.address_components[i].short_name );
									break;
								}
							}
						}
					}
				}
				if ( parsedAddress.find('.postal-code') ) {
					elem.find('[data-role="postalCode"]').focus().val( parsedAddress.find('.postal-code').text() );
				}
			});
		}

		// Register this module as a widget to enable the data API and
		// jQuery plugin functionality
		ips.ui.registerWidget( 'addressForm', ips.ui.addressForm, ['minimize','country','requireFullAddress'] );

		return {
			respond: respond,
			googlePlacesCallback: googlePlacesCallback
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.alert.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350">/* global ips, _, Debug */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.alert.js - Alert widget for alerts, prompts, confirms.
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.createModule('ips.ui.alert', function(){

		var respond = function (elem, options) {
			alertObj( options, elem );
		},

		show = function (options) {
			alertObj( options );
		};

		ips.ui.registerWidget('alert', ips.ui.alert, 
			[ 'message', 'type', 'icon', 'focus' ]
		);

		return {
			respond: respond,
			show: show
		};
	});

	/**
	 * Alert instance
	 *
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var alertObj = function (options) {

		var alert = null,
			modal = null;

		var _defaults = {
			type: 'alert', // alert, confirm, prompt, verify
			message: ips.getString('generic_confirm'),
			buttons: {
				ok: ips.getString('ok'),
				cancel: ips.getString('cancel'),
				yes: ips.getString('yes'),
				no: ips.getString('no')
			},
			icon: 'info',
			showIcon: true,
			callbacks: {}
		};

		var _icons = {
			warn: 'fa fa-exclamation-triangle',
			success: 'fa fa-check-circle',
			info: 'fa fa-info-circle',
			ok: 'fa fa-check-circle',
			question: 'fa fa-question-circle'
		};

		options = _.defaults( options || {}, _defaults );

		/**
		 * Initialization
		 *
		 * @returns {void}
		 */
		var init = function () {
			// Build alert
			_buildAlert();
			_setUpEvents();
		},

		/**
		 * Set up events for the alert - button clicks, and document events for keyboard accessibility
		 *
		 * @returns {void}
		 */
		_setUpEvents = function () {
			alert.on( 'click', '[data-action]', _clickButton );

			$( document ).on('keydown', function (e) {
				switch( e.keyCode ){
					case ips.ui.key.ESCAPE:
						if( options.type == 'alert' ){
							alert.find('[data-action=&quot;ok&quot;]').click();
						} else {
							alert.find('[data-action=&quot;cancel&quot;], [data-action=&quot;no&quot;]').click();
						}
					break;
					case ips.ui.key.ENTER:
						alert.find('[data-action=&quot;ok&quot;], [data-action=&quot;yes&quot;]').click();
					break;
				}
			});	
		},

		/**
		 * Event handler for clicking a button in the alert
		 * Looks for a callback to execute, then removes the alert from the dom
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		_clickButton = function (e) {
			var button = $( e.currentTarget );
			var action = button.attr('data-action');
			var value = null;

			if( options.type == 'prompt' ){
				value = alert.find('[data-role=&quot;promptValue&quot;]').val();
			}

			if( _.isFunction( options.callbacks[ action ] ) ){
				options.callbacks[ action ]( value );
			}

			_remove();
		},

		/**
		 * Removes the alert from the dom and unsets the events
		 *
		 * @returns {void}
		 */
		_remove = function () {
			modal.remove();

			ips.utils.anim.go( 'fadeOutDown fast', alert ).done( function () {
				alert.remove();
			});
		},

		/**
		 * Builds the alert element based on options
		 *
		 * @returns {void}
		 */
		_buildAlert = function () {

			var parts = {},
				buttons = [];

			// Icon
			if( options.showIcon ){
				parts.icon = ips.templates.render( 'core.alert.icon', {
					icon: _icons[ options.icon ] || options.icon
				});
			}

			parts.id = 'alert_' + ( Math.round( Math.random() * 10000000 ) );
			parts.text = options.message;

			if( options.subText ){
				parts.subtext = ips.templates.render( 'core.alert.subText', {
					text: options.subText
				});
			}

			if( options.subTextHtml ){
				parts.subtext = ips.templates.render( 'core.alert.subTextHtml', {
					text: options.subTextHtml
				});
			}

			// Build buttons
			switch( options.type ){
				case 'alert':
					buttons.push( ips.templates.render( 'core.alert.button', {
						action: 'ok',
						title: options.buttons.ok,
						extra: 'ipsButton_primary'
					}));
				break;
				case 'confirm':
				case 'prompt':
					buttons.push( ips.templates.render( 'core.alert.button', {
						action: 'ok',
						title: options.buttons.ok,
						extra: 'ipsButton_primary'
					}));
					buttons.push( ips.templates.render( 'core.alert.button', {
						action: 'cancel',
						title: options.buttons.cancel,
						extra: 'ipsButton_light'
					}));
				break;
				case 'verify':
					buttons.push( ips.templates.render( 'core.alert.button', {
						action: 'yes',
						title: options.buttons.yes,
						extra: 'ipsButton_primary'
					}));
					if( options.buttons.no ){
						buttons.push( ips.templates.render( 'core.alert.button', {
							action: 'no',
							title: options.buttons.no,
							extra: 'ipsButton_light'
						}));
					}
					if( options.buttons.cancel ){
						buttons.push( ips.templates.render( 'core.alert.button', {
							action: 'cancel',
							title: options.buttons.cancel,
							extra: 'ipsButton_light'
						}));
					}
				break;
			}

			parts.buttons = buttons.join('');

			// Prompt?
			if( options.type == 'prompt' ){
				parts.text += ips.templates.render( 'core.alert.prompt');
			}

			// Build box
			var tmpAlert = $.parseHTML( ips.templates.render( 'core.alert.box', parts ).trim() );
			alert = $( tmpAlert[0] );
			$('body').append( alert );

			// Build modal
			modal = ips.ui.getModal();
			modal.css({ zIndex: ips.ui.zIndex() }).show();

			alert.css( { zIndex: ips.ui.zIndex() } );
			ips.utils.anim.go('zoomIn fast', alert );

			// Focus the appropriate element
			if( options.focus ){
				alert
					.find('[data-action=&quot;' + options.focus + '&quot;]')
						.focus();
			} else {
				if( options.type == 'prompt' ){
					alert
						.find('[data-role=&quot;promptValue&quot;]')
							.val( options.value || '' )
							.focus();
				} else {
					alert
						.find('[data-action=&quot;ok&quot;], [data-action=&quot;yes&quot;]')
							.focus();
				}
			}
		};

		init();

	};
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.autoCheck.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/* global ips, _ */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.autoCheck.js -Enables easy 'filtering' of checkboxes in tables
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.autoCheck', function(){

		var defaults = {};

		var respond = function (elem, options) {
			if( !$( elem ).data('_autoCheck') ){
				$( elem ).data('_autoCheck', autoCheckObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Destruct the autoCheck widgets in elem
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		},

		/**
		 * Retrieve the autoCheck instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The autocheck instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_autoCheck') ){
				return $( elem ).data('_autoCheck');
			}

			return undefined;
		};

		ips.ui.registerWidget('autoCheck', ips.ui.autoCheck, 
			[ 'context' ]
		);

		return {
			respond: respond,
			getObj: getObj,
			destruct: destruct
		};
	});

	/**
	 * autoCheck instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var autoCheckObj = function (elem, options) {
		/**
		 * Store a count of items from other pages
		 */
		var initialCount = 0;

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			if( options.context && $( options.context ).length ){
				elem.on('menuItemSelected', clickedMenu); // Watch for the menu event
				$( options.context ).on( 'change', 'input[type="checkbox"][data-state]', _updateCount);
			}

			elem.on('refresh.autoCheck', refresh);
			elem.on('setInitialCount.autoCheck', setInitialCount);
			elem.find('[data-role="autoCheckCount"]').hide();
		},

		/**
		 * Destruct
		 * Removes event handlers assosciated with this instance
		 *
		 * @returns {void}
		 */
		destruct = function () {
			if( options.context ){
				$( options.context ).off( 'change', 'input[type="checkbox"][data-state]', _updateCount );
			}
		},

		/**
		 * Refreshes the autocheck
		 *
		 * @returns {void}
		 */
		refresh = function () {
			_updateCount();
		},

		/**
		 * Set the initial count
		 *
		 * @returns {void}
		 */
		setInitialCount = function ( e, data ) {
			initialCount = data.count;
			_updateCount();
		},

		/**
		 * One of the selection options has been chosen from the menu
		 *
		 * @param	{event} 	e 			The event object
		 * @param	{object} 	data 		The event data object from the menu widget
		 * @returns {void}
		 */
		clickedMenu = function (e, data) {
			
			if( !_.isUndefined( data.originalEvent ) ){
				data.originalEvent.preventDefault();
			}

			// Make sure we have a value
			if( !data.selectedItemID ){
				return;
			}

			var checkboxes = $( options.context ).find(':checkbox[data-state]');

			if( data.selectedItemID == 'all' ){
				checkboxes.prop( 'checked', true );
			} else if( data.selectedItemID == 'none' ){
				checkboxes.prop( 'checked', false );
			} else {
				checkboxes
					.prop( 'checked', false )
					.filter( '[data-state~="' + data.selectedItemID + '"]' )
						.prop( 'checked', true );
			}

			// Trigger an event
			checkboxes.trigger('change');

			var count = _updateCount();

			// Trigger an event
			elem.trigger('autoChecked', {
				menu: elem,
				currentFilter: data.selectedItemID,
				count: count
			});
		},

		/**
		 * Updates the count of selected items
		 *
		 * @returns {number}	Count of selected items
		 */
		_updateCount = function () {
			var checkboxes = $( options.context ).find(':checkbox[data-state]');

			// Now get an up to date count
			var count = $( options.context ).find(':checkbox[data-state]:checked');

			if( count.length == checkboxes.length && checkboxes.length !== 0 ){
				elem.find('.cAutoCheckIcon').html('<i class="fa fa-check-square"></i>');
			} else if( count.length === 0 ){
				elem.find('.cAutoCheckIcon').html('<i class="fa fa-square-o"></i>');
			} else {
				elem.find('.cAutoCheckIcon').html('<i class="fa fa-minus-square"></i>');
			}

			var countToDisplay = count.length + initialCount;

			if( countToDisplay ){
				elem.find('[data-role="autoCheckCount"]').text( countToDisplay ).show();
			} else if( elem.find('[data-role="autoCheckCount"]').is(':visible') ) {
				// We use a timeout because more than one setInitialCount call can be sent, so if a 0 comes in and then a 1, the fadeout will take
				// too long and a hide() call will occur after the show() call above since the fadeOut takes time.
				setTimeout( function(){
					if( !initialCount )
					{
						ips.utils.anim.go( 'fadeOut', elem.find('[data-role="autoCheckCount"]') );
					}
				}, 300 );
			}

			return count.length;
		};

		init();

		return {
			destruct: destruct,
			refresh: refresh
		};
	};

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.autocomplete.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/* global ips, _, Debug */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.autocomplete.js - Autocomplete widget for text fields
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.autocomplete', function(){

		var defaults = {
			multiValues: true,
			freeChoice: true,
			itemSep: { chr: ',', keycode: 188, charcode: 44 },
			disallowedCharacters: JSON.stringify( [ "<", ">", "'", "\"" ] ),
			unique: false,
			customValues: true,
			fieldTemplate: 'core.autocomplete.field',
			resultsTemplate: 'core.autocomplete.resultWrapper',
			resultItemTemplate: 'core.autocomplete.resultItem',
			tokenTemplate: 'core.autocomplete.token',
			addTokenTemplate: 'core.autocomplete.addToken',
			addTokenText: ips.getString( 'add_tag' ),
			queryParam: 'q',
			forceLower: false,
			minLength: 1,
			minAjaxLength: 1,
			commaTrigger: true,
			searchFieldThreshold: 20
		};

		var respond = function (elem, options) {
			if( !$( elem ).data('_autocomplete') ){
				$( elem ).data('_autocomplete', autocompleteObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Destruct the autocomplete widget on this elem
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		},

		/**
		 * Retrieve the autocomplete instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The dialog instance or undefined
		 */
		getObj = function (elem) {
			elem = $( elem );

			if( elem.data('_autocomplete') ){
				return elem.data('_autocomplete');
			} else if(  $( '[name="' + elem.attr('name') + '_original' + '"]' ).length &&  $( '[name="' + elem.attr('name') + '_original' + '"]' ).data('_autocomplete') ){
				return  $( '[name="' + elem.attr('name') + '_original' + '"]' ).data('_autocomplete');
			}

			return undefined;
		};

		ips.ui.registerWidget('autocomplete', ips.ui.autocomplete, 
			[ 'multiValues', 'freeChoice', 'dataSource', 'maxItems', 'itemSep', 'resultsElem', 'unique', 'commaTrigger', 
				'fieldTemplate', 'resultsTemplate', 'resultItemTemplate', 'tokenTemplate', 'addTokenTemplate',
				'addTokenText', 'queryParam', 'minLength', 'maxLength', 'forceLower', 'disallowedCharacters', 'minAjaxLength' ]
		);

		return {
			respond: respond,
			destruct: destruct,
			getObj: getObj
		};
	});

	/**
	 * autocomplete instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var autocompleteObj = function (elem, options, e) {

		var timer,
			blurTimer,
			lastValue = '',
			originalTextField,
			valueField,
			textField,
			dataSource,
			elemID = $( elem ).identify().attr('id'),
			wrapper,
			inputItem,
			resultsElem,
			selectedToken,
			disabled = false,
			required = false,
			tooltip = null,
			tooltipTimer = null,
			mouseOverResults = false,
			hasError = false;

		/**
		 * Sets up this instance. The datasource object is chosen depending on what options and/or
		 * attributes are provided. We can fetch results from a local <datalist>, remotely via ajax
		 * or not look up results from a data source at all.
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			if( $( elem ).is('textarea, input[type="text"], input[type="search"]') ){
				originalTextField = $( elem );
			} else {
				originalTextField = $( elem ).find('textarea, input[type="text"], input[type="search"]').first();
			}

			try {
				options.disallowedCharacters = $.parseJSON( options.disallowedCharacters );
			} catch (err) {
				Debug.error("Couldn't parse disallowed characters option");
			}

			// Add our autocomplete wrapper to the page, and move the element into it
			_buildWrapper();

			// Set up the data source for this control
			_getDataSource();

			// Remove list from original field
			originalTextField.removeAttr('list');

			// Build the list
			if( dataSource.type != 'none' ){
				_buildResultsList();

				if( dataSource.type == 'local' )
				{
					_initAutocomplete();
				}
			}

			if( originalTextField.is(':disabled') ){
				disabled = true;
			}

			if( originalTextField.is('[required]') ){
				required = true;
				originalTextField
					.removeProp('required')
					.removeAttr('aria-required');
			}

			// Turn off autocomplete and spellcheck so the browser menu doesn't get in the way
			textField
				.prop( 'autocomplete', 'off' )
				.prop( 'spellcheck', false )
				.prop( 'disabled', disabled )
				.attr( 'aria-autocomplete', 'list' )
				.attr( 'aria-haspopup', 'true' )
				.attr( 'tabindex', originalTextField.attr('tabindex') || '' );

			if( options.maxLength ){
				textField.attr( 'maxlength', options.maxLength + 1 );
			}

           $( document ).on( 'click', _documentClick );

            wrapper.click(function(e) {
                e.stopPropagation();
                return false;
            });

			// Set up events
			textField
				.on( 'focus', _focusField )
				.on( 'blur', _blurField )
				.on( 'keydown', _keydownField )
				.on( 'keyup', _keyupField )
				.on( 'input', _expandField )
				.on( 'keypress', _keypressField );

			wrapper
				.on( 'click', _clickWrapper )
				.on( 'click', '[data-action="addToken"]', _clickAddToken )
				.on( 'keydown', _keydownWrapper )
				.on( 'propChanged', _propChanged )
				.toggleClass( 'ipsField_autocompleteDisabled', disabled );

			_buildInitialTokens();

			elem
				.on( 'blur', function () {
					// For closed tagging, our text field is the same element as the autocomplete widget
					// In that case, we don't want to blur otherwise we go into a death loop.
					if( textField !== elem ){
						textField.trigger('blur');
					}
				})
				.trigger( 'autoCompleteReady', {
					elemID: elemID,
					elem: elem,
					currentValues: tokens.getValues()
				});

			// Some widgets like prefixedAutocomplete may not set up until after this UI module runs, so make sure we trigger the 'ready' event when needed
			elem.on( 'reissueReady', function() {
				elem.trigger( 'autoCompleteReady', {
					elemID: elemID,
					elem: elem,
					currentValues: tokens.getValues()
				});
			});
		},
		
		/**
		 * Destruct
		 * Removes event handlers assosciated with this instance
		 *
		 * @returns {void}
		 */
		destruct = function () {
			$( document ).off( 'click', _documentClick );
		},

		/**
 		 * Determines whether any errors are present in the autocomplete widget (e.g. duplicates)
		 *
		 * @returns {void}
		 */
		hasErrors = function () {
			return hasError;
		},

		/**
 		 * Responds to the propChange event, which we use to determine whether the original field has been toggled
		 *
		 * @returns {void}
		 */
		_propChanged = function (e) {
			disabled = originalTextField.is(':disabled');

			wrapper.toggleClass( 'ipsField_autocompleteDisabled', disabled );
		},

		/**
 		 * Builds tokens from whatever values exist in the original text field
		 *
		 * @returns {void}
		 */
		_buildInitialTokens = function () {
			var value = _getOriginalValue();
			
			if( !value ){
				return;
			}

			// Get individual values
			var splitValues = _.without( value.split( "\n" ), '' );
			var itemCount = 0;
			itemCount = splitValues.length;
			
			// Clear field, as tokens.add() will re-add value
			originalTextField.val('');

			if( splitValues.length ){
				for( var i = 0; i < itemCount; i++ ){
					_addToken( splitValues[i] );
				}
			}
		},

		/**
 		 * Returns the value from the original text field (i.e. the value provided by the backend on pag load)
		 *
		 * @returns {string}
		 */
		_getOriginalValue = function () {
			return originalTextField.val();
			//return _.unescape( originalTextField.val() ).replace("&#039;", "'").replace("&apos;", "'");
		},

		/**
 		 * Builds the element that results will appear in
		 *
		 * @returns {void}
		 */
		_buildResultsList = function () {

			if( options.resultsElem && $( options.resultsElem ).length ){
				resultsElem = $( options.resultsElem );
				return;
			}

			var resultsList = ips.templates.render( options.resultsTemplate, {
				id: elemID
			});

			wrapper.append( resultsList );

			resultsElem = $('#' + elemID + '_results');

			resultsElem
				.on('mouseover', '[data-value]', function (e) {
					results.select( $( e.currentTarget ) );
				})
				.on('mouseenter', function () {
					mouseOverResults = true;
				})
				.on('mouseleave', function () {
					mouseOverResults = false;
				})
				.on('click', '[data-value]', function (e) {
					_addToken( $( e.currentTarget ).attr('data-value') );
					textField.focus();
				})
				.attr( 'aria-busy', 'false' );
		},

		/**
		 * Builds an autocomplete search field for the list of tokens
		 *
		 * @returns {void}
		 */
		_initAutocomplete = function() {
			if( dataSource.totalItems() > options.searchFieldThreshold && !options.freeChoice ){
				var searchField = ips.templates.render( 'core.autocomplete.searchTypeAhead', {} );

				$('#' + elemID + '_results').prepend( searchField );

				// Set up events
				$('#' + elemID + '_results').on( 'keyup', 'input[type="search"]', _keyupAutocomplete );
			}
		},

		/**
		 * Character entered in the autocomplete field - update our results
		 *
		 * @returns {void}
		 */
		_keyupAutocomplete = function(e) {
			_loadResults( $(this).val() );
			return true;
		},

		/**
 		 * Builds the wrapper element that looks like a text input, but allows us to insert
 		 * items as tokens
		 *
		 * @returns {void}
		 */
		_buildWrapper = function () {
			var existingClasses = elem[0].className;
			
			$( elem )
				.after( ips.templates.render( options.fieldTemplate, {
					id: elemID
				}))
				.removeClass( existingClasses );

			wrapper = $( '#' + elemID + '_wrapper' );
			inputItem = $( '#' + elemID + '_inputItem' );

			// If users have to choose from a predefined list, we'll hide the text field
			// and build a link which will show the results panel
			if( !options.freeChoice ){
				var insertElem = ips.templates.render('core.autocomplete.addToken', {
					text: options.addTokenText
				});

				textField = elem;
			} else {
				var insertElem = $('<input/>').attr( {
					type: 'text',
					id: elemID + '_dummyInput'
				})
				.prop( 'autocomplete', 'off' );

				textField = insertElem;
			}

			// Make a copy of the original text field using its name. This is because it's difficult to set
			// arbitrary values in the original text field later if it's associated with a datalist.
			var name = originalTextField.attr('name');

			originalTextField.attr( 'name', originalTextField.attr('name') + '_original' );
			valueField = $('<textarea/>').attr( 'name', name ).hide();

			originalTextField.hide();

			// Move any classnames on the original element onto our new wrapper to maintain styling,
			// then move the original element into our reserved list element
			wrapper
				.addClass( existingClasses )
				.append( elem )
				.append( valueField )
				.find('#' + elemID + '_inputItem')
					.append( insertElem );

			// Set events for clicking on tokens
			wrapper
				.on('click', '[data-value]', function (e) {
					if( !disabled ){
						tokens.select( $( e.currentTarget ) );
					}
				})
				.on('click', '[data-action="delete"]', function (e) {
					_deleteToken( $( e.currentTarget ).parent('[data-value]') );
				});
		},

		/**
 		 * Gets the apprioriate data source for this control
		 *
		 * @returns {void}
		 */
		_getDataSource = function () {
			if( ( options.dataSource && options.dataSource.indexOf('#') === 0 && $( options.dataSource ).length ) ||
					originalTextField.is('[list]') ){
				dataSource = localData( 
					originalTextField.is('[list]') ? $('#' + originalTextField.attr('list') ) : options.dataSource,
					options
				);
			} else if( ips.utils.validate.isUrl( options.dataSource ) ){
				dataSource = remoteData( options.dataSource, options );
			} else {
				dataSource = noData();
			}
		},

		/**
 		 * When the wrapper is clicked, we see if a token was clicked. If it was, select it. If not, focus the textbox.
		 *
		 * @returns {void}
		 */
		_clickWrapper = function (e) {
			if( $( e.target ).is('[data-token]') || $( e.target ).parents('li[data-token]').length ){
				var token = ( $( e.target )  );
			} else {				
				if( !$( e.target ).is( textField ) && ( !resultsElem || !$.contains( resultsElem.get(0), e.target ) ) ){
					textField.focus();
				}
			}
		},

		/**
 		 * Event handler for focusing on the text field
		 *
		 * @returns {void}
		 */
		_clickAddToken = function (e) {
			e.preventDefault();

			if( resultsElem && resultsElem.is(':visible') ){
				_closeResults();
			} else {
				_loadResults('');
			}
		},

		/**
 		 * Focus the autocomplete field
		 *
		 * @returns {void}
		 */
		focus = function (e) {
			textField.focus();
		},

		/**
 		 * Event handler for focusing on the text field
		 *
		 * @returns {void}
		 */
		_focusField = function (e) {
			if( dataSource.type == 'none' ){
				return;
			}

			timer = setInterval( _timerFocusField, 400 );
		},

		/**
 		 * Event handler for blurring on the text field
		 *
		 * @returns {void}
		 */
		_blurField = function (e) {
			if( mouseOverResults ){
				return;
			}
			
			clearInterval( timer );

			_.delay( _timerBlurField, 300 );
		},

		/**
 		 * Timed event hides the results list
		 *
		 * @returns {void}
		 */
		_timerBlurField = function () {
			// See #47772
			/*if( dataSource.type == 'none' ){
				return;
			}*/

			if( textField.val() ){
				_addTokenFromCurrentInput();
			}

			_closeResults();
		},

		/**
 		 * Timed event, checks whether the value has changed, and fetches the results
		 *
		 * @returns {void}
		 */
		_timerFocusField = function () {
			if( dataSource.type == 'none' ){
				return;
			}

			// Fetch the current item value
			var currentValue = _getCurrentValue();

			// If the value hasn't changed, we can leave
			if( currentValue == lastValue ){
				return;
			}

			lastValue = currentValue;

			_loadResults( currentValue );
		},

		/**
 		 * Requests results from the data source, and shows/hides the loading widget
 		 * while that is happening.
		 *
		 * @returns {void}
		 */
		_loadResults = function (value) {
			_toggleLoading('show');

			// Set elem to busy
			resultsElem.attr( 'aria-busy', 'true' );

			// Get the results
			dataSource.getResults( value )
				.done( function (results) {
					// Show the results after processing them
					_showResults( _processResults( results, value ) );
				})
				.fail( function () {

				})
				.always( function () {
					resultsElem.attr( 'aria-busy', 'false' );
					_toggleLoading('hide');
				});
		},

		/**
 		 * Toggles the loading thingy in the control to signify data is loading
		 *
		 * @param 	{string} 	doWhat 	 Acceptable values: 'show' or 'hide'
		 * @returns {void}
		 */
		_toggleLoading = function (doWhat) {
			if( doWhat == 'show' ){
				wrapper.addClass('ipsField_loading');
			} else {
				wrapper.removeClass('ipsField_loading');
			}
		},

		/**
 		 * Closes the suggestions menu, sets the aria attrib, and tells the data source
 		 * to stop loading new results
		 *
		 * @returns {void}
		 */
		_closeResults = function (e) {
			if( e ){
				e.preventDefault();
			}

			if( resultsElem && resultsElem.length ){
				resultsElem
					.hide()
					.attr('aria-expanded', 'false');	

				if( resultsElem.find('input[type="search"]').length ){
					resultsElem.find('input[type="search"]').val('');
				}
			}

			dataSource.stop();
		},

		/**
 		 * Handles a click on the document, closing the results dropdown
		 *
		 * @returns {void}
		 */
		_documentClick = function () {
			_closeResults();
		},

		/**
 		 * Processes the results that are returned by the data source
		 *
		 * @returns {void}
		 */
		_processResults = function (results, text) {
			var existingTokens = tokens.getValues(),
				newResults = {};

			if( options.unique ){
				$.each( results, function (key, data) {
					if( !data.value || _.indexOf( existingTokens, data.value ) === -1 ){
						newResults[ key ] = data;
					}
				});

				return newResults;
			}

			return results;
		},

		/**
 		 * Gets the current item value from the text field
		 *
		 * @returns {string}
		 */
		_showResults = function (results) {

			var output = '';

			$.each( results, function (idx, value) {
				output += ips.templates.render( options.resultItemTemplate, value );
			});
			
			if( resultsElem.attr('id') == ( elemID + '_results' ) ){
				_positionResults();
			}

			// We need to clear out the results element, but without removing our search field if it's present
			resultsElem.find('[data-role="items"] li').remove();

			resultsElem
				.show()
				.attr('aria-expanded', 'true')
				.find('[data-role="items"]')
					.append( output );

			if( resultsElem.find('input[type="search"]').length ){
				resultsElem.find('input[type="search"]').focus();
			}
		},

		/**
 		 * Sizes and positions the results menu to match the wrapper
		 *
		 * @returns {void}
		 */
		_positionResults = function () {
			$( resultsElem ).css({
				left: "0px",
				top: wrapper.outerHeight() + 'px',
				width: wrapper.outerWidth() + 'px',
				position: 'absolute',
				zIndex: ips.ui.zIndex()
			});
		},

		/**
 		 * Gets the current item value from the text field
		 *
		 * @returns {string}
		 */
		_getCurrentValue = function () {
			var value = textField.val();

			if( options.multiValues ){
				if( value.indexOf( options.itemSep.chr ) === -1 || !options.commaTrigger ){
					// Multi items, but only one entered so far
					return value.trim();
				} else {
					// Get the last-entered item
					var pieces = value.split( options.itemSep.chr );
					return pieces[ pieces.length - 1 ].trim();
				}
			} else {
				return value;
			}
		},

		/**
 		 * Event handler for keydown event in wrapper.
 		 * We check for esape here, because if options.freeChoice is disabled, there's no textbox to
 		 * watch for events. By watching for escape on the wrapper, we can still close the menu.
		 *
		 * @returns {void}
		 */
		_keydownWrapper = function (e) {
			if( e.which == ips.ui.key.ESCAPE ){
				keyEvents.escape(e);
			}
		},

		/**
 		 * Event handler for keydown event in text field
		 *
		 * @returns {void}
		 */
		_keydownField = function (e) {
			_expandField();
			var ignoreKey = false;

			// Ignore irrelevant keycodes
			if( !_( [ ips.ui.key.UP, ips.ui.key.DOWN, ips.ui.key.LEFT, ips.ui.key.RIGHT,
						 ips.ui.key.ENTER, ips.ui.key.TAB, ips.ui.key.BACKSPACE, ips.ui.key.ESCAPE
					 ] ).contains( e.which ) ){
				ignoreKey = true;
			}
			
			var value = textField.val().trim();

			// If this is empty, remove errors
			if( !value.length ){
				hasError = false;
			}

			// If this is a normal key press and we're at our max length, prevent the keypress
			if( options.maxLength && value.length == options.maxLength && ignoreKey ){
				e.preventDefault();
				return;
			}

			// Check for duplicates if we're potentially adding a new tag
			if( _( [ ips.ui.key.ENTER, ips.ui.key.TAB ] ).contains( e.which ) && options.unique && _duplicateValue( value ) ){
				e.preventDefault();
				_showTooltip( ips.getString( 'ac_dupes' ) );
				return;
			}
			
			if( ignoreKey ){
				return;
			}

			switch(e.which){
				// Token keys
				case ips.ui.key.BACKSPACE:
					keyEvents.backspace(e);
				break;
				case ips.ui.key.TAB:
				case ips.ui.key.ENTER:
					keyEvents.enter(e);
				break;

				// Suggestions keys
				case ips.ui.key.UP:
					keyEvents.up(e);
				break;
				case ips.ui.key.DOWN:
					keyEvents.down(e);
				break;
				case ips.ui.key.ESCAPE:
					keyEvents.escape(e);
				break;
			}
		},

		/**
 		 * Event handler for keyup in the text field.
		 *
		 * @returns {void}
		 */
		_keyupField = function (e) {
			// Check for prohibited characters
			var i;
			for( i in options.disallowedCharacters ){
				if ( textField.val().indexOf( options.disallowedCharacters[i] ) !== -1 ) {
					textField.val( textField.val().replace( options.disallowedCharacters[i], '' ) );
					_showTooltip( ips.getString( 'ac_prohibit_special', {
						chars: options.disallowedCharacters.join(' ')
					} ) );
					e.preventDefault();
					return;
				}
			}

			// 229 is the 'input waiting' keycode. We need to check for it here because on IME keyboards,
			// we won't necessarily get a real keycode. e.g. Chrome on android. Instead we need to see if
			// the comma character is in the input, and process it that way.
			var lastCharIsComma = ( textField.val().substr( textField.val().length - 1 ) === ',' );

			if( e.which === 229 && lastCharIsComma ){
				_addTokenFromCurrentInput();
				e.preventDefault();
			}
		},

		/**
 		 * Event handler for keypress in the text field.
		 *
		 * @returns {void}
		 */
		_keypressField = function (e) {

			// If we aren't concerned about commas, we can stop here
			if( !options.commaTrigger ){
				return;
			}

			// Get rid of the comma
			textField.val( textField.val().replace(',', '') );

			// Check for duplicates if we're potentially adding a new tag by pressing comma
			if( e.charCode == options.itemSep.charcode && options.unique && _duplicateValue( textField.val() ) ){
				e.preventDefault();
				_showTooltip( ips.getString( 'ac_dupes' ) );
				return;
			}

			if( e.charCode == options.itemSep.charcode ){
				_addTokenFromCurrentInput();
				e.preventDefault();
			}
		},

		/**
 		 * A wrapper method for tokens.add which also clears the text field
 		 * and hides it if options.maxItems is reached
		 *
		 * @returns {void}
		 */
		_addToken = function (value) {
			tokens.add( value );
			textField.val('');
			lastValue = '';
			_resetField();

			if( options.maxItems && tokens.total() >= options.maxItems ){
				inputItem.hide();
			}

			if( options.unique && options.freeChoice == false && 
				dataSource.totalItems() !== -1 && dataSource.totalItems() <= tokens.total() ){
				wrapper.find('[data-action="addToken"]').hide();
			}

			// If we're here, remove any errors
			hasError = false;
		},

		/**
 		 * A wrapper method for tokens.remove which shows the text field if we're under
 		 * our options.maxItems limit
		 *
		 * @returns {void}
		 */
		_deleteToken = function (token) {
			if( disabled ){
				return;
			}
			
			tokens.remove( token );
		},

		/**
 		 * Object containing event handlers bound to individual keys
 		 */
		keyEvents = {

			/**
	 		 * Backspace handler. If the textfield is empty, we highlight the previous token.
	 		 * If a token is selected, hitting backspace deletes it.
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			backspace: function (e) {
				if( !textField.val() ){
					if( tokens.selected ){
						tokens.remove( tokens.selected );
					} else {
						if( inputItem.prev().length ){
							tokens.select( inputItem.prev() );
						}
					}
				}
			},

			/**
	 		 * Enter/tab handler. If text has been entered, we add it as a token, otherwise pass through
	 		 * to the browser to handle.
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void,boolean} 		
			 */
			enter: function (e) {
				if( e.which == ips.ui.key.TAB && textField.val() == '' ){
					return false;
				}

				e.preventDefault();

				var currentResult = results.getCurrent();
				var value = '';

				if( currentResult ){
					value = currentResult.attr('data-value');
				} else {
					if( options.commaTrigger ){
						value = _stripHTML( textField.val().replace( options.itemSep.chr, '' ) );
					} else {
						value = _stripHTML( textField.val() );
					}
				}

				if( !value ){
					return false;
				}

				_addToken( value );
			},

			/**
	 		 * Handler for 'up' key press. Selects previous item in the results list.
			 *
			 * @returns {void}
			 */
			up: function (e) {
				if( !resultsElem || !resultsElem.is(':visible') ){
					return;
				}

				e.preventDefault();

				var selected = results.getCurrent();				

				if( !selected ){
					results.selectLast();
				} else {
					var prev = results.getPrevious( selected );

					if( prev ){
						results.select( prev );
					} else {
						results.selectLast();
					}
				}
			},

			/**
	 		 * Handler for 'down' key press. Selects next item in the results list.
			 *
			 * @returns {void}
			 */
			down: function (e) {
				if( !resultsElem || !resultsElem.is(':visible') ){
					return;
				}

				e.preventDefault();

				var selected = results.getCurrent();
				
				if( !selected ){
					results.selectFirst();
				} else {
					var next = results.getNext( selected );

					if( next ){
						results.select( next );
					} else {
						results.selectFirst();
					}
				}

			},

			/**
	 		 * Handler for 'escape' key press. Closes the suggestions menu, if it's open.
			 *
			 * @returns {void}
			 */
			escape: function (e) {
				if( resultsElem && resultsElem.is(':visible') ){
					_closeResults();
				}
			}

		},

		/**
 		 * Object containing methods for dealing with the results list.
 		 */
		results = {

			/**
	 		 * Deselects any selected results
			 *
			 * @returns {void}
			 */
			deselectAll: function () {
				resultsElem
					.find('[data-selected]')
					.removeAttr('data-selected');
			},

			/**
	 		 * Returns the currently selected result
			 *
			 * @returns {element,boolean} 	Returns the jQuery object containing the selected result, or false
			 */
			getCurrent: function () {
				if( dataSource.type == 'none' ){
					return;
				}

				var cur = resultsElem.find('[data-selected]');

				if( cur.length && resultsElem.is(':visible') ){
					return cur;
				} 

				return false;
			},

			/**
	 		 * Gets the result preceding the provided result
			 *
			 * @returns {element,boolean} 	Returns the jQuery object containing the selected result, or false
			 */
			getPrevious: function (result) {
				var prev = $( result ).prev('[data-value]');

				if( prev.length ){
					return prev;
				}

				return false;
			},

			/**
	 		 * Gets the result following the provided result
			 *
			 * @returns {element,boolean} 	Returns the jQuery object containing the selected result, or false
			 */
			getNext: function (result) {
				var next = $( result ).next('[data-value]');

				if( next.length ){
					return next;
				}

				return false;
			},

			/**
	 		 * Selects the first result
			 *
			 * @returns {void}
			 */
			selectFirst: function () {
				results.select( resultsElem.find('[data-value]').first() );
			},

			/**
	 		 * Selects the last result
			 *
			 * @returns {void}
			 */
			selectLast: function () {
				results.select( resultsElem.find('[data-value]').last() );
			},

			/**
	 		 * Selects the provided item
			 *
			 * @returns {void}
			 */
			select: function (result) {
				results.deselectAll();
				result.attr('data-selected', true);
			}
		},

		/**
 		 * Object containing token methods
 		 */
		tokens = {

			selected: null,

			/**
	 		 * Adds a token to the control
			 *
			 * @param 	{string} 	value 	The value of this token
			 * @returns {void}
			 */
			add: function (value) {
				var html = '';

				value = _.escape( value ).trim();

				if( options.minLength && value.length < options.minLength ){
					return false;
				}

				if( options.maxLength && value.length > options.maxLength ){
					return false;
				}

				if( options.forceLower ){
					value = value.toLowerCase();
				}

				tokens.deselectAll();

				inputItem.before( ips.templates.render( options.tokenTemplate, {
					id: elemID,
					value: value,
					title: value
				}));

				if( resultsElem ){
					_closeResults();
				}

				// Update hidden textbox
				valueField.val( tokens.getValues().join( "\n" ) );

				if( dataSource.type != 'none' ){
					html = resultsElem.find('[data-value="' + value.replace("\\", "\\\\") + '"]').html();
				} else {
					html = value;
				}

				elem.trigger('tokenAdded', {
					token: value,
					html: html,
					tokenList: tokens.getValues(),
					totalTokens: tokens.total()
				});

				return true;
			},

			/**
	 		 * Deletes the given token
			 *
			 * @param 	{element} 	token 	The token element to select
			 * @returns {void}
			 */
			remove: function (token) {
				if( tokens.selected == token ){
					tokens.selected = null;
				}

				var value = $( token ).attr('data-value');
				$( token ).remove();

				if( options.maxItems && tokens.total() < options.maxItems ){
					inputItem.show();
				}

				if( options.unique && options.freeChoice == false && 
					( dataSource.totalItems() === -1 || dataSource.totalItems() > tokens.total() ) ) {
					wrapper.find('[data-action="addToken"]').show();
				}
				
				// Update text field
				valueField.val( tokens.getValues().join( "\n" ) );

				elem.trigger('tokenDeleted', {
					token: value,
					tokenList: tokens.getValues(),
					totalTokens: tokens.total()
				});
			},

			/**
	 		 * Removes all tokens
			 *
			 * @returns {void}
			 */
			removeAll: function () {
				var allTokens = inputItem.siblings().filter('[data-value]');

				allTokens.each( function () {
					tokens.remove( $( this ) );
				});
			},

			/**
	 		 * Selects a given token
			 *
			 * @param 	{element} 	token 	The token element to select
			 * @returns {void}
			 */
			select: function (token) {
				tokens.deselectAll();
				tokens.selected = $( token ).addClass('cToken_selected');
			},

			/**
	 		 * Returns total number of tokens entered
			 *
			 * @returns {number}
			 */
			total: function () {
				return inputItem.siblings().filter('[data-value]').length;
			},

			/**
	 		 * Returns all of the values
			 *
			 * @param 	{element} 	token 	The token element to select
			 * @returns {void}
			 */
			getValues: function () {
				var values = [];
				var allTokens = inputItem.siblings().filter('[data-value]');

				if( allTokens.length ){
					values = _.map( allTokens, function( item ){
						return $( item ).attr('data-value');
					});
				}

				return values;
			},

			/**
	 		 * Returns selected token value
			 *
			 * @returns {string|null} 	Value or null if no token selected
			 */
			getSelected: function () {
				return tokens.selected.attr('data-value');
			},

			/**
	 		 * Deselects all tokens
			 *
			 * @returns {void}
			 */
			deselectAll: function () {
				wrapper.find('[data-value]').removeClass('cToken_selected');
				tokens.selected = null;
			}
		},

		/**
 		 * Creates a token out of the current value in the text field
		 *
		 * @returns {void}
		 */
		_addTokenFromCurrentInput = function () {
			var value = '';

			if( options.commaTrigger ){
				value = _stripHTML( textField.val().replace( options.itemSep.chr, '' ) );
			} else {
				value = _stripHTML( textField.val() );
			}

			if( options.minLength && value.length < options.minLength || options.maxLength && value.length > options.maxLength ){
				if( options.commaTrigger ){
					textField.val( textField.val().replace( options.itemSep.chr, '' ) );
				}
				return;
			}

			if( options.unique && _duplicateValue( value ) ){
				_showTooltip( ips.getString( 'ac_dupes' ) );
				return;
			}

			_addToken( value );
		},

		/**
 		 * Determines whether the value would be a duplicate
		 *
		 * @param 	{string} 	value 	Value to check
		 * @returns {void}
		 */
		_duplicateValue = function (value) {
			var values = tokens.getValues();

			if( values.indexOf( value ) !== -1 ){
				return true;
			}

			return false;
		},

		/**
 		 * Removes special characters from text
		 *
		 * @returns {string}
		 */
		_stripHTML = function (text) {
			return text.replace(/<|>|"|'/g, '');
		},

		/**
 		 * Shows a tooltip on the autocomplete with the provided message
		 *
		 * @param 	{string} 	msg 	Message to show
		 * @returns {void}
		 */
		_showTooltip = function (msg) {
			if( !tooltip ){
				_buildTooltip();
			}

			// Set errors to true
			hasError = true;

			// If we're already showing a tooltip, remove the timeout before
			// showing this one.
			if( tooltipTimer ){
				clearTimeout( tooltipTimer );
			}

			tooltip.hide().text( msg );

			_positionTooltip();

			// Hide it automatically in a few seconds
			tooltipTimer = setTimeout( function () {
				_hideTooltip();
			}, 2500);
		},

		/**
 		 * Hides the tooltip
		 *
		 * @returns {void}
		 */
		_hideTooltip = function () {
			if( tooltip && tooltip.is(':visible') ){
				ips.utils.anim.go( 'fadeOut', tooltip );
			}
		},

		/**
 		 * Positions the tooltip over the autocomplete
		 *
		 * @returns {void}
		 */
		_positionTooltip = function () {
			var positionInfo = {
				trigger: wrapper,
				target: tooltip,
				center: true,
				above: true
			};

			var tooltipPosition = ips.utils.position.positionElem( positionInfo );

			$( tooltip ).css({
				left: tooltipPosition.left + 'px',
				top: tooltipPosition.top + 'px',
				position: ( tooltipPosition.fixed ) ? 'fixed' : 'absolute',
				zIndex: ips.ui.zIndex()
			});

			if( tooltipPosition.location.vertical == 'top' ){
				tooltip.addClass('ipsTooltip_top');
			} else {
				tooltip.addClass('ipsTooltip_bottom');
			}

			tooltip.show();
		},

		/**
 		 * Builds the tooltip element
		 *
		 * @param 	{string} 	msg 	Message to show
		 * @returns {void}
		 */
		_buildTooltip = function () {
			// Build it from a template
			var tooltipHTML = ips.templates.render( 'core.tooltip', {
				id: 'elAutoCompleteTooltip'
			});

			// Append to body
			ips.getContainer().append( tooltipHTML );

			tooltip = $('#elAutoCompleteTooltip');
		},

		/**
 		 * Expands the text field to fit the given text
		 *
		 * @returns {void}
		 */
		_expandField = function () {
			var text = textField.val();
			var widthOfElem = wrapper.width();

			widthOfElem -= ( parseInt( wrapper.css('padding-left') ) + parseInt( wrapper.css('padding-right') ) );

			// Create temporary span
			var span = $('<span/>').text( text ).css({
				'font-size': textField.css('font-size'),
				'letter-spacing': textField.css('letter-spacing'),
				'position': 'absolute',
				'top': '-100px',
				'left': '-300px',
				'opacity': "0.1"
			});

			ips.getContainer().append( span );

			// Get the width
			var width = span.width() + 20;

			// Remove it
			span.remove();

			textField.css({
				width: ( ( width >= widthOfElem ) ? widthOfElem : width ) + 'px'
			});
		},

		/**
 		 * Resets the width of the text input
		 *
		 * @returns {void}
		 */
		_resetField = function () {
			textField.css({
				width: '15px'
			});
		};

		init();

		return {
			init: init,
			destruct: destruct,
			hasErrors: hasErrors,
			addToken: tokens.add,
			getTokens: tokens.getValues,
			removeToken: tokens.remove,
			removeAll: tokens.removeAll,
			focus: focus
		};
	};

	/**
	 * Handler for local data retrieval
	 */
	var localData = function (source) {
		
		var items = $( source ).find('option');

		/**
 		 * Searches through the source element, matching option values to our search string
		 *
		 * @param 	{string}	String to search for
		 * @returns {void}
		 */
		var getResults = function (text) {
			var deferred = $.Deferred(),
				output = [],
				text = text.toLowerCase();

			items.each( function (idx, item) {
				if( item.innerHTML.toLowerCase().startsWith( text ) ){
					output.push( { id: item.value, value: item.value, html: item.value } );
				}
			});		

			deferred.resolve( output );
			return deferred.promise();
		},

		/**
 		 * Returns the number of items in the result set
		 *
		 * @returns {number}
		 */
		totalItems = function () {
			return items.length;
		};

		return {
			type: 'local',
			getResults: getResults,
			totalItems: totalItems,
			stop: $.noop
		};
	};

	/**
	 * Handler for remote data retrieval
	 */
	var remoteData = function (source, options) {

		var ajaxObj,
			loadedCache = false,
			cache = {};

		/**
 		 * Initiates either a remote search or a remote fetch
		 *
		 * @returns {promise}
		 */
		var getResults = function (text) {
			if( options.freeChoice ){
				return _remoteSearch( text );
			} else {
				return _remoteFetch( text );
			}
		},

		/**
 		 * Returns the number of items in the result set
		 *
		 * @returns {number}
		 */
		totalItems = function () {
			if( !options.freeChoice && loadedCache ){
				return _.size( cache );
			}

			return -1;
		},

		/**
 		 * Does a remote search (i.e. passing search string to backend, and returning results)
		 *
		 * @param 	{string}	String to search for
		 * @returns {promise}
		 */
		_remoteSearch = function (text) {
			var deferred = $.Deferred();

			if( ajaxObj ){
				ajaxObj.abort();
			}

			if( options.minAjaxLength > text.length ){
				deferred.reject();
				return deferred.promise();
			}

			if( cache[ text ] ){
				deferred.resolve( cache[ text ] );
			} else {				
				ajaxObj = ips.getAjax()( source + '&' + options.queryParam + '=' + encodeURIComponent( text ), { dataType: 'json' } )
					.done( function (response) {
						deferred.resolve( response );
						cache[ text ] = response;
					})
					.fail( function (jqXHR, status, errorThrown) {
						if( status != 'abort' ){
							Debug.log('aborting');
						}
						deferred.reject();
					});
			}

			return deferred.promise();
		},

		/**
 		 * Fetches remote data, and then performs a local search on the data to find results
		 *
		 * @param 	{string}	String to search for
		 * @returns {promise}
		 */
		_remoteFetch = function (text) {
			var deferred = $.Deferred();

			if( !loadedCache ){
				if( ajaxObj ){
					return;
				}

				if( options.minAjaxLength > text.length ){
					return;
				}

				ajaxObj = ips.getAjax()( source, { dataType: 'json' } )
					.done( function (response) {
						loadedCache = true;
						cache = response;
						_remoteFetch( text );
					})
					.fail( function (jqXHR, status, errorThrown) {
						if( status != 'abort' ){
							Debug.log('aborting');
						}
						deferred.reject();
					});
			}

			// Search through the cache for results
			cache.each( function (idx, item) {
				if( item.value.toLowerCase().startsWith( text ) ){
					output.push( item );
				}
			});

			return deferred.promise();
		},

		/**
 		 * Aborts the ajax request
		 *
		 * @param 	{string}	String to search for
		 * @returns {void}
		 */
		stop = function () {
			if( ajaxObj ){
				ajaxObj.abort();
			}
		};

		return {
			type: 'remote',
			getResults: getResults,
			totalItems: totalItems,
			stop: stop
		};
	};

	var noData = function () {
		return {
			type: 'none',
			getResults: $.noop,
			totalItems: -1,
			stop: $.noop
		};
	};

}(jQuery, _));

]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.captcha.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.captcha.js - Recaptcha widget. Allows for dynamic loading of recpatcha, so it works in popups
 *
 * Author: Rikki Tissier
 */

/* A global function breaks our coding standards, but it's the only way Google will allow it */
function recaptcha2Callback(){
	jQuery( window ).trigger( 'recaptcha2Loaded' );
};

;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.captcha', function(){

		var defaults = {
			lang: 'en-US',
			theme: 'white'
		};

		var recaptchaLoaded = false;

		var respond = function (elem, options) {
			options = _.defaults( options, defaults );
			
			if( options.service == 'recaptcha' ){
				_recaptcha( elem, options );
			} else if( options.service == 'recaptcha2' ){
				_recaptcha2( elem, options );
			} else if ( options.service == 'keycaptcha' ) {
				_keycaptcha(elem);
			} else if ( options.service == 'recaptcha_invisible' ) {
				_recaptcha_invisible( elem );
			}
		},

		/**
		 * Recaptcha
		 * Handles a recaptcha captcha, loading the JS file from google.com before setup
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		_recaptcha = function (elem, options) {
			ips.loader.get( [ document.location.protocol + '//www.google.com/recaptcha/api/js/recaptcha_ajax.js'] ).done( function () {
				var container = $('<div/>');
				var id = container.identify().attr('id');

				elem.append( container );

				Recaptcha.create( options.key, id, {
					theme: options.theme,
					lang: options.lang,
					callback: function () { Debug.log('done') }
				});
			});
		},

		/**
		 * Recaptcha2
		 * Handles a new recaptcha captcha, loading the JS file from google.com before setup
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		_recaptcha2 = function (elem, options) {
			ips.loader.get( [ 'https://www.google.com/recaptcha/api.js?hl=' + $(elem).attr('data-ipsCaptcha-lang') + '&onload=recaptcha2Callback&render=explicit' ] );

			var initRecaptcha2 = function () {
				elem.children('[data-captchaContainer]').remove();
				
				var container = $('<div data-captchaContainer/>');
				var id = container.identify().attr('id');

				elem.append( container );
				
				grecaptcha.render( id, {
					sitekey: $(elem).attr('data-ipsCaptcha-key'),
					theme: $(elem).attr('data-ipsCaptcha-theme')
				} );
			};

			if( recaptchaLoaded ){
				initRecaptcha2();
			} else {
				$( window ).on( 'recaptcha2Loaded', function() {
					recaptchaLoaded = true;
					initRecaptcha2();
				});	
			}
		},
		
		/**
		 * Invisible Recaptcha
		 * Handles a new recaptcha captcha, loading the JS file from google.com before setup
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		_recaptcha_invisible = function (elem, options) {
			ips.loader.get( [ 'https://www.google.com/recaptcha/api.js?hl=' + $(elem).attr('data-ipsCaptcha-lang') + '&onload=recaptcha2Callback&render=explicit' ] );

			var initRecaptchaInvisible = function () {
				elem.children('[data-captchaContainer]').remove();
				var container = $('<div data-captchaContainer/>');
				var id = container.identify().attr('id');
				elem.append( container );
				
				var form = elem.closest('form');
				var recaptchaId = grecaptcha.render( id, {
					sitekey: $(elem).attr('data-ipsCaptcha-key'),
					size: 'invisible',
					callback: function () {
						form.attr( 'data-recaptcha-done', 'true' );
						form.submit();
					}
				} );
				
				form.on( 'submit', function( e ) {
					if ( !form.attr( 'data-recaptcha-done') ) {
						e.stopPropagation();
						e.preventDefault();
						grecaptcha.execute(recaptchaId);
					}
				});
			};

			if( recaptchaLoaded ){
				initRecaptchaInvisible();
			} else {
				$( window ).on( 'recaptcha2Loaded', function() {
					recaptchaLoaded = true;
					initRecaptchaInvisible();
				});	
			}
		},

		/**
		 * Keycaptcha captcha
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @returns {void}
		 */
		_keycaptcha = function (elem) {
			ips.loader.get( [ document.location.protocol + '//backs.keycaptcha.com/swfs/cap.js' ] );
		};

		ips.ui.registerWidget( 'captcha', ips.ui.captcha, [
			'service', 'key', 'lang', 'theme'
		]);

		return {
			respond: respond
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.carousel.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.carousel.js - Carousel widget
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.carousel', function(){

		var defaults = {
			item: ".ipsCarousel_item",
			shadows: true
		};

		/**
		 * Responder for carousel widget
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var respond = function (elem, options) {
			if( !$( elem ).data('_carousel') ){
				$( elem ).data('_carousel', carouselObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		},

		/**
		 * Retrieve the carousel instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The carousel instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_carousel') ){
				return $( elem ).data('_carousel');
			}

			return undefined;
		};

		/**
		 * Carousel instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var carouselObj = function (elem, options) {

			var rtlMode = ( $('html').attr('dir') == 'rtl' );
			var currentItemCount = 0;
			var currentStartPos = 0; // Right edge for RTL, left edge for LTR
			var currentFirstItem = null;
			var ui = {};
			var slideshowTimer = null;
			var slideshowTimeout = 4000;
			var animating = false;

			/**
			 * Sets up this instance
			 *
			 * @returns 	{void}
			 */
			var init = function () {
				currentItemCount = elem.find( options.item );

				ui = {
					itemList: elem.find('[data-role="carouselItems"]'),
					next: elem.find('[data-action="next"]'),
					prev: elem.find('[data-action="prev"]'),
					nextShadow: elem.find('.ipsCarousel_shadowRight'),
					prevShadow: elem.find('.ipsCarousel_shadowLeft')
				};

				if( !options.shadows ){
					ui.nextShadow.hide();
					ui.prevShadow.hide();
				}

				var startPos = rtlMode ? parseInt( ui.itemList.css('right') ) : parseInt( ui.itemList.css('left') );

				if( !_.isNaN( startPos ) ){
					currentStartPos = startPos;
				}
				
				_build();
				
				/* Rebuild after all the images have been loaded in case we need to adjust the height */
				var images = elem.find('img[src]').length;
				elem.find('img[src]').load( function(){
					images--;
					if ( !images ) {
						_build();
					}
				} );
				_checkNav();

				elem.on( 'click', "[data-action='next']", _navNext );
				elem.on( 'click', "[data-action='prev']", _navPrev );
				elem.on( 'contentTruncated', _updateHeight );
				elem.on( 'gridRedraw.grid', _updateHeight );

				_setMaxWidth();

				// Add touch controls
				var mc = new Hammer( elem[0] );
				mc.on( 'panleft', function( e ) {
					if( !animating ){
						if( rtlMode ){
							_navPrev();
						} else {
							_navNext();
						}
					}
				} );
				mc.on( 'panright', function( e ) {
					if( !animating ){
						if( rtlMode ){
							_navNext();
						} else {
							_navPrev();
						}
					}
				} );

				if( options.slideshow ){
					// Start the timer for the slideshow
					slideshowTimer = setTimeout( _slideshowNext, slideshowTimeout );
					
					elem
						.on( 'mouseenter', function () {
							clearTimeout( slideshowTimer );
						})
						.on( 'mouseleave', function () {
							clearTimeout( slideshowTimer );
							slideshowTimer = setTimeout( _slideshowNext, slideshowTimeout );
						});
				}

				$( window ).on( 'resize', _resize );

				//elem.find('.ipsCarousel_inner').on( 'scroll', _checkNav );
			},

			/**
			 * Destruct this instance, unregistering event handlers
			 *
			 * @returns 	{void}
			 */
			destruct = function () {
				$( window ).off( 'resize', _resize );
				elem.off( 'click', "[data-action='next']", _navNext );
				elem.off( 'click', "[data-action='prev']", _navPrev );
				elem.off( 'contentTruncated', _updateHeight );
				elem.off( 'gridRedraw.grid', _updateHeight );
				elem.find('.ipsCarousel_inner').off( 'scroll', _checkNav );
				clearTimeout( slideshowTimer );
			},

			/**
			 * Build the carousel ui
			 *
			 * @returns 	{void}
			 */
			_build = function () {
				var maxHeight = _getMaxHeight();
				var elemWidth = 0;

				// Set the height of the carousel to be as high as the highest item
				elem.find('.ipsCarousel_inner').css({
					height: maxHeight + ( parseInt( elem.find('.ipsCarousel_inner').css('padding-top') ) + parseInt( elem.find('.ipsCarousel_inner').css('padding-bottom') ) ) + 'px'
				});

				// Are we making the items full width?
				if( options.fullSizeItems ){
					elemWidth = elem.find('.ipsCarousel_inner').outerWidth( true );
				}

				// Now align all the other items vertically
				elem.find( options.item ).each( function (item) {
					var height = $( this ).outerHeight();
					var diff = maxHeight - height;

					if( options.fullSizeItems ){
						$( this ).css({
							width: elemWidth + 'px'
						});
					}
				});

				elem.find( '[data-role="carouselItems"]' ).css({
					width: _getCurrentWidth() + 'px'
				})

				currentFirstItem = elem.find( options.item ).first();

				_buildNav();
			},

			/**
			 * When content is truncated, the max height of the items may change, so that
			 * event triggers this method to recalculate the height.
			 *
			 * @returns 	{void}
			 */
			_updateHeight = function () {
				var maxHeight = _getMaxHeight();

				// Set the height of the carousel to be as high as the highest item
				elem.find('.ipsCarousel_inner').css({
					height: maxHeight + 'px'
				});
			},

			/**
			 * Moves to the next item automatically as part of the slideshow
			 *
			 * @returns 	{void}
			 */
			_slideshowNext = function () {
				// If there's a next or prev button, we know we can call navNext or navPrev. They return a promise
				// so that we can re-set our timer after the animation is finished.
				if( ui.next.not('[data-disabled]').length ){
					_navNext().done( function () {
						slideshowTimer = setTimeout( _slideshowNext, slideshowTimeout );
					});
				} else if( ui.prev.not('[data-disabled]').length ){
					_navPrev( null, true ).done( function () {
						slideshowTimer = setTimeout( _slideshowNext, slideshowTimeout );
					});
				}
			},

			/**
			 * Event handler for the Next button
			 *
			 * @returns 	{object}	Returns promise, resolved after animation finishes
			 */
			_navNext = function (e) {
				if( e ){
					e.preventDefault();
				}

				// Get the first item which isn't completely shown; that will be our next first item
				var items = elem.find( options.item );
				var wrapperWidth = elem.outerWidth();
				var listWidth = _getCurrentWidth();
				var forceNext = false;
				var deferred = $.Deferred();

				var nextFirst = _.find( items, function (item, idx) {
					// If our previous iteration forced this one to be the current item, return now.
					if( forceNext ){
						return true;
					}

					var width = $( item ).outerWidth();
					var adjustedStartPos = currentStartPos;
					var stayOnScreen = adjustedStartPos + wrapperWidth;

					if( rtlMode ){
						// These values are all relative to the right-hand side of the carousel frame
						var margin = parseInt( $( item ).css('marginLeft') );
						var startEdge = ui.itemList.outerWidth() - ( $( item ).position().left + width ) - margin; // right edge
						var endEdge = startEdge + width;  // left edge

						// If this is off to the right of the screen, just cancel
						if( startEdge < adjustedStartPos ){
							return false;
						}

						// If the left edge is the exact same pixel as the end of the viewable area, or this item is
						// wider than the viewable area (e.g. on mobile), then force the next item to be the one we show
						if ( ( endEdge == stayOnScreen || startEdge == stayOnScreen && endEdge > adjustedStartPos ) ){
							forceNext = true;
						}

						// Otherwise, if the right edge is visible but the left edge isn't, this is the item to show
						if( startEdge < stayOnScreen && endEdge > stayOnScreen ){
							return true;
						}

						// Lastly if we haven't found one yet, get the next image that's off screen
						if( startEdge > stayOnScreen ){
							return true;
						}
					} else {
						var margin = parseInt( $( item ).css('marginRight') );
						var startEdge = $( item ).position().left; // left edge
						var endEdge = startEdge + width; // right edge
						
						// If this is off to the left of the screen, just cancel
						if( startEdge < adjustedStartPos ){
							return false;
						}

						// If the right edge is the exact same pixel as the end of the viewable area, or this item is
						// wider than the viewable area (e.g. on mobile), then force the next item to be the one we show
						if ( ( endEdge == stayOnScreen || startEdge == currentStartPos && endEdge > stayOnScreen ) ){
							forceNext = true;
						}

						// Otherwise, if the left edge is visible but the right edge isn't, this is the item to show
						if( startEdge < stayOnScreen && endEdge > stayOnScreen ){
							return true;
						}

						// Lastly if we haven't found one yet, get the next image that's off screen
						if( startEdge > stayOnScreen ){
							return true;
						}
					}

					// If the other conditions don't match, this isn't the item we're looking for
					return false;
				});

				var nextFirst = $( nextFirst );

				if( !nextFirst.length ){
					//Debug.error("nextFirst didn't exist");
					deferred.resolve();
					return deferred.promise();
				}

				// Get the position we'll need to scroll to
				if( rtlMode ){
					var nextFirstMargin = parseInt( $( nextFirst ).css('marginLeft') );
					var nextFirstPos = ui.itemList.outerWidth() - ( nextFirst.position().left + nextFirst.outerWidth() ) - nextFirstMargin; // Right hand edge
				} else {
					var nextFirstMargin = parseInt( $( nextFirst ).css('marginRight') );
					var nextFirstPos = nextFirst.position().left;
				}

				// If this would leave space at the end of the list, then we'll simply scroll to the end of the list
				// ( listWidth - nextFirstPos ) = What's left of the list to show
				if( ( listWidth - nextFirstPos ) < wrapperWidth ){
					nextFirstPos = listWidth - wrapperWidth;
				}

				currentStartPos = nextFirstPos;

				// Now animate the list to the new position
				animating = true;
				ui.itemList.animate(
					( rtlMode ) ? { right: ( nextFirstPos * -1 ) + 'px' } : { left: ( nextFirstPos * -1 ) + 'px' }
					, 'slow', function () {
						_checkNav();
						animating = false;
						deferred.resolve();
					}
				);

				return deferred.promise();
			},

			/**
			 * Event handler for the Prev button
			 *
			 * @returns 	{object} 	Returns promise, resolved after animation finishes
			 */
			_navPrev = function (e, backToBeginning) {
				if( e ){
					e.preventDefault();
				}

				// Get the first item which isn't completely shown; that will be our next first item
				var items = elem.find( options.item ).toArray();
				var wrapperWidth = elem.find('.ipsCarousel_inner').outerWidth();
				var listWidth = _getCurrentWidth();
				var stayOnScreen = currentStartPos + wrapperWidth;
				var forcePrev = false;
				var deferred = $.Deferred();

				// We ideal want to scroll back the width of the widget, but not so much that
				// our current left item goes off screen
				var idealStartPos = ( currentStartPos * -1 ) + wrapperWidth;

				// If we need to go back to the bottom, we can shortcut
				if( idealStartPos >= 0 || backToBeginning ){
					currentStartPos = 0;

					animating = true;
					ui.itemList.animate(
						( rtlMode ) ? { right: '0px' } : { left: '0px' }
						, 'slow', function () {
						_checkNav();
						animating = false;
					});

					deferred.resolve();
					return deferred.promise();
				}

				// We'll go through them from last to first, since we're scrolling backwards
				items.reverse();

				idealStartPos = ( idealStartPos * -1 ) + wrapperWidth;

				// Now we can find the first item that's fully on screen
				var prevFirst = _.find( items, function (item) {

					if( forcePrev ){
						return true;
					}

					var width = $( item ).outerWidth();

					if( rtlMode ){
						var margin = parseInt( $( item ).css('marginLeft') );
						var leftPos = $( item ).position().left;

						// These values are all relative to the right-hand side of the carousel frame
						var startEdge = ui.itemList.outerWidth() - ( leftPos + width ) - margin; // right edge
						var endEdge = startEdge + width;  // left edge
						
					} else {
						var startEdge = $( item ).position().left; // left edge
						var endEdge = startEdge + width; // right edge
						var margin = parseInt( $( item ).css('marginRight') );	
					}

					if( startEdge > idealStartPos ){
						return false;
					}

					// If left + width of this item perfectly equals the width of the wrapper, then the next item
					// to be shown is actually the first one that's offscreen. This also applies if the fullSizeItems
					// option is applied. Set forceNext to true so that on the next iteration we can return the item.
					if( stayOnScreen <= ( endEdge + margin ) || ( startEdge < idealStartPos && endEdge > idealStartPos ) ){
						forcePrev = true;
						return false;
					}

					if( endEdge > idealStartPos && startEdge <= idealStartPos ){
						return true;
					}

					return false;
				});

				prevFirst = $( prevFirst );				

				// The method above has given us the first that is *partially* on screen
				// We actually want the next one though so we know it's fully on screen,
				// except on mobile
				if( !ips.utils.responsive.currentIs('phone') && !options.fullSizeItems ){
					prevFirst = $( prevFirst ).next( options.item );	
				}

				currentFirstItem = prevFirst;

				// Set the left position so that prevFirst is still on-screen as the last item
				// E.g.
				// 
				//	Before									After
				//	|5  ][  6  ][  7  ][  8  ][  9  ]|		|[  1  ][  2  ][  3  ][  4  ][  5  ]|
				//
                if( prevFirst.position() != null ) {
                    currentStartPos = prevFirst.position().left + prevFirst.outerWidth() - wrapperWidth;

                    if( rtlMode ){
                    	var prevFirstMargin = parseInt( $( prevFirst ).css('marginLeft') );
						currentStartPos = ui.itemList.outerWidth() - ( prevFirst.position().left ) - prevFirstMargin - wrapperWidth; // right edge
					}
                } else {
                    currentStartPos = prevFirst.outerWidth() + wrapperWidth;
                }
				
				animating = true;
				ui.itemList.animate(
					( rtlMode ) ? { right: ( currentStartPos * -1 ) + 'px' } : { left: ( currentStartPos * -1 ) + 'px' }
				, 'slow', function () {
					_checkNav();
					animating = false;
					deferred.resolve();
				});

				return deferred.promise();
			},

			/**
			 * Returns the height of the tallest item in the carousel currently
			 *
			 * @returns 	{number}
			 */
			_getMaxHeight = function () {
				var items = elem.find( options.item );

				if( !items.length ){
					return 0;
				}

				var max = _.max( items, function (item) {
					var item = $( item );
					return item.outerHeight();
				});

				return $( max ).outerHeight();
			},

			/**
			 * Sets a css var to control our max width
			 */
			_setMaxWidth = function () {
				elem.get(0).style.setProperty('--carousel-maxWidth', elem.width() + 'px');
			},

			/**
			 * Shows or hides the navigation elements, depending on current position of the carousel
			 *
			 * @returns 	{void}
			 */
			_checkNav = function () {
				var container	= elem.find('.ipsCarousel_inner')[0].getBoundingClientRect();
				var list		= ui.itemList[0].getBoundingClientRect();

				if( ( !rtlMode && Math.floor(list.right) <= Math.floor(container.right) ) || (rtlMode && Math.floor(list.left) >= Math.floor(container.left)) ){
					ui.next.hide().attr('data-disabled', true);
					if( ui.nextShadow.is(':visible') && options.shadows ){
						ips.utils.anim.go('fadeOut fast', ui.nextShadow );
					}
				} else {
					ui.next.show().removeAttr('data-disabled');
					if( !ui.nextShadow.is(':visible') && options.shadows ){
						ips.utils.anim.go('fadeIn fast', ui.nextShadow );
					}
				}

				if( (!rtlMode && Math.floor(list.left) >= Math.floor(container.left) ) || (rtlMode && Math.floor(list.right) <= Math.floor(container.right)) ){
					ui.prev.hide().attr('data-disabled', true);
					if( ui.prevShadow.is(':visible') && options.shadows ){
						ips.utils.anim.go('fadeOut fast', ui.prevShadow );
					}
				} else {
					ui.prev.show().removeAttr('data-disabled');
					if( !ui.prevShadow.is(':visible') && options.shadows ){
						ips.utils.anim.go('fadeIn fast', ui.prevShadow );
					}
				}
			},

			/**
			 * Returns the current width of all items, including margins
			 *
			 * @returns 	{number}
			 */
			_getCurrentWidth = function () {
				var items = elem.find( options.item );
				var width = 0;

				items.each( function (item) {
					width += $( this ).outerWidth();
					width += parseInt( $( this ).css('margin-left') );
					width += parseInt( $( this ).css('margin-right') );
				});

				return width;
			},

			/** 
			 * Shows the nav items
			 *
			 * @returns {void}
			 */
			_buildNav = function () {
				elem.find('.ipsCarousel_nav').removeClass('ipsHide');
			},

			/** 
			 * Event handler for window resizing
			 *
			 * @returns {void}
			 */
			_resize = function () {
				// Are we making the items full width?
				if( options.fullSizeItems ){
					var elemWidth = elem.find('.ipsCarousel_inner').outerWidth( true );

					elem.find( options.item ).each( function (item) {
						$( this ).css({
							width: elemWidth + 'px'
						});
					});
				}

				_setMaxWidth();
			};

			init();

			return {
				destruct: destruct
			};
		};

		ips.ui.registerWidget( 'carousel', ips.ui.carousel, [ 'showDots', 'fullSizeItems', 'slideshow', 'shadows' ] );

		return {
			respond: respond,
			destruct: destruct
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.chart.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.chart.js - Converts a table into a Google Graph
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.createModule('ips.ui.chart', function(){
		
		var defaults = {};

		/**
 		 * Widget respond method
 		 * Simply sets a callback that will execute when the google visualization JS has loaded
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed
		 * @returns {void}
		 */
		var respond = function (elem, options) {

			var doInit = function () {
				if( !$( elem ).data('_chart') ){
					$( elem ).data('_chart', chartObj(elem, _.defaults( options, defaults ) ) );
				}
			};

			try {
				doInit();
			} catch (err) {
				ips.loader.get( ['https://www.gstatic.com/charts/loader.js'] ).then( function () {
					google.charts.load( '47', {'packages':['corechart', 'gauge', 'table'], 'callback': doInit } );
				});
			}
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		},

		/**
		 * Retrieve the carousel instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The carousel instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_chart') ){
				return $( elem ).data('_chart');
			}

			return undefined;
		};

		/**
		 * Chart instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var chartObj = function (elem, options) {
						
			var data = new google.visualization.DataTable();
			var headerTypes = {};
			var extraOptions = {};
			var chartElem = $(elem).next();
			//chartElem.css( 'height', Math.max(document.documentElement.clientHeight, window.innerHeight || 0) - chartElem.offset().top );
			var chart = null;

			/**
	 		 * Initialize this chart
			 *
			 * @returns {void}
			 */
			var init = function () {
							
				// Add headers
				elem.find('thead th').each( function (idx) {
					headerTypes[ idx ] = $( this ).attr('data-colType');										
					data.addColumn( $( this ).attr('data-colType'), $( this ).text() );
				});
								
				// Add rows
				elem.find('tbody tr').each( function () {
					var row = [];

					$( this ).find('td').each( function( idx ) {
						
						if( headerTypes[ idx ] == 'number' ){
							var val;
							if ( val = $( this ).text() ) {
								val = Number( val );
							} else {
								val = null;
							}
						} else if ( headerTypes[ idx ] == 'date' || headerTypes[ idx ] == 'datetime' || headerTypes[ idx ] == 'timeofday'  ) {
							var val = new Date( $( this ).text() );
						} else {
							var val = $( this ).text();
						}

						if( !_.isNaN( val ) ){
							if ( $(this).attr('data-key') ) {
								val = { v: $(this).attr('data-key'), f: val };
							}
							row.push( val );	
						}
					});
					
					data.addRow(row);
				});
				
				if ( options.format ) {
					var formatter = new google.visualization.NumberFormat({pattern:'# ' + options.format} );
					formatter.format( data, 1 );
				}
				
				// Set options
				extraOptions = $.parseJSON( options.extraOptions );

				if( !_.isUndefined( extraOptions.height ) ){
					chartElem.css({
						height: extraOptions.height + 'px'
					});
				} else {
					chartElem.css({
						minHeight: '250px'
					});
				}
				
				// Add Chart wrapper
				elem.hide().after( chartElem );
				
				// We need to redraw the chart when the window resizes
				$( window ).on( 'resize', drawChart );

				drawChart();

				// Callback to let the page know
				google.visualization.events.addListener( chart, 'ready', function () {
					$( elem ).trigger( 'chartInitialized');
				});

				// If this chart is in a tab, we need to re-initialize it after the tab is shown so that
				// it sizes properly
				$( document ).on( 'tabShown', tabShown );
			},

			/**
	 		 * Draws a Google graph using the data in a table
			 *
			 * @returns {void}
			 */
			drawChart = function (e) {
				chart = new google.visualization[ options.type ]( chartElem.get(0) );
				chart.draw( data, extraOptions );
			},

			/**
	 		 * Destruct the graph widget on this instance
			 *
			 * @returns {void}
			 */
			destruct = function () {
				$( window ).off( 'resize', drawChart );
				$( document ).off( 'tabShown', tabShown );
			},

			/**
	 		 * Event handler for a tab showing
			 *
			 * @param 	{event} 	e 		Event object
			 * @param 	{object} 	data 	Event data object
			 * @returns {void}
			 */
			tabShown = function (e, data) {
				if( $.contains( data.panel.get(0), elem.get(0) ) ){
					drawChart();
				}
			};

			if( _.isUndefined( google.visualization ) ){
				google.setOnLoadCallback( init );	
			} else {
				init();
			} 

			return {
				init: init,
				drawChart: drawChart
			};
		};

		// Register this module as a widget to enable the data API and
		// jQuery plugin functionality
		ips.ui.registerWidget( 'chart', ips.ui.chart, [
			'type', 'extraOptions', 'format'
		] );

		return {
			respond: respond,
			destruct: destruct,
			getObj: getObj
		};
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.contentItem.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/* global ips, _, Debug */
/**
 * IPS 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.contentItem.js - Autocomplete widget for text fields
 *
 * Author: MTM and Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.contentItem', function(){

		var defaults = {
			multiValues: true,
			unique: false,
			fieldTemplate: 'core.contentItem.field',
			resultsTemplate: 'core.contentItem.resultWrapper',
			resultItemTemplate: 'core.contentItem.resultItem',
			itemTemplate: 'core.contentItem.item',
			queryParam: 'q',
			minAjaxLength: 1
		};

		var respond = function (elem, options) {
			if( !$( elem ).data('_contentItem') ){
				$( elem ).data('_contentItem', contentItemObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Destruct the autocomplete widget on this elem
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		},

		/**
		 * Retrieve the  instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The dialog instance or undefined
		 */
		getObj = function (elem) {
			elem = $( elem );

			if( elem.data('_contentItem') ){
				return elem.data('_contentItem');
			} else if(  $( '[name="' + elem.attr('name') + '_original' + '"]' ).length &&  $( '[name="' + elem.attr('name') + '_original' + '"]' ).data('_contentItem') ){
				return  $( '[name="' + elem.attr('name') + '_original' + '"]' ).data('_contentItem');
			}

			return undefined;
		};

		ips.ui.registerWidget('contentItem', ips.ui.contentItem, 
			[ 'resultsTemplate', 'resultItemTemplate', 'itemTemplate', 'queryParam', 'dataSource', 'maxItems', 'minAjaxLength' ]
		);

		return {
			respond: respond,
			destruct: destruct,
			getObj: getObj
		};
	});

	/**
	 * Content item instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var contentItemObj = function (elem, options, e) {

		var timer,
			blurTimer,
			lastValue = '',
			originalTextField,
			valueField,
			hiddenValueField,
			itemListWrapper,
			textField,
			dataSource,
			elemID = $( elem ).identify().attr('id'),
			wrapper,
			inputItem,
			resultsElem,
			disabled = false,
			required = false,
			tooltip = null,
			tooltipTimer = null;

		/**
		 * Sets up this instance. The datasource object is chosen depending on what options and/or
		 * attributes are provided.
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			if( $( elem ).is('input[type="text"], input[type="search"]') ){
				originalTextField = $( elem );
			} else {
				originalTextField = $( elem ).find('input[type="text"], input[type="search"]').first();
			}

			// Add our autocomplete wrapper to the page, and move the element into it
			_buildWrapper();

			// Set up the data source for this control
			_getDataSource();

			// Remove list from original field
			originalTextField.removeAttr('list');

			// Build the list
			_buildResultsList();

			if( originalTextField.is(':disabled') ){
				disabled = true;
			}

			if( originalTextField.is('[required]') ){
				required = true;
				originalTextField
					.removeProp('required')
					.removeAttr('aria-required');
			}

			// Turn off autocomplete and spellcheck so the browser menu doesn't get in the way
			textField
				.prop( 'autocomplete', 'off' )
				.prop( 'spellcheck', false )
				.prop( 'disabled', disabled )
				.attr( 'aria-autocomplete', 'list' )
				.attr( 'aria-haspopup', 'true' )
				.attr( 'tabindex', originalTextField.attr('tabindex') || '' );

            $( document ).on( 'click', _documentClick );

            wrapper.click(function(e) {
                e.stopPropagation();
                return false;
            });

			// Set up events
			textField
				.on( 'focus', _focusField )
				.on( 'blur', _blurField )
				.on( 'keydown', _keydownField )

			wrapper
				.on( 'click', _clickWrapper )
				.on( 'keydown', _keydownWrapper )
				.on( 'propChanged', _propChanged )
				.toggleClass( 'ipsField_autocompleteDisabled', disabled );
			
			elem.trigger( 'autoCompleteReady', {
				elemID: elemID,
				elem: elem,
				currentValues: contentItems.getValues()
			});
		},
		
		/**
		 * Destruct
		 * Removes event handlers assosciated with this instance
		 *
		 * @returns {void}
		 */
		destruct = function () {
			$( document ).off( 'click', _documentClick );
		},

		/**
 		 * Responds to the propChange event, which we use to determine whether the original field has been toggled
		 *
		 * @returns {void}
		 */
		_propChanged = function (e) {
			disabled = originalTextField.is(':disabled');

			wrapper.toggleClass( 'ipsField_autocompleteDisabled', disabled );
		},

		/**
 		 * Builds the element that results will appear in
		 *
		 * @returns {void}
		 */
		_buildResultsList = function () {

			if( options.resultsElem && $( options.resultsElem ).length ){
				resultsElem = $( options.resultsElem );
				return;
			}

			var resultsList = ips.templates.render( options.resultsTemplate, {
				id: elemID
			});

			wrapper.append( resultsList );

			resultsElem = $('#' + elemID + '_results');

			resultsElem
				.on('mouseover', '[data-id]', function (e) {
					results.select( $( e.currentTarget ) );
				})
				.on('click', '[data-id]', function (e) {
					_addContentItem( $( e.currentTarget ) );
					textField.focus();
				})
				.attr( 'aria-busy', 'false' );
		},

		/**
 		 * Builds the wrapper element that looks like a text input, but allows us to search for items
		 * @returns {void}
		 */
		_buildWrapper = function () {
			var existingClasses = elem[0].className;
			
			$( elem )
				.after( ips.templates.render( options.fieldTemplate, {
					id: elemID
				}))
				.removeClass( existingClasses );

			wrapper = $( '#' + elemID + '_wrapper' );
			inputItem = $( '#' + elemID + '_inputItem' );

			var insertElem = $('<input/>').attr( {
				type: 'text',
				id: elemID + '_dummyInput'
			})
			.prop( 'autocomplete', 'off' );

			textField = insertElem;
			
			// Make a copy of the original text field using its name. This is because it's difficult to set
			// arbitrary values in the original text field later if it's associated with a datalist.
			var name = originalTextField.attr('name');

			originalTextField.attr( 'name', originalTextField.attr('name') + '_original' );
			valueField = $('<input/>').attr( 'name', name ).hide();
			hiddenValueField = $('input[name=' + name + '_values]');
			itemListWrapper = $('[data-contentitem-results=' + name + ']');
			
			originalTextField.hide();

			// Move any classnames on the original element onto our new wrapper to maintain styling,
			// then move the original element into our reserved list element
			wrapper
				.addClass( existingClasses )
				.append( elem )
				.append( valueField )
				.find('#' + elemID + '_inputItem')
					.append( insertElem );
			
			if ( options.maxItems && contentItems.total() >= options.maxItems )
			{
				wrapper.hide();
			}
				
			// Set events for clicking on item
			itemListWrapper
				.on('click', '[data-action="delete"]', function (e) {
					_deleteContentItem( $( e.currentTarget ).parent('[data-id]') );
				});
		},

		/**
 		 * Gets the apprioriate data source for this control
		 *
		 * @returns {void}
		 */
		_getDataSource = function () {
			if( ips.utils.validate.isUrl( options.dataSource ) ){
				dataSource = remoteData( options.dataSource, options );
			} else {
				dataSource = noData();
			}
		},

		/**
 		 * When the wrapper is clicked, we see if a item was clicked. If it was, select it. If not, focus the textbox.
		 *
		 * @returns {void}
		 */
		_clickWrapper = function (e) {
			if( !$( e.target ).is( textField ) && ( !resultsElem || !$.contains( resultsElem.get(0), e.target ) ) ){
				textField.focus();
			}
		},

		/**
 		 * Event handler for focusing on the text field
		 *
		 * @returns {void}
		 */
		_focusField = function (e) {
			if( dataSource.type == 'none' ){
				return;
			}

			timer = setInterval( _timerFocusField, 400 );
		},

		/**
 		 * Event handler for blurring on the text field
		 *
		 * @returns {void}
		 */
		_blurField = function (e) {
			clearInterval( timer );

			_.delay( _timerBlurField, 300 );
		},

		/**
 		 * Timed event hides the results list
		 *
		 * @returns {void}
		 */
		_timerBlurField = function () {
			
			_closeResults();
		},

		/**
 		 * Timed event, checks whether the value has changed, and fetches the results
		 *
		 * @returns {void}
		 */
		_timerFocusField = function () {
			if( dataSource.type == 'none' ){
				return;
			}

			// Fetch the current item value
			var currentValue = _getCurrentValue();

			// If the value hasn't changed, we can leave
			if( currentValue == lastValue ){
				return;
			}

			lastValue = currentValue;

			_loadResults( currentValue );
		},

		/**
 		 * Requests results from the data source, and shows/hides the loading widget
 		 * while that is happening.
		 *
		 * @returns {void}
		 */
		_loadResults = function (value) {
			_toggleLoading('show');

			// Set elem to busy
			resultsElem.attr( 'aria-busy', 'true' );

			// Get the results
			dataSource.getResults( value )
				.done( function (results) {
					// Show the results after processing them
					_showResults( _processResults( results, value ) );
				})
				.fail( function () {

				})
				.always( function () {
					resultsElem.attr( 'aria-busy', 'false' );
					_toggleLoading('hide');
				});
		},

		/**
 		 * Toggles the loading thingy in the control to signify data is loading
		 *
		 * @param 	{string} 	doWhat 	 Acceptable values: 'show' or 'hide'
		 * @returns {void}
		 */
		_toggleLoading = function (doWhat) {
			if( doWhat == 'show' ){
				wrapper.addClass('ipsField_loading');
			} else {
				wrapper.removeClass('ipsField_loading');
			}
		},

		/**
 		 * Closes the suggestions menu, sets the aria attrib, and tells the data source
 		 * to stop loading new results
		 *
		 * @returns {void}
		 */
		_closeResults = function (e) {
			if( e ){
				e.preventDefault();
			}

			if( resultsElem && resultsElem.length ){
				resultsElem
					.hide()
					.attr('aria-expanded', 'false');	
			}		

			dataSource.stop();
		},

		/**
 		 * Handles a click on the document, closing the results dropdown
		 *
		 * @returns {void}
		 */
		_documentClick = function () {
			_closeResults();
		},

		/**
 		 * Processes the results that are returned by the data source
		 *
		 * @returns {void}
		 */
		_processResults = function (results, text) {
			var existingItems = contentItems.getValues(),
				newResults = {};

			$.each( results, function (key, data) {
				if( !data.id || _.indexOf( existingItems, data.id ) === -1 ){
					newResults[ key ] = data;
				}
			});

			return newResults;
		},

		/**
 		 * Gets the current item value from the text field
		 *
		 * @returns {string}
		 */
		_showResults = function (results) {

			var output = '';

			$.each( results, function (idx, value) {
				output += ips.templates.render( options.resultItemTemplate, value );
			});
		
			if( resultsElem.attr('id') == ( elemID + '_results' ) ){
				_positionResults();
			}

			resultsElem
				.show()
				.html( output )
				.attr('aria-expanded', 'true');
		},

		/**
 		 * Sizes and positions the results menu to match the wrapper
		 *
		 * @returns {void}
		 */
		_positionResults = function () {

			resultsElem.css( {
				width: wrapper.outerWidth() + 'px'
			});

			var positionInfo = {
				trigger: wrapper,
				targetContainer: wrapper,
				target: resultsElem,
				center: false
			};

			var resultsPosition = ips.utils.position.positionElem( positionInfo );

			$( resultsElem ).css({
				left: '0px',
				top: resultsPosition.top + 'px',
				position: ( resultsPosition.fixed ) ? 'fixed' : 'absolute',
				zIndex: ips.ui.zIndex()
			});
		},

		/**
 		 * Gets the current item value from the text field
		 *
		 * @returns {string}
		 */
		_getCurrentValue = function () {
			var value = textField.val();
			return value;
		},

		/**
 		 * Event handler for keydown event in wrapper.
 		 * We check for esape here, because if options.freeChoice is disabled, there's no textbox to
 		 * watch for events. By watching for escape on the wrapper, we can still close the menu.
		 *
		 * @returns {void}
		 */
		_keydownWrapper = function (e) {
			if( e.keyCode == ips.ui.key.ESCAPE ){
				keyEvents.escape(e);
			}
		},

		/**
 		 * Event handler for keydown event in text field
		 *
		 * @returns {void}
		 */
		_keydownField = function (e) {
			_expandField();
			var ignoreKey = false;

			// Ignore irrelevant keycodes
			if( !_( [ ips.ui.key.UP, ips.ui.key.DOWN, ips.ui.key.ESCAPE, ips.ui.key.ENTER
					 ] ).contains( e.keyCode ) ){
				ignoreKey = true;
			}
			
			var value = textField.val().trim();
			
			if( ignoreKey ){
				return;
			}
			
			switch(e.keyCode){
				// Suggestions keys
				case ips.ui.key.UP:
					keyEvents.up(e);
				break;
				case ips.ui.key.DOWN:
					keyEvents.down(e);
				break;
				case ips.ui.key.ESCAPE:
					keyEvents.escape(e);
				break;
				case ips.ui.key.ENTER:
					keyEvents.enter(e);
				break;
			}
		},

		/**
 		 * A wrapper method for contentItems.add which also clears the text field
 		 * and hides it if options.maxItems is reached
		 *
		 * @returns {void}
		 */
		_addContentItem = function (elem) {
			contentItems.add( elem );
			textField.val('');
			lastValue = '';
			_resetField();

			if( options.maxItems && contentItems.total() >= options.maxItems ){
				wrapper.hide();
			}
		},

		/**
 		 * A wrapper method for contentItems.remove which shows the text field if we're under
 		 * our options.maxItems limit
		 *
		 * @returns {void}
		 */
		_deleteContentItem = function (item) {
			if( disabled ){
				return;
			}
			
			contentItems.remove( item );
		},

		/**
 		 * Object containing event handlers bound to individual keys
 		 */
		keyEvents = {

			/**
	 		 * Handler for 'up' key press. Selects previous item in the results list.
			 *
			 * @returns {void}
			 */
			up: function (e) {
				if( !resultsElem || !resultsElem.is(':visible') ){
					return;
				}

				e.preventDefault();

				var selected = results.getCurrent();				

				if( !selected ){
					results.selectLast();
				} else {
					var prev = results.getPrevious( selected );

					if( prev ){
						results.select( prev );
					} else {
						results.selectLast();
					}
				}
			},

			/**
	 		 * Handler for 'down' key press. Selects next item in the results list.
			 *
			 * @returns {void}
			 */
			down: function (e) {
				if( !resultsElem || !resultsElem.is(':visible') ){
					return;
				}

				e.preventDefault();

				var selected = results.getCurrent();
				
				if( !selected ){
					results.selectFirst();
				} else {
					var next = results.getNext( selected );

					if( next ){
						results.select( next );
					} else {
						results.selectFirst();
					}
				}

			},
			
			/**
	 		 * Enter/tab handler. If text has been entered, we add it as a item, otherwise pass through
	 		 * to the browser to handle.
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void,boolean} 		
			 */
			enter: function (e) {
				e.preventDefault();

				var currentResult = results.getCurrent();
				var value = '';

				if( currentResult ){
					value = currentResult.attr('data-id');
				}
				
				if( !value ){
					return false;
				}

				_addContentItem( currentResult );
			},

			/**
	 		 * Handler for 'escape' key press. Closes the suggestions menu, if it's open.
			 *
			 * @returns {void}
			 */
			escape: function (e) {
				if( resultsElem && resultsElem.is(':visible') ){
					_closeResults();
				}
			}

		},

		/**
 		 * Object containing methods for dealing with the results list.
 		 */
		results = {

			/**
	 		 * Deselects any selected results
			 *
			 * @returns {void}
			 */
			deselectAll: function () {
				resultsElem
					.find('[data-selected]')
					.removeAttr('data-selected');
			},

			/**
	 		 * Returns the currently selected result
			 *
			 * @returns {element,boolean} 	Returns the jQuery object containing the selected result, or false
			 */
			getCurrent: function () {
				if( dataSource.type == 'none' ){
					return;
				}

				var cur = resultsElem.find('[data-selected]');

				if( cur.length && resultsElem.is(':visible') ){
					return cur;
				} 

				return false;
			},

			/**
	 		 * Gets the result preceding the provided result
			 *
			 * @returns {element,boolean} 	Returns the jQuery object containing the selected result, or false
			 */
			getPrevious: function (result) {
				var prev = $( result ).prev('[data-id]');

				if( prev.length ){
					return prev;
				}

				return false;
			},

			/**
	 		 * Gets the result following the provided result
			 *
			 * @returns {element,boolean} 	Returns the jQuery object containing the selected result, or false
			 */
			getNext: function (result) {
				var next = $( result ).next('[data-id]');

				if( next.length ){
					return next;
				}

				return false;
			},

			/**
	 		 * Selects the first result
			 *
			 * @returns {void}
			 */
			selectFirst: function () {
				results.select( resultsElem.find('[data-id]').first() );
			},

			/**
	 		 * Selects the last result
			 *
			 * @returns {void}
			 */
			selectLast: function () {
				results.select( resultsElem.find('[data-id]').last() );
			},

			/**
	 		 * Selects the provided item
			 *
			 * @returns {void}
			 */
			select: function (result) {
				results.deselectAll();
				
				result.attr('data-selected', true);
			}
		},

		/* ! Content Items */
		/**
 		 * Object containing item methods
 		 */
		contentItems = {

			selected: null,

			/**
	 		 * Adds an item to the control
			 *
			 * @param 	{object} 	elem 	Element from result list to add
			 * @returns {void}
			 */
			add: function (elem) {
				var html = '';
				var obj  = $(elem).find('[data-role=contentItemRow]');
				html = obj.html();

				itemListWrapper.append( ips.templates.render( options.itemTemplate, {
					id: obj.attr('data-itemid'),
					html: html
				}));

				if( resultsElem ){
					_closeResults();
				}

				// Update hidden field
				hiddenValueField.val( contentItems.getValues().join( ',' ) );
				
				if ( options.maxItems && contentItems.total() >= options.maxItems )
				{
					wrapper.hide();
				}
				
				elem.trigger('contentItemAdded', {
					html: html,
					itemList: contentItems.getValues(),
					totalItems: contentItems.total()
				});

				return true;
			},

			/**
	 		 * Deletes the given item
			 *
			 * @param 	{element} 	item 	The item element to select
			 * @returns {void}
			 */
			remove: function (item) {
				if( contentItems.selected == item ){
					contentItems.selected = null;
				}

				var value = $( item ).attr('data-value');
				$( item ).remove();

				if( options.maxItems && contentItems.total() < options.maxItems ){
					wrapper.show();
				}

				// Update text field
				hiddenValueField.val( contentItems.getValues().join( ',' ) );

				elem.trigger('contentItemDeleted', {
					item: item,
					itemList: contentItems.getValues(),
					totalItems: contentItems.total()
				});
			},

			/**
	 		 * Returns total number of items entered
			 *
			 * @returns {number}
			 */
			total: function () {
				return itemListWrapper.find('[data-id]').length;
			},

			/**
	 		 * Returns all of the values
			 *
			 * @param 	{element} 	item 	The item element to select
			 * @returns {void}
			 */
			getValues: function () {
				var values = [];
				var allContentItems = itemListWrapper.find('[data-id]');
				if( allContentItems.length ){
					values = _.map( allContentItems, function( item ){
						return $( item ).attr('data-id');
					});
				}

				return values;
			}
		},

		/**
 		 * Determines whether the value would be a duplicate
		 *
		 * @param 	{string} 	value 	Value to check
		 * @returns {void}
		 */
		_duplicateValue = function (value) {
			var values = contentItems.getValues();

			if( values.indexOf( value ) !== -1 ){
				return true;
			}

			return false;
		},
		
		/**
 		 * Expands the text field to fit the given text
		 *
		 * @returns {void}
		 */
		_expandField = function () {
			var text = textField.val();
			var widthOfElem = wrapper.width();

			widthOfElem -= ( parseInt( wrapper.css('padding-left') ) + parseInt( wrapper.css('padding-right') ) );

			// Create temporary span
			var span = $('<span/>').text( text ).css({
				'font-size': textField.css('font-size'),
				'letter-spacing': textField.css('letter-spacing'),
				'position': 'absolute',
				'top': '-100px',
				'left': '-300px',
				'opacity': "0.1"
			});

			ips.getContainer().append( span );

			// Get the width
			var width = span.width() + 20;

			// Remove it
			span.remove();

			textField.css({
				width: ( ( width >= widthOfElem ) ? widthOfElem : width ) + 'px'
			});
		},

		/**
 		 * Resets the width of the text input
		 *
		 * @returns {void}
		 */
		_resetField = function () {
			textField.css({
				width: '15px'
			});
		};

		init();

		return {
			init: init,
			destruct: destruct,
			addContentItem: contentItems.add,
			getContentItem: contentItems.getValues,
			removeContentItem: contentItems.remove
		};
	};

	/**
	 * Handler for remote data retrieval
	 */
	var remoteData = function (source, options) {

		var ajaxObj,
			loadedCache = false,
			cache = {};

		/**
 		 * Initiates either a remote search or a remote fetch
		 *
		 * @returns {promise}
		 */
		var getResults = function (text) {
			return _remoteSearch( text );
		},

		/**
 		 * Returns the number of items in the result set
		 *
		 * @returns {number}
		 */
		totalItems = function () {
			return -1;
		},

		/**
 		 * Does a remote search (i.e. passing search string to backend, and returning results)
		 *
		 * @param 	{string}	String to search for
		 * @returns {promise}
		 */
		_remoteSearch = function (text) {
			var deferred = $.Deferred();

			if( ajaxObj ){
				ajaxObj.abort();
			}

			if( options.minAjaxLength > text.length ){
				deferred.reject();
				return deferred.promise();
			}

			if( cache[ text ] ){
				deferred.resolve( cache[ text ] );
			} else {				
				ajaxObj = ips.getAjax()( source + '&' + options.queryParam + '=' + encodeURIComponent( text ), { dataType: 'json' } )
					.done( function (response) {
						deferred.resolve( response );
						cache[ text ] = response;
					})
					.fail( function (jqXHR, status, errorThrown) {
						if( status != 'abort' ){
							Debug.log('aborting');
						}
						deferred.reject();
					});
			}

			return deferred.promise();
		},

		/**
 		 * Fetches remote data, and then performs a local search on the data to find results
		 *
		 * @param 	{string}	String to search for
		 * @returns {promise}
		 */
		_remoteFetch = function (text) {
			var deferred = $.Deferred();

			if( !loadedCache ){
				if( ajaxObj ){
					return;
				}

				if( options.minAjaxLength > text.length ){
					return;
				}

				ajaxObj = ips.getAjax()( source, { dataType: 'json' } )
					.done( function (response) {
						loadedCache = true;
						cache = response;
						_remoteFetch( text );
					})
					.fail( function (jqXHR, status, errorThrown) {
						if( status != 'abort' ){
							Debug.log('aborting');
						}
						deferred.reject();
					});
			}

			// Search through the cache for results
			cache.each( function (idx, item) {
				if( item.value.toLowerCase().startsWith( text ) ){
					output.push( item );
				}
			});

			return deferred.promise();
		},

		/**
 		 * Aborts the ajax request
		 *
		 * @param 	{string}	String to search for
		 * @returns {void}
		 */
		stop = function () {
			if( ajaxObj ){
				ajaxObj.abort();
			}
		};

		return {
			type: 'remote',
			getResults: getResults,
			totalItems: totalItems,
			stop: stop
		};
	};

	var noData = function () {
		return {
			type: 'none',
			getResults: $.noop,
			totalItems: -1,
			stop: $.noop
		};
	};

}(jQuery, _));

]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.copy.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.copy.js - Widget that has something that can be copied to clipboard
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.createModule('ips.ui.copy', function(){

		var respond = function (elem, options, e) {
			ips.loader.get( ['core/interface/clipboard/clipboard.min.js'] ).then( function()
	        {
		        if ( ClipboardJS.isSupported() ) {
			        elem.find('[data-role=&quot;copyButton&quot;]').show();
			
					var clipboard = new ClipboardJS( elem.find('[data-role=&quot;copyButton&quot;]').get(0) );
										
					clipboard.on('success', function(e) {
						elem.find('[data-role=&quot;copyButton&quot;]').text( ips.getString('copied') );
					});
				}
			} );
		};
		
		ips.ui.registerWidget( 'copy', ips.ui.copy, [] );

		return {
			respond: respond,
		};
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.dialog.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/* global ips, _, Debug */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.dialog.js - Popup dialog UI component
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.dialog', function(){

		var defaults = {
			modal: true,
			draggable: false,
			className: 'ipsDialog',
			extraClass: '',
			close: true,
			fixed: false,
			narrow: false,
			callback: null,
			forceReload: false,
			flashMessage: '',
			flashMessageTimeout: 2,
			flashMessageEscape: true,
			remoteVerify: true,
			remoteSubmit: false,
			destructOnClose: false,
			ajax: { type: 'get', data: {} }
		};

		var showStack = [];

		/**
 		 * Respond to a dialog trigger
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed
		 * @returns {void}
		 */
		var respond = function (elem, options, e) {
			e.preventDefault();

			// If no option URL and no local content is specified, see if we can use
			// the href of the source element
			if( !options.url && !options.content && $( elem ).attr('href') ){
				options.url = $( elem ).attr('href');
			}

			if( !$( elem ).data('_dialog') ){
				$( elem ).data('_dialog', dialogObj(elem, _.defaults( options, defaults ) ) );
			}

			$( elem ).data('_dialog').show();
		},

		/**
		 * Retrieve the dialog instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The dialog instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_dialog') ){
				return $( elem ).data('_dialog');
			}

			return undefined;
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
				$( elem ).removeData('_dialog');
			}
		},

		/**
		 * Creates a dialog that is not attached to a specific element
		 *
		 * @param	{object} 	options 		Options passed to the dialog
		 * @returns {object} 	The dialog instance
		 */
		create = function (options) {
			return dialogObj( null, _.defaults( options, defaults ) );
		},

		/**
		 * Determine if there are any open dialogs
		 *
		 * @returns	{bool}
		 */
		hasOpenDialogs = function() {
			return ( showStack.length > 0 );
		},

		/**
		 * Init
		 * Sets up events used to manage multiple dialog instances, primarily
		 * the escape key to hide the forefront dialog
		 *
		 * @returns {void}
		 */
		_init = function () {
			// Set up event checking for ESC
			$( document )
				.on( 'keydown', function (e) {
					if( e.keyCode == ips.ui.key.ESCAPE ){
						$( document ).trigger( 'closeDialog', {
							dialogID: showStack[ showStack.length - 1 ]
						});
					}
				})
				.on( 'openDialog', function (e, data) {
					showStack.push( data.dialogID );
				})
				.on( 'hideDialog', function (e, data) {
					showStack = _.without( showStack, data.dialogID );
				});
		};

		ips.ui.registerWidget('dialog', ips.ui.dialog, [ 
			'url', 'modal', 'draggable', 'size', 'title', 'close', 'fixed', 'destructOnClose', 'extraClass',
			'callback', 'content', 'forceReload' , 'flashMessage', 'flashMessageTimeout', 'flashMessageEscape', 'showFrom', 'remoteVerify', 'remoteSubmit'
		], { lazyLoad: true, lazyEvents: 'click' } );

		_init();

		return {
			respond: respond,
			destruct: destruct,
			getObj: getObj,
			create: create,
			hasOpenDialogs: hasOpenDialogs
		};
	});

	/**
	 * Dialog instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var dialogObj = function (elem, options) {

		var modal, // The modal background
			dialog, // The dialog element itself
			ajaxObj,
			dialogID = '',
			elemID = '',
			dialogBuilt = false,
			contentLoaded = false,
			modalEvent = { up: false, down: false };

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			
			if( elem === null ){
				elemID = 'elem_' + ( Math.round( Math.random() * 10000000 ) );
			} else {
				elemID = $(elem).identify().attr('id');
			}

			dialogID = elemID + '_dialog';

			// If we're fullscreen, make sure we're fixed too
			if( options.size == 'fullscreen' ){
				options.fixed = true;
			}

			// We watch for this on the document, to give our pages a chance
			// to intercept the event and cancel it (e.g. an unsaved form)
			$( document ).on( 'closeDialog', closeDialog );
		},

		/**
		 * Destruct the dialog for this instance
		 *
		 * @returns 	{void}
		 */
		destruct = function () {
			$( document ).off( 'closeDialog', closeDialog );

			if( modal ){
				modal.remove();
			}

			if( dialog ){
				dialog.remove();
			}
		},

		/**
		 * Event handler for the closeDialog event
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		closeDialog = function (e, data) {
			if( data && data.originalEvent ){
				data.originalEvent.preventDefault();
			}

			if( data && data.dialogID == dialogID ){
				hide();
				modalEvent = { up: false, down: false };
			}
		},

		/**
		 * Hides this dialog
		 *
		 * @returns 	{void}
		 */
		hide = function () {
			var deferred = $.Deferred();

			if( options.fixed ){
				$('body').removeClass('ipsNoScroll');
			}

			dialog.animationComplete( function () {
				if( options.forceReload || options.destructOnClose ){
					ips.controller.cleanContentsOf( dialog );
					dialog.find( '.' + options.className + '_content' ).html('');
				}
				
				$( elem || document ).trigger('hideDialog', {
					elemID: elemID,
					dialogID: dialogID,
					dialog: dialog
				});

				if( options.destructOnClose ){
					ips.ui.dialog.destruct(elem);
				}

				deferred.resolve();
			});

			ips.utils.anim.go( 'fadeOutDown fast', dialog );

			if( options.modal ){
				ips.utils.anim.go( 'fadeOut fast', modal );
			}

			return deferred.promise();
		},

		/**
		 * Public method for showing the dialog
		 * Builds local or remote dialog if necessary, then shows it
		 *
		 * @param		{bool}		initOnly	If TRUE, will create dialog but not show it
		 * @returns 	{void}
		 */
		show = function ( initOnly ) {
			if( options.url && !contentLoaded ){
				_remoteDialog( initOnly );
			} else if( !contentLoaded ) {
				_localDialog( initOnly );
			} else {
				if ( initOnly ) {
					return;
				}
				
				// Dialog already exists, so reset the zIndex and show it
				if( modal ){
                    modal.css( { zIndex: ips.ui.zIndex() } );
				}

				dialog.css( { zIndex: ips.ui.zIndex() } );
				_positionDialog();
				_showDialog();				
			}
		},

		/**
		 * Remove the dialog
		 *
		 * @returns 	{void}
		 */
		remove = function (hideFirst) {

			var doRemove = function () {
				if( ajaxObj && _.isFunction( ajaxObj.abort ) ){
					ajaxObj.abort();
				}

				// Remove the elements
				dialog.remove();

				if( modal ){
					modal.remove();
				}

				// Not built
				dialog = null;
				modal = null;
				dialogBuilt = false;
				contentLoaded = false;
				ajaxObj = null;
			};

			// If we're hiding first, we'll do it after the animation has finished
			if( hideFirst && dialog.is(':visible') ){
				hide().done( function () {
					doRemove();
				});
			} else {
				doRemove();
			}
		},

		/**
		 * Sets the dialog to 'loading' state.
		 * Hides the content, and adds a loading thingy.
		 *
		 * @returns 	{void}
		 */
		setLoading = function (loading) {
			if( loading ){
				dialog
					.find( '.' + options.className + '_loading')
						.show()
					.end()
					.find( '.' + options.className + '_content' )
						.hide();

				_positionDialog();
			} else {
				dialog
					.find( '.' + options.className + '_loading')
						.hide()
					.end()
					.find( '.' + options.className + '_content' )
						.show();
			}
		},

		/**
		 * Updates the contents of the dialog
		 *
		 * @returns 	{void}
		 */
		updateContent = function (newContent) {
			dialog.find( '.' + options.className + '_content' ).html( newContent );
			
			$( document ).trigger('contentChange', [ dialog ]);
		},

		/**
		 * Internal method to actually show the dialog
		 * Triggers the openDialog event to let the document know
		 *
		 * @returns 	{void}
		 */
		_showDialog = function () {
			if( options.fixed ){
				$('body').addClass('ipsNoScroll');
			}

			if( options.modal ){
				ips.utils.anim.go('fadeIn', modal);
			}

			if( options.showFrom && $( options.showFrom ).is(':visible') ){
				_showFrom( options.showFrom );
			} else {
				ips.utils.anim.go('fadeInDown', dialog)
					.done( function () {
						dialog.find( '.' + options.className + '_loading').removeClass('ipsLoading_noAnim');
					});
			}

			$( elem || document ).trigger('openDialog', {
				elemID: elemID,
				dialogID: dialogID,
				dialog: dialog,
				contentLoaded: contentLoaded
			});
		},

		/**
		 * Displays the popup zooming from the provided element
		 *
		 * @param		{element}	showFrom	The element from which the dialog will pop up
		 * @returns 	{void}
		 */
		_showFrom = function (showFrom) {
			// Get the position of the 'from' element
			dialog.show();
			var dialogBit = dialog.find('>div');
			var dialogPosition = ips.utils.position.getElemPosition( dialogBit );
			var dialogHeight = dialogBit.outerHeight();
			var dialogWidth = dialogBit.outerWidth();
			dialog.hide();

			// 'showFrom' position
			var showFrom = $( options.showFrom );
			var fromPosition = ips.utils.position.getElemPosition( showFrom );
			var fromPositionWidth = showFrom.width();
			var fromPositionHeight = showFrom.height();

			// Document sizing
			var docSize = $( document ).outerWidth();

			// Figure out the offset from the halfway mark
			var dialogCenterLeft = dialogPosition.absPos.left + ( dialogWidth / 2 );
			var dialogCenterTop = dialogPosition.absPos.top + ( dialogHeight / 2 );

			var widthDifference = ( fromPosition.absPos.left + ( fromPositionWidth / 2 ) - dialogCenterLeft );
			var heightDifference = ( fromPosition.absPos.top + ( fromPositionHeight / 2 ) - dialogCenterTop );

			$( dialog )
				.show();

			$( dialogBit )
				.css({
					transform: 'translateY(' + heightDifference + 'px) translateX(' + widthDifference + 'px) scale(0.1)',
					opacity: "1"
				})
				.animate( {
					transform: 'translateY(0px) translateX(0px) scale(1)',
					opacity: "1"
				}, { easing: 'swing', complete: function () {
					dialog.find( '.' + options.className + '_loading').removeClass('ipsLoading_noAnim');
				} } );
		},

		/**
		 * Builds a dialog from remote content
		 *
		 * @param		{bool}	initOnly	If TRUE, will create dialog but not show it
		 * @returns 	{void}
		 */
		_remoteDialog = function ( initOnly ) {

			// Build dialog wrapper
			if( !dialogBuilt ){
				if( options.modal ){
					_buildModal();
				}

				_buildDialog();
			}
			
			if ( initOnly ) {
				_fetchContent();
			} else {
				setLoading( true );
				_showDialog();			
				_fetchContent();
			}
			
			if( !options.forceReload ){
				contentLoaded = true;
			}
		},

		/**
		 * Builds a dialog from a local element
		 *
		 * @param		{bool}	initOnly	If TRUE, will create dialog but not show it
		 * @returns 	{void}
		 */
		_localDialog = function ( initOnly ) {

			if( !options.content && !$( options.content ).length ){
				Debug.warn("'content' option not specified for dialog, or element doesn't exist");
				return;
			}

			if( !dialogBuilt ){
				if( options.modal ){
					_buildModal();
				}

				_buildDialog();
			}
			
			if ( initOnly ) {
				return;
			}

			dialog.find( '.' + options.className + '_content').html( $( options.content ).first().show() );

			_showDialog();

			if( !options.forceReload ){
				contentLoaded = true;
			}
		},

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		_fetchContent = function () {

			var deferred = $.Deferred();

			// Set content to loading
			setLoading( true );

			// Get the content
			ajaxObj = ips.getAjax()( options.url, {
				type: options.ajax.type,
				data: options.ajax.data
				} )
				.done( function (response) {

					// Set our content
					setLoading( false );
					updateContent( response );
					deferred.resolve();
					
					// Run callback
					if ( options.callback !== null ) {
						options.callback( dialog );
					}
										
					// Send trigger
					$( elem || document ).trigger('dialogContentLoaded', {
						elemID: elemID,
						dialogID: dialogID,
						dialog: dialog,
						contentLoaded: true
					});
				})
				.fail( function (jqXHR, status, errorThrown) {
					if( jqXHR.responseJSON ){
						ips.ui.alert.show({
							message: jqXHR.responseJSON,
						});
						setLoading(false);
						contentLoaded = false;
						hide();
					} else if( Debug.isEnabled() ){
						Debug.error( "Ajax request failed (" + status + "): " + errorThrown );
					} else if ( elem ) {
						window.location = elem.href;
					} else {
						ips.ui.alert.show({
							message: ips.getString('errorLoadingContent'),
						});
						setLoading(false);
						contentLoaded = false;
						hide();
					}

					deferred.reject();
				})
				.always( function () {
					//_removeLoadingWidget();
				});

			return deferred.promise();
		},

		/**
		 * Builds the dialog frame
		 *
		 * @returns 	{void}
		 */
		_buildDialog = function () {

			if( dialogBuilt ){
				return;
			}

			var offset = 0;

			// Build dialog
			$('body').append(
				 ips.templates.render( 'core.dialog.main', {
					'class': options.className,
					title: options.title || '',
					id: dialogID,
					fixed: options.fixed,
					size: options.size,
					close: options.close,
					extraClass: options.extraClass
				})
			);

			dialog = $( '#' + dialogID );

			// Add to body
			dialog.css( {
				zIndex: ips.ui.zIndex(),
			});

			_positionDialog();
			
			// Add events
			dialog.on('click', '[data-action="dialogClose"]', function (e) {
				// We trigger on the dialog, but watch on the document
				$( dialog ).trigger('closeDialog', { 
					dialogID: dialogID,
					originalEvent: e
				});
			});

			$( dialog ).on('closeDialog', function (e, data) {
				hide();
			});

			if( options.close ){
				dialog.on( 'mouseup', function (e) {
					// This check is necessary so that if you click in the dialog then drag your mouse out and release over
					// the modal, we don't detect it as a full click on the modal. 
					if( e.target == dialog.get(0) ){
						modalEvent.up = true;
					}					
				});

				dialog.on( 'mousedown', function (e) {
					if( e.target == dialog.get(0) ){
						modalEvent.down = true;
					}					
				});

				dialog.on( 'click', function (e) {
					Debug.log( e.target );

					// If target still exists and isn't a child of the dialog, trigger closeDialog
					if( ( !modalEvent.up || ( dialog.get(0) == e.target && modalEvent.down ) ) && // Mouse up didn't happen on the modal, or it did but we clicked the modal completely
							dialog.find('> div').get(0) != e.target &&
							!$.contains( dialog.find('> div').get(0), e.target ) && 
							$.contains( document, e.target ) 
					){
						$( dialog ).trigger('closeDialog', { 
							dialogID: dialogID,
							originalEvent: e
						});
					}

					modalEvent = { up: false, down: false };
				});
			}

			if( options.remoteVerify || options.remoteSubmit ){
				dialog.find( '.' + options.className + '_content' ).on('submit', 'form', function(e) {
					_ajaxFormSubmit(e, $( this ) );
				});
			}

			dialogBuilt = true;
		},

		/**
		 * Positions the dialog window
		 *
		 * @returns 	{void}
		 */
		_positionDialog = function () {
			// Get the body scroll position
			if( dialog && !options.fixed ){
				var win = $( window );
				var offset = win.scrollTop();

				dialog.css({
					top: offset + 'px'
				});
			}
		},

		/**
		 * Fetches a modal element from ips.ui and sets the zindex on it
		 *
		 * @returns 	{void}
		 */
		_buildModal = function () {
			modal = ips.ui.getModal();
			modal.css( { zIndex: ips.ui.zIndex() } );
		},
		
		/**
		 * Submit a form within the dialog using AJAX
		 *
		 * @param		{event}		e			The submit event
		 * @param		{element} 	elem 		The element this widget is being created on
		 * @returns		{void}
		 */
		_ajaxFormSubmit = function(e, form) {
			if( form.attr('data-bypassValidation') ){
				return false;
			}

			e.preventDefault();
			setLoading( true );
			
			// This is necessary to stop CKEditor fields submitting blank
			try {
				if( !_.isUndefined( CKEDITOR ) && CKEDITOR != null ){
					for( var instance in CKEDITOR.instances ) {
						CKEDITOR.instances[ instance ].updateElement();
					}
				}
			} catch (err) { }
			
			var url = form.attr('action');
			var ajaxUrl = url;

			if( options.remoteVerify ){
				var joinWith = '?';
				
				if ( ajaxUrl.indexOf('?') != -1 ){
					joinWith = '&';
				}
				
				ajaxUrl	= ajaxUrl + joinWith + 'ajaxValidate=1';
			}

			ips.getAjax()( ajaxUrl, {
				data: form.serialize(),
				type: 'post'
			} )
				.done( function (response, status, jqXHR) {

					// If we are verifying remotely, and we haven't already checked everything is fine...
					if( options.remoteVerify && !form.attr('data-bypassValidation') ){
						if( jqXHR.getAllResponseHeaders().indexOf('X-IPS-FormError: true') !== -1 || jqXHR.getAllResponseHeaders().indexOf('X-IPS-FormNoSubmit: true') !== -1 || jqXHR.getAllResponseHeaders().indexOf('x-ips-formerror: true') !== -1 || jqXHR.getAllResponseHeaders().indexOf('x-ips-formnosubmit: true') !== -1 ){
							Debug.log('Validation failed');
							setLoading( false );
							updateContent( response );
							return;
						}
					}
					
					if( options.remoteSubmit ){

						var doneAfterSubmit = function (submitResponse) {

							// If we're submitting via ajax, then we've already done that; just need to trigger an event and hide the dialog
							$( elem || document ).trigger('submitDialog', {
								elemID: elemID,
								dialogID: dialogID,
								dialog: dialog,
								contentLoaded: contentLoaded,
								response: submitResponse
							});

							setLoading( false );
							contentLoaded = false; // This will cause the dialog to be reloaded again if we open it again, which we want so our previous values aren't still inputted
							hide();
							
							if( options.flashMessage ){
								ips.ui.flashMsg.show( options.flashMessage, { timeout: options.flashMessageTimeout, escape: options.flashMessageEscape } );
							}
						};

						// If we verified this submission first, we actually need to submit again, without the verification this time
						if( options.remoteVerify ) {
							ips.getAjax()( url, {
								data: form.serialize(),
								type: 'post',
								bypassRedirect: true
							})
								.done( function (response, status, jqXHR) {
									if( jqXHR.getAllResponseHeaders().indexOf('X-IPS-FormError: true') !== -1 || jqXHR.getAllResponseHeaders().indexOf('X-IPS-FormNoSubmit: true') !== -1 || jqXHR.getAllResponseHeaders().indexOf('x-ips-formerror: true') !== -1 || jqXHR.getAllResponseHeaders().indexOf('x-ips-formnosubmit: true') !== -1 ){
										form.attr( 'data-bypassValidation', true ).submit();
									} else {
										doneAfterSubmit( response );
									}
								})
								.fail( function (jqXHR, status, errorThrown) {
									form.attr( 'data-bypassValidation', true ).submit();
								});
						} else {
							doneAfterSubmit( response );
						}
						
					} else if( jqXHR.getAllResponseHeaders().indexOf('X-IPS-FormNoSubmit: true') !== -1 || jqXHR.getAllResponseHeaders().indexOf('x-ips-formnosubmit: true') !== -1 ) {
						// If the response from the verification told us not to submit the form, we'll update the dialog
						setLoading( false );
						updateContent( response );
					} else {
						// Otherwise, we've passed verification and we can submit the form as normal
						form.attr( 'data-bypassValidation', true ).submit();
					}
				})
				.fail( function () {
					form.attr( 'data-bypassValidation', true ).submit();
				});
		};

		init();

		return {
			init: init,
			show: show,
			hide: hide,
			remove: remove,
			setLoading: setLoading,
			updateContent: updateContent,
			dialogID: dialogID,
			destruct: destruct
		};
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.drawer.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/* global ips, _ */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.drawer.js - A drawer (e.g. iOS-style sidebar) widget
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.drawer', function(){

		var defaults = {};

		/**
		 * Respond to a menu trigger being clicked
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{event} 	e 		 	The event object
		 * @returns {void}
		 */
		var respond = function (elem, options, e) {
			e.preventDefault();

			if( !$( elem ).data('_drawer') ){
				$( elem ).data('_drawer', drawerObj(elem, _.defaults( options, defaults ) ) );
			}

			$( elem ).data('_drawer').show();
		};

		ips.ui.registerWidget('drawer', ips.ui.drawer, 
			[ 'drawerElem' ],	
			{ lazyLoad: true, lazyEvents: 'click' } 
		);

		return {
			respond: respond
		};
	});

	/**
	 * Drawer instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var drawerObj = function (elem, options) {

		var modal, // The modal background
			drawerElem,
			drawerContent;

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			modal = ips.ui.getModal().addClass('ipsDrawer_modal');
			drawerElem = $( options.drawerElem ),
			drawerContent = drawerElem.find('.ipsDrawer_menu');

			drawerElem.on('click', '[data-action="close"]', function (e) {
				e.preventDefault();
				hide();
			});

			drawerElem.on('click', function (e) {
				if( !$.contains( drawerContent.get(0), e.target ) ){
					hide();
				}
			});

			// set up sub-menus
			drawerElem
				.on( 'click', '.ipsDrawer_itemParent > h4', _showSubMenu )
				.on( 'click', '[data-action="back"]', _subMenuBack )
				.find('.ipsDrawer_itemParent > ul')
					.addClass('ipsDrawer_subMenu')
					.hide();
		},

		_showSubMenu = function (e) {
			e.preventDefault();

			var item = $( e.currentTarget );
						
			item
				.parents('.ipsDrawer_list')
					.animate( ( $('html').attr('dir') === 'rtl' ) ? { marginRight: '-100%' } : { marginLeft: '-100%' } )
				.end()
				.siblings('.ipsDrawer_list')
					.show();

			drawerElem.find('.ipsDrawer_content').animate({
				scrollTop: "0px"
			});
		},

		_subMenuBack = function (e) {
			e.preventDefault();
			
			var item = $( e.currentTarget ),
				thisMenu = item.parent('.ipsDrawer_list');
						
			thisMenu	
				.parents('.ipsDrawer_list')
				.first()
					.animate( ( $('html').attr('dir') === 'rtl' ) ? { marginRight: '0' } : { marginLeft: '0' }, function () {
						thisMenu.hide();
					});

		},

		show = function () {
			window.scrollTo(0,-1);

			// Show modal
			modal.css( { zIndex: ips.ui.zIndex() } );
			
			// Hide close elem
			drawerElem.find('.ipsDrawer_close').hide();
			ips.utils.anim.go( 'fadeIn fast', modal );

			// Show drawer
			drawerElem
				.css( { zIndex: ips.ui.zIndex() } )
				.show();
				
			if( $('html').attr('dir') === 'rtl' ) {
				ips.utils.anim.go( 'slideRight fast', drawerElem );
			} else {
				ips.utils.anim.go( 'slideLeft fast', drawerElem );
			}

			drawerElem.find('.ipsDrawer_close').delay(500).fadeIn();

			// Make body non-scrolly
			$('body').css( {
				overflow: 'hidden'
			});
		},

		hide = function () {
			ips.utils.anim.go( 'fadeOut fast', modal );

			drawerElem.hide();

			$('body').css( {
				overflow: 'auto'
			});
		};

		init();

		return {
			init: init,
			show: show,
			hide: hide
		};
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.editor.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/* global ips, _, CKEDITOR */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.editor.js - Editor widget
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.editor', function(){
		
		var defaults = {
			allbuttons: false,
			postKey: '',
			toolbars: '',
			extraPlugins: '',
			contentsCss: '',
			minimized: false,
			autoSaveKey: null,
			skin: 'ips',
			autoGrow: true,
			pasteBehaviour: 'rich',
			autoEmbed: true,
			controller: null,
			defaultIfNoAutoSave: false,
			minimizeAfterReset: false
		};
		
		/**
		 * Respond method, sets up the editor widget.
		 * Loads the CKEditor libraries, then boots the editor
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var respond = function (elem, options) {
			
			var loadTries = 0;
			
			var fileToLoad = ips.getSetting('useCompiledFiles') !== true ? 'core/dev/ckeditor/ckeditor.js' : 'core/interface/ckeditor/ckeditor/ckeditor.js';
						
			if ( !options.minimized || ips.getSetting('memberID') ) {
				ips.loader.get([fileToLoad]).then( bootEditor );
			} else {
				$( elem ).data('_editorInit', function( callback ) {
					$( elem ).find('.ipsComposeArea_dummy').html( ips.templates.render('core.editor.initLoading') );
					ips.loader.get([fileToLoad]).then( function(){
						bootEditor( callback );
					} );
				});
				$( elem ).find('.ipsComposeArea_dummy').show().on('focus', function(){
					$( elem ).data('_editorInit')( function( instance ){
						instance.unminimize( function() {
								/* If this is guest posting, there'll be an email (for post-before-register) or text (for name) field, so focus that */
								var inputs = elem.closest('.ipsComposeArea').find('input[type="text"], input[type="email"]');
								if ( inputs.length ) {
									inputs[0].focus();
								}
								/* Otherwise focus the editor itself */
								else {
									instance.focus();
								}
							});
						});
				} ).end().find('[data-role="mainEditorArea"]').hide().end().closest('.ipsComposeArea').addClass('ipsComposeArea_minimized').find('[data-ipsEditor-toolList]').hide();
			}

			/**
			 * Wrapper function that ensures we don't try and boot ckeditor until the library is ready
			 *
			 * @returns {void}
			 */
			function bootEditor ( callback ) {
				if( ( !CKEDITOR || _.isUndefined( CKEDITOR.on ) ) && loadTries < 60 ){ // We'll wait 3 seconds for it to init
					loadTries++;
					setTimeout( bootEditor, 50 );
					return;
				}

				if( CKEDITOR.status == 'loaded' ){
					ckLoaded( callback );
				} else {
					CKEDITOR.on( 'loaded', function () {
						ckLoaded( callback );		
					});	
				}
			};

			/**
			 * The function that actually initializes the editor on our widget element
			 *
			 * @returns {void}
			 */
			function ckLoaded ( callback ) {
				if( !$( elem ).data('_editor') ){
					var editor = editorObj( elem, _.defaults( options, defaults ) );
					$( elem ).data('_editor', editor );
					editor.init( callback );
				}
			};
		},

		/**
		 * Destruct the editor
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );
			
			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		},

		/**
		 * Retrieve the editor instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The editor instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_editor') ){
				return $( elem ).data('_editor');
			}
			
			return undefined;
		},
		
		/**
		 * Retrieve the editor instance (if any) on the given element, initiating it if it isn't already
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void} 
		 */
		getObjWithInit = function ( elem, callback ) {
			var obj = this.getObj( elem );
			if ( obj ) {
				callback( obj );
			} else {
				var initFunction = $( elem ).data('_editorInit');
				if ( initFunction ) {
					initFunction( callback );
				}
			}
		};

		ips.ui.registerWidget('editor', ips.ui.editor, 
			[ 'allbuttons', 'postKey', 'toolbars', 'extraPlugins', 'autoGrow', 'contentsCss', 'minimized', 'minimizeAfterReset', 'autoSaveKey', 'skin', 'name', 'pasteBehaviour', 'autoEmbed', 'controller', 'defaultIfNoAutoSave', 'ipsPlugins' ]
		);
		
		return {
			respond: respond,
			getObj: getObj,
			getObjWithInit: getObjWithInit,
			destruct: destruct
		};
	});

	/**
	 * Editor instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var editorObj = function (elem, options) {
		
		var changePolled = false;
		var instance = null;
		var hiddenAtStart = false;
		var minimized = options.minimized;
		var hiddenInterval = null;
		var size = 'phone';
		var name = '';
		var previewIframe = null;
		var currentPreviewView = '';
		var previewInitialHeight = 0;
		var previewSizes = {
			phone: 375,
			tablet: 780
		};
						
		/**
		 * Initializes ckeditor
		 *
		 * @returns void
		 */
		var init = function (callback) {
			
			// CKEditor automatically removes inline tags which do not have any content (which is sensible) but for some weird reason,
			// Google Adsense uses <ins> (which is supposed to be for indicating text that has been inserted into a document) to pass
			// variables to itself. CKEditor sees this empty tag as meaningless (which, in an actual document it would be) and removes
			// it. See https://dev.ckeditor.com/ticket/12397 for the (closed without fix) bug report
			// To work around this, this line just removes <ins> from the list of tags CKEditor will remove if empty.
			delete CKEDITOR.dtd.$removeEmpty['ins'];

			// Similarly many people use <i> tags to insert font-awesome icons, but an empty <i> tag will get stripped. Let's allow those too.
			delete CKEDITOR.dtd.$removeEmpty['i'];
						
			// Build config
			var config = {
				// We totally bypass CKEditor's ACF because custom plugins can add anything. HTMLPurifier will later remove anything we don't want
				allowedContent: true,
				// LTR or RTL
				contentsLangDirection: $('html').attr('dir'),
				// Don't disable the browser's native spellchecker
				disableNativeSpellChecker: false,
				// Adds IPS-created plugins
				extraPlugins: options.ipsPlugins,
				// Autosave key
				ipsAutoSaveKey: options.autoSaveKey,
				ipsDefaultIfNoAutoSave: options.defaultIfNoAutoSave,
				// Behaviour for pasting
				ipsPasteBehaviour: options.pasteBehaviour,
				// Auto emebed?
				ipsAutoEmbed: options.autoEmbed,
				// The default configuration removes underline and other buttons, but we want to control that ourselves
				removeButtons: '',
				// The skin
				skin: options.skin,
				// Autogrow
				height: 'auto',
				// Title for tooltip
				title: window.navigator.platform == 'MacIntel' ? ips.getString('editorRightClickMac') : ips.getString('editorRightClick'),
				// controller
				controller: options.controller
			};
			
			/* Paste behaviour. If we are forcing paste as plaintext, set that... */
			if ( options.pasteBehaviour == 'force' ) {
				config.pasteFilter = 'plain-text';
			}
			/* Otherwise it's a bit complicated... */
			else {
				/* If this is a Webkit browser, pasted data contains lots of inline styles which, if we allow to be pasted, can make the content unable to be formatted
					(this is documented http://docs.ckeditor.com/#!/guide/dev_drop_paste and reported in IPS bug tracker #13006) so we need to filter it. CKEditor does
					have a special option ("semantic-content") for this, which is their default on Webkit, but this excludes colors and other styles users might reasonably use,
					so what we're doing here is emulating CKEditor's semantic-content filter, but also allowing some basic styles */
				if ( CKEDITOR.env.webkit ) {
					var tags = [];
					for ( var tag in CKEDITOR.dtd ) {
						if ( tag.charAt( 0 ) != '$' ) {
							tags.push(tag);
						}
					}
					config.pasteFilter = tags.join(' ') + '[*]{background-color,border*,color,padding,text-align,vertical-align,font-size}';
				}
				/* On other browsers we can trust them to paste sensible data */
				else {
					config.pasteFilter = null;
				}
			}
			

			// http://dev.ckeditor.com/ticket/13713
			if( !/iPad|iPhone|iPod/.test( navigator.platform ) ){
				config.removePlugins = 'elementspath';
			}
			
			if ( ips.getSetting('ipb_url_filter_option') == 'none' && ips.getSetting('url_filter_any_action') == 'moderate' && ips.getSetting('bypass_profanity') == 0 ) {
				config.removePlugins = 'ipslink';
			}

			name = $( elem ).find('textarea').attr('name');

			// Let the documnt know whether we can actually use the editor
			$( elem ).trigger('editorCompatibility', {
				compatible: CKEDITOR.env.isCompatible
			});

			if( options.minimized && minimized ){
				$( elem )
					.find('.ipsComposeArea_dummy')
						.show()
						.on('focus click', function(e) {
							$( this ).off('focus click'); // Ensure these events only fire once

							unminimize( function() {
								focus();
							});			
						})
					.end()
					.find('[data-role="mainEditorArea"]')
						.hide()
					.end()
					.closest('.ipsComposeArea')
						.addClass('ipsComposeArea_minimized')
						.find('[data-ipsEditor-toolList]')
							.hide();

				// Let other controllers initialize us
				$( document ).on( 'initializeEditor', _initializeEditor );

				minimized = true;
			}

			// If we aren't visible, we'll need to reinit when we show so that
			// we can get the correct width to show the buttons
			if( !elem.is(':visible') ){
				hiddenAtStart = true;

				// If it's minimized, we don't need to do anything - we'll check the size
				// again when we unminimize. When we're already full size, we need to run
				// an interval to check the visibility.
				if( !options.minimized && !minimized ){
					clearInterval( hiddenInterval );

					hiddenInterval = setInterval( function () {
						if( elem.is(':visible') ){
							clearInterval( hiddenInterval );
							resize(false);
							hiddenAtStart = false;
						}
					}, 400);
				}
			}
			
			// Language
			var language = $('html').attr('lang').toLowerCase();
			if ( !CKEDITOR.lang.languages[language] ) {
				var language = language.substr( 0, 2 );
				if ( CKEDITOR.lang.languages[language] ) {
					config.language = language;
				}
			} else {
				config.language = language;
			}
			
			// Toolbars
			if( !options.allbuttons ){
				var toolbars = $.parseJSON( options.toolbars );
				var width = elem.width();
				if ( width > 700 ) {
					size = 'desktop';
				} else if ( width > 400 ) { 
					size = 'tablet';
				}
				
				config.toolbar = toolbars[ size ];
			} else {
				config.removePlugins = 'sourcearea';
			}
			
			// Extra plugins
			if( options.extraPlugins !== true ){
				config.extraPlugins += ',' + options.extraPlugins;
			}
			
			if ( ips.getSetting('cloud2') ) {
				var addPlugins, i;
				if( options.extraPlugins !== true ){
					addPlugins = options.extraPlugins.split(',');
					
					for( i = 0; i < addPlugins.length; i++ ) {
						CKEDITOR.plugins.addExternal( addPlugins[i], '/ckeditor_custom/' + addPlugins[i] + '/' );
					}
				}
				
				/* These are the default we ship with, so will always be in interface/ckeditor */
				if( options.skin !== 'ips' && options.skin !== 'moono' ) {
					config.skin = options.skin + ',/ckeditor_custom/skin_' + options.skin + '/';
				}
			}
			
			// Actually initiate
			// 01/05/16 - Changed to replacing a dom node instead of form field name here
			// because in some places we use the same field name multiple times on the page
			// e.g. editing posts in a topic. Using a string name broke the second editor.
			instance = CKEDITOR.replace( $( elem ).find('textarea').get(0), config );
			
			instance.once('instanceReady', function(){
				// Disable grammarly as it confuses CKEditor DOM- test re-enable 4.7.7
				//$( instance.container.$ ).find('div.cke_wysiwyg_div').attr( 'data-gramm', 'false' );
		
				elem.trigger( 'editorWidgetInitialized', { id: name } );
				
				// Saved editor content might have lazy load attributes.
				// We won't bother observing editors, just swap out the content now
				ips.utils.lazyLoad.loadContent( elem );

				// Any other callback?
				if( _.isFunction( callback ) ){
					callback( this );
				}
			}.bind(this));

			// Focus event handling
			let focusTimeout;

			instance.on('focus', function () {
				_checkFocusState();
				focusTimeout = setInterval( _checkFocusState, 2000 );
			});

			instance.on('blur', function () {
				_triggerBlurEvent();
				clearInterval(focusTimeout);
			}.bind(this));

			const _checkFocusState = () => {
				if( !instance.focusManager.hasFocus ){
					clearInterval(focusTimeout);
					return;
				}

				_triggerFocusEvent()
			};

			const _triggerFocusEvent = () => elem.trigger('editor.focused', { elem });
			const _triggerBlurEvent = () => elem.trigger('editor.blurred', { elem });

			// Set listener to apply ratio to images and replace any lazy loaded 
			// content after insertHtml is called directly on the CKEditor (not to 
			// be confused with ips.ui.editor.insertHtml)
			instance.on('afterInsertHtml', function (e) {
				$( instance.container.$ ).find('img:not([data-ratio])').each( function () {
					ips.utils.lazyLoad.applyLazyLoadAttributes( this );
				});

				ips.utils.lazyLoad.loadContent( instance.container.$ );
			});

			// Resize the editor as the element resizes
			if( !options.allbuttons ){
				$( window ).on( 'resize', resize );
			}
			
			// When we delete a file from the uploader, we need to remove it from the editor
			$( document ).on( 'fileDeleted', _deleteFile );
			
			// And listen for emoticon inserts
			$( document ).on( 'insertEmoji', _insertEmoji );

			// Editor preview
			$( elem ).on( 'togglePreview', _togglePreview );
			$( window ).on( 'message', _previewMessage );
			
			// Have a jolly good clear out of old editor saves
			_cleanUpStaleAutoSaves();
		};
		
		/**
		 * Remove old auto saves if they've been there for more than 3 days
		 *
		 * @returns void
		 */
		var _cleanUpStaleAutoSaves = function() {
			var keys = ips.utils.db.getByType('editorSave');
			
			$.each( keys, function(i)
			{
				try{
					// Older than 3 days, remove.
					if ( this[1] < Math.round( new Date().getTime() / 1000 ) - ( 86400 * 3 ) ) {
						ips.utils.db.remove( 'editorSave', i );
					}
				} catch( err ) {
					Debug.error("Trying to remove editorSave keys:");
					Debug.error( err );
				}
			} );
		};
		
		/**
		 * Destructs this object
		 *
		 * @returns void
		 */
		var destruct = function () {
			// Tell editor we are resetting
			instance.fire( 'resetOrDestroy' );

			try {
				if( instance.status == 'ready' ) {
					instance.destroy();
				} else {
					instance.on( 'instanceReady', function(){ instance.destroy(); } );
				}

				Debug.log("Destroyed editor instance");
			} catch (err) { 
				Debug.error("Editor destruct error:");
				Debug.error( err );

				// Turns out ckInstance.destroy() is not reliable and CKE doesn't clean itself up properly.
				// Manually removing listeners and then using CKEDITOR.remove is more reliable when dynamically creating/destroying instances.
				// See http://stackoverflow.com/questions/19328548/ckeditor-uncaught-typeerror-cannot-call-method-unselectable-of-null-in-emberj
				instance.removeAllListeners();
				CKEDITOR.remove( instance );
			}

			_offEvents();
		};

		/**
		 * Returns this instance of CKEditor
		 *
		 * @returns CKEDITOR.editor
		 */
		var getInstance = function () {
			if( instance ){
				return instance;
			}

			return null;
		};

		/**
		 * Stop listening to events for this editor
		 *
		 * @returns void
		 */
		var _offEvents = function () {
			$( window ).off( 'resize', resize );
			$( document ).off( 'fileDeleted', _deleteFile );
			$( document ).off( 'initializeEditor', _initializeEditor );
			$( document ).off( 'insertEmoji', _insertEmoji );
			$( elem ).off( 'togglePreview', _togglePreview );
			$( window ).off( 'message', _previewMessage );
		};

		/**
		 * Handles window resizes
		 *
		 * @returns void
		 */
		var resize = function (focus) {
			var width = elem.width();
			var newSize = 'phone';

			if ( width > 700 ) {
				newSize = 'desktop';
			} else if ( width > 400 ) { 
				newSize = 'tablet';
			}
			
			if ( newSize != size ) {
				size = newSize;
				instance.destroy();
				_offEvents(); // Stop listening to all events that init is about to set up again

				init( function () {
					if( focus ){
						instance.focus();
					}	
				});
			}
		};

		/**
		 * Focus
		 *
		 * @returns void
		 */
		var focus = function () {
			instance.focus();
		};
		
		/**
		 * Unminimize
		 *
		 * @param	{callback}	callback	Function to run after unminimized
		 * @returns void
		 */
		var unminimize = function ( callback ) {
			if( !_.isFunction(callback) ){
				callback = $.noop;
			}

			if( minimized ){
				var _unminimize = function () {
					// Hide the dummy area and show the actual editor
					$( elem )
						.find('.ipsComposeArea_dummy')
							.hide()
						.end()
						.find('[data-role="mainEditorArea"]')
							.show()
						.end()
						.closest('.ipsComposeArea')
							.removeClass('ipsComposeArea_minimized')
							.find('[data-ipsEditor-toolList]')
								.show();
					
					// Focus it. If it isn't ready yet, wait until it is
					if ( instance.status == 'ready' ) {
						minimized = false;
						callback();

						if( hiddenAtStart ){
							resize(true);
							hiddenAtStart = false;
						}
						
						instance.on( 'change', function(e) {
							if ( ! changePolled && _.isUndefined( ips.getSetting('isAcp') ) ) {
								changePolled = true;
								ips.getAjax()( elem.parentsUntil( '', 'form' ).attr('action'), { 
									'data': { 
										'usingEditor': 1
									} 
								} );
							}
						} );
					} else {
						instance.once( 'instanceReady', function(){
							minimized = false;
							callback();

							if( hiddenAtStart ){
								resize(true);
								hiddenAtStart = false;
							}
						});
					}
					
					// Load the upload area
					var minimizedUploader = $(elem).find('[data-ipsEditor-toolListMinimized]');
					if ( minimizedUploader.length ) {
						minimizedUploader.show();
						ips.getAjax()( elem.parentsUntil( '', 'form' ).attr('action'), { 
							'data': { 
								'getUploader': minimizedUploader.attr('data-name')
							} 
						})
							.done( function (response) {
								minimizedUploader.replaceWith( response );
								elem.trigger( 'uploaderReady', {} );
								$( document ).trigger( 'contentChange', [ elem ] );
							});
					}
				};

				// Some browsers will see a click on the editor toolbar after unminimizing.
				// Initially to fix this, we had a timeout on unminmizing the editor, but this then
				// meant iOS would not focus it (because that must happen as a response to a user action)
				// So, to solve both, the timeout is now placed here, and it works by setting the editor to 
				// readonly for 200ms when unminimizing when the user isn't using iOS
				if( !/iPad|iPhone|iPod/.test( navigator.platform ) ){
					//instance.setReadOnly( true );
					setTimeout( function () {
						_unminimize();
					}, 200);
				} else {
					_unminimize();
				}				
			} else {
				callback();
			}
		};

		/**
		 * Minimize
		 *
		 * @returns void
		 */
		var minimize = function () {
			if( !minimized ){
				$( elem )
					.find('.ipsComposeArea_dummy')
						.show()
					.end()
					.find('[data-role="mainEditorArea"]')
						.hide()
					.end()
					.closest('.ipsComposeArea')
						.addClass('ipsComposeArea_minimized')
						.find('[data-ipsEditor-toolList]')
							.hide();

				minimized = true;
			}
		};
		
		/**
		 * Insert quotes into editor
		 *
		 * @param 		{array} 	quotes 	Array of data objects (which should contain all of the properties necessary for a quote)
		 * @returns void
		 */
		var insertQuotes = function ( quotes ) {

			// Wrapper method for inserting quotes into the editor
			var _doInsert = function () {
				/* Now insert the posts */
				for ( var i = 0; i < quotes.length; i++ ) {
					var data = quotes[i];

					// Remove any lightboxes on the content (they'll be reapplied when viewing the quote)
					var regex = /data-ipsLightbox(-group)?="([\w]+)?"/ig;
					var html = data.quoteHtml.replace(regex, '');

					/* Build quote */
					var quote = $( ips.templates.render( 'core.editor.quote', { citation: ips.utils.getCitation( data ), contents: html } ) );
					var attrs = [ 'timestamp', 'userid', 'username', 'contentapp', 'contenttype', 'contentclass', 'contentid', 'contentcommentid' ];
					var j = 0;
					for ( j in attrs ) {
						if ( data[ attrs[j] ] ) {
							quote.attr( 'data-ipsQuote-' + attrs[j], data[ attrs[j] ] );
						}
					}
					
					/* Insert it */
					var element = CKEDITOR.dom.element.createFromHtml( $('<div>').append( quote ).html() );
					instance.setReadOnly( false );
					instance.insertElement( element );
					instance.widgets.initOn( element, 'ipsquote' );
					
					/* Insert a blank paragraph between multiple quotes */
					if ( i + 1 < quotes.length ) {
						var blankParagraph = new CKEDITOR.dom.element('p');
						( new CKEDITOR.dom.element( 'br' ) ).appendTo( blankParagraph );
						instance.insertElement( blankParagraph );
					}
				}
			};

			// If we are minimized, we will unminimize, then empty the editor contents, and then insert the quotes
			// If we aren't minimized, keep the existing content.
			if( minimized ){
				unminimize( function () {
					try {
						instance.setData('');
						elem.find('[data-role="autoSaveRestoreMessage"]').hide();
					} catch (err) {}
					
					_doInsert();
				});	
			} else {
				/* If we're up against another widget, insert a blank paragraph between them */
				var ranges = instance.getSelection().getRanges();
				for ( var i = 0; i < ranges.length; i ++ ) {
					var previousNode = ranges[i].getCommonAncestor( true, true ).getPrevious();
					if ( previousNode && previousNode.hasClass('cke_widget_wrapper') ) {
						var blankParagraph = new CKEDITOR.dom.element('p');
						( new CKEDITOR.dom.element( 'br' ) ).appendTo( blankParagraph );
						instance.insertElement( blankParagraph );
					}
				}

				_doInsert();
			}
		};
		
		/**
		 * Update a selected image
		 *
		 * @param	{number}		width	Width in pixels
		 * @param	{number}		height	Height in pixels
		 * @param	{string}		align	'left', 'right' or ''
		 * @param	{string}		url		URL to link to
		 * @param	{string}		alt		Image Alt Tag
		 * @returns	{void}
		 */
		var updateImage = function ( width, height, align, url, alt ) {
			var selection = instance.getSelection();
			var selectedElement = $( selection.getSelectedElement().$ );
			
			if ( url ) {
				if ( !url.match( /^[a-z]+\:\/\//i ) ) {
					url = 'http://' + url;
				}
				
				if ( selectedElement.parent().prop('tagName') === 'A' ) {
					selectedElement.parent().attr( 'href', url ).removeAttr('data-cke-saved-href');
				} else {
					selectedElement.wrap( $('<a>').attr( 'href', url ) );
				}
			} else {
				if ( selectedElement.parent().prop('tagName') === 'A' ) {
					selectedElement.parent().replaceWith( selectedElement );
				}
			}
															
			selectedElement.css({
				"width": width,
				"height": height
			});
			
			var alignClasses = 'ipsAttachLink_left ipsAttachLink_right';

			if ( align ) {
				if ( selectedElement.parent().prop('tagName') === 'A' ) {
					selectedElement.parent().css( 'float', align ).removeClass( alignClasses ).addClass('ipsAttachLink ipsAttachLink_' + align);
				} else {
					selectedElement.css( 'float', align ).removeClass( alignClasses ).addClass('ipsAttachLink_image ipsAttachLink_' + align);
				}
			} else {
				selectedElement.css( 'float', '' ).removeClass( alignClasses );
				if ( selectedElement.parent().prop('tagName') === 'A' ) {
					selectedElement.parent().css( 'float', '' ).removeClass( alignClasses );
				}
			}

			if ( alt ) {
				selectedElement.attr( 'alt', alt );
			} else {
				selectedElement.removeAttr( 'alt' );
			}

			// Apply our lazy load attributes
			ips.utils.lazyLoad.applyLazyLoadAttributes( selectedElement.get(0), {
				width: width,
				height: height
			}, true);
		};

		/**
		 * Check if the editor content has been changed from the default
		 *
		 * @returns	bool
		 */
		 var checkDirty = function() {
		 	return instance.checkDirty();
		 };

		/**
		 * Resets whether the editor content has been changed or not
		 *
		 * @returns	void
		 */
		 var resetDirty = function() {
		 	return instance.resetDirty();
		 };
		
		/**
		 * Insert arbitrary HTML into editor
		 *
		 * @param 		{string} 	html 	HTML to insert
		 * @returns void
		 */
		var insertHtml = function ( html ) {
			instance.insertHtml( html );
		};
		
		/**
		 * Reset the editor
		 *
		 * @param 		{string} 	html 	HTML to insert
		 * @returns void
		 */
		var reset = function () {
			// Tell editor we are resetting
			instance.fire( 'resetOrDestroy' );
				
			instance.setData('<p></p>');
			_closePreview();
			elem.find('[data-ipsUploader]').trigger('resetUploader');

			if( options.minimized && options.minimizeAfterReset ){
				minimize();
			}
		};
		
		/**
		 * Save and clear autosave
		 *
		 * @returns void
		 */
		var saveAndClearAutosave = function () {
			instance.updateElement();
			ips.utils.db.remove( 'editorSave', options.autoSaveKey );
		};

		/**
		 * Determines whether the provided editor ID matches this widget
		 *
		 * @param 		{object} 	Event data; requires editorID key which is the editor name to check
		 * @returns 	boolean
		 */
		var _belongsToThisEditor = function (data) {
			if( _.isUndefined( data.editorID ) || data.editorID !== name ){		
				return false;
			}

			return true; 
		};

		/**
		 * Allows other JS to initialize the editor
		 *
		 * @returns void
		 */
		var _initializeEditor = function (e, data) {
			if( !_belongsToThisEditor( data ) ){
				return;
			}

			unminimize(	function () {
				_scrollToEditor();
				focus();
			});
		};

		/**
		 * Remove a file from the editor
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data 	Data object from the event
		 * @returns {boolean}
		 */
		var _deleteFile = function(e, data){
			var document = elem.find('.cke_contents');
			var links = document.find('a');

			var images = document.find('img,video,audio');
			var toRemove = [];

			// Push items to remove into a new array because otherwise javascript removes all except one of the same attached image
			$.each( images, function () {
				var image = $( this );
				if( image.attr('data-fileid') == data.fileElem.attr('data-fileid')  || image.children('a').attr('data-fileid') == data.fileElem.attr('data-fileid') ){
					toRemove.push( image );
				}
			});

			$.each( links, function () {
				var link = $( this );
				if( link.attr('data-fileid') == data.fileElem.attr('data-fileid') || link.attr('href') == ips.getSetting('baseURL') + 'applications/core/interface/file/attachment.php?id=' + data.fileElem.attr('data-fileid') ){
					link.remove();
				}
			});


			
			for( var i = 0 ; i < toRemove.length; i++ ) {
				toRemove[i].remove();
			}
		};

		/**
		 * Scrolls the page to the editor
		 *
		 * @returns {boolean}
		 */
		var _scrollToEditor = function () {
			var elemPosition = ips.utils.position.getElemPosition( elem );

			// Is it on the page?
			var windowScroll = $( window ).scrollTop();
			var viewHeight = $( window ).height();

			// Only scroll if it isn't already on the screen
			if( elemPosition.absPos.top < windowScroll || elemPosition.absPos.top > ( windowScroll + viewHeight ) ){
				$('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' } );	
			}
		};
		
		/**
		 * Responds to an insertEmoji event
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data 	Data object from the event
		 * @returns {boolean}
		 */
		var _insertEmoji = function (e, data) {
			try {
				if( _belongsToThisEditor( data ) ){
					
					/* Get element */
					var element = ips.utils.emoji.editorElement( data.emoji );
					
					/* Check it won't exceed our 75 limit */
					if ( element.getName() == 'img' && $('<div>' + instance.getData() + '</div>' ).find('img[data-emoticon]').length >= 75 ) {
						var emoMessage = $(elem).closest('[data-ipsEditor]').find('[data-role="emoticonMessage"]');
						emoMessage.slideDown();
						
						/* Function to handle cancels */
						var hideEmoMessage = function(){
							emoMessage.slideUp();
						};
								
						/* After 2.5 seconds, more typing will remove */
						setTimeout(function(){
							instance.once( 'key', function() {
								hideEmoMessage();
							});
							instance.once( 'setData', function() {
								hideEmoMessage();
							});
						}, 2500);
						
						return;
					}
					
					/* Insert */
					instance.setReadOnly( false );
					instance.insertElement( element );
					if ( element.getName() == 'span' && element.hasClass( 'ipsEmoji' ) ) {
						instance.widgets.initOn( element, 'ipsemoji' );
					}
					
					/* Add to recently used */
					ips.utils.emoji.logUse( data.emoji );
				}
			} catch (err) {
				Debug.error("CKEditor instance couldn't be fetched");
				return;
			}	
		};
		
		//============================================================================================================
		// EDITOR PREVIEW FUNCTIONALITY
		//============================================================================================================

		/**
		 * Toggles the preview mode of the editor instance
		 *
		 * @returns void
		 */
		var _togglePreview = function () {
			if( elem.find('[data-role="previewFrame"]').length ){
				_showPreview();
			} else {
				_buildAndShowPreview();
			}
		};

		/**
		 * Hides the editor and shows the preview
		 *
		 * @returns void
		 */
		var _showPreview = function () {
			// Show preview
			var currentHeight = elem.height();

			elem.find('[data-role="editorComposer"]').hide();
			elem.find('[data-role="editorPreview"]').show();

			var toolbarHeight = elem.find('[data-role="previewToolbar"]').height();

			elem.find('[data-role="previewFrame"]').css({ height: ( currentHeight - toolbarHeight ) + 'px' });

			// Fetch a new preview
			_fetchPreview();
		};

		/**
		 * Builds the preview frame and sets up initial handling
		 *
		 * @returns void
		 */
		var _buildAndShowPreview = function () {
			// Create an iframe that we'll insert into the preview area. Set the height to the current height of the editor.
			var currentHeight = elem.height();
			var iframe = $('<iframe />')
					.addClass('ipsAreaBackground_reset')
					.css({ border: "0", width: '100%' })
					.prop('seamless', true)
					.attr('src', ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=editor&do=preview&editor_id=' + name )
					.attr('data-role', 'previewFrame');

			// Reset the toolbar
			currentPreviewView = ips.utils.responsive.getCurrentKey();
			_showPreviewButtons( currentPreviewView );

			// Watch for event
			elem.on( 'click', 'a[data-action="closePreview"]', _closePreview );
			elem.on( 'click', '[data-action="resizePreview"] a', _resizePreview );

			// Show preview
			elem.find('[data-role="editorComposer"]').hide();
			elem.find('[data-role="editorPreview"]').show();

			// Subtract the height of toolbar so that the overall height stays the same
			var toolbarHeight = elem.find('[data-role="previewToolbar"]').height();
			previewInitialHeight = currentHeight - toolbarHeight;

			elem.find('[data-role="previewContainer"]').append( iframe.css({ height: previewInitialHeight + 'px' }) );

			// Get the reference we'll need to the iframe
			previewIframe = iframe.get(0).contentWindow;
		};

		/**
		 * Show and toggle the appropriate view buttons
		 *
		 * @param 	{string} 	currentView 		The current view key (phone, tablet, desktop)
		 * @returns void
		 */
		var _showPreviewButtons = function (currentView) {
			var toolbar = elem.find('[data-role="previewToolbar"]');

			// Shortcut - if we're on mobile, hide all the buttons
			if( ips.utils.responsive.getCurrentKey() == 'phone' || size == 'phone' ){
				toolbar.find('[data-size]').hide();
				return;
			}

			// Set active button
			toolbar
				.find('[data-size]')
					.show()
					.filter('[data-size="' + currentView + '"]')
						.find('a')
							.removeClass('ipsButton_light')
							.addClass('ipsButton_primary');

			// If we're on tablet, we can't switch to desktop
			if( ips.utils.responsive.getCurrentKey() == 'tablet' || size == 'tablet' ){
				toolbar.find('[data-size="desktop"]').hide();
			}
		};

		/**
		 * Resizes the preview frame
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns void
		 */
		var _resizePreview = function (e) {
			e.preventDefault();
			var newKey = $( e.target ).closest('[data-size]').attr('data-size');

			if( newKey == currentPreviewView ){
				return;
			}

			// Highlight
			var toolbar = elem.find('[data-role="previewToolbar"]');

			toolbar.find('[data-size] a').removeClass('ipsButton_primary').addClass('ipsButton_light');
			toolbar.find('[data-size="' + newKey + '"] a').addClass('ipsButton_primary').removeClass('ipsButton_light');

			currentPreviewView = newKey;

			// Reset the height
			// The iframe will send us its new height every 150ms
			elem
				.find('[data-role="previewFrame"]')
				.css({
					height: previewInitialHeight + 'px'
				});

			// If the new size is our actual size, we don't want any spacing
			if( newKey == size ){
				elem.find('[data-role="previewFrame"]')
					.removeClass('ipsComposeArea_smallPreview')
					.css({ 
						margin: '0px',
						maxWidth: '100%',
						width: '100%'
					});
			} else {
				elem.find('[data-role="previewFrame"]')
					.addClass('ipsComposeArea_smallPreview')
					.css({
						marginTop: '10px',
						marginBottom: '10px',
						maxWidth: previewSizes[ newKey ] + 'px',
						width: '100%'
					});
			}
		};

		/**
		 * Handles a message posted to us by the iframe controller
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object}	data 	Data object
		 * @returns void
		 */
		var _previewMessage = function (e, data) {
			var oE = e.originalEvent;

			// Security: check our origin is what we expect so that third-party frames can't tamper
			// Check the source is what we expect so we don't handle messages not meant for us
			if( oE.origin !== ips.utils.url.getOrigin() || oE.source !== previewIframe ){
				return;
			}

			try {
				var json = $.parseJSON( oE.data );
			} catch (err) {
				Debug.err("Error parsing JSON from preview frame");
				return;
			}

			// Ignore any messages not for this editor
			if( _.isUndefined( json.editorID ) || json.editorID !== name || _.isUndefined( json.message ) ){
				return;
			}

			switch( json.message ){
				case 'iframeReady':
					// Send our data to the iframe
					_fetchPreview();
				break;
				case 'previewHeight':
					_setPreviewHeight( json );
				break;
			}
		};

		/**
		 * Instructs iframe to fetch the preview
		 *
		 * @returns void
		 */
		var _fetchPreview = function () {
			_sendMessage({
				message: 'fetchPreview',
				editorContent: instance.getData(),
				url: elem.closest('form').attr('action')
			});
		};

		/**
		 * Instructs iframe to fetch the preview
		 *
		 * @returns void
		 */
		var _closePreview = function (e) {
			if( e ){
				e.preventDefault();
			}

			// Hide preview
			elem.find('[data-role="editorPreview"]').hide();
			elem.find('[data-role="editorComposer"]').show();

			_sendMessage({
				message: 'previewClosed'
			});	
		};

		/**
		 * Sets the height of the iframe preview window
		 *
		 * @param 	{object} 	data 	Data from iframe's postMessage
		 * @returns void
		 */
		var _setPreviewHeight = function (data) {
			if( data.height > previewInitialHeight ){
				elem
					.find('[data-role="previewFrame"]')
					.css({
						height: data.height + 'px'
					});	
			}			
		};

		/**
		 * Send a message to the preview iframe
		 *
		 * @param 	{object}	data 	Data object to serialize and send
		 * @returns void
		 */
		var _sendMessage = function (data) {
			Debug.log('Sending message FROM parent');
			if( previewIframe !== null ){
				previewIframe.postMessage( JSON.stringify( data ), ips.utils.url.getOrigin() );
			}
		};		
		
		return {
			init: init,
			focus: focus,
			unminimize: unminimize,
			minimize: minimize,
			insertQuotes: insertQuotes,
			insertHtml: insertHtml,
			checkDirty: checkDirty,
			resetDirty: resetDirty,
			updateImage: updateImage,
			reset: reset,
			destruct: destruct,
			saveAndClearAutosave: saveAndClearAutosave,
			getInstance: getInstance
		};
		
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.filterBar.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350">/* global ips, _ */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.filterBar.js - Filter bar widget
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.createModule('ips.ui.filterBar', function(){

		var defaults = {
			on: 'phone,tablet',
			viewDefault: 'filterContent'
		};

		/**
		 * Respond to a menu trigger being clicked
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{event} 	e 		 	The event object
		 * @returns {void}
		 */
		var respond = function (elem, options) {
			if( !$( elem ).data('_filterBar') ){
				$( elem ).data('_filterBar', filterBarObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Retrieve the filterBar instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The filterBar instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_filterBar') ){
				return $( elem ).data('_filterBar');
			}

			return undefined;
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		};

		ips.ui.registerWidget( 'filterBar', ips.ui.filterBar, [ 'on', 'viewDefault' ] );

		return {
			respond: respond,
			destruct: destruct,
			getObj: getObj
		};
	});

	/**
	 * Filter bar instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var filterBarObj = function (elem, options) {

		var filterBar = null;
		var filterContent = null;
		var workOn = [];
		var currentBreak;
		var currentlyShowing = null;

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			if( !ips.utils.responsive.enabled() ){
				return;
			}

			filterBar = elem.find('[data-role=&quot;filterBar&quot;]');
			filterContent = elem.find('[data-role=&quot;filterContent&quot;]');
			workOn = options.on.split(',');
			currentBreak = ips.utils.responsive.getCurrentKey();

			// Document events
			$( document ).on( 'breakpointChange', _breakpointChange );

			// Widget events
			elem
				.on( 'switchTo.filterBar', function (e, data) {
					// Make sure we're in a breakpoint we're working with
					if( _.indexOf( workOn, ips.utils.responsive.getCurrentKey() ) === -1 ){
						return;
					}
					
					_switchView( data.switchTo );
				})
				.on( 'click', '[data-action=&quot;filterBarSwitch&quot;]', _switchToggle );


			if( _.indexOf( workOn, currentBreak ) !== -1 ){
				_setUpBar();
			}					
		},

		/**
		 * Destruct the instance
		 *
		 * @returns {void}
		 */
		destruct = function () {
			$( document ).off( 'breakpointChange', _breakpointChange );
		},

		/**
		 * Sets up the filter bar on widget initialization
		 *
		 * @returns 	{void}
		 */
		_setUpBar = function () {
			if( options.viewDefault == 'filterBar' ){
				filterContent.addClass('ipsHide');
				currentlyShowing = 'filterBar';
			} else {
				filterBar.addClass('ipsHide');
				currentlyShowing = 'filterContent';
			}
		},

		/**
		 * A manual toggle by the user (e.g. clicking a link)
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		_switchToggle = function (e) {
			e.preventDefault();

			// Make sure we're in a breakpoint we're working with
			if( _.indexOf( workOn, ips.utils.responsive.getCurrentKey() ) === -1 ){
				return;
			}

			_switchView( $( e.currentTarget ).attr('data-switchTo') == 'filterBar' ? 'filterBar' : 'filterContent' );
		},

		/**
		 * Toggles the current view from filters to content or vice-versa
		 *
		 * @param 		{string} 	switchTo 	The view to switch to (filterBar or filterContent)
		 * @returns 	{void}
		 */
		_switchView = function (switchTo) {
			if( switchTo == currentlyShowing ){
				return;
			}

			// Set the height of the container
			elem.css({
				height: ( currentlyShowing == 'filterBar' ) ? filterBar.outerHeight() : filterContent.outerHeight() + 'px'
			});

			// Add the class that sets each column to absolute
			filterBar.addClass('ipsFilter_layout');
			filterContent.addClass('ipsFilter_layout');

			// Function to run when we've finished animating
			var done = function () {
				filterBar.removeClass('ipsFilter_layout');
				filterContent.removeClass('ipsFilter_layout');

				elem.css({
					height: 'auto'
				});

				currentlyShowing = switchTo;
			};

			// Set up and animate each column
			if( switchTo == 'filterBar' ){
				filterBar
					.css({ left: '-100%' })
					.removeClass('ipsHide')
					.animate({ left: '0%' }, {
						duration: 300
					});

				filterContent
					.css({ left: '0%' })
					.animate({ left: '100%'	}, {
						duration: 300,
						complete: function () {
							$( this ).addClass('ipsHide')
							done();
						}
					});
			} else {
				filterBar
					.css({ left: '0%' })
					.animate({ left: '-100%' }, {
						duration: 300,
						complete: function () {
							$( this ).addClass('ipsHide')
							done();
						}
					});

				filterContent
					.css({ left: '100%' })
					.removeClass('ipsHide')
					.animate({ left: '0%' }, {
						duration: 300
					});
			}
		},

		/**
		 * Cancels the widget from operating, cleaning up classes and positioning
		 *
		 * @returns 	{void}
		 */
		_cancel = function () {
			elem.find('[data-role=&quot;filterBar&quot;], [data-role=&quot;filterContent&quot;]' )
				.removeClass('ipsFilter_layout')
				.css({
					left: 'auto'
				})
				.removeClass('ipsHide');

			elem.css({
				height: 'auto'
			});

			currentlyShowing = null;
		},

		/**
		 * Event handler for the responsive breakpoint changing
		 *
		 * @returns 	{void}
		 */
		_breakpointChange = function (e, data) {
			currentBreak = data.curBreakName;

			if( _.indexOf( workOn, currentBreak ) !== -1 ){
				_switchView( options.viewDefault );
			} else {
				_cancel();
			}
		};

		init();

		return {
			init: init,
			destruct: destruct
		};
	};
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.flashMsg.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/* global ips, _ */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.flashMsg.js - Flash message widget
 * Creates a flash message - a box used for communicating quick messages to the user such as 'success' text.
 *
 * Although this widget can be initialized on an element with the data api, it will primarily be
 * called programatically:
 *
 * ips.ui.flashMsg.show('text');
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.flashMsg', function(){

		var _queue = [],
			_doneInit = false,
			_box,
			_content,
			_isShowing = false,
			_currentDismissHandler = null;

		var defaults = {
			timeout: 2,
			extraClasses: '',
			location: 'top',
			sticky: false,
			escape: true
		};

		/**
		 * Responder for flash card widget
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{event} 	e 		 	The event object passed through
		 * @returns {void}
		 */
		var respond = function (elem, options) {
			if( options.text ){
				show( options.text, options );
			}
		},

		/**
		 * Check the URL and cookie for any flash card we might need to show
		 *
		 * @returns {void}
		 */
		init = function () {
			$( document ).ready( function () {
				if( $('body').attr('data-message') ){
					show( $('body').attr('data-message') );
				}
								
				if( ips.utils.url.getParam('flmsg') ){
					show( _.escape( decodeURIComponent( ips.utils.url.getParam('flmsg') ) ) );
				}

				if( ips.utils.cookie.get('flmsg') ){
					show( _.escape( ips.utils.cookie.get('flmsg') ) );
					ips.utils.cookie.unset('flmsg');
				}
			});

			$( document ).on( 'closeFlashMsg.flashMsg', hide );
		},

		/**
		 * Shows the flash message
		 *
		 * @param	{string} 	message 	The flash message
		 * @param	{object} 	options 	Options for showing this flash message
		 * @returns {void}
		 */
		show = function (message, options, update) {
			if( !_doneInit ){
				_initElement();
			}

			options = _.defaults( options || {}, defaults );
			
			if ( options.escape ) {
				message = _.escape( message );
			}

			// If there's already a message showing, add to the queue
			if( _isShowing && !update ){
				_queue.push( [ message, options ] );
				return;
			}

			// If we're updating the current flash message and already showing...
			if( update && _isShowing ){
				_content.html( message );
				ips.utils.anim.go( 'pulseOnce', _box );

				if( !options.sticky ){
					setTimeout( hide, options.timeout * 1000 );
				}

				return;
			}

			_currentDismissHandler = null;
			_isShowing = true;
			_content.html( message );

			_box
				.attr( 'class', '' ) // Reset classes
				.addClass( options.extraClasses )
				.addClass( options.dismissable ? 'ipsFlashMsg_dismissable' : '' )
				.addClass( options.position == 'bottom' ? 'ipsFlashMsg_bottom' : 'ipsFlashMsg_top' )
				.on( 'click', 'a:not( [data-action="dismissFlashMessage"] )', function () {
					hide();
				})
				.animationComplete( function () {
					if ( !options.sticky ) {
						setTimeout( hide, options.timeout * 1000 );
					}
				});

			// Any close handlers?
			if( _.isFunction( options.dismissable ) ){
				_currentDismissHandler = options.dismissable;
			}

			ips.utils.anim.go( 'fadeInDown', _box );
		},

		/**
		 * Hides the flash message
		 *
		 * @param	{string} 	message 	The flash message
		 * @param	{object} 	options 	Options for showing this flash message
		 * @returns {void}
		 */
		hide = function () {
			if( _queue.length ){
				var next = _queue.shift();
				show( next[0], next[1], true );
			} else {
				_box
					.animationComplete( function () {
						_isShowing = false;
						_box.hide();

						if( _queue.length ){
							var next = _queue.shift();
							show( next[0], next[1] );
						}
					});

				ips.utils.anim.go('fadeOutDown', _box);
			}
		},

		dismiss = function (e) {
			e.preventDefault();
			hide();

			if( _.isFunction( _currentDismissHandler ) ){
				_currentDismissHandler();
				_currentDismissHandler = null;
			}
		},

		/**
		 * Initialize the element used for the flash message
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{event} 	e 		 	The event object passed through
		 * @returns {void}
		 */
		_initElement = function () {
			// Create element
			$('body').append( ips.templates.render("core.general.flashMsg") );

			// Find the box, then find the content element (which might be the same one)
			_box = $('#elFlashMessage').hide();
			_content = ( _box.is('[data-role="flashMessage"]') ) ? _box : _box.find('[data-role="flashMessage"]');

			// Dismiss event
			_box.on( 'click', 'a[data-action="dismissFlashMessage"]', dismiss );

			_doneInit = true;
		};

		// Register this widget with ips.ui
		ips.ui.registerWidget('flashMsg', ips.ui.flashMsg,
			['text', 'extraClasses', 'timeout', 'position', 'sticky', 'dismissable' ]
		);

		init();

		return {
			respond: respond,
			show: show
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.form.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.forms.js - Form handling in the AdminCP
 * Sets up basic form elements and behaviors used throughout the acp. More complex form controls (e.g.
 * uploading or autocomplete) are handled in their own widgets.
 *
 * Author: Mark Wade & Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.form', function(){

		var _cmInstances = {};
		var _support = {};

		var formTypes = {
			// Toggles to allow 'unlimited' values
			'unlimited': '[data-control~="unlimited"]',
			// Disables elements when certain values in a select are chosen
			'selectDisable': '[data-control~="selectDisable"]',
			// Polyfills dates
			'date': 'input[type="date"], [data-control~="date"]',
			// Makes range inputs a bit nicer
			'range': 'input[type="range"], [data-control~="range"]',
			// Polyfills colors
			'color': 'input[type="color"], [data-control~="color"]',
			// Width/height changers
			'dimensions': '[data-control~="dimensions"]',
			// Width/height unlimited toggles
			'dimensionsUnlimited': '[data-control~="dimensionsUnlimited"]',
			// Disables fields if JS is enabled
			'jsDisable': 'input[data-control~="jsdisable"]',
			// Toggles form rows when another element value changes
			'toggle': '[data-control~="toggle"]',
			// Codemirror
			'codemirror': '[data-control~="codemirror"]',
			// CheckboxSets with Unimited toggles
			'granularCheckboxset': '[data-control~="granularCheckboxset"]',
		};

		/**
		 * Called when module is initialized.
		 * Observes the content change event, and calls our respond method. This allows us to initialize new form controls
		 * that might be added inside the form.
		 *
		 * @returns {void}
		 */
		var init = function () {
			$( document ).on( 'contentChange', function (e, data) {
				if( !_.isUndefined( data ) && $( data[0] ).closest('[data-ipsForm]').length ){
					respond( $( data[0] ) );
				}
			});

			$( document ).on( 'menuOpened', function (e,data) {
				if( data.menu.closest('[data-ipsForm]').length ){
					respond( data.menu );
				}
			});

			/* This listens for codeMirrorInsert which is triggered from ips.editor.customtags.js and then inserts into code mirror instances */
			$( document ).on( 'codeMirrorInsert', function (e, data) {
				if( !_.isUndefined( _cmInstances[ data.elemID ] ) ) {
					_cmInstances[ data.elemID ].replaceRange( data.tag, _cmInstances[ data.elemID ].getCursor( "end" ) );
				}
			});
			
			$( document ).on( 'tabChanged', function (e, data) {
				var form = $( '#' + data['barID'] ).closest('[data-ipsForm]');
				if ( $('input[name=' + form.attr('data-formId' ) + '_activeTab]' ).length ) {
					$('input[name=' + form.attr('data-formId' ) + '_activeTab]' ).val( data['tabID'].replace( form.attr('data-formId' ) + '_tab_', '' ) );
				}
			});
		},

		/**
		 * Respond method
		 * Loops through each form type, finds elements that match, then initializes them
		 *
		 * @returns {void}
		 */
		respond = function (elem, options) {
			var runControlMethod = function (i){				
				_controlMethods[ type ]( $(this), elem ) || $.noop;
			};

			// Loop through each type of control we'll work with
			for( var type in formTypes ){
				$( elem ).find( formTypes[ type ] ).each( runControlMethod );
			}	
			
			/* Sort any select boxes that need it */
			$(elem).find('select[data-sort]').each(function(){
				var value = $(this).val();
				$(this).children('optgroup').each(function(){
					$(this).append( $(this).children('option').remove().sort(localeSort) );
				});
				$(this).append( $(this).children('optgroup').remove().sort(localeSort) );
				$(this).append( $(this).children('option').remove().sort(localeSort) );
				$(this).val( value );
			});
		};
		
		/**
		 * Locale sort
		 */
		var localeSort = function (a, b) {
			if ( $(a).prop("tagName") == 'OPTGROUP' ) {
				var aValue = $(a).attr('label');
			} else {
				if (!a.value) {
			        return -1;   
			    }
				var aValue = a.innerHTML;
			}
			if ( $(b).prop("tagName") == 'OPTGROUP' ) {
				var bValue = $(b).attr('label');
			} else {
				if (!b.value) {
			        return 1;   
			    }
				var bValue = b.innerHTML;
			}
									 
		    try {
		    	return aValue.localeCompare( bValue );
		    } catch ( err ) {
			    return ( aValue > bValue ) ? 1 : -1;
		    }
		};

		// Object containing methods for each control
		var _controlMethods = {
			/**
			 * Handles codemirror fields
			 *
			 * @param	{element}	elem		The textarea element
			 * @returns {void}
			 */
			codemirror: function (elem) {
				ips.loader.get( ['core/interface/codemirror/diff_match_patch.js','core/interface/codemirror/codemirror.js'] ).then( function () {
					var elemId	= $( elem ).attr('id');

					// If there's already an instance here, we need to remove it and reinitialize
					// This happens when, for example, a form is validated in a modal, meaning the same element
					// ID is used again
					if( !_.isUndefined( _cmInstances[ elemId ] ) ) {
						// 10/23/15 we were still losing the contents of codemirror when switching tabs. To fix this, we need to save
						// the contents to the textarea *before* removing the CM instance.
						_cmInstances[ elemId ].save();
						//-----

						$( _cmInstances[ elemId ].getWrapperElement() ).remove();
						delete _cmInstances[ elemId ];
					} 

					_cmInstances[ elemId ]	= CodeMirror.fromTextArea( document.getElementById(elemId), { 
						mode: $(elem).attr('data-mode'),
						lineWrapping: true,
						lineNumbers: false,
						leaveSubmitMethodAlone: true
					} );
					
					if ( $(elem).attr('data-height') ){
						_cmInstances[ elemId ].setSize( null, $(elem).attr('data-height') );					
						$('div[data-codemirrorid=' + elemId + '] ul[data-role=tagsList]').css('max-height', $(elem).attr('data-height') );
					}
					
					$( '#' + elemId ).data('CodeMirrorInstance', _cmInstances[ elemId ] );
					
					// Support custom tags
					$('[data-codemirrorcustomtag]').on( 'click', function( e ){
						_cmInstances[ elemId ].replaceRange( $( e.currentTarget ).attr('data-codemirrorcustomtag'), _cmInstances[ elemId ].getCursor( "end" ) );
					});
				});
			},

			/**
			 * Makes range inputs a little nicer to use
			 *
			 * @param	{element}	elem		The range element
			 * @returns {void}
			 */
			range: function (elem) {
				if( _.isUndefined( _support['range'] ) ){
					var i = document.createElement("input");
					i.setAttribute("type", "range");

					_support['range'] = !( i.type === 'text' );
				}

				if( !_support['range'] ){
					elem.siblings('[data-role="rangeBoundary"]').hide();
				} else {
					var valueElem = $( '#' + elem.attr('name') + '_rangeValue' );
					valueElem.text( elem.val() );
					
					elem.on( 'change', function () {
						valueElem.text( elem.val() );
					});
				}
			},

			/**
			 * Enables functionality for 'unlimited' toggles
			 *
			 * @param	{element}	elem		The checkbox element
			 * @returns {void}
			 */
			unlimited: function (elem) {				
				elem.on( 'change', function () {
					_unlimitedCheck( elem );
				});

				if( !elem.attr( 'data-initialized' ) )
				{
					elem.attr( 'data-initialized', '1' );
					_unlimitedCheck( elem );
				}
			},

			/**
			 * Disables select boxes when the selected option element has a data-disable attribute
			 *
			 * @param	{element}	elem		The select box
			 * @returns {void}
			 */
			selectDisable: function (elem) {
				elem.on( 'change', function () {
					_selectDisable( elem );
				});
				
				_selectDisable( elem );
			},

			/**
			 * Handles date fields by adding a jquery plugin if the browser doesn't natively support type='date'
			 *
			 * @param	{element}	elem		The date form control
			 * @returns {void}
			 */
			date: function (elem) {
				if( _.isUndefined( _support['date'] ) ){
					var i = document.createElement("input");
					i.setAttribute("type", "date");

					_support['date'] = !( i.type === 'text' );
				}

				if( !_support['date'] ){
					if( $(elem).attr('data-preferredFormat') )
					{
						$(elem).val( $(elem).attr('data-preferredFormat') );
					}

					ips.loader.get( ['core/interface/jquery/jquery-ui.js'] ).then( function () {
												
						var _buildDatepicker = function () {
							
							$.datepicker.regional['xx'] = {
								closeText: ips.getString('date_picker_done'), // Display text for close link
								prevText: ips.getString('date_picker_prev'), // Display text for previous month link
								nextText: ips.getString('date_picker_next'), // Display text for next month link
								currentText: ips.getString('date_picker_next'), // Display text for current month link
								monthNames: [ips.getString('month_0'),ips.getString('month_1'),ips.getString('month_2'),ips.getString('month_3'),ips.getString('month_4'),ips.getString('month_5'),ips.getString('month_6'),ips.getString('month_7'),ips.getString('month_8'),ips.getString('month_9'),ips.getString('month_10'),ips.getString('month_11')], // Names of months for drop-down and formatting
								monthNamesShort: [ips.getString('month_0'),ips.getString('month_1'),ips.getString('month_2'),ips.getString('month_3'),ips.getString('month_4'),ips.getString('month_5'),ips.getString('month_6'),ips.getString('month_7'),ips.getString('month_8'),ips.getString('month_9'),ips.getString('month_10'),ips.getString('month_11')], // For formatting
								dayNames: [ips.getString('day_0'),ips.getString('day_1'),ips.getString('day_2'),ips.getString('day_3'),ips.getString('day_4'),ips.getString('day_5'),ips.getString('day_6')], // For formatting
								dayNamesShort: [ips.getString('day_0_short'),ips.getString('day_1_short'),ips.getString('day_2_short'),ips.getString('day_3_short'),ips.getString('day_4_short'),ips.getString('day_5_short'),ips.getString('day_6_short')], // For formatting
								dayNamesMin: [ips.getString('day_0_short'),ips.getString('day_1_short'),ips.getString('day_2_short'),ips.getString('day_3_short'),ips.getString('day_4_short'),ips.getString('day_5_short'),ips.getString('day_6_short')], // Column headings for days starting at Sunday
								weekHeader: ips.getString('date_picker_week'), // Column header for week of the year
								dateFormat: ips.getSetting( 'date_format' ), // See format options on parseDate
								firstDay: ips.getSetting( 'date_first_day' ), // The first day of the week, Sun = 0, Mon = 1, ...
								isRTL: $('html').attr('dir') == 'rtl', // True if right-to-left language, false if left-to-right
								showMonthAfterYear: false, // True if the year select precedes month, false for month then year
								yearSuffix: "", // Additional text to append to the year in the month headers
								shortYearCutoff: 10
							};
							$.datepicker.setDefaults($.datepicker.regional['xx']);
							
							elem.datepicker( {
								changeMonth: true,
								changeYear: true,
								yearRange: "-120:+10",
								dateFormat: ips.getSetting( 'date_format' ),
								firstDay: ips.getSetting( 'date_first_day' ),
							});
							
							//elem.datepicker('refresh');
							elem.datepicker('show');
						};

						elem.on( 'focus', function () {
							_buildDatepicker();
						});
					});
				}
			},

			/**
			 * Handles color fields by adding a jquery plugin if the browser doesn't natively support type='color'
			 *
			 * @param	{element}	elem		The color control
			 * @returns {void}
			 */
			color: function (elem) {
				if( elem.attr('data-ipsFormData') )
				{
					return;
				}

				ips.loader.get( ['core/interface/spectrum/spectrum.js'] ).then( function () {
					elem.attr('data-ipsFormData', 1);
					
					var options = {
						type: "text",
						clickoutFiresChange: true,
						hideAfterPaletteSelect: true,
						preferredFormat: "hex",
						appendTo: $(elem).closest('li')
					};
					
					if ( ! elem.attr('data-rgba') ) {
						options.showAlpha = false;
					}
					
					if ( elem.attr('data-swatches') ) {
						options.showPalette = true;
						options.showSelectionPalette = true;
						options.localStorageKey      = 'ips.ColorPicker';
					}
	
					$(elem).spectrum( options );
				} );
			},

			/**
			 * Toggles one or more form rows when the element value changes
			 *
			 * @param	{element}	elem		The form control that is the trigger
			 * @returns {void}
			 */
			toggle: function (elem, form) {	

				// "On" toggles
				var togglesOn = ( elem.attr('data-togglesOn') || elem.attr('data-toggles') || '' ).split(',');
				var togglesOff = ( elem.attr('data-togglesOff') || '' ).split(',');

				// Call _toggler once each for 'on' and 'off' toggles
				if( togglesOn.length ){
					_toggler( elem, form, togglesOn, true );
				}

				if( togglesOff.length ){
					_toggler( elem, form, togglesOff, false );
				}
			},

			/**
			 * Handles dimension controls, which have dragging functionality to choose a size
			 *
			 * @param	{element}	elem	The dimensions element
			 * @returns {void}
			 */
			dimensions: function (elem) {
				var container = elem.closest('.ipsWidthHeight_container');

				elem.resizable( {
					resize: function (event, ui) {
						container.find('input.ipsWidthHeight_width').val( elem.width() );
						container.find('input.ipsWidthHeight_height').val( elem.height() );
					}
				});

				container.find('input.ipsWidthHeight_width').on( 'change', function () {
					elem.width( $( this ).val() );
				});

				container.find('input.ipsWidthHeight_height').on( 'change', function () {
					elem.height( $( this ).val() );
				});			
			},

			/**
			 * Sets up events for unlimited checkbox for dimension controls
			 *
			 * @param	{element}	elem	THe checkbox element
			 * @returns {void}
			 */
			dimensionsUnlimited: function (elem) {
				elem.on( 'change', function () {
					_dimensionsUnlimitedCheck( elem );
				});

				_dimensionsUnlimitedCheck( elem );
			},

			/**
			 * Disables fields if JS is enabled
			 *
			 * @param	{element}	elem		The element this widget is being created on
			 * @returns {void}
			 */
			jsDisable: function (elem) {
				elem.prop('disabled', true);
			},
			
			/**
			 * CheckboxSet with Unlimited checkbox
			 *
			 * @param	{element}	elem		The element this widget is being created on
			 * @returns {void}
			 */
			granularCheckboxset: function (elem) {
				elem.find('[data-role="checkboxsetUnlimitedToggle"]').on( 'change', function () {
					// We don't want to check disabled boxes, but we do want to uncheck them
					if( $(this).is(':checked') ){
						elem.find('[data-role="checkboxsetGranular"] input:enabled[type="checkbox"]').prop( 'checked', $(this).is(':checked') );
					} else {
						elem.find('[data-role="checkboxsetGranular"] input[type="checkbox"]').prop( 'checked', $(this).is(':checked') );
					}
				});
				
				elem.find('[data-action="checkboxsetCustomize"]').on( 'click', function () {
					elem.find('[data-role="checkboxsetUnlimited"]').hide();
					elem.find('[data-role="checkboxsetUnlimitedToggle"]').prop( 'checked', false );

					if( elem.find('[data-role="checkboxsetUnlimitedToggle"]').length > 0 ) {
						elem.find('[data-role="checkboxsetGranular"]').slideDown();
					}
				});
				
				elem.find('[data-action="checkboxsetAll"]').on( 'click', function () {
					elem.find('[data-role="checkboxsetGranular"] input:enabled[type="checkbox"]').prop( 'checked', true );

					if( elem.find('[data-role="checkboxsetUnlimitedToggle"]').length > 0 ) {
						elem.find('[data-role="checkboxsetUnlimited"]').slideDown();
						elem.find('[data-role="checkboxsetGranular"]').slideUp();
						elem.find('[data-role="checkboxsetUnlimitedToggle"]').prop( 'checked', true ).change();
					}
				});
				elem.find('[data-action="checkboxsetNone"]').on( 'click', function () {
					elem.find('[data-role="checkboxsetGranular"] input:enabled[type="checkbox"]').prop( 'checked', false );

					if( elem.find('[data-role="checkboxsetUnlimitedToggle"]').length > 0 ) {
						elem.find('[data-role="checkboxsetUnlimited"]').slideDown();
						elem.find('[data-role="checkboxsetGranular"]').slideUp();
						elem.find('[data-role="checkboxsetUnlimitedToggle"]').prop( 'checked', false ).change();
					}
				});
				
				elem.find('[data-role="search"]').on( 'keydown', function(e){
					if ( e.keyCode == 13 || e.keyCode == 38 || e.keyCode == 40 ) {
						e.preventDefault();
					}
				});
				// We call .off() first in case this function is hit more than once - we don't want the event handler set more than once
				// or it will cause items to be skipped when navigating via arrows, and checkboxes not to check when hitting enter
				elem.find('[data-role="search"]').off('keyup').on( 'keyup', function(e){
					var focussedCheckbox = elem.find('[data-role="result"].ipsField__checkboxOverflow__focused');
					
					switch ( e.keyCode ) {
						case 13: // Return
							focussedCheckbox.find('input').prop( 'checked', !focussedCheckbox.find('input').prop('checked') );
							break;

						case 38: // Up
							var prev = focussedCheckbox.prevAll(':visible').first();
							if ( prev.length ) {
								focussedCheckbox.removeClass('ipsField__checkboxOverflow__focused')
								prev.addClass('ipsField__checkboxOverflow__focused');
							}
							break;
						
						case 40: // Down
							var next = focussedCheckbox.nextAll(':visible').first();
							if ( next.length ) {
								focussedCheckbox.removeClass('ipsField__checkboxOverflow__focused')
								next.addClass('ipsField__checkboxOverflow__focused');
							}
							break;
							
						default:
							focussedCheckbox.removeClass('ipsField__checkboxOverflow__focused');

							var val = $(this).val().toLowerCase();
							if ( val ) {
								elem.find('[data-role="massToggles"]').hide();
								elem.find('[data-role="result"]').each(function(){
									if ( $(this).find('[data-role="label"]').text().toLowerCase().includes( val ) ) {
										$(this).show();
									} else {
										$(this).hide();
									}
								});
								elem.find('[data-role="result"]:visible').first().addClass('ipsField__checkboxOverflow__focused');
							} else {
								elem.find('[data-role="result"]:hidden').show();
								elem.find('[data-role="massToggles"]').show();
							}
					}
				});

				elem.find('[data-role="search"]').on( 'clear blur', function(e){
					// Did we want to select/un select an input first?
					if( $( e.relatedTarget ).closest('[data-role="checkboxsetGranular"]').find('[data-role="search"]').is( this ) ){
						e.preventDefault();
						$( this ).focus();
						return;
					}
					
					$(this).val('');
					elem.find('[data-role="result"].ipsField__checkboxOverflow__focused').removeClass('ipsField__checkboxOverflow__focused');
					elem.find('[data-role="result"]:hidden').show();
					elem.find('[data-role="massToggles"]').show();
				});

				var count = parseInt( elem.attr('data-count') );

				// If we have more than 10 items, then make this a scrolling selection box with search
				if( count > 10 ){
					elem.find('.ipsField__checkboxOverflow').addClass('ipsField__checkboxOverflow--active');
					elem.find('[data-role="search"]').removeClass('ipsHide');
				}
			},
			
			/**
			 * Dialling code select box
			 *
			 * @param	{element}	elem		The element this widget is being created on
			 * @returns {void}
			 */
			diallingCode: function(elem) {
				
				var selected = elem.find('option:selected');
				if ( selected.length ) {
					selected.html( selected.attr('data-code') );
				}
				
				elem.on('change mouseleave', function(){
				    elem.find('option').each(function(){
				      $(this).html( $(this).attr('data-text') ); 
				    });
				    elem.find('option:selected').html( elem.find('option:selected').attr('data-code')  );
				    $(this).blur();
				});
				elem.on('focus', function(){
				    elem.find('option').each(function(){
				        $(this).html( $(this).attr('data-text') ); 
				    });
				});
			}
		},

		//--------------------------------------------------------------
		// Helper methods for individual form control types
		//--------------------------------------------------------------

		/**
		 * Handles toggling for a given element by calling the appropriate method for the type of element
		 *
		 * @param	{element}	elem			The element on which the toggle is specified
		 * @param 	{element}	form 			The form element
		 * @param	{string} 	toggleList 		The comma-separated list of element IDs to be toggled
		 * @param 	{boolean} 	toggleOn 		Whether the provided IDs should be shown (true) or hidden (false)
		 * @returns {void}
		 */
		_toggler = function (elem, form, toggleList, toggleOn) {
			var toCall;
			var triggerElem;
			var eventType = 'change';

			// Turn toggleList into a selector
			var selectorList = ips.utils.getIDsFromList( toggleList );

			if( !selectorList ){
				return;
			}

			// Get the right function and element depending on the type
			if( elem.is('option') ){
				toCall = _toggleSelect;
				triggerElem = elem.closest('select');
			} else if( elem.is('input[type="checkbox"]') ){
				toCall = _toggleCheckbox;
				triggerElem = elem;
			} else if( elem.is('input[type="radio"]') ){
				toCall = _toggleRadio;
				triggerElem = form.find('input[name="' + elem.attr('name') + '"]');
			} else if( elem.is('.ipsSelectTree_item') ){
				toCall = _toggleNode;
				triggerElem = elem.closest('.ipsSelectTree');
				eventType = 'nodeSelectedChanged';
			} else {
				toCall = _toggleGeneric;
				triggerElem = elem;
			}

			var reverse = !toggleOn;

			// Set the event
			triggerElem.on( eventType, function () {
				toCall.call( this, triggerElem, selectorList, elem, form, reverse );
			});
			
			// And call immediately to initialize, if it's currently visible
			if( triggerElem.is(':visible') || ( triggerElem.attr('data-toggle-visibleCheck') && $( triggerElem.attr('data-toggle-visibleCheck') ).is(':visible') ) ){
				toCall.call( this, triggerElem, selectorList, elem, form, reverse );	
			}
		},


		/**
		 * Handles the 'unlimited' checkbox for dimension controls
		 *
		 * @param	{element}	elem	The checkbox element
		 * @returns {void}
		 */
		_dimensionsUnlimitedCheck = function (elem) {
			var container = elem.closest('.ipsWidthHeight_container');

			if( elem.is(':checked') ){
				container
					.find('[data-control="dimensions"]')
						.hide()
					.end()
					.find('input.ipsWidthHeight_width, input.ipsWidthHeight_height')
						.val('')
						.prop( 'disabled', true );
			} else {
				container
					.find('[data-control="dimensions"]')
						.show()
					.end()
					.find('input.ipsWidthHeight_width, input.ipsWidthHeight_height')
						.change()
						.prop( 'disabled', false );
			}
		},

		/**
		 * Toggle behavior for radio buttons
		 * Hides all toggle panes assosciated with radio buttons sharing the same name, then shows
		 * panes necessary if this radio button is checked
		 *
		 * @param	{array}		radioList	All radio buttons that share the same name
		 * @param	{string}	toggleList	Selector list of elements to toggle
		 * @param	{element}	thisElem	The radio button that was clicked
		 * @returns {void}
		 */
		_toggleRadio = function (radioList, toggleList, thisElem, form) {

			// Hide all toggles
			radioList.each( function () {
				var thisToggles = ips.utils.getIDsFromList( $( this ).attr('data-toggles') );

				if( thisToggles ){
					_hideFormRows( thisToggles, form );
				}
			});

			// Find the checked one
			radioList.each( function () {
				if( $( this ).is(':checked') ){
					var thisToggles = ips.utils.getIDsFromList( $( this ).attr('data-toggles') );

					if( thisToggles ){
						_showFormRows( thisToggles, form );
					}
				}
			});
			
		},

		/**
		 * Toggle behavior for select boxes
		 *
		 * @param	{element}	elem		Checkbox that was changed
		 * @param	{string}	toggleList	Selector list of elements to toggle
		 * @returns {void}
		 */
		_toggleSelect = function (selectElem, toggleList, thisElem, form) {
			selectElem.find('option').each( function (idx, elem) {
				if( $( this ).attr('data-toggles') ){
					_hideFormRows( ips.utils.getIDsFromList( $( this ).attr('data-toggles') ), form );
				}
			});

			// Get selected items
			selectElem.find('option:selected').each( function (i, elem) {
				if( $( elem ).attr('data-toggles') ){
					_showFormRows( ips.utils.getIDsFromList( $( this ).attr('data-toggles') ), form );
				}
			});
		},

		/**
		 * Toggle behavior for checkboxes
		 *
		 * @param	{element}	elem		Checkbox that was changed
		 * @param	{string}	toggleList	Selector list of elements to toggle
		 * @returns {void}
		 */
		_toggleCheckbox = function (elem, toggleList, thisElem, form, reverse) {
			// Get the value
			var show = elem.is(':checked');

			if( reverse ){
				show = !show;
			}

			// If this is an "unlimited" checkbox that isn't checked, make sure we don't hide something that should be shown by virtue of the container form control
			if( elem.is('[data-control~="unlimited"]') && !elem.is(':checked') )
			{
				var inputs = elem.closest('.ipsFieldRow_content,[data-role="unlimitedCatch"]').find('input[type="radio"],select');

				if( inputs.length )
				{
					inputs.each( function () {
						var toggle = $( this );
						if( toggle.is('select') )
						{
							var toggleList = _.difference( toggleList, toggle.find('option:selected').attr('data-toggles') );
						}
						else if( toggle.is(':checked') )
						{
							var toggleList = _.difference( toggleList, toggle.attr('data-toggles') );
						}
					});
				}
			}

			if( show ){
				_showFormRows( toggleList, form );
			} else {
				_hideFormRows( toggleList, form );
			}
		},

		/**
		 * Toggle behavior for node selector
		 *
		 * @param	{element}	elem		Input field that was changed
		 * @param	{string}	toggleList	Selector list of elements to toggle
		 * @returns {void}
		 */
		_toggleNode = function (nodeElem, toggleList, thisElem, form) {

			nodeElem.find('[data-action="nodeSelect"][data-toggles]').each( function (idx, elem) {
				_hideFormRows( ips.utils.getIDsFromList( $( this ).attr('data-toggles') ), form );
			});

			// Now get the selected ones
			nodeElem.find('[data-action="nodeSelect"][data-toggles].ipsSelectTree_selected').each( function (idx, elem) {
				_showFormRows( ips.utils.getIDsFromList( $( this ).attr('data-toggles') ), form );
			});
		},
		
		/**
		 * Toggle behavior for other input types
		 *
		 * @param	{element}	elem		Input field that was changed
		 * @param	{string}	toggleList	Selector list of elements to toggle
		 * @returns {void}
		 */
		_toggleGeneric = function (elem, toggleList, thisElem, form) {
			// Get the value
			var show = elem.val() == 0 ? false : true;

			if( !_.isUndefined( elem.attr('data-togglereverse') ) ){
				show = !show;
			}
			
			if( show ){
				_showFormRows( toggleList, form );
			} else {
				_hideFormRows( toggleList, form );
			}
		},

		/**
		 * Hides the form rows contained in the provided selector. Works recursively to hide any toggled
		 * panes of elements that become hidden
		 *
		 * @param	{string}	hide	Selector containing elements to hide
		 * @returns {void}
		 */
		_hideFormRows = function (hide, form) {
			if( _.isArray( hide ) ){
				hide = hide.join(',');
			}
						
			$( form || document ).find( hide )
				.hide()
				.addClass('ipsHide')
				.find('[data-toggles],[data-togglesOn],[data-togglesOff]')
					.each( function (i, elem) {
						_hideFormRows( ips.utils.getIDsFromList( $( elem ).attr('data-toggles') ), form );
						_hideFormRows( ips.utils.getIDsFromList( $( elem ).attr('data-togglesOn') ), form );
						_hideFormRows( ips.utils.getIDsFromList( $( elem ).attr('data-togglesOff') ), form );
					});
		},

		/**
		 * Shows the form rows contained in the provided selector, and sets up any toggles contained on
		 * elements that become visible
		 *
		 * @param	{string}	show	Selector containing elements to show
		 * @returns {void}
		 */
		_showFormRows = function (show, form) {
			if( _.isArray( show ) ){
				show = show.join(',');
			}
			$( form || document )
				.find( show )
					.not('[data-ipsToggle]')
					.show()
				.end()
				.removeClass('ipsHide')
				.find('[data-toggles],[data-togglesOn]')
					.each( function (i, elem) {
						_controlMethods.toggle( $( elem ), form );
					})
				.end()
				.find('[data-ipsUploader]')
					.each( function (i, elem) {
						ips.ui.uploader.refresh( elem );
					})
				.end()
				.find('[data-ipsMatrix]')
					.each( function (i, elem) {
						ips.ui.matrix.refresh( elem );
					});
		},

		/**
		 * Disables elements when an option within the <select> has a data-disable attribute
		 *
		 * @param	{element}	elem		Select element
		 * @returns {void}
		 */
		_selectDisable = function (elem) {
			var option = elem.find('[data-disable]');

			if( !option.length ){
				return;
			}

			var disable = option.attr('data-disable');

			if( !option.is(':selected') ){
				$( disable ).prop('disabled', false);
			} else {
				$( disable ).prop('disabled', true);
			}
		},

		/**
		 * 'Unlimited' checkbox implementation
		 * Disables inputs if the element is checked
		 *
		 * @param	{element}	checkbox		Checkbox element
		 * @returns {void}
		 */
		_unlimitedCheck = function (checkbox) {
			var inputs = checkbox.closest('.ipsFieldRow_content,[data-role="unlimitedCatch"]').find('input:not([type="checkbox"],[type="hidden"]),select,textarea');

			// Helper function to check toggle states within an input
			var checkToggles = function (input) {
				var toggles = input.find('[data-control="toggle"]');
				var form = input.closest('[data-ipsForm]');

				if( toggles.length ){
					toggles.each( function () {
						var toggle = $( this );
						_controlMethods.toggle( toggle, form );
					});
				}
			};

			if( !checkbox.is(':disabled') ){
				if( checkbox.is(':checked') ){
					inputs.each( function () {
						var thisInput = $( this );
						var val = thisInput.val();

						if( val !== null ){
							thisInput.attr( 'data-previousvalue', val );
						}

						thisInput.val('');

						checkToggles( thisInput ); // Does the input have any toggle to check?
						thisInput.prop( 'disabled', true );
					})
					.find('[data-role="rangeBoundary"]')
						.css( { opacity: "0.5" } );
				} else {
					inputs.each( function () {
						var thisInput = $( this );

						thisInput.prop( 'disabled', false );
						if ( thisInput.attr( 'data-previousvalue' ) ) {
							thisInput.val( thisInput.attr( 'data-previousvalue' ) );
						}

						// Does the input have any toggle to check?
						checkToggles( thisInput );
					})
					.find('[data-role="rangeBoundary"]')
						.css( { opacity: "1" } );
				}
			}
		},
		
		/**
		 * Displays a validation error
		 *
		 * @param	{elem}		field		Field row in which to display an error
		 * @param	{string}	error		Error message
		 * @returns {void}
		 */
		_validationError = function (field, error) {
			field
				.closest('.ipsFieldRow')
				.find('.ipsFieldRow_title')
					.addClass('error')
				.end()
				.find('.ipsType_warning')
					.html( error );
		};

		ips.ui.registerWidget( 'form', ips.ui.form );

		return {
			respond: respond,
			init: init
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.formSubmit.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.formSubmit.js - Disables form submit button when form is submitted to help prevent duplicated submissions
 *
 * Author: Mark Wade & Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.formSubmit', function(){

		/**
		 * Respond
		 *
		 * @returns {void}
		 */
		var respond = function (elem, options) {
			var formElement = $(elem).is('form') ? $(elem) : $(elem).closest('form');

			// Disable submit button when form is submitted to prevent duplicate submissions
			formElement.on( 'submit', function( e ){
				formElement.find('input[type="submit"],button[type="submit"]').prop( 'disabled', true );
			});

			// If attachment is still uploading, form submission is stopped. If that happens, re-enable submit button so user can try again
			formElement.on( 'fileStillUploading', function( e ){
				formElement.find('input[type="submit"],button[type="submit"]').prop( 'disabled', false );
			});
		};

		ips.ui.registerWidget( 'formSubmit', ips.ui.formSubmit );

		return {
			respond: respond
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.grid.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.grid.js - Widget for managing contents of a grid, such that parts scale in proportion, and cells do not become too small 
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.grid', function(){

		var defaults = {
			patchwork: false,
			items: '[data-role="gridItem"]',
			equalHeights: false
		};

		var respond = function (elem, options) {
			if( !$( elem ).data('_grid') ){
				$( elem ).data('_grid', gridObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Retrieve the grid instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The grid instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_grid') ){
				return $( elem ).data('_grid');
			}

			return undefined;
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		};


		/**
		 * Grid instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var gridObj = function (elem, options) {
			var originalSpan = 3;
			var currentSpan = 3;
			var possibleSizes = [ 1, 2, 3, 4, 6, 12 ]; // Since we're doing an even grid
			var deferInit = false;

			/**
			 * Initialization: get the current span, and make sure all items are using it for consistency
			 *
			 * @returns {void}
			 */
			var init = function () {
				if( !elem.is(':visible') ){
					deferInit = true;
					Debug.log('ui.ipsGrid is not visible; deferring init...');
				}

				if( !deferInit ){
					_initWhenVisible();
				}

				// If we have images inside, we'll need to redraw after they are loaded
				elem.imagesLoaded( function () {
					redrawGrid();
				});

				// Window resize event which keeps everything at the right size
				$( window ).on( 'resize', redrawGrid );

				// If this chart is in a tab, we need to re-initialize it after the tab is shown so that
				// it sizes properly
				$( document ).on( 'tabShown', _tabShown );

				// A new item has been added to the grid
				$( elem ).on( 'newItem', function (e, data) {
					data = $( data );
					_removeSpans( data );
					_addSpan( data, currentSpan );

					_checkDeferredInit();

					if( !deferInit ){
						_scaleProportions( data );	
						_equalHeights();
					}					
				});
			},

			/**
			 * Destruct this instance
			 *
			 * @returns {void}
			 */
			destruct = function () {
				$( window ).off( 'resize', redrawGrid );
				$( document ).off( 'tabShown', _tabShown );
			},

			redrawGrid = function () {
				_checkDeferredInit();

				if( !deferInit ){
					if( options.minItemSize || options.maxItemSize ){
						_checkItemWidth(0);
					}
					_scaleProportions( _getAll() );
					_equalHeights();
					elem.trigger('gridRedraw.grid');
				}
			},

			/**
			 * Event handler for when a tab is shown
			 *
			 * @param 	{event} 	e 		Event object
			 * @param 	{object} 	data 	Event data object
			 * @returns {void}
			 */
			_tabShown = function (e, data) {
				if( $.contains( data.panel.get(0), elem.get(0) ) ){
					redrawGrid();
				}
			},

			/**
			 * Init stuff that can only be done when the elem is visible
			 *
			 * @returns {void}
			 */
			_initWhenVisible = function () {
				var firstItem = _getFirst();
				var allItems = _getAll();

				if( !options.defaultSpan ){
					for( var i = 1; i <= 12; i++ ){
						if( firstItem.hasClass( 'ipsGrid_span' + i ) ){
							originalSpan = currentSpan = i;
							break;
						}
					}
				} else {
					originalSpan = currentSpan = options.defaultSpan;
				}

				_changeSpan( currentSpan );
				_scaleProportions( _getAll() );
				_equalHeights();
				elem.trigger('gridRedraw.grid');
			},

			/**
			 * Checks if init is deferred, and if so and the element is now visible, runs it
			 *
			 * @returns {void}
			 */
			_checkDeferredInit = function () {
				if( deferInit && elem.is(':visible') ){
					Debug.log('ui.ipsGrid is visible; now running init...');
					deferInit = false;
					_initWhenVisible();
				}
			},

			/**
			 * Scales the proportions of elements with data-grid-ratio
			 *
			 * @returns {void}
			 */
			_scaleProportions = function (item) {
				var width = _getFirst().outerWidth();

				item.addBack().find('[data-grid-ratio]').each( function () {
					var item = $( this );
					var newHeight = ( width / 100 ) * parseInt( item.attr('data-grid-ratio') );

					item.css({
						height: Math.ceil( newHeight ) + 'px'
					});
				});
			},

			/**
			 * Scales the proportions of elements with data-grid-ratio
			 *
			 * @returns {void}
			 */
			_equalHeights = function () {
				if( !options.equalHeights ){
					return;
				}

				var items = _getAll();

				if( options.equalHeights == 'row' ){
					var numPerRow = 12 / currentSpan;
					var loops = Math.ceil( items.length / numPerRow );
					var idx = 0;

					// If we are on a phone, and collapsed, reset the heights
					if( ( elem.hasClass('ipsGrid_collapsePhone') && ips.utils.responsive.currentIs('phone') ) ||
						( elem.hasClass('ipsGrid_collapseTablet') && ips.utils.responsive.currentIs('tablet') ) ){
						items.css({
							height: 'auto'
						});
						
						return;
					}

					for( var i = 0; i < loops; i++ ){
						var rowItems = items.slice( idx, idx + numPerRow );
						idx = idx + numPerRow;

						// Reset the height so that we recalculate properly
						rowItems.css({
							height: 'auto'
						});

						var max = _.max( rowItems, function (item) {
							return $( item ).outerHeight();
						});

						rowItems.css({
							height: $( max ).outerHeight() + 'px'
						});
					}
				} else {
					// Reset the height so that we recalculate properly
					items.css({
						height: 'auto'
					});

					var max = _.max( items, function (item) {
						return $( item ).outerHeight();
					});

					items.css({
						height: $( max ).outerHeight() + 'px'
					});
				}
			},

			/**
			 * Checks the item width, and if it's less or more than our min/max widths, apply a new span
			 *
			 * @returns {void}
			 */
			_checkItemWidth = function (iteration) {

				var firstItem = _getFirst();
				var bestFit = originalSpan;

				// Here we loop through each possible size, fetch the actual pixel width it results in,
				// and then determine if it meets our params. We go backwards through possibleSizes because
				// we want the smallest possible size to win
				for( var i = possibleSizes.length - 1; i > 0; i-- ){
					// Add this span to the element, figure out if this is a good width
					_removeSpans( firstItem );
					_addSpan( firstItem, possibleSizes[ i ] );

					var size = firstItem.outerWidth();

					if( options.minItemSize && size < parseInt( options.minItemSize ) ){
						continue;
					}

					if( options.maxItemSize && size > parseInt( options.maxItemSize ) ){
						continue;
					}

					bestFit = possibleSizes[ i ];
				}

				// Now update the span
				_changeSpan( bestFit );
			},

			/**
			 * Return the first grid item
			 *
			 * @returns {element} 	First grid item
			 */
			_getFirst = function () {
				return elem.find('> [class*="ipsGrid_span"]').first()
			},

			/**
			 * Return all grid items
			 *
			 * @returns {element} 	All grid items
			 */
			_getAll = function () {
				return elem.find('> [class*="ipsGrid_span"]');
			},

			/**
			 * Remove all grid spans from the provided item(s)
			 *
			 * @param	{element} 	items 		Items to remove span from
			 * @returns {void}
			 */
			_removeSpans = function (items) {
				for( var i = 1; i <= 12; i++ ){
					items.removeClass( 'ipsGrid_span' + i );
				}
			},

			/**
			 * Adds the given span to the given items
			 *
			 * @param	{element} 	items 		The elements to apply the new span to
			 * @param	{number} 	size 		The new span size
			 * @returns {void}
			 */
			_addSpan = function (items, size) {
				items.addClass( 'ipsGrid_span' + size );
			},

			/**
			 * Change the current span size on the given elements
			 *
			 * @param	{number} 	newSize 	New span size
			 * @returns {void}
			 */
			_changeSpan = function (newSize) {
				if( newSize <= 1 ){
					return;
				}

				var items = _getAll();

				_removeSpans( items );
				_addSpan( items, newSize );

				currentSpan = newSize;
			};

			init();

			return {
				init: init,
				destruct: destruct
			};
		};

		ips.ui.registerWidget( 'grid', ips.ui.grid, [
			'minItemSize', 'maxItemSize', 'items', 'equalHeights'
		] );

		return {
			respond: respond,
			getObj: getObj,
			destruct: destruct
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.hovercard.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[ /* global ips, _, Debug */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.hovercard.js - Hovercard UI component
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.hovercard', function(){

		var defaults = {
			timeout: 0.75, // Hovercard timeout, in seconds
			showLoading: true, // Show the loading widget for ajax requests?
			width: 450, // Default width of hovercards
			className: 'ipsHovercard',
			onClick: false,
			target: null,
			cache: true
		};

		// Cache object for URLs
		var cache = {};

		var respond = function (elem, options) {

			if( !$( elem ).data('_hover') ){
				$( elem ).data('_hover', hoverCardObj(elem, _.defaults( options, defaults ) ) );
			}

			if( options.onClick ){
				// We have to remove the click event before reapplying, or multiple events
				// will be trying to open the hovercard
				$( elem ).off('.hovercard').on( 'click.hovercard', function (e) {
					e.preventDefault();
					$( elem ).data('_hover').start();
				});
			} else {
				// Don't show hovercards on small touch devices
				if( ips.utils.events.isTouchDevice() && ( ips.utils.responsive.currentIs('phone') || ips.utils.responsive.currentIs('tablet') ) ){
					return;
				}

				$( elem ).data('_hover').start();
			}			
		},

		/**
		 * Retrieve the hovercard instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The hovercard instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_hover') ){
				return $( elem ).data('_hover');
			}

			return undefined;
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		},

		setCache = function (url, content) {
			cache[ url ] = content;
		},

		unCache = function (url) {
			delete cache[ url ];
		},

		getCache = function (url) {
			return cache[ url ];
		};

		ips.ui.registerWidget('hover', ips.ui.hovercard, 
			[ 'timeout', 'attach', 'content', 'width', 'onClick', 'target', 'cache' ],
			{ lazyLoad: true, lazyEvents: 'mouseover' } 
		);

		return {
			respond: respond,
			destruct: destruct,
			setCache: setCache,
			getCache: getCache
		};
	});


	/**
	 * Hovercard instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var hoverCardObj = function (elem, options) {

		var onTimeout = null, // Reference to our show timeout
			offTimeout = null, // Reference to hide timeout
			ajaxObj, // Ajax object reference
			content, // Content of the hovercard
			target, // The actual element the hovercard is attached to (usually elem)
			loading, // Our loading element
			card, // The hovercard itself
			working = false, // Are we in the middle of setup?
			elemID = '';

		/**
 		 * Sets up this instance
		 * This method does not start showing a hovercard. Call 'start' to do that.
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			elemID = $( elem ).identify().attr('id');
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @returns {void}
		 */
		destruct = function () {
			// Clear mouseout timeouts
			clearTimeout( offTimeout );
			// Remove document click event for hiding
			$( document ).off( 'click.' + elemID );
			// Remove loading widget in case it's there
			_removeLoadingWidget();
			// Delete the card element
			if( card ){
				card.remove();
			}
		},

		/**
		 * Starts the process of building and showing a hovercard
		 * Sets up events and starts a timeout to make sure we should really show it
		 *
		 * @returns 	{void}
		 */
		start = function () {

			// Check we aren't already in setup - prevents double-clicks
			if( working !== false && options.onClick ){
				return;
			}

			working = true;

			// Get the target
			target = ( $( options.attach ).length ) ? $( options.attach ) : $( elem );

			// Clear the timeout for our mouse off event
			clearTimeout( offTimeout );

			if( !options.onClick ){
				// We set a timeout before we do anything, which means we can cancel the event
				// if the user moves a mouse off the target
				onTimeout = setTimeout( _startShow, ( options.timeout * 1000 ) );

				// Set the event handler for when the mouse stops hoving
				$( elem ).off('mouseout.hovercard', _mouseOut).on('mouseout.hovercard', _mouseOut);
				$( elem ).off('mousedown.hovercard', _elemClick).on( 'mousedown.hovercard', _elemClick );
			} else {
				$( document ).off( 'click.' + elemID ).on( 'click.' + elemID, _documentClick );
				_startShow();
			}
		},

		/**
		 * The trigger element was clicked, so we cancel the hovercard showing
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_elemClick = function (e) {
			if( onTimeout ){
				clearTimeout( onTimeout );
			}

			if( offTimeout ){
				clearTimeout( offTimeout );
			}

			if( ajaxObj && _.isFunction( ajaxObj.abort ) ){
				ajaxObj.abort();
			}
			
			_removeLoadingWidget();
			_hideCard();
		},

		/**
		 * Reacts to a click on the document (used for an onclick hovercard)
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_documentClick = function (e) {
			if( !$( card ).is(':visible') ){
				return;
			}

			if( e.target != elem && !$.contains( elem, e.target ) && e.target != card.get(0) && !$.contains( card.get(0), e.target ) ){
				_hideCard();
				$( document ).off( 'click.' + elemID );
			}
		},

		/**
		 * Internal call to fetch content, build, position and show a hovercard
		 *
		 * @returns 	{void}
		 */
		_startShow = function () {

			if( card && card.length && _.isElement( card.get(0) ) ){
				_positionCard();
				working = false;
				return;
			}

			// Determine where content is coming from
			if( options.content && $( options.content ).length ) {
				_buildLocalContent();
				_buildCard();
				_positionCard();

				working = false;
			} else {
				_buildRemoteContent()
					.done( function () {
						_buildCard();
						_positionCard( true );
					})
					.fail( function () {})
					.always( function () {
						working = false;
					});
			}
		},

		/**
		 * Hides the hovercard
		 *
		 * @returns 	{void}
		 */
		_hideCard = function () {
			ips.utils.anim.go( 'fadeOut', card );
		},

		/**
		 * Positions a hovercard relative to the target
		 *
		 * @param 	{boolean} 	showImmediate 	If true, no fade-in animation is used
		 * @returns {void}
		 */
		_positionCard = function ( showImmediate ) {

			if( !card.length ){
				Debug.warn("_positionCard called before a card element exists");
				return;
			}

			if( !target.is(':visible') ){
				Debug.info("Can't show hovercard when target isn't visible");
				return;
			}

			// Reset menu positioning
			card.css({
				left: 'auto',
				top: 'auto',
				position: 'static'
			});

			if( card.attr('data-originalWidth') ){
				card.css({
					width: card.attr('data-originalWidth') + 'px'
				});
			}

			// Figure out where we'll place it
			var elemPos = ips.utils.position.getElemPosition( target );
			var tooWide = false;
			var elemHeight = $( target ).height();
			var elemWidth = $( target ).width();
			var actualWidth = $( card ).width();
			var actualHeight = $( card ).height();
			var win = $( window );
			
			// Set up the data we'll use to position it
			var positionInfo = {
				trigger: elem,
				target: card,
				above: true,
				stemOffset: { left: 20, top: 0 }
			};

			var location = ips.utils.position.positionElem( positionInfo );

			// Position the hovercard with the resulting styles
			card.css({
				left: location.left + 'px',
				top: location.top + 'px',
				position: ( location.fixed ) ? 'fixed' : 'absolute',
				zIndex: ips.ui.zIndex()
			});

			var newElemPosition = ips.utils.position.getElemPosition( card );

			// If the menu is wider than the window, reset some styles
			if( ( actualWidth > $( document ).width() ) || newElemPosition.viewportOffset.left < 0 ){
				options.noStem = true;
				
				card
					.attr( 'data-originalWidth', actualWidth )
					.css({
						left: '10px',
						width: ( $( document ).width() - 20 ) + 'px'
					});

				var newLocation = ips.utils.position.positionElem( positionInfo );

				card.css({
					top: newLocation.top + 'px'
				});
			}

			// Remove old stems
			card.find('.ipsHovercard_stem').remove();

			_.each( ['Top', 'Bottom', 'Left', 'Right'], function (type) {
				card.removeClass( 'ipsHovercard_stem' + type );
			});

			// Build stem
			var stem = $('<span/>').addClass('ipsHovercard_stem');
			card
				.append( stem )
				.addClass( options.className + '_stem' + ( location.location.vertical.charAt(0).toUpperCase() + location.location.vertical.slice(1) ) );

			// If the card is a full-width size, we position the stem to the trigger.
			// Otherwise we just apply a classname
			if( tooWide ){
				stem.css({
					left: ( elemPos.viewportOffset.left - 10 ) + 'px'
				});
			} else {
				card.addClass( options.className + '_stem' + ( location.location.horizontal.charAt(0).toUpperCase() + location.location.horizontal.slice(1) ) );
			}		

			// And now animate in
			if( showImmediate ){
				card.show();
			} else {
				ips.utils.anim.go( 'fadeIn', card );
			}
		},

		/**
		 * Builds the hovercard
		 *
		 * @returns 	{void}
		 */
		_buildCard = function () {

			var cardId = $( elem ).identify().attr('id') + '_hovercard',
				actualWidth = options.width || 300;

			// Build the card wrapper
			card = $('<div/>');

			card
				.attr( { id: cardId } )
				.addClass( options.className )
				.css( {
					width: actualWidth + 'px',
					zIndex: ips.ui.zIndex()
				});

			if( _.isString( content ) ){
				card.append( $('<div/>').html( content ) );
			} else {
				card.append( content.show() );
			}

			// Append to container
			ips.getContainer().append( card );	

			// Watch event handlers
			if( !options.onClick ){
				card
					.on('mouseenter', _cardMouseOver)
					.on('mouseleave', _cardMouseOut);	
			}			

			// Let everyone know
			$( document ).trigger('contentChange', [ card ]);		
		},

		/**
		 * If this card is using local content, we build it here
		 *
		 * @returns 	{void}
		 */
		_buildLocalContent = function () {
			content = $( options.content );
		},

		/**
		 * Fetch remote content based on the target href
		 *
		 * @returns 	{promise}
		 */
		_buildRemoteContent = function () {

			var deferred = $.Deferred();

			if( !elem.href ){
				deferred.reject();
				return deferred.promise();
			}

			if( options.cache && ips.ui.hovercard.getCache( elem.href ) ){
				content = ips.ui.hovercard.getCache( elem.href );
				deferred.resolve();
				return deferred.promise();
			}

			// Show temporary loading thingy
			_buildLoadingWidget();

			// Get our ajax handler
			if ( options.target ) {
				var target = options.target;
			} else {
				var target = elem.href;
			}
			ajaxObj = ips.getAjax()( target )
				.done( function (response) {
					// Set our content
					content = response;
					// Let everyone know
					deferred.resolve();
					// Set a cache for this URL
					if( options.cache ){
						ips.ui.hovercard.setCache( target, content );
					}
				})
				.fail( function (jqXHR, status, errorThrown) {

					if( Debug.isEnabled() ){
						if( status != 'abort' ){
							Debug.error( "Ajax request failed (" + status + "): " + errorThrown );
						} else {
							Debug.warn("Ajax request aborted");
						}

						_removeLoadingWidget();
						deferred.reject();
					} else {
						if( status != 'abort' ){
							content = $('<div/>').addClass('ipsPad_half ipsType_light').html( ips.getString('errorLoadingContent') );
							deferred.resolve();
						} else {
							deferred.reject();
						}						
					}					
				})
				.always( function () {
					_removeLoadingWidget();
				});

			return deferred.promise();
		},

		/**
		 * Builds a little loading hovercard, before we replace it with the full card
		 *
		 * @returns 	{void}
		 */
		_buildLoadingWidget = function () {

			if( !options.showLoading ){
				return;
			}

			// Create loading dom node
			loading = $('<div/>').addClass('ipsHovercard_loading').html( ips.templates.render('core.hovercard.loading') );

			// Add it to our main container
			ips.getContainer().append( loading );

			// Get the dimensions of it
			var loadingDims = { width: loading.width(), height: loading.height() };

			// And hide it
			loading.hide();

			// Get the real position of our target
			var elemPos = ips.utils.position.getElemPosition( target ),
				dimsToUse = ( elemPos.fixed ) ? 'fixedPos' : 'absPos';

			loading.css( {
				left: elemPos[ dimsToUse ].left + 'px',
				top: ( elemPos[ dimsToUse ].top - loadingDims.height - 10 ) + 'px',
				position: ( elemPos.fixed ) ? 'fixed' : 'absolute',
				zIndex: "50000"
			});

			ips.utils.anim.go( 'fadeIn', loading );
		},

		/**
		 * Removes the loading hovercard
		 *
		 * @returns 	{promise}
		 */
		_removeLoadingWidget = function () {
			if( loading && loading.length ){
				loading.remove();
			}
		},

		/**
		 * Event handler for mouseout of the target
		 *
		 * @param 	{event} 	e 	The event object
		 * @returns {void}
		 */
		_mouseOut = function () {

			// Stop waiting for this
			clearTimeout( onTimeout );

			// Abort the Ajax request if necessary
			if( ajaxObj ){
				ajaxObj.abort();
			}

			// Remove the loading thingy if it exists
			_removeLoadingWidget();

			if( card && card.is(':visible') ){
				offTimeout = setTimeout( _hideCard, options.timeout * 1000 );
			}

			// Remove mouseout event
			$( elem ).off('.hovercard', _mouseOut);
		},

		/**
		 * Event handler for mouseover of the hovercard
		 *
		 * @param 	{event} 	e 	The event object
		 * @returns {void}
		 */
		_cardMouseOver = function () {
			clearTimeout( offTimeout );
		},

		/**
		 * Event handler for mouseout of the hovercard
		 *
		 * @returns 	{event} 	e 	The event object
		 * @returns 	{void}
		 */
		_cardMouseOut = function () {
			clearTimeout( offTimeout );
			offTimeout = setTimeout( _hideCard, options.timeout * 1000 );
		};

		init();

		return {
			init: init,
			destruct: destruct,
			start: start
		};
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.infiniteScroll.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.infiniteScroll.js - Infinite scrolling widget
 * Loads new content into the bottom of the container when the user approaches the bottom
 * Infinite scrolling can be a real usability problem if used in the wrong place. Please use responsibly ;)
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.infiniteScroll', function(){

		var defaults = {
			distance: 50,
			loadingTpl: 'core.infScroll.loading',
			scrollScope: window,
			pageParam: 'page',
			pageBreakTpl: 'core.infScroll.pageBreak',
			totalPages: null,
			disableIn: 'phone'
		};

		var respond = function (elem, options) {
			if( !$( elem ).data('_infinite') ){
				$( elem ).data('_infinite', infiniteScrollObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Retrieve the infinite scroll instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The dialog instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_infinite') ){
				return $( elem ).data('_infinite');
			}

			return undefined;
		};

		ips.ui.registerWidget( 'infScroll', ips.ui.infiniteScroll, [
			'container', 'scrollScope', 'distance', 'url', 'pageParam', 'loadingTpl',
			'pageBreakTpl', 'disableIn'
		] );

		/**
		 * Infinite scroll instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var infiniteScrollObj = function (elem, options) {

			var state = 'ready',
				scrollScope = null,
				container = null,
				ajaxObj = null,
				currentPage = 1;

			/**
			 * Initializes this Infinite Scroll instance
			 *
			 * @returns 	{void}
			 */
			var init = function () {
				container = $( options.container );
				scrollScope = $( options.scrollScope );				
				scrollScope.on( 'scroll', _scrollEvent );

				options.disableIn = options.disableIn.split(',');

				if( _.isString( options.distance ) && options.distance.indexOf('%') !== -1 ){
					var percent = parseInt( options.distance );
					options.distance = ( scrollScope.height() / 100 ) * percent;
				}
				
				if ( options.totalPages == null ) {
					options.totalPages = _getTotalPages();
				}

				currentPage = _getStartPage();

				elem.on( 'refresh.infScroll', _refresh );
			},

			/**
			 * Refreshes the data the infScroll widget uses
			 *
			 * @returns 	{void}
			 */
			_refresh = function () {
				options.totalPages = _getTotalPages();
				currentPage = _getStartPage();

				try {
					ajaxObj.abort();
				} catch (err) {}
			},

			/**
			 * Event handler for scrolling in the scroll scope element
			 * If we're within the 'distance' value from the bottom, load more results into the container
			 * Won't do anything if we're finished or loading, though
			 * 
			 * @param 		{event}	 	e 		Event object
			 * @returns 	{void}
			 */
			_scrollEvent = function (e) {
					
				// Only be concerned if we are working in this device
				if( ips.utils.responsive.enabled() && _.indexOf( options.disableIn, ips.utils.responsive.getCurrentKey() ) !== -1 ){
					return;
				}

				if( state == 'loading' || state == 'done' ){
					return;
				}

				if( currentPage >= _getTotalPages() ){
					return;
				}

				var distanceFromBottom = _getDistance();
				
				if( distanceFromBottom <= options.distance ){
					state = 'loading';
					_loadMoreResults();
				}
			},

			/**
			 * Fetches more results to display 
			 *
			 * @returns 	{void}
			 */
			_loadMoreResults = function () {
							
				_showLoadingElem();

				if( ajaxObj && ajaxObj.abort ){
					ajaxObj.abort();
				}
				
				ajaxObj = ips.getAjax()( _getPageURL( currentPage + 1 ) )
					.done( function (response) {
						currentPage++;
						_insertNewResults( response );
						state = 'ready';
						$( elem ).trigger( 'infScrollPageLoaded', {
							page: currentPage
						});
					})
					.fail( function () {

					})
					.always( function () {
						_removeLoadingElem();
					});
			},

			/**
			 * Inserts new results into the container
			 * 
			 * @param 		{string}	 	response 		Response from ajax request
			 * @returns 	{void}
			 */
			_insertNewResults = function (response) {
				var output = '';

				if( options.pageBreakTpl ){
					output += ips.templates.render( options.pageBreakTpl, {
						page: currentPage
					});
				}

				output += response;

				// count how many children container *currently* has
				var oldChildLength = container.children().length;

				// append new results
				container.append( output );

				// Now trigger content change on only the new items
				container.children().slice( oldChildLength ).each( function (child) {
					$( document ).trigger( 'contentChange', [ $( this ) ] );
				});				
			},

			/**
			 * Appends the loading row to the container
			 *
			 * @returns 	{void}
			 */
			_showLoadingElem = function () {
				container.append( ips.templates.render( options.loadingTpl ) );
			},

			/**
			 * Removes the loading row from the container
			 *
			 * @returns 	{void}
			 */
			_removeLoadingElem = function () {
				container.find('[data-role="infScroll_loading"]').remove();
			},

			/**
			 * Works out the distance remaining in the scroll scope, in pixels
			 * Different logic is used depending on whether the scope is the body, or an overflow'd element
			 *
			 * @returns 	{void}
			 */
			_getDistance = function () {

				if( options.scrollScope == window ){
					var scrollHeight = $( document ).height();
					var distanceFromBottom = scrollHeight - $( window ).height() - $( window ).scrollTop();
				} else {
					var scrollHeight = scrollScope[0].scrollHeight;
					var distanceFromBottom = scrollHeight - scrollScope.height() - scrollScope.scrollTop();
				}

				return distanceFromBottom;
			},

			/**
			 * Builds a page url with the given page number
			 * 
			 * @param 		{number}	pageNo 		Page number
			 * @returns 	{string}	Query string
			 */
			_getPageURL = function (pageNo) {
				var url = elem.attr('data-ipsInfScroll-url');
				return url + ( url.match(/\?/) ? '&' : '?' ) + options.pageParam + '=' + parseInt( pageNo );
			},

			/**
			 * Returns the current/starting page number based on the currently-active item from pagination
			 * 
			 * @returns 	{number}
			 */
			_getStartPage = function () {
				var paginationElem = elem.find('.ipsPagination').first();

				if( !paginationElem.length ){
					return 1;
				}

				var activePage = paginationElem.find('.ipsPagination_active').attr('data-page');

				if( !activePage ){
					return 1;
				} else {
					return parseInt( activePage );
				}
			},

			/**
			 * Returns the total number of pages based on the value provided in the pagination HTML
			 * 
			 * @returns 	{number}
			 */
			_getTotalPages = function () {				
				var paginationElem = elem.find('.ipsPagination').first();

				if( !paginationElem.length ){
					return 1;
				}

				var totalPages = paginationElem.attr('data-pages');

				if( !totalPages ){
					return 1;
				} else {
					return parseInt( totalPages );
				}
			};

			init();

			return {
				init: init
			};
		};

		return {
			respond: respond
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.js" javascript_type="ui" javascript_version="107643" javascript_position="1000349"><![CDATA[/* global ips, _, Debug */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.js - UI widget parent
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui', function(){

		var widgets = {}, // Registry for our widgets
			doneInitialInit = false, // Have we done the initial DOM setup on page load yet?
			ziCounter = ips.getSetting('zindex_start') || 5000, // Counter for zIndex incrementing
			ziIncrement = ips.getSetting('zindex_inc') || 50; // Increment for zindex

		// Set some keycodes as 'constants'
		var key = {
			BACKSPACE: 8,
			ESCAPE: 27,
			TAB: 9,
			LEFT: 37,
			RIGHT: 39,
			UP: 38,
			DOWN: 40,
			ENTER: 13,
			COMMA: 188,
			SPACE: 32
		};

		/**
		 * Register a widget module. Widget modules are for interface items, and work with
		 * a dataAPI that looks for data- options in HTML.
		 *
		 * @param	{string} 	widgetId 		The ID of this widget, which also forms its dataAPI key
		 * @param 	{array} 	acceptedOptions	Array of option keys that will be accepted by this widget
		 * @param 	{object}	widgetOptions 	Options to change the way this widget is registered/executed
		 * @param 	{function} 	fnCallback		The callback function when a widget is found on the page
		 * @returns {void}
		 */
		var	registerWidget = function (widgetID, handler, acceptedOptions, widgetOptions, fnCallback) {

			widgetOptions = _.defaults( widgetOptions || {}, {
				'lazyLoad': false, // Whether to only init this widget when necessary
				'lazyEvents': '', // The event to watch for to trigger lazy init
				'makejQueryPlugin': true
			});

			if( widgets[ widgetID ] ){
				Debug.warn( "'" + widgetID + "' is already registered as a widget. Skipping...");
			}

			widgets[ widgetID ] = { handler: handler, callback: fnCallback,
				acceptedOptions: acceptedOptions || [], widgetOptions: widgetOptions || {} };

			if( widgetOptions.makejQueryPlugin !== false ){
				buildjQueryPlugin( widgetID, handler, acceptedOptions, widgetOptions, fnCallback );
			}

			//Debug.info("Registered widget " + widgetID);
		},

		/**
		 * Returns an array of the options accepted for the given widget
		 *
		 * @param	{string} 	widgetId 		The ID of this widget, which also forms its dataAPI key
		 * @returns {array}
		 */
		getAcceptedOptions = function (widgetID) {
			return widgets[ widgetID ]['acceptedOptions'] || [];
		},

		/**
		 * Adds the provided widget as a jQuery plugin, enabling it to be instantiated programatically
		 * e.g. $('selector').ipsMenu({ options });
		 *
		 * @param	{string} 	widgetId 		The ID of this widget, which also forms its dataAPI key
		 * @param 	{array} 	acceptedOptions	Array of option keys that will be accepted by this widget
		 * @param 	{object}	widgetOptions 	Options to change the way this widget is registered/executed
		 * @param 	{function} 	fnCallback		The callback function when a widget is found on the page
		 * @returns {void}
		 */
		buildjQueryPlugin = function ( widgetID, handler, acceptedOptions, widgetOptions ) {

			var jQueryKey = widgetOptions.jQueryKey || 'ips' + widgetID.charAt(0).toUpperCase() + widgetID.slice(1),
				dataID = 'ips' + widgetID;

			if( $.fn[ jQueryKey ] ){
				Debug.warn("jQuery plugin '" + jQueryKey + "' already exists.");
				return;
			}

			$.fn[ jQueryKey ] = function (providedOptions) {

				this.each( function () {
					var elem = $( this );

					if( elem.attr( dataID ) ){
						removeExistingWidget( widgetID, this );
					}

					// Add the main widget attr
					elem.attr('data-' + dataID, '');

					// Add each option
					$.each( providedOptions, function (key, value) {
						if( _.indexOf( acceptedOptions, key ) !== false ){
							elem.attr('data-' + dataID + '-' + key, value);
						}
					});

					if( widgetOptions.lazyLoad === false ){
						_callWidget( widgetID, elem, _getWidgetOptions( widgetID, elem ) );
					}
				});

			};

			//Debug.log( 'Created $.fn.' + jQueryKey + ' jQuery plugin' );
		},

		/**
		 * Register a widget module. Widget modules are for interface items, and work with
		 * a dataAPI that looks for data- options in HTML.
		 *
		 * @param	{element} 	context 	The dom node that will be searched for widgets
		 * @param	{boolean} 	forceInit 	Force init widgets even if already initialized?
		 * @returns {void}
		 */
		_initializeWidgets = function (context /*, forceInit*/) {

			var immediateWidgets = [],
				lazyWidgets = [];

			if( !_.isElement( context ) ){
				context = document;
			}

			// Get all the widgets we'll attempt to load now
			_.each( widgets, function (item, key) {
				if( _.isUndefined( item.widgetOptions.lazyLoad ) || item.widgetOptions.lazyLoad === false ){
					immediateWidgets.push( key );
				} else {
					lazyWidgets.push( key );
				}
			});

			_doImmediateWidgets( immediateWidgets, context );

			// Lazy widgets only get set up once since they use delegated events
			if( !doneInitialInit ){
				_doLazyWidgets( lazyWidgets, context );
				doneInitialInit = true;
			}
		},

		destructAllWidgets = function (context) {
			var widgetIDs = _.keys( widgets );

			// Builds a selector that finds all of our widgets
			var selector = _.map( widgetIDs, function (item) {
				return "[data-ips" + item + "]";
			});

			// This is an expensive selector, so only do it once if possible.
			// We get all dom nodes that match any of our widgets, then we'll match them
			// up and fire off their respond methods
			var foundWidgets = $( context ).find( selector.join(',') );

			// Now we've found our widgets, we can set them up
			foundWidgets.each( function (idx, elem) {
				elem = $( elem );

				for( var i=0; i < widgetIDs.length; i++ ){
					if( !_.isUndefined( elem.attr( 'data-ips' + widgetIDs[i] ) ) ){
						_destructWidget( widgetIDs[i], elem );
					}
				}
			});
		},

		/**
		 * Calls the destruct method on a widget
		 *
		 * @param	{string} 	widgetID 	ID of the widget to destruct
		 * @param	{element} 	elem 		Element on which the widget exists
		 * @returns {void}
		 */
		_destructWidget = function (widgetID, elem) {
			if( _.isFunction( widgets[ widgetID ].handler.destruct ) ){
				try {
					widgets[ widgetID ].handler.destruct.call( widgets[ widgetID ].handler, elem );
				} catch (err) {
					Debug.error("Error calling destruct on " + widgetID );
					Debug.error( err );
				}
			}
		},

		/**
		 * Sets up immediately-initialized widgets
		 *
		 * @param	{array} 	widgetsToLoad 	Keys of those widgets to initialize immediately
		 * @param	{element} 	context 		The dom node that will be searched for widgets
		 * @returns {void}
		 */
		_doImmediateWidgets = function (widgetsToLoad, context) {

			if( !widgetsToLoad.length ){
				return;
			}

			// We'll create another var that this time contains the format needed for a css selector
			var selector = _.map( widgetsToLoad, function (item) {
				return "[data-ips" + item + "]";
			});

			// This is an expensive selector, so only do it once if possible.
			// We get all dom nodes that match any of our widgets, then we'll match them
			// up and fire off their respond methods
			var foundWidgets = $( context ).find( selector.join(',') );

			// Now we've found our widgets, we can set them up
			foundWidgets.each( function (idx, elem) {
				elem = $(elem);

				for( var i=0; i < widgetsToLoad.length; i++ ){
					if( !_.isUndefined( elem.attr( 'data-ips' + widgetsToLoad[i] ) ) ){
						_callWidget( widgetsToLoad[i], elem, _getWidgetOptions( widgetsToLoad[i], elem ) );
					}
				}
			});
		},

		/**
		 * Sets up events for lazily-loaded widgets
		 *
		 * @param	{array} 	widgetsToLoad 	Keys of those widgets to initialize immediately
		 * @param	{element} 	context 		The dom node that will be searched for widgets
		 * @returns {void}
		 */
		_doLazyWidgets = function (widgetsToLoad) {

			if( !widgetsToLoad.length ){
				return;
			}

			for( var i=0; i < widgetsToLoad.length; i++ ){
				var lazyEvents = widgets[ widgetsToLoad[i] ].widgetOptions.lazyEvents;

				if( !lazyEvents ){
					lazyEvents = 'click';
				}

				$( document ).on( lazyEvents, "[data-ips" + widgetsToLoad[i] + "]", _.partial( function (widgetKey, e) {
					_callWidget( widgetKey, this, _getWidgetOptions( widgetKey, this ), e );
				}, widgetsToLoad[i] ) );
			}

		},

		/**
		 * Calls a widget callback. If a callback is provided, that will be called. If not, we look
		 * for a 'respond' method on the handler and call that instead.
		 *
		 * @param	{string} 	widgetID 	The ID of the widget being processed
		 * @param	{element} 	elem 		The element being passed through
		 * @params 	{object} 	options 	The widget options being passed through
		 * @params 	{event} 	e 			Event object that may be passed for lazy-load widgets
		 * @returns {void}
		 */
		_callWidget = function (widgetID, elem, options, e) {
			if( _.isFunction( widgets[ widgetID ].callback ) ){
				widgets[ widgetID ].callback.call( widgets[ widgetID ].handler, elem, options, e );
			} else if( _.isFunction( widgets[ widgetID ].handler.respond ) ){
				widgets[ widgetID ].handler.respond.call( widgets[ widgetID ].handler, elem, options, e );
			} else {
				Debug.error("No callback method specified for " + widgetID);
			}
		},

		/**
		 * Calls a widget callback. If a callback is provided, that will be called. If not, we look
		 * for a 'respond' method on the handler and call that instead.
		 *
		 * @param	{string} 	widgetID 	The ID of the widget being processed
		 * @param	{element} 	elem 		The element being passed through
		 * @params 	{object} 	options 	The widget options being passed through
		 * @returns {void}
		 */
		_getWidgetOptions = function (widgetID, elem) {

			var options = {},
				optionKeys = widgets[ widgetID ].acceptedOptions;

			elem = $( elem );

			// First let's see there's a full options object waiting for us
			try {
				if( elem.attr('data-ips' + widgetID + '-options') ){
					var optionsObj = $.parseJSON( elem.attr('data-ips' + widgetID + '-options') );

					if( _.isObject( optionsObj ) ){
						return optionsObj;
					}
				}
			} catch(err) {
				Debug.warn("Invalid options object passed in for a " + widgetID + " widget. Must be valid JSON.");
			}

			// Loop through each option this widget will accept in order to see whether
			// that option exists on this element.
			if( optionKeys.length ){
				for( var i=0; i < optionKeys.length; i++ ){
					var thisOption = elem.attr( 'data-ips' + widgetID + '-' + optionKeys[i] );

					if( !_.isUndefined( thisOption ) ){

						// Try and correct numbers
						if( thisOption.match(/^[1-9][0-9]*$/g) ){
							// Don't match numbers with leading zeros
							// We do this since parsing as an int will strip leading zeros. We'll assume that if a
							// leading zero exists, it's intentional, so skip this to treat it as a string instead.
							thisOption = parseInt( thisOption, 10 );
						}

						// And try and cast booleans
						if( thisOption === 'true' ){
							thisOption = true;
						} else if( thisOption === 'false' ){
							thisOption = false;
						}

						// If no value is supplied, treat it as true
						if( typeof thisOption === 'string' && thisOption.trim() === '' ){
							thisOption = true;
						}

						options[ optionKeys[i] ] = thisOption;
					}
				}
			}

			return options;
		},

		/**
		 * Returns the next zIndex value
		 *
		 * @returns 	{number}
		 */
		zIndex = function () {
			ziCounter += ziIncrement;
			// jQuery 3.5+ requires a string for the $.css method, and since that's primarily 
			// what this method is used for, it makes more sense to do the cast here
			return String(ziCounter);
		},

		/**
 		 * Returns the modal element, building it if necessary
		 *
		 * @returns 	{element}
		 */
		getModal = function () {

			return $('<div/>')
						.addClass( 'ipsModal' )
						.hide()
						.appendTo( $('body') )
						.identify();
		},

		/**
 		 * Initialize ips.ui
		 *
		 * @returns 	{void}
		 */
		init = function () {
			// Listen for content change
			$( document ).on('contentChange', function (e, newContent) {

				// Initialize widgets; if we're passed a jQuery collection, loop through each
				if( newContent instanceof jQuery ){
					newContent.each( function () {
						if( Debug.isEnabled ){
							Debug.info("contentChange event, reinitializing widgets in " + $( this ).identify().attr('id') );
						}
						_initializeWidgets( this );
					});
				} else {
					if( Debug.isEnabled ){
						Debug.info("contentChange event, reinitializing widgets in " + $( newContent ).identify().attr('id') );
					}
					_initializeWidgets( newContent );
				}

				// Rerun prettyprint
				if (typeof PR != 'undefined') {
					PR.prettyPrint();
				}
			});

			_initializeWidgets( document );

		};

		return {
			registerWidget: registerWidget,
			init: init,
			zIndex: zIndex,
			getModal: getModal,
			getAcceptedOptions: getAcceptedOptions,
			key: key,
			destructAllWidgets: destructAllWidgets
		};
	});

}(jQuery, _));
]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.lazyLoad.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.lazyLoad.js - Widget that will find lazy loaded elements and begin observing them
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.createModule('ips.ui.lazyLoad', function(){

		/**
		 * Responder for lazyLoad widget
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{event} 	e 		 	The event object passed through
		 * @returns {void}
		 */
		var respond = function (elem, options, e) {
			ips.utils.lazyLoad.observe( elem );	
		};
		
		// Register this widget with ips.ui
		ips.ui.registerWidget( 'lazyLoad', ips.ui.lazyLoad );

		return {
			respond: respond
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.lightbox.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.lightbox.js - Lightbox component
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.lightbox', function(){

		var defaults = {
			className: 'ipsLightbox',
			useEvents: false
		};

		var currentLightbox;

		var respond = function (elem, options, e) {
			options = _.defaults( options, defaults );
			currentLightbox = new lightboxObj( elem, options, e );
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			if( currentLightbox ){
				currentLightbox.destruct();
				currentLightbox = null;
			}
		};

		ips.ui.registerWidget('lightbox', ips.ui.lightbox, 
			[ 'group', 'commentsURL', 'className', 'preload', 'useEvents' ],
			{ lazyLoad: true, lazyEvents: 'click' }
		);

		return {
			respond: respond,
			destruct: destruct
		};
	});


	/**
	 * Lightbox instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var lightboxObj = function (elem, options, e) {
		
		if( e ){
			e.preventDefault();
		}

		var imageCollection = [],
			commentsAjax,
			modal, 
			pieces, 
			currentImage,
			phoneBreakpoint = false;

		/**
		 * Kick off showing the lightbox
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			// Blur the trigger
			elem.blur();
			
			_getAllImages();
			_buildModal();
			_buildWrapper();
			_setUpEvents();
			_show();
			_loadFirstImage();
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function () {
			$( window ).off( 'resize', _resize );
			$( document ).off( 'keydown', _keyPress );
			modal.off( 'click', close );
		},

		/**
		 * Sets up the events we'll need to watch for, on the modal, lightbox and doc
		 *
		 * @returns 	{void}
		 */
		_setUpEvents = function () {

			// Lightbox events
			pieces.lightbox
				.on( 'click', '.' + options.className + '_next', nextImage )
				.on( 'click', '.' + options.className + '_prev', prevImage )
				.on( 'click', '.' + options.className + '_close', close )
				.on( 'click', clickedLightbox )
				.on( 'click', '[data-action="rotateImage"]', _rotateImage );

			// Modal events
			modal.on( 'click', close );

			// Handle window resizing
			$( window ).on( 'resize', _resize );

			// Document key events
			$( document ).on( 'keydown', _keyPress );

			// Monitor for content changes that we might need to act on
			$( document ).on( 'imageUpdated', _updateImage );
			$( document ).on( 'imageLoading', _mainImageLoading );

			// If we are using events for managing the lightbox, listen for those events
			if( options.useEvents ){
				$( document ).on( 'lightboxDisable_next', function() {
					$('.' + options.className + '_next').hide();
				});

				$( document ).on( 'lightboxDisable_prev', function() {
					$('.' + options.className + '_prev').hide();
				});

				$( document ).on( 'lightboxEnable_next', function() {
					$('.' + options.className + '_next').show();
				});

				$( document ).on( 'lightboxEnable_prev', function() {
					$('.' + options.className + '_prev').show();
				});
			}
		},

		/**
		 * Image needs to be updated event
		 *
		 * @returns 	{void}
		 */
		_mainImageLoading = function( e ) {
			_setLoading( true );

			pieces.imagePanel
				.find('.' + options.className + '_image ')
					.hide();
		},

		/**
		 * Image needs to be updated event
		 *
		 * @returns 	{void}
		 */
		_updateImage = function( e, data ) {
			if( data.closeLightbox === true )
			{
				close(e);
			}
			else if( data.updateImage )
			{
				_showImage( data.updateImage );
			}
		},

		/**
		 * Window resize event
		 *
		 * @returns 	{void}
		 */
		_resize = function (e) {
			if( pieces.lightbox && pieces.imagePanel ){
				if( pieces.imagePanel.find( '.' + options.className + '_image' ).length ){
					_positionCenter( pieces.imagePanel.find( '.' + options.className + '_image:visible' ) );
				}
			}
		},

		/**
		 * Handles a keydown event
		 *
		 * @returns 	{void}
		 */
		_keyPress = function (e) {
			if( !pieces.lightbox.is(':visible') ){
				return;
			}

			switch( e.keyCode ){
				case ips.ui.key.ESCAPE:
					close(e);
				break;
				case ips.ui.key.RIGHT:
					nextImage(e);
				break;
				case ips.ui.key.LEFT:
					prevImage(e);
				break;
			}
		},

		/**
		 * Retrieves the image that was clicked from imageCollection, then passes it to showImage
		 *
		 * @returns 	{void}
		 */
		_loadFirstImage = function () {
			// Find the image that was clicked
			var firstImage = function () {
				for( var i = 0; i < imageCollection.length; i++ ){
					if( imageCollection[ i ].elem == elem ){
						return imageCollection[ i ];		
					}
				}
			}();

			currentImage = firstImage;
			_showImage( firstImage );
		},

		/**
		 * Handles the process of showing a new image, including loading the image and comments,
		 * determining whether next/prev should show, updating meta data
		 *
		 * @param	{object} 	image 		The image data object from imageCollection, for this image
		 * @returns {void}
		 */
		_showImage = function (image) {
			_setLoading( true );

			pieces.imagePanel
				.find('.' + options.className + '_image ')
					.hide();

			if( image.imageElem ){

				// Hide all images
				pieces.imagePanel.find('.' + options.className + '_image ').hide()

				// Show this image, then hand it off to the event handler
				var thisImage = image.imageElem.css( { opacity: "0" } ).show();

				_imageLoaded( thisImage );
			} else {	

				// New image, so build it and set the event handler
				var thisImage = image.imageElem = $('<img/>')
										.attr( 'src', image.largeImage )
										.addClass( options.className + '_image' )
										.css( { opacity: "0" } )
										.imagesLoaded( function (imagesLoaded){
											try {
												_imageLoaded( $( imagesLoaded.images[0].img ) );
											} catch(err) {
												Debug.error("Error loading image");
											}
										});

				if ( ! _.isUndefined( $(image.elem).attr('data-fileId') ) ) {
					thisImage.attr( 'data-fileId', $(image.elem).attr('data-fileId') );
				}
				
				// Hide all images, and append this new one
				pieces.imagePanel
					.find('.' + options.className + '_image ')
						.hide()
					.end()
					.append(
						thisImage
					);
			}

			// rotate the image if we need to
			var rotatedImage = $( '.ipsAttachLink_image img[data-rotate][data-fileId=\'' + thisImage.attr( 'data-fileId' ) + '\']' );
			if ( rotatedImage.length ){
				thisImage.attr( 'data-rotate', $( rotatedImage ).attr( 'data-rotate' ) );
				_applyRotation( thisImage, $( rotatedImage ).attr( 'data-rotate' ) );
			}
			
			// Full size link
			pieces.fullSize.attr( 'href', image.largeImage );

			// Handle comments
			if( image.commentsURL ){
				_loadComments( image );
			} else {
				_hideCommentsPanel();
			}

			// Build meta info
			if( image.meta ){
				pieces.metaPanel
					.show()
					.html( ips.templates.render('core.lightbox.meta', { title: image.largeImage } ) );
			} else {
				pieces.metaPanel.hide();
			}

			$( elem ).trigger( 'lightboxImageShown', {
				image: image,
				triggerElem: elem
			});
		},

		/**
		 * Loads remote comments into the lightbox
		 *
		 * @param	{object} 	image 		The image data object from imageCollection, for this image
		 * @returns {void}
		 */
		_loadComments = function (image) {

			// Abort anything running already
			if( commentsAjax ){
				Debug.warn("Aborting comment load");
				commentsAjax.abort();
			}

			// Get new ajax object
			pieces.commentsPanel
				.html('')
				.show()
				.addClass( 'ipsLoading' );

			pieces.imagePanel
				.addClass( options.className + '_withComments' );

			commentsAjax = ips.getAjax()( image.commentsURL )
				.done( function (response){
					pieces.commentsPanel
						.html( response )
						.removeClass( 'ipsLoading' );

					$( document ).trigger('contentChange', [ pieces.commentsPanel ]);

					$( elem ).trigger( 'lightboxCommentsLoaded', {
						image: image,
						triggerElem: elem,
						commentsArea: pieces.commentsPanel
					});
				});
		},

		/**
		 * Hides the comments panel
		 *
		 * @returns 	{void}
		 */
		_hideCommentsPanel = function () {
			pieces.commentsPanel.hide();
			pieces.imagePanel.removeClass( options.className + '_withComments' );
		},

		/**
		 * Shows and hides the loading widget on the lightbox
		 *
		 * @param	{boolean} 	status 		True to show, false to hide
		 * @returns {void}
		 */
		_setLoading = function (status) {
			if( status === true ){
				pieces.imagePanel.addClass( 'ipsLoading ipsLoading_dark' );
			} else {
				pieces.imagePanel.removeClass( 'ipsLoading ipsLoading_dark' );

				$( '.' + options.className + '_imagePanel > img, .' + options.className + '_fullSize' )
					.on( 'mouseover', function(){ $( '.' + options.className + '_fullSize' ).show(); } )
					.on( 'mouseout', function(){ $( '.' + options.className + '_fullSize' ).hide(); } );
			}
		},

		/**
		 * Event handler fired when an image has finished loading
		 *
		 * @param	{array} 	image 		Image that has loaded
		 * @returns {void}
		 */
		_imageLoaded = function (image) {
			image.css( { opacity: "1" } );
			_positionCenter( image );
			_setLoading( false );
			_setButtons( image );
			
			// If we are using events, we can return now and let the events handle the rest
			if( options.useEvents )	{
				return;
			}

			// Toggle the navigation buttons as needed
			if( imageCollection.length < 2 ){
				pieces.next.hide();
				pieces.prev.hide();
			} else {
				var curPos = _.indexOf( imageCollection, currentImage );

				pieces.next.show();
				pieces.prev.show();

				if( curPos == 0 ){
					pieces.prev.hide();	
				}

				if( curPos == ( imageCollection.length - 1 ) ){
					pieces.next.hide();
				}
			}
		},
		
		/**
		 * Adds any image buttons needed
		 *
		 * @param	{object} 	image 		The image data object from imageCollection
		 * @returns {void}
		 */
		_setButtons = function (image) {
			if( !_.isUndefined( image.attr( 'data-fileId' ) ) ){
				/*$( '.' + options.className + '_toolsPanel' ).html(
					ips.templates.render('core.lightbox.toolsMenu', {
						url: ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=attachments&do=rotate&id=' + image.attr('data-fileId')
					} )
				).show();*/

				// Inform the document
				$( document ).trigger( 'contentChange', [ $( '.' + options.className ) ] );
			}
		},
		
		/**
		 * Positions the image in the center of the image panel
		 *
		 * @param	{object} 	image 		The image data object from imageCollection
		 * @returns {void}
		 */
		_positionCenter = function (image) {
			// Get image size
			var imageSize = { width: image.width(), height: image.height() },
				panelSize = { width: pieces.imagePanel.width(), height: pieces.imagePanel.height() };

			Debug.log( "Dimensions: " + imageSize.width + " x " + imageSize.height );

			// Center it
			image.css( {
				left: '50%',
				marginLeft: '-' + Math.max( ( imageSize.width / 2 ), 0 ) + 'px',
				top: '50%',
				marginTop: '-' + Math.max( ( imageSize.height / 2 ), 0 ) + 'px'
			});

			if( pieces.fullSize )
			{
				pieces.fullSize.css( {
					left: '50%',
					marginLeft: '-' + Math.max( ( imageSize.width / 2 ), 0 ) + 'px',
					width: ( imageSize.width + 2 ) + 'px',
					top: '50%',
					height: ( imageSize.height + 2 ) + 'px',
					marginTop: '-' + Math.max( ( imageSize.height / 2 ), 0 ) + 'px',
					/* We take half of the height for padding-top, but we need to remove half of that again because of our negative margin-top.
						The added 50px represents half of the element's height (fa icon is 80px and the text is 20px) to provide the best center position */
					paddingTop: Math.max( ( imageSize.height / 2 ) - ( imageSize.height / 2 / 2 ) + 50, 0 ) + 'px'
				});
			}
		},

		/**
		 * A click on the lightbox
		 *
		 * @param	{event} 	e 		The event object
		 * @returns {void}
		 */
		clickedLightbox = function (e) {
			// Don't fire if we're inside an <a>
			if( $( e.target ).closest('a').length ){
				return;
			}

			// Get window width
			var width = $( document ).width();
			var halfPos = width / 2;

			// If we're clicking the right side of the screen, go forwards.
			// If we're clicking the left side of the screen, go backwards.
			// Otherwise, close the lightbox.
			if( e.pageX >= halfPos && pieces.next.is(':visible') ){
				pieces.next.click();
			} else if( e.pageX < halfPos && pieces.prev.is(':visible') ){
				pieces.prev.click();
			} else {
				close();
			}
		},

		/**
		 * Retrieves the next image and shows it
		 *
		 * @param	{event} 	e 		The event object
		 * @returns {void}
		 */
		nextImage = function (e) {
			e.preventDefault();
			e.stopPropagation();

			// If we are using events, we can return now and let the events handle the rest
			if( options.useEvents )
			{
				$( document ).trigger( 'lightboxNextImage' );
				return;
			}

			currentImage = _getNextImage();
			_showImage( currentImage );
		},

		/**
		 * Retrieves the previous image and shows it
		 *
		 * @param	{event} 	e 		The event object
		 * @returns {void}
		 */
		prevImage = function (e) {
			e.preventDefault();
			e.stopPropagation();

			if( options.useEvents )
			{
				$( document ).trigger( 'lightboxPrevImage' );
				return;
			}

			currentImage = _getPrevImage();
			_showImage( currentImage );
		},

		/**
		 * Returns the previous image from imageCollection
		 *
		 * @param	{event} 	e 		The event handler
		 * @returns {object} 	The previous image object
		 */
		_getPrevImage = function (e) {
			var curPos = _.indexOf( imageCollection, currentImage );

			if( curPos === 0 ){
				return imageCollection[ imageCollection.length - 1 ];
			}

			return imageCollection[ curPos - 1 ];
		},

		/**
		 * Returns the next image from imageCollection
		 *
		 * @param	{event} 	e 		The event handler
		 * @returns {object} 	The next image object
		 */
		_getNextImage = function () {
			var curPos = _.indexOf( imageCollection, currentImage );

			if( curPos == ( imageCollection.length - 1 ) ){
				return imageCollection[0];
			}

			return imageCollection[ curPos + 1 ];	
		},

		/**
		 * Rotates the image
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		_rotateImage = function (e) {
			e.preventDefault();

			var visibleImage = $( 'img.ipsLightbox_image:visible' );
			var url = $( e.currentTarget ).attr('href') + '&current=' + $( visibleImage ).attr( 'data-rotate' );

			ips.getAjax()( url, {
				showLoading: true
			} )
				.done( function (response) {

					/* Images on the page */
					$('img[data-fileId="' + response.fileId + '"]').each( function() {
						$( visibleImage ).attr( 'data-rotate', response.rotate );
						_applyRotation( this, response.rotate );

						/* Only trigger the imageRotated event if we permanently stored the rotation angle */
						if( response.saved == 1 ){
							$( document ).trigger( 'imageRotated', response );
						}
					} );
										
					ips.ui.flashMsg.show( response.message );
				})
				.fail( function () {
					window.location = url;
				});
		},

		/**
		 * Apply the transformation CSS to rotate the image properly
		 *
		 * @param	{object}	elem
		 * @param 	{string}	angle
		 * @returns	{void}
		 */
		_applyRotation = function( elem, angle ){
			$(elem).css( { 'transform': 'rotate(' + angle + 'deg)' } );

			/* If we are rotating the image on its side, adjust the width so that it fits in the panel */
			if( angle == '90' || angle == '-90' ) {
				var panelHeight = $( elem ).parents( '.ipsLightbox_imagePanel' ).height();
				$( elem ).css( {
					'max-width': ( panelHeight - 20 ).toString() + 'px'
				});
			}
		},

		/**
		 * Closes the lightbox
		 *
		 * @param	{event} 	e 		The event handler
		 * @returns {void}
		 */
		close = function (e) {
			if( e ){
				e.preventDefault();
				e.stopPropagation();	
			}
			
			$( document ).off( 'imageUpdated', _updateImage );
			$( document ).off( 'imageLoading', _mainImageLoading );

			modal.hide();
			pieces.lightbox.hide();
		},

		/**
		 * Displays the lightbox on-screen
		 *
		 * @returns 	{void}
		 */
		_show = function () {
			ips.utils.anim.go( 'fadeIn fast', modal );
			ips.utils.anim.go( 'fadeIn fast', pieces.lightbox );
			//pieces.lightbox.show();
		},

		/**
		 * Builds the lightbox UI
		 *
		 * @returns 	{void}
		 */
		_buildWrapper = function () {

			// Build pieces
			pieces = {
				lightbox: $('<div/>')
					.addClass( options.className )
					.css( { zIndex: ips.ui.zIndex() } ),

				imagePanel: $('<div/>')
					.addClass( options.className + '_imagePanel' ),

				commentsPanel: $('<div/>')
					.addClass( options.className + '_commentsPanel' )
					.html('')
					.hide(),
				
				toolsPanel: $('<div/>')
					.addClass( options.className + '_toolsPanel' )
					.html('')
					.hide(),
				
				next: $('<a/>')
					.addClass( options.className + '_next' )
					.html("<i class='fa fa-angle-right'></i>"),

				prev: $('<a/>')
					.addClass( options.className + '_prev' )
					.html("<i class='fa fa-angle-left'></i>"),

				close: $('<a/>')
					.addClass( options.className + '_close' )
					.html("&times;"),

				fullSize: $('<a/>')
					.attr( 'href', '#' )
					.attr( 'target', '_blank' )
					.addClass( options.className + '_fullSize' ),

				metaPanel: $('<div/>')
					.addClass( options.className + '_meta' )
					.hide()
			};

			// Assemble
			pieces.lightbox
				.append( 
					pieces.imagePanel
						.append( pieces.next )
						.append( pieces.prev )
						.append( pieces.fullSize )
						
				)
				.append( pieces.metaPanel )
				.append( pieces.commentsPanel )
				.append( pieces.toolsPanel )
				.append( pieces.close );

			$('body').append( pieces.lightbox );
		},

		/**
		 * Populates imageCollection with the images grouped with this lightbox
		 *
		 * @returns 	{void}
		 */
		_getAllImages = function () {
			
			if( options.group ){
				var images = $('[data-ipslightbox-group="' + options.group + '"]');
			} else {
				var images = $( elem );
			}

			$.each( images, function (i, thisElem) {
				imageCollection.push( _returnImageData( thisElem ) );
			});
		},

		/**
		 * Returns image data for the provided element
		 *
		 * @param	{element} 	thisElem 		The element being worked with
		 * @returns {object} 	Image data
		 */
		_returnImageData = function (thisElem) {

			var origImage,
				largeImage;

			if( thisElem.tagName != 'IMG' ){
				origImage = $( thisElem ).find('img').attr('src');
			} else {
				origImage = $( thisElem ).attr('src');
			}

			if( $( thisElem ).attr('data-fullURL') ){
				largeImage = $( thisElem ).attr('data-fullURL');
			} else if( thisElem.tagName == 'A' && $( thisElem ).attr('href') ){
				largeImage = $( thisElem ).attr('href');
			}

			return {
				elem: thisElem,
				originalImage: origImage,
				largeImage: largeImage || origImage,
				meta: $( thisElem ).attr('data-ipsLightbox-meta'),
				commentsURL: $( thisElem ).attr('data-ipsLightbox-commentsURL')
			};
		},

		/**
		 * Gets the modal element from ips.ui, and sets a new zIndex on it
		 *
		 * @returns 	{void}
		 */
		_buildModal = function () {
			modal = ips.ui.getModal();
			modal.css( { zIndex: ips.ui.zIndex() } );
		};

		init();

		return {
			destruct: destruct
		};
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.map.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.map.js - Interactive Map
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){

	ips.createModule('ips.ui.map', function(){
		
		var defaults = {
			zoom: 2,
			maxZoom: 16,
			markers: '[]',
			contentUrl: null
		};

		/**
		 * Respond to a map widget
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{event} 	e 		 	The event object
		 * @returns {void}
		 */
		var respond = function (elem, options, e) {
			options = _.defaults( options, defaults );
			
			var callback = function () {
				if( ips.getSetting('mapProvider') == 'google' ){
					_google( elem, options );
				} else if( ips.getSetting('mapProvider') == 'mapbox' ){
					_mapbox( elem, options );
				}
			};

			if( ips.getSetting('lazyLoadEnabled') ){
				ips.utils.lazyLoad.observe( elem, {
					loadCallback: callback
				});
			} else {
				callback();
			}
		};

		/**
		 * Handle Mapbox
		 *
		 * @returns {void}
		 */
		var _mapbox = function (elem, options) {
			$('head').append( "<link rel='stylesheet' type='text/css' media='all' href='https://api.mapbox.com/mapbox.js/v3.3.1/mapbox.css'><link href='https://api.mapbox.com/mapbox.js/plugins/leaflet-markercluster/v1.0.0/MarkerCluster.css' rel='stylesheet' /><link href='https://api.mapbox.com/mapbox.js/plugins/leaflet-markercluster/v1.0.0/MarkerCluster.Default.css' rel='stylesheet' />" );

			var handlePopup = function (e) {
				var popup = e.target.getPopup();
				var clubID = e.target.options.clubID;

				ips.getAjax()( options.contentUrl + clubID ).done( function (response) {
					popup.setContent( response );
					popup.update();
				});
			};

			ips.loader.get( [ 'https://api.mapbox.com/mapbox.js/v3.3.1/mapbox.js' ] ).then( function () {
				ips.loader.get( [ 'https://api.mapbox.com/mapbox.js/plugins/leaflet-markercluster/v1.0.0/leaflet.markercluster.js' ] ).then( function () {
					L.mapbox.accessToken = ips.getSetting('mapApiKey');
					var map = L.mapbox
						.map( elem.get(0) )
						.setView([45, 0], options.zoom)
						.addLayer(L.mapbox.styleLayer('mapbox://styles/mapbox/streets-v11'));

					var cluster = new L.MarkerClusterGroup();
					map.addLayer( cluster );

					var markers = $.parseJSON( options.markers );
					for ( var id in markers ) {
						var marker = L.marker([markers[id].lat, markers[id].long], {
							icon: L.mapbox.marker.icon({
								'marker-color': '#0000ff'
							}),
							clubID: id,
							title: markers[id].title,
							draggable: false
						});

						cluster.addLayer( marker );

						// Build info popup for this marker
						if( options.contentUrl ){
							marker.bindPopup( ips.getString('loading') );
							marker.on('click', handlePopup );
						}
					}

					// Center on markers
					map.fitBounds(cluster.getBounds().pad(0.5));
				});
			});
		};

		/**
		 * Handle google maps
		 *
		 * @returns {void}
		 */
		var _google = function (elem, options) {
			
			if ( typeof google === 'undefined' || typeof google.maps === 'undefined' ) {
				ips.loader.get( [ 'https://maps.googleapis.com/maps/api/js?key=' + ips.getSetting('mapApiKey') + '&libraries=places&sensor=false&callback=ips.ui.map.googleCallback' ] );
			} else {
				ips.ui.map.googleCallback();
			}
						
			$( window ).on( 'googleApiLoaded', function(){
				
				var mapOptions = {
					zoom: options.zoom,
					maxZoom: options.maxZoom,
					scrollwheel: false
				};
				if ( options.zoom ) {
					mapOptions.center = { lat: 45, lng: 0 };
				} else {
					mapOptions.center = { lat: 30, lng: 0 };
				}
				
				var map = new google.maps.Map( elem.get(0), mapOptions);
				var bounds = new google.maps.LatLngBounds();
				
				var infowindow = new google.maps.InfoWindow({
					content: ips.getString('loading')
				});
				
				var markers = $.parseJSON( options.markers );
				for ( var id in markers ) {
															
					var marker = new google.maps.Marker({
					  position: { lat: markers[id].lat, lng: markers[id].long },
					  map: map,
					  title: markers[id].title,
					  id: id
					});
					
					if ( options.contentUrl ) {
						marker.addListener('click', function() {	
							
							infowindow.setContent( ips.getString('loading') )					
							infowindow.open(map,this);
							
							ips.getAjax()( options.contentUrl + this.id ).done(function(response){
								infowindow.setContent( response );
							});

						});
					}

					// Increase bounds to include this marker
					bounds.extend(marker.position);
				}



				//Center on markers
				map.fitBounds(bounds);
			});
		};
		
		/**
		 * Provides a callback for Google Maps API
		 *
		 * @returns {void}
		 */
		var googleCallback = function(){
			$( window ).trigger( 'googleApiLoaded' );
		};
		
		ips.ui.registerWidget('map', ips.ui.map, [ 'zoom', 'maxZoom', 'markers', 'contentUrl' ] );

		return {
			respond: respond,
			googleCallback: googleCallback
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.menu.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.menu.js - Menu component
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){

	ips.createModule('ips.ui.menu', function(){

		var defaults = {
			className: 'ipsMenu',
			activeClass: '',
			closeOnClick: true,
			closeOnBlur: true,
			selectable: false,
			withStem: true,
			stemOffset: 25, // half the stem width
			stopLinks: false,
			above: 'auto'
		};

		var stems = ['topLeft', 'topRight', 'topCenter', 'bottomLeft', 'bottomRight', 'bottomCenter'];

		if( !defaults.withStem ){
			defaults.stemOffset = 0;
		}

		var _menuRegistry = {};

		/**
		 * Respond to a menu trigger being clicked
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{event} 	e 		 	The event object
		 * @returns {void}
		 */
		var respond = function (elem, options, e) {
			e.preventDefault();

			var elemID = $( elem ).identify().attr('id'),
				options = _.defaults( options, defaults );

			if( $( elem ).attr('data-disabled') || $( elem ).is(':disabled') ){
				return;
			}

			if( !$( elem ).data('_menuBody') ){
				var menu = _getMenu(elem, elemID, options);
				$( elem ).data('_menuBody', menu);
			} else {
				var menu = $( elem ).data('_menuBody');
			}

			if( !menu.length ){
				Debug.warn( "Couldn't find or build a menu for " + elemID );
				return;
			}

			$( window ).on( 'resize', function (e) {
				if( menu.is(':visible') ){
					menu.hide();
					_positionMenu( elem, elemID, options, menu, true );
					menu.show();
				} 				
			});

			// Show or hide the menu depending on visibility
			if( !menu.is(':visible') ){
				_showMenu( elem, elemID, options, menu, e );
			} else {
				_hideMenu( elem, elemID, options, menu, false );
			}
		},

		/**
		 * Shows the menu, and sets necessary events
		 *
		 * @param	{element} 	elem 		The element this widget belongs to
		 * @param	{string} 	elemID 		ID of the trigger element
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{element} 	menu 		The menu itself
		 * @param	{event} 	e 			The original event
		 * @returns {void}
		 */
		_showMenu = function ( elem, elemID, options, menu, e ) {
			Debug.log(options);
			if( options.closeOnBlur ){
				$( document ).on('click.' + elemID, _.partial( _closeOnBlur, elem, menu ) );
			}

			$( menu )
				.on( 'closeMenu', _.partial( _hideMenu, elem, elemID, options, menu, false ) )
				.on( 'mouseenter', '.ipsMenu_subItems', _.bind( _showSubMenu, this, elem, elemID, options, menu ) );
				

			$( elem ).on( 'closeMenu', _.partial( _hideMenu, elem, elemID, options, menu, false ) ); 

			// Move it into place
			_positionMenu( elem, elemID, options, menu );

			// Show menu
			menu.show();

			// Add active class to trigger
			$( elem ).addClass( options.activeClass );
			$( elem ).trigger('menuOpened', { 
				elemID: elemID,
				originalEvent: e,
				menu: menu
			});
		},

		/**
		 * Shows a submenu
		 *
		 * @param	{element} 	elem 		The element this widget belongs to
		 * @param	{string} 	elemID 		ID of the trigger element
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{element} 	menu 		The menu itself
		 * @param	{event} 	e 			The original event
		 * @returns {void}
		 */
		_showSubMenu = function ( elem, elemID, options, menu, e ) {
			var menuItem = $( e.currentTarget ).find('> a');
			var subMenu = menuItem.next('.ipsMenu');

			// Set the mouseleave event on this item
			$( e.currentTarget ).on( 'mouseleave', _.bind( _hideSubMenu, this, elem, elemID, options, menu ) );

			// Try and position the menu
			var itemPosition = ips.utils.position.getElemPosition( menuItem );
			var itemSize = ips.utils.position.getElemDims( menuItem );
			var subMenuSize = ips.utils.position.getElemDims( subMenu );
			
			// If we're in RTL, we should open to the left #attentionToDetail
			if( $('html').attr('dir') == 'rtl' ) {
				
				var left = ( itemSize.outerWidth - 5 );
				var top = menuItem.position()['top'] - 5;

				// If the submenu won't fit to the left of the item...
				if( ( itemPosition.viewportOffset.left - subMenuSize.outerWidth - 5 ) < 0 ){
					// ... See if it will fit to the right of the item...
					if( ( itemPosition.viewportOffset.right + 5 + subMenuSize.outerWidth ) <= $( window ).width() ){
						left = ( itemSize.outerWidth + 5 );
					} else {
						// ... but if not, position it *under* the item.
						// Since the submenu is pos relative to the parent item, we need to subtract the parent item pos from the ideal left pos
						left = 15 - itemPosition.absPos.left; 
						top = menuItem.position()['top'] + itemSize.height + 15;
					}
				}
	
				subMenu
					.css({
						left: left + 'px',
						top: top + 'px'
					})
					.show();
				
			} else {
							
				var left = ( itemSize.outerWidth - 5 );
				var top = menuItem.position()['top'] - 5;
	
				// If the submenu won't fit to the right of the item...
				if( ( itemPosition.viewportOffset.left + itemSize.outerWidth + subMenuSize.outerWidth - 5 ) > $( window ).width() ){
					// ... See if it will fit to the left of the item...
					if( ( itemPosition.viewportOffset.left + 5 - subMenuSize.outerWidth ) >= 0 ){
						left = ( ( subMenuSize.outerWidth * -1 ) + 5 );
					} else {
						// ... but if not, position it *under* the item.
						// Since the submenu is pos relative to the parent item, we need to subtract the parent item pos from the ideal left pos
						left = ( $( window ).width() - subMenuSize.outerWidth ) - itemPosition.absPos.left - 15; 
						top = menuItem.position()['top'] + itemSize.height + 15;
					}
				}
	
				subMenu
					.css({
						left: left + 'px',
						top: top + 'px'
					})
					.show();
			
			}
		},

		/**
		 * Hides submenus within the item identified by e.currentTarget
		 *
		 * @param	{element} 	elem 		The element this widget belongs to
		 * @param	{string} 	elemID 		ID of the trigger element
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{element} 	menu 		The menu itself
		 * @param	{event} 	e 			The original event
		 * @returns {void}
		 */
		_hideSubMenu = function ( elem, elemID, options, menu, e ) {
			var subMenus = $( e.currentTarget ).closest('.ipsMenu_item').find('.ipsMenu');
			subMenus.hide();
		},

		/**
		 * Hides the menu, and unsets necessary events
		 *
		 * @param	{element} 	elem 		The element this widget belongs to
		 * @param	{string} 	elemID 		ID of the trigger element
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{element} 	menu 		The menu itself
		 * @param	{boolean} 	immediate 	Whether to hide this immediately, or animate
		 * @returns {void}
		 */
		_hideMenu = function ( elem, elemID, options, menu, immediate) {
			if( options.closeOnBlur ){
				$( document ).off('click.' + elemID );
			}

			$( elem ).off( 'closeMenu' );
			$( menu ).off( 'closeMenu' );

			// Remove active class on trigger element
			$( elem ).removeClass( options.activeClass );

			// ...then hide it
			if( !immediate ){
				ips.utils.anim.go('fadeOut fast', menu);
			} else {
				menu.hide();
			}

			$( elem ).trigger('menuClosed', { 
				elemID: elemID,
				menu: menu 
			});
		},

		/**
		 * Positions the menu correctly
		 *
		 * @param	{element} 	elem 		The element this widget belongs to
		 * @param	{string} 	elemID 		ID of the trigger element
		 * @param	{object} 	options		Options object
		 * @param	{element} 	menu 	 	The menu element
		 * @returns {void}
		 */
		_positionMenu = function (elem, elemID, options, menu, repositioning) {

			var above = options.above;
			if ( above == 'auto' ) {
				above = false;
				if ( ( $(elem).offset().top + menu.height() ) > $(window).height() ) {
					above = true;
				}
			}
			
			var positionInfo = {
				trigger: elem,
				target: menu,
				center: true,
				above: above,
				stemOffset: { left: options.stemOffset, top: -2 }
			};

			// Reset menu positioning
			menu.css({
				left: 'auto',
				top: 'auto',
				position: 'static'
			});

			if( menu.attr('data-originalWidth') ){
				menu.css({
					width: menu.attr('data-originalWidth') + 'px'
				});
			}

			if( options.appendTo ){
				var appendTo = _getAppendContainer( elem, options.appendTo );

				if( appendTo.length ){
					_.extend( positionInfo, {
						targetContainer: appendTo
					});	
				}				
			}

			var menuPosition = ips.utils.position.positionElem( positionInfo );
			var menuDims = ips.utils.position.getElemDims( menu );
			var triggerPosition = ips.utils.position.getElemPosition( $( elem ) );

			// Position the menu with the resulting styles
			menu.css({
				left: menuPosition.left + 'px',
				top: menuPosition.top + 'px',
				position: ( menuPosition.fixed ) ? 'fixed' : 'absolute',
			});

			// Only update zindex if we're showing afresh, rather than simply repositioning
			if( !repositioning ){
				menu.css({
					zIndex: ips.ui.zIndex()
				});
			}

			var newMenuPosition = ips.utils.position.getElemPosition( menu );

			// If the menu is wider than the window, reset some styles
			if( ( menuDims.width > $( document ).width() ) || newMenuPosition.viewportOffset.left < 0 ){
				options.noStem = true;

				var left = "10px";

				// If we're appending somewhere else, we need to subtract the offset of it to get a value relative to the window
				if( options.appendTo && appendTo.length ){
					var appendPosition = ips.utils.position.getElemPosition( appendTo );
					left = ( 10 - appendPosition.viewportOffset.left ) + 'px';
				}

				menu
					.attr( 'data-originalWidth', menuDims.width )
					.css({
						left: left,
						width: ( $( document ).width() - 20 ) + 'px'
					});
			}

			// Remove existing stems
			_removeExistingStems( menu, options );

			// Add a stem if needed
			if( !menu.hasClass( options.className + '_noStem') && !options.noStem ){
				var stemClass = '';
					stemClass += menuPosition.location.vertical;
					stemClass += menuPosition.location.horizontal.charAt(0).toUpperCase();
					stemClass += menuPosition.location.horizontal.slice(1);

				menu.addClass( options.className + '_' + stemClass );
			}
		},

		/**
		 * Removes any existing stem classes
		 *
		 * @param	{element} 	menu 	 	The menu element
		 * @param	{object} 	options		Options object
		 * @returns {void}
		 */
		_removeExistingStems = function (menu, options) {
			var stemClasses = [];

			$.each( stems, function (idx, value) {
				stemClasses[ idx ] = options.className + '_' + value;
			});

			menu.removeClass( stemClasses.join(' ') );
		},

		/**
		 * Returns the menu dom node, after setting appropriate events 
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{string} 	elemID 		ID of the trigger element
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {mixed} 	Returns the menu node, or false if it can't be found 	
		 */
		_getMenu = function (elem, elemID, options) {

			// We can find a menu either by ID, or by an option param
			// We can also build it from a provided JSON object
			if( $( '#'+options.menuID ).length ){
				var menu = $( '#'+options.menuID );
			} else if( $( '#'+elemID+'_menu' ).length ){
				var menu = $( '#'+elemID + '_menu' );
			} else if( options.menuContent ) {
				var menu = buildMenuFromJSON( elem, elemID, options.menuContent );
			} else {
				return false;
			}

			// Move menu to appropriate place
			if( options.appendTo ){
				var appendTo = _getAppendContainer( elem, options.appendTo );

				if( appendTo.length ){
					appendTo.append( menu );	
				}				
			} else {
				ips.getContainer().append( menu );
			}

			// Event handler for clicking within the menu
			$( menu ).on('click.' + elemID, _.partial( _menuClicked, elem, elemID, options, menu ) );

			// Add a reference to our trigger
			$( menu ).data('_owner', elem);

			// Add to registry
			_menuRegistry[ elemID ] = { elem: elem, options: options, menu: menu };

			return $(menu);
		},

		/**
		 * Event handler for clicking on the document, outside of the menu or trigger
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{element} 	menu 		The menu element
		 * @param	{event} 	e 			The event object
		 * @returns {void} 	
		 */
		_closeOnBlur = function (elem, menu, e) {

			// This function returns the trigger element that was clicked on, if any
			var clickedOnTrigger = function () {
				if( $( e.target ).is('[data-ipsMenu]') ){
					Debug.log( e.target );
					return e.target;
				} else if ( $( e.target ).parent('[data-ipsMenu]') ){
					return $( e.target ).parent('[data-ipsMenu]').get(0);
				}
			}();

			// Here we loop through each menu we have registered in order to close them
			// Don't hide the menu if:
			// 		- We clicked inside this menu, or
			// 		- We clicked the trigger element for this menu
			// If we have clicked on a trigger, we tell _hideMenu to do an immediate hide
			// so that it feels snappy to the user.
			$.each( _menuRegistry, function (key, value){
				var clickInMenu = _clickIsChildOfMenu( e.target, value.elem, value.menu.get(0) );

				if( value.elem ){
					if( clickInMenu || value.elem == clickedOnTrigger || $.contains( value.elem, e.target ) ){
						return;
					}
				}

				if( value.menu.is(':visible') ){
					_hideMenu( value.elem, key, value.options, value.menu, !!clickedOnTrigger );
				}
			});
		},

		/**
		 * Determines whether the clicked element is within a menu element
		 *
		 * @param	{element} 	clickTarget		The element directly clicked on
		 * @param	{element} 	triggerElem 	An element that triggers a menu
		 * @param	{element} 	menuElem 		The menu element opened by triggerElem
		 * @returns {boolean} 	Whether the click occurred within this menu 	
		 */
		_clickIsChildOfMenu = function (clickTarget, triggerElem, menuElem) {

			// Also check if this is within a jQuery date picker
			// Some jQUI elements are detatched from the dom by the time we get here, so we can't easily check its ancestors to 
			// figure out if it's in a datepicker. Instead, we'll check the classname applied to the clickTarget.
			if( _.isString( $( clickTarget ).get(0).className ) && ( $( clickTarget ).get(0).className.startsWith('ui-datepicker') || $( clickTarget ).closest('#ui-datepicker-div').length ) ) {
				return true;
			}

			if( clickTarget == menuElem || $.contains( menuElem, clickTarget ) ){
				return true;
			}			

			return false;
		},

		/**
		 * Main event handler for the menu itself
		 *
		 * @param	{event} 	e 		The event object
		 * @returns {void}
		 */
		_menuClicked = function (elem, elemID, options, menu, e) {


			if( $( e.target ).hasClass( options.className + '_item' ) ){
				var itemClicked = $( e.target );
			} else {
				var itemClicked = $( e.target ).parents( '.' + options.className + '_item' );
			}

			if( itemClicked.length === 0 ){
				return;
			}

			if( options.stopLinks ){
				e.preventDefault();
			}

			if( itemClicked.hasClass( options.className + '_itemDisabled') || itemClicked.is(':disabled') ){
				return;
			}
			
			if( options.closeOnClick ){
				if( itemClicked.find('[data-action="ipsMenu_ping"]').length ){
					e.preventDefault();
					itemClicked.find('[data-action="ipsMenu_ping"]').each( function () {
						ips.getAjax()( $( this ).attr('href') ).done( function () {
							$( elem ).trigger( 'menuPingSuccessful', {} );
						});
					});
				}
				
				// Cause the selected item to blink briefly, then add
				// a short delay before hiding the menu				
				var addItemClicked = function () {
					itemClicked.addClass( options.className + '_itemClicked');
				};

				var removeItemClicked = function () {
					itemClicked.removeClass( options.className + '_itemClicked');
				};

				if( e.button !== 1 ){
					_.delay( addItemClicked, 100 );
					_.delay( removeItemClicked, 200 );
					_.delay( _hideMenu, 300, elem, elemID, options, menu, false );
				}
			}
			
			if( itemClicked.find('[data-role="ipsMenu_selectedText"]').length ){
				$( elem ).find('[data-role="ipsMenu_selectedText"]').html( itemClicked.find('[data-role="ipsMenu_selectedText"]').html() );
			}

			if( itemClicked.find('[data-role="ipsMenu_selectedIcon"]').length ){
				$( elem ).find('[data-role="ipsMenu_selectedIcon"]').replaceWith( itemClicked.find('[data-role="ipsMenu_selectedIcon"]').clone() );
			}

			var data = {
				triggerElem: elem,
				triggerID: elemID,
				menuElem: $( menu[0] ),
				originalEvent: e
			};

			if( options.selectable ){
				_.extend( data, _handleSelectableClick( elem, elemID, options, menu, e ) );
			}
			
			if( !_.isUndefined( itemClicked.attr('data-ipsmenuvalue') ) ){
				_.extend( data, { selectedItemID: itemClicked.attr('data-ipsmenuvalue') } );
			}

			$( elem ).trigger('menuItemSelected', data);
		},

		/**
		 * Handles toggling settings if this is a selectable menu
		 *
		 * @param	{event} 	e 		The event object
		 * @returns {void}
		 */
		_handleSelectableClick = function (elem, elemID, options, menu, e) {

			var thisItem = $( e.target ).closest( '.' + options.className + '_item' );

			if( !thisItem.length ){
				return;
			}
			if( thisItem.attr('data-noselect') ){
				return;
			}

			if( options.selectable == 'radio' ){
				menu
					.find( '.' + options.className + '_itemChecked' )
					.removeClass( options.className + '_itemChecked' );
					
				thisItem
					.addClass( options.className + '_itemChecked' )
					.find('input[type="radio"]').prop( 'checked', true )
						.change();
			} else {
				if( thisItem.hasClass( options.className + '_itemChecked' ) ){
					thisItem
						.removeClass( options.className + '_itemChecked' )
						.find('input[type="checkbox"]').prop( 'checked', false )
							.change();
				} else {
					thisItem
						.addClass( options.className + '_itemChecked' )
						.find('input[type="checkbox"]').prop( 'checked', true )
							.change();
				}					
			}			

			// Get selected items
			var selectedItems = menu.find( '.' + options.className + '_itemChecked' ),
				selected = {};

			$.each( selectedItems, function (idx, item) {
				selected[ $( item ).identify().attr( 'id' ) ] = item;
			});

			return {
				selectedItems: selected
			};
		},

		/**
		 * Gets the element into which the menu will be inserted.
		 * We support a comma-delimited list of selectors, and will choose the first that exists.
		 * If an ID is provided, match it explicitly. Other selectors are based on a match with .closest().
		 *
		 * @param 	{element} 	elem 		The trigger element
		 * @param	{string} 	appendTo 	The value from options.appendTo
		 * @returns {element}
		 */
		_getAppendContainer = function (elem, appendTo) {
			var appends = appendTo.split(',');
			var elem = $( elem );

			for( var i = 0; i < appends.length; i++ ){
				var selector = appends[i].trim();

				if( selector.startsWith('#') ){
					if( $( selector ).length ){
						return $( selector );
					}
				} else {
					if( elem.closest( selector ).length ){
						return elem.closest( selector );
					}
				}
			};
		};

		// Register menu as a widget
		ips.ui.registerWidget('menu', ips.ui.menu, 
			[ 'className', 'menuID', 'closeOnClick', 'closeOnBlur', 'menuContent', 'appendTo',
				'activeClass', 'selectable', 'withStem', 'stemOffset', 'stopLinks', 'above' ],
			{ lazyLoad: true, lazyEvents: 'click' } 
		);

		return {
			respond: respond
		};
	});

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.pageAction.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.pageAction.js - Page action widget
 * Converts a select box of actions (e.g. moderator actions) into a fancy floating button bar with menus.
 * The select should be formatted like the following example. Optgroups are turned into a menu, while options are
 * turned into buttons. optgroups and options can have a data-icon attribue to specify the icon to use.
 * @example
 * 
 *	<select>
 *		<optgroup label="Pin" data-action='pin' data-icon='thumb-tack'>
 *			<option value='pin'>Pin</option>
 *			<option value='unpin'>Unpin</option>
 *		</optgroup>
 *		<option value='split' data-icon='code-fork'>Split</option>
 *	</select>
 * 
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.pageAction', function(){

		var defaults = {};

		var respond = function (elem, options) {
			if( !$( elem ).data('_pageAction') ){
				$( elem ).data('_pageAction', pageActionObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Retrieve the pageAction instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The pageAction instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_pageAction') ){
				return $( elem ).data('_pageAction');
			}

			return undefined;
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		};

		/**
		 * Page action instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var pageActionObj = function (elem, options) {

			var wrapper = null,
				initialized = false,
				id = '',
				_checkedItems = {};

			/**
			 * Sets up this instance
			 *
			 * @returns 	{void}
			 */
			var init = function () {
				// Hide the tools select
				elem.find('[data-role="pageActionOptions"]').hide();

				// Add events on the checkboxes
				_setUpEvents();
			},

			/**
			 * Destruct this widget on this element
			 *
			 * @returns {void}
			 */
			destruct = function () {
				if( wrapper ){
					wrapper
						.off( 'click', '[data-role="actionButton"]', _selectItem )
						.off( 'menuItemSelected', '[data-role="actionMenu"]', _selectMenuItem );	
				}				
			},

			/**
			 * Public access for the internal _updateBar method
			 *
			 * @returns 	{void}
			 */
			refresh = function () { 
				_refreshPageAction();
				_updateBar();
			},

			/**
			 * Resets the pageAction bar
			 *
			 * @returns 	{void}
			 */
			reset = function () {
				_checkedItems = {};
				_updateBar();
			},

			/**
			 * Sets up events that this instance will watch for
			 *
			 * @returns 	{void}
			 */
			_setUpEvents = function () {
				elem.on( 'change', 'input[type="checkbox"][data-actions]', _toggleCheckbox );
				elem.on( 'refresh.pageAction', _refreshPageAction );
				elem.on( 'addManualItem.pageAction', _addManualItem );
			},

			/**
			 * Allows controllers to manually add an ID to the page action widget if necessary
			 *
			 * @param 		{event} 	e 		Event object
			 * @param 		{object} 	data 	Event data object, containing keys 'id' and 'actions'
			 * @returns 	{void}
			 */
			_addManualItem = function (e, data) {
				_checkedItems[ data.id ] = data.actions;
				_updateBar( true );
			},

			/**
			 * Called when the contents are refreshed. Loops through our checkedItems list, and checks any
			 * of the matching checkboxes found on the page.
			 *
			 * @returns 	{void}
			 */
			_refreshPageAction = function () {
				_.each( _checkedItems, function (val, key) {
					if( elem.find('input[type="checkbox"][name="' + key + '"]').length ){
						elem.find('input[type="checkbox"][name="' + key + '"]').attr( 'checked', true ).trigger('change');
					}
				});
			},

			/**
			 * Updates the display of the bar, including available actions and the count
			 *
			 * @returns 	{void}
			 */
			_toggleCheckbox = function (e) {
				var checkbox = $( e.currentTarget );

				if( checkbox.is(':checked') ){
					_checkedItems[ checkbox.attr('name') ] = checkbox.attr('data-actions');
				} else {
					delete _checkedItems[ checkbox.attr('name') ];
				}

				_updateBar( true );
			},
			
			/**
			 * Updates the display of the bar, including available actions and the count
			 *
			 * @returns 	{void}
			 */
			_updateBar = function (doImmediate) {

				if( !initialized ){
					// Build the action bar
					_buildActionBar();
					doImmediate = true;
					initialized = true;
				}

				var possibleValues = _getActionValues();
				var size = _.size( _checkedItems );

				// Update the bar to show/hide appropriate buttons
				_showCorrectButtons( possibleValues );

				// Center it
				_positionBar();

				// Update the 'with x selected' text
				wrapper.find('[data-role="count"]').text( ips.pluralize( ips.getString('pageActionText_number'), size ) );

				// Animate the wrapper as needed (fade out if none selected)
				if( !size ){
					if( doImmediate ){
						wrapper.hide();
					} else {
						ips.utils.anim.go( 'fadeOut fast', wrapper );
					}
				} else if( initialized ){
					if( wrapper.is(':visible') ){
						ips.utils.anim.go( 'pulseOnce fast', wrapper );	
					} else {
						if( doImmediate ){
							wrapper.show();
						} else {
							ips.utils.anim.go( 'fadeIn', wrapper );
						}
					}					
				}
			},

			/**
			 * Centers the bar by adjusting the margin-left. The other positioning is handled in the CSS (including responsive)
			 *
			 * @returns {void}
			 */
			_positionBar = function () {
				var width = wrapper.outerWidth();
				var newLeft = width / 2;

				wrapper.css({
					marginLeft: ( newLeft * -1 ) + 'px'
				});
			},

			/**
			 * Shows/hides the buttons on the action bar depending on the actions we need
			 *
			 * @param	{array} 	possibleValues		Array of action keys we want to show
			 * @returns {void}
			 */
			_showCorrectButtons = function (possibleValues) {

				// Hide/show each button as needed
				wrapper.find('[data-role="actionMenu"], [data-role="actionButton"]').each( function () {
					var show = false;
					var action = $( this ).attr('data-action');

					// Check buttons
					if( $( this ).is('[data-role="actionButton"]') ){
						
						if( _.indexOf( possibleValues, action ) !== -1 ){
							show = true;
						}

					// Check menus, by checking each individual sub-menu item
					} else {
						var menuID = id + '_' + action + '_menu';

						$( '#' + menuID ).find('[data-ipsMenuValue]').each( function () {
							var menuAction = $( this ).attr('data-ipsMenuValue');

							if( _.indexOf( possibleValues, menuAction ) !== -1 ){
								show = true;
								// Enable this option if the action is relevant
								$( this ).removeClass('ipsMenu_itemDisabled');
							} else {
								$( this ).addClass('ipsMenu_itemDisabled');
							}
						});
					}

					if( show && !$( this ).is(':visible') ){
						$( this ).removeClass('ipsHide');
					} else if( !show && $( this ).is(':visible') ) {
						$( this ).addClass('ipsHide');
					}
				});
			},

			/**
			 * Event handler for clicking a button in the action bar
			 *
			 * @param	{event} 	e			Event object
			 * @returns {void}
			 */
			_selectItem = function (e) {
				e.preventDefault();
				_triggerAction( $( e.currentTarget ).attr('data-action') );
			},

			/**
			 * Event handler for a menu item being clicked
			 *
			 * @param	{event} 	e			Event object
			 * @param	{object} 	data 		Event data object
			 * @returns {void}
			 */
			_selectMenuItem = function (e, data) {
				e.preventDefault();

				if( !_.isUndefined( data.originalEvent ) ){
					data.originalEvent.preventDefault();
				}

				_triggerAction( data.selectedItemID );
			},

			/**
			 * Triggers an action by setting the original select value and submitting the form
			 *
			 * @param	{string} 	action 		Action to trigger
			 * @returns {void}
			 */	
			_triggerAction = function (action) {
				var tools = elem.find('[data-role="pageActionOptions"]');
				
				// Set the select to the value
				tools.find('select').val( action );

				// Add any missing checkboxes as hidden values in the form
				_.each( _checkedItems, function (val, key) {
					if( !elem.find('input[type="checkbox"][name="' + key + '"]').length && !elem.find('input[type="hidden"][name="' + key + '"]').length ){
						elem.append( $('<input/>').attr( 'type', 'hidden' ).attr( 'name', key ).attr( 'data-role', "moderation" ).val(1) );
					}
				});

				// Add page number
				var page = ips.utils.url.getPageNumber('page');

				if( !_.isUndefined( page ) ){
					var pageNumber = $('<input/>').attr( 'type', 'hidden' ).attr( 'name', 'page' ).attr( 'value', ips.utils.url.getPageNumber('page') );
					tools.find('[type="submit"]').before( pageNumber );
					tools.closest('form').attr( 'action', tools.closest('form').attr( 'action' ) + '&page=' +ips.utils.url.getPageNumber('page') );
				}
				
				// Click submit
				tools.find('[type="submit"]').click();
			},

			/**
			 * Builds the action bar
			 *
			 * @returns {void}
			 */
			_buildActionBar = function () {
				var content = '';
				var select = elem.find('[data-role="pageActionOptions"] select');

				// Get ID of select
				id = select.identify().attr('id');

				select.children().each( function () {
					if( $( this ).is('optgroup') ){
						content += _buildOptGroup( $( this ), id );  
					} else {
						content += _buildOption( $( this ), id );
					}
				});

				var bar = ips.templates.render('core.pageAction.wrapper', {
					content: content,
					id: id,
					selectedLang: ips.getString('pageActionText')
				});

				elem.after( bar );

				wrapper = elem.next();

				wrapper
					.on( 'click', '[data-role="actionButton"]', _selectItem )
					.on( 'menuItemSelected', '[data-role="actionMenu"]', _selectMenuItem );

				$( document ).trigger( 'contentChange', [ wrapper ] );
			},

			/**
			 * Build an option group menu for the action bar
			 *
			 * @param	{element} 	optgroup	The optgroup element
			 * @param	{string} 	id 			The ID of the select control this optgroup belongs to
			 * @returns {string}	The built template
			 */
			_buildOptGroup = function (optgroup, id) {
				var content = '';
				var options = optgroup.find('option');

				options.each( function () {
					content += ips.templates.render('core.menus.menuItem', {
						value: $( this ).attr('value'),
						title: $( this ).html(),
						link: '#',
					});
				});

				return ips.templates.render('core.pageAction.actionMenuItem', {
					icon: optgroup.attr('data-icon'),
					title: optgroup.attr('label'),
					id: id,
					action: optgroup.attr('data-action'),
					menucontent: content
				});
			},

			/**
			 * Build an option menu item for the action bar
			 *
			 * @param	{element} 	option 		The option element
			 * @param	{string} 	id 			The ID of the select control this option belongs to
			 * @returns {string} 	The built template
			 */
			_buildOption = function (option, id) {
				return ips.templates.render('core.pageAction.actionItem', {
					icon: option.attr('data-icon'),
					id: id,
					title: option.html(),
					action: option.attr('value')
				});
			},

			/**
			 * Gets the action values from the provided checkboxes
			 *
			 * @param	{element} 	checkboxes 		The checkboxes to get the action values from
			 * @returns {void}
			 */
			_getActionValues = function () {
				var values = [];

				_.each( _checkedItems, function (val, key) {
					var splitValues = val.split(' ');
					values = _.union( values, splitValues );
				});

				return values;
			},

			/**
			 * Retuens a jQuery object of the checked checkboxes
			 *
			 * @returns {element}	Checkbox elements
			 */
			_getCheckedBoxes = function () {
				return elem.find('input[type="checkbox"][data-actions]:checked');
			};

			init();

			return {
				refresh: refresh,
				destruct: destruct,
				bar: wrapper,
				reset: reset
			};
		};

		ips.ui.registerWidget( 'pageAction', ips.ui.pageAction, [] );

		return {
			respond: respond,
			destruct: destruct,
			getObj: getObj
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.pagination.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.pagination.js - Pagination UI component
 * Fires events that a controller can look for to facilitate AJAX pagination
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.pagination', function(){

		var defaults = {
			ajaxEnabled: true,
			perPage: 25, // number of items per perPage,
			pageParam: 'page',
			seoPagination: false,
		};

		/**
		 * Responder for pagination widget
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{event} 	e 		 	The event object passed through
		 * @returns {void}
		 */
		var respond = function (elem, options) {
			options = _.defaults( options, defaults );

			if( !$( elem ).data('_pagination') ){
				$( elem ).data('_pagination', paginationObj(elem, _.defaults( options, defaults ) ) );
			}
		};

		// Register this widget with ips.ui
		ips.ui.registerWidget('pagination', ips.ui.pagination,
			['ajaxEnabled', 'perPage', 'pages', 'pageParam', 'seoPagination']
		);

		return {
			respond: respond
		};
	});

	/**
	 * Pagination instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var paginationObj = function (elem, options) {

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			// The ajaxEnabled option is read at run-time in this case,
			// meaning controller can disable it on the fly if necessary
			if( !options.ajaxEnabled ){
				return;
			}

			// Set events
			// Click on a page number
			elem.on( 'click', '[data-page]', function (e) {

				var targetElem = $( e.currentTarget );

				$( elem ).trigger('paginationClicked', { 
					href: targetElem.attr('href') || '#',
					hrefTitle: targetElem.attr('title') || '',
					paginationElem: $(elem),
					seoPagination: options.seoPagination,
					pageElem: targetElem,
					perPage: options.perPage,
					pageParam: options.pageParam,
					pageNo: targetElem.attr('data-page'),
					lastPage: ( parseInt( targetElem.attr('data-page') ) === parseInt( options.pages ) ),
					originalEvent: e || null
				});
			});
				
			// Use the page jump
			elem.on( 'menuOpened', function (e, data) {
				$( elem ).find('input[type="number"]').focus();
			});

			elem.on( 'submit', '[data-role="pageJump"]', function (e) {
				var value = parseInt( $( e.currentTarget ).find('input[type="number"]').val() );
                var href = $( e.currentTarget ).closest('[data-baseURL]').attr('data-baseurl');

				if( value < 1 || value > options.pages ){
					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warning',
						message: ips.getString('not_valid_page', [ options.pages ] ),
						callbacks: {}
					});

					return;
				}

				$( elem ).trigger('paginationJump', { 
					originalEvent: e || null,
					href: href || '#',
					paginationElem: $(elem),
					seoPagination: options.seoPagination,
					pageNo: value,
					perPage: options.perPage,
					pageParam: options.pageParam,
					lastPage: ( parseInt( value ) === parseInt( options.pages ) )
				});
			});
		};

		init();

		return {
			init: init
		};
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.passwordStrength.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/* global ips, _ */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.passwordStrength.js - Checks password fields for strength
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.passwordStrength', function(){

		var defaults = {};

		var respond = function (elem, options) {
			if( !$( elem ).data('_passwordStrength') ){
				$( elem ).data('__passwordStrength', passwordStrengthObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Destruct the passwordStrength widgets in elem
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		},

		/**
		 * Retrieve the passwordStrength instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The passwordStrength instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_passwordStrength') ){
				return $( elem ).data('_passwordStrength');
			}

			return undefined;
		};

		ips.ui.registerWidget('passwordStrength', ips.ui.passwordStrength, 
			[ 'enforced', 'enforcedStrength', 'checkAgainstMember', 'checkAgainstRequest' ]
		);

		return {
			respond: respond,
			getObj: getObj,
			destruct: destruct
		};
	});

	/**
	 * passwordStrength instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var passwordStrengthObj = function (elem, options) {

		var _popup = null,
			_passwordBlurred = false,
			_field = null,
			_dirty = false,
			_timer = null,
			_ajax = ips.getAjax();

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			elem.on( 'focus', 'input[type="password"]', _passwordFocus );
			elem.on( 'blur', 'input[type="password"]', _passwordBlur );
			elem.on( 'keyup blur', 'input[type="password"]', _passwordKeyEvent );

			_field = elem.find('input[type="password"]');
			_field.after( $('<span/>').attr( 'data-role', 'validationCheck' ) );

			// If there's a value already in the box, run now
			if( _field.val() !== '' ){
				_changePassword();
			}
		},

		/**
		 * Destruct
		 * Removes event handlers assosciated with this instance
		 *
		 * @returns {void}
		 */
		destruct = function () {
			if( _timer ){
				clearTimeout( _timer );
			}

			if( _ajax && _ajax.abort ){
				_ajax.abort();
			}

			elem.off( 'focus', 'input[type="password"]', _passwordFocus );
			elem.off( 'blur', 'input[type="password"]', _passwordBlur );
			elem.off( 'keyup blur', 'input[type="password"]', _passwordKeyEvent );
		},

		/**
		 * Focus handler for password field
		 *
		 * @returns 	{void}
		 */
		_passwordFocus = function () {
			// We encountered a strange issue where Chrome's password auto-fill would focus
			// the password field before the form was actually visible, causing the field to
			// be hidden due to the way getElemPosition in _buildAdvicePopup works.
			// So if the field isn't visible when focus is called, just blur and return.
			if( !elem.is(':visible') ){
				_field.blur();
				return;
			}

			if( _.isNull( _popup ) ){
				_buildAdvicePopup();
			}

			_popup.show();
			_positionAdvicePopup();
			_passwordBlurred = false;
		},

		/**
		 * Blur handler for password field
		 *
		 * @returns 	{void}
		 */
		_passwordBlur = function () {
			if( _popup ){
				_popup.hide();
			}

			_passwordBlurred = true;
		},

		/**
		 * Clears error/success status from field
		 *
		 * @returns 	{void}
		 */
		_clearResult = function () {
			// Rmmove error/success classes
			_field
				.removeClass('ipsField_error')
				.removeClass('ipsField_success')
					.next('[data-role="validationCheck"]')
						.html('');
		},

		/**
		 * Main event handler for password field. Sets a timeout so that we don't
		 * bombard the ajax handler with requests.
		 *
		 * @returns 	{void}
		 */
		_passwordKeyEvent = function (e) {
			if( _timer ){
				clearTimeout( _timer );
			}

			if( _field.val().length > 2 || e.type != "keyup" ){
				_timer = setTimeout( _changePassword, 750 );
			} else {
				_clearResult();
			}
		},

		/**
		 * Main business happens here. Fire ajax request to check password
		 * strength; show error or status
		 *
		 * @returns 	{void}
		 */
		_changePassword = function () {
			var value = _field.val();
			var resultElem = _field.next('[data-role="validationCheck"]');
			var wrapper = elem.find('[data-role="strengthInfo"]');
			var meter = elem.find('[data-role="strengthMeter"]');
			var text = elem.find('[data-role="strengthText"]');

			if( _ajax && _ajax.abort ){
				_ajax.abort();
			}

			if( value.length ){
				_dirty = true;
			} else {
				if( !_dirty ){
					return;
				}
			}

			// Show meter if needed
			if( !meter.is(':visible') ){
				ips.utils.anim.go('fadeInDown fast', wrapper);
			}

			// Set loading
			_field.addClass('ipsField_loading');

			// Capture other useful values to check against, if available
			var alsoCheckAgainst = new Array;

			if( options.checkAgainstRequest )
			{
				var requestNames = JSON.parse( options.checkAgainstRequest );

				_.each( requestNames, function( value ){
					if( elem.closest('form').find('input[name="' + value + '"]') )
					{
						alsoCheckAgainst.push( elem.closest('form').find('input[name="' + value + '"]').val() );
					}
				});
			}

			if( options.checkAgainstMember )
			{
				var memberValues = JSON.parse( options.checkAgainstMember );

				_.each( memberValues, function( value ){
					alsoCheckAgainst.push( value );
				});
			}

			// Do _ajax
			_ajax( ips.getSetting('baseURL') + '?app=core&module=system&controller=ajax&do=passwordStrength', {
				dataType: 'json',
				data: {
					input: value,
					checkAgainstRequest: alsoCheckAgainst
				},
				method: 'post'
			})
				.done( function (response) {
					if( response.result == 'ok' ){
						
						meter.val( response.granular );
						meter.attr( 'data-adviceValue', response.score );
						text.html( ips.getString('strength_' + response.score) );

						if( options.enforced ){
							_clearResult();

							if( response.score >= parseInt( options.enforcedStrength ) ){
								// If our score is above the threshold show the success state
								resultElem.hide().html('');
								_field.addClass('ipsField_success');

								// If the row has error status (i.e. we arrived at this page with an error)
								// remove it.
								_field.closest('.ipsFieldRow')
									.removeClass('ipsFieldRow_error')
									.find('.ipsType_warning')
										.hide();	
							} else {
								// If our score is below the threshold and we're blurred
								// show the error state
								if( _passwordBlurred ){
									resultElem
										.show()
										.html( ips.templates.render( 'core.forms.validateFailText', { 
											message: ips.getString('err_password_strength', { 
												strength: ips.getString('strength_' + options.enforcedStrength ) 
											}) 
										}));
									_field.addClass('ipsField_error');
								}
							}
						}
					} else {
						resultElem.show().html( ips.templates.render( 'core.forms.validateFailText', { message: response.message } ) );
						_field.removeClass('ipsField_success').addClass('ipsField_error');
					}
				})
				.fail( function () {} )
				.always( function () {
					_field.removeClass('ipsField_loading');
				});
		},

		/**
		 * Builds the advice popup
		 *
		 * @returns 	{void}
		 */
		_buildAdvicePopup = function () {
			var text = ips.getString('password_advice');
			var min = false;

			if( !_.isNull( _popup ) ){
				return;
			}

			if( options.enforced ){
				min = ips.getString('err_password_strength', { strength: ips.getString('strength_' + options.enforcedStrength) } );
			}

			var tmpPopup = ips.templates.render('core.forms.advicePopup', {
				id: elem.identify().attr('id'),
				min: min,
				text: text
			});

			$('body').append( tmpPopup );

			_popup = $('body').find( '#elPasswordAdvice_' + elem.identify().attr('id') );

			_popup.css({
				position: 'absolute'
			});
		},

		/**
		 * Positions the advice popup
		 *
		 * @returns 	{void}
		 */
		_positionAdvicePopup = function () {
			var isRTL = $('html').attr('dir') == 'rtl';
			var position = ips.utils.position.getElemPosition( _field );
			var fieldWidth = _field.width();
			var fieldHeight = _field.height();
			var adviceWidth = _popup.width();
			var adviceHeight = _popup.height();
			var windowWidth = $( window ).width();
			var stemOffset = 30;

			_popup
				.removeClass('cStem_rtl cStem_ltr cStem_above')
				.css({
					zIndex: ips.ui.zIndex()
				});

			if( isRTL && ( position.absPos.left - adviceWidth - stemOffset ) > 0 ){
				_popup
					.addClass('cStem_rtl')
					.css({
						top: ( position.absPos.top - ( stemOffset / 2 ) ) + 'px',
						left: ( position.absPos.left - stemOffset - adviceWidth ) + 'px'
					});
			} else if( !isRTL && ( position.absPos.left + fieldWidth + adviceWidth + stemOffset ) < windowWidth ) {
				_popup
					.addClass('cStem_ltr')
					.css({
						top: ( position.absPos.top - ( stemOffset / 2 ) ) + 'px',
						left: ( position.absPos.left + fieldWidth + stemOffset ) + 'px'
					});
			} else {
				_popup
					.addClass('cStem_above')
					.css({
						top: ( position.absPos.top - ( stemOffset / 2 ) - adviceHeight ) + 'px',
						left: ( position.absPos.left + ( fieldWidth / 2 ) - ( adviceWidth / 2 ) ) + 'px'
					});
			}
		};

		init();

		return {
			destruct: destruct
		};
	};

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.patchwork.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.patchwork.js - Turns a list of items into a patchwork, evenly distributed across equal columns
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.patchwork', function(){

		var defaults = {
			items: '[data-role="patchworkItem"]',
			minColSize: 300,
			maxColSize: 600
		};

		var respond = function (elem, options) {
			if( !$( elem ).data('_patchwork') ){
				$( elem ).data('_patchwork', patchWorkObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Retrieve the patchwork instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The patchwork instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_patchwork') ){
				return $( elem ).data('_patchwork');
			}

			return undefined;
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		};

		/**
		 * Patchwork instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var patchWorkObj = function (elem, options) {
			var currentColCount = 1;
			var possibleSizes = [ 1, 2, 3, 4, 6, 12 ]; // Since we're doing an even grid
			var containerList = null;
			var itemList = null;
			var items = null;

			/**
			 * Initialization: get the current span, and make sure all items are using it for consistency
			 *
			 * @returns {void}
			 */
			var init = function () {
				// Get all the items in the list
				itemList = elem.find('[data-role="patchworkList"]');
				items = itemList.find( options.items );
				
				items.each( function (idx) {
					$( this ).attr( 'data-position', idx );
				});

				// Build new list
				_buildList();

				// Window resize event which keeps everything at the right size
				$( window ).on( 'resize', _checkItemWidth );
			},

			/**
			 * Destruct this widget on this element
			 *
			 * @returns {void}
			 */
			destruct = function () {
				$( window ).off( 'resize', _checkItemWidth );
			},

			/**
			 * Redraws the layout, adding new list items and distributing the patchwork items among them
			 *
			 * @param 	{boolean} 	force 	If true, the items will be redrawn even if the column count hasn't changed
			 * @returns {void}
			 */
			_redrawLayout = function (force) {

				// Hide all list items
				var columnElems = containerList.find('> [class*="ipsGrid_span"]').hide();				
				var elemSize = elem.outerWidth();
				var columns = Math.ceil( elemSize / options.maxColSize );

				if( possibleSizes.indexOf( columns ) === -1 ){
					columns = _getCorrectColumnCount( columns );
				}

				if( currentColCount === columns && force !== true ){
					columnElems.show();
					return;
				}

				// Hide the items
				items.css({
					'opacity': "0.001"
				});

				var spanClass = _getSpanFromCount( columns );

				// Now add the new columns
				for( var i = 0; i < columns; i++ ){
					containerList.append( $('<li/>').addClass( 'ipsGrid_span' + spanClass ).attr('data-working', true) );
				}

				currentColCount = columns;

				// Sort items by position
				var currentItems = _.sortBy( items, function (item) {
					return parseInt( $( item ).attr('data-position') );
				});

				_distributeItems( currentItems );

				// Remove other list items
				containerList.find('> [class*="ipsGrid_span"]:not( [data-working] )').remove();
				containerList.find('> [data-working]').removeAttr('data-working');

				// Show the items
				setTimeout( function () {
					items.animate({
						opacity: "1"
					});
				}, 250 );

			},

			/**
			 * Distributes the items as evenly as possible between columns
			 *
			 * @returns {void}
			 */
			_distributeItems = function (currentItems) {
				// Get columns
				var columns = containerList.find('> [class*="ipsGrid_span"][data-working]');
				var itemCount = currentItems.length;
				var heights = [];
				var isLastRow = false;

				columns.each( function () {
					heights.push( {
						column: $( this ),
						height: 0
					});
				});

				_.each( currentItems, function (item, idx) {
					// Determine if this item starts our last row
					if( ( ( itemCount - ( idx + 1 ) ) % columns.length === 0 ) && ( idx >= itemCount - columns.length ) ){
						isLastRow = true;
					}

					//Debug.log( isLastRow  );
					// Get column with shortest height
					var shortest = _.min( heights, function (value) {
						return value.height;
					});

					// Move item to shortest column
					if( shortest ){
						shortest.column.append( $( item ) );
						// Add height
						shortest.height += $( item ).outerHeight();
					}
				});
			},

			/**
			 * Builds the list into which patchwork items will be distributed
			 *
			 * @returns {void}
			 */
			_buildList = function () {
				var elemSize = elem.outerWidth();

				containerList = $('<ul/>').addClass('ipsGrid ipsGrid_collapsePhone ipsPatchwork');
				itemList.after( containerList );

				_redrawLayout( true );
			},

			/**
			 * Returns the CSS class span for the given column count (e.g. 2 visual columns spans 6 grid columns)
			 *
			 * @returns {number} 	Span number
			 */
			_getSpanFromCount = function (columns) {
				return possibleSizes[ ( possibleSizes.length - 1 ) - possibleSizes.indexOf( columns ) ];
			},

			/**
			 * Returns a valid column count. Using a 12-column grid system, certain values are not supported (e.g. 5).
			 * This method finds the closest valid column number.
			 *
			 * @returns {number} 	Valid column count
			 */
			_getCorrectColumnCount = function (columns) {
				if( columns > 12 ){
					return 12;
				}

				for( var i = 0; i <= possibleSizes.length; i++ ){
					if( columns == possibleSizes[ i ] ){
						return possibleSizes[ i ];
					}

					if( columns > i && columns <= possibleSizes[ i + 1 ] ){
						var diffA = columns - i;
						var diffB = possibleSizes[ i + 1 ] - columns;

						if( diffA > diffB ){
							return possibleSizes[ i + 1 ];
						} else {
							return possibleSizes[ i ];
						}
					}
				}
			},

			/**
			 * Return the first grid item
			 *
			 * @returns {element} 	First grid item
			 */
			_getFirst = function () {
				return elem.find('.ipsPatchwork > [class*="ipsGrid_span"]').first();
			},

			/**
			 * Checks the column width, and if it's less or more than our min/max widths, apply a new span
			 *
			 * @returns {void}
			 */
			_checkItemWidth = function () {
				var firstItem = _getFirst();

				if( options.minColSize && firstItem.outerWidth() < parseInt( options.minColSize ) || options.maxItemSize && firstItem.outerWidth() >= parseInt( options.maxItemSize ) ){
					_redrawLayout();
				}
			};

			init();

			return {
				init: init,
				destruct: destruct
			};
		};

		ips.ui.registerWidget( 'patchwork', ips.ui.patchwork, [
			'minColSize', 'maxColSize', 'items'
		] );

		return {
			respond: respond,
			destruct: destruct,
			getObj: getObj
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.photoLayout.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.photoLayout.js - Photo layout widget for photo galleries
 * Based on the algorithm detailed at http://www.wackylabs.net/2012/03/flickr-justified-layout-in-jquery/
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.photoLayout', function(){
		
		var defaults = {
			minHeight: 250,
			maxItems: 5,
			gap: 4,
			itemTemplate: 'core.patchwork.imageList'
		};

		var respond = function (elem, options, e) {
			options = _.defaults( options, defaults );

			if( !$( elem ).data('_photoLayout') ){
				$( elem ).data('_photoLayout', photoLayoutObj(elem, _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Retrieve the photoLayout instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The photoLayout instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_photoLayout') ){
				return $( elem ).data('_photoLayout');
			}

			return undefined;
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		};

		ips.ui.registerWidget('photoLayout', ips.ui.photoLayout, [ 
			'minHeight', 'maxItems', 'maxRows', 'gap', 'data', 'itemTemplate'
		] );

		return {
			respond: respond,
			destruct: destruct,
			getObj: getObj
		};
	});

	/**
	 * Photo layout instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var photoLayoutObj = function (elem, options) {

		var currentWidth = 0;
		var imageData;
		var noOfPhotos = 0;
		var dataStore = $('<div/>');
		var windowWidth = 0;
		var checkboxes = [];
		var timer = null;

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			windowWidth = $( window ).width();
			currentWidth = Math.floor( elem.width() );
			imageData = _getData();
			noOfPhotos = imageData.length;

			elem.empty();
			run( imageData );

			timer = setInterval( function () {
				_checkCurrentWidth();
			}, 300 );

			// Set up events
			$( window ).on( 'resize', _resizeWindow );
			$( document ).trigger( 'contentChange', [ elem ] );
		},

		/**
		 * Checks the width of the parent element on an interval,
		 * and if it's changed, will rebuild the layout
		 *
		 * @returns {void}
		 */
		_checkCurrentWidth = function () {
			var newWidth = Math.floor( elem.width() );
			
			if( currentWidth !== newWidth ){
				currentWidth = newWidth;
				_resizeWindow(true);
			}
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @returns {void}
		 */
		destruct = function () {
			$( window ).off( 'resize', _resizeWindow );
			clearInterval( timer );
		},

		/**
		 * Refresh the layout
		 *
		 * @returns {void}
		 */
		refresh = function () {
			imageData = _getData();
			noOfPhotos = imageData.length;

			elem.empty();

			run( imageData );
		},

		/**
		 * Run the resizing process
		 *
		 * @returns 	{void}
		 */
		run = function (data) {

			// We can't run the resize process if we aren't showing right now
			if( !elem.is(':visible') ){
				return;
			}

			var initialHeight = Math.max( options.minHeight, Math.floor( currentWidth / options.maxItems ) );
			var currentWidthForCalc = ( currentWidth - options.gap );
			// Get relative sizes
			var relativeSizes = [];

			_.each( data, function (image) {
				var w = 0;
				var h = 0;

				if( !_.isString( image.filenames.small[0] ) ){
					w = options.minHeight;
					h = options.minHeight;
				} else {
					//alert(image.filenames.small[1]);
					w = parseInt( image.filenames.small[1], 10 );
	            	h = parseInt( image.filenames.small[2], 10 );	
				}			
	            
	            if ( ! w )  {
		            w = options.minWidth;
	            }
	            
	            if ( ! h ) {
		            w = options.minHeight;
	            }
	            	
	            if( h != initialHeight ){
	            	w = Math.floor( w * ( initialHeight / h ) );
	            }

	            relativeSizes.push( w );
			});

			var imagesSoFar = 0;
			var rowNum = 0;

			// Start positioning images
			while ( imagesSoFar < noOfPhotos ) {
				rowNum++;

				if( options.maxRows && rowNum > options.maxRows ){
					break;
				}

				var imagesInRow = 0;
				var widthOfRow = 0;

				// Figure out how many images to show in this row and how wide it will be
				while ( ( widthOfRow * 1.1 < currentWidthForCalc ) && ( imagesSoFar + imagesInRow < noOfPhotos ) ) {
					var gap = options.gap * 2;

					if( imagesInRow === 0 ){
						gap = options.gap;
					}

					widthOfRow += relativeSizes[ imagesSoFar + imagesInRow++ ] + gap;
				}

				// Subtract one measure of gap for the right-hand side
				widthOfRow -= options.gap;

				var row = _getRow();
				var ratio = ( currentWidthForCalc ) / widthOfRow;
				var lastRowNotFit = false;
				var i = 0;

				widthOfRow = 0;

				var rowHeight = Math.floor( initialHeight * ratio );
				row.height( rowHeight + ( options.gap * 2 ) );

				// Now loop through each image and insert it
				while ( i < imagesInRow ) {
					var thisImage = data[ imagesSoFar + i ];
					var newWidth = Math.floor( relativeSizes[ imagesSoFar + i ] * ratio );
					var thisWidth = ( !_.isNull( thisImage.filenames.small[1] ) && thisImage.filenames.small[1] > 0 ) ? thisImage.filenames.small[1] : options.minWidth;
					var thisHeight = ( !_.isNull( thisImage.filenames.small[2] ) && thisImage.filenames.small[2] > 0  ) ? thisImage.filenames.small[2] : options.minHeight;

					// If this is the last row, we might not have enough images to fill it
					// If the height is bigger than our starting height, we'll reset it, and scale this image to match
					if( imagesSoFar + imagesInRow >= noOfPhotos && rowHeight >= initialHeight ){
						rowHeight = initialHeight; // Reset height
						row.height( rowHeight + ( options.gap * 2 ) ); // Resize row
						newWidth = Math.floor( thisWidth * ( initialHeight / thisHeight ) ); // Scale width
						lastRowNotFit = true;
					}

					widthOfRow += newWidth + ( options.gap * 2 );

					// Build the item using a template
					var item = _buildItem( thisImage, {
						width: newWidth,
						height: rowHeight,
						margin: options.gap,
						marginLeft: ( i === 0 ) ? 0 : options.gap,
						marginRight: ( i + 1 === imagesInRow ) ? 0 : options.gap,
						ratio: ( ( rowHeight / newWidth ) * 100 ).toFixed(2)
					});

					row.append( item );

					i++;
				}

				// Subtract two counts of margin, for the left and right sides of the row
				widthOfRow -= ( options.gap * 2 );

				imagesSoFar += imagesInRow;

				// We can now tweak image widths to make sure it fills the full row
				// If this is the last row, and we don't have enough images to perfectly fill the row, we don't
				// want to tweak the sizes or it'll stretch them strangely. We can skip this bit.
				if( !lastRowNotFit ){
					var smWidth = 0;

					while ( widthOfRow < currentWidthForCalc ) {
						var item = row.find('.cGalleryPatchwork_item:nth-child(' + ( smWidth + 1 ) + ")");

						item.css({ width: ( item.width() + 1 ) + 'px' });

						smWidth = ( smWidth + 1 ) % imagesInRow;
						widthOfRow++;
					}

					var bigWidth = 0;

					while ( widthOfRow > currentWidthForCalc ) {
						var item = row.find('.cGalleryPatchwork_item:nth-child(' + ( bigWidth + 1 ) + ")");

						item.css({ width: ( item.width() - 1 ) + 'px' });

						bigWidth = ( bigWidth + 1 ) % imagesInRow;
						widthOfRow--;
					}
				}
			}

			// Check any that should be checked
			if( checkboxes.length ){
				_.each( checkboxes, function (val) {
					elem.find('input[type="checkbox"][name="' + val + '"]').prop('checked', true);
				});

				checkboxes = [];
			}

			elem.find('.cGalleryPatchwork_row').css({
				width: '100%'
			});

			// Set up lazy load for each item
			if( ips.getSetting('lazyLoadEnabled') ){
				elem.find('img[data-src]').each( function () {
					ips.utils.lazyLoad.observe( this );
				});
			}
		},

		/**
		 * Builds an item using the specified data
		 *
		 * @param 		{object} 	imageData 	The image data (that came from JSON on the page)
		 * @param 		{pbject} 	extra 		The processed extra data for the image, such as dims
		 * @returns 	{string}	The rendered HTML
		 */
		_buildItem = function (imageData, extra) {
				
			// Calculate retina sizes
			var multipliedWidth = extra.width;// * window.devicePixelRatio;
			var multipliedHeight = extra.height;// * window.devicePixelRatio;
			var showThumb = true;

			if( !_.isString( imageData.filenames.small[0] ) ){
				showThumb = false
			} else {
				// If the width/height we'll show at is bigger than the small image size, use the large size instead
				if( multipliedWidth <= imageData.filenames.small[1] && multipliedHeight <= imageData.filenames.small[2] ){
					imageData.src = imageData.filenames.small[0];
				} else {
					imageData.src = imageData.filenames.large[0];
				}
			}
			
			if ( ! _.isUndefined( imageData.container ) ) {
				// To make json safe, we convert ' to &apos;
				imageData.container = imageData.container.replace( /&apos;/ig, "'" );
			}
			
			return ips.templates.render( options.itemTemplate, {
				showThumb: showThumb,
				lazyLoad: ips.getSetting('lazyLoadEnabled'),
				image: imageData,
				dims: extra
			} );
		},

		/**
		 * Returns a new row element
		 *
		 * @returns 	{element}
		 */
		_getRow = function () {
			var row = $('<div/>').addClass('cGalleryPatchwork_row ipsClearfix');
			elem.append( row );

			return row;
		},

		/**
		 * Gets the data for the patchwork widget to use, either from the data option or from
		 * elements inside the widget
		 *
		 * @returns 	{object}
		 */
		_getData = function () {
			// If data has been specified on the widget, just use that
			if( options.data ){
				return $.parseJSON( options.data );
			}	

			var data = [];

			// Find all the images and extract data from it
			elem.find('[data-role="patchworkImage"][data-json]').each( function () {
				try {
					var _data = $.parseJSON( $( this ).attr('data-json') );
				} catch (err) {
					return;
				}

				data.push( _data );
			});

			return data;
		},

		/**
		 * Event handler for resizing the window
		 *
		 * @returns {void}
		 */
		_resizeWindow = function (force) {
			checkboxes = _getCheckedImages();

			// Only check the width here - we don't care if the height changes
			// Necessary because mobile browsers change the height to show the address bar when scrolling
			if( force || $( window ).width() !== windowWidth ){
				elem.empty();
				noOfPhotos = imageData.length;
				run( imageData );	
			}

			windowWidth = $( window ).width();
		},

		/**
		 * Returns the names of any checked checkboxes.
		 *
		 * @returns {array}
		 */
		_getCheckedImages = function () {
			var names = [];
			var checks = elem.find('input[type="checkbox"]:checked');

			if( checks.length ){
				checks.each( function () {
					names.push( $( this ).attr('name') );
				});
			}

			return names;
		};

		init();

		return {
			init: init,
			destruct: destruct,
			refresh: refresh
		};
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.productZoom.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.productZoom.js - Enables viewing an image in detail
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.productZoom', function(){

		var defaults = {};

		var respond = function (elem, options) {
			if( !$( elem ).data('_productZoom') ){
				$( elem ).data('_productZoom', productZoomObj(elem, _.defaults( options, defaults ) ) );
			}
		};

		ips.ui.registerWidget('productZoom', ips.ui.productZoom, 
			[ 'largeURL' ]
		);

		return {
			respond: respond
		};
	});

	/**
	 * productZoom instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var productZoomObj = function (elem, options) {

		var initialized = false,
			wrapper = null,
			imageElem = null,
			zoomArea = null,
			currentlyOver = false,
			ratio = 0,
			zoomerSize = 0,
			thumbW = 0,
			thumbH = 0,
			wrapperW = 0,
			wrapperH = 0,
			fullW = 0,
			fullH = 0,
			triggerBuffer = 25,
			disabled = false,
			isRTL = $('html').attr('dir') == 'rtl';

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			elem.on( 'mouseenter', _enterElem );
			elem.on( 'mouseleave', _leaveElem );
			elem.on( 'mousemove', _moveElem );
		},

		/**
		 * Event handler for mouse entering the thumb elem
		 * Sets up the zoomer if it isnt already, and shows it
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		_enterElem = function (e) {
			if( disabled ){
				return;
			}

			currentlyOver = true;

			if( !initialized ){
				_setUpZoomer();
				return;
			}

			if( !_checkAcceptableSize() ){
				disabled = true;
				return;
			}

			_showZoomer();
		},

		/**
		 * Event handler for mouse leaving the thumb elem
		 * Hides the zoomer widget
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		_leaveElem = function (e) {
			if( disabled ){
				return;
			}

			currentlyOver = false;

			wrapper.fadeOut('fast');
			zoomArea.hide();
		},

		/**
		 * Event handler for mouse moving over the thumb elem
		 * Moves the two zoomer elements based on the current mouse cursor position
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		_moveElem = function (e) {
			if( !initialized || !currentlyOver || disabled ){
				return;
			}

			var cursorPos = _cursorPos(e);
			var halfZoomer = ( zoomerSize / 2 );
			var halfWrapper = wrapper.width() / 2;
			var tLeft = 0; var tTop = 0;
			var fLeft = 0; var fTop = 0;

			// Put zoomer in middle of cursor
			if( cursorPos.left - halfZoomer < 0 ){
				tLeft = 0;
			} else if( cursorPos.left + halfZoomer > thumbW ){
				tLeft = thumbW - zoomerSize;
			} else {
				tLeft = cursorPos.left - halfZoomer;
			}

			if( cursorPos.top - halfZoomer < 0 ){
				tTop = 0;
			} else if( cursorPos.top + halfZoomer > thumbH ){
				tTop = thumbH - zoomerSize;
			} else {
				tTop = cursorPos.top - halfZoomer;
			}

			zoomArea.css({
				left: tLeft + 'px',
				top: tTop + 'px'
			});

			var reciprocal = 1 / ratio;

			// Get ratio of position
			var cursorPosLarge = {
				left: cursorPos.left * reciprocal,
				top: cursorPos.top * reciprocal
			};

			// Now position large image in the same way
			if( cursorPosLarge.left - halfWrapper < 0 ){
				fLeft = 0;
			} else if( cursorPosLarge.left + halfWrapper > fullW ){
				fLeft = fullW - wrapperW;
			} else {
				fLeft = cursorPosLarge.left - halfWrapper;
			}
			
			if( cursorPosLarge.top - halfWrapper < 0 ){
				fTop = 0;
			} else if( cursorPosLarge.top + halfWrapper > fullH ){
				fTop = fullH - wrapperH;
			} else {
				fTop = cursorPosLarge.top - halfWrapper;
			}

			imageElem.css({
				left: ( fLeft * -1 ) + 'px',
				top: ( fTop * -1 ) + 'px',
			});
		},

		/**
		 * Called when the large image has finished loading
		 *
		 * @returns {void}
		 */
		_imageLoaded = function () {
			wrapper.removeClass('ipsLoading');

			if( currentlyOver ){
				_showZoomer();
			}
		},

		/**
		 * Shows the zoomer widget
		 *
		 * @returns {void}
		 */
		_showZoomer = function () {
			var elemPosition = ips.utils.position.getElemPosition( elem );
			var elemSize = ips.utils.position.getElemDims( elem );

			wrapper.css({
				width: elemSize.outerHeight + 'px',
				height: elemSize.outerHeight + 'px',
				top: elemPosition.absPos.top + 'px'
			});

			if( isRTL ){
				wrapper.css({
					left: ( elemPosition.absPos.left - elemSize.outerWidth - 20 ) + 'px',
				});
			} else {
				wrapper.css({
					left: elemPosition.absPos.left + elemSize.outerWidth + 20 + 'px'
				});
			}

			wrapper.show();
			zoomArea.show();

			_getDimensions();

			if( !_checkAcceptableSize() ){
				disabled = true;
				wrapper.hide();
				zoomArea.hide();
				return;
			}

			ratio = thumbW / fullW;

			wrapper.css({
				opacity: "1"
			});

			zoomArea.css({
				width: ( thumbW * ratio ) + 'px',
				height: ( thumbW * ratio ) + 'px',
			});

			zoomerSize = zoomArea.width();
		},

		/**
		 * Sets up the zoomer widget, creating the elements needed
		 *
		 * @returns {void}
		 */
		_setUpZoomer = function () {
			$('#ipsZoomer, #ipsZoomer_area').remove();

			var elemPosition = ips.utils.position.getElemPosition( elem );
			var elemSize = ips.utils.position.getElemDims( elem );
			var imgURL = ( options.largeURL ) ? options.largeURL : elem.find('img').attr('src');

			wrapper = $('<div/>').attr( 'id', 'ipsZoomer' );
			zoomArea = $('<div/>').attr('id', 'ipsZoomer_area').hide();
			imageElem = $('<img/>').attr( 'src', imgURL ).css({ position: 'absolute' });

			$('body').append( wrapper.append( imageElem ) );
			
			elem.append( zoomArea ).css({
				position: 'relative'
			});

			wrapper
				.css({
					opacity: "0.0001",
					zIndex: ips.ui.zIndex()
				})
				.addClass('ipsLoading');

			imageElem.imagesLoaded( _imageLoaded );

			initialized = true;	

			_getDimensions();	
		},

		/**
		 * Retrieve various dimensions we need
		 *
		 * @returns {void}
		 */
		_getDimensions = function () {
			// Get dims of the various elems
			thumbW = elem.width();
			thumbH = elem.height();
			//--
			wrapperW = wrapper.width();
			wrapperH = wrapper.height();
			//--
			fullW = imageElem.width();
			fullH = imageElem.height();
			//--
		},

		/**
		 * Checks whether we should show the zoomer for this image
		 *
		 * @returns {void}
		 */
		_checkAcceptableSize = function () {
			if( ( fullW - triggerBuffer ) <= ( wrapperW ? wrapperW : wrapper.width() ) || ( fullH - triggerBuffer ) <= ( wrapperH ? wrapperH : wrapper.height() ) ){
				return false;
			}

			return true;
		},

		/**
		 * Returns the cursor position relative to the thumbnail
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		_cursorPos = function (e) {
			var offset = elem.offset();

			return {
				left: e.pageX - offset.left,
				top: e.pageY - offset.top
			};
		};

		init();

		return {};
	};

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.quote.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.quote.js - Quote widget, builds the citations for quotes in content
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.quote', function(){

		var defaults = {
			timestamp: '',
			userid: 0,
			username: '',
			contenttype: '',
			contentclass: '',
			contentid: 0
		};

		/**
		 * Respond method for quotes.
		 * Builds the quote HTML using the options passed into the widget and inserts it into the content post
		 *
		 * @param 	{element}	elem 		The quote element
		 * @param	{object} 	options	 	Options for this quote
		 * @returns {void}
		 */
		var respond = function (elem, options) {
			
			/* Don't rebuild if we've already done this */			
			if( elem.data('quoteBuilt') || elem.parents( '.cke_wysiwyg_div' ).length ){
				return;
			}
			
			/* Do we have an existing citation block? (quotes from older versions won't, newer will) */
			var existingCitation = elem.children('.ipsQuote_citation');
			
			/* What should the citation say? */
			var citation = ips.utils.getCitation( options, true, existingCitation.length ? existingCitation.text() : ips.getString('editorQuote') );
			
			/* Build the citation block */
			var data = {
				citation: citation,
				contenturl: options.contentid && options.contentcommentid ? ips.getSetting('baseURL') + "?app=core&module=system&controller=content&do=find&content_class=" + options.contentclass + "&content_id=" + options.contentid + "&content_commentid=" + options.contentcommentid : ''
			};
			var citation = ips.templates.render( 'core.editor.citation', data );
			
			/* Add or replace it */
			if ( existingCitation.length ) {
				existingCitation.replaceWith( citation );
			} else {
				elem.prepend( citation );
			}
			
			/* Set the event handler for opening/closing */
			elem.find('> .ipsQuote_citation').on( 'click', _toggleQuote );
			elem.find('> .ipsQuote_contents')
				.addClass('ipsClearfix')
				.attr('data-ipsTruncate', true)
				.attr('data-ipsTruncate-type', 'hide')
				.attr('data-ipsTruncate-size', '7 lines')
				.attr('data-ipsTruncate-expandText', ips.getString('expand_quote'));
			
			/* Hide embedded quotes */
			if( elem.is('blockquote.ipsQuote > blockquote.ipsQuote') ){
				elem
					.find('> *:not( .ipsQuote_citation )')
						.hide()
					.end()
					.find('> .ipsQuote_citation')
						.removeClass('ipsQuote_open')
						.addClass('ipsQuote_closed');					
			}
			
			/* And save that we've done this */
			elem.trigger('quoteBuilt.quote');
			elem.data( 'quoteBuilt', true );

			$( document ).trigger( 'contentChange', [ elem ] );
		},
		
		/**
		 * Event handler for toggling the quote visibility
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		_toggleQuote = function (e) {
			var cite = $( e.currentTarget );
			var target = $( e.target );

			if( target.is('a:not( [data-action="toggleQuote"] )') || ( target.closest('a').length && !target.closest('a').is('[data-action="toggleQuote"]') ) ){
				return;
			}

			e.preventDefault();

			if( cite.hasClass('ipsQuote_closed') ){
				ips.utils.anim.go( 'fadeIn', cite.siblings() );
				cite.removeClass('ipsQuote_closed').addClass('ipsQuote_open');
			} else {
				cite.siblings().hide();
				cite.removeClass('ipsQuote_open').addClass('ipsQuote_closed');
			}

			e.stopPropagation();
		};

		ips.ui.registerWidget('quote', ips.ui.quote, 
			[ 'timestamp', 'userid', 'username', 'contentapp', 'contenttype', 'contentclass', 'contentid', 'contentcommentid' ]
		);

		return {
			respond: respond
		};
	});
}(jQuery, _));
]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.rating.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.rating.js - Rating widget
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.rating', function(){

		var defaults = {
			changeRate: true,
			canRate: true
		};

		var respond = function (elem, options) {
			if( !$( elem ).data('_rating') ){
				$( elem ).data('_rating', ratingObj(elem, _.defaults( options, defaults ) ) );
			}
		};

		ips.ui.registerWidget('rating', ips.ui.rating, 
			[ 'url', 'changeRate', 'canRate', 'size', 'value', 'userRated' ]
		);

		/**
		 * Rating instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var ratingObj = function (elem, options) {

			var selected = null,
				max = 0,
				ratingElem = null,
				userRated = false, // Has the user rated on this page load?
				loading = false;

			/**
			 * Sets up this instance
			 * Hides the contents of thw widget, fetch the current value (based on which radio is selected),
			 * then build the stars dynamically
			 *
			 * @returns 	{void}
			 */
			var init = function () {
				// Hide all inputs
				elem.children().hide();

				// Get the selected value (if any)
				if ( options.value ) {
					selected = options.value;
				} else {
					selected = elem.find('input[type="radio"]:checked').val();
				}
				
				var maxElem = _.max( elem.find('input[type="radio"]'), function (value) {
					return parseInt( $( value ).attr('value') );
				});

				max = $( maxElem ).attr('value');

				_buildRatingElem();

				// Set up events
				ratingElem.on( 'mouseenter', 'li', _enterStar );
				ratingElem.on( 'mouseleave', 'li', _leaveStar );
				ratingElem.on( 'click', 'li', _clickStar );
			},

			/**
			 * Builds the stars elements
			 *
			 * @returns {void}
			 */
			_buildRatingElem = function () {
				var content = '';
				
				for( var i = 1; i <= max; i++ ){
					if ( i <= selected ) {
						content += ips.templates.render('core.rating.star', {
							value: i,
							className: 'ipsRating_on'
						});
					} else if ( ( i - 0.5 ) <= selected ) {
						content += ips.templates.render('core.rating.halfStar', {
							value: i
						});
					} else {
						content += ips.templates.render('core.rating.star', {
							value: i,
							className: 'ipsRating_off'
						});
					}
				}

				content = ips.templates.render('core.rating.wrapper', {
					content: content,
					status: ( options.userRated ) ? ips.pluralize( ips.getString('youRatedThis'), [ options.userRated ] ) : ''
				});
				
				elem.append( content );

				// Get new rating elem
				ratingElem = elem.find('.ipsRating');

				// Size?
				if( options.size ){
					ratingElem.addClass( 'ipsRating_' + options.size );
				}

				// URL?
				/*if( options.url ){
					ratingElem.after( ips.templates.render('core.rating.loading') );
				}*/
			},

			/**
			 * User hovers on a star
			 * If rating is possible, highlight the stars up the one being hovered
			 *
			 * @param	{event}		e 		Event object
			 * @returns {void}
			 */
			_enterStar = function (e){
				if( ( selected != null && !options.changeRate ) || !options.canRate || loading ){
					return;
				}

				_starActive( $( e.currentTarget ).attr('data-ratingValue'), true );
			},

			/**
			 * User stops hovering on a star
			 * If rating was possible, unhighlight all the stars then set them back to the proper rating
			 *
			 * @param	{event}		e 		Event object
			 * @returns {void}
			 */
			_leaveStar = function (e) {
				if( ( selected != null && !options.changeRate ) || !options.canRate || loading ){
					return;
				}

				// Put the rating back to what it was
				_starActive( selected, false );
			},

			/**
			 * User clicks a star
			 * If rating is possible, either fire an ajax request or set a radio
			 *
			 * @param	{event}		e 		Event object
			 * @returns {void}
			 */
			_clickStar = function (e) {
				e.preventDefault();

				if( ( selected != null && !options.changeRate ) || !options.canRate || loading ){
					return;
				}

				var value = $( e.currentTarget ).attr('data-ratingValue');
				selected = value;
				userRated = true;

				_starActive( value );

				// Animate the selected one
				ips.utils.anim.go( 'pulseOnce', $( e.currentTarget ) );

				elem.find('[data-role="ratingStatus"]').text( ips.pluralize( ips.getString('youRatedThis'), [ value ] ) );

				// If this is pinging a URL, do that now
				if( options.url ){
					_remoteRating( value );
					return;
				}

				// Set the form field
				elem
					.find('input[type="radio"]')
						.prop( 'checked', false )
						.filter('input[type="radio"][value="' + value + '"]')
							.prop( 'checked', true );

				elem.trigger('ratingSaved', {
					value: value
				});				
			},

			/**
			 * Makes a star active, either in 'on' or 'hover' state
			 *
			 * @param	{number}		value 		Value up to and including the highlighted value
			 * @param 	{boolean}		hover 		Should the value be shown as 'hover'?
			 * @returns {void}
			 */
			_starActive = function (value, hover) {
				ratingElem
					.find('> ul[data-role="ratingList"]')
						.toggleClass('ipsRating_mine', ( hover || userRated ) )
					.end()
					.find('.ipsRating_half').each(function(){
						$(this).replaceWith( ips.templates.render('core.rating.star', {
							value: $(this).attr('data-ratingValue'),
							className: 'ipsRating_off'
						}) );
					})
					.end()
					.find('li')
						.removeClass('ipsRating_on')
						.removeClass('ipsRating_hover')
						.addClass('ipsRating_off')
					.end()
					.find('li[data-ratingValue="' + value + '"]')
						.prevAll('li')
						.andSelf()
							.removeClass('ipsRating_off')
							.addClass( 'ipsRating_on');
			},

			/**
			 * Handles pinging a URL with the rating value
			 *
			 * @param	{number}		value 		Value the user rated
			 * @returns {void}
			 */
			_remoteRating = function (value) {
				_setLoading( true );

				var statusElem = elem.find('[data-role="ratingStatus"]');

				// Show loading
				statusElem.html( ips.templates.render('core.rating.loading' ) );

				ips.getAjax()( options.url, {
					data: {
						rating: parseInt( value )
					}
				})
					.done( function (response) {
						statusElem.text( ips.getString('rating_saved') );
						elem.trigger('ratingSaved', {
							value: value
						});
					})
					.fail( function (jqXHR) {
						statusElem.text( ips.getString('rating_failed') );
						elem.trigger('ratingFailed', {
							value: value
						});
					})
					.always( function () {
						//_setLoading( false );
					});
			},

			/**
			 * Toggle the loading status of the widget
			 *
			 * @param 	{boolean} 	isLoading 	Show as loading?
			 * @returns {void}
			 */
			_setLoading = function (isLoading) {
				loading = isLoading;
				ratingElem.toggleClass( 'ipsRating_loading', isLoading );
			};

			init();

			return {};
		};

		return {
			respond: respond
		};
	});

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.selectTree.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.selectTree.js - Allows users to select values from a dynamic tree select
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.selectTree', function(){

		var defaults = {
			multiple: false,
			selected: false,
			searchable: true,
			placeholder: ips.getString('select')
		};

		var respond = function (elem, options) {
			if( !$( elem ).data('_selecttree') ){
				$( elem ).data('_selecttree', selectTreeObj( $( elem ), _.defaults( options, defaults ) ) );
			}
		},

		/**
		 * Retrieve the select tree instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The select tree instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_selecttree') ){
				return $( elem ).data('_selecttree');
			}

			return undefined;
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		};

		/**
		 * Select Tree instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var selectTreeObj = function (elem, options) {

			var results = null,
				elemID = null,
				selectedItems = [],
				name = '';

			var init = function () {
				elemID = elem.identify().attr('id');
				results = elem.find('.ipsSelectTree_nodes');
				name = elem.attr('data-name');

				// Events
				elem.on( 'click', _toggleResults );
				elem.on( 'click', '[data-action="getChildren"]', _toggleChildren );
				elem.on( 'click', '[data-action="nodeSelect"]', _toggleNodeSelection );

				elem.on( 'click', '[data-action="nodeLoadMore"] a:not(.ipsButton_disabled)', _nodeLoadMore );

				if( $('input[name="' + name + '-zeroVal"]') ){
					$('input[name="' + name + '-zeroVal"]').on( 'change', _zeroValChange );
				}

				// Show the placeholder if nothing is selected
				if( options.selected ){
					try {
						var preSelected = $.parseJSON( options.selected );
					} catch( err ) { }

					if( preSelected && _.isObject( preSelected ) && _.size( preSelected ) ){
						_buildPreSelected( preSelected );
						return;
					}
				}

				elem
					.find('.ipsSelectTree_value')
						.addClass('ipsSelectTree_placeholder')
						.text( ( options.placeholder ) ? options.placeholder : ips.getString('select') );

				_zeroValChange();
			},

			/**
			 * Destruct this widget on this element
			 *
			 * @returns {void}
			 */
			destruct = function () {
				$( document ).off('click.' + elemID );
			},

			/**
			 * Builds the values that are already selected
			 *
			 * @param 	{object} 	preSelected 	Object containing pre-selected node data
			 * @returns {void}
			 */
			_buildPreSelected = function (preSelected) {
				if( _.size( preSelected ) ){
					_.each( preSelected, function (val, key) {
						selectedItems.push( key );

						if( options.multiple ){
							var id = key;
							if ( val.id ) {
								id = val.id;
							}
							_addToken( val.title, id );
						} else {
							_setValue( val.title );
						}

						// Check our results panel and select if it exists
						if( results.find('[data-id="' + key + '"]').length ){
							results.find('[data-id="' + key + '"]').addClass('ipsSelectTree_selected');
						}
					});

					_updateSelectedValues();
					
					// Emit event that indicates our initial values are in place
					elem.trigger( 'nodeInitialValues', {
						selectedItems: selectedItems
					});
				}
			},

			/**
			 * Event handler for changing the state of the 'zero val' checkbox
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			_zeroValChange = function (e) {
				elem.toggleClass('ipsSelectTree_disabled', $('input[name="' + name + '-zeroVal"]').is(':checked') );

				if( !$('input[name="' + name + '-zeroVal"]').is(':checked') && results.is(':visible') ){
					_closeResults();
				}
			},

			/**
			 * Show or hide children in this node
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			_toggleChildren = function (e, ignoreClosed) {
				e.preventDefault();
				e.stopPropagation();

				var item = $( e.currentTarget ).closest('.ipsSelectTree_item');
				var listItem = item.closest('li');
				var id = item.attr('data-id');
				var url = options.url + '&_nodeSelect=children&_nodeId=' + id;

				if( !item.hasClass('ipsSelectTree_withChildren') ){
					// No children to load in this node
					return;
				}

				if( item.hasClass('ipsSelectTree_itemOpen') ){
					if( ignoreClosed !== true ){
						item.removeClass('ipsSelectTree_itemOpen');
						listItem.find('> [data-role="childWrapper"]').hide();
						_positionResults();
					}
				} else {
					item.addClass('ipsSelectTree_itemOpen');

					// Does this node already have children loaded?
					if( item.attr('data-childrenLoaded') ){	
						ips.utils.anim.go( 'fadeIn fast', listItem.find('> [data-role="childWrapper"]') );
						_positionResults();
					} else {
						listItem.append( $('<div/>').attr('data-role', 'childWrapper').html( ips.templates.render('core.general.loading', { text: ips.getString('loading') } ) ) );
						_positionResults();

						// Fetch it
						ips.getAjax()( url )
							.done( function (response) {
								item.attr( 'data-childrenLoaded', true );
								listItem.find('[data-role="childWrapper"]').html( response.output );
								listItem.find('[data-role="childWrapper"] .ipsSelectTree_item').each(function(){
									if ( $(this).attr('data-id') && selectedItems.indexOf( $(this).attr('data-id') ) != -1 ) {
										$(this).addClass('ipsSelectTree_selected');
									}
								});	

								_positionResults();
							});

					}
				}
			},

			/**
			 * Toggles the selected state of a node
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			_toggleNodeSelection = function (e) {
				var node = $( e.currentTarget );

				// Is this node already selected?
				if( node.hasClass('ipsSelectTree_selected') ){
					_unselectNode( node, e );
				} else {
					_selectNode( node, e );
				}

				_updateSelectedValues();
			},

			/**
			 * Load more nodes
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			 _nodeLoadMore = function(e) {
			 	var offset = results.find('[data-action="nodeLoadMore"]').attr('data-offset');
			 	var url = options.url + '&_nodeSelect=loadMore&_nodeSelectOffset=' + offset;

			 	results.find('[data-action="nodeLoadMore"] span.ipsLoading').removeClass('ipsHide');
			 	results.find('[data-action="nodeLoadMore"] > a.ipsButton').addClass('ipsButton_disabled');

				// Fetch it
				ips.getAjax()( url )
					.done( function (response) {
						if( !_.isUndefined( response.globalOutput ) )
						{
							results.find('[data-role="globalNodeList"]').append( response.globalOutput );

							if( !_.isUndefined( response.clubsOutput ) )
							{
								results.find('[data-role="clubNodeList"]').append( response.clubsOutput );
							}
						}
						else
						{
							results.find('[data-role="nodeList"]').append( response.output );
						}

						results.find('[data-action="nodeLoadMore"] span.ipsLoading').addClass('ipsHide');

						if( response.loadMore )
						{
							results.find('[data-action="nodeLoadMore"]').attr( 'data-offset', response.loadMore );
						}
						else
						{
							results.find('[data-action="nodeLoadMore"]').addClass( 'ipsHide' );
						}

						results.find('[data-action="nodeLoadMore"] > a.ipsButton').removeClass('ipsButton_disabled');

						results.find('[data-role="nodeList"]').find('.ipsSelectTree_item').each(function(){
							if ( $(this).attr('data-id') && selectedItems.indexOf( $(this).attr('data-id') ) != -1 ) {
								$(this).addClass('ipsSelectTree_selected');
							}
						});	

						_positionResults();
					});
			 },

			/**
			 * Selects the provided node
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			_selectNode = function (node, e) {				
				// Remove selected class from other nodes
				if( !options.multiple ){
					elem.find('.ipsSelectTree_selected').removeClass('ipsSelectTree_selected');
				}

				// Add our selected class
				node.addClass('ipsSelectTree_selected');

				var title = node.find('[data-role="nodeTitle"]').text();
				var id = node.attr('data-id');

				// Add the value to the select box
				if( !options.multiple ){
					_setValue( title );
				} else {
					_addToken( title, id );
				}

				// Add value to our array
				if( options.multiple ){
					selectedItems.push( node.attr('data-id') );
				} else {
					selectedItems = [ node.attr('data-id') ];
				}

				// If this node has children, we'll also load them
				if( e ){
					_toggleChildren(e, true);
				}

				// Emit event that node items have been updated
				elem.trigger( 'nodeItemSelected', {
					title: title,
					id: id
				});

				// If we aren't allowing multiple selections, close the widget now
				if( !options.multiple && !node.hasClass('ipsSelectTree_withChildren') ){
					setTimeout( function () {
						_closeResults();
					}, 200 );
				}
			},

			/**
			 * Unselects the provided node
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			_unselectNode = function (node, e) {
				// Remove selected class from this node
				node.removeClass('ipsSelectTree_selected');

				// Remove value from our selected items
				selectedItems = _.without( selectedItems, node.attr('data-id') );
				
				// Emit event that node items have been updated
				elem.trigger( 'nodeItemUnselected', {
					title: node.find('[data-role="nodeTitle"]').text(),
					id: node.attr('data-id')
				});
				
				if( !options.multiple ){
					_setValue();
				} else {
					_removeToken( node );
				}
			},

			/**
			 * Adds a token to the select
			 *
			 * @param 	{string} 	value 		Value to set
			 * @returns {void}
			 */
			_addToken = function (title, id) {
				var valueElem = elem.find('.ipsSelectTree_value');
				var elemHeight = elem.outerHeight();

				if( !elem.find('[data-role="tokenList"]').length ){
					valueElem.html( $('<ul/>').addClass('ipsList_inline').attr('data-role', 'tokenList' ) );
				}

				elem.find('[data-role="tokenList"]').append( ips.templates.render('core.selectTree.token', {
					title: title,
					id: id
				}) );

				elem.find('.ipsSelectTree_value').removeClass('ipsSelectTree_placeholder');

				// Recheck the height
				if( elemHeight != elem.outerHeight() ){
					_positionResults();
				}
			},

			/**
			 * Removes a token from the selector
			 *
			 * @param 	{string} 	value 		Value to set
			 * @returns {void}
			 */
			_removeToken = function (node) {
				var id = node.attr('data-id');
				var tokenList = elem.find('[data-role="tokenList"]');
				var elemHeight = elem.outerHeight();

				// Find the token
				var token = tokenList.find('[data-nodeId="' + id + '"]').closest('li').remove();

				if( !tokenList.find('[data-nodeId]').length ){
					tokenList.remove();
					_setValue();
				}

				// Recheck the height
				if( elemHeight != elem.outerHeight() ){
					_positionResults();
				}

			},

			/**
			 * Updates the hidden form field containing our current values
			 *
			 * @returns {void}
			 */
			_updateSelectedValues = function () {
				elem.find('[data-role="nodeValue"]').val( _.uniq( selectedItems ).join(',') );

				// Emit event that node items have been updated
				elem.trigger( 'nodeSelectedChanged', {
					selectedItems: selectedItems
				});
			},

			/**
			 * Changes the value of the select box
			 *
			 * @param 	{string} 	value 		Value to set
			 * @returns {void}
			 */
			_setValue = function (value) {
				if( value ){
					elem.find('.ipsSelectTree_value').text( value ).removeClass('ipsSelectTree_placeholder');
				} else {
					elem.find('.ipsSelectTree_value').text( ( options.placeholder ) ? options.placeholder : ips.getString('select') ).addClass('ipsSelectTree_placeholder');
				}
			},

			/**
			 * Toggle showing the results list
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			_toggleResults = function (e) {
				if( results.is(':visible') ){
					_maybeHideResults(e);
				} else {
					_showResults(e);
				}
			},

			/**
			 * Hides the results panel if the click is not within the results (i.e. if it's on the select itself)
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			_maybeHideResults = function (e) {
				var rawResults = results.get(0);

				if( ( !$.contains( rawResults, e.target ) && rawResults != e.target ) ){
					_closeResults();
				}
			},

			/**
			 * Closes the results list
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			_closeResults = function () {
				ips.utils.anim.go( 'fadeOut fast', results );
				$( document ).off('click.' + elemID );
				elem.removeClass('ipsSelectTree_active');
				elem.trigger( 'nodeSelectionClosed' );
			},

			/**
			 * Show the results list
			 *
			 * @returns {void}
			 */
			_showResults = function () {
				$( document ).on('click.' + elemID, _closeResultsOnBlur );

				_positionResults();

				results.show();

				elem.addClass('ipsSelectTree_active');

				// Focus the text box if searching is enabled
				if( elem.find('[data-role="nodeSearch"]') ){
					elem.find('[data-role="nodeSearch"]').focus();
				}
			},

			/**
			 * Position the results panel so that it appears attached to the select
			 *
			 * @returns {void}
			 */
			_positionResults = function () {
				var above = false;
				var selectWidth = elem.outerWidth();				
				var elemTop = elem.offset().top;
				var elemHeight = elem.height();

				// Do we need to prefer upwards-opening due to being near the bottom of the browser?
				if( ( elemTop + elemHeight + results.height() ) > $(window).height() ){
					above = true;

					var scrollParent = _getScrollParent();
					var scrollParentTop = $(scrollParent).offset().top;

					Debug.log("elemTop: " + elemTop);
					Debug.log("elemTop - resultsHeight: " + (elemTop - results.height()));
					Debug.log("scrollParentTop: " + scrollParentTop);

					// If our selecttree is inside a scrollable container (including <body>), we need to make sure
					// that if it opens upwards that it won't extend beyond the top, otherwise some items won't
					// be visible. In that case, we'll go back to opening downwards.
					if( ( elemTop - results.height() ) < scrollParentTop ){
						above = false;
					}
					
				}

				// Get position of select box
				var positionInfo = {
					trigger: elem,
					target: results,
					targetContainer: elem,
					above: above
				};

				var resultsPosition = ips.utils.position.positionElem( positionInfo );

				results.css({
					top: resultsPosition.top + 'px',
					left: String( -parseFloat( results.css('borderLeftWidth') ) - parseFloat( results.css('marginLeft') ) ),
					position: 'absolute',
					zIndex: ips.ui.zIndex(),
					minWidth: selectWidth + 'px'
				});

				results.each(function(){
					this.style.setProperty('--ipsSelectTree-offset', elemHeight + 'px');
				});

				if( resultsPosition.location.vertical == 'top' ){
					results.removeClass('ipsSelectTree_bottom').addClass('ipsSelectTree_top');
					elem.removeClass('ipsSelectTree_bottom').addClass('ipsSelectTree_top');
				} else {
					results.removeClass('ipsSelectTree_top').addClass('ipsSelectTree_bottom');
					elem.removeClass('ipsSelectTree_top').addClass('ipsSelectTree_bottom');
				}
			},

			/**
			 * Close the results list when the element is blurred
			 *
			 * @param 	{event} 	e 		Event object
			 * @returns {void}
			 */
			_closeResultsOnBlur = function (e) {
				if( !_clickIsInElem( e.target ) ){
					_closeResults();
				}
			},

			/**
			 * Determines whether the provided target element is contained within the select or results list
			 *
			 * @param 	{element} 	target 		Target element
			 * @returns {boolean}
			 */
			_clickIsInElem = function (target) {
				var rawElem = elem.get(0);
				var rawResults = results.get(0);

				if( target == rawElem || target == rawResults || $.contains( rawResults, target ) || $.contains( rawElem, target ) ){
					return true;
				}

				return false;
			},

			_getScrollParent = function (includeHidden) {
				var element = elem.get(0);
				var style = getComputedStyle(element);
				var excludeStaticParent = style.position === "absolute";
				var overflowRegex = /(auto|scroll|hidden)/;

				if (style.position === "fixed") {
					return document.body;
				}
				
				for (var parent = element; (parent = parent.parentElement); ) {
					style = getComputedStyle(parent);
					if (excludeStaticParent && style.position === "static") {
						continue;
					}
					if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) {
						return parent;
					}
				}

				return document.body;
			}

			init();

			return {
				destruct: destruct
			};
		};

		ips.ui.registerWidget( 'selectTree', ips.ui.selectTree, [
			'placeholder', 'multiple', 'selected', 'url', 'searchable'
		] );

		return {
			respond: respond,
			destruct: destruct
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.sideMenu.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.sideMenu.js - Side menu widget. Simple widget that adds responsive interactivity to side menus
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.sideMenu', function(){

		var defaults = {
			type: 'radio',
			responsive: true,
			group: false
		};

		var respond = function (elem, options) {
			if( !$( elem ).data('_sidemenu') ){
				$( elem ).data('_sidemenu', sideMenuObj(elem, _.defaults( options, defaults ) ) );
			}
		};

		ips.ui.registerWidget( 'sideMenu', ips.ui.sideMenu, [
			'responsive', 'type', 'group'
		] );


		/**
		 * Side menu instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var sideMenuObj = function (elem, options) {

			var init = function () {
				if( options.responsive && ips.utils.responsive.enabled() ){
					$( elem ).on( 'click', '[data-action="openSideMenu"]', _toggleSideMenu );

					$( elem )
						.find('.ipsSideMenu_mainTitle')
							.after( $('<div/>') )
						.end()
						.find('.ipsSideMenu_mainTitle + div')
							.append( $( elem ).find('.ipsSideMenu_title, .ipsSideMenu_subTitle, .ipsSideMenu_list') );
				}

				// Set up event
				$( elem ).on( 'click', '.ipsSideMenu_item', _clickEvent );
				$( elem ).on( 'selectItem.sideMenu', _selectItem );
			},

			/**
			 * Handles click events on the items
			 *
			 * @param	{event} 	e 		Event object
			 * @returns {void}
			 */
			_clickEvent = function (e) {
				_doSelectItem( $( e.currentTarget ), e );
				_toggleSideMenu();
			},

			/**
			 * Handles a selectItem event
			 *
			 * @param	{event} 	e 		Event object
			 * @param	{object} 	data	Event data object
			 * @returns {void}
			 */
			_selectItem = function (e, data) {
				_doSelectItem( elem.find('[data-ipsMenuValue="' + data.value + '"]'), e );
			},

			/**
			 * Selects an item in the menu
			 *
			 * @param 	{element} 	item 	jQuery object containing the menu item to be selected
			 * @param	{event} 	e 		Event object
			 * @returns {void}
			 */
			_doSelectItem = function (item, e) {

				// We'll only handle the click in this widget if the item has a menu value attribute
				if( ( _.isUndefined( item.attr('data-ipsMenuValue') ) && !item.find('input[type="radio"], input[type="checkbox"]').length ) ||
						!item.length ){
					return;
				}

				if( e ){
					e.preventDefault();
				}

				// If this item is disabled, bail out
				if( item.hasClass('ipsSideMenu_itemDisabled') ){
					return;
				}

				var workingItems;

				if( !options.group ){
					workingItems = $( elem ).find('.ipsSideMenu_item');
				} else {
					workingItems = item.closest('.ipsSideMenu_list').find('.ipsSideMenu_item');
				}

				if( options.type == 'check' ){
					item
						.toggleClass('ipsSideMenu_itemActive')
						.find('input[type="radio"], input[type="checkbox"]')
							.prop( "checked", function (i, val) {
								return !val; // Toggles inputs to their opposite state
							}).change();
				} else {
					workingItems
						.removeClass('ipsSideMenu_itemActive')
						.find('input[type="radio"], input[type="checkbox"]')
							.prop( "checked", false );
					item
						.addClass('ipsSideMenu_itemActive')
						.find('input[type="radio"], input[type="checkbox"]').prop( "checked", true ).change();
				}

				// Get all selected items
				var selectedItems = [];

				workingItems.filter('.ipsSideMenu_itemActive').each( function () {
					selectedItems.push( $( this ).attr('data-ipsMenuValue') );
				});

				$( elem ).trigger('itemClicked.sideMenu', {
					id: $( elem ).identify().attr('id'),
					menuElem: $( elem ),
					selectedElem: item,
					selectedItemID: item.attr('data-ipsMenuValue'),
					selectedItems: selectedItems
				});
			},

			/**
			 * Toggles the menu when in responsive mode
			 *
			 * @param	{event} 	e 		Event object
			 * @returns {void}
			 */
			_toggleSideMenu = function (e) {
				if( e ){
					e.preventDefault();	
				}				

				var menuContainer = $( elem ).find('.ipsSideMenu_mainTitle + div');

				if( $( elem ).hasClass('ipsSideMenu_open') ){
					$( elem ).removeClass('ipsSideMenu_open');
				} else {
					$( elem ).addClass('ipsSideMenu_open');
					ips.utils.anim.go('fadeIn', menuContainer );
				}
			};

			init();

			return {};
		};

		return {
			respond: respond
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.spoiler.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.spoiler.js - Spoiler widget for use in posts
 * Content is hidden until the user elects to view it
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.spoiler', function(){

		/**
		 * Responder for spoiler widget
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @param	{event} 	e 		 	The event object passed through
		 * @returns {void}
		 */
		var respond = function (elem, options, e) {
			// Add clearfix class in case we have a floated image. It's ok to do this in the editor as well.
			if( !elem.find( '.ipsSpoiler_contents' ).hasClass('ipsClearfix') )
			{
				elem.find( '.ipsSpoiler_contents' ).addClass('ipsClearfix');
			}

			/* Don't do this in the editor */			
			if( elem.parents( '.cke_wysiwyg_div' ).length ){
				return;
			}
			
			/* Hide the contents */
			elem.contents().hide();
			
			/* Do we have an existing citation block? (quotes from older versions won't, newer will) */
			var existingHeader = elem.children('.ipsSpoiler_header');
						
			/* Build the header block */
			var header = ips.templates.render( 'core.editor.spoilerHeader' );
			
			/* Add or replace it */
			if ( existingHeader.length ) {
				existingHeader.replaceWith( header );
			} else {
				elem.prepend( header );
			}
			
			/* Set the event handler for opening/closing */
			elem.find('> .ipsSpoiler_header').on( 'click', _toggleSpoiler );
			
		},
		
		/**
		 * Event handler for toggling the spoiler visibility
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		_toggleSpoiler = function (e) {
			var header = $( e.currentTarget );
			var target = $( e.target );
			var spoiler = $( e.target ).closest('[data-ipsSpoiler]');

			if( target.is('a:not( [data-action="toggleSpoiler"] )') || ( target.closest('a').length && !target.closest('a').is('[data-action="toggleSpoiler"]') ) ){
				return;
			}

			e.preventDefault();

			if( header.hasClass('ipsSpoiler_closed') ){
				ips.utils.anim.go( 'fadeIn', header.siblings() );
				header.removeClass('ipsSpoiler_closed').addClass('ipsSpoiler_open').find('span').text( ips.getString('spoilerClickToHide') );
				$( document ).trigger('contentChange', [ spoiler ] );
			} else {
				header.siblings().hide();
				header.removeClass('ipsSpoiler_open').addClass('ipsSpoiler_closed').find('span').text( ips.getString('spoilerClickToReveal') );
			}

			e.stopPropagation();
		};
		
		// Register this widget with ips.ui
		ips.ui.registerWidget( 'spoiler', ips.ui.spoiler );

		return {
			respond: respond
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.stack.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.stack.js - Stack widget for use in ACP
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.stack', function(){

		var defaults = {
			sortable: true,
			itemTemplate: 'core.forms.stack'
		};

		var respond = function (elem, options) {
			if( !$( elem ).data('_stack') ){
				$( elem ).data('_stack', stackObj( elem, _.defaults( options, defaults ) ) );
			}
		};

		ips.ui.registerWidget( 'stack', ips.ui.stack, [
			'sortable', 'maxItems', 'itemTemplate'
		]);

		return {
			respond: respond
		};
	});

	/**
	 * Stack instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var stackObj = function (elem, options) {

		var stack = null;
		var currentIndex = 0;

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
								
			if ( !elem.attr('data-initiated') ) {
				stack = elem.find('[data-role="stack"]');
				currentIndex = _getItemCount();

				// Events
				elem.on( 'click', '[data-action="stackAdd"]', _addItem );
				elem.on( 'click', '[data-action="stackDelete"]', _deleteItem );
				elem.on( 'keydown', '[data-role="stackItem"] input[type="text"]', _keyDown );
	
				if( options.sortable ){
					ips.loader.get( ['core/interface/jquery/jquery-ui.js'] ).then( function () {
						stack.sortable( {
							handle: '[data-action="stackDrag"]'
						});
					});
				}
				
				elem.attr( 'data-initiated', 'true' );

				$( elem ).trigger( 'stackInitialized', {
					count: _getItemCount()
				});
			}
		},

		/**
		 * Event handler for keydown event in a stack textbox
		 * Creates a new stack row if Enter is pressed
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_keyDown = function (e) {
			if( e.keyCode == ips.ui.key.ENTER ){
				e.preventDefault();
				_addItem(null, $( e.currentTarget ).closest('[data-role="stackItem"]') );
			}
		},

		/**
		 * Event handler for the Add Item link
		 * Adds a new stack row, either at the end of the stack or after a provided element
		 *
		 * @param 	{event} 	e 			Event object
		 * @param 	{element} 	[after] 	Optional element after which the new row should be inserted
		 * @returns {void}
		 */
		_addItem = function (e, after) {
						
			if( e ){
				e.preventDefault();
			}

			if( options.maxItems && _getItemCount() >= options.maxItems ){
				return;
			}

			currentIndex++;
			
			var field = stack.find('[data-ipsStack-wrapper]')
				.first()
				.html()
				.replace( /(name=['"][a-zA-Z0-9\-_]+?)\[([^\]]+?)?\]/g, '$1[' + currentIndex + ']' )
				.replace( /data-ipsFormData=['"](.+?)['"]/ig, '' )
				.replace( /id=['"](.+?)['"]/g, 'id="$1_' + currentIndex + '"' )
				.replace( /data-toggles=['"](.+?)['"]/g, function (match, p1) {
					var pieces = p1.split(',');
					var newPieces = [];

					_.each( pieces, function (val) {
						if( val.match( /_[0-9]+$/g) ){
							newPieces.push( val + '_' + currentIndex );
						} else {
							newPieces.push( val );
						}
					});

					return 'data-toggles="' + newPieces.join(',') + '"';
				});

			field = field.replace( /\<input(.+?)value=['"](.*?)['"](.*?)\>/g, '<input$1value=""$3>' );

			if( stack.find('select').length ) {
				field = field.replace( /\<option(.+?)selected(?:=['"]selected["'])?(.*?)\>/g, '<option$1$2>' );
			}

			var html = ips.templates.render( options.itemTemplate, {
				field: field
			});

			// Insert the new row either at the end of the stack or after the current item
			if( after ){
				after
					.after( html )
					.next('[data-role="stackItem"]')
						.find('input,textarea')
							.focus();
			} else {
				stack
					.append( html )
					.find('[data-role="stackItem"] input,[data-role="stackItem"] textarea')
						.last()
						.focus();
			}

			if( options.maxItems && _getItemCount() >= options.maxItems ){
				elem.find('[data-action="stackAdd"]').hide();
			}

			$( document ).trigger( 'contentChange', [ elem ] );

			$( elem ).trigger( 'stackRowAdded', {
				count: _getItemCount()
			});
		},

		/**
		 * Event handler for the Delete Item link
		 * Removes the row from the stack
		 *
		 * @param 	{event} 	e 			Event object
		 * @returns {void}
		 */
		_deleteItem = function (e) {
			e.preventDefault();
			var row = $( e.currentTarget ).closest('[data-role="stackItem"]');

			if( _getItemCount() === 1 ){
				// Only one item left, so just empty it
				row.find('input,textarea').val('');
				row.find("option:selected").removeAttr("selected");
				return;
			}

			ips.utils.anim.go( 'fadeOutDown', row )
				.done( function () {
					row.hide();
					// Add a little timeout before removing, so that any widgets
					// which rely on clicks work properly, e.g. menus
					setTimeout( function () {
						row.remove();
                        if( options.maxItems && _getItemCount() < options.maxItems ){
                            elem.find('[data-action="stackAdd"]').show();
                        }
					}, 100);
				});
		},

		/**
		 * Returns a count of the number of items in the stack
		 *
		 * @returns {number}
		 */
		_getItemCount = function () {
			return stack.find('[data-role="stackItem"]').length;
		};

		init();
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.sticky.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.sticky.js - A component that enables elements to be 'sticky'
 * A sticky element sits in place until it's about to scroll offscreen, at which
 * point it sticks to the top of the screen, making it always visible.
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.sticky', function(){

		// Default widget options
		var defaults = {
			stickyClass: 'ipsSticky',
			stickTo: 'top',
			spacing: 0,
			disableIn: 'phone'
		};

		/**
		 * Responder for sticky widget
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var respond = function (elem, options) {
			$( elem ).data( '_sticky', stickyObj(elem, _.defaults( options, defaults ) ) );
		},

		/**
		 * Retrieve the sticky element instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The sticky element instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_sticky') ){
				return $( elem ).data('_sticky');
			}

			return undefined;
		},

		/**
		 * Destruct this widget on this element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {void}
		 */
		destruct = function (elem) {
			var obj = getObj( elem );

			if( !_.isUndefined( obj ) ){
				obj.destruct();
			}
		};

		// Register this widget with ips.ui
		ips.ui.registerWidget( 'sticky', ips.ui.sticky, [ 
			'stickyClass', 'relativeTo', 'spacing', 'stickTo', 'width', 'disableIn'
		]);

		/**
		 * Sticky instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var stickyObj = function (elem, options) {

			var relativeTo = false,
				originalStyles = {},
				originalOffsetTop,
				status,
				dummyElem,
				locked = false;

			/**
			 * Initialization
			 *
			 * @returns {void}
			 */
			var _init = function () {

				if( !$( elem ).is(':visible') ){
					Debug.warn("Can't set up a sticky element if the element is hidden when init is called.");
					return;
				}

				// Sort out the parent wrapper
				relativeTo = options.relativeTo ? $( options.relativeTo ) : false;
				
				// Disable in breakpoints
				if( options.disableIn ){
					options.disableIn = _.map( options.disableIn.split(','), function (item) {
						return item.trim()
					});
				}

				// Remember the original styles
				originalStyles = {
					top: $( elem ).css('top'),
					bottom: $( elem ).css('bottom'),
					position: $( elem ).css('position'),
					width: $( elem ).get(0).style.width
				};

				status = 'normal';

				originalOffsetTop = $( elem ).offset().top;

				// Trigger now too
				_scrollDocument();

				// Set scroll & resize event
				$( document )
					.on( 'scroll', _scrollDocument )
					.on( 'breakpointChange', _breakpointChange );

				$( window ).on( 'resize', _windowResize );

				$( elem ).trigger('stickyInit', {
					id: $( elem ).identify().attr('id'),
					status: status
				});
			},

			/**
			 * Destruct this widget on this element
			 *
			 * @returns {void}
			 */
			destruct = function () {
				$( document )
					.off( 'scroll', _scrollDocument )
					.off( 'breakpointChange', _breakpointChange );

				$( window ).off( 'resize', _windowResize );
			},

			/**
			 * Event handler for responsive breakpoint changes
			 * If the current breakpoint is included in the 'disableIn' option, then
			 * we reset the element to 'normal', and set locked to true, which will prevent
			 * fixed mode from being enabled. We unset locked if in an acceptable breakpoint.
			 *
			 * @returns {void}
			 */
			_breakpointChange = function (e, data) {
				if( !ips.utils.responsive.enabled ){
					return;
				}

				if( _.indexOf( options.disableIn, data.curBreakName ) !== -1 ){
					_makeNormal();
					locked = true;
				} else {
					locked = false;
				}
			},

			/**
			 * Event handler for window resizing
			 *
			 * @returns {void}
			 */
			_windowResize = function () {
				// This is expensive, but it's the easiest way to stop the element 'falling out' of its
				// wrapper when the window resizes.
				// If the window is resized, we have to remove the fixed positioning, calculate the new top
				// position, then call our method to see if it should return to being fixed.
				if( $( elem ).is(':visible') ){
					_makeNormal();
					originalOffsetTop = $( elem ).offset().top;
					_scrollDocument();
				}
			},

			/**
			 * Event handler for document scrolling
			 *
			 * @returns {void}
			 */
			_scrollDocument = function () {
				var bodyScroll = $( document ).scrollTop();
				var elemSize = $( elem ).outerHeight();
				var originalBottom = originalOffsetTop + elemSize;
				var wrapperSize = $( elem ).outerHeight();
				var viewportHeight = $( window ).height();

				if( _.indexOf( options.disableIn, ips.utils.responsive.getCurrentKey() ) !== -1 ){
					_makeNormal();
					locked = true;
				} else {
					locked = false;
				}
				
				if( options.stickTo == 'bottom' ){
					// If the bottom of the element is offscreen, and the status hasn't already been changed,
					// set it to fixed. Otherwise, it should be set to normal.
					if( ( ( viewportHeight + bodyScroll ) <= ( originalBottom + options.spacing ) ) &&
							status == 'normal' ){
						_makeFixed();
					} else if( ( ( viewportHeight + bodyScroll ) >= ( originalBottom + options.spacing ) ) &&
							status == 'fixed' ){
						_makeNormal();
					}
				} else {

					// If the top of our element goes off the screen and we're currently 'normal', then
					// set the element to fixed
					if( bodyScroll >= ( originalOffsetTop - options.spacing ) ){

						// If we're working relative to a parent, and the sticky element would go outside the bounds
						// of the parent, then we'll keep it fixed but adjust the top so it stays inside.
						if( relativeTo ){
							var relativeHeight = relativeTo.height();
							var relativePosition = relativeTo.offset();

							if( ( options.spacing + elemSize ) > ( relativePosition.top + relativeHeight - bodyScroll ) ){
								_makeFixed( -( ( elemSize ) - ( relativePosition.top + relativeHeight - bodyScroll ) ) );
							} else if( status == 'normal' ){
								_makeFixed();
							}

						} else if( status == 'normal' ) {
							_makeFixed();
						}
					} else if( bodyScroll <= ( originalOffsetTop - options.spacing ) ){
						if( status == 'fixed' ){
							_makeNormal();	
						}					
					}
				}
			},

			/**
			 * Puts the element into 'fixed' mode at the top
			 *
			 * @returns {void}
			 */
			_makeFixed = function (offset) {

				if( locked ){
					return;
				}

				var width;

				if( !dummyElem && !relativeTo ){
					_makeDummyElem();
				}

				// If we're already fixed, we might just be changing the offset - short circuit if so
				if( status == 'fixed' && !_.isUndefined( offset ) ){
					$( elem ).css( {
						top: ( offset ) + 'px'
					});

					$('#ipsStickySpacer').remove();
					return;
				}

				var bottomSpacing = $( document ).height() - $( window ).height() - $( document ).scrollTop() - 10;

				// Do we need to add bottom spacing too?
				// THis is needed for edge cases. If the sticky header is, say, 100px high, but when we reach it
				// there's only 80px of scroll left on the document, the header will constantly pop in and out of fixed
				// positioning. To get around this, we can add a spacer to the end of the document, allowing sufficient scrolling
				// space for the document.
				if( options.stickTo == 'top' && bottomSpacing < $( elem ).outerHeight() ){
					_makeBottomSpacer( bottomSpacing );
				}
				
				// Figure out what width we should set the element to
				if( options.width && ( options.width.indexOf('#') === 0 || options.width.indexOf('.') === 0 ) ){
					width = $( options.width ).first().outerWidth();
				} else if( options.width ){
					width = parseInt( options.width );
				} else {
					//width = originalStyles.width;
					width = $( elem ).css('width');
				}

				$( elem )
					.css( {
						position: 'fixed',
						width: width,
						zIndex: ips.ui.zIndex()
					})
					.addClass( options.stickyClass );

				// Fix the element in position, to the correct browser side
				if( options.stickTo == 'bottom' ){
					$( elem )
						.css( { bottom: options.spacing + 'px' } )
						.addClass( options.stickyClass + '_bottom');
				} else {
					$( elem )
						.css( { top: options.spacing + 'px' } )
						.addClass( options.stickyClass + '_top');
				}

				if( !relativeTo ){
					dummyElem
						.css( {
							width: String($( elem ).width()),
							height: String($( elem ).outerHeight())
						})
						.show();	
				}			

				status = 'fixed';

				$( elem ).trigger( 'stickyStatusChange.sticky', {
					status: 'fixed'
				});
			},

			/**
			 * Returns the element to 'normal' mode
			 *
			 * @returns {void}
			 */
			_makeNormal = function () {
				$( elem )
					.css( {
						position: String(originalStyles.position),
						width: String(originalStyles.width)
					})
					.removeClass( options.stickyClass )
					.removeClass( options.stickyClass + '_top' )
					
				// Reset the value we set earlier
				if( options.stickTo == 'bottom' ){
					$( elem )
						.css( { bottom: String(originalStyles.bottom) } )
						.removeClass( options.stickyClass + '_bottom' );
				} else {
					$( elem )
						.css( { top: String(originalStyles.bottom) } )
						.removeClass( options.stickyClass + '_top' );
				}	

				if( dummyElem ){
					dummyElem.hide();
				}

				_makeBottomSpacer( 0 );

				status = 'normal';

				$( elem ).trigger( 'stickyStatusChange.sticky', {
					status: 'normal'
				});
			},

			/**
			 * Builds a dummy element which will take up the space of the main element, when
			 * the main element is in 'fixed' mode
			 *
			 * @returns {void}
			 */
			_makeDummyElem = function () {
				dummyElem = $('<div/>')
					.insertBefore( elem )
					.hide();
			},

			/**
			 * Adds an element to the bottom of the document which acts as a spacer allowing proper scrolling
			 *
			 * @param 	{number} 	size 	Size the spacer should be
			 * @returns {void}
			 */
			_makeBottomSpacer = function (size) {
				if( !$('#ipsStickySpacer').length ){
					$('<div/>').attr('id', 'ipsStickySpacer').insertAfter( elem );
				}

				$('#ipsStickySpacer').css({
					height: ( size + 10 ) + 'px'
				});
			};

			_init();

			return {
				destruct: destruct
			};
		};

		return {
			respond: respond,
			destruct: destruct
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.tabbar.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.tabbar.js - A tab bar UI component
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.tabbar', function(){

		// Default widget options
		var defaults = {
			itemSelector: '.ipsTabs_item', // The CSS selector used to find clickable tab items
			activeClass: 'ipsTabs_activeItem', // Classname applied to the active item
			loadingClass: 'ipsLoading ipsTabs_loadingContent', // Classname applied to loading panel
			panelClass: 'ipsTabs_panel', // Classname applied to panels
			panelPrefix: 'ipsTabs',
			updateURL: true, // Whether the browser URL should be updated when tab is switched
			updateTitle: false, // Whether the browser title should also be updated, when updateURL is true
			disableNav: false // Disables fancy tab loading functionality. Ideal for using this widget just for the mobile tab menu.
		};

		/**
		 * Responder for tab widget
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var respond = function (elem, options) {
			if( !$( elem ).data('_tabbar') ){
				$( elem ).data('_tabbar', tabBarObj(elem, _.defaults( options, defaults ) ) );
			}
		};
		
		// Register this widget with ips.ui
		ips.ui.registerWidget('tabbar', ips.ui.tabbar, [ 
			'contentArea', 'itemSelector', 'activeClass', 'loadingClass', 'disableNav',
			'panelClass', 'updateURL', 'updateTitle', 'panelPrefix', 'defaultTab'
		]);

		return {
			respond: respond
		};
	});
	
	/**
	 * Tab Bar instance
	 * Handles events and logic for a single tab bar instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var tabBarObj = function (elem, options) {
				
		var rawElem = elem.get(0), //Non-jQuery element
			barId = rawElem.id, // ID of this tab bar
			tabs = $( elem ).find( options.itemSelector ),	// Collection of the tabs in this bar
			active,	// The active tab
			ajaxObj, // Reference to the ajax object we use
			loadPanel; // Reference to our loading panel

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {

			if( !barId ){
				barId = $( rawElem ).identify().attr('id');
			}
			
			if( !options.contentArea || !$( options.contentArea ).length ) {
				options.contentArea = '#' + $( rawElem ).next().identify().attr('id');
			}

			if( !tabs.length ){
				Debug.warn( "No tabs found in tab bar" + barId );
				return;
			}

			// Find our active tab
			active = _getActiveTab();

			// And do we need to enable it?
			_initializeActive();

			// Finally set the event handlers
			$( elem ).on( 'click', options.itemSelector, _handleTabClick );
			$( elem ).on( 'click', "[data-action='expandTabs']", _expandMenu );
		},

		/**
		 * Event handler for a tab click
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		_handleTabClick = function (e) {

			// If we aren't handling any navigation here, then let the browser handle it
			if( options.disableNav ){
				return;
			}

			e.preventDefault();

			_tabClickPhone( e );

			// Is this tab active?
			if( $( this ).hasClass( options.activeClass ) ){
				return;
			}

			var thisId = $( this ).identify().attr('id'),
				thisContent = $( '#' + options.panelPrefix + '_' + barId + '_' + thisId + '_panel' );

			// Does this tab content area exist already?
			if( !thisContent.length ) {
				thisContent = _createTabPanel( thisId );
				// Load content
				_loadContent( this, thisContent )
					.done( function () {
						_switchTab( thisId );
					})
					.fail( function () {
						Debug.log('failed');
					});
			} else {
				_hideAllPanels();
				_switchTab( thisId );				
			}

			// Update URL if necessary
			_updateURL( thisId );
		},

		/**
		 * Shows the phone-accessible tab menu
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		_expandMenu = function (e) {
			e.preventDefault();
				
			if( $( elem ).find( options.itemSelector ).length > 1 ){
				if( $( elem ).hasClass('ipsTabs_showMenu') ){
					$( elem ).removeClass('ipsTabs_showMenu');
				} else {
					$( elem )
						.addClass('ipsTabs_showMenu')
						.css({
							zIndex: ips.ui.zIndex()
						});

				}	
			}			
		},

		/**
		 * Clicked on a tab on the phone menu
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		_tabClickPhone = function (e) {
			$( elem ).removeClass('ipsTabs_showMenu');
		},

		/**
		 * Switches to the specified tab
		 *
		 * @param	{string} 	tabId 		ID of the tab to make active
		 * @param 	{boolean} 	immediate	Show immediately, rather than fading in?
		 * @returns {void}
		 */
		_switchTab = function (tabId, immediate) {

			// Hide all panels
			_hideAllPanels();

			// Get the new panel
			var thisContent = $( '#' + options.panelPrefix + '_' + barId + '_' + tabId + '_panel' );

			// Animate it
			if( !immediate ){
				ips.utils.anim.go( 'fadeIn', thisContent ).done( function () {
					thisContent.attr( 'aria-hidden', 'false' );

					$( elem ).trigger('tabShown', {
						barID: barId,
						tabID: tabId,
						tab: active,
						panel: thisContent
					});

					// Let everyone know
					$( document ).trigger( 'contentChange', [ thisContent ] );
				});
			} else {
				thisContent.show().attr( 'aria-hidden', 'false' );

				$( elem ).trigger('tabShown', {
					barID: barId,
					tabID: tabId,
					tab: active,
					panel: thisContent
				});

				// Let everyone know
				$( document ).trigger( 'contentChange', [ thisContent ] );
			}

			// Set as active
			active = $( '#' + tabId );

			// Switch tab
			_makeTabActive( active );

			// Let document know
			$( elem ).trigger('tabChanged', {
				barID: barId,
				tabID: tabId,
				tab: active,
				panel: thisContent
			});
		},

		/**
		 * Updates the browser URL
		 *
		 * @param	{string} 	tabId 	ID of the tab to make active
		 * @returns {void}
		 */
		_updateURL = function (tabId) {
			if( !options.updateURL ){
				return;
			}

			var href = $( '#' + tabId ).attr('href'),
				title = ( options.updateTitle && $( '#' + tabId ).attr('title') ) ? $( '#' + tabId ).attr('title') : document.title;

			if( !_.isEmpty( href ) && !href.startsWith('#') ){
				// Replace state
				History.replaceState( {}, title, href );
				// Track page view
				ips.utils.analytics.trackPageView( href );
			}
		},

		/**
		 * Determines which tab is 'active'
		 *
		 * @returns 	{element}	The tab deemed to be 'active'
		 */
		_getActiveTab = function () {

			// Try css class first
			var activeTab = elem.find( '.' + options.activeClass );
			
			if( activeTab.length ){
				return activeTab.get(0);
			}

			// Next see if a default is specified
			if( options.defaultTab && $( elem ).find( options.defaultTab ) ){
				return $( elem ).find( options.defaultTab ).get(0);
			}

			// Finally just return the first tab
			return $( elem ).find( options.itemSelector ).first();
		},

		/**
		 * Initializes the tab that is first to be active
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		_initializeActive = function () {			
			// Do we have an active panel?
			var activeId = $( active ).identify().attr('id');

			if( !$( '#' + options.panelPrefix + '_' + barId + '_' + activeId + '_panel' ).length ){
				if( $( options.contentArea ).children().length ){
					// We have content in the content area, so make that this panel
					$( options.contentArea ).wrapInner( _createTabPanel( activeId, true ) );
					_switchTab( activeId, true );
				} else {
					// Load content
					var newPanel = _createTabPanel( activeId );
					_loadContent( active, newPanel ).done( function () {
						_switchTab( activeId );
					});
				}
			} else {
				_switchTab( activeId, true );
			}
		},

		/**
		 * Makes a tab active and other tabs inactive
		 *
		 * @param 	{element} 	activeTab 	Reference to the tab to make active
		 * @returns {void}
		 */
		_makeTabActive = function (activeTab) {

			// Unselect all tabs
			$( elem )
				.find( options.itemSelector )
				.removeClass( options.activeClass )
				.removeAttr( 'aria-selected' );

			// Select the new tab
			$( activeTab )
				.addClass( options.activeClass )
				.attr( 'aria-selected', 'true' );
		},

		/**
		 * Loads and inserts content via ajax
		 *
		 * @param	{element} 	tab 		The tab element being loaded
		 * @param	{element} 	container 	The container panel for the content
		 * @returns {promise} 	Promise object
		 */
		_loadContent = function (tab, container) {
			var deferred = $.Deferred();

			// Hide all other panels before we start
			_hideAllPanels();

			// Which URL should we load?
			if( $( tab ).attr('data-tabURL') ){
				var url = $( tab ).attr('data-tabURL');
			} else {
				var url = $( tab ).attr('href');
			}

			// Set loading class
			$( options.contentArea ).addClass( options.loadingClass );

			// Get ajax object
			ajaxObj = ips.getAjax();
			
			ajaxObj( url )
				.done( function (response) {
					container.html( response );

					// Let everyone know
					//$( document ).trigger( 'contentChange', [ container ] );

					// Resolve promise so callbacks can execute
					deferred.resolve();
				})
				.fail( function (jqXHR, status, errorThrown) {
					window.location = $( tab ).attr('href');
				})
				.always( function () {
					$( options.contentArea ).removeClass( options.loadingClass );
				});

			return deferred.promise();
		},

		/**
		 * Hides all tab panels
		 * @returns 	{element} 	The new panel
		 */
		_hideAllPanels = function () {
			$( options.contentArea )
				.find( '> .' + options.panelClass )
				.hide()
				.attr('aria-hidden', 'true');
		},

		/**
		 * Creates an empty panel for the specific tab
		 *
		 * @param	{string} 	tabId 		Tab ID from which the panel is being created
		 * @returns {element} 	The new panel
		 */
		_createTabPanel = function (tabId, noAppend) {

			var newPanel = $('<div/>')
					.attr( { 'id': options.panelPrefix + '_' + barId + '_' + tabId + '_panel' } )
					.addClass( options.panelClass )
					.attr( { 'aria-labelledby': tabId } );

			if( !noAppend ){
				$( options.contentArea ).append( newPanel );
			}

			return newPanel;
		};

		init();

		return {
			init: init
		}
	};

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.toggle.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.toggle.js - A toggle UI component that replaces checkboxes
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.createModule('ips.ui.toggle', function(){

		// Default widget options
		var defaults = {
			template: 'core.forms.toggle' // The template used to build the toggle
		};

		/**
		 * Responder for toggle widget
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var respond = function (elem, options) {
			if( !$( elem ).data('_toggle') ){
				$( elem ).data('_toggle', toggleObj(elem, _.defaults( options, defaults ) ) );
			}
		};

		// Register this widget with ips.ui
		ips.ui.registerWidget('toggle', ips.ui.toggle, [ 
			'template'
		]);

		return {
			respond: respond
		};
	});


	/**
	 * Toggle instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var toggleObj = function (elem, options, e) {

		var checkID = $( elem ).identify().attr('id'),
			wrapper;

		/**
 		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			
			var status = $( elem ).prop('checked'),
				className = ( status ) ? 'ipsToggle_on' : 'ipsToggle_off';

			// Build toggle wrapper
			$( elem )
				.after( 
					ips.templates.render( options.template, {
						id: checkID + '_wrapper',
						status: !!status,
						className: className
					})
				)
				.hide();

			// Set events on wrapper
			wrapper = $('#' + checkID + '_wrapper');
			wrapper
				.on( 'click', function (e) {
					if( !$( elem ).is(':disabled') ){
						if( $( elem ).prop('checked') ){
							_doToggle('off');
						} else {
							_doToggle('on');
						}

						$( elem ).change();
					}
					
					e.preventDefault();
				})
				.on( 'keypress', _keyPress );

			// Did checkbox have a tooltip? Put it on the toggle instead
			if( $( elem ).is('[data-ipstooltip]') ){
				wrapper
					.attr('data-ipsTooltip', '')
					.attr('title', $( elem ).attr('title') );

				$( document ).trigger( 'contentChange', [ wrapper ] ); 
			}

			/* Is it disabled? */
			if( $( elem ).is(':disabled') ){
				wrapper.addClass('ipsToggle_disabled');
			}

			// Set events on checkbox
			$( elem )
				.on( 'change', function (e) {
					// The action we take here is the opposite of what's called in the wrapper click
					// event, because by the time the change event is called, the checkbox value has
					// already been changed by the browser.
					if( $( elem ).is(':checked') ){
						_doToggle('on');
					} else {
						_doToggle('off');
					}
				});
		},

		/**
 		 * Change the value of the toggle widget and the checkbox
		 *
		 * @param 		{string} 	type 	'on' or 'off' - the state the widget will be set to
		 * @returns 	{void}
		 */
		_doToggle = function (type) {
			if( type == 'off' ){
				wrapper
					.removeClass('ipsToggle_on')
					.addClass('ipsToggle_off')
					.attr('aria-checked', false);

				elem.get(0).checked = false;
			} else {
				wrapper
					.removeClass('ipsToggle_off')
					.addClass('ipsToggle_on')
					.attr('aria-checked', true);

				elem.get(0).checked = true;
			}

			// Programatically checking a box doesn't fire the change event. Fire it manually so that anything
			// observing the checkbox is told about it.
			//elem.trigger('change');
		},

		/**
 		 * Event handler for keypress when the widget has focus. Enables us to toggle the widget
 		 * with the keyboard
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_keyPress = function (e) {
			if( e.keyCode == ips.ui.key.SPACE || e.keyCode == ips.ui.key.ENTER ){
				e.preventDefault();

				if( $( elem ).prop('checked') ){
					_doToggle('off');
				} else {
					_doToggle('on');
				}

				$( elem ).change();
			}
		};

		init();

		return {
			init: init
		}

	};
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.tooltip.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.tooltip.js - Tooltip UI component
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.tooltip', function(){

		var _animating = false,
			_tooltip = null,
			_timer = [],
			_currentElem = null;

		/**
		 * Widget responder
		 * Creates the tooltip if it doesn't exist, then gets the content and shows/hides tooltip
		 *
		 * @param 	{element} 	elem		Original element
		 * @param 	{object} 	options		Widget options
		 * @param 	{event} 	e 			Event object
		 * @returns {void}
		 */
		var respond = function (elem, options, e) {
			
			// Don't show tooltips on touch devices
			if( ips.utils.events.isTouchDevice() ){
				return;
			}
			
			if( !_tooltip ){
				_createTooltipElement();
			}

			var content = _getContent( elem, options );

			if( e.type == 'mouseleave' || e.type == 'blur' || e.type == 'focusout' ){
				_hide();
			} else {
				if( content ){
					_show( elem, options, content );
				}
			}
		},
		
		/**
		 * Works out the positioning of the tooltip in order to show it
		 *
		 * @param 	{element} 	elem 		Original element
		 * @param 	{element} 	_tooltip		Tooltip element
		 * @returns {void}
		 */
		_calculatePosition = function (elem, _tooltip) {
			// Set up the data we'll use to position it
			var positionInfo = {
				trigger: elem,
				target: _tooltip,
				center: true,
				above: true,
				stemOffset: { left: 10, top: 0 }
			};

			var tooltipPosition = ips.utils.position.positionElem( positionInfo );

			$( _tooltip ).css({
				left: tooltipPosition.left + 'px',
				top: tooltipPosition.top + 'px',
				position: ( tooltipPosition.fixed ) ? 'fixed' : 'absolute',
				zIndex: ips.ui.zIndex()
			});

			if( tooltipPosition.location.vertical == 'top' ){
				_tooltip.addClass('ipsTooltip_top').removeClass('ipsTooltip_bottom');
			} else {
				_tooltip.addClass('ipsTooltip_bottom').removeClass('ipsTooltip_top');
			}

			_tooltip.removeClass('ipsTooltip_left').removeClass('ipsTooltip_right');

			if( tooltipPosition.location.horizontal == 'left' ){
				_tooltip.addClass('ipsTooltip_left');
			} else if( tooltipPosition.location.horizontal == 'right' ){
				_tooltip.addClass('ipsTooltip_right');
			}
		},
		
		/**
		 * Actually show a tooltip
		 *
		 * @param 	{element} 	elem 		Original element
		 * @param 	{object} 	options		Widget options
		 * @param	{string}	content 	Content of tooltip
		 * @returns {void}
		 */
		_show = function (elem, options, content) {
			
			elem = $( elem );
			
			ips.utils.anim.cancel( _tooltip );

			// Hide the tooltip and update the content
			if( options.safe ){
				_tooltip.hide().html( content );
			} else {
				_tooltip.hide().text( content );
			}
			
			// Fire an AJAX request for the real content if needed
			if ( options.ajax && !elem.data('_tooltip') ) {
				ips.getAjax()( options.ajax ).done(function(response){
					elem.data( '_tooltip', response );
					if( options.safe ){
						_tooltip.html( response );
					} else {
						_tooltip.text( response );
					}
					_calculatePosition(elem, _tooltip);
				}.bind(this));
			}

			// Remove the title if any
			if( elem.attr('title') ){
				elem
					.attr( '_title', elem.attr('title') )
					.removeAttr('title');
			}

			// Show it
			_calculatePosition(elem, _tooltip);
			_tooltip.show();
			_currentElem = elem;

			// Set an interval which checks the element is still on the page (useful when a dialog closes, for example)
			_timer.push( setInterval( _checkForElemPresence, 100 ) );

			$( elem ).trigger( 'tooltipShown' );
		},

		/**
		 * Hides the tooltip
		 *
		 * @returns {void}
		 */
		_hide = function () {
			ips.utils.anim.go( 'fadeOut', _tooltip );
			_currentElem = null;

			// Clear out current timers
			if( _timer.length ){
				for( var i = 0; i < _timer.length; i++ ){
					clearInterval( _timer[ i ] );
				}

				_timer = [];
			}
		},

		/**
		 * Checks that an element exists
		 *
		 * @param 	{element} 	elem 	The element to look for
		 * @returns {void}
		 */
		_checkForElemPresence = function (element) {
			if( !_currentElem || !_currentElem.length || !_currentElem.is(':visible') ){
				_hide();
			}
		},

		/**
		 * Figures out which string should form the tooltip text
		 *
		 * @param 	{element} 	elem 		Original element
		 * @param 	{object} 	options		Widget options
		 * @returns {string}
		 */
		_getContent = function (elem, options) {
			elem = $( elem );

			if ( elem.data('_tooltip') ) {
				return elem.data('_tooltip');
			}
			else if( options.label ){
				if ( options.json ) {
					return $.parseJSON( options.label ).join("<br>");
				} else {
					return options.label;
				}
			} else if( elem.attr('aria-label') ){
				return elem.attr('aria-label');
			} else if( elem.attr('_title') ){
				return elem.attr('_title');
			} else if( elem.attr('title') ){
				return elem.attr('title');
			}

		},

		/**
		 * Creates the tooltip element from a template
		 *
		 * @returns {void}
		 */
		_createTooltipElement = function () {
			// Build it from a template
			var tooltip = ips.templates.render( 'core.tooltip', {
				id: 'ipsTooltip'
			} );

			// Append to body
			ips.getContainer().append( tooltip );

			_tooltip = $('#ipsTooltip');
		};

		// Register this module as a widget to enable the data API and
		// jQuery plugin functionality
		ips.ui.registerWidget('tooltip', ips.ui.tooltip, 
			['label', 'extraClass', 'safe', 'json', 'ajax' ], 
			{ lazyLoad: true, lazyEvents: 'mouseenter mouseleave focus blur' } 
		);

		return {
			respond: respond
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.truncate.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.truncate.js - Text truncating widget
 * Either removes text to make it fit (with dotdotdot.js), or hides the overflow with a 'show more' link
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.truncate', function(){

		var defaults = {
			type: 'remove', // Type of truncating: 'remove' cuts off text, 'hide' hides it
			size: 100, // Size the box should be, in px, lines or a selector for an element to fix inside
			expandText: ips.getString('show_more'),
			watch: true
		};

		var respond = function (elem, options) {
			if( options.type == 'remove' ){
				_removeTruncate( elem, _.defaults( options, defaults ) );
			} else {
				_hideTruncate( elem, _.defaults( options, defaults ) );
			}
		},

		/**
		 * Truncates content by removing text
		 *
		 * @param	{elem} 		elem 		The element containing text being truncated
		 * @param	{object} 	options 	The options passed into this widget
		 * @returns {void}
		 */
		_removeTruncate = function (elem, options) {		
			//First reduce to first paragraph only if this is post content
			if(elem.children().first().prop('tagName') == 'P') {
				elem.html( elem.children().first().html() );
			}
			
			// Use dotdotdot
			var size = _getSizeValue( options.size, elem );			
			var clampTo = ( size.lines ) ? size.lines : size.pixels + 'px';

			elem.dotdotdot({
				height: size.pixels,
				watch: options.watch
			});

			elem.trigger( 'contentTruncated', {
				type: 'remove'
			});
		},

		/**
		 * Truncates content by hiding text
		 *
		 * @param	{elem} 		elem 		The element containing text being truncated
		 * @param	{object} 	options 	The options passed into this widget
		 * @returns {void}
		 */
		_hideTruncate = function (elem, options) {
			if( elem.attr('data-truncated') ){
				return;
			}

			var size = _getSizeValue( options.size, elem );
			var originalSize = elem.outerHeight();
			var originalPos = $( elem ).css('position');

			// If we're smaller than the specified size anyway, just return
			if( originalSize <= size.pixels ){
				Debug.log('Smaller than the specified size, finishing...');
				return;
			}

			// If the elem isn't positioned, set it to relative
			if( originalPos == 'static' ){
				$( elem ).css( 'position', 'relative' );
			}

			// Set the size of the element
			$( elem )
				.css( {
					height: size.pixels + 'px'
				})
				.addClass('ipsTruncate');

			// Build the template
			var showMore = ips.templates.render( 'core.truncate.expand', {
				text: options.expandText
			});

			$( elem ).after( showMore );

			var expander = elem.next("[data-action='expandTruncate']");

			elem.trigger( 'contentTruncated', {
				type: 'hide'
			});

			elem.attr('data-truncated', true);

			// Hook up event
			expander.on('click', function (e) {

				// Get the expanded height. Draw it expanded for a split second to capture height then set it back
				elem.css({
					position: originalPos,
					height: ''
				});
				var newOriginalSize = elem.outerHeight();
				elem.css({ height: size.pixels + 'px', position: (originalPos === 'static') ? 'relative' : originalPos });

				// Make the expander button fly down
				ips.utils.anim.go( 'fadeOutDown fast', expander )
					.done( function () {
						expander.remove();
					});

				// Smooth animation to make it big if it needs to be bigger
				if( newOriginalSize > size.pixels ){
					elem.animate( { 'height': newOriginalSize + 'px' }, {
						done: function() {
							// When this is done, set the height back to 'auto' because this can change still
							elem.css({
								position: originalPos,
								height: ''
							});

							elem.trigger('truncateExpanded');

							// Don't need this anymore
							elem.removeClass( 'ipsTruncate' );
						}
					});
				} else {
					// Don't need this anymore
					elem.removeClass( 'ipsTruncate' );
				}
			});
		},

		/**
		 * Works out the size that we're going to truncate to in the relevant format
		 *
		 * @param	{mixed} 	value 		The value, as a selector, lines or pixel
		 * @param	{element} 	elem 		The element being truncated
		 * @returns {object}	Object of sizes, with 'lines' and/or 'pixels' keys
		 */
		_getSizeValue = function (value, elem) {
			// See if it's a selector to start with
			try {
				if( $( value ).length ){
					return { pixels: $( value ).first().outerHeight() };
				}
			} catch( err ) {}
			
			if( String(value).indexOf('lines') !== -1 ){
				// Still here? OK, see if it's lines
				var lines = parseInt( value.replace('lines', '') );
				var number = lines * _getLineHeight( elem );

				return { lines: lines, pixels: number };
			} else {
				// Assume it's pixels if all else fails
				return { pixels: parseInt( value ) };
			}
		},

		/**
		 * Returns the line-height of the element
		 *
		 * @param	{elem} 		elem 		The element
		 * @returns {number}
		 */
		_getLineHeight = function (elem) {
			var lH = $( elem ).css('line-height');
			return parseFloat( lH );
		};

		ips.ui.registerWidget( 'truncate', ips.ui.truncate,
			['type', 'size', 'expandText', 'watch']
		);

		return {
			respond: respond
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.uploader.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.uploader.js - Uploading component, serving as a wrapper to plupload
 *
 * Author: Rikki Tissier 
 */
;( function($, _, undefined){
	"use strict";
	
	var ipsUploaders = [];

	var iconMap = {
		'txt': 'file-text-o',
		'rtf': 'file-text-o',
		'csv': 'file-text-o',
		'pdf': 'file-pdf-o',
		'doc': 'file-word-o',
		'docx': 'file-word-o',
		'xls': 'file-excel-o',
		'xlsx': 'file-excel-o',
		'xlsm': 'file-excel-o',
		'zip': 'file-archive-o',
		'tar': 'file-archive-o',
		'gz': 'file-archive-o',
		'ppt': 'file-powerpoint-o',
		'pptx': 'file-powerpoint-o',
		'ico': 'file-image-o',
		'gif': 'file-image-o',
		'jpeg': 'file-image-o',
		'jpg': 'file-image-o',
		'jpe': 'file-image-o',
		'png': 'file-image-o',
		'psd': 'file-image-o',
		'webp': 'file-image-o',
		'aac': 'file-audio-o',
		'mp3': 'file-audio-o',
		'ogg': 'file-audio-o',
		'ogv': 'file-audio-o',
		'wav': 'file-audio-o',
		'm4a': 'file-audio-o',
		'avi': 'file-video-o',
		'flv': 'file-video-o',
		'mkv': 'file-video-o',
		'mp4': 'file-video-o',
		'mpg': 'file-video-o',
		'mpeg': 'file-video-o',
		'3gp': 'file-video-o',
		'webm': 'file-video-o',
		'wmv': 'file-video-o',
		'avi': 'file-video-o',
		'm4v': 'file-video-o',
		'mov': 'file-video-o',
		'css': 'file-code-o',
		'html': 'file-code-o',
		'js': 'file-code-o',
		'xml': 'file-code-o',
	};

	ips.createModule('ips.ui.uploader', function(){

		var defaults = {
			multiple: false,
			allowedFileTypes: null,
			maxFileSize: null, // in megabytes
			maxTotalSize: null, // in megabytes
			maxChunkSize: null,
			action: null,
			name: 'upload',
			button: '.ipsButton_primary',
			key: null,
			autoStart: true,
			insertable: false,
			template: 'core.attachments.fileItem',
			postkey: '',
			supportsDelete: true,
			maxImageDims: null, // "{width}x{height}"
			allowStockPhotos: false,
		};

		var respond = function (elem, options, e) {				
			if( !$( elem ).data('_uploader') ){
				$( elem ).show();
				$( elem ).data('_uploader', uploadObj(elem, _.defaults( options, defaults ) ) );
			} else {
				try {
					var obj = $( elem ).data('_uploader');
					obj.refresh();
				} catch (err) {
					Debug.log("Couldn't refresh uploader " + $( elem ).identify().attr('id') );
				}
			}
		},

		/**
		 * Refresh an existing uploader instance
		 *
		 * @param	{element} 	elem 		The element to refresh
		 * @returns {void}
		 */
		refresh = function (elem) {
			try {
				var obj = $( elem ).data('_uploader');
				obj.refresh();
			} catch (err) {
				Debug.log("Couldn't refresh uploader " + $( elem ).identify().attr('id') );
			}
		},

		/**
		 * Retrieve the uploader instance (if any) on the given element
		 *
		 * @param	{element} 	elem 		The element to check
		 * @returns {mixed} 	The uploader instance or undefined
		 */
		getObj = function (elem) {
			if( $( elem ).data('_uploader') ){
				return $( elem ).data('_uploader');
			}

			return undefined;
		},

		/**
		 * Return an appropriate file type icon based on a provided filename extension
		 *
		 * @param	{string} 	filename 		The filename to use
		 * @returns {string} 	A fontawesome classname
		 */
		getExtensionIcon = function (filename) {
			var extRegex = /(?:\.([^.]+))?$/;
			var ext = extRegex.exec(filename);

			if( !_.isUndefined( iconMap[ext[1]] ) ){
				return iconMap[ext[1]];
			}

			return 'file-o';
		};

		ips.ui.registerWidget('uploader', ips.ui.uploader, [ 
			'multiple', 'allowedFileTypes', 'maxFileSize', 'maxTotalSize', 'maxChunkSize', 'action', 'name', 'button', 'key', 
			'maxFiles', 'dropTarget', 'listContainer', 'autoStart', 'insertable', 'template', 'existingFiles', 'postkey',
			'supportsDelete', 'maxImageDims', 'allowStockPhotos'
		] );

		return {
			respond: respond,
			refresh: refresh,
			getObj: getObj,
			getExtensionIcon: getExtensionIcon
		};
	});

	/**
	 * Upload instance
	 * Interfaces with plupload to provide consistent uploading behavior
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var uploadObj = function (elem, options, e) {

		var pluploadObj,
			runtime,
			listContainer,
			uploadCount = 0,
			totalSize = 0,
			injectIds = {},
			uploaderID = '',
			sound = null;

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			
			uploaderID = $( elem ).identify().attr('id');

			if( options.listContainer ){
				listContainer = $( options.listContainer );
			} else if( $( elem ).find('[data-role="fileList"]').length ) {
				listContainer = $( elem ).find('[data-role="fileList"]');
			} else {
				listContainer = $( elem );
			}

			// Do we need to insert a wrapper though?
			if( ips.templates.get( options.template + 'Wrapper' ) ){
				listContainer.prepend( ips.templates.render( options.template + 'Wrapper' ) );

				// Move any existing items
				var firstItem = listContainer.children().first();
				firstItem.append( listContainer.children().not( firstItem ) );

				// Set listContainer to the wrapper
				listContainer = firstItem;

				// And initialize any widgets we might have
				$( document ).trigger( 'contentChange', [ listContainer.parent() ] );
			}

			// Add document events
			$( document ).on( 'addUploaderFile', _addUploaderFile );
			$( document ).on( 'removeAllFiles', _removeAllFiles );

			// Any files to start with?
			if( options.existingFiles ){
				try {
					var files = $.parseJSON( options.existingFiles );

					if( files.length ){
						_buildExistingFiles( files );
					}
				} catch (err) {
					Debug.error("Couldn't build existing files: " + err );
				}
			}

			if( _supportsDraggable() ){
				$( elem ).find('.ipsAttachment_supportDrag')
					.show()
				.end()
				.find('.ipsAttachment_nonDrag')
					.hide();
			}

			// Load the appropriate files
			var load = ['core/interface/plupload/plupload.full.min.js'];

			if( !ips.getSetting('useCompiledFiles') ){
				load = ['core/interface/plupload/moxie.js', 'core/interface/plupload/plupload.dev.js'];
			}

			ips.loader.get( load ).then( function () {
				_setUpUploader();
				_initUploader();
				_postInitEvents();
				_setUpFormEvents();
			});
		},

		/**
		 * Refreshes the pluploader, if available
		 *
		 * @returns   {void}
		 */
		refresh = function () {
			if( pluploadObj ){
				Debug.log("Refreshing");
				pluploadObj.refresh();
			}
		},

		/**
		 * Sets up form handling, allowing us to stop a form submit if an upload is in progress
		 *
		 * @returns   {void}
		 */
		_setUpFormEvents = function () {
			if( !elem.closest('form').length ){
				return;
			}

			/* Stock photo handler init */
			if ( options.allowStockPhotos !== false ) {
				$( elem ).find('[data-action="stockPhoto"]').on( 'click', function(e) {
					e.preventDefault();
					e.stopPropagation();
					
					var dialogRef = ips.ui.dialog.create({
						title: ips.getString("stockphoto_header"),
						fixed: false,
						destructOnClose: true,
					    url: ips.getSetting('baseURL') + '?app=core&module=system&controller=pixabay&uploader=' + $(elem).attr('data-ipsUploader-name'),
					});
					dialogRef.show();
				} );
			}
			
			// plupload.STOPPED indicates either nothing has been uploaded or all files
			// have finished uploading. For any other state, we'll stop the form submitting.
			elem.closest('form').on( 'submit', function (e) {
				if( pluploadObj.state != plupload.STOPPED ){
					e.preventDefault();
					e.stopPropagation();

					ips.ui.alert.show({
						type: 'alert',
						message: ips.getString('filesStillUploading'),
						subText: ips.getString('filesStillUploadingDesc'),
						icon: 'warn'
					});

					elem.trigger('fileStillUploading');
				}
			});
		},

		/**
		 * Responds to event and adds an uploaded file to the uploader container
		 *
		 * @param 	  {event} 	e 		Event object
		 * @param 	  {object} 	data	Event data object
		 * @returns   {void}
		 */
		_addUploaderFile = function (e, data) {
			if( data.uploaderID == uploaderID ){	
				data.extIcon = ips.ui.uploader.getExtensionIcon( data.title );	
				listContainer.append( ips.templates.render( _getTemplateName(data), data ) );
			}	
		},

		/**
		 * Return the appropriate template name based on options.template or the data type
		 *
		 * @param 	  {event} 	e 		Event object
		 * @param 	  {object} 	data	Event data object
		 * @returns   {void}
		 */
		_getTemplateName = function (data) {
			if( options.template !== null ){
				return options.template;
			}
		
			let type = 'fileItem'; // Default type

			// Show the appropriate container for this kind of file
			if( data.isImage ){
				type = 'imageItem';
			} else if( data.isVideo ){
				type = 'videoItem';
			}

			return `core.attachments.${type}`;
		},

		/**
		 * Responds to event and removes all the files in the uploader
		 *
		 * @param 	  {event} 	e 		Event object
		 * @param 	  {object} 	data	Event data object
		 * @returns   {void}
		 */
		_removeAllFiles = function (e, data) {
			listContainer.find('[data-role="file"]').remove();
		},

		/**
		 * Builds existing file items using the parsed JSON from the widget settings
		 *
		 * @param 	  {object} 	files 	Object containing file data
		 * @returns   {void}
		 */
		_buildExistingFiles = function (files) {
			if( !files.length ){
				return;
			}

			for( var i = 0; i < files.length; i++ ){
				var data = {
					id: files[i].id,
					imagesrc: files[i].imagesrc,
					thumbnail: files[i].thumbnail ? files[i].thumbnail : '',
					thumbnail_for_css: files[i].thumbnail ? files[i].thumbnail.replace( '(', '\\(' ).replace( ')', '\\)' ) : '',
					title: files[i].originalFileName,
					size: files[i].size,
					field_name: elem.attr('data-ipsUploader-name'),
					newUpload: false,
					insertable: !options.insertable,
					done: true,
					'default': files[i].default ? files[i].default : null,
					supportsDelete: options.supportsDelete,
					extIcon: ips.ui.uploader.getExtensionIcon(files[i].originalFileName)
				};

				if( files[i].id == elem.attr('data-ipsUploader-default') ){
					data['checked'] = "checked";
				}
				if( files[i]['hasThumb'] ){
					data['thumb'] = "<img src='" + ( files[i]['thumbnail'] ? files[i]['thumbnail'] : files[i]['imagesrc'] ) + "' class='ipsImage'>";
				}
                                
				listContainer.append( ips.templates.render( options.template, data ) );

				$('#' + files[i].id)
					.trigger( 'newItem', [ $('#' + files[i].id) ] );
			};

			elem.trigger('fileAdded', {
				count: files.length,
				uploader: options.name
			});
		},
		
		/**
		 * Passes a settings object through to plupload, but does not initialize it yet
		 *
		 * @returns   {void}
		 */
		 _setUpUploader = function () {
			pluploadObj = new plupload.Uploader( _getUploaderSettings() );
			pluploadObj.bind('Init', events.init );
			listContainer.find( '[data-role="file"]' ).each( function () {
				var fileElem = $( this );
				fileElem.on( 'click', '[data-role="deleteFile"]', _.bind( _deleteFile, fileElem, fileElem ) );
				uploadCount++;
			});
		},
		
		/**
		 * Builds the settings object which will be passed to plupload
		 *
		 * @returns   {object}
		 */
		_getUploaderSettings = function () {
			
			// If there is no action, find one
			var form = elem.parentsUntil( '', 'form' );
			if ( options.action === null ) {
				options.action = form.attr('action');
			}
			if ( options.key === null ) {
				options.key = form.children("[name='plupload']").val();
			}

			// Init Plupload Options
			var pluploadOptions = {
				runtimes : 'html5,flash,silverlight,html4',
				multi_selection: options.multiple,
				url: encodeURI( _decodeUrl( options.action ) ),
				file_data_name: options.name,
				flash_swf_url: 'applications/core/interface/plupload/Movie.swf',
				silverlight_xap_url: 'applications/core/interface/plupload/Movie.xap',
				browse_button: elem.find( options.button ).identify().attr('id'),
				headers: { 'x-plupload': options.key },
				chunk_size: options.maxChunkSize + 'mb'
			};

			/*if( options.maxFileSize ) {
				pluploadOptions.filters = {
					'max_file_size': ( options.maxFileSize > 1 ) ? options.maxFileSize + 'mb' : ( options.maxFileSize * 1024 ) + 'kb'
				};
			}*/
			/*if ( options.maxFileSize ) {
				pluploadOptions.max_file_size = ( options.maxFileSize > 1 ) ? options.maxFileSize + 'mb' : ( options.maxFileSize * 1024 ) + 'kb';
			}*/

			// Dragdrop target
			if( options.dropTarget ){
				pluploadOptions.drop_element = $( options.dropTarget ).attr('id');
			} else if( $( elem ).hasClass('ipsAttachment_dropZone') ){
				pluploadOptions.drop_element = $( elem ).attr('id');
			} else if( $( elem ).find('.ipsAttachment_dropZone').length ){
				pluploadOptions.drop_element = $( elem ).find('.ipsAttachment_dropZone').identify().attr('id');
			}
			
			// Max image dimensions
			if ( options.maxImageDims ) {
				var maxImageDims = options.maxImageDims.split('x');
				pluploadOptions.resize = {
					width: parseInt( maxImageDims[0] ),
					height: parseInt( maxImageDims[1] ),
					quality: parseInt( ips.getSetting('image_jpg_quality') ),
					fit: false
				};
			}
			
			return pluploadOptions;
		},
		
		/**
		 * Tests to see if the URL is encoded or not
		 *
		 * @returns   boolean
		 */
		_isEncoded = function( url ) {
			url = url || '';
			
			return url !== decodeURI( url );
		},
		
		/**
		 * Decode an encoded URL
		 *
		 * @returns   boolean
		 */
		_decodeUrl = function( url ) {
			while( _isEncoded( url ) ) {
				url = decodeURI( url );
			}
			return url;
		},
		
		/**
		 * Inits pluploader
		 *
		 * @returns   {void}
		 */
		_initUploader = function () {
			pluploadObj.init();
		},
		
		/**
		 * Binds all of the post-init events for pluploader
		 *
		 * @returns   {void}
		 */
		_postInitEvents = function () {
			pluploadObj.bind('Error', events.error );						// An error occured
			pluploadObj.bind('FilesAdded', events.filesAdded );				// Files added to the queue
			pluploadObj.bind('FilesRemoved', events.filesRemoved );			// Files are removed from the queue
			pluploadObj.bind('UploadFile', events.uploadFile );				// A file is starting
			pluploadObj.bind('UploadProgress', events.uploadProgress );		// There's progress on a file
			pluploadObj.bind('FileUploaded', events.fileUploaded );			// A file finished
			pluploadObj.bind('UploadComplete', events.uploadComplete );		// All files in the queue finished
			
			$( elem )
				.on( 'injectFile', function( e, data ){
					var pluploadFile = new plupload.File( new moxie.file.File( getRuntimeUid(), data.file ) );					
					injectIds[pluploadFile.id] = data.data;
					
					pluploadObj.addFile( pluploadFile );
				} )
				.on( 'resetUploader', function ( data ){
					_resetUploader( e, data );
				} );

			$(window).on( 'resize', function(){
				pluploadObj.refresh();
			} );
		},

		/**
		 * Gets runtime UID
		 *
		 * @returns	{obj}
		 */
		getRuntimeUid = function() {
			if( !runtime )
			{
				runtime = new (moxie.runtime.Runtime.getConstructor(pluploadObj.runtime))();
				runtime.init();
			}

			return runtime.uid;
		},

		/**
		 * Resets this uploader instance, clearing it of files
		 *
		 * @returns 	{void}
		 */
		_resetUploader = function (data) {
			// Update upload count
			uploadCount = 0;
			totalSize = 0;
			_updateCount();

			$( elem ).trigger( 'removeAllFiles', { uploaderID: uploaderID } );
		},

		/**
		 * Begins the upload process (called automatically when files are added)
		 *
		 * @returns 	{void}
		 */
		_startUpload = function () {
			Debug.log('Starting upload process...');
			pluploadObj.start();
		},

		/**
		 * Returns a human-readable, translated status string
		 *
		 * @param 	{number}	status 		Status code
		 * @returns {string}
		 */
		_getStatus = function (status) {
			switch( status ) {
				case plupload.QUEUED:
					return ips.getString('attachQueued');
				break;
				case plupload.UPLOADING:
					return ips.getString('attachUploading');
				break;
				case plupload.FAILED:
					return ips.getString('attachFailed');
				break;
				case plupload.DONE:
					return ips.getString('attachDone');
				break;
			}
		},

		/**
		 * Updates a file element with the current status
		 *
		 * @param 	{object} 	file 	File information object from plupload
		 * @returns {element} 	The file element
		 */
		_updateFileElement = function (file) {
			var fileElem = _findFileElem( file );
		
			_removeStatusClasses( fileElem );
			_updateStatus( fileElem, file.status );
			_updateCount();

			return fileElem;
		},

		/**
		 * Returns the file element for a given file object
		 *
		 * @param 	{object}	file 	 	File object from plupload
		 * @returns {element}
		 */
		_findFileElem = function (file) {
			return $( elem ).find('#' + file.id);
		},

		/**
		 * Updates the relevant elements within a file element with the current file status
		 *
		 * @param 	{element}	fileElem 	The element that represents this file
		 * @param	{number} 	status 	 	Current status code
		 * @returns {void}
		 */
		_updateStatus = function (fileElem, status) {
			fileElem.find('[data-role="status"]').html( _getStatus(status) );
			fileElem.toggleClass('ipsAttach_error', status === plupload.FAILED);
		},

		/**
		 * Removes the 4 status classes from the provided file element
		 *
		 * @param 	{element}	fileElem 	The element that represents this file
		 * @returns {void}
		 */
		_removeStatusClasses = function (fileElem) {
			_.each( ['uploading', 'done', 'error', 'queued'], function (type) {
				fileElem.removeClass( 'ipsAttach_' + type );
			});
		},

		/**
		 * Updates relevant elements with the uploaded count, and fires an event to let everyone know
		 *
		 * @returns 	{void}
		 */
		_updateCount = function () {			
			$( elem ).find('[data-role="count"]').text( uploadCount );
			
			elem.trigger('uploadedCountChanged', { 
				count: uploadCount,
				percent: pluploadObj.total.percent,
				uploader: options.name
			});
		},

		/**
		 * Updates relevant element within a file element with the percentage completed
		 *
		 * @param 	{element}	fileElem 	The element that represents this file
		 * @param	{number} 	percent 	Percentage complete
		 * @returns {void}
		 */
		_setPercent = function (fileElem, percent) {
			fileElem.find('[data-role="progressbar"]').css( { width: percent + '%' } );

			if( percent === 100 ){
				fileElem.find('.ipsAttachment_progress').slideUp();
			}
		},

		/**
		 * Builds a thumbnail element
		 *
		 * @param 	{element}	fileElem 	The element that represents this file
		 * @param	{object} 	file 	 	File information object
		 * @param	{object} 	info 	 	Info object from events.uploadDone
		 * @returns {void}
		 */
		_buildThumb = function (fileElem, file, info) {
			var toInsert = '';
												
			if( info.imagesrc ){

				Debug.log( fileElem.find('[data-role="preview"]') );

				toInsert = $('<img/>').attr( { src: info.thumbnail ? info.thumbnail : info.imagesrc } ).addClass('ipsImage');
				fileElem
					.attr( 'data-fullsizeurl', info.imagesrc )
					.attr( 'data-thumbnailurl', info.thumbnail ? info.thumbnail : info.imagesrc )
					.find('[data-role="preview"]')
						.html( toInsert )
						.css( 'background-image', 'url( "' + ( info.thumbnail ? info.thumbnail : info.imagesrc ) + '")' );
			} else if( info.videosrc ){
				toInsert = $('<video/>').append( $('<source/>').attr( { src: info.videosrc, type: fileElem.attr('data-mimeType') } ) );
				fileElem
					.attr( 'data-fullsizeurl', info.videosrc )
					.find('[data-role="preview"]')
						.html( toInsert );
			}

			
			fileElem.attr( 'data-fileid', info.id );	
			if ( info.securityKey ) {
				fileElem.attr( 'data-filekey', info.securityKey );
			}		
		},
		
		/**
		 * Deletes a pre-existing file
		 *
		 * @param 	{element}	fileElem 	The element that represents this file
		 * @param	{event} 	e 	 		Info object from events.uploadDone
		 * @returns {void}
		 */
		_deleteFile = function (fileElem, e) {
			e.preventDefault();
			e.stopPropagation();

			var baseUrl = options.action;

			if( baseUrl.match(/\?/) ) {
				if( baseUrl.slice(-1) != '?' ){
					baseUrl += '&';	
				}	
			} else {
				baseUrl += '?';
			}

			// Delete via ajax
			ips.getAjax()( baseUrl + 'postKey=' + options.postkey + '&deleteFile=' + encodeURIComponent( fileElem.attr('data-fileid') ) );
			
			// Update upload count
			uploadCount--;
			totalSize = totalSize - fileElem.attr('data-filesize');
			_updateCount();

			// Remove element
			/*fileElem.animationComplete( function () {
				fileElem.remove();

				// Let the document know
				elem.trigger( 'fileDeleted', { fileElem: fileElem, uploader: options.name, count: uploadCount } );
			});

			ips.utils.anim.go( 'fadeOutDown', fileElem );*/
			
			fileElem.fadeOut( function () {
				fileElem.remove();
				// Let the document know
				elem.trigger( 'fileDeleted', { fileElem: fileElem, uploader: options.name, count: uploadCount } );
			});
		},

		/**
		 * Returns boolean indicating if dragging is supported
		 *
		 * @returns {boolean}	Supports draggable?
		 */
		_supportsDraggable = function () {
			if('draggable' in document.createElement('span') && typeof FileReader != 'undefined' && !/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ){
				return true;
			}

			return false;
		},

		// Events object
		events = {
			/**
			 * Init
			 */
			init: function(up, err) {
				if( _supportsDraggable() ) {
					var dropElement = $( up.settings.drop_element );

					// Handles adding the dragging class to the upload element.
					// dragleave is a pain and works like mouseout instead of mouseleave in that it fires as soon as we leave the parent,
					// even if we're actually in a child now. So we have to get creative about detecting it correctly by listening for
					// dragenters on the whole document, and seeing whether they're children of our upload element.
					var _drag = function () {
						var currentElem = null;

						$( document )
							.on('dragenter', function (e) {
								if( currentElem && !$( e.target ).is( dropElement) && !$.contains( dropElement.get(0), currentElem ) ){
									dropElement.removeClass('ipsDragging');	
									currentElem = null;
								}
							});

						dropElement
							.on('dragleave', function (e) {
								if( !$( currentElem ).is( dropElement) && !$.contains( dropElement.get(0), currentElem ) ){
									dropElement.removeClass('ipsDragging');	
									currentElem = null;
								}
							})
							.on('dragenter', function (e) {
								var target = $( e.target );

								if( target.is( dropElement ) || $.contains( dropElement.get(0), e.target ) ){
									dropElement.addClass('ipsDragging');	
									currentElem = e.target;
								}						
							})
							.on('drop', function (e) {
								dropElement.removeClass('ipsDragging');	
								currentElem = null;
							});
					}();
				}
			},

			/**
			 * Files Added
			 */
			filesAdded: function(up, files) {
												
				if( !options.multiple ) {
					listContainer.find( '[data-role="deleteFile"]' ).click();
					if ( files.length > 1 ) {
						alert( ips.getString( 'uploadSingleErr' ) );
						return false;
					}
				} else if( options.maxFiles ) {
					if( files.length > options.maxFiles || ( uploadCount + files.length ) > options.maxFiles ){
						ips.ui.alert.show( {
							type: 'alert',
							icon: 'warn',
							message: ips.pluralize( ips.getString( 'uploadMaxFilesErr' ), options.maxFiles ),
							callbacks: {}
						});
						_.each(files, function (file, idx) {
							up.removeFile( file );
						});
						return false;						
					}
				} else if( options.maxTotalSize && totalSize > ( options.maxTotalSize * 1048576 ) ) {
										
					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.getString( 'uploadTotalErr', {
							size: parseFloat(
								( options.maxTotalSize >= 1 ) ? options.maxTotalSize : ( options.maxTotalSize * 1024 )
							).toLocaleString( $('html').attr('lang') ),
							size_suffix: ( options.maxTotalSize >= 1 ) ? ips.getString('size_mb') : ips.getString('size_kb')
						} ),
						callbacks: {}
					});
					_.each(files, function (file, idx) {
						up.removeFile( file );
					});
					return false;
				}
					
				var tooLarge			= 0;
				var overTotalLimit		= 0;
				var badType				= 0;
				var allowedFileTypes	= ( options.allowedFileTypes !== null ) ? $.parseJSON( options.allowedFileTypes ).join(',').toLowerCase().split(',') : '';
				var sizeAllowance		= options.maxTotalSize ? ( options.maxTotalSize * 1048576 ) - totalSize : null;

				_.each(files, function (file, idx) {
					
					// Check the size of this file
					if( options.maxFileSize !== null && ( ( file.size / 1024 ) > ( options.maxFileSize * 1024 ) ) ){
						// The file is too big, so remove it
						tooLarge++;
						up.removeFile( file );
						return;
					}

					// Check max size allowed
					if( !_.isNull( sizeAllowance ) )
					{
						if( ( sizeAllowance - file.size ) < 0 )
						{
							// The file would put us over our limit, so remove it
							overTotalLimit++;
							up.removeFile( file );
							return;
						}

						sizeAllowance -= file.size;
					}
					
					// And the extension
					if ( allowedFileTypes && allowedFileTypes.indexOf( file.name.substr( ( ~-file.name.lastIndexOf(".") >>> 0 ) + 2 ).toLowerCase() ) === -1 ) {
						badType++;
						up.removeFile( file );
						return;
					}

					var size = plupload.formatSize( file.size ),
						status = _getStatus( file.status ),
						isImage = false,
						isVideo = false,
						isAudio = false;

					// Figure out if this is an video or image or audio based on extension
					if( [ 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp' ].indexOf( file.name.substr( ( ~-file.name.lastIndexOf(".") >>> 0 ) + 2 ).toLowerCase() ) !== -1 ){
						isImage = true;
					}
					if( [ 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mpg', 'mpeg', 'flv', 'webm', 'wmv', 'avi', 'm4v' ].indexOf( file.name.substr( ( ~-file.name.lastIndexOf(".") >>> 0 ) + 2 ).toLowerCase() ) !== -1 ){
						isVideo = true;
					}
					if( [ 'mp3', 'ogg', 'wav', 'm4a' ].indexOf( file.name.substr( ( ~-file.name.lastIndexOf(".") >>> 0 ) + 2 ).toLowerCase() ) !== -1 ){
						isAudio = true;
					}

					var data = {
						uploaderID: uploaderID,
						id: file.id,
						title: file.name,
						mime: file.type,
						size: size,
						sizeRaw: file.size,
						status: status,
						statusCode: file.status,
						statusText: ips.getString('attachStatus', { size: size, status: status } ),
						field_name: elem.attr('data-ipsUploader-name'),
						newUpload: true,
						insertable: true,
						isImage: isImage,
						isVideo: isVideo,
						isAudio: isAudio,
						supportsDelete: options.supportsDelete
					};
					
					// Trigger event for adding the file element, to allow controllers to intercept
					$( elem ).trigger( 'addUploaderFile', data );

					$('#' + file.id)
						.addClass( 'ipsAttach_queued' )
						.trigger( 'newItem', [ $('#' + file.id) ] );
				});
	
				// Do we need to warn the user?
				if( tooLarge ){
					var errorString = ips.getString( 'uploadSizeErr', {
						max_file_size: parseFloat(
							( options.maxFileSize > 1 ) ? options.maxFileSize : ( options.maxFileSize * 1024 )
						).toLocaleString( $('html').attr('lang') ),
						size_suffix: ( options.maxFileSize > 1 ) ? ips.getString('size_mb') : ips.getString('size_kb')
					});

					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.pluralize( errorString, [ tooLarge, tooLarge ] ),
						callbacks: {}
					});
				}
				if( overTotalLimit ){
					var errorString = ips.getString( 'uploadSizeTotalErr', {
						max_file_size: parseFloat(
							( options.maxTotalSize > 1 ) ? options.maxTotalSize : ( options.maxTotalSize * 1024 )
						).toLocaleString( $('html').attr('lang') ),
						size_suffix: ( options.maxTotalSize > 1 ) ? ips.getString('size_mb') : ips.getString('size_kb')
					});

					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.pluralize( errorString, [ overTotalLimit, overTotalLimit ] ),
						callbacks: {}
					});
				}
				if( badType ){
					var errorString = ips.getString( 'pluploaderr_-601', {
						allowed_extensions: allowedFileTypes.join(', ')
					});

					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.pluralize( errorString, [ tooLarge, tooLarge ] ),
						callbacks: {}
					});
				}

				// If we still have some files, we can start them
				if( up.files.length ){
					elem.trigger('fileAdded', {
						count: up.files.length,
						uploader: options.name
					});

					if( options.autoStart ){
						_startUpload();
					}	
				}				
			},

			filesRemoved: function () {
				Debug.log('removed');
			},
			
			/**
			 * Upload File
			 */
			uploadFile: function(up, file) {
				var fileElem = _updateFileElement( file );
				fileElem.addClass('ipsAttach_uploading');
			},
			
			/**
			 * Upload Progress
			 */
			uploadProgress: function(up, file) {
				var fileElem = _updateFileElement( file );
				fileElem.addClass('ipsAttach_uploading');

				// Set the progress percent
				_setPercent( fileElem, file.percent );

				elem.trigger('uploadProgress', { 
					count: uploadCount,
					percent: pluploadObj.total.percent,
					uploader: options.name
				});
			},

			/**
			 * All files have finished uploading
			 * We'll show a notification & sound if supported
			 */
			uploadComplete: function (up, files) {	
				
				var success = 0;
				var error = 0;

				// Figure out how many files were successful/errors, ignore other statuses
				_.each( files, function (file) {
					if( file.status === 5 ){
						success++;
					} else if ( file.status === 4 ){
						error++;
					}
				});

				var total = success + error;
				
				elem.trigger('uploadComplete', { 
					success: success,
					error: error,
					total: total,
					uploader: options.name
				});

				// Only if we aren't active on the page
				if( _.isUndefined( ips.utils.events.getVisibilityProp() ) || !document[ ips.utils.events.getVisibilityProp() ] ){
					return;
				}

				var text = [];

				if( !total ){
					return;
				}

				if( success ){
					text.push( ips.pluralize( ips.getString('notifyUploadSuccess'), [ success ] ) );
				}

				if( error ){
					text.push( ips.pluralize( ips.getString('notifyUploadError'), [ error ] ) );
				}

				if( ips.utils.notification.hasPermission() ){
					ips.utils.notification.create({
						title: ips.pluralize( ips.getString('yourUploadsFinished'), [ total ] ),
						body: text.join(' '),
						icon: ips.getSetting('upload_imgURL'),
						onClick: function () {
							try {
								window.focus();
							} catch (err) {}
						}
					}).show();
				}

				if( sound ){
					try {
						sound.play();
					} catch (err) { }
				}
			},
			
			/**
			 * File Uploaded
			 */
			fileUploaded: function(up, file, info) {													
				// Update count of completed files
				uploadCount++;

				totalSize += file.size;

				var fileElem = _updateFileElement( file );
								
				fileElem.addClass('ipsAttach_done');

				if( options.insertable ){
					ips.utils.anim.go('fadeIn', fileElem.find('[data-role="insert"]') );
				}

				fileElem.find('[data-role="deleteFileWrapper"]').slideDown();
				//ips.utils.anim.go('fadeIn',  );
				
				/* Make sure the spinner is reset if shown from elsewhere */
				if ( $(elem).find('.ipsAttachment_loading').is(':visible') ) {
					$(elem).find('.ipsAttachment_loading').hide();
					$(elem).find('.ipsAttachment_dropZoneSmall_info').show();
				}
				
				// Set the progress percent
				_setPercent( fileElem, 100 );
																				
				// Do we have an image to process?
				try {
					var jsonInfo = $.parseJSON( info.response );
										
					elem.before( $('<input type="hidden">').attr( 'name', elem.attr('data-ipsUploader-name') + '_existing[' + file.id + ']' ).attr( 'value', jsonInfo.id ) );
															
					if( jsonInfo['error'] ){
						fileElem.on( 'click', '[data-role="deleteFile"]', _.bind( _deleteFile, fileElem, fileElem ) );
						file.status	= plupload.FAILED;

						up.trigger('error', { 
							code: jsonInfo['error'],
							extra: jsonInfo['extra'],
							subText: jsonInfo['sub'],
							file: file,
							uploader: options.name
						});

						return;
					}

					if( jsonInfo ){
						_buildThumb( fileElem, file, jsonInfo );
						fileElem.on( 'click', '[data-role="deleteFile"]', _.bind( _deleteFile, fileElem, fileElem ) );
					}
				} catch (err) {

					fileElem.on( 'click', '[data-role="deleteFile"]', _.bind( _deleteFile, fileElem, fileElem ) );
					file.status	= plupload.FAILED;

					up.trigger('error', { 
						code: 'upload_error',
						extra: err.message,
						file: file,
						uploader: options.name
					});

					Debug.warn( err );
				}
				
				// Are we handling this immediately?
				if ( file.id && injectIds[ file.id ] ) {
					$( elem ).trigger( 'fileInjected', { 'fileElem': fileElem, 'data': injectIds[ file.id ] } );
					delete injectIds[ file.id ];
				}
			},
						
			/**
			 * Error
			 */
			error: function(up, err) {			
				if( err.file ){
					_updateFileElement( err.file );
				}	

				// If this is a 'too large' error, we won't
				if( err.code == -600 || err.code == -601 ){
					return;
				}
				
				var errorMessage = ips.getString( 'pluploaderr_' + err.code, {
					max_file_size: parseFloat(
						( options.maxFileSize > 1 ) ? options.maxFileSize : ( options.maxFileSize * 1024 )
					).toLocaleString( $('html').attr('lang') ),
					size_suffix: ( options.maxFileSize > 1 ) ? ips.getString('size_mb') : ips.getString('size_kb'),
					allowed_extensions: ( options.allowedFileTypes !== null ) ? $.parseJSON( options.allowedFileTypes ).join(',') : '',
					server_error_code: ( err.extra !== null ) ? err.extra : 0,
				});
				
				if ( !errorMessage ) {
					errorMessage = ips.getString( 'pluploaderr_SERVER_CONFIGURATION', {
						server_error_code: err.code
					} );

					err.subText = ips.getString('pluploaderr_error_code', { code: err.code });
				}

				ips.ui.alert.show( {
					type: 'alert',
					icon: 'warn',
					message: errorMessage,
					callbacks: {},
					subTextHtml: err.subText ? err.subText : null
				});
			}
		};
	
		init();
	
		return {
			init: init,
			refresh: refresh
		};

	}
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.validation.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.validation.js - A form validation UI component
 * A wrapper for our main form validation that enables us to show
 * pretty messages to the user, and expose a data api
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.validation', function(){

		var defaults = {
			live: true,
			submit: true,
			characterLimit: 3,
			displayAs: 'list'
		};

		/**
 		 * Respond to a dialog trigger
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed
		 * @returns {void}
		 */
		var respond = function (elem, options, e) {
			if( !$( elem ).data('_validation') ){
				$( elem ).data('_validation', validateObj(elem, _.defaults( options, defaults ) ) );
			}
		};

		/**
		 * Validation instance
		 *
		 * @param	{element} 	elem 		The element this widget is being created on
		 * @param	{object} 	options 	The options passed into this instance
		 * @returns {void}
		 */
		var validateObj = function (elem, options) {

			/**
			 * Sets up this instance
			 * Adds necessary events using delegates to fields in this form
			 *
			 * @returns 	{void}
			 */
			var init = function () {
				
				// Set up events
				if( options.live ){
					// Text-like inputs
					$( elem ).on( 'keyup blur', 'input:not( [type="button"] ):not( [type="checkbox"] ):not( [type="hidden"] )' + 
						':not( [type="radio"] ):not( [data-validate-bypass] ), textarea:not( [data-validate-bypass] )', _textEvent );	

					// Selects
					$( elem ).on( 'change', 'select', _selectEvent );
				}

				if( options.submit ){
					$( elem ).closest('form').on( 'submit', _submitEvent );
				}				
			},

			/**
			 * Handles the form submit event
			 *
			 * @param	{event} 	e 		Event object
			 * @returns {void}
			 */
			_submitEvent = function (e) {
				var errors = 0;

				// Find all relevant fields
				var elements = $( elem ).find('input:not( [type="submit"] ):not( [type="checkbox"] )' + 
						':not( [type="radio"] ):not( [type="hidden"] ), select, textarea');

				// Validate each field in turn
				elements.each( function () {
					if( !_validate( $( this ) ) ){
						errors++;
					}
				});

				if( errors > 0 ){
					e.preventDefault();
					$( e.currentTarget ).trigger( 'error.validation', { count: errors } );
				} else {
					$( e.currentTarget ).trigger( 'success.validation' );
				}
			},

			_selectEvent = function (e) {

			},

			/**
			 * Handles events on text-like fields
			 *
			 * @param	{element} 	elem 		The element this widget is being created on
			 * @param	{object} 	options 	The options passed into this instance
			 * @returns {void}
			 */
			_textEvent = function (e) {
				var field = $( e.currentTarget );

				// If this is the blur event, only validate if we're above the character limit or this is a numerical field
				// If this is the keyup event, only validate if we're currently displaying some errors
				if( e.type == 'blur' || e.type == 'focusout' ){
					if( field.val().length >= options.characterLimit || field.is('[type="number"], [type="range"]') ){
						_validate( field );	
					}			
				} else {
					if( field.attr('data-hasErrors') ){
						_validate( field );
					} 
				}
			},

			/**
			 * Validates an individual field, displaying or clearing errors as needed
			 *
			 * @param	{element} 	target 		The element being validated
			 * @returns {boolean}	Whether the field is valid
			 */
			_validate = function (target) {
				var result = ips.utils.validate.validate( target );

				if( result !== true && !result.result ){
					_displayErrors( target, result );
				} else {
					_clearErrors( target );
				}

				return result.result;
			},

			/**
			 * Displays errors for a field
			 *
			 * @param	{element} 	target 		The element being validated
			 * @param	{object} 	results 	Results object returned from ips.utils.validate.validate
			 * @returns {void}
			 */
			_displayErrors = function (target, results) {
				var id = target.identify().attr('id');
				var errorList = $( '#' + id + '_errors' );

				// Build error list if necessary
				if( !errorList.length ) {
					var wrapper = ips.templates.render( 'core.forms.validationWrapper', {
						id: id + '_errors'
					} );

					target.after( wrapper );
					errorList = $('#' + id + '_errors');
				}

				// Reset contents of list
				errorList.html('');

				// Loop through each message
				for( var i = 0; i < results.messages.length; i++ ){
					errorList.append( ips.templates.render( 'core.forms.validationItem', {
						message: results.messages[ i ].message
					}));
				}

				// Add error class and attribute to the input
				target
					.addClass('ipsField_error')
					.attr( 'data-hasErrors', true );
			},

			/**
			 * Clears errors for a field
			 *
			 * @param	{element} 	target 		The form element being cleared
			 * @returns {void}
			 */
			_clearErrors = function (target) {
				var id = target.identify().attr('id');

				if( $( '#' + id + '_errors').length ){
					$( '#' + id + '_errors' ).remove();
				}

				// Remove classname and attribute
				target
					.removeClass('ipsField_error')
					.removeAttr('data-hasErrors');
			}

			init();

			return { };
		};

		ips.ui.registerWidget('validation', ips.ui.validation);

		return {
			respond: respond
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/ui" javascript_name="ips.ui.wizard.js" javascript_type="ui" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.wizard.js - Wizard widget
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.wizard', function(){
		
		var respond = function (elem, options, e) {
			elem.on( 'click', '[data-action="wizardLink"]', _.bind( refresh, e, elem ) );
			elem.on( 'submit', 'form', _.bind( refresh, e, elem ) );
		};
		
		/**
		 * Reloads a page of the wizard
		 *
		 * @param 		{element} 	elem 	The wizard element
		 * @param		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		var refresh = function(elem, e) {
			var target = $( e.currentTarget );
			
			_showLoading( elem );
						
			if( target.is('form') ){
				if( target.attr('data-bypassAjax') ){
					return true;
				}
				e.preventDefault();

				var data = target.serialize() + '&_wizardHeight=' + elem.find('[data-role="loading"]').outerHeight();

				// Include the value of any button clicked
				if( e.originalEvent && e.originalEvent.submitter && e.originalEvent.submitter.name && e.originalEvent.submitter.value ) {
					data += '&' + e.originalEvent.submitter.name + '=' + e.originalEvent.submitter.value;
				}

				ips.getAjax()( target.attr('action'), {
					data: data,
					type: 'post'
				}).done( function (response) {
					_insertHtml( response, elem );
				})
				.fail(function(response, textStatus, errorThrown){
					target.attr( 'data-bypassAjax', true );
					target.submit();
				})
			} else {
				e.preventDefault();
				
				ips.getAjax()( target.attr('href') ).done( function (response) {
					_insertHtml( response, elem );
				});
			}
			
		},

		/**
		 * Get wizard response HTML (including assets) and insert
		 *
		 * @param		{string}	response	HTML response (or, occasionally, an object with a redirect URL)
		 * @param		{element}	elem	The wizard element
		 * @return		{void}
		 */
		 _insertHtml = function (response, elem) {
			// If a json object is returned with a redirect key, send the user there
			if( _.isObject( response ) && response.redirect ){
				window.location = response.redirect;
				return;
			}

			var responseDiv		= $( '<div>' + response + '</div>' );
			var responseWizard	= responseDiv.find('[data-ipsWizard]').html();
			if ( !responseWizard ) {
				responseWizard = response;
			}

			// Find any CSS or JS references and include them. This is necessary if, e.g., a codemirror form element
			// was included in a wizard step as we need to include its js and CSS files.
			responseDiv.find( "link", "script" ).appendTo( 'head' );

			ips.controller.cleanContentsOf( elem );
			elem.html( responseWizard );
			$( document ).trigger( 'contentChange', [ elem ] );

			elem.trigger( 'wizardStepChanged' );
		 },

		/**
		 * Shows the loading thingy by working out the size of the form its replacing
		 *
		 * @param 		{element} 	elem 	The wizard element
		 * @returns 	{void}
		 */
		_showLoading = function (elem) {
			var loading = elem.find('[data-role="loading"]');
			var formContainer = elem.find('[data-role="wizardContent"]');
			
			if ( !formContainer.is(':visible') ) { // May have already been replaced by a loading indicator in userland code - for example, nexus.global.gateways.stripe does this
				return;
			}

			if( !loading.length ){
				loading = $('<div/>').attr('data-role', 'loading').addClass('ipsLoading').hide();
				elem.append( loading );
			}

			var dims = {
				width: formContainer.outerWidth(),
				height: formContainer.outerHeight()
			};

			loading
				.css({
					width: dims.width + 'px',
					height: dims.height + 'px'
				})
				.show();

			formContainer
				.hide()
				.after( loading.show() );
		},

		/**
		 * Hides the loading thingy
		 *
		 * @returns 	{void}
		 */
		_hideLoading = function () {
			var loading = elem.find('[data-role="loading"]');
			var formContainer = elem.find('[data-role="registerForm"]');

			loading.remove();
			formContainer.show();
		};

		// Register this module as a widget to enable the data API and
		// jQuery plugin functionality
		ips.ui.registerWidget( 'wizard', ips.ui.wizard );

		return {
			respond: respond
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.analytics.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.analytics.js - Analytics helper methods
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	&quot;use strict&quot;;
	
	ips.createModule('ips.utils.analytics', function () {

		/**
		 * Initialize analytics module
		 *
		 * @returns 	{void}
		 */
		var init = function () {

		},

		trackPageView = function (url) {
			try {
				if( ips.getSetting('googleAnalyticsEnabled') ){
					if( !_.isUndefined( window.ga ) ){
						var urlObj = ips.utils.url.getURIObject( url || document.location );
						Debug.log(&quot;Manual page view tracked with Google Analytics: &quot; + urlObj.relative);
						ga('send', 'pageview', urlObj.relative);
					}
				}
				
				if( ips.getSetting('matomoEnabled') ){
					if( !_isUndefined( window._paq ) ){
						Debug.log(&quot;Manual page view tracked with Matomo&quot;);
						_paq.push(['trackPageView']);
					}
				}
				
				if( _.isFunction( ips.getSetting('paginateCode') ) ){
					ips.getSetting('paginateCode').call(url);
				}
			} catch (err) {
				Debug.log(&quot;Error tracking page view: &quot; + err);
			}
		};

		return {
			init: init,
			trackPageView: trackPageView
		};
	});

}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.anim.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/* global ips, _, Debug */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.anim.js - Simple CSS classname-based animations
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
"use strict";

	ips.createModule('ips.utils.anim', function(){

		/* Check for animation support */
		var animationSupport = false;
		var elm = document.createElement('div'),
			animation = false,
		    animationstring = 'animation',
		    keyframeprefix = '',
		    domPrefixes = 'Webkit Moz O ms Khtml'.split(' '),
		    pfx  = '';
		if( elm.style.animationName ){
			animationSupport = true; 
		}    
		if( animationSupport === false ) {
			for( var i=0; i < domPrefixes.length; i++ ) {
		    	if( elm.style[ domPrefixes[i] + 'AnimationName' ] !== undefined ) {
		      		pfx = domPrefixes[ i ];
		      		animationstring = pfx + 'Animation';
		      		keyframeprefix = '-' + pfx.toLowerCase() + '-';
		      		animationSupport = true;
		      		break;
		    	}
		 	}
		}

		var init = function () {
			
		},

		/** Object containing all of our transition definitions */
		_transitions = {
			// Fades a single element in
			fadeIn: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem							
							.show()
							.addClass( [ 'ipsAnim', 'ipsAnim_fade', 'ipsAnim_in', speed ].join(' ') )
							.animationComplete( function() {
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem.fadeIn('fast');
				}
			},

			// Fades a single element out
			fadeOut: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem							
							.addClass( [ 'ipsAnim', 'ipsAnim_fade', 'ipsAnim_out', speed ].join(' ') )
							.animationComplete( function() { 
								elem.hide();
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem.fadeOut('fast');
				}
			},

			// Fades a single element in while sliding down a little
			fadeInDown: {
				anim: function (elem, speed) {
					cleanClasses( elem );
					
					return elem
							.show()
							.addClass( [ 'ipsAnim', 'ipsAnim_fade', 'ipsAnim_in', 'ipsAnim_down', speed ].join(' ') )
							.animationComplete( function() {
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem.fadeIn('fast');
				}
			},

			// Fades a single element out while sliding down a little
			fadeOutDown: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem
							.addClass( [ 'ipsAnim', 'ipsAnim_fade', 'ipsAnim_out', 'ipsAnim_down', speed ].join(' ') )
							.animationComplete( function() { 
								elem.hide();
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem.fadeOut('fast');
				}
			},

			// Slide an element from the right, to the left
			slideLeft: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem
							.addClass( [ 'ipsAnim', 'ipsAnim_slide', 'ipsAnim_left', speed ].join(' ') )
							.animationComplete( function() {
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem.show();
				}
			},

			// Blind down animation, from 0 height to full height
			blindDown: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem
							.show()
							.addClass( [ 'ipsAnim', 'ipsAnim_blind', 'ipsAnim_down', speed ].join(' ') )
							.animationComplete( function() {
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem.show();
				}
			},

			// Blind up animation, from full to 0 height
			blindUp: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem
							.show()
							.addClass( [ 'ipsAnim', 'ipsAnim_blind', 'ipsAnim_up', speed ].join(' ') )
							.animationComplete( function() {
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem.hide();
				}
			},

			// Zoom element in from 0x0 to normal size
			zoomIn: {
				anim: function (elem, speed) {
					cleanClasses( elem );
					
					return elem
							.show()
							.addClass( [ 'ipsAnim', 'ipsAnim_zoom', 'ipsAnim_in', speed ].join(' ') )
							.animationComplete( function() {
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem.show();
				}
			},

			// Zoom element in from 0x0 to normal size
			zoomOut: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem
							.addClass( [ 'ipsAnim', 'ipsAnim_zoom', 'ipsAnim_out', speed ].join(' ') )
							.animationComplete( function() {
								elem.hide();
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem.hide();
				}
			},

			// Shake from left to right
			wobble: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem
							.addClass( [ 'ipsAnim', 'ipsAnim_wobble', speed ].join(' ') )
							.animationComplete( function() {
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem;
				}
			},

			jiggle: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem
							.addClass( [ 'ipsAnim', 'ipsAnim_jiggle' ].join(' ') )
							.animationComplete( function () {
								cleanClasses( elem );
							})
				},
				fallback: function (elem) {
					return elem;
				}
			},

			// Pulse one time
			pulseOnce: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem
							.addClass( [ 'ipsAnim', 'ipsAnim_pulseOnce', speed ].join(' ') )
							.animationComplete( function() {
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem;
				}
			},
			
			// Pulse one time and jiggle
			pulseAndJiggle: {
				anim: function (elem, speed) {
					cleanClasses( elem );

					return elem
							.addClass( [ 'ipsAnim', 'ipsAnim_pulseAndJiggle', speed ].join(' ') )
							.animationComplete( function() {
								cleanClasses( elem );
							});
				},
				fallback: function (elem) {
					return elem;
				}
			}
		},

		/**
		 * Executes the given transition, passing through provided objects
		 *
		 * @param	{string} 	animationInfo 	Name of the transition to use, and 
		 *										optionally a speed (space-separated)
		 * @param	{element} 	[...]	 		Arbitrary number of elements to pass to the transition handler
		 * @returns {object}	Returns a promise object that resolves when animation is completed on ALL provided elements
		 */
		go = function (animationInfo) {

			var thisArgs = arguments,
				run = 'anim';

			// Make arguments an array first
			thisArgs = ips.utils.argsToArray( thisArgs );

			// Get rid of the first item
			thisArgs.shift();

			// Get animName pieces
			animationInfo = animationInfo.split(' ');

			var animName = animationInfo[0];
			var animSpeed = ( animationInfo[1] ) ? 'ipsAnim_' + animationInfo[1] : ''; // default is blank right now

			if( !_transitions[ animName ] ){
				Debug.warn( "The animation '" + animName + "' doesn't exist");
				return;
			}

			// Which kind of function should we run?
			if( !animationSupport ){
				run = 'fallback';	
			}

			// Make the animation speed the last argument
			thisArgs.push( animSpeed );

			var elem = $( thisArgs[0] );
			var deferred = $.Deferred();
			var done = 0;

			// Function which checks whether all elements are done animating
			var _checkCount = function () {
				done++;

				if( done >= elem.length ){
					deferred.resolve();
				}
			};

			// Loop through each element, adding to its animation queue
			_.each( elem, function () {
				_addToQueue( elem, animName, run, thisArgs ).always( _checkCount );
				_checkQueue( elem );
			});
			
			return deferred.promise();
		},

		/**
		 * Add an animation to the queue of the provided element
		 *
		 * @param	{element} 	elem 		Element on which the animation executes
		 * @param	{string} 	animName	Animaton to be run
		 * @param 	{string}	toRun 		Type of anim to run (anim, or fallback)
		 * @param	{array} 	args 		Array of arguments to be passed to animation method
		 * @returns {object}	Returns a promise object, resolved when this animation has been executed
		 */
		_addToQueue = function (elem, animName, toRun, args) {
			var deferred = $.Deferred();

			// If we currently have a queue, then add this item
			if( !elem.data('animQueue') || !_.isArray( elem.data('animQueue') ) ){
				elem.data( 'animQueue', [] );
			}

			elem.data('animQueue').push({
				animName: animName,
				run: toRun,
				args: args,
				deferred: deferred
			});

			return deferred.promise();
		},

		/**
		 * Checks the queue of the provided element, and executes the next animation if ready
		 *
		 * @param	{element} 	elem 	Element to be checked
		 * @returns {void}
		 */
		_checkQueue = function (elem) {
			var queue = elem.data('animQueue');

			if( elem.attr('animating') == true || !queue || !_.isArray( queue ) || !queue.length ){
				return;
			}

			var item = queue.shift();

			if( item.run == 'anim' ){
				elem.animationComplete( function () {
					elem.attr( 'animating', false );
					item.deferred.resolve();
					_checkQueue( elem );
				});

				elem.attr( 'animating', true );
				_transitions[ item.animName ][ item.run ].apply( this, item.args );
			} else {
				item.deferred.resolve();
				_transitions[ item.animName ][ item.run ].apply( this, item.args );
				_checkQueue( elem );
			}

			item.deferred.resolve();
		},

		/**
		 * Removes all ipsAnim_* classnames from an element
		 * Used to prepare an element for new animations
		 *
		 * @param	{element} 	elem 	Element to clean
		 * @returns {element} 	Cleaned element
		 */
		cleanClasses = function (elem) {
			$( elem ).removeClass('ipsAnim').removeClass( function (index, css) {
				return ( css.match( /ipsAnim[0-9a-z_\-]+/gi ) || [] ).join(' ');
			});

			return elem;
		},

		/**
		 * Determines whether a transition already exists
		 *
		 * @param	{string} 	name 	Name of transition to check
		 * @returns {boolean}
		 */
		isTransition = function (name) {
			return !_.isUndefined( _transitions[ name ] );
		},

		/**
		 * Registers a transition
		 *
		 * @param	{string} 	name 				Name of this transitions
		 * @param	{function} 	cssAnimation		Function to execute when CSS animation is used
		 * @param 	{function} 	fallbackAnimation	Function to execute when fallback animation is needed
		 * @returns {void}
		 */
		addTransition = function (name, cssAnimation, fallbackAnimation) {
			if( _transitions[ name ] ){
				Debug.warn("A transition with the name '" + name + "' already exists");
				return;
			}

			_transitions[ name ] = {
				anim: cssAnimation,
				fallback: fallbackAnimation
			};
		},

		/**
		 * Animates scrolling on an element
		 *
		 * @returns 	{boolean}
		 */
		scrollTo = function (elem, options) {
			
		};

		return {
			init: init,
			cleanClasses: cleanClasses,
			animationSupport: animationSupport,
			isTransition: isTransition,
			addTransition: addTransition,
			go: go,
			cancel: cleanClasses
		};

	});

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.color.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.color.js - Utilities for working with colors
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.color', function () {

		/**
		 * Initialize color module
		 *
		 * @returns 	{void}
		 */
		var init = function () {};

		/**
		 * Changes the provided hex value to have the provided hue/saturation levels
		 *
		 * @param		{string}	hex 	Hex color to start with
		 * @param 		{number}	toHue	New hue level
		 * @param 		{number}	toSat 	New saturation level
		 * @returns 	{string} 	New hex color
		 */
		var convertHex = function (hex, toHue, toSat) {
			hex = hex.replace( '#', '' );
			
			// Check for shorthand hex
			if( hex.length === 3 ){
				hex = hex.slice( 0, 1 ) + hex.slice( 0, 1 ) + hex.slice( 1, 2 ) + hex.slice( 1, 2 ) + hex.slice( 2, 3) + hex.slice( 2, 3 );
			}
			
			if( hex.length !== 6 ){
				Debug.write( hex + " isn't a valid hex color");
				return false;
			}
			
			// Split the hex into pieces, convert to RGB, and create fraction
			var r = ( hexToRGB( hex.slice( 0, 2 ) ) / 255 );
			var g = ( hexToRGB( hex.slice( 2, 4 ) ) / 255 );
			var b = ( hexToRGB( hex.slice( 4, 6 ) ) / 255 );

			// Convert to HSL
			var hsl = RGBtoHSL( r, g, b );

			// Change our hue to a fraction
			hsl[0] = (1 / 360) * toHue;			
			hsl[1] = (1 / 100) * toSat;

			// Back to RGB
			var rgb = HSLtoRGB( hsl[0], hsl[1], hsl[2] );

			return RGBtoHex( rgb[0], rgb[1], rgb[2] );
		},
		
		/**
		 * Converts the provided HSL values to their RGB versions
		 *
		 * @param		{number}	h 	Hue
		 * @param 		{number}	s	Saturation
		 * @param 		{number}	l 	Luminosity
		 * @returns 	{array} 	[ red, green, blue ]
		 */
		HSLtoRGB = function( h, s, l ){			
			var red = 0;
			var green = 0;
			var blue = 0;
			var v2 = 0;

			if( s == 0 ){
				red = l * 255;
				green = l * 255;
				blue = l * 255;
			} else {
				if( l < 0.5 ){
					v2 = l * ( 1 + s );
				} else {
					v2 = ( l + s ) - ( s * l );
				}
				
				var v1 = 2 * l - v2;
				
				red = 255 * hueToRGB( v1, v2, (h + ( 1 / 3 ) ) );
				green = 255 * hueToRGB( v1, v2, h );
				blue = 255 * hueToRGB( v1, v2, (h - ( 1 / 3 ) ) );
			}

			return [ Math.round( red ), Math.round( green ), Math.round( blue ) ];
		},
		
		/**
		 * Changes the provided hue values into an RGB value
		 *
		 * @returns 	{number} 	New RGB value
		 */
		hueToRGB = function( v1, v2, h ){
			if( h < 0 ){ 
				h += 1; 
			}

			if( h > 1 ){
				h -= 1; 
			}
			
			if( ( 6 * h ) < 1 ){
				return ( v1 + ( v2 - v1 ) * 6 * h );
			}

			if( ( 2 * h ) < 1 ){
				return v2;
			}

			if( ( 3 * h ) < 2 ){
				return ( v1 + ( v2 - v1 ) * ( ( 2 / 3 ) - h ) * 6 );
			}
			
			return v1;
		},
		
		/**
		 * Converts the provided RGB values to their HSL versions
		 *
		 * @param		{number}	r 	Red
		 * @param 		{number}	g	Green
		 * @param 		{number}	b 	Blue
		 * @returns 	{array} 	[ hue, saturation, lightness ]
		 */
		RGBtoHSL = function (r, g, b) {
			var lightness, hue, saturation = 0;
			
			var min = _.min( [ r, g, b ] );
			var max = _.max( [ r, g, b ] );

			var delta = max - min;
			
			lightness = ( max + min ) / 2;
			
			if( delta == 0 ){ 	// Grey
				hue = 0;
				saturation = 0;
			} else {
				if( lightness < 0.5 ){
					saturation = delta / ( max + min );
				} else {
					saturation = delta / ( 2 - max - min );
				}
				
				var delta_r = ( ( ( max - r ) / 6 ) + ( delta / 2 ) ) / delta;
				var delta_g = ( ( ( max - g ) / 6 ) + ( delta / 2 ) ) / delta;
				var delta_b = ( ( ( max - b ) / 6 ) + ( delta / 2 ) ) / delta;
				
				if( r == max ){
					hue = delta_b - delta_g;
				} else if( g == max ){
					hue = ( 1 / 3 ) + delta_r - delta_b;
				} else if( b == max ){
					hue = ( 2 / 3 ) + delta_g - delta_r;
				}
				
				if( hue < 0 ){
					hue += 1;
				}
				
				if( hue > 1 ){
					hue -= 1;
				}
			}
			
			return [ hue, saturation, lightness ];
		},
		
		/**
		 * Converts the provided hex into an RGB value
		 *
		 * @param		{string}	hex 	Hex value
		 * @returns 	{number} 	RGB value (0-255)
		 */
		hexToRGB = function (hex) {
			if( hex.length === 2 ){
				return parseInt( hex,16 );
			}

			hex = hex.replace( '#', '' );

			if( hex.length === 3 ){
				hex = hex.slice( 0, 1 ) + hex.slice( 0, 1 ) + hex.slice( 1, 2 ) + hex.slice( 1, 2 ) + hex.slice( 2, 3) + hex.slice( 2, 3 );
			}

			if( hex.length !== 6 ){
				Debug.write( hex + " isn't a valid hex color");
				return [0,0,0];
			}

			return [ hexToRGB( hex.slice( 0, 2 ) ) / 255, hexToRGB( hex.slice( 2, 4 ) ) / 255, hexToRGB( hex.slice( 4, 6 ) ) / 255 ];
		},
		
		/**
		 * Converts the provided RGB values to their Hex version
		 *
		 * @param		{number}	r 	Red
		 * @param 		{number}	g	Green
		 * @param 		{number}	b 	Blue
		 * @returns 	{string} 	Hex
		 */
		RGBtoHex = function (r, g, b) {
			var hex = [ r.toString(16), g.toString(16), b.toString(16) ];

			_.each( hex, function (val, nr) {
				if( val.length == 1 ){
					hex[ nr ] = '0' + val;
				}
			});

			return hex.join('');
		};

		return {
			convertHex: convertHex,
			HSLtoRGB: HSLtoRGB,
			hueToRGB: hueToRGB,
			RGBtoHSL: RGBtoHSL,
			hexToRGB: hexToRGB,
			RGBtoHex: RGBtoHex
		};
	});

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.cookie.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.cookie.js - Cookie management module
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.cookie', function () {

		var _store = {},
			_init = false;

		/**
		 * Initialize cookie module
		 *
		 * @returns 	{void}
		 */
		var init = function () {

			var cookies = _parseCookies( document.cookie.replace(" ", '') ),
				cookieID = ips.getSetting('cookie_prefix') || false;

			$.each( cookies, function (key, cookie) {
				
				if( cookieID ){
					if( key.startsWith( cookieID ) ){
						key = key.replace( cookieID, '' );
						_store[ key ] = unescape( cookie || '' );
					}
				}				
			});

			_init = true;
		},

		/**
		 * Return a cookie value
		 *
		 * @param	{string} 	cookieKey 	Cookie value to get, passed without the prefix
		 * @returns {mixed}		Cookie value or undefined
		 */
		get = function (cookieKey) {
			if( !_init ){
				init();
			}

			if( _store[ cookieKey ] ){
				return _store[ cookieKey ];
			}

			return undefined;
		},

		/**
		 * Set a cookie value
		 *
		 * @param	{string} 	cookieKey 	Key to set
		 * @param 	{mixed} 	value 		Value to set in this cookie
		 * @param 	{boolean} 	sticky 		Whether to make this a long-lasting cookie
		 * @returns {void}
		 */
		set = function( cookieKey, value, sticky ) {

			var expires = '',
				path = '/',
				domain = '',
				ssl = '',
				prefix = '';
			
			if( !cookieKey ){
				return;
			}

			if( !_.isUndefined( sticky ) ){	
				if( sticky === true ){
					var expirationDate = new Date();
					expirationDate.setFullYear( expirationDate.getFullYear() + 100 );

					expires = "; expires=" + expirationDate.toUTCString();
				} else if( sticky === -1 ){
					expires = "; expires=Thu, 01-Jan-1970 00:00:01 GMT";
				} else if( sticky.length > 10 ){
					expires = "; expires=" + sticky;
				}
			}

			if( !_.isUndefined( ips.getSetting('cookie_domain') ) && ips.getSetting('cookie_domain') != '' ){
				domain = "; domain=" + ips.getSetting('cookie_domain');
			}

			if( !_.isUndefined( ips.getSetting('cookie_path') ) && ips.getSetting('cookie_path') != '' ){
				path = ips.getSetting('cookie_path');
			}

			if( !_.isUndefined( ips.getSetting('cookie_prefix') ) && ips.getSetting('cookie_prefix') != '' ){
				prefix = ips.getSetting('cookie_prefix');
			}

			if( !_.isUndefined( ips.getSetting('cookie_ssl') ) && ips.getSetting('cookie_ssl') != '' ){
				ssl = '; secure';
			}
			
			document.cookie = prefix + cookieKey + "=" + escape( value ) + "; path=" + path + expires + domain + ssl + ';';

			_store[ cookieKey ] = value;
		},

		/**
		 * Deletes a cookie
		 *
		 * @param	{string} 	cookieKey 	Key to delete
		 * @returns {void}
		 */
		unset = function (cookieKey) {
			if( _store[ cookieKey ] ){
				set( cookieKey, '', -1 );
			}
		},

		/**
		 * Parses the provided string as a query string and returns an object representation
		 *
		 * @param	{string} 	cookieString 	Query string to parse
		 * @returns {object}
		 */
		_parseCookies = function (cookieString) {
			var pairs = cookieString.split(";"),
				cookies = {};
			
			for ( var i=0; i<pairs.length; i++ ){
				var pair = pairs[i].split("=");
				cookies[ pair[0].trim() ] = unescape( pair[1] ).trim();
			}

			return cookies;
		};

		return {
			init: init,
			get: get,
			set: set,
			unset: unset
		};
	});

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.css.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.css.js - CSS utilities
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.css', function () {

		var prefixes = [ 'webkit', 'moz', 'ms', 'o', 'w3c' ];

		/**
		 * Initialize CSS module
		 *
		 * @returns 	{void}
		 */
		var init = function () {},

		/**
		 * Builds a CSS style block from the provided selector/styles object
		 *
		 * @param 		{string} 	selector 		Selector to use
		 * @param 		{object}	styles			Object of styles, with the key being the property. If value is an array, 
		 *		multiple entries for the same property will be added (good for vendor prefixed)
		 * @returns 	{string}	Complete CSS style block
		 */
		buildStyleBlock = function (selector, styles, important) {
			var output = selector + " {\n";

			var getValue = function (key, value) {
				return "\t" + key + ': ' + value + ( ( important ) ? ' !important' : '' ) + ";\n";
			};

			_.each( styles, function (value, key) {
				if( _.isArray( value ) ){
					for( var i = 0; i < value.length; i++ ){
						output += getValue( key, value[ i ] );
					}
				} else {
					output += getValue( key, value );
				}
			});

			output += "}";

			return output;
		},

		/**
		 * Checks support for CSS transforms
		 *
		 * @returns 	{boolean}
		 */
		supportsTransform = function() {
			var bs = document.body.style;

			if( !_.isUndefined( bs.transform ) || !_.isUndefined( bs.WebkitTransform ) || 
					!_.isUndefined( bs.MozTransform ) || !_.isUndefined( bs.OTransform ) ){
				return true;
			}

			return false;
		},

		/**
		 * Replaces a style rule based on selector, in the stylesheet with the provided ID
		 *
		 * @param		{string}	stylesheetID	ID of stylesheet to update
		 * @param 		{string}	selector 		Selector to replace
		 * @param 		{object}	styles 			Object of style rules to build into a style block
		 * @returns 	{void}
		 */
		replaceStyle = function (stylesheetID, selector, styles) {	
			var stylesheet = getStylesheetRef( stylesheetID );
			var styleBlock = buildStyleBlock( selector, styles );
			var rulesKey = ( stylesheet['cssRules'] ) ? 'cssRules' : 'rules';
			var done = false;		

			// Loop through rules
			for( var rules = 0; rules < stylesheet[ rulesKey ].length; rules++ ){
				if( stylesheet[ rulesKey ][ rules ].selectorText == selector ){
					// Remove rule completely then readd it
					stylesheet.deleteRule( rules );
					stylesheet.insertRule( styleBlock, rules );
					done = true;
				}
			}

			// If we need a new rule...
			if( !done ){
				var idx = stylesheet.insertRule( styleBlock, stylesheet[ rulesKey ].length );
			}
		},

		/**
		 * Returns a reference to the stylesheet DOM object with the given ID
		 *
		 * @param 		{string}		stylesheet 		ID of the stylesheet to match
		 * @returns 	{element|boolean}	False if not found in document
		 */
		getStylesheetRef = function (stylesheetID) {
			var stylesheets = document.styleSheets;

			for( var sheet = 0; sheet < stylesheets.length; sheet++ ){
				if( stylesheets[ sheet ].ownerNode.id == stylesheetID ){
					return stylesheets[ sheet ];
				}
			}
			
			return false;
		},
		
		/**
		 * Returns an escaped version of a selector to use in jQuery
		 *
		 * @param 		{string}		selector 		Selector to escape
		 * @returns 	{string}
		 */
		escapeSelector = function (selector) {
			return selector.replace( /(:|\.|\[|\]|,)/g, "\\$1" );
		},

		/**
		 * Builds a prefixed CSS gradient
		 *
		 * @param 		{number}		angle 		The angle of the gradient (can be negative)
		 * @param		{array}			stops 		Array of stop data, in format [ [ color, location ], [ color, location ] ]
		 * @param 		{boolean} 		asPureCSS 	Should the method return ready-to-use javascript? If not, it returns an array
		 * @returns 	{string|array}
		 */
		generateGradient = function (angle, stops, asPureCSS) {
			var stops = _buildStops( stops );
			var angles = _buildAngles( angle );
			var output = [];			

			for( var i = 0; i < prefixes.length; i++ ){
				output.push( _buildPrefix( prefixes[ i ], 'linear-gradient' ) + 
					'(' + angles[ prefixes[ i ] ] + ', ' + stops + ')' );
			}

			if( !asPureCSS ){
				return output;
			} else {
				var prefixOutput = [];

				for( var i = 0; i < output.length; i++ ){
					prefixOutput.push( 'background-image: ' + output[ i ] + ';');
				}

				return prefixOutput.join("\n");
			}
		},

		/**
		 * Builds a string for stops in a gradient
		 *
		 * @param		{array}		stops 		Array of stop data, in format [ [ color, location ], [ color, location ] ]
		 * @returns 	{string}	Stops in the format: <code>#fff 0%,#333 50%,#000 100%</code>
		 */
		_buildStops = function (stops) {
			var line = [];

			for( var i = 0; i < stops.length; i++ ){
				if( stops[ i ][0].charAt(0) != '#' ){
					stops[ i ][0] = '#' + stops[ i ][0];
				}

				line.push( stops[ i ][0] + ' ' + stops[ i ][1] + '%' );
			}

			return line.join(',');
		},

		/**
		 * Returns the correct angle value for each supported vendor, accounting for w3c difference and directional keywords
		 *
		 * @param 		{number}	angle 		The angle of the gradient (can be negative)
		 * @returns 	{object} 	e.g. { w3c: 'to bottom', moz: 'top', webkit: 'top', o: 'top', ms: 'top' }
		 */
		_buildAngles = function (angle) {
			var mapDegrees = {
				'0': 'right',
				'90': 'top',
				'-90': 'bottom',
				'180': 'left'
			};

			var opposites = { '0':'180', '90':'-90', '-90':'90', '180':'0' };
			var output = {};

			for( var i = 0; i < prefixes.length; i++ ){
				if( !_.isUndefined( mapDegrees[ angle ] ) ){
					if( prefixes[ i ] == 'w3c' ){
						output[ prefixes[ i ] ] = 'to ' + mapDegrees[ opposites[ angle ] ];
					} else {
						output[ prefixes[ i ] ] = mapDegrees[ angle ];
					}
				} else {
					output[ prefixes[ i ] ] = angle + 'deg';
				}				
			}

			return output;
		},

		/**
		 * Builds a vendor-prefixed version of the given style property
		 * Does not validate that the style property is one that actually needs prefixing
		 *
		 * @param 		{string} 	vendor 		Vendor key to use
		 * @param 		{string}	style		Style property to prefix
		 * @returns 	{string}	e.g. -webkit-linear-gradient
		 */
		_buildPrefix = function (vendor, style) {
			return ( ( vendor != 'w3c' ) ? '-' + vendor + '-': '' ) + style;
		};

		return {
			generateGradient: generateGradient,
			replaceStyle: replaceStyle,
			getStylesheetRef: getStylesheetRef,
			buildStyleBlock: buildStyleBlock,
			escapeSelector: escapeSelector
		};
	});

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.db.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.db.js - A module for writing to per-user storage
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.db', function () {

		var enabled = null;
		var sessionStorageSupported = null;

		var init = function (queryString) {
			enabled = isEnabled();
		},		

		/**
		 * Sets a record in storage
		 *
		 * @param	{string} 	type 	 	Category to store in
		 * @param 	{string} 	key 		Key for this value
		 * @param 	{mixed} 	value 		Value to store
		 * @param	{bool}		isPrivate	If TRUE, and the member has not used the "Remember Me" setting to log in, the value will be stored in session storage rather than persistant storage
		 * @returns {void}
		 */
		set = function (type, key, value, isPrivate) {
			if( enabled ){
				var storageEngine = localStorage;
				
				if ( isPrivate && ( !_.isUndefined( ips.getSetting('isAcp') ) || ( _.isUndefined( ips.utils.cookie.get('login_key') ) && _.isUndefined( ips.getSetting('cookie_prefix') + ips.utils.cookie.get('login_key') ) ) ) && sessionStorageIsSupported() ) {
					storageEngine = sessionStorage;
				}
				
				if( value ){
					try {
						storageEngine.setItem( type + '.' + key, JSON.stringify( value ) );
					} catch (err) {}
				} else {
					storageEngine.removeItem( type + '.' + key );
				}
			}
		},

		/**
		 * Sets a record in storage
		 * If no key is specified, all items in the category are returned
		 *
		 * @param	{string} 	type 	 	Category to get
		 * @param 	{string} 	[key] 		Key of value to get
		 * @returns {mixed} 	If key is omitted, returns object containing all values, otherwise returns original value
		 */
		get = function (type, key) {
			if( _.isUndefined( key ) ){
				return getByType( type );
			}

			var val = localStorage.getItem( type + '.' + key );
			if ( _.isNull( val ) && sessionStorageIsSupported() ) {
				val = sessionStorage.getItem( type + '.' + key );
			}

			try {
				return JSON.parse( val );
			} catch(err) {
				return val;
			}
		},

		/**
		 * Removes values from storage
		 * If no key is specified, all items in the category are removed
		 *
		 * @param	{string} 	type 	 	Category to remove
		 * @param 	{string} 	[key] 		Key of value to remove
		 * @returns {void, number} 	If key is omitted, returns count of removed items, otherwise returns void
		 */
		remove = function (type, key) {
			if( _.isUndefined( key ) ){
				removeByType( type );
				return;
			}

			localStorage.removeItem( type + '.' + key );
			if ( sessionStorageIsSupported() ) {
				sessionStorage.removeItem( type + '.' + key );
			}
		},

		/**
		 * Returns all values for the given category
		 *
		 * @param	{string} 	type 	 	Category to return
		 * @returns {object} 	Key/value pairs of each value in the category
		 */
		getByType = function (type) {
			try {
				var results = {};
				
				if ( sessionStorageIsSupported() && sessionStorage.length ) {
					for( var key in sessionStorage ){
						if( key.startsWith( type + '.' ) ){
							var actualKey = key.replace( type + '.', '' );
							results[ actualKey ] = get( type, actualKey );
						}
					}
				}
				
				if ( localStorage.length ) {
					for( var key in localStorage ){
						if( key.startsWith( type + '.' ) ){
							var actualKey = key.replace( type + '.', '' );
							results[ actualKey ] = get( type, actualKey );
						}
					}
				}
	
				return results;
			} catch(e) {
				return {};
			}
		},

		/**
		 * Removes all values in the given category
		 *
		 * @param	{string} 	type 	 	Category to return
		 * @returns {number} 	Number of values removed
		 */
		removeByType = function (type) {
			var count = 0;
						
			for( var key in getByType(type) ){
				remove( type, key );
				count++;
			}

			return count;
		},

		/**
		 * Returns boolean indicating if localStorage is available
		 *
		 * @returns {boolean}
		 */
		isEnabled = function () {
			if( !_.isBoolean( enabled ) ){
				try {
					if( 'localStorage' in window && window['localStorage'] !== null && window.JSON ){
						return _testEnabled();
					} else {
						return false;
					}
				} catch (e) {
					return false;
				}
			} else {
				return enabled;
			}
		},

		/**
		 * Tests whether using localstorage will trigger a QuotaExceeded error
		 *
		 * @returns {boolean}
		 */
		_testEnabled = function () {
			try {
				localStorage.setItem('test', 1);
				localStorage.removeItem('test');
			} catch (err) {
				Debug.log("Writing to localstorage failed");
				return false;
			}

			return true;
		},
		
		/**
		 * Returns boolean indicating if sessionStorage is available
		 *
		 * @returns {boolean}
		 */
		sessionStorageIsSupported = function () {
			if( !_.isBoolean( sessionStorageSupported ) ){
				try {
					if( 'sessionStorage' in window && window['sessionStorage'] !== null && window.JSON ){
						sessionStorageSupported = true;
					} else {
						sessionStorageSupported = false;
					}
				} catch (e) {
					sessionStorageSupported = false;
				}
			}
			return sessionStorageSupported;
		};

		init();

		return {
			set: set,
			get: get,
			getByType: getByType,
			remove: remove,
			removeByType: removeByType,
			enabled: enabled,
			isEnabled: isEnabled
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.defaultEditorPlugins.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.defaultEditorPlugins.js - Code for editor plugins which don't have their own code
 *
 * Author: Mark Wade
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.defaultEditorPlugins', function () {
		return {
			/**
			 * Inline
			 */
			inline: function( command, html ) {
				return {
					exec: function( editor ) {
						var range = editor.getSelection().getRanges()[0];
						var selected = range.extractContents();
												
						var element = new CKEDITOR.dom.element( 'span' );
						element.setHtml( html.replace( /\{contents\}/g, '<span data-content-marker="true"></span>' ).replace( /\{content\}/g, '<span data-content-marker="true"></span>' )  );
						
						var spans = element.getElementsByTag( 'span' );
						for ( var i = 0; i < spans.count(); i++ ) {
							if ( spans.getItem( i ).hasAttribute( 'data-content-marker' ) ) {
								selected.insertAfterNode( spans.getItem( i ) );
								spans.getItem( i ).remove();
							}
						}
												
						editor.insertHtml( element.getHtml() );
					}
				}
			},
			
			/**
			 * Single-Line Blocks
			 */
			singleblock: function( command, tagName, tagAttributes, beforeContent, includeContent, afterContent, optionValue ) {
				return {
					exec: function( editor ) {
						editor.focus();

						// Parse option
						if ( !_.isUndefined( optionValue ) ) {
							beforeContent = beforeContent.replace( /\{option\}/g, optionValue );
							afterContent = afterContent.replace( /\{option\}/g, optionValue );
							var _tagAttributes = tagAttributes;
							tagAttributes = {};
							for ( i in _tagAttributes ) {
								var newI = i.replace( /\{option\}/g, optionValue );
								tagAttributes[newI] = _tagAttributes[i].replace( /\{option\}/g, optionValue );
							}
						}

						var style = new CKEDITOR.style( { element: tagName, attributes: tagAttributes } );
						var elementPath = editor.elementPath();
						if ( style.checkActive( elementPath, editor ) ) {
							editor['removeStyle']( style );
						} else {							
							if ( beforeContent || !includeContent || afterContent ) {
								var range = editor.getSelection().getRanges()[0];
								var selected = range.extractContents();

								var element = new CKEDITOR.dom.element( tagName );
								element.setAttributes(tagAttributes);
								
								var content = beforeContent;
								if ( includeContent ) {
									content += "{content}";
								}
								content += afterContent;
								element.setHtml( content.replace( /\{contents\}/g, '<span data-content-marker="true"></span>' ).replace( /\{content\}/g, '<span data-content-marker="true"></span>' )  );
								
								var spans = element.getElementsByTag( 'span' );
								for ( var i = 0; i < spans.count(); i++ ) {
									if ( spans.getItem( i ).hasAttribute( 'data-content-marker' ) ) {
										selected.insertAfterNode( spans.getItem( i ) );
										spans.getItem( i ).remove();
									}
								}
								editor.insertElement( element );
							} else {
								editor['applyStyle']( style );
							}
						}						
					}
				}
			},
			
			/**
			 * Blocks
			 */
			block: function( command, tagName, tagAttributes, beforeContent, includeContent, afterContent, optionValue ) {
				if ( tagName == 'p' ) {
					tagName = 'div';
				}				
				return {
					exec: function( editor ) {

						var selection = editor.getSelection(),
							range = selection && selection.getRanges( true )[ 0 ];
			
						if ( !range )
							return;
			
						var bookmarks = selection.createBookmarks();
			
						// Kludge for #1592: if the bookmark nodes are in the beginning of
						// blockquote, then move them to the nearest block element in the
						// blockquote.
						if ( CKEDITOR.env.ie ) {
							var bookmarkStart = bookmarks[ 0 ].startNode,
								bookmarkEnd = bookmarks[ 0 ].endNode,
								cursor;
			
							if ( bookmarkStart && bookmarkStart.getParent().getName() == tagName ) {
								cursor = bookmarkStart;
								while ( ( cursor = cursor.getNext() ) ) {
									if ( cursor.type == CKEDITOR.NODE_ELEMENT && cursor.isBlockBoundary() ) {
										bookmarkStart.move( cursor, true );
										break;
									}
								}
							}
			
							if ( bookmarkEnd && bookmarkEnd.getParent().getName() == tagName ) {
								cursor = bookmarkEnd;
								while ( ( cursor = cursor.getPrevious() ) ) {
									if ( cursor.type == CKEDITOR.NODE_ELEMENT && cursor.isBlockBoundary() ) {
										bookmarkEnd.move( cursor );
										break;
									}
								}
							}
						}
			
						var iterator = range.createIterator(),
							block;
						iterator.enlargeBr = editor.config.enterMode != CKEDITOR.ENTER_BR;
			
						var paragraphs = [];
						while ( ( block = iterator.getNextParagraph() ) )
							paragraphs.push( block );
			
						// If no paragraphs, create one from the current selection position.
						if ( paragraphs.length < 1 ) {
							var para = editor.document.createElement( editor.config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ),
								firstBookmark = bookmarks.shift();
							range.insertNode( para );
							para.append( new CKEDITOR.dom.text( '\ufeff', editor.document ) );
							range.moveToBookmark( firstBookmark );
							range.selectNodeContents( para );
							range.collapse( true );
							firstBookmark = range.createBookmark();
							paragraphs.push( para );
							bookmarks.unshift( firstBookmark );
						}
								
						// Make sure all paragraphs have the same parent.
						var commonParent = paragraphs[ 0 ].getParent(),
							tmp = [];
						for ( var i = 0; i < paragraphs.length; i++ ) {
							block = paragraphs[ i ];
							commonParent = commonParent.getCommonAncestor( block.getParent() );
						}
			
						// The common parent must not be the following tags: table, tbody, tr, ol, ul.
						var denyTags = { table:1,tbody:1,tr:1,ol:1,ul:1 };
						while ( denyTags[ commonParent.getName() ] )
							commonParent = commonParent.getParent();
			
						// Reconstruct the block list to be processed such that all resulting blocks
						// satisfy parentNode.equals( commonParent ).
						var lastBlock = null;
						while ( paragraphs.length > 0 ) {
							block = paragraphs.shift();
							while ( !block.getParent().equals( commonParent ) )
								block = block.getParent();
							if ( !block.equals( lastBlock ) )
								tmp.push( block );
							lastBlock = block;
						}
			
						// If any of the selected blocks is a blockquote, remove it to prevent
						// nested blockquotes.
						while ( tmp.length > 0 ) {
							block = tmp.shift();
							if ( block.getName() == tagName ) {
								var docFrag = new CKEDITOR.dom.documentFragment( editor.document );
								while ( block.getFirst() ) {
									docFrag.append( block.getFirst().remove() );
									paragraphs.push( docFrag.getLast() );
								}
			
								docFrag.replace( block );
							} else
								paragraphs.push( block );
						}
						
						// Parse option
						if ( !_.isUndefined( optionValue ) ) {
							beforeContent = beforeContent.replace( /\{option\}/g, optionValue );
							afterContent = afterContent.replace( /\{option\}/g, optionValue );
							var _tagAttributes = tagAttributes;
							tagAttributes = {};
							for ( i in _tagAttributes ) {
								var newI = i.replace( /\{option\}/g, optionValue );
								tagAttributes[newI] = _tagAttributes[i].replace( /\{option\}/g, optionValue );
							}
						}
			
						// Now we have all the blocks to be included in a new blockquote node.
						var bqBlock = editor.document.createElement( tagName );
						bqBlock.setAttributes( tagAttributes );
						bqBlock.insertBefore( paragraphs[ 0 ] );
						var content = '';
						if ( beforeContent ) {
							content += beforeContent;
						}
						if ( includeContent ) {
							content += '<span data-content-marker="true"></span>';
						}
						if ( afterContent ) {
							content += afterContent;
						}
						bqBlock.appendHtml( content );
						if ( includeContent ) {
							var spans = bqBlock.getElementsByTag( 'span' );
							for ( var i = 0; i < spans.count(); i++ ) {
								if ( spans.getItem( i ).hasAttribute( 'data-content-marker' ) ) {
									while ( paragraphs.length > 0 ) {
										block = paragraphs.pop();
										block.insertAfter( spans.getItem( i ) );
									}
									spans.getItem( i ).remove();
								}
							}
						}
						selection.selectBookmarks( bookmarks );
						editor.focus();
					}
				}
			}
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.emoji.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.emoji.js - Emoji module
 *
 * Author: Mark Wade
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.emoji', function () {

		var _emoji = null,
		_categories = null,
		_testingCanvasContext = null,
		_ajax = null,
		_callbacks = [],

		init = function () {
			this._invalidCharacterImageData = ['0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0'];
		},
				
		/**
		 * Get all supported Emoji
		 *
		 * @param 		{function}	callback	Function to run once emoji data has been fetched
		 * @returns 	{bool}
		 */
		getEmoji = function(callback) {
			if ( this._emoji && this._categories ) {
				callback(this._emoji, this._categories);
				return;
			}
			
			var storage = ips.utils.db.get( 'emoji', ips.getSetting('baseURL') + '-' + ips.getSetting('emoji_cache') );
			var categories = ips.utils.db.get( 'emojiCategories', ips.getSetting('baseURL') + '-' + ips.getSetting('emoji_cache') );
			if ( storage && categories ) {
				this._emoji = storage;
				callback( storage, categories );
			} else {
				ips.utils.db.removeByType('emoji');
				if ( callback ) {
					if ( !this._callbacks ) { 
						this._callbacks = [];
					}
					this._callbacks.push( callback );
				}
				if( this._ajax && this._ajax.abort ){
					this._ajax.abort();
				}
				this._ajax = ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=editor&do=emoji' ).done(function(emoji){
					this._emoji = {};
					this._categories = [];
					var canUseTones = null;
					for ( var category in emoji ) {
						var categoryName = emoji[category].category;
						this._categories.push( categoryName );
						this._emoji[categoryName] = [];
						for ( var i = 0; i < emoji[category].emoji.length; i++ ) {
							if ( this.canRender( emoji[category].emoji[i].code ) ) {

								// If the emoji supports skin tone modifiers, check if our browser does
								if ( emoji[category].emoji[i].skinTone ) {
									if( canUseTones === null ) {
										if( !this.canRender( this.tonedCode( emoji[category].emoji[i].code, 'light' ) ) ) {
											canUseTones = false;
										}
										else {
											canUseTones = true;
										}
									}
									if ( canUseTones === false ) {
										emoji[category].emoji[i].skinTone = false;
									}
								}

								// Add translated name too
								emoji[category].emoji[i].shortNames.push( ips.getString('emoji-' + emoji[category].emoji[i].name ) );
								this._emoji[categoryName].push( emoji[category].emoji[i] );
							}
						}
					}
					ips.utils.db.set( 'emoji', ips.getSetting('baseURL') + '-' + ips.getSetting('emoji_cache'), this._emoji );
					ips.utils.db.set( 'emojiCategories', ips.getSetting('baseURL') + '-' + ips.getSetting('emoji_cache'), this._categories );
					for ( var i = 0; i < this._callbacks.length; i++ ) {
						this._callbacks[i](this._emoji,this._categories);
					}
				}.bind(this));
			}
		},
		
		/**
		 * Check if we can render a particular Emoji
		 *
		 * @param 		{string}	code	The code 
		 * @returns 	{bool}
		 */
		canRender = function(code) {
												
			/* We only need to do this check for native */
			if ( code.substr( 0, 7 ) == 'custom-' ) {
				return true;
			}
			else if ( ips.getSetting('emoji_style') == 'disabled' ) {
				return false;
			}
			else if ( ips.getSetting('emoji_style') != 'native' ) {
				return true;
			}
			
			/* Windows renders country flags as letters which looks terrible */
			if ( navigator.platform.indexOf('Win') > -1 && code.match( /^1F1(E[6-9A-F]|F[0-9A-F])/ ) ) {
				return false;
			}
			
			/* Load the canvas if we haven't already */
			if ( this._testingCanvasContext == null ) {
				var testingCanvas = document.createElement('canvas');
				testingCanvas.width = 8;
				testingCanvas.height = 6;
				if ( testingCanvas && testingCanvas.getContext && typeof String.fromCodePoint == 'function' ) {
					this._testingCanvasContext = testingCanvas.getContext('2d');
					if ( typeof this._testingCanvasContext.fillText == 'function' ) {
						this._testingCanvasContext.textBaseline = 'top';
						this._testingCanvasContext.font = '5px "Apple Color Emoji", "Segoe UI Emoji", "NotoColorEmoji", "Segoe UI Symbol", "Android Emoji", "EmojiSymbols"';
						
						/* Draw a deliberately invalid character (x1 and x2) so that we can compare it with valid emojis to see if they were rendered properly */
						this._testingCanvasContext.fillText( String.fromCodePoint( parseInt( '1F91F', 16 ) ), 0, 0 );
						this._invalidCharacterImageData.push( Array.prototype.toString.call( this._testingCanvasContext.getImageData( 0, 0, 6, 6 ).data ) );
						this._testingCanvasContext.clearRect( 0, 0, 8, 6 );
						this._testingCanvasContext.fillText( String.fromCodePoint( parseInt( '1F91F', 16 ) ) + String.fromCodePoint( parseInt( '1F91F', 16 ) ), 0, 0 );
						this._invalidCharacterImageData.push( Array.prototype.toString.call( this._testingCanvasContext.getImageData( 0, 0, 6, 6 ).data ) );
					} else {
						return false;
					}
				} else {
					return false;
				}
			}
			
			/* Clear the canvas */
			this._testingCanvasContext.clearRect( 0, 0, 8, 6 );
						
			/* Draw the character */
			var emoji = this.emojiFromHex( code );
			if ( emoji == null ) {
				return false;
			}
			this._testingCanvasContext.fillText( emoji, 0, 0 );
			
			/* If it rendered the same as the deliberately invalid character, or it's blank, we know it can't be rendered */
			if ( this._invalidCharacterImageData.indexOf( Array.prototype.toString.call( this._testingCanvasContext.getImageData( 0, 0, 6, 6 ).data ) ) != -1  ) {
				return false;
			}
			
			/* Look at an imaginary line down the right side, if it's *not* totally blank, there is probably two characters
				(i.e. the base character and a modifier like a gender sign), so assume this emoji not supported */
			if ( Array.prototype.toString.call( this._testingCanvasContext.getImageData( 7, 0, 1, 6 ).data ) != '0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0' ) {
				return false;
			}
						
			/* Return true */
			return true;
		},
		
		/**
		 * Get emoji character from hex
		 *
		 * @param 		{string}	hex Hexadecimal code(s) separated by -
		 * @returns 	{string|null}
		 */
		emojiFromHex = function(hex) {
			try {
				var decimals = [];
				var hexPoints = hex.split('-');
				for ( var p = 0; p < hexPoints.length; p++ ) {
					decimals.push( parseInt( hexPoints[p], 16 ) );
				}
				
				return String.fromCodePoint.apply( null, decimals );
			} catch ( err ) {
				return null;
			}
		},
				
		/**
		 * Get emoji image from hex
		 *
		 * @param 		{string}	codeToUse Hexadecimal code(s) separated by -
		 * @returns 	{string|null}
		 */
		emojiImage = function( codeToUse, lazyLoad ) {
			if ( codeToUse.substr( 0, 7 ) == 'custom-' ) {
				var parts = codeToUse.split('-');
				
				// Recent emoji are stored in a recentEmoji cookie, but the group may have since been deleted or renamed, so account for that
				if( _.isUndefined( this._emoji[ parts[1] ] ) )
				{
					return null;
				}

				for ( var i = 0; i < this._emoji[ parts[1] ].length; i++ ) {
					if( this._emoji[ parts[1] ][i].code == codeToUse ) {
						var imgTag = '<img src="' + this._emoji[ parts[1] ][i].image + '" loading="lazy"';

						imgTag += 'title="' + this._emoji[ parts[1] ][i].name + '" alt="' + this._emoji[ parts[1] ][i].name + '"';
	
						if( this._emoji[ parts[1] ][i].image2x )
						{
							imgTag += ' srcset="' + this._emoji[ parts[1] ][i].image2x + ' 2x"';
						}

						if( parseInt( this._emoji[ parts[1] ][i].width ) && parseInt( this._emoji[ parts[1] ][i].height ) )
						{
							imgTag += ' width="' + this._emoji[ parts[1] ][i].width + '" height="' + this._emoji[ parts[1] ][i].height + '"';
						}
						
						imgTag += ' data-emoticon="true">';
						
						return imgTag;
					}
				}
				return null;
			} else {
				var url;
				var image = codeToUse.toLowerCase();
				
				if ( image.indexOf( '200d' ) == -1 || ['1f441-fe0f-200d-1f5e8-fe0f'].indexOf( image ) != -1 ) {
					image = image.replace( /\-fe0f/g, '' );
				}
				if ( ['0031-20e3', '0030-20e3', '0032-20e3', '0034-20e3', '0035-20e3', '0036-20e3', '0037-20e3', '0038-20e3', '0033-20e3', '0039-20e3', '0023-20e3', '002a-20e3', '00a9', '00ae'].indexOf( image ) != -1 ) {
					image = image.replace( '00', '' );
				}
				
				url = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/" + image + ".png";
				
				var character = this.emojiFromHex( codeToUse );

				return '<img src="' + url + '" alt="' + character + '" loading="lazy" class="ipsEmoji" data-emoticon="true">';
			}
		},
				
		/**
		 * Get the code for an emoji with a skin tone modifier
		 *
		 * @param 		{string}	code	Hexadecimal code(s) separated by -
		 * @param 		{string}	tone	Skin tone to use (light, medium, etc)
		 * @returns 	{string|null}
		 */
		tonedCode = function( code, tone ) {
			switch ( tone ) {
			case 'light':
				return code.replace( /^([0-9A-F]*)(\-|$)(?:FE0F\-)?/, '$1-1F3FB$2' );
				break;
			case 'medium-light':
				return code.replace( /^([0-9A-F]*)(\-|$)(?:FE0F\-)?/, '$1-1F3FC$2' );
				break;
			case 'medium':
				return code.replace( /^([0-9A-F]*)(\-|$)(?:FE0F\-)?/, '$1-1F3FD$2' );
				break;
			case 'medium-dark':
				return code.replace( /^([0-9A-F]*)(\-|$)(?:FE0F\-)?/, '$1-1F3FE$2' );
				break;
			case 'dark':
				return code.replace( /^([0-9A-F]*)(\-|$)(?:FE0F\-)?/, '$1-1F3FF$2' );
				break;
			}
			return code;
		},
		
		/**
		 * Get HTML to preview a particular emoji
		 *
		 * @param 		{string}	code	Hexadecimal code(s) separated by -
		 * @returns 	{string}
		 */
		preview = function(code) {
			if ( ips.getSetting('emoji_style') == 'native' && code.substr( 0, 7 ) != 'custom-' ) {
				return "<span class='ipsEmoji'>" + this.emojiFromHex( code ) + '</span>';
			}
			else {
				return this.emojiImage( code, true );
			}
		},
		
		/**
		 * Get the CKEditor element to use for a particular emoji
		 *
		 * @param 		{string}	code	Hexadecimal code(s) separated by -
		 * @returns 	{CKEDITOR.dom.element}
		 */
		editorElement = function( code ) {
			if ( ips.getSetting('emoji_style') == 'native' && code.substr( 0, 7 ) != 'custom-' ) {
				return CKEDITOR.dom.element.createFromHtml( "<span class='ipsEmoji'>" + this.emojiFromHex( code ) + "</span>" );
			} else {
				return CKEDITOR.dom.element.createFromHtml( this.emojiImage( code, false ) );
			}
		},
		
		/**
		 * Log that an emoji has been used for the "Recently Used" section
		 *
		 * @param 		{string}	code	Hexadecimal code(s) separated by -
		 * @returns 	{CKEDITOR.dom.element}
		 */
		logUse = function( code ) {
			var recentEmoji = [];
			if ( ips.utils.cookie.get( 'recentEmoji' ) ) {
				recentEmoji = ips.utils.cookie.get( 'recentEmoji' ).split(',');
			}
			
			var index = recentEmoji.indexOf( code );
			if ( index != -1 ) {
				recentEmoji.splice( index, 1 );
			}
			recentEmoji.unshift(code);
			recentEmoji.splice( 24 );
			
			ips.utils.cookie.set( 'recentEmoji', recentEmoji.join(','), true );
		};
		
		return {
			init: init,
			getEmoji: getEmoji,
			canRender: canRender,
			emojiFromHex: emojiFromHex,
			emojiImage: emojiImage,
			tonedCode: tonedCode,
			preview: preview,
			editorElement: editorElement,
			logUse: logUse
		};
	});

}(jQuery, _));

/* This is a polyfill for IE11 to support String.fromCodePoint, taken from
	https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint */
/*! https://mths.be/fromcodepoint v0.2.1 by @mathias */
if (!String.fromCodePoint) {
  (function() {
    var defineProperty = (function() {
      // IE 8 only supports `Object.defineProperty` on DOM elements
      try {
        var object = {};
        var $defineProperty = Object.defineProperty;
        var result = $defineProperty(object, object, object) && $defineProperty;
      } catch(error) {}
      return result;
    }());
    var stringFromCharCode = String.fromCharCode;
    var floor = Math.floor;
    var fromCodePoint = function(_) {
      var MAX_SIZE = 0x4000;
      var codeUnits = [];
      var highSurrogate;
      var lowSurrogate;
      var index = -1;
      var length = arguments.length;
      if (!length) {
        return "";
      }
      var result = "";
      while (++index < length) {
        var codePoint = Number(arguments[index]);
        if (
          !isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
                    codePoint < 0 || // not a valid Unicode code point
                    codePoint > 0x10FFFF || // not a valid Unicode code point
                    floor(codePoint) != codePoint // not an integer
        ) {
          throw RangeError("Invalid code point: " + codePoint);
        }
        if (codePoint <= 0xFFFF) { // BMP code point
          codeUnits.push(codePoint);
        } else { // Astral code point; split in surrogate halves
          // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
          codePoint -= 0x10000;
          highSurrogate = (codePoint >> 10) + 0xD800;
          lowSurrogate = (codePoint % 0x400) + 0xDC00;
          codeUnits.push(highSurrogate, lowSurrogate);
        }
        if (index + 1 == length || codeUnits.length > MAX_SIZE) {
          result += stringFromCharCode.apply(null, codeUnits);
          codeUnits.length = 0;
        }
      }
      return result;
    };
    if (defineProperty) {
      defineProperty(String, "fromCodePoint", {
        "value": fromCodePoint,
        "configurable": true,
        "writable": true
      });
    } else {
      String.fromCodePoint = fromCodePoint;
    }
  }());
}]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.events.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.events.js - A module for working with events
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.events', function () {

		/**
		 * Fires a manual event on an element
		 *
		 */
		var manualEvent = function (element, ev) {
			if( _.isObject( element ) ){
				element.each( function () {
					_fireEvent( this, ev );
				});
			} else {
				_fireEvent( element, ev );
			}			
		},

		/**
		 * Simple test to determine if this is a touch browser
		 *
		 * @returns	boolean
		 */
		isTouchDevice = function () {
			return ( ('ontouchstart' in window) || (navigator.MaxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0) );
		},

		/**
		 * Returns the correct Page Visibility API event
		 *
		 * @returns string
		 */
		getVisibilityEvent = function () {
			if( !_.isUndefined( document.hidden ) ){
				return 'visibilitychange';
			} else if( !_.isUndefined( document.mozHidden ) ){
				return 'mozvisibilitychange';
			} else if( !_.isUndefined( document.msHidden ) ){
				return 'msvisibilitychange';
			} else if( !_.isUndefined( document.webkitHidden ) ){
				return 'webkitvisibilitychange';
			}

			return '_unsupported';
		},

		/**
		 * Returns the correct Page Visibility API property
		 *
		 * @returns string or undefined
		 */
		getVisibilityProp = function () {
			if( !_.isUndefined( document.hidden ) ){
				return 'hidden';
			} else if( !_.isUndefined( document.mozHidden ) ){
				return 'mozHidden';
			} else if( !_.isUndefined( document.msHidden ) ){
				return 'msHidden';
			} else if( !_.isUndefined( document.webkitHidden ) ){
				return 'webkitHidden';
			}

			return undefined;
		},

		/**
		 * Does the actual firing
		 *
		 */
		_fireEvent = function (element, ev) {
			if( document.createEvent ) {
				var evObj = document.createEvent('MouseEvents');
				evObj.initEvent( ev, true, false );
				element.dispatchEvent( evObj );
			} else if ( document.createEventObject ) {
				var evObj = document.createEventObject();
				element.fireEvent( 'on' + evt, evObj );
			}
		};
		
		return {
			manualEvent: manualEvent,
			isTouchDevice: isTouchDevice,
			getVisibilityEvent: getVisibilityEvent,
			getVisibilityProp: getVisibilityProp
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.form.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.form.js - Form utilities
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.form', function () {

		/**
		 * Serialize a jquery object as an object of values
		 *
		 * @returns object
		 */
		var serializeAsObject = function (jQueryObj, customSerializers) {
			var asArray = jQueryObj.serializeArray();
			var output = {};

			_.each( asArray, function (val) {
				var outValue = val.value;

				// Do we have a custom serializer function for this field?
				if( customSerializers && !_.isUndefined( customSerializers[ val.name ] ) && _.isFunction( customSerializers[ val.name ] ) ){
					outValue = customSerializers[ val.name ]( val.name, val.value );
				} 

				var keys = _splitFieldName( val.name );
				_addValueToKey( output, keys, outValue );
			});

			return output;
		};

		/**
		 * Splits a field name into an array of keys
		 * e.e. key[subkey][subsubkey] becomes ['key', 'subkey', 'subsubkey']
		 *
		 * @returns 	{array}
		 */
		var _splitFieldName = function (name) {
			var parts = name.split('[');

			parts = _.map( parts, function (part) {
				return part.replace(/\]/g, '')
			});

			if( parts[0] === '' ){
				parts.shift(); 
			}

			return parts;
		},

		/**
		 * Takes an array of keys (from _splitFieldName) and creates the deep array in 
		 * output with the specified value
		 *
		 * @returns 	{void}
		 */
		_addValueToKey = function (output, keys, value) {
			if( !_.isObject( output ) ){
				output = {};
			}

			var currentPath = output;

			if( _.isArray( keys ) ){
				for( var i = 0; i < keys.length; i++ ){
					if( _.isUndefined( currentPath[ keys[i] ] ) ){
						currentPath[ keys[i] ] = ( i == keys.length - 1 ) ? value : {};
					}
					currentPath = currentPath[ keys[i] ];
				}
			} else {
				output[ keys ] = value;
			}
		};

		return {
			serializeAsObject: serializeAsObject		
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.geolocation.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.geolocation.js - Geolocation helper methods
 * 
 * Author: IPS
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.geolocation', function () {

        const permissions = {
            GRANTED: 'granted',
            DENIED: 'denied',
            PROMPT: 'prompt'
        },
        type = 'geolocation',

        /**
         * Checks geolocation is allowed asynchronously 
         * 
         * @returns {boolean|null}
         */
		getGeolocationIsAllowed = function () {

            if ( ! "geolocation" in navigator ) return false; 

            const permission = ips.utils.db.get( type, `permission.${ipsSettings.memberID}` );

            if ( permission == permissions.GRANTED ) return true;

            return false;          
        },

        /**
         * Set - user allowed location or not. 
         * 
         * @param {string}     status   Permission Status ('granted', 'denied', 'prompt')
         * @returns {void}
         */
        setGeolocationIsAllowed = function ( status ) {

            if ( ! _.values( permissions ).includes( status ) ) return;

            ips.utils.db.set( type, `permission.${ipsSettings.memberID}`, status, false );
            
        },

        /** 
         * Gets the user coordinates asynchronously 
         * 
         * @returns {object}    The coordinates 
         */
        getCurrentPosition = function () {
            return new Promise( ( res, rej ) => {
                navigator.geolocation.getCurrentPosition( position => {
                    setGeolocationIsAllowed( permissions.GRANTED );
                    res( position.coords );
                }, error => {
                    if ( error.code == error.PERMISSION_DENIED ) {
                        setGeolocationIsAllowed( permissions.DENIED );
                    }
                    rej( error );
                });
            });
        },

        /** == INCOMPLETE ==
         *  Gets IP Address location asynchronously 
         * 
         * @returns {object}     The coordinates
         */ 
        generalLocation = function () {
            return new Promise( ( res, rej ) => {

                /* Check cache for guests first */
                if ( ! ipsSettings.memberID ) {
                    const ipLocation = ips.utils.db.get( type, `iplocation`);
                    if ( ipLocation ) {
                        res( ipLocation );
                    }
                }

                /* Fetch IP Location - TODO */
                res({
                    'latitude': 0,
                    'longitude': 0
                });
            });

        }

		return {
            getGeolocationIsAllowed,
            getCurrentPosition,
            setGeolocationIsAllowed,
            permissions
		};
	});

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.js" javascript_type="framework" javascript_version="107643" javascript_position="1000399"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.js - General utilities
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils', function (options) {

		/**
		 * Converts an arguments object to an array
		 *
		 * @param	{object} 	obj 	Arguments object
		 * @returns {array}
		 */
		var argsToArray = function (obj) {
			return Array.prototype.slice.call( obj );
		},

		/**
		 * Uppercases the first letter in a string
		 *
		 * @param 	{string} 	fromString 		String to use
		 * @returns {string}	String with first letter uppercased
		 */
		uppercaseFirst = function (fromString) {
			return fromString.charAt(0).toUpperCase() + fromString.slice(1);
		},

		/**
		 * Given a comma-delimited string, returns a string that can be used as a selector for IDs
		 *
		 * @param	{array,string} 	list 	Array of values, or comma-delimited string
		 * @returns {mixed}	 Selector string in format: #value1, #value2, or false if no values
		 */
		getIDsFromList = function (list) {
			if( !list ){
				return '';
			}
			
			if( !_.isArray( list ) ){
				list = list.toString().split(',');
			}

			list = _.compact( list );

			if( !list.length ){
				return false;
			}

			return _.map( list, function (val){
				return '#' + val;
			}).join(',');
		},
		
		/**
		 * Get a citation for a quote
		 *
		 * @param	{object} 	data 	The quote data
		 * @param	{bool}		html	If this can include HTML
		 * @returns {string}
		 */
		getCitation = function(data, html, defaultValue) {
			var citation = ips.getString('editorQuote');
			if ( defaultValue ) {
				var citation = defaultValue;
			}
			if( data.username ){
				var username = data.username;
				if ( html && data.userid && ips.getSetting('viewProfiles') ) {
					username = ips.templates.render( 'core.editor.citationLink', {
						baseURL: ips.getSetting('baseURL'),
						userid: data.userid,
						username: data.username
					} );
				} else {
					username = _.escape( username );
				}
				if( data.timestamp ){
					var citation = ips.getString( 'editorQuoteLineWithTime', {
						date: ips.utils.time.readable( data.timestamp ),
						username: username
					} );
				} else {
					var citation = ips.getString( 'editorQuoteLine', { username: username } );
				}
			}
			return citation;
		},

		escapeRegexp = function (toEscape) {
			return toEscape.replace( /[.*+?^${}()|[\]\\]/g, "\\$&" );
		},

		/**
		 * urlBase64ToUint8Array
		 * 
		 * @param {string} base64String a public vavid key
		 */
		urlBase64ToUint8Array = function (base64String) {
			var padding = '='.repeat((4 - base64String.length % 4) % 4);
			var base64 = (base64String + padding)
				.replace(/\-/g, '+')
				.replace(/_/g, '/');

			var rawData = window.atob(base64);
			var outputArray = new Uint8Array(rawData.length);

			for (var i = 0; i < rawData.length; ++i) {
				outputArray[i] = rawData.charCodeAt(i);
			}
			return outputArray;
		};

		return {
			argsToArray: argsToArray,
			uppercaseFirst: uppercaseFirst,
			getIDsFromList: getIDsFromList,
			getCitation: getCitation,
			escapeRegexp: escapeRegexp,
			urlBase64ToUint8Array: urlBase64ToUint8Array
		};
	});
}(jQuery,_));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.lazyLoad.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.lazyLoad.js - Content lazy loading manager
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.lazyLoad', function () {

		var _observer;
		var _rootMargin = "150px";
		var _threshold = 0;
		var _supportsObserver = true;
		var _registry = {};
		var _document = $( document );
		var contentSelector = "img[data-src], [data-background-src], iframe[data-embed-src], video[data-video-embed], audio[data-audio-embed]";

		/**
		 * Initialize module
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			if( !window.IntersectionObserver || _.isUndefined( window ) ){
				_supportsObserver = false;
			}
		},

		/**
		 * Observe intersection on an element
		 * Sets up an IntersectionObserver if it doesn't already exist
		 *
		 * @param 		{element}		node			Node to watch for intersections
		 * @param 		{function} 		loadCallback 	Optional callback function to handle loading the element's content. Replaces default behavior.
		 * @param 		{function} 		loadedCallback 	Optional callback function called after the element has loaded its content
		 * @returns 	{void}
		 */
		observe = function (node, callbacks, config) {
			var rawNode = node;

			if( node instanceof $ ){
				rawNode = node.get(0);
			}

			// Set default values
			config = _.defaults( config || {}, {
				rootMargin: _rootMargin,
				threshold: _threshold
			});

			// Create our observer if it doesn't exist. Note that we create one observer 
			// for the whole page, not one per observed element.
			if( !_observer ){
				_observer = new IntersectionObserver( _intersection, {
					rootMargin: config.rootMargin,
					threshold: config.threshold
				} );
			}

			if( rawNode.hasAttribute('data-loaded') ){
				return;
			}

			// If we're already observing this node, unobserve and start again
			try {
				_observer.unobserve(rawNode);
			} catch (err) { }

			if( _.isUndefined( callbacks ) ){
				callbacks = {};
			}

			// If this browser doesn't support IntersectionObserver, just load the images up front
			if( !_supportsObserver ){
				if( !_.isUndefined( callbacks.loadCallback ) ){
					callbacks.loadCallback.call(null, rawNode)
				} else {
					_load(rawNode);
				}
				return;
			}			

			if( !_.isUndefined( callbacks.loadCallback ) || !_.isUndefined( callbacks.loadedCallback ) || !_.isUndefined( callbacks.imgLoadedCallback ) ){
				_registry[ $( rawNode ).identify().attr('id') ] = {
					loadCallback: callbacks.loadCallback || null,
					loadedCallback: callbacks.loadedCallback || null,
					imgLoadedCallback: callbacks.imgLoadedCallback || null
				};
			}

			if( !_.isUndefined( callbacks.preloadCallback ) ){
				callbacks.preloadCallback.call(null, rawNode);
			} else {
				preload(rawNode);
			}

			_observer.observe(rawNode);
		},

		/**
		 * Set up padding on images within this node, so as to make them take up the appropriate
		 * spacing within the content before the image loads
		 *
		 * @param 		{element}		node	The element in which to search for images
		 * @returns 	{void}
		 */
		preload = function (rawNode) {
			if( _isImg(rawNode) && !rawNode.hasAttribute('data-loaded') && !rawNode.hasAttribute('data-loading') ){
				var lazyLoaded = [ rawNode ];
			} else {
				var lazyLoaded = rawNode.querySelectorAll('img[data-src]:not([data-loaded]):not([data-loading])');
			}

			_.each( lazyLoaded, function (element) {
				if( !element.hasAttribute('data-ratio') ){
					return;
				}

				var ratio = element.getAttribute('data-ratio').replace( ',', '.' );

				// If the element has a css width applied to it in style, or a width attribute, we'll use that to
				// fix the sizes because our padding-bottom trick for responsive styles won't work in this case.
				if( ( ( element.hasAttribute('style') && element.style.width ) || element.hasAttribute('width') ) ){
					if( element.style.height !== 'auto'){
						var width = element.hasAttribute('style') && element.style.width ? element.style.width : element.width;
						element.style.height = parseInt( ( parseInt( width ) / 100 ) * parseFloat( ratio ) ) + 'px';
					}
				} else {
					// 0 height is ok here because the padding bottom will cause the element to be visible
					// and therefore IntersectionObserver will see it
					element.style.height = '0';
					element.style.paddingBottom = parseFloat( ratio ) + '%';
				}
			});
		},

		/**
		 * Handles looping through intersected elements.
		 *
		 * @param 		{array}		entries		The watched intersect elements
		 * @returns 	{void}
		 */
		_intersection = function (entries) {
			for( var i = 0; i < entries.length; i++ ){
				if( entries[i].isIntersecting || entries[i].intersectionRatio > 0 ){
					_observer.unobserve( entries[i].target );

					// It's possible the node we were watching is now unattached from the document
					// If that's the case, we don't need to do anything with it here.
					if( !document.body.contains( entries[i].target ) ){
						continue;
					}
					
					if( entries[i].target.id && !_.isUndefined( _registry[ entries[i].target.id ] ) && _.isFunction( _registry[ entries[i].target.id ].loadCallback ) ){
						_registry[ entries[i].target.id ].loadCallback.call(null, entries[i].target);
					} else {
						_load(entries[i].target);
					}
				}
			}
		},

		/**
		 * Default load handler. Replaces data-src, data-background-src and data-embed-src
		 *
		 * @param 		{element}		node			Node in which to load content
		 * @returns 	{void}
		 */
		_load = function (rawNode, imgLoadedCallback) {
			// To avoid expensive jquery objects here, we'll use vanilla JS to swap attributes
			// Work inside an IntersectionObserver should be as fast as possible to avoid jank
			if( _isImg(rawNode) || _isEmbed(rawNode) || _isBackgroundImg(rawNode) || _isVideo(rawNode) ){
				var itemsToLoad = [ rawNode ];
			} else {		
				var itemsToLoad = rawNode.querySelectorAll( contentSelector );
			}

			Debug.log('Loading ' + itemsToLoad.length + ' items...');

			_.each( itemsToLoad, function (element) {
				if( _isImg(element) ){
					// Normal images
					// If we have an imgLoadedCallback we need to pass that through here
					if( ( ( rawNode.id && !_.isUndefined( _registry[ rawNode.id ] ) && _.isFunction( _registry[ rawNode.id ].imgLoadedCallback ) ) || _.isFunction(imgLoadedCallback) ) ){
						replaceImg(element, imgLoadedCallback ? imgLoadedCallback : _registry[ rawNode.id ].imgLoadedCallback);
					} else {
						replaceImg(element);
					}					
				} else if ( _isBackgroundImg(element) ) {
					// Background images
					replaceBackgroundImg(element);
				} else if ( _isEmbed(element) ){
					// Embeds (including external imbeds that are routed through our handler)
					replaceEmbed(element);
				} else if( _isVideo(element) ){
					// Videos
					// This section of code REPLACES core.front.core.embeddedVideo's functionality
					// But we keep that controller around for legacy videos and editor live preview
					replaceVideo(element);
				}
			});

			// If we have a loaded callback, call it now
			if( rawNode.id && !_.isUndefined( _registry[ rawNode.id ] ) && _.isFunction( _registry[ rawNode.id ].loadedCallback ) ){
				_registry[ rawNode.id ].loadedCallback.call(null, rawNode);
			}
		},

		/**
		 * Determine if this is a supported image
		 *
		 * @param 		{element}		rawNode		Node to check
		 * @returns 	{boolean}
		 */
		_isImg = function (rawNode) {
			return rawNode.tagName.toLowerCase() == 'img' && rawNode.hasAttribute('data-src');
		},

		/**
		 * Determine if this is a supported background image
		 *
		 * @param 		{element}		rawNode		Node to check
		 * @returns 	{boolean}
		 */
		_isBackgroundImg = function (rawNode) {
			return rawNode.hasAttribute('data-background-src');
		},

		/**
		 * Determine if this is a supported embed
		 *
		 * @param 		{element}		rawNode		Node to check
		 * @returns 	{boolean}
		 */
		_isEmbed = function (rawNode) {
			return rawNode.tagName.toLowerCase() == 'iframe' && rawNode.hasAttribute('data-embed-src');
		},

		/**
		 * Determine if this is a supported video
		 *
		 * @param 		{element}		rawNode		Node to check
		 * @returns 	{boolean}
		 */
		_isVideo = function (rawNode) {
			return rawNode.tagName.toLowerCase() == 'video' && rawNode.hasAttribute('data-video-embed');
		},

		/**
		 * Loads the provided img element
		 *
		 * @param 		{element}			element				The image to load
		 * @param 		{function|void} 	imgLoadedCallback	Optional callback for image onload
		 * @returns 	{void}
		 */
		replaceImg = function (element, imgLoadedCallback) {
			// Set fixed height to prevent page bouncing as images load in (we'll reset to auto later)
			// However if the height is already auto, leave it as it is to let the browser handle it
			if( element.hasAttribute('style') && element.style.height !== 'auto'){
				element.style.height = element.offsetHeight + 'px';
			}

			// Reset the bottom padding now that the image is loading
			element.style.paddingBottom = '';
			// Image loaded event handler. Needs to be set *before* the src is changed, for compatibility.
			// Once image has finished loading, remove the fixed height
			element.addEventListener('load', function () {
				// If a width/height have been manually specified on this image, and we have a ratio, then calculate height manually
				// For other situations, just set height to auto
				if( element.hasAttribute('data-ratio') && element.hasAttribute('style') && element.style.width && element.style.height && element.style.height !== 'auto' ){
					element.style.height = parseInt( ( parseInt( element.style.width ) / 100 ) * element.getAttribute('data-ratio').replace( ',', '.' ) ) + 'px';
				} else {				
					element.style.height = 'auto';
				}
				element.removeAttribute('data-loading');
				element.setAttribute('data-loaded', true);

				if( _.isFunction( imgLoadedCallback ) ){
					// If we have an imgLoaded callback, call it now
					imgLoadedCallback.call(null, element);
				}
			});
			// Now load the image
			element.src = element.getAttribute('data-src');
			element.setAttribute('data-loading', true);
		},

		/**
		 * Loads the provided background img element
		 *
		 * @param 		{element}			element				The element to load
		 * @returns 	{void}
		 */
		replaceBackgroundImg = function (element) {
			element.style.backgroundImage = "url('" + element.getAttribute('data-background-src') + "')";
		},

		/**
		 * Loads the provided embed
		 *
		 * @param 		{element}			element				The image to load
		 * @returns 	{void}
		 */
		replaceEmbed = function (element) {
			// We can't just the src on an iframe, because most browsers will create a history
			// entry, which will break the back button. Instead we have to copy the node,
			// set the src outside of the dom, then replace the current one.
			var copy = element.cloneNode();
			var src = element.getAttribute('data-embed-src');

			copy.setAttribute('src', src);
			copy.removeAttribute('data-embed-src');

			// URL of frames will contain protocol, but JS baseURL might not. 
			// Remove protocol here before comparing, to be safe.
			var srcWithoutProtocol = src.replace(/(^\w+:|^)\/\//, '');
			var baseWithoutProtocol = ips.getSetting('baseURL').replace(/(^\w+:|^)\/\//, '');

			// Internal embeds need our autosizing JS applied
			if( srcWithoutProtocol.startsWith( baseWithoutProtocol ) ){
				copy.setAttribute('data-controller', 'core.front.core.autoSizeIframe');
			}

			// Now replace and reinit
			element.parentNode.replaceChild(copy, element);
			_document.trigger('contentChange', [ $( copy ) ]);			
		},

		/**
		 * Loads the provided video element
		 *
		 * @param 		{element}			element				The image to load
		 * @returns 	{void}
		 */
		replaceVideo = function (element) {
			// Update source element to add src
			var sources = element.querySelectorAll('[data-video-src]');
			var canPlay = false;

			if( sources.length ){
				for( var i = 0; i < sources.length; i++ ){
					if( element.canPlayType( sources[i].getAttribute('type') ) ){
						// We can play this type, so set the src,
						// add our controller to the video and trigger an init
						element.setAttribute('src', sources[i].getAttribute('data-video-src'));
						sources[i].setAttribute('src', sources[i].getAttribute('data-video-src'));
						_document.trigger('contentChange', [ $( element ) ]);
						canPlay = true;
						break;
					}
				}
			}

			// If we can't play this video in this browser, replace it with either an embed, or a link
			if( !canPlay ){
				var embed = element.querySelector("embed");
				var link = element.querySelector(".ipsAttachLink");

				if( embed ){
					element.parentNode.replaceChild( embed, element );
				} else if( link ) {
					element.parentNode.replaceChild( link, element );
				}
			}					
		},

		/**
		 * Public method to call _load, which can be used to allow controllers to manage the loading process
		 *
		 * @param 		{element}		node			Node in which to load content
		 * @returns 	{void}
		 */
		loadContent = function (rawNode, imgLoadedCallback) {
			if( typeof bypassLoadContent !== "undefined" ) {
			    return;
			}
			
			if( rawNode instanceof $ ){
				rawNode = rawNode.get(0);
			}
			_load(rawNode, imgLoadedCallback);
		},

		/**
		 * Takes a node and adds the attributes necessary for lazy-loading to work
		 * This is designed for create-time use, i.e. when uploading or attaching images
		 *
		 * @param 		{element}		node			Node to work with
		 * @param 		{object} 		dims 			width/height to use (instead of natural<dim>)
		 * @param 		{boolean}		forceUpdate 	Update the element even if data-loaded is set?
		 * @returns 	{void}
		 */
		applyLazyLoadAttributes = function (rawNode, dims, forceUpdate) {
			if( rawNode.tagName.toLowerCase() !== 'img' || ( ( rawNode.hasAttribute('data-loaded') || rawNode.hasAttribute('data-emoticon') ) && !forceUpdate ) ){
				return;
			}

			var _loadHandler = function () {
				dims = dims || {};
				var height = dims.height || rawNode.naturalHeight;
				var width = dims.width || rawNode.naturalWidth;

				if( height == 'auto' ){
					width = rawNode.offsetWidth;
					height = rawNode.offsetHeight;
				}

				if( !_.isUndefined( ips.getSetting('maxImageDimensions') ) )
				{
					if( width > ips.getSetting('maxImageDimensions').width )
					{
						width = ips.getSetting('maxImageDimensions').width;

						if( height && height != 'auto' )
						{
							height = parseInt( ( parseInt( width ) / 100 ) * ( ( height / width ) * 100 ).toFixed(2) );
						}
					}

					if( height > ips.getSetting('maxImageDimensions').height )
					{
						height = ips.getSetting('maxImageDimensions').height;

						width = parseInt( ( parseInt( height ) / 100 ) * ( ( width / height ) * 100 ).toFixed(2) );
					}
				}

				if( !rawNode.hasAttribute('width') && width ){
					rawNode.setAttribute('width', width);
				}

				var ratio = ( ( height / width ) * 100 ).toFixed(2);

				if( ratio ){
					rawNode.setAttribute('data-ratio', ratio );
				}

				rawNode.setAttribute('data-loaded', true);
			};


			if( rawNode.hasAttribute('data-loaded') ){
				_loadHandler();
			} else {
				rawNode.addEventListener('load', _loadHandler);
			}
		};

		init();

		return {
			observe: observe,
			preload: preload,
			loadContent: loadContent,
			applyLazyLoadAttributes: applyLazyLoadAttributes,
			replaceImg: replaceImg,
			replaceBackgroundImg: replaceBackgroundImg,
			replaceEmbed: replaceEmbed,
			replaceVideo: replaceVideo,
			contentSelector: contentSelector
		};
	});

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.links.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.links.js - A module for working with links
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	&quot;use strict&quot;;
	
	ips.createModule('ips.utils.links', function () {

		/**
		 * Fires a manual event on an element
		 */
		var updateExternalLinks = function (element) {            
			if( ips.getSetting('links_external') ) {
				if( _.isUndefined( element ) ){
					return;
				}
				
				element.find('a[rel*=&quot;external&quot;]').each( function( index, elem ){
					elem.target = &quot;_blank&quot;;
					elem.rel = elem.rel.replace(&quot; noopener&quot;, &quot;&quot;) + &quot; noopener&quot;;
				});
			}
		};
		
		return {
			updateExternalLinks: updateExternalLinks
		};
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.notification.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.notification.js - A module for working with HTML5 notifications
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.notification', function () {

		var _isSupported = function() {
			// Apple does apple things so even though Safari will tell you it is supported, it really isn't
			if( "safari" in window ){
				return false;
			}

			if ( !("Notification" in window) || !Notification.requestPermission ) {
				return false;
			}

			if( _.isNull( ips.getSetting('pushPublicKey') ) ){
				return false;
			}
			
			if( Notification.permission == 'granted' ) {
				return true;
			}

			return true;
		},

		supported = _isSupported(),

		/**
		 * Requests permission for notifications from the user
		 *
		 * @returns 	{void}
		 */
		requestPermission = function () {
			if( supported ){
				Notification.requestPermission( function (result) {
					if( result == 'granted' ){
						$( document ).trigger('permissionGranted.notifications');	
					} else {
						$( document ).trigger('permissionDenied.notifications');
					}					
				});
			}
		},

		permissionGranted = function () {
			subscribeToPush();
		},

		/**
		 * Subscribes a user to push notifications
		 * Note: permission must be checked before calling or it will fail
		 *
		 * @returns 	{Promise}
		 */
		subscribeToPush = function () {
			$(document).trigger('subscribePending.notifications');

			return ips.utils.serviceWorker.registerServiceWorker('front', true)
				.then( (registration) => {					
					const options = {
						userVisibleOnly: true, // Required: https://developers.google.com/web/fundamentals/push-notifications/subscribing-a-user#uservisibleonly_options
						applicationServerKey: ips.utils.urlBase64ToUint8Array( ips.getSetting('pushPublicKey') )
					};

					return registration.pushManager.subscribe(options);
				})
				.then( (pushSubscription) => {
					Debug.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
					
					ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=notifications&do=subscribeToPush', {
						type: 'post',
						data: {
							subscription: JSON.stringify(pushSubscription),
							encoding: ( PushManager.supportedContentEncodings || ['aesgcm'] )[0]
						}
					})
						.done( (response) => {
							$(document).trigger('subscribeSuccess.notifications');
						})
						.fail( (jqXHR, textStatus, errorThrown) => {
							$(document).trigger('subscribeFail.notifications');
						});
				});
		},

		/**
		 * Determines whether the user has granted permission for notifications
		 *
		 * @returns 	{boolean}	Whether notifications have permission
		 */
		hasPermission = function () {
			if( !supported || ips.utils.cookie.get('noBrowserNotifications') || Notification.permission == 'denied' || Notification.permission == 'default' ){
				return false;
			}

			return true;
		},

		/**
		 * Get the user's subscription, if any
		 *
		 * @returns 	{Promise}
		 */
		getSubscription = function () {
			return ips.utils.serviceWorker.getRegistration()
				.then( (registration ) => {
					return registration.pushManager.getSubscription().then( subscription => {
						if( !subscription ){
							return false;
						}

						return subscription;
					})
					.catch( err => {
						Debug.log( err );
					});
				})
				.catch( err => {
					Debug.log(err);
				});
		},

		/**
		 * Do we need permission to show notifications? If the user has agreed or explicitly declined, this
		 * will be false. If they haven't decided yet, it'll return true.
		 *
		 * @returns 	{boolean} 	Whether the browser needs to ask for permission
		 */
		needsPermission = function () {
			if( supported && !ips.utils.cookie.get('noBrowserNotifications') && Notification.permission == 'default' ){
				return true;
			}

			return false;
		},

		/**
		 * Returns the granted permission level
		 *
		 * @returns 	{mixed}
		 */
		permissionLevel = function () {
			if( !supported ){
				return null;
			}

			return Notification.permission;
		},

		

		/**
		 * Creates a new notification and returns a notification object
		 *
		 * @param 		{object} 	options 	Configuration object
		 * @returns 	{function}
		 */
		create = function (options) {
			try {
				return new notification( options );
			} catch( e ) {
				Debug.log( e );

				if ( e.name == 'TypeError' ) {
					try {
						navigator.permissions.revoke( { name: "notifications" } );
					} catch( e ) {}

					return false;
				}
			}
		},

		/**
		 * Plays the notification sound
		 *
		 * @returns 	{function}
		 */
		playSound = function () {
			Debug.log("ips.utils.notification.playSound() is deprecated");
		};

		/**
		 * Our notification construct
		 *
		 * @param 		{object} 	options 	Configuration object
		 * @returns 	{void}
		 */
		function notification (options) {
			this._notification = null;

			this._options = _.defaults( options, {
				title: '',
				body: '',
				icon: '',
				timeout: false,
				tag: '',
				dir: $('html').attr('dir') || 'ltr',
				lang: $('html').attr('lang') || '',
				onShow: $.noop,
				onHide: $.noop,
				onClick: $.noop,
				onError: $.noop
			} );

			// Unescape body & title because we'll be getting escaped chars from the backend
			this._options.body = _.unescape( this._options.body.replace( /&#039;/g, "'" ).replace( /<[^>]*>?/g, '' ) );
			this._options.title = _.unescape( this._options.title.replace( /&#039;/g, "'" ) );

			this.show = function () {
				this._notification = new Notification( this._options.title, this._options );
				this._notification.addEventListener( 'show', this._options.onShow, false );
				this._notification.addEventListener( 'hide', this._options.onHide, false );
				this._notification.addEventListener( 'click', this._options.onClick, false );
				this._notification.addEventListener( 'error', this._options.onError, false );

				if( this._options.timeout !== false ){
					setTimeout( _.bind( this.hide, this ), this._options.timeout * 1000 );
				}
			};

			this.hide = function () {
				this._notification.close();
				this._notification.removeEventListener( 'show', this._options.onShow, false );
				this._notification.removeEventListener( 'hide', this._options.onHide, false );
				this._notification.removeEventListener( 'click', this._options.onClick, false );
				this._notification.removeEventListener( 'error', this._options.onError, false );
			};
		};

		$( document ).on('subscribeToPush.notifications', subscribeToPush);
		$( document ).on('requestPermission.notifications', requestPermission);
		$( document ).on('permissionGranted.notifications', permissionGranted );
		
		return {
			supported: supported,
			subscribeToPush: subscribeToPush,
			hasPermission: hasPermission,
			getSubscription: getSubscription,
			needsPermission: needsPermission,
			permissionLevel: permissionLevel,
			requestPermission: requestPermission,
			create: create,
			playSound: playSound
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.position.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.position.js - Positioning utilities
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.position', function () {

		var theWindow = $( window );

		/**
		 * Returns positioning information for the element
		 *
		 * @param 		{element} 	elem 	The element we're working on
		 * @returns 	{object}
		 */
		var getElemPosition = function (elem) {

			if( !elem ){
				return false;
			}

			var elem = $( elem );
			var props = {};
			var hidden = !elem.is(':visible');
			var opacity = elem.css('opacity');

			// We can only fetch the values we need if the element is visible
			// If it's hidden, make it ever so slightly visible - we'll hide it again later
			if( hidden ){
				elem.css({ opacity: "0.0001" }).show();
			}
			
			var offset = elem.offset();
			var position = elem.position();
			var dims = getElemDims( elem );

			// Absolute position
			props.absPos = {
				left: offset.left,
				top: offset.top,
				right: ( offset.left + dims.outerWidth ),
				bottom: ( offset.top + dims.outerHeight ),
			};

			// Offset position
			props.offsetPos = {
				left: position.left,
				top: position.top,
				right: ( position.left + dims.outerWidth ),
				bottom: ( position.top + dims.outerHeight )
			};

			// Viewport offsets
			// These will be overwritten for fixed elements
			props.viewportOffset = {
				left: offset.left - theWindow.scrollLeft(),
				top: offset.top - theWindow.scrollTop()
			};

			props.offsetParent = elem.offsetParent();

			// Special values if the element is in a fixed container
			props.fixed = ( hasFixedParents( elem, true ) );

			// Re-hide it if necessary
			if( hidden ){
				elem.hide().css({ opacity: String(opacity) });
			}

			return props;
		},

		/**
		 * Figures out the coords to position a popup element appropriately
		 * Abandon hope all ye who enter here
		 *
		 * @param	{object} 	options	 	Options for positioning
		 * @param 	{element}	options.trigger 	The trigger element for this popup
		 * @param	{element}	options.target 		The target element, i.e. the popup itself
		 * @param	{element}	options.targetContainer 	Container element of target, if not body
		 * @param	{boolean}	options.center 		Should the target be centered?
		 * @returns {void}
		 */
		positionElem = function (options) {
			var trigger = $( options.trigger );
			var triggerPos = getElemPosition( trigger );
			var triggerDims = getElemDims( trigger );
			var targetDims = getElemDims( options.target );
			var toReturn = {};
			var stemOffset = options.stemOffset || { top: 0, left: 0 };
			var offsetParent;
			var positioned = false;

			if( options.targetContainer ){
				// If we have any fixed parents, we switch to using the viewport offsets, since that's how
				// fixed elements are measured. For normal absolute/relative positioned parents, use the
				// values from position() instead.
				if( hasFixedParents( trigger ) ){
					offsetParent = triggerPos.viewportOffset;
				} else {
					var containerPos = getElemPosition( options.targetContainer );
					var containerOffset;

					// If the container we're adding to is static, then we'll also need to add its own offset positioning
					// If the container is positioned, we can skip this because the target will be positioned relative to it anyway.
					if( $( options.targetContainer ).css('position') == 'static' ){
						containerOffset = $( options.targetContainer ).position();
					} else {
						containerOffset = { left: 0, top: 0 };
					}

					// Here we work out the difference between the left positions of the trigger and the container to find out
					// how much we need to adjust the position of the target. We add in the offset to account for positioning, as above.
					offsetParent = {
						left: ( triggerPos.absPos.left - containerPos.absPos.left ) + containerOffset.left,
						top: ( triggerPos.absPos.top - containerPos.absPos.top ) + containerOffset.top
					};
				}

				positioned = true;
			} else {
				// Use the body
				offsetParent = triggerPos.viewportOffset;
			}

			// Work out the best fit for the target, trying to keep it from going off-screen
			var bestFit = _getBestFit( 
				triggerPos.viewportOffset, 
				triggerDims,	
				targetDims, 
				stemOffset, 
				{ 
					horizontal: ( options.center ) ? 'center' : 'left',
					vertical: ( options.above === true || options.above === 'force' ) ? 'top' : 'bottom'
				}, 
				( options.above === 'force' ) ? false : !options.above,
				!( options.above === 'force' )
			);

			// Start to build the return object
			switch( bestFit.horizontal ){
				case 'center':
					toReturn.left = offsetParent.left + ( triggerDims.outerWidth / 2 ) - 
							( targetDims.outerWidth / 2 );
				break;
				case 'left':
					toReturn.left = offsetParent.left - stemOffset.left + ( triggerDims.outerWidth / 2 );
				break;
				case 'right':
					toReturn.left = offsetParent.left - targetDims.outerWidth +
							 ( triggerDims.outerWidth / 2 ) + stemOffset.left;
				break;
			}

			switch( bestFit.vertical ){
				case 'top':
					toReturn.top = offsetParent.top - targetDims.outerHeight + 
										stemOffset.top;
				break;
				case 'bottom':
					toReturn.top = offsetParent.top + triggerDims.outerHeight -
										stemOffset.top;
				break;
			}

			if( !positioned && !triggerPos.fixed ) {
				toReturn.top += theWindow.scrollTop();
			}

			toReturn.fixed = triggerPos.fixed;
			toReturn.location = bestFit;

			return toReturn;
		},

		/**
		 * Returns true if the provided element has any fixed-position ancestors (including tables)
		 *
		 * @param	{element} 	elem	 	Element to test
		 * @returns {boolean}
		 */
		hasFixedParents = function (elem, andSelf) {
			elem = $( elem );
			var fixed = false;
			var parents = elem.parents();

			if( andSelf ){
				parents = parents.addBack();
			}

			parents.each( function () {
				if( this.style.position == 'fixed' ) {//|| $( this ).css('display').startsWith('table') ){
					fixed = true;
				}
			});

			return fixed;
		},

		/**
		 * Works out the best location for an element such that it tries to avoid going off-screen
		 *
		 * @param	{object} 	viewportOffset	 	viewport offset values for the element
		 * @param	{object}	triggerDims			Dimensions of the trigger element
		 * @param 	{object}	targetDims			Dimentions of the target element (menu, tooltip etc.)
		 * @param 	{object} 	offset 				Any offset to apply to numbers (e.g. to allow for stem)
		 * @param 	{object} 	posDefaults 		set default vertical/horizontal position
		 * @param 	{boolean}	preferBottom		Should target prefer opening under the trigger?
		 * @param 	{boolean} 	attemptToFit		If true, will change vertical position based on available space (and depending on preferBottom)
		 * @returns {object}
		 */
		_getBestFit = function (viewportOffset, triggerDims, targetDims, offset, posDefaults, preferBottom, attemptToFit) {
			var	position = _.defaults( posDefaults || {}, { vertical: 'bottom', horizontal: 'left' } );
			
			// Left pos
			if( position.horizontal == 'center' ){
				var targetLeft = viewportOffset.left + ( triggerDims.outerWidth / 2 ) - ( targetDims.outerWidth / 2 );
				var targetRight = targetLeft + targetDims.outerWidth;

				if( targetLeft < 0 || targetRight > theWindow.width() ){
					position.horizontal = 'left';
				}
			}

			if( position.horizontal == 'left' ){
				if( ( viewportOffset.left + (triggerDims.outerWidth / 2) + targetDims.outerWidth - offset.left ) > theWindow.width() ){
					position.horizontal = 'right';
				}
			} 
			
			if ( position.horizontal == 'right' ) {
				if( ( viewportOffset.right - (triggerDims.outerWidth / 2) - targetDims.outerWidth + offset.left ) < 0 ){
					position.horizontal = 'left';
				}
			}

			// Top pos
			if( attemptToFit ){
				if( position.vertical == 'top' || preferBottom ){
					if( ( viewportOffset.top - targetDims.outerHeight - offset.top ) < 0 ){
						position.vertical = 'bottom';
					}
				} else {
					if( ( viewportOffset.top + triggerDims.outerHeight + targetDims.outerHeight + offset.top ) > theWindow.height() ){
						position.vertical = 'top';
					}
				}
			}
			
			return position;
		},

		/**
		 * Returns dimensions for the given element
		 *
		 * @param	{element} 	elem	 	Element to test
		 * @returns {object}
		 */
		getElemDims = function (elem) {
			elem = $( elem );

			return {
				width: elem.width(),
				height: elem.height(),
				outerWidth: elem.outerWidth(),
				outerHeight: elem.outerHeight()
			};
		},

		/**
		 * Returns the natural width for an image element
		 *
		 * @param	{element} 	elem	 	Image element to use
		 * @returns {number}
		 */
		naturalWidth = function (elem) {
			return _getNatural( elem, 'Width' );
		},

		/**
		 * Returns the natural height for an image element
		 *
		 * @param	{element} 	elem	 	Image element to use
		 * @returns {number}
		 */
		naturalHeight = function (elem) {
			return _getNatural( elem, 'Height' );
		},

		/**
		 * Attempts to get the line-height of the provided element. Note; there's cases where this won't be reliable.
		 * For example, if CSS styles <span> differently, it may give a value different to the parent. Test your case
		 * before relying on this.
		 *
		 * @param	{element} 	parent	 	Element to fetch the line-height for
		 * @returns {number}
		 */
		lineHeight = function (parent) {
			// Create a little dummy element we can use to sniff the line height
			var newElem = $('<span/>').html('abc').css({
				opacity: "0.1"
			});
			parent.append( newElem );

			// Get it then remove the dummy element
			var height = newElem.height();
			newElem.remove();

			return height;
		},

		/**
		 * Returns the given natural dimension of an image, using the built in naturalWidth/Height property if available
		 *
		 * @param	{element} 	elem	 	Element to test
		 * @param 	{string} 	type 		Width or Height; dimension to return
		 * @returns {number}
		 */
		_getNatural = function (elem, type) {
			if( ( 'natural' + type ) in new Image() ){
				return elem[0][ 'natural' + type ];
			} else {
				var img = new Image();
				img.src = elem[0].src;
				return img[ 'natural' + type ];
			}
		};

		return {
			getElemPosition: getElemPosition,
			getElemDims: getElemDims,
			positionElem: positionElem,
			hasFixedParents: hasFixedParents,
			naturalWidth: naturalWidth,
			naturalHeight: naturalHeight,
			lineHeight: lineHeight
		};
	});
}(jQuery, _));
]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.responsive.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.responsive.js - A library for managing breakpoints in a responsive layout, 
 * and setting callbacks to be executed when breakpoints are hit.
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.responsive', function (options) {

		options = $.extend({
			breakpoints: {
				980: 'desktop',
				768: 'tablet',
				0: 'phone'
			}
		});

		var self = this,
			previousBreakpoint = [],
			currentBreakpoint = [],
			breakpointsBySize = {},
			breakpointsByKey = {},
			callbacks = {};

		// --------------------------------------------------------------------
		// PUBLIC METHODS
		
		/**
		 * Returns boolean denoting whether the supplied key name is
		 * the current size
		 *
		 * @param 	{mixed} 	toCheck 	The breakpoint value or key name to check
		 * @returns {boolean}
		 */
		var currentIs = function (toCheck) {

			if( _.isNumber( toCheck ) ){
				return currentBreakpoint[0] == toCheck;
			} else {
				return toCheck && currentBreakpoint[1] == toCheck;
			}
		},

		/**
		 * Returns the key name of the current size (e.g. 'phone')
		 *
		 * @returns 	{string}
		 */
		getCurrentKey = function () {
			return currentBreakpoint[1];
		},

		/**
		 * Returns all breakpoints, as an object
		 *
		 * @returns 	{object}
		 */
		getAllBreakpoints = function () {
			return breakpointsBySize;
		},

		/**
		 * Adds a breakpoint value
		 *
		 * @param 	{number}	breakpoint 	The size in px being registered
		 * @param 	{string}	name		An alphanumeric name to identify this breakpoint
		 * @returns {void}
		 */
		addBreakpoint = function (breakpoint, name) {
			breakpointsBySize[ breakpoint ] = name;
			breakpointsByKey[ name ] = breakpoint;

			// Add an empty array to the callback stack for later
			callbacks[ breakpoint ] = { enter: [], exit: [] };
		},

		/**
		 * Adds a callback to the queue for the specified breakpoint
		 * 
		 * @param 	breakpoint 		The breakpoint at which the callback will fire
		 * @param 	type 			Type of callback to add (enter or exit)
		 * @param 	callback 		The callback to be fired
		 * @returns {mixed} 		Void, or false if invalid callback is provided
		 */
		addCallback = function (breakpoint, type, callback) {

			if( !breakpointsBySize[ breakpoint ] || ( type != 'enter' && type != 'exit' ) ){
				return false;
			}

			callbacks[ breakpoint ][ type ].push( callback );
		},

		/**
		 * Fetches the current relevant breakpoint, and determines whether
		 * it has changed from the previous firing. If so, calls the callbacks
		 *
		 * @returns 	{void}
		 */
		checkForBreakpointChange = function () {
			
			var newBreak = getCurrentBoundary();

			// Different to the last round?
			if( newBreak != currentBreakpoint[0] ){

				// Execute any callbacks
				executeCallbacks( newBreak, currentBreakpoint[0] );

				// Update our previous/current breakpoint records
				previousBreakpoint = currentBreakpoint;
				currentBreakpoint = [ newBreak, breakpointsBySize[ newBreak ] ];

				// Fire event
				$( document ).trigger('breakpointChange', {
					curBreakSize: newBreak,
					curBreakName: breakpointsBySize[ newBreak ]
				});
			}
		},

		/**
		 * Runs the enter/exit callbacks for given breakpoints
		 *
		 * @param 	enterPoint	The breakpoint for the 'enter' callback
		 * @param 	exitPoint	The breakpoint for the 'exit' callback
		 *
		 * @returns {void}
		 */
		executeCallbacks = function (enterPoint, exitPoint) {

			if( !_.isUndefined( enterPoint ) && !_.isUndefined( callbacks[ enterPoint ] ) &&
				 !_.isUndefined( callbacks[ enterPoint ]['enter'] ) && callbacks[ enterPoint ]['enter'].length ){
				$.each( callbacks[ enterPoint ]['enter'], function (idx, thisCallback) {
					thisCallback();
				});
			}

			if( !_.isUndefined( exitPoint ) && !_.isUndefined( callbacks[ exitPoint ] ) &&
				 !_.isUndefined( callbacks[ exitPoint ]['exit'] ) && callbacks[ exitPoint ]['exit'].length ){
				$.each( callbacks[ exitPoint ]['exit'], function (idx, thisCallback) {
					thisCallback();
				});
			}
		},

		/**
		 * Works out the most relevant breakpoint based on window width
		 *
		 * @returns 	{number} 	Breakpoint size in px
		 */
		getCurrentBoundary = function () {

			var curWidth = window.innerWidth || $( window ).width();
			var curBreak;

			// Iterate to find which breakpoints are within range,
			// given the current window width
			var possibleSizes = _.filter( breakpointsByKey, function (num) { 
				return curWidth >= num; 
			});

			// If we have any breakpoints in range, get the biggest,
			// otherwise we'll get the smallest possible size
			if( possibleSizes.length ){
				curBreak = _.max( possibleSizes, function (num) { 
					return parseInt( num ); 
				});
			} else {
				curBreak = _.min( breakpointsByKey, function (num) {
					return parseInt( num );
				});
			}

			return curBreak;
		},

		// --------------------------------------------------------------------
		// PRIVATE METHODS

		/**
		 * Initialization method; imports default breakpoints and set up
		 * window.resize event
		 *
		 * @returns 	{void}
		 */
		init = function (){

			// Add our default breakpoints to start with
			$.each( options.breakpoints, function (size, name) {
				addBreakpoint(size, name);
			});

			$( window ).on( 'resize', windowResize );

			checkForBreakpointChange();
		},

		/**
		 * Event handler fired when window resizes
		 *
		 * @returns 	{void}
		 */
		windowResize = function () {
			checkForBreakpointChange();
		},

		enabled = function () {
			return true;
		};

		// Initialize this module
		init();

		// Expose public methods
		return {
			addBreakpoint: addBreakpoint,
			addCallback: addCallback,
			currentIs: currentIs,
			getCurrentKey: getCurrentKey,
			getAllBreakpoints: getAllBreakpoints,
			enabled: enabled
		}
	});

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.selection.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.selection.js - A module for working with text selection
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.selection', function () {

		/**
		 * Returns the text selected, ensuring it's fully within the provided ancestor
		 *
		 * @param 	{element} 	ancestor 	Ancestor element
		 * @returns {void}
		 */
		var getSelectedText = function (querySelector, container) {
			var text = '';
			var container = container.get(0);
			var selection = getSelection();

			if( selection.isCollapsed ){
				return '';
			}

			if( selection.rangeCount > 0 ){
				var range = selection.getRangeAt(0);
				var clonedSelection = range.cloneContents().querySelector( querySelector );

				// This loop checks that the selection is within the ancestor, so that we don't
				// show Quote This accidentally when another comment is selected.
				for (var i = 0; i < selection.rangeCount; ++i) {
					if( !_isChild( selection.getRangeAt(i).commonAncestorContainer, container ) ){
						return '';
					}
				}

				if( clonedSelection ){
					text = clonedSelection.innerHTML;
				} else {
					clonedSelection = range.cloneContents();
					var startNode = selection.getRangeAt(0).startContainer.parentNode;

					if( _isChild( startNode, container ) ) {
						var div = document.createElement('div');
						div.appendChild( clonedSelection );
						text = div.innerHTML;
					}
				}
					
				return text;
			} else if ( document.selection ){
				return document.selection.createRange().htmlText;
			}

			return '';
		};

		var getCommonAncestor = function () {
			var selection = getSelection();

			if( selection.isCollapsed ){
				return false;
			}

			var range = selection.getRangeAt(0);
			return $( range.commonAncestorContainer );
		};

		/**
		 * Get the Range of the selected text
		 *
		 * @param 	{element} 	container 	The container element
		 * @returns {object} Containing `type` (outside or inside) determining whether selection extends beyond our container, and `range`, the Range itself
		 */
		var getRange = function (container) {
			var selection = getSelection();

			if( selection.isCollapsed ){
				return false;
			}

			var range = selection.getRangeAt(0);
			var ancestor = $( range.commonAncestorContainer );

			// Figure out if the selection goes beyond our ancestor
			if( ancestor != container && !$( range.commonAncestorContainer ).closest( container ).length ){
				return {
					type: 'outside',
					range: range
				};
			}

			return {
				type: 'inside',
				range: range
			};
		};

		/**
		 * Returns the correct selection
		 *
		 * @returns {object}
		 */
		var getSelection = function () {
			return ( window.getSelection ) ? window.getSelection() : document.getSelection();
		};

		/**
		 * Returns boolean indicating whether child belongs to parent
		 *
		 * @param 	{element} 	child 	Child element
		 * @param 	{element} 	parent 	Parent element
		 * @returns {void}
		 */
		var _isChild = function (child, parent) {
			if(child === parent){
				return true;	
			} 
			
			var current = child;
			
			while (current) {
				if(current === parent){
					return true;	
				} 
				current = current.parentNode;
			}
			
			return false;
		};
		
		return {
			getSelectedText: getSelectedText,
			getSelection: getSelection,
			getRange: getRange,
			getCommonAncestor: getCommonAncestor
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.serviceWorker.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.serviceWorker.js - A module for working with service workers
 *
 * Author: Rikki Tissier
 */

(function ($, _, undefined) {
	"use strict";

	ips.createModule("ips.utils.serviceWorker", function () {
		const SERVICE_WORKER_URL = ips.getSetting("baseURL") + "index.php?app=core&module=system&controller=serviceworker&v=" + ips.getSetting("jsAntiCache");
		const supported = "serviceWorker" in navigator;

		/**
		 * Registers the service worker. When the user log in state changes, the URL of the SW changes
		 * which forces the browser to accept it as an update to the running SW. This means we can inspect the URL
		 * inside the SW and using the loggedIn param determine if the user is logged in or not.
		 *
		 * @returns {Promise}
		 */
		const registerServiceWorker = (type, loggedIn) => {
			return navigator.serviceWorker
				.register(`${SERVICE_WORKER_URL}&type=${type}&loggedIn=${JSON.stringify(loggedIn)}`)
				.then((registration) => {
					// Registration was successful
					Debug.log("ServiceWorker registration successful with scope: ", registration.scope);
					return registration;
				})
				.catch((err) => {
					// registration failed :(
					Debug.log("ServiceWorker registration failed: ", err);
				});
		};

		/**
		 * Gets the service worker registration
		 *
		 * @returns {Promise}
		 */
		const getRegistration = () => {
			return navigator.serviceWorker.ready;
		};

		return {
			registerServiceWorker,
			supported,
			getRegistration,
		};
	});
})(jQuery, _);
]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.sockets.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.sockets.js - A module for working with sockets
 *
 * Author: Rikki Tissier
 */

 (function ($, _, undefined) {
	"use strict";

	ips.createModule("ips.utils.sockets", function () {
		let socketIo;
		let pingInterval;
		let refreshToken;
		let refreshTokenUsed = false;
		let connected = false;

		const init = function () {
			if( !enabled() ){
				return;
			}

			ips.loader.get( [`${ips.getSetting('socketUrl')}/socket.io/socket.io.min.js`] ).then( function () {
				initializeSocketIo();
			});
		},

		/**
		 *
		 * Is realtime interaction enabled?
		 */
		enabled = function () {
			let res = !!(ips.getSetting('memberID') && ips.getSetting('socketEnabled') && ips.getSetting('page_token'));
			return res;
		},

		/**
		 * Set up socket io connection and event handlers
		 */
		initializeSocketIo = function () {
			socketIo = io(`${ips.getSetting('socketUrl')}/site-${ips.getSetting('siteId')}`, {
				transports: ['websocket'],
				forceNew: true,
				multiplex: false,
				query: {
					token: ips.getSetting('page_token')
				}
			});
		
			socketIo.on('connect', _eventConnect(socketIo));
			socketIo.on('message', _eventMessage);
			socketIo.on('ping', function () {
				socketIo.emit('pong');
			});
			socketIo.on('ping_interval', interval => {
				interval = Number(interval);
				if (interval && Number.isInteger(interval) && interval > 0) {
					clearInterval(pingInterval);
					pingInterval = setInterval(() => {
						socketIo.emit('ping');
					}, interval * 0.98); // we reduce by a factor of 2% for latency
				}
			})
			socketIo.on('connect_error', _eventConnectError);
			socketIo.on('disconnect', _eventDisconnect);

			socketIo.on('refresh_token', token => {
				// we'll do some LIGHT checking to make sure this is a JWT
				let isJwt = true;
				if (typeof token !== "string") {
					isJwt = false;
				} else {
					let components = token.split('.');
					if (components.length < 3) {
						isJwt = false;
					} else {
						for (let component of components.slice(0,2)) {
							if (typeof JSON.parse(atob(component)) !== 'object') {
								isJwt = false;
								break;
							}
						}
					}
				}

				if (isJwt) {
					refreshToken = token;
					refreshTokenUsed = false;
					Debug.log(`Got a refresh token from Node Services`)
				} else {
					Debug.warn(`Got a refresh token that is not a valid JWT from Node Services`)
				}
			})
		},

		useRefreshToken  = function() {
			// we wait a couple seconds before trying to refresh
			setTimeout(() => {
				if (refreshTokenUsed || typeof refreshToken !== 'string' || typeof ips.getSetting('page_token') !== 'string' || connected) {
					return
				}

				refreshTokenUsed = true;
				const url = new URL('?app=core&module=system&controller=ajax&do=refreshRealtimeToken', ipsSettings.baseURL?.replace(/^(?:http)?(s)?:?\/\/(.*)$/, `http$1://$2`));
				url.searchParams.append('page_token', ips.getSetting('page_token'));
				url.searchParams.append('refresh_token', refreshToken);
				url.searchParams.append('csrfKey', ips.getSetting('csrfKey'));
				ips.getAjax()( url.toString())
					.done( function(response) {
						if (response.page_token) {
							ips.setSetting('page_token', response.page_token);
							socketIo.off();
							socketIo.disconnect();
							refreshToken = undefined;
							initializeSocketIo();
						}
					} );

			}, 2000);
		},
		
		/**
		 * Emit an event to the socket server
		 * Note: events are whitelisted on the server
		 */
		send = function (event, data = {}) {
			if( !enabled() || !socketIo ){
				return;
			}

			socketIo.emit(event, data);
		},

		/**
		 * Event handlers
		 */

		/**
		 * Handle a 'message' event. This handler is a simple broker that re-emits the event
		 * on the document for interested controllers to listen to
		 */
		_eventMessage = function (data = {}) {
			if( typeof data !== 'object' || data.event === undefined ){
				Debug.log('Invalid message data');
				return;
			}

			const { event, type = null, ...rest } = data;
			let eventType = '';

			if( type !== null ){
				eventType = `:${type}`;
			}

			$(document).trigger(`socket.${event}${eventType}`, rest);
		},

		_setupInterval = function(interval) {

		},
		_eventConnect = socket => function () {
			connected = true;
			Debug.log("Connected to socket server");
			$(document).trigger('socket.connected');
			window.ips.socket = socket;
		},
		_eventConnectError = function () {
			Debug.log("Error connecting to socket server");
			$(document).trigger('socket.error');

			// let's try refreshing the access token
			useRefreshToken()
		},
		_eventDisconnect = function () {
			connected = false;
			Debug.log("Disconnected from socket server");
			$(document).trigger('socket.disconnected');
		};

		return {
			init,
			enabled,
			send
		};
	});
})(jQuery, _);
]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.time.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.time.js - A module for working with time/date
 *
 * Author: Mark Wade
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.time', function () {

		var _supportsLocale = null;

		/**
		 * Convert Unix Timestamp to human-readable (relative where appropriate) string
		 *
		 * @param		{number} 		timestamp 	Unix Timestamp
		 * @returns		{string}
		 */
		var readable = function (timestamp) {
			var date = new Date();
			var time = date.getTime() / 1000; // Timestamp in seconds
			var dst = 0;
			var now = time + dst;
			var elapsed = now - timestamp;

			if( ips.getSetting('relativeDates') ) {
				if ( elapsed < 60 ) {
					 return ips.getString('time_just_now');   
				} else if ( elapsed < 3600 ) {
					return ips.pluralize( ips.getString( 'time_minutes_ago' ), Math.floor( elapsed / 60 ) );  
				} else if ( elapsed < 5400 ) {
					return ips.getString( 'time_1_hour_ago' );
				} else if ( elapsed < 86400 ) {
					return ips.pluralize( ips.getString( 'time_hours_ago' ), Math.floor( elapsed / 3600 ) );   
				}
			}

			// Get an appropriate format
			var dateObj = new Date( timestamp * 1000 );
			var format = localeTimeFormat( $('html').attr('lang') );
			var time = formatTime( dateObj, format );

			// Format the datetime string
			var timeParts = ips.getString('time_at')
				? [ localeDateString( dateObj ), ips.getString('time_at') ]
				: [ localeDateString( dateObj ) ];
			timeParts.push(time);

			return ips.getString( 'time_other', { time: timeParts.join(' ') } );
		},

		/**
		 * Get date object from input field - abstracted to handle polyfills
		 *
		 * @param 		{object} 	input 	Input element
		 * @returns 	{object}	Date object
		 */
		getDateFromInput = function(input) {
			// jQuery UI Polyfill needs to be changed into the correct format
			if( !ips.utils.time.supportsHTMLDate() ) {
				// If it has been initiated, we can use the getDate method
				try {
					var thisDate = null;

					if( input.hasClass('hasDatepicker') ){
						thisDate = input.datepicker('getDate');
						Debug.log( 'hasDatePicker: ' + thisDate.toString() + '(' + thisDate.getTime() + ')' );
						//thisDate = new Date( thisDate.getUTCFullYear(), thisDate.getUTCMonth(), thisDate.getUTCDate() );
					} else {
						thisDate = new Date( input.attr('value') );
						Debug.log( 'no datepicker yet: ' + thisDate.toString()  + '(' + thisDate.getTime() + ')' );
					}

					return thisDate;
				}
				// If it hasn't we can pull the 'value' attribute which can't have been changed yet. Not .val() as that will be in the wrong format
				catch(err) {
					return new Date( input.attr('value') );
				}
			}
			// Actual HTML5 input always returns .val() in the correct YYYY-MM-DD format
			else {
				return new Date( input.val() );
			}
		},

		/**
		 * Removes the timezone from a date object, e.g. 1st Feb 00:00 EST will be turned into 1st Feb 00:00 GMT
		 *
		 * @param 		{object} 	input 	Input element
		 * @returns 	{Date}
		 */
		removeTimezone = function (date) {
			if( ips.utils.time.supportsHTMLDate() ){
				date.setTime( new Date( date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0 ).getTime() );	
			}

			var offset = date.getTimezoneOffset();
			var adjustedOffset = offset * 60000;

			if( offset > 0 ){
				date.setTime( date.getTime() + adjustedOffset );	
			} else {
				date.setTime( date.getTime() - adjustedOffset );
			}

			return date;
		},

		/**
		 * Returns a boolean indicating whether the user is in DST
		 *
		 * @param		{object} 		d 	Date object to test
		 * @returns		{boolean}
		 */
		isDST = function () {
			var today = new Date();		    
			var jan = new Date( today.getFullYear(), 0, 1);
			var jul = new Date( today.getFullYear(), 6, 1);
			var stdOffset = Math.max( jan.getTimezoneOffset(), jul.getTimezoneOffset() );

			return today.getTimezoneOffset() < stdOffset;
		},

		/**
		 * Returns true is the provided object is a valid Date object (containing a valid date)
		 *
		 * @param		{object} 		d 	Date object to test
		 * @returns		{boolean}
		 */
		isValidDateObj = function (d) {
			if( Object.prototype.toString.call( d ) !== "[object Date]" ){
				return false;
			}

			return !isNaN( d.getTime() );
		},

		/**
		 * Returns a current unix timestamp
		 *
		 * @returns		{number}
		 */
		timestamp = function () {
			return Date.now();
		},

		/**
		 * Returns javascript's toLocaleDateString, passing the options object
		 * if the browser supports the parameter
		 *
		 * @param		{date} 		date 		Date object
		 * @param 		{object} 	options		Options object (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString)
		 * @returns		{string}
		 */
		localeDateString = function (date, options) {
			if( !_.isBoolean( _supportsLocale ) ){
				_supportsLocale = _checkLocaleSupport();
			}

			if( _supportsLocale && _.isObject( options ) && $('html').attr('lang') ){
				return date.toLocaleDateString( $('html').attr('lang'), options );
			}
			else if( _supportsLocale && $('html').attr('lang') ){
				return date.toLocaleDateString( $('html').attr('lang') );
			} else {
				// UAs that don't support the options object will show the date in the system timezone, which means a midnight time will
				// show on the wrong day if you aren't UTC. To get around that, we'll work out the number of hours offset in the current
				// timezone, and adjust our date based on that to normalize it.
				//----
				// This caused all sorts of pain, so it has been commented out. Instead, methods should call ips.utils.time.removeTimezone
				// on date objects and then this method.
				/*var currentTimeZoneOffsetInHours = new Date().getTimezoneOffset() / 60;
				if ( currentTimeZoneOffsetInHours ) {
					date.setUTCHours( currentTimeZoneOffsetInHours );
				}*/
				//----
				return date.toLocaleDateString();
			}
		},

		/**
		 * Tests whether the browser supports native date pickers
		 *
		 * @returns		{boolean}
		 */
		supportsHTMLDate = function () {
			var i = document.createElement('input');
			i.setAttribute( 'type', 'date' );

			return i.type !== 'text';
		},

		/**
		 * Formats the given date object's time using the given locale format
		 *
		 * @param 		{object} 	Date object to format
		 * @param 		{object} 	Formatting object for a locale returned from localeTimeFormat()
		 * @returns		{string}	Locale-formatted time string
		 */
		formatTime = function (dateObj, localeFormat) {
			if( !_.isDate( dateObj ) ){
				dateObj = timestamp();
			}

			var formatters = {
				/* Day */
				"a": function(d) { // An abbreviated textual representation of the day
					return ips.getString( 'day_' + d.getDay() + '_short' );
				},
				"A": function(d) { // A full textual representation of the day
					return ips.getString( 'day_' + d.getDay() );
				},
				"d": function (d) { // Two-digit day of the month (with leading zeros)
					var day = d.getDate().toString();
					return ( ( day.length === 1 ) ? '0' : '' ) + day;
				},
				"e": function (d) { // Day of the month, with a space preceding single digits
					var day = d.getDate().toString();
					return ( ( day.length === 1 ) ? ' ' : '' ) + day;
				},
				"j": function (d) { // Day of the year, 3 digits with leading zeros
					var day = d.getDate();
					var month = d.getMonth();
					if ( month > 0 ) { // Jan
						day += 31;
					}
					if ( month > 1 ) { // Feb
						day += 28;
						if ( d.getFullYear() % 4 == 0 ) {
							day += 1;
						}
					}
					if ( month > 2 ) { // Mar
						day += 31;
					}
					if ( month > 3 ) { // Apr
						day += 30;
					}
					if ( month > 4 ) { // May
						day += 31;
					}
					if ( month > 5 ) { // Jun
						day += 30;
					}
					if ( month > 6 ) { // Jul
						day += 31;
					}
					if ( month > 7 ) { // Aug
						day += 31;
					}
					if ( month > 8 ) { // Sep
						day += 30;
					}
					if ( month > 9 ) { // Oct
						day += 31;
					}
					if ( month > 10 ) { // Nov
						day += 30;
					}
					if ( month > 11 ) { // Dec
						day += 31;
					}
					return day.toString().padStart( 3, '0' );
				},
				"u": function (d) { // ISO-8601 numeric representation of the day of the week
					return d.getDay() + 1;
				},
				"w": function (d) { // Numeric representation of the day of the week
					return d.getDay();
				},
				
				/* Week */
				"U": function (d) { // Week number of the given year, starting with the first Sunday as the first week
					var firstSundayDate = 1;
					var firstSunday = new Date( d.getFullYear(), 0, firstSundayDate, 0, 0, 0, 0 );
				    
					while ( firstSunday.getDay() != 0 ) {
				        firstSundayDate += 1;
				        firstSunday = new Date( d.getFullYear(), 0, firstSundayDate, 0, 0, 0, 0 );
					}
					
					var now = d.getTime() / 1000;
					var weekNumber = 0;
					var timestamp = firstSunday.getTime() / 1000;
					
					while ( timestamp < now ) {
						weekNumber++;
						timestamp += 604800;
					}
					
					weekNumber = weekNumber.toString();
					return ( ( weekNumber.length === 1 ) ? '0' : '' ) + weekNumber;
				},
				"V": function (d) { // ISO-8601:1988 week number of the given year, starting with the first week of the year with at least 4 weekdays, with Monday being the start of the week
					var firstApplicableDate = 1;
					var firstApplicable = new Date( d.getFullYear(), 0, firstApplicableDate, 0, 0, 0, 0 );
					
					while( [ 2, 3, 4, 5 ].indexOf( firstApplicable.getDay() ) == -1 ) {
						firstApplicableDate += 1;
						firstApplicable = new Date( d.getFullYear(), 0, firstApplicableDate, 0, 0, 0, 0 );
					}
					
					var now = d.getTime() / 1000;
					var weekNumber = 0;
					var timestamp = firstApplicable.getTime() / 1000;
					
					while ( timestamp < now ) {
						weekNumber++;
						timestamp += 604800;
					}
					
					weekNumber = weekNumber.toString();
					return ( ( weekNumber.length === 1 ) ? '0' : '' ) + weekNumber;
				},
				"W": function (d) { // A numeric representation of the week of the year, starting with the first Monday as the first week
					var firstMondayDate = 1;
					var firstMonday = new Date( d.getFullYear(), 0, firstMondayDate, 0, 0, 0, 0 );
				    
					while ( firstMonday.getDay() != 1 ) {
				        firstMondayDate += 1;
				        firstMonday = new Date( d.getFullYear(), 0, firstMondayDate, 0, 0, 0, 0 );
					}
					
					var now = d.getTime() / 1000;
					var weekNumber = 0;
					var timestamp = firstMonday.getTime() / 1000;
					
					while ( timestamp < now ) {
						weekNumber++;
						timestamp += 604800;
					}
					
					return weekNumber;
				},
				
				/* Month */
				"b": function(d) { // Abbreviated month name, based on the locale
					return ips.getString( 'month_' + d.getMonth() + '_short' );
				},
				"B": function(d) { // Full month name, based on the locale
					return ips.getString( 'month_' + d.getMonth() );
				},
				// OB is same as B, but is used occasionally due to oddities in certain locales
				"OB": function(d) { // Full month name, based on the locale
					return ips.getString( 'month_' + d.getMonth() );
				},
				"h": function(d) { // Abbreviated month name, based on the locale (an alias of %b)
					return ips.getString( 'month_' + d.getMonth() + '_short' );
				},
				"m": function(d) { // Two digit representation of the month
					var month = d.getMonth() + 1;
					var realMonth = month.toString();
					return ( ( realMonth.length === 1 ) ? '0' : '' ) + realMonth;
				},
				
				/* Year */
				"C": function(d) { // Two digit representation of the century (year divided by 100, truncated to an integer)
					return parseInt( ( d.getFullYear() / 100 ).toString().substr( 0, 2 ) );
				},
				"g": function(d) { // Two digit representation of the year going by ISO-8601:1988 standards (see %V)
					var year = d.getFullYear();
					if ( d.getMonth() == 0 && d.getDate() < 3 && d.getDay() < 2 ) {
						year--;
					}
					return parseInt( year.toString().substr( 0, 2 ) );
				},
				"G": function(d) { // The full four-digit version of %g
					var year = d.getFullYear();
					if ( d.getMonth() == 0 && d.getDate() < 3 && d.getDay() < 2 ) {
						year--;
					}
					return year;
				},
				"y": function(d) { // Two digit representation of the year
					return parseInt( d.getFullYear().toString().substr( 0, 2 ) );
				},
				"Y": function(d) { // Four digit representation for the year
					return d.getFullYear();
				},
				
				/* Time */
				"H": function (d) { // Two digit representation of the hour in 24-hour format
					var hrs = d.getHours().toString();
					return ( ( hrs.length === 1 ) ? '0' : '' ) + hrs;
				},
				"k": function (d) { // Hour in 24-hour format, with a space preceding single digits
					var hrs = d.getHours();
					return ( ( hrs.length === 1 ) ? ' ' : '' ) + hrs;
				},
				"I": function (d) { // Two digit representation of the hour in 12-hour format
					var hrs = d.getHours();
					hrs = ( hrs > 12 ) ? hrs - 12 : hrs;

					if( hrs == 0 )
					{
						hrs = 12;
					}

					return ( ( hrs.length === 1 ) ? '0' : '' ) + hrs;
				},
				"l": function (d) { // Hour in 12-hour format, with a space preceding single digits
					var hrs = d.getHours();
					hrs = ( hrs > 12 ) ? hrs - 12 : hrs;

					if( hrs == 0 )
					{
						hrs = 12;
					}

					return ( ( hrs.length === 1 ) ? ' ' : '' ) + hrs;
				},
				"M": function (d) { // Two digit representation of the min
					var mins = d.getMinutes().toString();
					return ( ( mins.length === 1 ) ? '0' : '' ) + mins;
				},
				"N": function (d) { // Single digit representation of the min
					return d.getMinutes();
				},
				"p": function (d) { // UPPER-CASE 'AM' or 'PM' based on the given time
					var hrs = d.getHours();
					if( !_.isFunction( localeFormat.meridiem ) ){
						return '';
					}

					return localeFormat.meridiem( hrs, false );
				},
				"P": function (d) { // lower-case 'am' or 'pm' based on the given time
					var hrs = d.getHours();
					if( !_.isFunction( localeFormat.meridiem ) ){
						return '';
					}

					return localeFormat.meridiem( hrs, true );
				},
				"r": function(d) { // Same as "%I:%M:%S %p"
					var hrs = d.getHours();
					hrs = ( hrs >= 12 ) ? hrs - 12 : hrs;
					var mins = d.getMinutes().toString();
					var seconds = d.getSeconds().toString();
					
					return ( ( hrs.length === 1 ) ? '0' : '' ) + hrs + ':' + ( ( mins.length === 1 ) ? '0' : '' ) + mins + ':' + ( ( seconds.length === 1 ) ? '0' : '' ) + seconds;
				},
				"R": function(d) { // Same as "%H:%M"
					var hrs = d.getHours().toString();
					var mins = d.getMinutes().toString();
					return ( ( hrs.length === 1 ) ? '0' : '' ) + hrs + ':' + ( ( mins.length === 1 ) ? '0' : '' ) + mins;
				},
				"S": function(d) { // Two digit representation of the second
					var seconds = d.getSeconds().toString();
					return ( ( seconds.length === 1 ) ? '0' : '' ) + seconds;
				},
				"T": function(d) { // Same as "%H:%M:S"
					var hrs = d.getHours().toString();
					var mins = d.getMinutes().toString();
					var seconds = d.getSeconds().toString();
					return ( ( hrs.length === 1 ) ? '0' : '' ) + hrs + ':' + ( ( mins.length === 1 ) ? '0' : '' ) + mins + ':' + ( ( seconds.length === 1 ) ? '0' : '' ) + seconds;
				},
				"X": function(d) { // Preferred time representation based on locale, without the date
					return d.toLocaleTimeString();
				},
				"z": function(d) { // The time zone offset
					var matches = d.toString().match( /GMT([+\-]\d{4}) \((.+)\)$/ );
					return matches[1];
				},
				"Z": function(d) { // The time zone abbreviation
					var matches = d.toString().match( /GMT([+\-]\d{4}) \((.+)\)$/ );
					return matches[2];
				},

				/* Time and Date Stamps */
				"c": function(d) { // Preferred date and time stamp based on locale
					var hrs = d.getHours().toString();
					var mins = d.getMinutes().toString();
					var seconds = d.getSeconds().toString();
					return ips.getString( 'day_' + d.getMonth() + '_short' ) + ' ' + ips.getString( 'month_' + d.getMonth() + '_short' ) + ' ' + d.getDate().toString() + ' ' + ( ( hrs.length === 1 ) ? '0' : '' ) + hrs + ':' + ( ( mins.length === 1 ) ? '0' : '' ) + mins + ':' + ( ( seconds.length === 1 ) ? '0' : '' ) + seconds + ' ' + d.getFullYear().toString();
				},
				"D": function(d) { // Same as "%m/%d/%y"
					var month = d.getMonth().toString();
					var day = d.getDate().toString();
					return ( ( month.length === 1 ) ? '0' : '' ) + month + '/' + ( ( day.length === 1 ) ? '0' : '' ) + day + '/' + parseInt( d.getFullYear().toString().substr( 0, 2 ) ).toString();
				},
				"F": function(d) { // Same as "%Y-%m-%d" (commonly used in database datestamps)
					var month = d.getMonth().toString();
					var day = d.getDate().toString();
					return d.getFullYear().toString() + '-' + ( ( month.length === 1 ) ? '0' : '' ) + month + '-' + ( ( day.length === 1 ) ? '0' : '' ) + day;
				},
				"s": function(d) { // Unix Epoch Time timestamp
					return parseInt( d.getTime() / 1000 );
				},
				"x": function(d) { // Preferred date representation based on locale, without the time
					return d.toLocaleDateString(0);
				},
				
				/* Miscellaneous */
				"n": function (d) { // A newline character ("\n")
					return "\n";
				},
				"t": function (d) { // A Tab character ("\t")
					return "\t";
				},
				"%": function (d) { // A literal percentage character ("%")
					return '%';
				}
			};
			
			return localeFormat.format.replace(/%([aAdejuwUVWbBhmCgGyYHkIlMNPprRSTXzZcDFsxnt%]|OB)/g, function (match0, match1) {
				if( formatters[ match1 ] ){
					return formatters[ match1 ]( dateObj );
				}
			});
		},

		/**
		 * Returns the formatting information for the given locale (defaults to English)
		 *
		 * @param 		{string} 	ISO 639‑1 code (e.g. en-gb)
		 * @returns		{object}	Formatting object containing 'format' and sometimes 'meridiem' keys
		 */
		localeTimeFormat = function (locale) {
			var locales = _getLocaleTimeFormat();
			var language = locale.split('-');

			if( !_.isUndefined( locales[ locale.toLowerCase() ] ) ){
				// Check the full locale first (e.g. en-US and en-GB dialects are different )
				return locales[ locale.toLowerCase() ];
			} else if( !_.isUndefined( locales[ language[0].toLowerCase() ] ) ){
				// Try the main language next
				return locales[ language[0].toLowerCase() ];
			} else {
				// Default to English
				return locales['en'];
			}
		},

		/**
		 * Tests if the browser supports the options parameter of toLocaleDateString
		 *
		 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString
		 * @returns		{boolean}
		 */
		_checkLocaleSupport = function () {
			try {
				new Date().toLocaleDateString("i");
			} catch (e) {
				return e.name === "RangeError";
			}

			return false;
		},

		/**
		 * Returns time formats for each locale
		 * Data pieced together from what moment.js provides
		 *
		 * @returns		{object}
		 */
		_getLocaleTimeFormat = function () {

			var defaultMeridiem = function (hour, lower) {
				if( hour < 12 ){
					return ( lower ? 'am' : 'AM' );
				} else {
					return ( lower ? 'pm' : 'PM' );
				}
			};

			// %H - Two digit hour in 24hr format (01, 23)
			// %k - Two digit hour in 24hr format (1, 23)
			// %l - Hour in 12hr format (1, 12)
			// %M - Two digit minute (01, 56)
			// %N - Single didget minite (1, 56)
			// %p - Uppercase AM/PM
			// %P - Lowercase am/pm
			return {
				'af': { format: '%H:%M' },
				'ar-ma': { format: '%H:%M' },
				'ar-sa': { format: '%H:%M' },
				'ar-tn': { format: '%H:%M' },
				'ar': { format: '%H:%M' },
				'az': { format: '%H:%M' },
				'be': { format: '%H:%M' },
				'bg': { format: '%k:%M' },
				'bn': { format: '%p %l:%M সময়', meridiem: function (hour) {
					if (hour < 4) {
						return 'রাত';
					} else if (hour < 10) {
						return 'সকাল';
					} else if (hour < 17) {
						return 'দুপুর';
					} else if (hour < 20) {
						return 'বিকেল';
					} else {
						return 'রাত';
					}
				} },
				'bo': { format: '%p %l:%M', meridiem: function (hour) {
					if (hour < 4) {
						return 'མཚན་མོ';
					} else if (hour < 10) {
						return 'ཞོགས་ཀས';
					} else if (hour < 17) {
						return 'ཉིན་གུང';
					} else if (hour < 20) {
						return 'དགོང་དག';
					} else {
						return 'མཚན་མོ';
					}
				} },
				'br': { format: '%le%M %p', meridiem: defaultMeridiem },
				'bs': { format: '%k:%M' },
				'ca': { format: '%k:%M' },
				'cs': { format: '%k:%M' },
				'cv': { format: '%H:%M' },
				'cy': { format: '%H:%M' },
				'da': { format: '%H:%M' },
				'de-at': { format: '%H:%M' },
				'de': { format: '%H:%M' },
				'el': { format: '%l:%M %p', meridiem: function (hour, lower) {
					if (hour > 11) {
						return lower ? 'μμ' : 'ΜΜ';
					} else {
						return lower ? 'πμ' : 'ΠΜ';
					}
				} },
				'en-au': { format: '%l:%M %p', meridiem: defaultMeridiem },
				'en-ca': { format: '%l:%M %p', meridiem: defaultMeridiem },
				'en-gb': { format: '%H:%M' },
				'en': { format: '%l:%M %p', meridiem: defaultMeridiem },
				'eo': { format: '%H:%M' },
				'es': { format: '%k:%M' },
				'et': { format: '%k:%M' },
				'eu': { format: '%H:%M' },
				'fa': { format: '%H:%M' },
				'fi': { format: '%H.%M' },
				'fo': { format: '%H:%M' },
				'fr-ca': { format: '%H:%M' },
				'fr': { format: '%H:%M' },
				'fy': { format: '%H:%M' },
				'gl': { format: '%k:%M' },
				'he': { format: '%H:%M' },
				'hi': { format: '%p %l:%M बजे', meridiem: function (hour) {
					if (hour < 4) {
						return 'रात';
					} else if (hour < 10) {
						return 'सुबह';
					} else if (hour < 17) {
						return 'दोपहर';
					} else if (hour < 20) {
						return 'शाम';
					} else {
						return 'रात';
					}
				} },
				'hr': { format: '%k:%M' },
				'hu': { format: '%k:%M' },
				'hy-am': { format: '%H:%M' },
				'id': { format: '%H.%M' },
				'is': { format: '%k:%M' },
				'it': { format: '%H:%M' },
				'ja': { format: '%p%l時%N分', meridiem: function (hour) {
					if (hour < 12) {
						return '午前';
					} else {
						return '午後';
					}
				} },
				'jv': { format: '%H.%M' },
				'ka': { format: '%l:%M %p', meridiem: defaultMeridiem },
				'km': { format: '%H:%M' },
				'ko': { format: '%p %l시 %N분', meridiem: function (hour) {
					return hour < 12 ? '오전' : '오후';
				} },
				'lb': { format: '%k:%M Auer' },
				'lt': { format: '%H:%M' },
				'lv': { format: '%H:%M' },
				'me': { format: '%k:%M' },
				'mk': { format: '%k:%M' },
				'ml': { format: '%p %l:%M -നു', meridiem: function (hour) {
					if (hour < 4) {
						return 'രാത്രി';
					} else if (hour < 12) {
						return 'രാവിലെ';
					} else if (hour < 17) {
						return 'ഉച്ച കഴിഞ്ഞ്';
					} else if (hour < 20) {
						return 'വൈകുന്നേരം';
					} else {
						return 'രാത്രി';
					}
				} },
				'mr': { format: '%p %l:%M वाजता', meridiem: function (hour) {
					if (hour < 4) {
						return 'रात्री';
					} else if (hour < 10) {
						return 'सकाळी';
					} else if (hour < 17) {
						return 'दुपारी';
					} else if (hour < 20) {
						return 'सायंकाळी';
					} else {
						return 'रात्री';
					}
				} },
				'ms-my': { format: '%H.%M' },
				'ms': { format: '%H.%M' },
				'my': { format: '%H:%M' },
				'nb': { format: '%k.%M' },
				'ne': { format: '%pको %l:%M बजे', meridiem: function (hour) {
					if (hour < 3) {
						return 'राती';
					} else if (hour < 10) {
						return 'बिहान';
					} else if (hour < 15) {
						return 'दिउँसो';
					} else if (hour < 18) {
						return 'बेलुका';
					} else if (hour < 20) {
						return 'साँझ';
					} else {
						return 'राती';
					}
				} },
				'nl': { format: '%H:%M' },
				'nn': { format: '%H:%M' },
				'pl': { format: '%H:%M' },
				'pt-br': { format: '%H:%M' },
				'pt': { format: '%H:%M' },
				'ro': { format: '%k:%M' },
				'ru': { format: '%H:%M' },
				'si': { format: '%P %l:%M', meridiem: function (hours, lower) {
					if (hours > 11) {
						return lower ? 'ප.ව.' : 'පස් වරු';
					} else {
						return lower ? 'පෙ.ව.' : 'පෙර වරු';
					}
				} },
				'sk': { format: '%k:%M' },
				'sl': { format: '%k:%M' },
				'sq': { format: '%H:%M' },
				'sr-cyrl': { format: '%k:%M' },
				'sr': { format: '%k:%M' },
				'sv': { format: '%H:%M' },
				'ta': { format: '%H:%M' },
				'th': { format: '%k นาฬิกา %N นาที' },
				'tl-ph': { format: '%H:%M' },
				'tr': { format: '%H:%M' },
				'tzl': { format: '%H.%M' },
				'tzm-latn': { format: '%H:%M' },
				'tzm': { format: '%H:%M' },
				'uk': { format: '%H:%M' },
				'uz': { format: '%H:%M' },
				'vi': { format: '%H:%M' },
				'zh-cn': { format: '%p%l点%M分', meridiem: defaultMeridiem },
				'zh-tw': { format: '%p%l點%M分', meridiem: defaultMeridiem }
			}
		};
		
		return {
			readable: readable,
			localeDateString: localeDateString,
			isValidDateObj: isValidDateObj,
			timestamp: timestamp,
			supportsHTMLDate: supportsHTMLDate,
			localeTimeFormat: localeTimeFormat,
			formatTime: formatTime,
			getDateFromInput: getDateFromInput,
			removeTimezone: removeTimezone
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.url.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.url.js - A module for getting query params from the URL
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.url', function () {

		var _skipped = ['s'],
			_store = {},
			_origin;

		var init = function (queryString) {

		},
		
		/**
		 * Returns the requested parameter from the URL
		 *
		 * @param 	{string} 	name 	Parameter to return
		 * @param 	{string} 	[url] 	Url to parse (uses current url if none specified)
		 * @returns {string}
		 */
		getParam = function (name, url) {
			/*Debug.log( 'getParam:' + parseUri( url || window.location.href ).queryKey[ name ] );
			Debug.log( 'getParam params: ');
			Debug.log( parseUri( url || window.location.href ) );
			Debug.log( 'href: ' + window.location.href );*/
			return parseUri( url || window.location.href ).queryKey[ name ];
		},
		
		/**
		 * Returns the page number from the URL
		 *
		 * @param 	{string} 	name 	Parameter to return
		 * @param 	{string} 	[url] 	Url to parse (uses current url if none specified)
		 * @returns {string}
		 */
		getPageNumber = function (param, url) {
			if ( param == 'page' ) {
				var parsedurl = parseUri( url || window.location.href );
				
				if ( parsedurl.path.match( /\/index\.php/ ) ) {
					// Query string based FURL
					var pageNum = null;
					$.each( parsedurl.queryKey, function (key, value) {
						if ( pageNum === null && key.match( /\/page\/\d+?(\/|$)/ ) && ! value ) {
							var match = key.match( /\/page\/(\d+?)(\/|$)/ );

							if ( match !== null && ! _.isUndefined(match[1]) ) {
								pageNum = parseInt( match[1] );
							}
						}
					} );
					
					if ( pageNum !== null ) {
					 	return pageNum;
					}
				}
				else {
					var matches = parsedurl.path.match( /\/page\/(\d+?)\// );
					
					if ( matches !== null && ! _.isUndefined( matches ) ) {
						return parseInt( matches[1] );
					}
				}
			}
			
			var paramPage = this.getParam( param, url || window.location.href );
			return ! _.isUndefined( paramPage ) ? parseInt( paramPage ) : 1;
		},

		/**
		 * Moves site.com/forums/topic/123-foo/?page=4&sort=foo to site.com/forums/topic/123-foo/page/4/?sort=foo
		 *
		 * @param 	{string} 	url 		URL
		 * @param 	{string} 	param 		Page param to use
		 * @param 	{string} 	number 		Page number
		 * @returns {string}	new URL
		 */
		pageParamToPath = function (url, name, number) {
			var uriObject = getURIObject( url );
	
			uriObject.queryKey = _.pick( uriObject.queryKey, function( value, key ) {
				return ( key != name );
			} );
			
			if ( uriObject.path.match( /\/index\.php/ ) ) {
				// Query string based FURL
				var f = null;
				$.each( uriObject.queryKey, function (key, value) {
					if ( f === null && key.match( /^\// ) && ! value ) {
						f = key;
					}
				} );
				
				if ( f !== null ) {
					uriObject.queryKey = _.omit( uriObject.queryKey, f );
					
					var match = f.match( new RegExp( '/' + name + '/\\d+?/$' ) );
				
					if ( match !== null && ! _.isUndefined(match[0]) ) {
						f = f.replace( new RegExp( match[0] ), '/' );
					}

					var newKey = {};
					newKey[ ( number > 1 ) ? decodeURI( f ) + name + '/' + number + '/' : f ] = '';
					uriObject.queryKey = _.extend( newKey, uriObject.queryKey );
				}
			}
			else {
				// .htaccess based FURL		
				var match = uriObject.path.match( new RegExp( '/' + name + '/\\d+?/$' ) );
				
				if ( match !== null && ! _.isUndefined(match[0]) ) {
					uriObject.path = uriObject.path.replace( new RegExp( match[0] ), '/' );
				}
				
				/* now add on the new path */
				if ( number > 1 ) {
					uriObject.path += name + '/' + number + '/';
				}
			}			

			return rebuildUriObject( uriObject );
		},

		/**
		 * Strips the requested parameter from the URL and returns the URL
		 *
		 * @param 	{string} 	name 	Parameters to strip
		 * @param 	{string} 	[url] 	Url to parse (uses current url if none specified)
		 * @returns {string}
		 * @note This is just a shortcut to removeParams
		 */
		removeParam = function (name, url) {
			return this.removeParams( [ name ], url );
		},

		/**
		 * Strips the requested parameters from the URL and returns the URL
		 *
		 * @param 	{array} 	name 	Parameters to strip
		 * @param 	{string} 	[url] 	Url to parse (uses current url if none specified)
		 * @returns {string}
		 */
		removeParams = function (name, url) {
			var uriObject = parseUri( url || window.location.href );

			uriObject.queryKey = _.pick( uriObject.queryKey, function( value, key ) { 
				return ( jQuery.inArray( key, name ) == -1 );
			} );

			return this.rebuildUriObject( uriObject );
		},
		
		/**
		 * Rebuilds a parseUri object back into a URL string
		 *
		 * @param 	{object} 	[uriObject] 	Result of parseUri
		 * @returns {string}
		 */
		rebuildUriObject = function( uriObject ) {
			var returnUrl = uriObject.protocol + '://' + uriObject.host + ( ( uriObject.port !== '' ) ? ':' + uriObject.port : '' ) + uriObject.path;

			if( _.keys( uriObject.queryKey ).length )
			{
				var qsParam = '?';

				_.each( uriObject.queryKey, function( value, key ) {
					if( value )
					{
						returnUrl = returnUrl + qsParam + key + '=' + value;
					}
					else
					{
						// This is here for older style furls, e.g. /index.php?/app/path/ so that we don't end up with /index.php?/app/path/=
						returnUrl = returnUrl + qsParam + key;
					}
					qsParam = '&';
				})
			}

			return returnUrl;
		},

		/**
		 * Returns the parsed URL object from parseUri
		 *
		 * @param 	{string} 	[url] 	Url to parse (uses current url if none specified)
		 * @returns {string}
		 */
		getURIObject = function (url) {
			return parseUri( url || window.location.href );
		},

		/**
		 * Returns an origin for use in window.postMessage
		 *
		 * @returns {string}
		 */
		getOrigin = function () {
			if( !_origin ){
				var url = getURIObject();
				_origin = url.protocol + '://' + url.host + ( ( url.port !== '' ) ? ':' + url.port : '' );
			}
			
			return _origin;
		};

		// parseUri 1.2.2
		// (c) Steven Levithan <stevenlevithan.com>
		// MIT License

		function parseUri (str) {
			// If we have the modern 'URL' API, use that definitely
			if( 'URL' in window ) {
				try {
					// Fix protocol-relative URLs as the URL API does not like them
					if( str.indexOf('//') === 0 )
					{
						str = location.protocol + str;
					}

					var o = new URL( str );

					// We need to reformat the returned URL object keys
					var uri = {
						'source': o.href,
						'protocol': o.protocol.substring( 0, ( o.protocol.length - 1 ) ),
						'userInfo': ( o.username ? o.username : '' ) + ( ( o.username && o.password ) ? ':' : '' ) + ( o.password ? o.password : '' ),
						'user': o.username,
						'password': o.password,
						'host': o.hostname,
						'port': o.port,
						'relative': o.pathname + ( o.search ? o.search : '' ),
						'path': o.pathname,
						'directory': '',  // The URL class does not give us just the path
						'file': '',  // The URL class does not give us just the filename
						'query': o.search.substring(1),
						'anchor': o.hash,
						'queryKey': {}
					};

					// Set the authority using shortcuts we just set
					uri.authority = uri.userInfo + ( uri.userInfo ? '@' : '' ) + uri.host;

					// Set the queryKey object
					o.searchParams.forEach( function( v, k ) {
						uri.queryKey[ k ] = v;
					});

					// Figure out the path and file
					var urlBits		= uri.path.split('/');
					uri.file		= urlBits.pop();
					uri.directory	= urlBits.join('/');

					return uri;
				} catch( err ) {
					// If it failed, likely due to a bad URL, we can let the older polyfill take a stab at it - but log to console so we know.
					Debug.log( "Failed to parse URL: " + str + " ; " + err );
				}
			}

			var	o   = parseUri.options,
				m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
				uri = {},
				i   = 14;

			while (i--) uri[o.key[i]] = m[i] || "";

			uri[o.q.name] = {};
			uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
				if ($1) uri[o.q.name][$1] = $2;
			});

			return uri;
		};

		parseUri.options = {
			strictMode: false,
			key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
			q:   {
				name:   "queryKey",
				parser: /(?:^|&)([^&=]*)=?([^&]*)/g
			},
			parser: {
				strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
				loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
			}
		};
	
		return {
			getPageNumber: getPageNumber,
			getParam: getParam,
			removeParam: removeParam,
			removeParams: removeParams,
			getURIObject: getURIObject,
			getOrigin: getOrigin,
			rebuildUriObject: rebuildUriObject,
			pageParamToPath: pageParamToPath
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.validate.js" javascript_type="framework" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.utils.validate.js - A library for validating values
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";
	
	ips.createModule('ips.utils.validate', function (options) {

		/**
		 * Format object - checks data in a field matches a particular regex format
		 */
		var formats = {
			email: /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))){2,6}$/i,
			url: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/,
			alphanum: /^\w+$/,
			integer: /^\d+$/,
			number: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/,
			creditcard: /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/,
			hex: /^[0-9a-f]+$/i
		};

		/**
		 * Validators object - methods run to check a field meets conditions
		 */
		var validators = {
			maxlength: function (val, max) {
				return val.length <= max;
			},

			minlength: function (val, min) {
				return val.length >= min;
			},

			rangelength: function (val, min, max) {
				return validators.maxlength( val, max ) && validators.minlength( val, min );
			},

			min: function (val, min) {
				return Number( val ) >= min;
			},

			max: function (val, max) {
				return Number( val ) <= max;
			},

			range: function (val, min, max) {
				return validators.min( val, min ) && validators.max( val, max );
			},

			required: function (val) {
				return val.length > 0
			},

			regex: function (val, regex) {
				return new RegExp( regex ).test( val );
			},

			format: function (val, format) {
				return new RegExp( formats[ format ] ).test( val );
			},

			remote: function (val, url) {
				var deferred = $.Deferred();

				ips.getAjax()( url, {
					dataType: 'json',
					data: {
						input: encodeURI( val )
					}
				})
					.done( function (response) {
						if( response.result == 'ok' ){
							deferred.resolve();
						} else {
							deferred.reject( response.message || null );
						}
					})
					.fail( function (jqHXR, textStatus) {
						deferred.reject( textStatus );
					});

				return deferred.promise();
			}
		};

		// Setters
		/**
		 * Adds a custom format
		 *
		 * @param	{string} 	name 		Identifying name for this format
		 * @param	{regexp} 	format 		The format, as a regexp literal
		 * @returns {void}
		 */
		var addFormat = function (name, format) {
			formats[ name ] = format;
		},

		/**
		 * Adds a custom validator
		 *
		 * @param	{string} 	name 		Identifying name for this validator
		 * @param	{function} 	fn 			Function called when this validator is used
		 * @returns {void}
		 */
		addValidator = function (name, fn) {
			validators[ name ] = fn;
		};

		// Shortcut methods for individual validators/formats
		/**
		 * Checks whether the value is a valid URL
		 *
		 * @param	{string} 	url 		The URL to validate
		 * @returns {boolean}
		 */
		var isUrl = function (url) {
			return validators.regex( url, formats.url );
		},

		/**
		 * Checks whether the value is an allowed URL
		 *
		 * @param	{string} 	url 		The URL to validate
		 * @returns {boolean}
		 */
		isAllowedUrl = function (url) {
			var returnValue;
			returnValue	= true;

			if( ips.getSetting('blacklist') )
			{
				for( var i in ips.getSetting('blacklist') )
				{
					var blacklistUrl = ips.getSetting('blacklist')[i];
					blacklistUrl	= escapeRegExp( blacklistUrl );
					blacklistUrl	= blacklistUrl.replace( /\\\*/g, '(.+?)' );

					var index		= url.search( new RegExp( blacklistUrl, 'ig' ) );

					if( index >= 0 )
					{
						returnValue	= false;
						break;
					}
				}
			}

			if( ips.getSetting('whitelist') )
			{
				returnValue	= false;

				for( var i in ips.getSetting('whitelist') )
				{
					var whitelistUrl = ips.getSetting('whitelist')[i];
					whitelistUrl	= escapeRegExp( whitelistUrl );
					whitelistUrl	= whitelistUrl.replace( /\\\*/g, '(.+?)' );

					var index		= url.search( new RegExp( whitelistUrl, 'ig' ) );

					if( index >= 0 )
					{
						returnValue	= true;
						break;
					}
				}
			}

			return returnValue;
		},

		/**
		 * Checks whether the value is a valid email address
		 *
		 * @param	{string} 	email 		The email address to validate
		 * @returns {boolean}
		 */
		isEmail = function (email) {
			return validators.regex( email, formats.email );
		};

		/**
		 * Main validation method
		 * Combines provided conditions with HTML5 conditions gleaned from the element. Checks each condition
		 * by executing the relevant validators.
		 *
		 * @param	{element} 	field 			The element being validated
		 * @param	{object} 	conditions 		Object of conditions/values to use when validating
		 * @param 	{boolean} 	ignoreHTML5		Ignore the HTML5 validation properties?
		 * @returns {object}	
		 */
		var validate = function (field, conditions, ignoreHTML5) {
			if( !ignoreHTML5 ){
				conditions = _.extend( _getAutomaticConditions( field ), conditions || {} );
			}

			if( !_.size( conditions ) ){
				return true;
			}

			// Now work through each condition
			var validated = true;
			var messages = [];

			for( var i in conditions ) {
				if( !_.isFunction( validators[ i ] ) ){
					continue;
				}

				var value = field.val();
				var args = [];

				if( _.isObject( conditions[ i ] ) ){
					args = _.values( conditions[ i ] )
					args.splice( 0, 1 );
				} else {
					args = [ conditions[ i ] ];
				}

				args.unshift( value );

				if( validators[ i ].apply( this, args ) !== true ) {
					validated = false;
					messages.push( {
						condition: i,
						message: _getMessage( i, args )
					});
				}
			}

			return {
				result: validated,
				messages: messages
			}
		},

		/**
		 * Returns the parsed error message for the given validator type
		 *
		 * @param	{string} 	type 		The validator type
		 * @param	{object} 	args 		The values originally passed into the validator
		 * @returns {string}
		 */
		_getMessage = function (type, args) {
			return ips.pluralize( ips.getString( 'validation_' + type, { data: args } ), [ ( ( type == 'rangelength' ) ? args[2] : args[1] ) ] );
		},

		/**
		 * Builds an object of conditions for an element based on HTML5 attributes (e.g. required)
		 *
		 * @param	{element} 	field 		The element being validated
		 * @returns {object}
		 */
		_getAutomaticConditions = function (field) {
			var conditions = {};

			if( field.is('[required]') ){
				conditions.required = true;
			}

			if( field.is('input[type="number"], input[type="range"], input[type="email"], input[type="url"]') ){
				conditions.format = field.attr('type');
			}

			if( field.is('[max]') ){
				conditions.max = field.attr('max');
			}

			if( field.is('[min]') ){
				conditions.min = field.attr('min');
			}

			if( field.is('[pattern]') ){
				conditions.regex = field.attr('pattern');
			}

			return conditions;
		};

		// Expose public methods
		return {
			isUrl: isUrl,
			isEmail: isEmail,
			addFormat: addFormat,
			addValidator: addValidator,
			validate: validate,
			isAllowedUrl: isAllowedUrl
		}
	});

}(jQuery, _));

// http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
var escapeRegExp;

(function () {
  // Referring to the table here:
  // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/regexp
  // these characters should be escaped
  // \ ^ $ * + ? . ( ) | { } [ ]
  // These characters only have special meaning inside of brackets
  // they do not need to be escaped, but they MAY be escaped
  // without any adverse effects (to the best of my knowledge and casual testing)
  // : ! , = 
  // my test "~!@#$%^&*(){}[]`/=?+\|-_;:'\",<.>".match(/[\#]/g)

  var specials = [
        // order matters for these
          "-"
        , "["
        , "]"
        // order doesn't matter for any of these
        , "/"
        , "{"
        , "}"
        , "("
        , ")"
        , "*"
        , "+"
        , "?"
        , "."
        , "\\"
        , "^"
        , "$"
        , "|"
      ]

      // I choose to escape every character with '\'
      // even though only some strictly require it when inside of []
    , regex = RegExp('[' + specials.join('\\') + ']', 'g')
    ;

  escapeRegExp = function (str) {
    return str.replace(regex, "\\$&");
  };

  // test escapeRegExp("/path/to/res?search=this.that")
}());]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/clubs" javascript_name="ips.clubs.navbar.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.clubs.navbar.js - Club Navigation Manager
 *
 * Author: Daniel Fatkic
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.clubs.navbar', {

		_interval: null,
		_sortableElem: null,

		initialize: function () {
			this.on( document, 'click', '[data-action="reorderClubmenu"]', this.startReorder );
			this.on( document, 'click', '[data-action="saveClubmenu"]', this.saveOrder );
			this.setup();
		},

		setup: function () {
			this._sortableElem = this.scope.find('ul');
		},

		/**
		 * Starts the tab reordering interface by building drag handles for each tab
		 * and setting up sortable
		 *
		 * @returns 	{void}
		 */
		startReorder: function (e) {
			e.preventDefault();

			$('[data-action="saveClubmenu"]').removeClass('ipsHide');
			$('[data-action="reorderClubmenu"]').parent().addClass('ipsHide');

			var self = this;
			this._sortableElem.find('a')
				.addClass('ipsCursor_drag')
				.prepend( ips.templates.render('club.menu.dragHandle') );

			ips.loader.get( ['core/interface/jquery/jquery-ui.js'] ).then( function () {
				self._sortableElem
					.sortable({
						items: '> li',
						forcePlaceholderSize: true,
						update: function () {
							self._orderChanged = true;
						}
					});

				self._reordering = true;
				self._orderChanged = false;
				self._sortableElem.find('a [data-role="clubMenuDrag"]').fadeIn();
			});
		},

		/**
		 * Finish sorting
		 *
		 * @returns 	{void}
		 */
		_finishReorder: function () {
			this._sortableElem.find('a')
				.removeClass('ipsCursor_drag')
				.find('[data-role="clubMenuDrag"]')
					.remove();

			$('[data-action="saveClubmenu"]').addClass('ipsHide');
			$('[data-action="reorderClubmenu"]').parent().removeClass('ipsHide');
			this._sortableElem.sortable( 'destroy' );
			this._reordering = false;
		},

		/**
		 * Saves the new order of tabs, sending an ajax request with the new order
		 *
		 * @returns 	{void}
		 */
		saveOrder: function (e) {
			e.preventDefault();
			
			var self = this;
			var tabOrder = this._sortableElem.sortable( 'toArray', { attribute: 'data-tab'} );
			if( this._orderChanged ){
				ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=clubs&controller=view&do=saveMenu', {
					data: {
						tabOrder: tabOrder,
						id: this.scope.attr('data-clubID')
					},
					dataType: 'json',
					type: 'post'
				} )
					.done( function (response) {
						ips.ui.flashMsg.show( ips.getString('tab_order_saved') );
					})
					.fail( function ( ) {
						ips.ui.alert.show( {
							type: 'alert',
							icon: 'warning',
							message: ips.getString('tab_order_not_saved'),
							callbacks: {
								ok: function () {}
							}
						});
					})
					.always( function () {
						self._finishReorder();
					});
			} else {
				this._finishReorder();
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/clubs" javascript_name="ips.clubs.requests.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.clubs.requests.js - Requests handler
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.clubs.requests', {

		_interval: null,

		initialize: function () {
			this.on( 'click', '[data-action="requestApprove"], [data-action="requestDecline"]', this.handleRequest );
			this.on( document, 'menuItemSelected', this.handleRequest );
			this.on( window, 'resize', this.resizeCovers );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			
		},

		/**
		 * Handles resizing cover divs when the window resizes
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		resizeCovers: function (e) {
			var self = this;
			var cards = this.scope.find('.ipsMemberCard[data-hasCover]');

			if( cards.length ){
				$.each( cards, function () {
					var id = $( this ).identify().attr('id');
					var cover = $('body').find('.cClubRequestCover[data-cardId="' + id + '"]');
					self._positionCover( $( this ), cover );
				});
			}
		},

		/**
		 * Handles approving/declining a request
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		handleRequest: function (e, data) {
			
			var self = this;
			if ( e.type == 'menuItemSelected' ) {
				if ( data.menuElem.attr('data-role') != 'acceptMenu' ) {
					return;
				}
				data.originalEvent.preventDefault();
				var url = $( data.originalEvent.target ).attr('href');
				var card = $( e.target ).closest('.ipsMemberCard');
			} else {
				e.preventDefault();
				var url = $( e.currentTarget ).attr('href');
				var card = $( e.currentTarget ).closest('.ipsMemberCard');
			}			
			var id = card.identify().attr('id');
			
			// Disable the buttons while we wait
			card.find('[data-action]').addClass('ipsButton_disabled');

			ips.getAjax()( url, {
				showLoading: true
			})
				.done( function (response) {

					card.attr('data-hasCover', true);

					// Fade out the card
					card.animate({
						opacity: "0.2"
					});

					// Build a cover
					var cover = $('<div/>').addClass('cClubRequestCover').attr('data-cardId', id);
					$('body').append( cover );

					self._positionCover( card, cover );

					cover
						.append( ips.templates.render( response.status == 'approved' ? 'club.request.approve' : 'club.request.decline' ) )
						.fadeIn();

					// Show flash message
					ips.ui.flashMsg.show( response.status == 'approved' ? ips.getString('clubMemberApproved') : ips.getString('clubMemberDeclined'), { escape: false } );

					if( !self._interval ){
						self._interval = window.setInterval( _.bind( self._checkCardsExist, self ), 200 );
					}
				})
				.fail( function () {
					window.location = url;
				});
		},

		/**
		 * Fired by an interval timer, checks whether a card still exists, and removes the cover if not
		 *
		 * @returns {void}
		 */
		_checkCardsExist: function () {
			var self = this;
			var covers = $('body').find('.cClubRequestCover');
			var cards = this.scope.find('.ipsMemberCard[data-hasCover]');

			// If we have the same count, we can leave
			if( cards.length == covers.length ){
				return;
			}

			if( covers.length ){
				$.each( covers, function () {
					var cardId = $( this ).attr('data-cardId');
					var card = self.scope.find('#' + cardId);

					if( !card.length ){
						$( this ).remove();
					}
				});
			}
		},
		
		/**
		 * Position the cover over the card
		 *
		 * @param 	{element} 	card 	Card div
		 * @param 	{element} 	cover 	Cover div
		 * @returns {void}
		 */
		_positionCover: function (card, cover) {
			var elemPosition = ips.utils.position.getElemPosition( card );
			var dims = ips.utils.position.getElemDims( card );

			cover.css({
				position: 'absolute',
				top: elemPosition.absPos.top + 'px',
				left: elemPosition.absPos.left + 'px',
				width: dims.outerWidth + 'px',
				height: dims.outerHeight + 'px'
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.acpSearchKeywords.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.acpSearchKeywords.js - Faciliates editing keywords for ACP search when IN_DEV
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.core.acpSearchKeywords', {

		initialize: function () {
			this.on( 'click', '[data-action=&quot;save&quot;]', this.saveKeywords );
		},
		
		/**
		 * Save the keywords
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		saveKeywords: function (e) {			
			e.preventDefault();
			var scope = this.scope;
			
			var data = {
				url: scope.attr('data-url'),
				lang_key: scope.find( &quot;[data-role='lang_key']&quot; ).val(),
				restriction: scope.find( &quot;[data-role='restriction']&quot; ).val(),
				keywords: []
			};
			
			scope.find( &quot;[data-role='keywords']&quot; ).each(function(){
				if( $(this).val() ){
					data.keywords.push( $(this).val() );
				}
			});
						
			ips.getAjax()( scope.attr('data-action'), {
			   data: data,
			   type: 'post',
			   showLoading: true
			})
			.done( function(response) {
			   scope.trigger('closeMenu');
			   ips.ui.flashMsg.show('Keywords saved');
			});
		},
	});
}(jQuery, _));
</file>
 <file javascript_app="global" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.app.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/* global ips, _, Debug */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.app.js - AdminCP base app controller
 * This controller is used only for items that are global event handlers. Where functionality is specific to
 * a feature or section, a new controller should be created.
 *
 * Author: Rikki Tissier
 */ 
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.app', {

		initialize: function () {
			this.on( 'click', 'a.noscript_fallback, a.ipsJS_preventEvent', this.noScriptFallback );
			this.on( 'click', '[data-clickshow]', this.clickShow );
			this.on( 'click', '[data-clickhide]', this.clickHide );
			this.on( 'click', '[data-clickempty]', this.clickEmpty );
			this.on( 'click', '[data-delete]', this.deleteSomething );
			this.on( 'click', 'a[data-confirm]', this.confirmSomething );
			this.on( 'click', '[data-doajax]', this.doAjaxAction );
			this.on( document, 'contentChange', this._checkAndClearAutosave );

			this.setup();
		},

		/**
		 * General app setup. Creates flashMsgs if needed, shows/hides JS elements
		 *
		 * @returns {void}
		 */
		setup: function () {
			if( ips.utils.serviceWorker.supported ) {
				ips.utils.serviceWorker.registerServiceWorker('admin', false);
			}
			
			// Add a classname to the document for js purposes
			this.scope.addClass('ipsJS_has').removeClass('ipsJS_none');
			
			// Clear any autosave stuff
			this._checkAndClearAutosave();
			if ( !ips.getSetting('memberID') && ips.utils.url.getParam('_fromLogout') ) {
				ips.utils.db.removeByType('editorSave');
			}

			// Set up prettyprint
			prettyPrint();

			// Set our JS cookie for future use - we'll set it for a day
			if( _.isUndefined( ips.utils.cookie.get( 'hasJS') ) ){
				var expires = new Date();
				expires.setDate( expires.getDate() + 1 );
				ips.utils.cookie.set( 'hasJS', true, expires.toUTCString() );
			}
		},

		/**
		 * Check and clear autosave
		 *
		 * @returns {void}
		 */
		 _checkAndClearAutosave: function() {
		 	if( ips.utils.cookie.get('clearAutosave') ) {
				var autoSaveKeysToClear = ips.utils.cookie.get('clearAutosave').split(',');
				for ( var i = 0; i < autoSaveKeysToClear.length; i++ ) {
					ips.utils.db.remove( 'editorSave', autoSaveKeysToClear[i] );
				}
				ips.utils.cookie.unset('clearAutosave');
			}
		 },

		/**
		 * Sends an ajax request with the link's href, and shows a flash message on success
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		doAjaxAction: function (e) {
			e.preventDefault();

			ips.getAjax()( $( e.currentTarget ).attr('href'), { dataType: 'json' } )
				.done( function (response) {
					ips.ui.flashMsg.show( response );
				})
				.fail( function (jqXHR) {
					if( Debug.isEnabled() ){
						Debug.error( jqXHR.responseText );
					} else {
						window.location = $( e.currentTarget ).attr('href');
					}
				});
		},

		/**
		 * Prompts the user to confirm a deleting action, sends ajax request to do the delete,
		 * and removes a row if necessary
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		deleteSomething: function (e) {
			e.preventDefault();

			var elem = $( e.currentTarget );
			var deleteTitle = elem.attr('data-delete-message');
			var extraWarning = elem.attr('data-delete-warning');

			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'warn',
				message: deleteTitle || ips.getString('delete_confirm'),
				subText: extraWarning || '',
				focus: 'cancel',
				callbacks: {
					ok: function () {
						// Do we not want to execute via AJAX?
						if( elem.attr('data-noajax') !== undefined ){
							window.location = elem.attr('href') + '&csrfKey=' + ips.getSetting('csrfKey') + '&wasConfirmed=1';
							return;
						}

						var row = null;

						// Check if there's any rows to delete
						if( elem.attr('data-deleterow') ){
							row = $( elem.attr('data-deleterow') );
						} else {
							row = elem.closest('tr, .row, [data-role=node]');
						}

						let bypassRedirect = !(elem.attr( 'data-accept-redirect' ));

						// Trigger ajax request to actually delete
						ips.getAjax()( elem.attr('href'), {
							showLoading: true,
							bypassRedirect: bypassRedirect,
							data: {
								form_submitted: 1,
								wasConfirmed: 1
							}
						} )
							.done( function (response) {
								if( row.hasClass('parent') ){
									row.next().remove();
								}

								ips.utils.anim.go( 'fadeOut', row );
							})
							.fail( function () {
								window.location = elem.attr('href') + '&csrfKey=' + ips.getSetting('csrfKey') + '&wasConfirmed=1';
							});
					}
				}
			});
		},
		
		/**
		 * Prompts the user to confirm an action
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		confirmSomething: function (e) {
			e.preventDefault();
			var elem = $( e.currentTarget );
			var customMessage = $( e.currentTarget ).attr('data-confirmMessage');
			var customSubMessage = $( e.currentTarget ).attr('data-confirmSubMessage');
			var type = $( e.currentTarget ).attr('data-confirmType');
			var icon = $( e.currentTarget ).attr('data-confirmIcon');
			
			var alert = {
				type: ( type ) ? type : 'confirm',
				icon: ( icon ) ? icon : 'warn',
				message: ( customMessage ) ? customMessage : ips.getString('generic_confirm'),
				subText: ( customSubMessage ) ? customSubMessage : '',
				callbacks: {
					ok: function () {
						window.location = elem.attr('href') + '&wasConfirmed=1';
					},
					yes: function () {
						window.location = elem.attr('href') + '&prompt=1';
					},
					no: function () {
						window.location = elem.attr('href') + '&prompt=0';
					}
				}
			};
			
			if ( $( e.currentTarget ).attr('data-confirmButtons') ) {
				alert.buttons = $.parseJSON( $( e.currentTarget ).attr('data-confirmButtons') );
			}
			
			ips.ui.alert.show( alert );
		},

		/**
		 * Shows elements when clicked
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		clickShow: function ( e ) {
			e.preventDefault();
			
			var elems = ips.utils.getIDsFromList( $( e.currentTarget ).attr('data-clickshow') );
			this.scope.find( elems ).show().removeClass('ipsHide');
		},

		/**
		 * Hides elements when clicked
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		clickHide: function ( e ) {
			e.preventDefault();
			
			var elems = ips.utils.getIDsFromList( $( e.currentTarget ).attr('data-clickhide') );
			this.scope.find( elems ).hide().addClass('ipsHide');
		},

		/**
		 * Empties given form elements when clicked
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		clickEmpty: function ( e ) {
			e.preventDefault();
			
			var elems = ips.utils.getIDsFromList( $( e.currentTarget ).attr('data-clickempty') );
			this.scope.find( elems ).val('');
		},

		/**
		 * Prevents default event handler from executing 
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		noScriptFallback: function (e) {
			e.preventDefault();
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.changeTheme.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.changeTheme.js - ACP theme changing
 *
 * Author: Brandon Farber
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.changeTheme', {

		initialize: function () {
			$('#elAdminUser,#elThemeLangMenuMob').on( 'menuItemSelected', _.bind( this.themePreferenceSelected, this ) );

			this.setTheme();
		},

		/**
		 * Determine if we should be using dark mode or not and set the class if so
		 *
		 * @returns {void}
		 */
		setTheme: function () {
			// If we don't have a specific preference, use the OS default
			if( _.isUndefined( ips.utils.cookie.get('acptheme') ) || ips.utils.cookie.get('acptheme') == 'undefined' )
			{
				if ( window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches )
				{
					$('body').addClass('ipsDarkMode');
					ips.utils.cookie.set( 'acpthemedefault', 'dark' );
				}
				else
				{
					$('body').removeClass('ipsDarkMode');
					ips.utils.cookie.set( 'acpthemedefault', 'light' );
				}
			}
			// We have a cookie preference set so make sure that's being used
			else
			{
				$('body').toggleClass( 'ipsDarkMode', ips.utils.cookie.get('acptheme') === 'dark' );
			}
		},

		/**
		 * Change our AdminCP theme
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object}	data	Event data
		 * @returns {void}
		 */
		themePreferenceSelected: function (e, data) {
			// Make sure something was selected and this isn't a click on a different menu item
			if( _.isUndefined( data.selectedItemID ) )
			{
				return;
			}

			e.preventDefault();

			if( data.selectedItemID == 'os' )
			{
				// If we selected to use the OS preference delete the cookie (if it exists) and let the normal behavior resume
				ips.utils.cookie.unset('acptheme');
			}
			else
			{
				// Otherwise set our cookie and force the behavior we requested
				var expires = new Date();
				expires.setFullYear( expires.getFullYear() + 1 );
				ips.utils.cookie.set( 'acptheme', data.selectedItemID, expires.toUTCString() );
			}

			// The menu system can't handle our nested radio-selectable list, so manually update what is checked
			$('#elThemeMenu_menu, #elNavThemeMob_menu').find('.ipsMenu_itemChecked').removeClass('ipsMenu_itemChecked');
			$('#elThemeMenu_menu, #elNavThemeMob_menu').find( '[data-ipsMenuValue="' + data.selectedItemID + '"]' ).addClass('ipsMenu_itemChecked');

			this.setTheme();
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.dynamicChart.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.dynamicChart.js - Dynamic chart controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.dynamicChart', {

		storedValues: {},
		identifier: '',
		
		type: '',

		initialize: function () {
			// Set up the events that will capture chart changes
			this.on( 'click', '[data-timescale]', this.changeTimescale );
			this.on( document, 'submit', '[data-role="dateForm"]', this.changeDateRange );
			this.on( document, 'click', '[data-role="clearSearchTerm"]', this.resetSearch );
			this.on( document, 'submit', '[data-role="searchForm"]', this.changeSearch );
			this.scope.find('[data-role="clearSearchTerm"]').hide();
			this.on( 'menuItemSelected', '[data-action="chartFilter"]', this.changeFilter );
			this.on( 'click', '[data-type]', this.changeChartType );

			// Select all/none for filters
			$('[data-role="filterMenu"] [data-role="selectAll"]').on( 'click', _.bind( this.selectAllFilters, this ) );
			$('[data-role="filterMenu"] [data-role="unselectAll"]').on( 'click', _.bind( this.unselectAllFilters, this ) );
			$('button[data-role="applyFilters"]').on( 'click', function() {
				$('[data-action="chartFilter"]').trigger('closeMenu');
			});

			// Save filters
			this.on( 'click', '[data-role="saveReport"]', _.bind( this.saveFilters, this ) );
			this.on( 'click', '[data-role="renameChart"]', _.bind( this.renameFilters, this ) );

			this.setup();
		},

		/**
		 * Select all filters
		 *
		 * @returns {void}
		 */
		selectAllFilters: function ( e ) {
			$( e.currentTarget ).closest('.ipsMenu').find('.ipsMenu_item:not( .ipsMenu_itemChecked ) a:not( .ipsMenu_itemInline )').trigger('click');

			$('button[data-role="applyFilters"]').prop( 'disabled', false );

			this.showSaveButton();
		},

		/**
		 * Un-select all filters
		 *
		 * @returns {void}
		 */
		unselectAllFilters: function ( e ) {
			$( e.currentTarget ).closest('.ipsMenu').find('.ipsMenu_item.ipsMenu_itemChecked a').trigger('click');

			$('button[data-role="applyFilters"]').prop( 'disabled', false );

			this.showSaveButton();
		},

		/**
		 * Setup method. Sets the default storeValues values.
		 *
		 * @returns {void}
		 */
		setup: function () {
			var self = this;
			this.identifier = this.scope.attr('data-chart-identifier');

			// Get the initial values
			// Timescale & type
			this.storedValues.timescale = this.scope.find('[data-timescale][data-selected]').attr('data-timescale');
			this.storedValues.type = this.scope.find('[data-type][data-selected]').attr('data-type');

			if( this.scope.find('[data-role="searchForm"] input') )
			{
				this.storedValues.term = null;
			}

			this.storedValues.filters = [];
			this.storedValues.dates = { start: '', end: '' };
			this.type = this.scope.attr('data-chart-type');
			this.timescale = this.scope.attr('data-chart-timescale');
			
			// Filters
			$('#el' + this.identifier + 'Filter_menu').find('.ipsMenu_itemChecked').each( function () {
				self.storedValues.filters.push( $( this ).attr('data-ipsMenuValue') );
			});

			if ( $(this.scope).attr('data-chart-customfilter-submitted') == 'true' ) {
				this.showSaveButton();
			}
			
			this.checkType();
		},

		/**
		 * Event handler for changing the timescale
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		changeTimescale: function (e) {
			this.timescale = $(e.currentTarget).attr('data-timescale');
			this._toggleButtonRow( e, 'timescale');
		},

		/**
		 * Event handler for changing the chart type
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		changeChartType: function (e) {
			this.type = $(e.currentTarget).attr('data-type');
			this._toggleButtonRow( e, 'type' );
			this.checkType();
		},
		
		/**
		 * Check type
		 */
		checkType: function() {
			if ( this.type == 'Table' || this.type == 'PieChart' || this.type == 'GeoChart' ) {
				$(this.scope).find('[data-role="groupingButtons"] a.ipsButton').addClass('ipsButton_disabled ipsButton_veryLight').removeClass('ipsButton_primary');
			} else {
				$(this.scope).find('[data-role="groupingButtons"] a.ipsButton').removeClass('ipsButton_disabled');
				$(this.scope).find('a.ipsButton[data-timescale="' + this.timescale + '"]').removeClass('ipsButton_veryLight').addClass('ipsButton_primary');
			}
		},

		/**
		 * Track whether we've bound the updateUrl method
		 */
		updateUrlBound: false,

		/**
		 * Event handler for changing the filter
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Data object from the menu widget
		 * @returns {void}
		 */
		changeFilter: function (e, data) {
			data.originalEvent.preventDefault();

			// Reset filters
			this._resetFilters();

			if( this.updateUrlBound === false )
			{
				$('[data-action="chartFilter"]').one( 'menuClosed', _.bind( this.fetchNewResults, this ) );
				this.updateUrlBound = true;
			}

			$('button[data-role="applyFilters"]').prop( 'disabled', false );

			this.showSaveButton();
		},

		/**
		 * Show the button to save filters
		 *
		 * @returns	{void}
		 */
		 showSaveButton: function()
		 {
		 	var button = this.scope.find('[data-role="saveReport"]');

		 	if( !button.is(':visible') )
		 	{
		 		ips.utils.anim.go( 'fadeIn fast', button );
		 	}
		 },

		/**
		 * Wrapper method for _updateUrl() so we only send request if needed
		 *
		 * @returns {void}
		 */
		fetchNewResults: function( e ) {
			this._updateURL();

			this.updateUrlBound = false;
		},

		/**
		 * Save the current filters
		 *
		 * @returns	{void}
		 */
		 saveFilters: function( event )
		 {
		 	// Init
		 	var identifier	= this.scope.closest('.ipsTabs_panel').closest('section').attr('id');
		 	var self		= this;
		 	var pieces		= [];

			_.each( this.storedValues.filters, function (value, idx ) {
				pieces.push( "chartFilters[" + idx + "]=" + value );
			});
			
			// Work in custom form filters
			$('#el' + this.identifier + 'CustomFiltersForm input[data-role="nodeValue"]').each( function () {
				pieces.push( 'customform_' + $(this).attr('name') + "=" + $(this).val() );
			});
			
		 	var newFilters	= pieces.join('&');

		 	// Are we on default panel or a custom one?
		 	if( !$( event.currentTarget ).attr('data-chartId') )
		 	{
		 		// When clicking on the save button, stop the chart from updating initially
		 		this._skipUpdate = true;

		 		// Then set an event handler for the form submission where we specify the chart title
		 		$('#el' + this.identifier + 'FilterSave_menu').one( 'submit', 'form', function( e ){
					if( $(this).attr('data-bypassValidation') ){
						return false;
					}

					// Hide the menu
					$(this).trigger('closeMenu');

					var tabTitle = $(this).find('[name="custom_chart_title"]').val();

		 			e.preventDefault();

					ips.getAjax()( $(this).attr('action'), {
						data: $(this).serialize() + '&' + newFilters,
						type: 'post'
					} )
						.done( function (response, status, jqXHR) {
							// Add the tab
							$('[data-ipsTabBar-contentArea="#' + identifier + '"]').find('ul[role="tablist"]').append(
								"<li><a href='" + response.tabHref + "' id='" + response.tabId + "_tab_" + response.chartId + "' class='ipsTabs_item' title='" + tabTitle + "' role='tab' aria-selected='true'>" + tabTitle + "</a></li>"
							);

							// Add the new tab content area
							$('#' + identifier ).append(
								"<div id='ipsTabs_elTabs_" + response.tabId + "_" + response.tabId + "_tab_" + response.chartId + "_panel' class='ipsTabs_panel' aria-labelledby='" + response.tabId + "_tab_" + response.chartId + "' aria-hidden='true'></div>"
							);

						 	// Hide the button
						 	ips.utils.anim.go( 'fadeOut fast', self.scope.find('[data-role="saveReport"]') );

						 	// Make sure we update URL correctly moving forward
						 	self._skipUpdate = false;

						 	// And trigger content change event
						 	$( document ).trigger( 'contentChange', [ self.scope ] );

						 	// Clear input field
						 	$('#el' + self.identifier + 'FilterSave_menu').find('[name="custom_chart_title"]').val( '' );

						 	// And then switch to the tab
						 	$('#' + response.tabId + "_tab_" + response.chartId ).click();
						})
						.fail( function () {
							$(this).attr( 'data-bypassValidation', true ).submit();
						});
		 		});
		 	}
		 	else
		 	{
			 	// Send AJAX request to save report
				ips.getAjax()( this.scope.attr('data-chart-url') + '&saveFilters=1&chartId=' + $( event.currentTarget ).attr('data-chartId'), {
					data: newFilters,
					type: 'post'
				});

			 	// Hide the button
			 	ips.utils.anim.go( 'fadeOut fast', this.scope.find('[data-role="saveReport"]') );

			 	// And update the chart
			 	this._updateURL();
		 	}
		 },

		/**
		 * Rename the current filters
		 *
		 * @returns	{void}
		 */
		 renameFilters: function( event )
		 {
		 	// Init
		 	var identifier	= this.scope.closest('.ipsTabs_panel').closest('section').attr('id');

	 		// Set an event handler for the form submission where we specify the chart title
	 		$('#el' + this.identifier + 'FilterRename_menu').on( 'submit', 'form', function( e ){
				if( $(this).attr('data-bypassValidation') ){
					return false;
				}

				// Hide the menu
				$(this).trigger('closeMenu');

	 			e.preventDefault();

				ips.getAjax()( $(this).attr('action'), {
					data: $(this).serialize(),
					type: 'post'
				} )
					.done( function (response, status, jqXHR) {
						// Update the tab
						$('[data-ipsTabBar-contentArea="#' + identifier + '"]').find('ul[role="tablist"]').find('.ipsTabs_activeItem').text( response.title );
					})
					.fail( function () {
						$(this).attr( 'data-bypassValidation', true ).submit();
					});
	 		});
		 },

		/**
		 * Searches for a specific value in the graph
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		changeSearch: function (e) {
			e.preventDefault();

			var form = $('#el' + this.identifier + 'Search_menu');

			this.storedValues.term = form.find('[name="search"]').val();

			this.scope.find('[data-role="searchSummary"]').text( this.storedValues.term );

			if( this.storedValues.term )
			{
				Debug.log( this.scope.find('[data-role="clearSearchTerm"]') );
				form.find('[data-role="clearSearchTerm"]').show();
			}
			else
			{
				form.find('[data-role="clearSearchTerm"]').hide();
			}

			form.trigger('closeMenu');

			this._updateURL();
		},

		/**
		 * Resets our search term
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		resetSearch: function (e) {
			$('#el' + this.identifier + 'Search_menu').find('[name="search"]').val( '' );

			// The event bubbles up since this is a "submit" button, and changeSearch will be called next
		},

		/**
		 * Changes the range of the graph being shown
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		changeDateRange: function (e) {
			e.preventDefault();

			var form = $('#el' + this.identifier + 'Date_menu');

			this.storedValues.dates.start = form.find('[name="start"]').val();
			this.storedValues.dates.end = form.find('[name="end"]').val();

			form.trigger('closeMenu');

			if( this.storedValues.dates.start && this.storedValues.dates.end )
			{
				this.scope.find('[data-role="dateSummary"]').text( '(' + ips.getString('betweenXandX', { start: ips.utils.time.localeDateString( ips.utils.time.removeTimezone( new Date( this.storedValues.dates.start ) ) ), end: ips.utils.time.localeDateString( ips.utils.time.removeTimezone( new Date( this.storedValues.dates.end ) ) ) }) + ')' );
			}
			else if( this.storedValues.dates.start )
			{
				this.scope.find('[data-role="dateSummary"]').text( '(' + ips.getString('afterX', { start: ips.utils.time.localeDateString( ips.utils.time.removeTimezone( new Date( this.storedValues.dates.start ) ) ) }) + ')' );
			}
			else if( this.storedValues.dates.end )
			{
				this.scope.find('[data-role="dateSummary"]').text( '(' + ips.getString('beforeX', { end: ips.utils.time.localeDateString( ips.utils.time.removeTimezone( new Date( this.storedValues.dates.end ) ) ) }) + ')' );
			}
			else
			{
				this.scope.find('[data-role="dateSummary"]').text( '' );
			}

			this._updateURL();
		},

		/**
		 * Method for toggling buttons and setting new values in the store
		 *
		 * @param 	{event} 	e 		Event object from the event handler
		 * @param 	{string} 	type 	The type being handled (timescale or type)
		 * @returns {void}
		 */
		_toggleButtonRow: function (e, type) {
			e.preventDefault();

			var val = $( e.currentTarget ).attr( 'data-' + type );

			this.scope.find('[data-' + type + ']')
				.removeClass('ipsButton_primary')
				.addClass('ipsButton_veryLight')
				.removeAttr('data-selected')
				.filter('[data-' + type + '="' + val + '"]')
					.addClass('ipsButton_primary')
					.removeClass('ipsButton_veryLight')
					.attr('data-selected', true);

			this.storedValues[ type ] = val;

			// Reset filters
			this._resetFilters();

			this._updateURL();
		},

		/**
		 * Reset our stored filters to only include what we presently have selected
		 *
		 * @return {void}
		 */
		_resetFilters: function() {
			// Reset filters
			var self = this;
			this.storedValues.filters = [];

			$('#el' + this.identifier + 'Filter_menu').find('.ipsMenu_itemChecked').each( function () {
				self.storedValues.filters.push( $( this ).attr('data-ipsMenuValue') );
			});
		},

		_skipUpdate: false,

		/**
		 * Fetches new chart HTML from the server, then reinits the chart widget
		 *
		 * @returns {void}
		 */
		_updateURL: function () {
			// We skip updating the chart when clicking over to the save button the first time
			if( this._skipUpdate === true )
			{
				this._skipUpdate = false;
				return;
			}

			var url = this._buildURL();
			var chartArea = this.scope.find('[data-role="chart"]');
			chartArea.css( 'height', chartArea.height() ).html( '' ).addClass('ipsLoading');

			ips.getAjax()( this.scope.attr('data-chart-url'), {
				data: url,
				type: 'post'
			})
				.done( function (response) {
					chartArea.css( 'height', 'auto' ).html( response );
					$( document ).trigger( 'contentChange', [ chartArea ] );
				})
				.always( function () {
					chartArea.removeClass('ipsLoading');
				});

			$('button[data-role="applyFilters"]').prop( 'disabled', true );

			// Update download button URL
			this.scope.find('[data-role="downloadChart"]').attr('href', this.scope.attr('data-chart-url') + '&' + url + '&download=1');
		},

		/**
		 * Builds a query param based on the values we have stored
		 *
		 * @returns {string}
		 */
		_buildURL: function () {
			var pieces = [];
			var self = this;

			// Needed for dynamic chart buttons. We can't simply rely on checking request isAjax() as it could be loaded inside tabs etc. 
			pieces.push( "noheader=1" );
			
			// Timescale
			pieces.push( "timescale[" + this.identifier + "]=" + this.storedValues.timescale );

			// Type
			if( !_.isUndefined( this.storedValues.type ) )
			{
				pieces.push( "type[" + this.identifier + "]=" + this.storedValues.type );
			}

			// Term
			if( !_.isUndefined( this.storedValues.term ) && !_.isNull( this.storedValues.term ) )
			{
				pieces.push( "search[" + this.identifier + "]=" + this.storedValues.term );
			}

			// Filters
			_.each( this.storedValues.filters, function (value, idx ) {
				pieces.push( "filters[" + self.identifier + "][" + idx + "]=" + value );
			});

			// Dates
			if( this.storedValues.dates.start || this.storedValues.dates.end ){
				pieces.push( "start[" + this.identifier + "]=" + this.storedValues.dates.start );
				pieces.push( "end[" + this.identifier + "]=" + this.storedValues.dates.end );
			}
			
			// Work in custom form filters
			$('#el' + this.identifier + 'CustomFiltersForm input[data-role="nodeValue"]').each( function () {
				pieces.push( 'customform_' + $(this).attr('name') + "=" + $(this).val() );
			});

			return pieces.join('&');
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.editable.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.editable.js - Inline editing support
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.editable', {

		_editTimeout: null,
		_editing: false,

		initialize: function () {
			this.on( 'mousedown', this.editMousedown );
			this.on( 'mouseup mouseleave', this.editMouseup );
			this.on( 'click', this.editMouseclick );
			this.on( 'click', '[data-role="edit"]', this.clickEdit );
			this.setup();
		},
		
		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			var defaultFill = this.scope.attr('data-default');
			// If this is a touch device, remove the highlight class and show the button
			if( ips.utils.events.isTouchDevice() || ( ! _.isUndefined( defaultFill ) && defaultFill == 'empty' ) ) {
				this.scope.removeClass('ipsType_editable').find('[data-role="edit"]').show();
			}
		},

		/**
		 * Handles a click on the Edit button
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		clickEdit: function (e) {
			e.preventDefault();
			this._triggerEdit();
		},

		/**
		 * Event handler called when the user clicks down an editable text area
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		editMousedown: function (e) {
			var self = this;

			if( e.which !== 1 ){ // Only care if it's the left mouse button
				return;
			}

			this._editTimeout = setTimeout( _.bind( this._triggerEdit, this ), 1000);
		},
		
		/**
		 * Event handler called when the user clicks up an editable title
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		editMouseup: function (e) {
			clearTimeout( this._editTimeout );
		},
		
		/**
		 * Event handler called when the user clicks up an editable title
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		editMouseclick: function (e) {
			if ( this._editing ) {
				e.preventDefault();
			}
		},

		/**
		 * Transforms our scope element into an editable text field
		 *
		 * @returns {void}
		 */
		_triggerEdit: function () {
			var self = this;

			this._editing = true;
			clearTimeout( this._editTimeout );
			
			var span = this.scope;
			var url = span.attr('data-url');
			var textField = span.find('[data-role="text"]');
			var fieldName = span.find('[data-name]').attr('data-name');
			var defaultFill = span.attr('data-default');
			span.hide();
			
			var defaultText = ( _.isUndefined( defaultFill ) || defaultFill != 'empty' ) ? textField.text().trim() : '';
			var inputNode = $('<input/>').attr( { type: 'text' } ).attr( 'data-role', 'editField' ).val( defaultText );

			span.after(inputNode);
			inputNode.focus();
			
			inputNode.on('blur', function(){
				inputNode.addClass('ipsField_loading');
				if( inputNode.val() == '' ){
					inputNode.remove();
					span.show();
					self._editing = false;
				} else {
					var dataToSend = {};
					dataToSend[fieldName] = inputNode.val();

					ips.getAjax()( url, { method: 'post', data: dataToSend } )
						.done( function(response) {
							textField.text( inputNode.val() );
						})
						.fail( function(response) {
							ips.ui.alert.show( {
								type: 'alert',
								icon: 'warn',
								message: response.responseJSON,
							});
						})
						.always(function(){
							inputNode.remove();
							span.show();
							self._editing = false;
						});
				}

			});
			
			inputNode.on('keypress', function(e){
				if( e.keyCode == ips.ui.key.ENTER ){
					e.stopPropagation();
					e.preventDefault();
					inputNode.blur();
					return false;
				}
			});

			// Chrome requires checking keydown instead for escape
			inputNode.on('keydown', function(e){
				if( e.keyCode == ips.ui.key.ESCAPE ){
					inputNode.remove();
					span.show();
					self._editing = false;
					return false;
				}
			});
		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.genericDialog.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.genericDialog.js - A controller that can be used so that forms inside dialogs submit via ajax
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.genericDialog', {

		initialize: function () {
			this.on( 'submit', 'form', this.submitAjaxForm );
			$( document ).on( 'multipleRedirectFinished', _.bind( this.dismissDialog, this ) );
		},

		/**
		 * Event handler for form submit
		 * If the form is inside a dialog widget, this method will attempt to validate the form remotely
		 * If it fails, the dialog is updated with new HTML from the server. On success, the form is submitted
		 * as normal.
		 *
		 * @returns {void}
		 */
		submitAjaxForm: function (e) {
						
			if( $( e.currentTarget ).attr('data-bypassValidation') ){
				return;
			}
			
			e.preventDefault();

			var dialog = this.scope.closest('.ipsDialog');

			if( !dialog.length ){
				return;
			}

			// Get the dialog object so we can work with it
			var elemId = dialog.attr('id').replace(/_dialog$/, '');
			var dialogObj = ips.ui.dialog.getObj( $('#' + elemId) );

			if( !dialogObj ){
				return;
			}

			// Set the loading status
			dialogObj.setLoading(true);

			ips.getAjax()( $( e.currentTarget ).attr('action') + '&ajaxValidate=1', {
				data: $( e.currentTarget ).serialize(),
				type: 'post'
			})
				.done( function (response, textStatus, jqXHR) {					
					if( jqXHR.getAllResponseHeaders().indexOf('X-IPS-FormError: true') !== -1 || jqXHR.getAllResponseHeaders().indexOf('x-ips-formerror: true') !== -1 ){
						dialogObj.updateContent( jqXHR.responseText );
						$( document ).trigger( 'contentChange', [ $('#' + elemId) ] );
						dialogObj.setLoading(false);
					} else if( jqXHR.getAllResponseHeaders().indexOf('X-IPS-FormNoSubmit: true') !== -1 || jqXHR.getAllResponseHeaders().indexOf('x-ips-formnosubmit: true') !== -1 ){
						dialogObj.updateContent( jqXHR.responseText );
						$( document ).trigger( 'contentChange', [ $('#' + elemId) ] );
						dialogObj.setLoading(false);
					} else {
						$( e.currentTarget ).attr('data-bypassValidation', true).submit();
					}
				})
				.fail( function (jqXHR, textStatus, errorThrown) {
					if( Debug.isEnabled() ){
						Debug.error( "Ajax request failed (" + status + "): " + errorThrown );
						Debug.error( jqXHR.responseText );
					} else {
						// rut-roh, we'll just do a manual submit
						$( e.currentTarget ).attr('data-bypassValidation', true).submit();
					}
				});
		},
		
		/**
		 * Event to dismiss the dialog
		 *
		 */
		dismissDialog: function (e) {
			var elemId = $('.ipsDialog').attr('id').replace(/_dialog$/, '');
			
			var dialogObj = ips.ui.dialog.getObj( $('#' + elemId) );
			dialogObj.hide();
			
			$( '#' + elemId ).data('_dialog', '')

		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.langString.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.langString.js - Faciliates editing language strings in the ACP translator
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.langString', {

		_url: null,
		_hideTimeout: null,
		_currentValue: '',

		initialize: function () {
			this.on( 'change', 'textarea', this.changeTextarea );
			this.on( 'focus', 'textarea', this.focusTextarea );
			this.on( 'blur', 'textarea', this.blurTextarea );
			this.on( 'click', '[data-action="saveWords"]', this.saveWords );
			this.on( 'click', '[data-action="revertWords"]', this.revertWords );	
			this.setup();
		},
		
		/**
		 * Setup method
		 * Replaces the scope element with a textbox containing the scope's HTML
		 *
		 * @returns {void}
		 */
		setup: function () {
			this._url = this.scope.attr('data-saveURL');

			var contents = this.scope.find('a').html();

			var html = ips.templates.render( 'languages.translateString', {
				value: _.unescape( contents )
			});

			this._currentValue = _.unescape( contents );
			
			this.scope.html( html );

			// Set the height to match the cell size
			this.scope.find('textarea').css({
				height: this.scope.closest('td').innerHeight() + 'px'
			});
		},

		/**
		 * Event handler for changing the textarea value
		 *
		 * @returns {void}
		 */
		changeTextarea: function () {
			//
		},

		/**
		 * Event handler for focusing the textarea
		 *
		 * @returns {void}
		 */
		focusTextarea: function () {
			this.scope
				.addClass('cTranslateTable_field_focus')
				.find('textarea')
					.removeClass('ipsField_success')
				.end()
				.find('[data-action]')
					.show();
		},

		/**
		 * Event handler for blurring the textarea
		 * Sets a timeout which hides the buttons in 300ms
		 *
		 * @returns {void}
		 */
		blurTextarea: function () {
			this._saveWords(true);
		},

		/**
		 * Hides the buttons
		 *
		 * @returns {void}
		 */
		_hideButtons: function (e) {
			this.scope.removeClass('cTranslateTable_field_focus');
		},

		/**
		 * Event handler for clicking the save button
		 *
		 * @returns {void}
		 */
		saveWords: function (e) {
			e.preventDefault();
			this._saveWords(false);			
		},

		_saveWords: function (hideButtonsImmediately) {
			var self = this;
			var url = this._url + '&form_submitted=1&csrfKey=' + ips.getSetting('csrfKey');
			var textarea = this.scope.find('textarea');
			var value = textarea.val();

			// Don't save if the value hasn't changed
			if( this._currentValue == value ){
				this._hideButtons();
				return;
			}

			// Remove timeout for hiding buttons
			if( this._hideTimeout ){
				clearTimeout( this._hideTimeout );
			}

			this.scope.find('[data-action]').addClass('ipsButton_disabled');

			// Send the translated string, and show flash message on success
			// On failure we'll reload the page
			ips.getAjax()( url, { type: 'post', data: { lang_word_custom: encodeURIComponent( value ) } } )
				.done( function() {
					textarea
						.removeClass('ipsField_loading')
						.addClass('ipsField_success');

					ips.ui.flashMsg.show( ips.getString('saved') );

					if( !hideButtonsImmediately ){
						self._hideTimeout = setTimeout( _.bind( self._hideButtons, self ), 300 );	
					} else {
						self._hideButtons();
					}

					self._currentValue = value;	
				})
				.fail( function () {
					window.location = url;
				});
		},

		revertWords: function (e) {

		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.liveSearch.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.liveSearch.js - ACP livesearch controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.liveSearch', {
		_textField: null,
		_searchMenu: null,
		_resultsContainer: null,
		_lastValue: '',
		_results: {},
		_timers: {},
		_modal: null,
		_activePanel: null,
		_ajax: {},
		_defaultTab: null,

		initialize: function () {
			this.setup();

			this.on( document, 'focus', '#acpSearchKeyword', this.fieldFocus );
			this.on( document, 'blur', '#acpLiveSearch', this.fieldBlur );
			this.on( document, 'click', '.ipsModal', this.clickModal );
			this.on( 'itemClicked.sideMenu', this.changeSection );
		},

		setup: function () {
			this._searchMenu = this.scope.find('[data-role="searchMenu"]');
			this._resultsContainer = this.scope.find('[data-role="searchResults"]');
			this._defaultTab = this.scope.find('[data-role="defaultTab"]').attr('data-ipsMenuValue');
			this._textField = $('#acpSearchKeyword');
			this._textField
				.prop( 'autocomplete', 'off' )
				.prop( 'spellcheck', false )
				.attr( 'aria-autocomplete', 'list' )
				.attr( 'aria-haspopup', 'true' );

			// Is there an active panel?
			this._activePanel = this._searchMenu.find('.ipsSideMenu_itemActive').attr('data-ipsMenuValue');
			//this._activePanel = 'core_Members';
		},

		/**
		 * Event handler for the itemClicked event fired by the sideMenu widget
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		changeSection: function (e, data) {
			this._activePanel = data.selectedItemID;
			
			this.scope.find( '[data-ipsMenuValue] [data-role="resultCount"]' ).removeClass('ipsLoading_dark ipsSideMenu_clearCount');
			this.scope.find( '[data-ipsMenuValue="' + data.selectedItemID + '"] [data-role="resultCount"]' ).addClass('ipsLoading_dark');
			
			this._showResultsInPanel( this._activePanel, this._results[ this._activePanel ] );
		},

		/**
		 * Event handler for focusing in the search box
		 * Set a timer going that will watch for value changes. If there's already a value,
		 * we'll show the results immediately
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		fieldFocus: function (e) {
			// Set the timer going
			this._timers.focus = setInterval( _.bind( this._timerFocus, this ), 700 );

			// Show immediately?
			if( this._textField.val() && this._textField.val().length >= 3 ){
				this._showResults();
			}
		},

		/**
		 * Event handler for field blur
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		fieldBlur: function (e) {
			clearInterval( this._timers );
		},

		/**
		 * Event handler for clicking the modal
		 * We have to check this is our modal by comparing IDs. If it is, we close the results.
		 *
		 * @param 	{event} 	e 	Event object 
		 * @returns {void}
		 */
		clickModal: function (e) {
			if( this._modal == null || $( e.currentTarget ).attr('id') != this._modal.attr('id') ){
				return;
			}

			this._hideResults();
		},

		/**
		 * Timer callback from this.fieldFocus
		 * Compares current value to previous value, and shows/loads new results if it's changed
		 *
		 * @returns {void}
		 */
		_timerFocus: function () {
			var currentValue = this._textField.val().trim();

			if( currentValue == this._lastValue || this._textField.val().trim().length < 3 ){
				return;
			}

			this._lastValue = currentValue;

			this._showResults();
			this._loadResults();
		},

		/**
		 * Hides the results and modal
		 *
		 * @returns {void}
		 */
		_hideResults: function () {
			ips.utils.anim.go( 'fadeOut fast', this._modal );
			ips.utils.anim.go( 'fadeOut', this.scope );
		},

		/**
		 * Shows the results panel and modal, setting the zIndex on them so they stay in order
		 *
		 * @returns {void}
		 */
		_showResults: function () {
			if( !this._modal ){
				this._buildModal();
			}

			// Set new z-indexes to keep everything in order
			this._modal.css( { zIndex: ips.ui.zIndex() } );
			$('#ipsLayout_header').css( { zIndex: ips.ui.zIndex() } );
			this.scope.css( { zIndex: ips.ui.zIndex() } );

			// Show the results and or modal
			if( !this.scope.is(':visible') ){
				ips.utils.anim.go( 'fadeIn fast', this.scope );
			}

			if( !this._modal.is(':visible') ){
				ips.utils.anim.go( 'fadeIn fast', this._modal );
			}
		},

		/**
		 * Load results from the server
		 *
		 * @returns {void}
		 */
		_loadResults: function () {
			var self = this;

			// Abort any requests running now
			if( _.size( this._ajax ) ){
				_.each( this._ajax, function (ajax) {
					try {
						if( _.isFunction( ajax.abort ) ) {
							ajax.abort();
							Debug.log('aborted ajax');
						}
					} catch (err) { }
				});
			}

			this.scope.find( '[data-ipsMenuValue] [data-role="resultCount"]' ).addClass('ipsLoading').html('&nbsp;');
			this.scope.find( '[data-ipsMenuValue] [data-role="resultCount"]' ).removeClass('ipsLoading_dark ipsSideMenu_clearCount');
			this.scope.find( '[data-ipsMenuValue].ipsSideMenu_itemActive [data-role="resultCount"]' ).addClass('ipsLoading_dark ipsSideMenu_clearCount');

			var value = this._lastValue.trim();

			this.scope.find('[data-ipsMenuValue]').each( function () {
				var tab = this;
				var key = $( this ).attr('data-ipsMenuValue');

				self._setPanelToLoading( key );

				self._ajax[ key ] = ips.getAjax()('?app=core&module=system&controller=livesearch', {
					dataType: 'json',
					data: {
						search_key: key,
						search_term: encodeURIComponent( value )
					}
				}).done( function (response) {
					
					self._results[key] = response;
					
					$( tab )
						.find('[data-role="resultCount"]')
							.removeClass('ipsLoading ipsLoading_dark ipsSideMenu_clearCount')
							.text( parseInt( response.length ) )
						.end()
						.toggleClass( 'ipsSideMenu_itemDisabled', ( response.length === 0 ) ? true : false );

					if( $( tab ).attr('data-ipsMenuValue') == self._activePanel ){
						self._showResultsInPanel( self._activePanel, response, true );
					}
					
					if( !self._searchMenu.find('[data-ipsMenuValue].ipsSideMenu_itemActive:not( .ipsSideMenu_itemDisabled )').length ) {
						self._selectFirstResultsTab();
					}
					if ( response.length > 0 && key == self._defaultTab && self._activePanel == self._defaultTab ) {
						tab.click();
					}
				}).fail( function (err) {
					// fail gets called when it's aborted, so deliberately do nothing here
				});
			});
		},

		/**
		 * Selects the first section that has some results to show
		 *
		 * @returns {void}
		 */
		_selectFirstResultsTab: function () {
			var first = this._searchMenu.find('[data-ipsMenuValue]:not( .ipsSideMenu_itemDisabled )').first();
			first.click();
		},

		/**
		 * Sets the given panel into loading state
		 *
		 * @returns {void}
		 */
		_setPanelToLoading: function (panel) {
			var panelContainer = this._resultsContainer.find('[data-resultsSection="' + panel + '"]');
			panelContainer.addClass('ipsLoading').find('> ol').hide();
		},

		/**
		 * Shows the results in the relevant panel, building it if it doesn't exist
		 *
		 * @param 	{string}	panel 		Panel ID
		 * @param 	{object}	results 	Results object
		 * @param 	{booolean}	animate		Animate the results being shown?
		 * @returns {void}
		 */
		_showResultsInPanel: function (panel, results, animate) {
						
			// Hide all panels
			this._resultsContainer.find('[data-resultsSection]').hide();

			var panelContainer = this._resultsContainer.find('[data-resultsSection="' + panel + '"]');
			var panelList = panelContainer.find('> ol');

			// Build the container if needed
			if( !panelContainer.length ){
				this._buildResultsContainer( panel );
				panelContainer = this._resultsContainer.find('[data-resultsSection="' + panel + '"]');
				panelList = panelContainer.find('> ol');
			}

			panelContainer.removeClass('ipsLoading');
			panelList.hide().html('');

			// Any results to show?
			if( _.isUndefined( results ) || _.isUndefined( results ) || _.size( results ) == 0 ){
				// No results
				panelList.html( ips.templates.render('core.livesearch.noResults') ).show();
				return;
			}

			// Loop through each result to build it
			_.each( results, function (val) {
				panelList.append( val );
			});

			// Find all results
			var resultItems = panelList.find('[data-role="result"]').hide();
			var delay = 25;

			panelList.show();
			panelContainer.show();

			if( animate == true ){
				resultItems.each( function () {
					var item = $( this );

					setTimeout( function () {
						ips.utils.anim.go( 'fadeIn fast', item );
					}, delay);

					delay += 25;				
				});
			} else {
				resultItems.show();
			}
		},

		/**
		 * Builds a results container
		 *
		 * @param 	{string} 	panel 	Panel ID
		 * @returns {void}
		 */
		_buildResultsContainer: function (panel) {
			this._resultsContainer.append( 
				$('<div/>')
					.attr('data-resultsSection', panel )
					.addClass('ipsScrollbar') 
					.append( $('<ol/>')
								.addClass('ipsList_reset') 
					)
			);
		},

		/**
		 * Builds the modal element
		 *
		 * @returns {void}
		 */
		_buildModal: function () {
			this._modal = ips.ui.getModal();
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.mobileNav.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.mobileNav.js - ACP mobile navigation
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.core.mobileNav', {

		initialize: function () {
			this.on( 'click', '[data-action=&quot;mobileSearch&quot;]', this.mobileSearch );
		},

		/**
		 * Mobile search; simply adds a class to the body. CSS shows the search box.
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		mobileSearch: function (e) {
			e.preventDefault();

			if( $('body').hasClass('acpSearchOpen') ){
				$('body').find('.ipsModal').trigger('click');
			}

			$('body').toggleClass('acpSearchOpen');
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.nav.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.nav.js - AdminCP Nav
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.nav', {

		_reordering: false,
		_orderChanged: false,
		_currentMenu: null,
		_menuTimer: null,

		initialize: function () {
			this.setup();

			this.on( document, 'click', '#acpMainArea', this.clickMainArea );
			this.on( 'click', '[data-action="reorder"]', this.startReorder );
			this.on( 'click', '[data-action="saveOrder"]', this.saveOrder );
			this.on( 'click', '#elHideMenu', this.toggleMenu );
		},

		setup: function () {
			var self = this;
			var activating;

			this._currentMenu = this.scope.find('.acpAppList_active');

			if( ips.utils.responsive.enabled() && ips.utils.events.isTouchDevice() && ips.utils.responsive.currentIs('tablet') ){
				this.on('click', '> li > a', this.handleTouchMenu);
			} else {
				$('#acpAppMenu').menuAim({
					rowSelector: "> #acpAppList > li:not( #elLogo )",
					enter: function (row) {
						if( $( row ).attr('id') == 'elReorderAppMenu' || $( row ).attr('id') == 'elHideMenu' ){
							activating = self.scope.find('.acpAppList_active');
						}

						if( self._menuTimer ){
							clearTimeout( self._menuTimer );
						}
					},
					activate: function (row) {
						if( $( row ).attr('id') == 'elReorderAppMenu' || $( row ).attr('id') == 'elHideMenu' ){
							activating.addClass('acpAppList_active');
						} else {
							$( row ).addClass('acpAppList_active');
						}
					},
					deactivate: function (row) {
						$( row ).removeClass('acpAppList_active');
					},
					exitMenu: function () {
						self._menuTimer = setTimeout( function () {
							self.scope.find('.acpAppList_active').removeClass('acpAppList_active');
							self._currentMenu.addClass('acpAppList_active');
						}, 2000 );

						return false;
					}
				});
			}
		},

		toggleMenu: function (e) {
			e.preventDefault();
			
			if( $('body').hasClass('cAdminHideMenu') ){
				$('body').removeClass('cAdminHideMenu');
				ips.utils.cookie.unset('hideAdminMenu');
			} else {
				$('body').addClass('cAdminHideMenu');
				ips.utils.cookie.set('hideAdminMenu', true, true);
			}

			this.trigger( 'menuToggle.acpNav' );
		},

		/**
		 * Event handler for touch devices; shows the sub menu on first tap
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		handleTouchMenu: function (e) {
			// If we've already activated it, just go to the link
			if( $( e.currentTarget ).hasClass('acpAppList_active') && $( e.currentTarget ).find('> ul:visible').length ){
				return;
			}

			e.preventDefault();

			// Otherwise, deactivate other menus, show this one
			this.scope.find('.acpAppList_active').removeClass('acpAppList_active').find('> ul').hide();
			$( e.currentTarget )
				.closest('li')
					.addClass('acpAppList_active')
					.find('> ul').show();
		},

		/**
		 * Hides the submenu when the main body is clicked on
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		clickMainArea: function (e) {
			if( ips.utils.responsive.enabled() && ips.utils.events.isTouchDevice() && ips.utils.responsive.currentIs('tablet') ){
				this.scope.find('.acpAppList_active > ul').hide();
			} else {
				if( !this._reordering ){
					$('#acpAppList')
					.removeClass('acpAppList_childHovering')
					.find('> li')
						.trigger('mouseleave');
				}
			}
		},

		/**
		 * Starts the tab reordering interface by building drag handles for each tab
		 * and setting up sortable
		 *
		 * @returns 	{void}
		 */
		startReorder: function () {
			var self = this;

			this.scope.find('> li:not( #elReorderAppMenu ):not( #elHideMenu ) > a').each( function () {
				$( this ).append( ips.templates.render('core.appMenu.reorder') );
			});

			this.scope.find('> li > ul > li h3').each( function () {
				$( this ).prepend( ips.templates.render('core.appMenu.reorder') );
			});

			ips.utils.anim.go( 'zoomIn', this.scope.find('[data-role="reorder"]') );

			this.scope
				.addClass('acpAppList_reordering')
				.sortable({
					start: function (e, ui) {
						ui.item.addClass('acpAppList_dragging');
					},
					stop: function (e, ui) {
						ui.item.removeClass('acpAppList_dragging');
					},
					update: function () {
						self._orderChanged = true;
					}
				})
				.find('#elReorderAppMenu')
					.find('[data-action="reorder"]')
						.addClass('ipsHide')
					.end()
					.find('[data-action="saveOrder"]')
						.removeClass('ipsHide');

			this.scope
				.find('> li > ul')
				.sortable({
					start: function (e, ui) {
						ui.item.addClass('acpAppList_dragging');
					},
					stop: function (e, ui) {
						ui.item.removeClass('acpAppList_dragging');
					},
					update: function () {
						self._orderChanged = true;
					}
				});

			this._reordering = true;
			this._orderChanged = false;
		},

		/**
		 * Saves the new order of tabs, sending an ajax request with the new order
		 *
		 * @returns 	{void}
		 */
		saveOrder: function () {
			// Remove drag handles
			this.scope.find('[data-role="reorder"]').remove();

			// Get serialized list
			var tabOrder = this.scope.sortable( 'toArray', { attribute: 'data-tab'} );
			var subMenus = {};
			var self = this;

			// Get each submenu
			_.each( tabOrder, function (val) {
				if( val ){
					subMenus[ val ] = self.scope.find('> li[data-tab="' + val + '"] > ul').sortable( 'toArray', { attribute: 'data-menuKey' } );
				}
			});

			// Switch tbe buttons around
			this.scope
				.removeClass('acpAppList_reordering')
				.find('#elReorderAppMenu')
					.find('[data-action="reorder"]')
						.removeClass('ipsHide')
					.end()
					.find('[data-action="saveOrder"]')
						.addClass('ipsHide');

			if( this._orderChanged ){
				ips.getAjax()('?app=core&module=system&controller=ajax&do=saveTabs', {
					data: {
						tabOrder: tabOrder,
						menuOrder: subMenus
					},
					dataType: 'json',
					type: 'post'
				} )
				.done( function (response) {
					ips.ui.flashMsg.show( ips.getString('tab_order_saved') );
				})
				.fail( function ( ) {
					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warning',
						message: ips.getString('tab_order_not_saved'),
						callbacks: {
							ok: function () {}
						}
					});
				});
			}
			this.scope.sortable( 'destroy' );
			this._reordering = false;
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.nodeCopySetting.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.nav.js - AdminCP Nav
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.nodeCopySetting', {

		initialize: function () {
			this.on( 'click', this.click );
		},

		/**
		 * Handles clicks
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		click: function (e) {
			/*var value = null;
			ips.getAjax()( this.scope.closest('form').attr('action') + '&massChangeValue=' + this.scope.attr('data-field'), {
				async: false,
				data: this.scope.closest('form').serialize(),
				type: 'post'
			}).done( function (response, textStatus, jqXHR) {					
				value = response;
			});*/

			e.preventDefault();
		
			if( this.scope.next().hasClass('ipsSelectTree') )
			{
				var vals = this.scope.next('.ipsSelectTree').find("input").first().val();
			}
			else
			{
				var vals = '';
				
				if( this.scope.next().hasClass('ipsField_stack') ) {
					var valArray = [];
					this.scope.next().find('input:not([type=hidden]):not([type=submit]),textarea,select').each(function(){
						if ( $(this).attr('type') == 'checkbox' ) {
							valArray.push( $(this).is(':checked') ? 1 : 0 );
						} else {
							valArray.push( $(this).val() );
						}
					})
					vals = valArray.join(',');
				}
				else if( this.scope.next().hasClass('ipsField_autocomplete') ) {
					if( $('#' + this.scope.data('field')).is( ':checked ') ) {
						vals = this.scope.nextAll("input:not([type=hidden]),textarea,select").first().val();
					}
					else {
						vals = $( 'input[name="' + this.scope.data('field') + '"]' ).val();
					}
				}
				else {
					var input = this.scope.nextAll("input:not([type=hidden]),textarea,select").first();

					// Check if there's an unlimited checkbox here
					if( $('#' + this.scope.attr('data-field') + "-unlimitedCheck:checked" ).length ){
						vals = "-1";
					} else if ( input.attr('type') == 'checkbox' ) {
						vals = input.is(':checked') ? 1 : 0;
					} else {
						vals = input.val();
					}
				}
			}
			
			var dialogRef = ips.ui.dialog.create({
				title: this.scope.attr('_title'),
				url: this.scope.attr('data-baseLink'),
				forceReload: true,
				fixed: false,
				ajax: { type: 'post', data: { value: vals } }
			});
			dialogRef.show();
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.notificationMenu.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.notificationMenu.js - Controller for the notification menu icon
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.notificationMenu', {

		initialize: function () {
			this.on( document, 'menuOpened', this.menuOpened );
			$('body').on( 'updateNotificationCount', this.updateNotificationCount );
			this.setup();
		},
		
		setup: function () {
			var notificationCount = parseInt( this.scope.find('[data-role="notificationCount"]').text() );
			if ( isNaN( notificationCount ) ) {
				notificationCount = 0;
			}
			
			var storedNotificationCount = parseInt( ips.utils.cookie.get('acpNotificationCount') );
			if ( isNaN( storedNotificationCount ) ) {
				storedNotificationCount = 0;
			}
									
			if ( notificationCount > storedNotificationCount ) {
				setTimeout(function(){
					$(this.scope).find('[data-role="notificationIcon"]').addClass('cAcpNotifications_animate');
				}.bind(this), 800 );
			}

			ips.utils.cookie.set( 'acpNotificationCount', notificationCount );
		},
		
		loaded: false,
		menuOpened: function (e, data) {
			if( !this.loaded ){
				var self = this;
				var ajaxObj = ips.getAjax();
				
				$('[data-role="notificationList"]')
					.html('')
					.css( { height: '100px' } )
					.addClass('ipsLoading');

				ajaxObj( '?app=core&module=overview&controller=notifications', { dataType: 'json' } )
					.done( function (returnedData) {
	 					
	 					// Add this content to the menu
						$('[data-role="notificationList"]')
							.css( { height: 'auto' } )
							.removeClass('ipsLoading')
							.html( returnedData.data );

						// Remember we've loaded it
						self.loaded = true;

						$( document ).trigger( 'contentChange', [ $('[data-role="notificationList"]') ] );
					});
			}
		},
		
		updateNotificationCount: function () {
			ips.getAjax()( '?app=core&module=overview&controller=notifications' ).done( function(response) {
				var count = parseInt( response.count );
				if ( count ) {
					$(this).find('[data-role="notificationCount"]').removeClass('ipsHide').text( count ).closest('.cAcpNotifications').addClass('cAcpNotifications_active');
				} else {
					$(this).find('[data-role="notificationCount"]').addClass('ipsHide').closest('.cAcpNotifications').removeClass('cAcpNotifications_active');
				}
			}.bind(this));
		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.pageActions.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.pageActions.js - Expands/contracts page actions
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.core.pageActions', {

		_expanded: false,

		initialize: function () {
			this.on( 'click', '[data-action="expandPageActions"]', this.togglePageActions );	
		},

		/**
		 * Toggles the page action buttons
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		togglePageActions: function (e) {
			e.preventDefault();

			if( this._expanded ){
				this.scope.find('> li:not( .acpToolbar_primary ):not( .acpToolbar_more )').slideUp();
			} else {
				this.scope.find('> li:not( .acpToolbar_primary ):not( .acpToolbar_more )').slideDown();
			}

			this.scope.find('[data-role="more"]').toggle( this._expanded );
			this.scope.find('[data-role="fewer"]').toggle( !this._expanded );

			this._expanded = !this._expanded;
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.announcementBanner.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.announcementBanner.js - Announcement Banners
 *
 * Author: Stuart Silvester
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.announcementBanner', {

		/**
		 * Initialise event handlers
		 *
		 * @returns		{void}
		 */
		initialize: function () {
			this.setup();
			this.on( 'click', '[data-role=&quot;dismissAnnouncement&quot;]', this.dismissAnnouncement );
		},

		/**
		 * Set up CSS for announcements
		 *
		 * @returns		{void}
		 */
		setup: function(){
			$('.cAnnouncements').addClass( 'cAnnouncementsFloat' ).css( 'zIndex', ips.ui.zIndex() );

			// Cycle and show announcements, the HTML will always contain the announcement HTML so that it's present
			// for guest caching, we use JS to show them based on the cookie values.
			this.scope.find('[data-announcementId]').each( function() {
				var announcement = $( this );
				if( !ips.utils.cookie.get( 'announcement_' + announcement.attr('data-announcementId') ) ) {
					announcement.show();
				}
			});
		},

		/**
		 * Dismiss Announcement
		 *
		 * @param		{event}		e		Event object
		 * @returns		{void}
		 */
		dismissAnnouncement: function ( e ) {
			if( e ){
				e.preventDefault();
			}

			var element = $( e.target ).closest('[data-announcementId]');
			var id = element.attr('data-announcementId');

			var date = new Date();
			date.setTime( date.getTime() + ( 7 * 86400000 ) );
			ips.utils.cookie.set( 'announcement_' + id, true, date.toUTCString() );

			element.slideUp( {
				duration: 400,
				complete: function() {
					$(this).remove();
				},
				progress: function() {

				}
			});
		},

		/**
		 * Adjust the body margin
		 *
		 * @param		{event}		e		Event object
		 * @returns		{void}
		 */
		reflow: function( e ){
			// Deprecated
		}

	});
}(jQuery, _));
</file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.app.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.app.js - Front end app controller 
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.app', {
		
		initialize: function () {
			this.on( 'click', 'a[data-confirm],button[data-confirm]', this.confirmSomething );
			this.on( document, 'contentChange', this._checkAndClearAutosave );
			this.on( document, 'contentChange', this.checkOldEmbeds );
			this.on( document, 'contentChange', this.updateExternalLinks );
			
			this.setup();
		},

		/**
		 * Setup method for the front app
		 *
		 * @returns {void}
		 */
		setup: function () {	
			if( ips.utils.serviceWorker.supported ){
				ips.utils.serviceWorker.registerServiceWorker('front', !_.isUndefined( ips.utils.cookie.get('loggedIn') ));
			}

			// Add a classname to the document for js purposes
			this.scope.addClass('ipsJS_has').removeClass('ipsJS_none');
			if ( !ips.utils.events.isTouchDevice() ) {
				this.scope.addClass('ipsApp_noTouch');
			}

			// Timezone detection
			ips.utils.cookie.set( 'ipsTimezone', new Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC" );
			
			// Clear any autosave stuff
			this._checkAndClearAutosave();
			if ( !ips.getSetting('memberID') && ips.utils.url.getParam('_fromLogout') ) {
				ips.utils.db.removeByType('editorSave');
			}

			// Inline message popup
			// Create a dialog if it exists
			if( $('#elInlineMessage').length ){
				var dialogRef = ips.ui.dialog.create({
					showFrom: '#inbox',
					content: '#elInlineMessage',
					title: $('#elInlineMessage').attr('title')
				});

				// Leave a little time
				setTimeout( function () {
					dialogRef.show();
				}, 800);				
			}

			// Open external links in a new window
			ips.utils.links.updateExternalLinks( this.scope );

			// Find any embeds on the page and upgrade them, and fix lazy load images without a wrapper
			this._upgradeOldEmbeds( this.scope );
			this._fixMissingLazyLoadItems( this.scope );

			// Set up prettyprint
			prettyPrint();

			// Set our JS cookie for future use - we'll set it for a day
			if( _.isUndefined( ips.utils.cookie.get( 'hasJS') ) ){
				var expires = new Date();
				expires.setDate( expires.getDate() + 1 );
				ips.utils.cookie.set( 'hasJS', true, expires.toUTCString() );
			}

			// If we can't view user profiles, remove links in the comment content
			if( !ips.getSetting('viewProfiles') ){
				this._removeProfileLinks();
			}
		},

		/**
		 * Called on content change. Updates externals links to open in a new window.
		 *
		 * @returns {void}
		 */
		updateExternalLinks: function (e, data) {
			ips.utils.links.updateExternalLinks( data );
		},

		/**
		 * Called on content change. Upgrade any old embeds within this content container.
		 *
		 * @returns {void}
		 */
		checkOldEmbeds: function (e, data) {
			this._upgradeOldEmbeds( data );
		},

		/**
		 * Remove user profile links where possible if the current viewing user cannot access profiles
		 *
		 * @returns {void}
		 */
		_removeProfileLinks: function () {
			this.scope
				.find('a[data-mentionid],a[href*="controller=profile"]')
					.replaceWith( function(){ return $(this).contents(); } );
		},

		/**
		 * Upgrade old embeds so that they include the correct controller
		 *
		 * @returns {void}
		 */
		_upgradeOldEmbeds: function (element) {
			// element is not always defined
			if( _.isUndefined( element ) ){
				return;
			}

			// Apply embed controllers on old content
			var oldEmbeds = element.find('[data-embedcontent]:not([data-controller], [data-embed-src])');
			var toRefresh = [];

			if( oldEmbeds.length ){
				oldEmbeds.each( function () {
					$( this ).attr('data-controller', 'core.front.core.autoSizeIframe');
					toRefresh.push( this );
					Debug.log("Upgraded old embed");
				});
				$( document ).trigger( 'contentChange', [ jQuery([]).pushStack( toRefresh ) ] );
			}
		},

		/**
		 * Check and clear autosave
		 *
		 * @returns {void}
		 */
		 _checkAndClearAutosave: function() {
		 	if( ips.utils.cookie.get('clearAutosave') ) {
				var autoSaveKeysToClear = ips.utils.cookie.get('clearAutosave').split(',');
				for ( var i = 0; i < autoSaveKeysToClear.length; i++ ) {
					ips.utils.db.remove( 'editorSave', autoSaveKeysToClear[i] );
				}
				ips.utils.cookie.unset('clearAutosave');
			}
		 },
		
		/**
		 * Prompts the user to confirm an action
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		confirmSomething: function (e) {
			e.preventDefault();
			var elem = $( e.currentTarget );
			var customMessage = $( e.currentTarget ).attr('data-confirmMessage');
			var subMessage = $( e.currentTarget ).attr('data-confirmSubMessage');
			var icon = $( e.currentTarget ).attr('data-confirmIcon');
			
			ips.ui.alert.show( {
				type: 'confirm',
				icon: ( icon ) ? icon : 'warn',
				message: ( customMessage ) ? customMessage : ips.getString('generic_confirm'),
				subText: ( subMessage ) ? subMessage : '',
				callbacks: {
					ok: function () {
						window.location = elem.attr('href') + '&wasConfirmed=1';
					}
				}
			});
		},

		/**
		 * A catch-all for items that need to be lazy loaded, but which might not be inside a lazy load wrapper
		 *
		 * @returns {void}
		 */
		_fixMissingLazyLoadItems: function (container) {
			// Find any lazy load stuff in this container
			var content = $( container ).find( ips.utils.lazyLoad.contentSelector ).not('.ipsHide');
			var initialLength = content.length;
			var toObserve = [];

			if( !initialLength ){
				return;
			}

			content.each( function () {
				// Loop through each item that needs to be loaded, and check it isn't already within
				// a lazy-load compatible wrapper. If it isn't, then manually observe it now.
				var closest = $( this ).closest('[data-ipsLazyLoad], [data-controller^="core.front.core.lightboxedImages"]');

				if( !closest.length ){
					if( ips.getSetting('lazyLoadEnabled') ){
						ips.utils.lazyLoad.observe( this );
					} else {
						ips.utils.lazyLoad.loadContent(this); // load immediately
					}
				}
			});
		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.articlePages.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.articlePages.js - Turns content using the page bbcode into paginated content
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.articlePages', {

		_currentPage: 1,
		_pages: null,
		_articleID: '',

		initialize: function () {
			this.on( 'paginationClicked', this.paginationClicked );
			
			// Primary event that watches for URL changes
			History.Adapter.bind( window, 'statechange', _.bind( this.stateChange, this ) );

			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			this._articleID = this._getArticleID();
			this._setupPages();
		},

		/**
		 * stateChange event
		 *
		 * @returns {void}
		 */
		stateChange: function () {
			var state = History.getState();

			if( _.isUndefined( state.data.controller ) || state.data.controller != 'article-' + this._articleID ){
				return;
			}

			var newPage = parseInt( state.data[ 'page' + this._articleID ] );

			if( _.isUndefined( this._pages[ newPage - 1 ] ) ){
				return;
			}

			this._pages.hide();
			this._currentPage = newPage;

			ips.utils.anim.go( 'fadeIn', $( this._pages[ newPage - 1 ] ) );

			this._checkButtons();
		},

		/**
		 * Event handler for the pagination widget being clicked
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data 	Event data object
		 * @returns {void}
		 */
		paginationClicked: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
				data.originalEvent.stopPropagation();
			}

			// Don't allow these pagination events to bubble up to main comment feed pagination
			e.stopPropagation();

			var urlData = {
				controller: 'article-' + this._articleID
			};

			if( data.pageNo == 'next' ){
				urlData[ 'page' + this._articleID ] = this._currentPage + 1;
			} else {
				urlData[ 'page' + this._articleID ] = this._currentPage - 1;
			}

			var url = this._buildURL( urlData[ 'page' + this._articleID ] );

			History.pushState( urlData, document.title, url );
		},

		/**
		 * Determines an appropriate article ID for this controller
		 * If data-articleID isn't specified, it'll try and find a comment ID,
		 * or default to the sequential dom element ID.
		 *
		 * @returns {string}
		 */
		_getArticleID: function () {
			if( this.scope.attr('data-articleID') ){
				return this.scope.attr('data-articleID');
			} else if( this.scope.closest('[data-commentID]') ) {
				return 'comment' + this.scope.closest('[data-commentID]').attr('data-commentID');
			} else {
				// This isn't great because it'll change for each user, but if we've
				// got nothing else to go on...
				return this.scope.identify().attr('id');	
			}			
		},

		/**
		 * Builds a URL that is a link to this particular page of the document
		 *
		 * @param 	{number} 	pageNo 		Page number to include in the URL
		 * @returns {void}
		 */
		_buildURL: function (pageNo) {
			// Get URL object first
			var urlObj = ips.utils.url.getURIObject();
			// Build the base URL
			var url = urlObj.protocol + '://' + urlObj.host + ( urlObj.port ? ( ':' + urlObj.port ) : '' ) + urlObj.path + '?';

			// Add or replace our page number param
			urlObj.queryKey[ 'page' + this._articleID ] = pageNo;

			var params = _.clone( urlObj.queryKey );

			// If we're using index.php? urls, the keys may be /forum/forum-2/ style
			// The /../ part will have an empty value, so we add those to the URL manually first
			if( urlObj.file == 'index.php' ){
				_.each( params, function (val, key) {
					if( key.startsWith('/') ){
						url += key;
						delete params[ key ];					
					}
				});

				url += '&';
			}

			// If we still have other params, add those to the URL
			if( ! _.isEmpty( params ) ){
				url += $.param( params );
			}

			return url;
		},

		/**
		 * Checks the next/prev buttons, and shows/hides them as needed
		 *
		 * @returns {void}
		 */
		_checkButtons: function () {
			var indexedPage = this._currentPage - 1;

			this.scope.find('.ipsPagination_prev').toggle( !( indexedPage <= 0 ) );
			this.scope.find('.ipsPagination_next').toggle( !( indexedPage >= ( this._pages.length - 1 ) ) );
		},

		/**
		 * Sets up the content, showing pagination
		 *
		 * @returns {void}
		 */
		_setupPages: function () {
			// Find the pages
			this._pages = this.scope.find('[data-role="contentPage"]');

			if( this._pages.length < 2 ){
				return;
			}

			// Add pagination to the top and bottom
			this.scope.prepend( ips.templates.render('core.pagination') );
			this.scope.append( ips.templates.render('core.pagination') );

			// Hide all pages
			this._pages.hide();

			// Do we have a page in the URL?
			if( !_.isUndefined( ips.utils.url.getParam('page' + this._articleID ) ) ){
				this._currentPage = parseInt( ips.utils.url.getParam('page' + this._articleID ) );
			}

			// Show the right page
			$( this._pages[ this._currentPage - 1 ] ).show();

			// Hide the 'previous'
			this._checkButtons();

			// Hide the breaks
			this.scope.find('[data-role="contentPageBreak"]').hide();

			// And reinit content
			$( document ).trigger( 'contentChange', [ this.scope ] );
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.autoSizeIframe.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.autoSizeIframe.js - Controller to automatically adjust the height of an iframe
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.autoSizeIframe', {

		_origin: ips.utils.url.getOrigin(),
		_embedId: '',
		_iframe: null,
		_border: { vertical: 0, horizontal: 0 },

		initialize: function () {
			if( !this.scope.is('iframe') ){
				return;
			}

			this.on( window, 'message', this.receiveMessage );
			this.on( document, 'breakpointChange', this.breakpointChange );
			this.setup();
		},

		/**
		 * Sets some basic styles on the iframe, and sets up an interval
		 * to constantly check for any change in sizing
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		setup: function () {
			this._lastDims = { height: 0, width: 0 };

			var iframe = this.scope.get(0);
			iframe.style.overflow = 'hidden';

			this._getBorderAdjustment();

			// Make sure the built-in height is reasonable
			if( this.scope.height() > 800 ){
				this.scope.css({
					height: '800px'
				});
			}

			this._iframe = iframe.contentWindow;

			// Do we have an embed ID? If not, we need to generate one
			this._embedId = 'embed' + parseInt( Math.random() * (10000000000 - 1) + 1 );
			this.scope.attr('data-embedId', this._embedId );

			// Check for postMessage and JSON support
			if( !window.postMessage || !window.JSON.parse ){
				this.scope.css({
					height: '400px'
				});
				Debug.error("Can't resize embed: " + this._embedId );
				return;
			}

			// We can now tell the iframe we are ready
			this._startReadyTimeout();
		},

		/**
		 * Sets an interval that pings the iframe with a ready message
		 * This needs to repeat because the iframe might not immediately be ready to receive messages.
		 *
		 * @returns {void}
		 */
		_startReadyTimeout: function () {
			this._readyTimeout = setInterval( _.bind( function () {
				this._postMessage('ready');
			}, this ), 100 );

			// We'll just give it 10 seconds, then stop trying
			setTimeout( _.bind( function () {
				this._stopReadyTimeout();
			}, this ), 10000 );
		},

		/**
		 * Stops the ready message interval from firing
		 *
		 * @returns {void}
		 */
		_stopReadyTimeout: function () {
			if( this._readyTimeout !== null ){
				Debug.log("Stopped posting to embed " + this._embedId);
				clearInterval( this._readyTimeout );
				this._readyTimeout = null;
			}
		},

		/**
		 * Event handler for this controller being destructed
		 *
		 * @returns {void}
		 */
		destruct: function () {
			Debug.log('Destruct autoSizeIframe ' + this._embedId);
			this._stopReadyTimeout();
			this._postMessage('stop');
		},

		/**
		 * Event handler for this controller receiving a messgae.
		 * Actually, all messages to this window are handled here, so we have to check the origin,
		 * and whether it's a message for this controller in particular (i.e. the embedIds match)
		 *
		 * @returns {void}
		 */
		receiveMessage: function (e) {
			if( e.originalEvent.origin !== this._origin ){
				return;
			}

			try {
				var pmData = JSON.parse( e.originalEvent.data );
				var method = pmData.method;
			} catch (err) {
				Debug.log("Invalid data");
				return;
			}

			if( _.isUndefined( pmData.embedId ) || pmData.embedId !== this._embedId ){
				return;
			}

			// Stop telling it we're ready now
			this._stopReadyTimeout();

			if( method && !_.isUndefined( this[ method ] ) ){
				this[ method ].call( this, pmData );
			}
		},

		/**
		 * Post a message to the iframe
		 *
		 * @returns {void}
		 */
		_postMessage: function (method, obj) {
			// Send to iframe
			Debug.log("Posting to iframe " + this._embedId);

			this._iframe.postMessage( JSON.stringify( _.extend( obj || {}, {
				method: method,
				embedId: this._embedId
			} ) ), this._origin );
		},

		/**
		 * Get the border widths, which we'll use to adjust the widths we set on the iframe
		 *
		 * @returns {void}
		 */
		_getBorderAdjustment: function () {
			this._border.vertical = parseInt( this.scope.css('border-top-width') ) + parseInt( this.scope.css('border-bottom-width') );
			this._border.horizontal = parseInt( this.scope.css('border-left-width') ) + parseInt( this.scope.css('border-right-width') );
		},

		/**
		 * Event handler for the breakpoint changing
		 *
		 * @returns {void}
		 */
		breakpointChange: function (e, data) {
			// Now send the frame our responsive state
			this._postMessage('responsiveState', {
				currentIs: data.curBreakName
			});
		},

		/**********************************/
		/* Events from the iframe */

		/**
		 * Display a dialog
		 *
		 * @returns {void}
		 */
		dialog: function (data) {
			var options = ips.ui.getAcceptedOptions('dialog');
			var dialogOptions = {};

			_.each( options, function (opt) {
				if( !_.isUndefined( data.options['data-ipsdialog-' + opt.toLowerCase() ] ) ){
					dialogOptions[ opt ] = data.options['data-ipsdialog-' + opt.toLowerCase() ];
				}
			});

			if( _.isUndefined( dialogOptions['url'] ) ){
				dialogOptions['url'] = data.url;
			}

			var dialogRef = ips.ui.dialog.create( dialogOptions );

			dialogRef.show();
		},

		/**
		 * Set the height of the iframe
		 *
		 * @returns {void}
		 */
		height: function (data) {
			if( this._lastDims.height !== data.height ){
				this.scope.css({
					height: parseInt( data.height ) + this._border.vertical + 'px'
				});

				this._postMessage('setDimensions', {
					height: parseInt( data.height )
				});

				this._lastDims.height = data.height;
			}
		},

		/**
		 * Set the dimensions of the iframe
		 *
		 * @returns {void}
		 */
		dims: function (data) {
			if( parseInt( this._lastDims.height ) !== parseInt( data.height ) || this._lastDims.width !==  data.width ){
				this.scope.css({
					height: parseInt( data.height ) + this._border.vertical + 'px',
					maxWidth: ( data.width.toString().indexOf('%') == -1 ) ? parseInt( data.width ) + this._border.horizontal + 'px' : '100%'
				});

				this._lastDims.height = data.height;
				this._lastDims.width = data.width;
			}
		},

		/**
		 * The iframe received our Ready message
		 *
		 * @returns {void}
		 */
		ok: function () {
			this._stopReadyTimeout();
			this.scope.addClass('ipsEmbed_finishedLoading');

			// Now send the frame our responsive state
			this._postMessage('responsiveState', {
				currentIs: ips.utils.responsive.getCurrentKey()
			});
		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.comment.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.comment.js - General controller for comments
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.comment', {

		_quoteData: null,
		_commentContents: '',
		_quotingDisabled: false,
		_quoteTimeout: null,
		_isEditing: false,
		_clickHandler: null,
		
		initialize: function () {		
										
			// Events from within scope
			this.on( 'click', '[data-action="editComment"]', this.editComment );
			this.on( 'click', '[data-action="cancelEditComment"]', this.cancelEditComment );  
			this.on( 'click', '[data-action="deleteComment"]', this.deleteComment );
			this.on( 'click', '[data-action="approveComment"]', this.approveComment );
			this.on( 'click', '[data-action="quoteComment"]', this.quoteComment );
			this.on( 'click', '[data-action="multiQuoteComment"]', this.multiQuoteComment );
			this.on( 'click', '[data-action="rateReview"]', this.rateReview );
			this.on( 'submit', 'form', this.submitEdit );
			this.on( 'change', 'input[type="checkbox"][data-role="moderation"]', this.commentCheckbox );
			this.on( 'mouseup touchend touchcancel selectionchange', '[data-role="commentContent"]', this.inlineQuote );
			this.on( 'click', '[data-action="quoteSelection"]', this.quoteSelection );
			this.on( 'openDialog', '[data-role="shareComment"]', this.shareCommentDialog );
			this.on( 'submitDialog', '[data-action="recommendComment"]', this.recommendComment );
			this.on( 'click', '[data-action="unrecommendComment"]', this.unrecommendComment );

			// Events sent down by the commentFeed controller
			this.on( 'setMultiQuoteEnabled.comment setMultiQuoteDisabled.comment', this.setMultiQuote );
			this.on( 'disableQuoting.comment', this.disableQuoting );

			// Model events that are handled all at once
			this.on( document, 'getEditFormLoading.comment saveEditCommentLoading.comment ' + 
									'deleteCommentLoading.comment', this.commentLoading );
			
			this.on( document, 'getEditFormDone.comment saveEditCommentDone.comment ' + 
									'deleteCommentDone.comment', this.commentDone );

			// Model events
			this.on( document, 'getEditFormDone.comment', this.getEditFormDone );
			this.on( document, 'getEditFormError.comment', this.getEditFormError );
			//---
			this.on( document, 'saveEditCommentDone.comment', this.saveEditCommentDone );
			this.on( document, 'saveEditCommentError.comment', this.saveEditCommentError );
			//---
			this.on( document, 'deleteCommentDone.comment', this.deleteCommentDone );
			this.on( document, 'deleteCommentError.comment', this.deleteCommentError );
			//---
			this.on( document, 'unrecommendCommentDone.comment', this.unrecommendCommentDone );
			this.on( document, 'unrecommendCommentError.comment', this.unrecommendCommentError );
			//---
			this.on( document, 'approveCommentLoading.comment', this.approveCommentLoading );
			this.on( document, 'approveCommentDone.comment', this.approveCommentDone );
			this.on( document, 'approveCommentError.comment', this.approveCommentError );

			this.setup();
		},
		
		/**
		 * Setup method for comments
		 *
		 * @returns {void}
		 */
		setup: function () {
			this._commentID = this.scope.attr('data-commentID');
			this._clickHandler = _.bind( this._hideQuoteTooltip, this );
		},

		destroy: function () {
			// --
		},

		/**
		 * Event handler for selective quoting, called on mouseup (after user has dragged/clicked)
		 * Get the selected text, then leave a short timeout before showing the tooltip
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns	{void}
		 */
		inlineQuote: function (e) {
			var self = this;
			var quoteButton = this.scope.find('[data-action="quoteComment"]');

			if( this._isEditing || this._quotingDisabled || !quoteButton.length ){
				return;
			}			

			clearInterval( this._quoteTimeout );

			this._quoteTimeout = setInterval( function () {
				self._checkQuoteStatus(e);
			}, 400 );			
		},

		/**
		 * Event handler for recommending comments. Triggered by the dialog being submitted and a successful response
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns	{void}
		 */
		recommendComment: function (e, data) {
			var commentHtml = $('<div>' + data.response.comment + '</div>').find('[data-controller="core.front.core.comment"]').html();

			this.scope
				.html( commentHtml )
				.closest('.ipsComment')
					.addClass('ipsComment_popular');

			// Let document know
			$( document ).trigger( 'contentChange', [ this.scope ] );

			// Set up multiquote in this comment
			if( ips.utils.db.isEnabled() ){
				this.scope.find('[data-action="multiQuoteComment"]').removeClass('ipsHide');
			}

			this.trigger('refreshRecommendedComments', {
				scroll: true,
				recommended: data.response.recommended
			});
		},

		/**
		 * Event handler for un-recommending comments.
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns	{void}
		 */
		unrecommendComment: function (e, data) {
			e.preventDefault();

			var url = $( e.currentTarget ).attr('href');

			this.trigger( 'unrecommendComment.comment', {
				url: url,
				commentID: this._commentID
			});
		},

		/**
		 * Unrecommending a comment was successful (triggered by comment model)
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns	{void}
		 */
		unrecommendCommentDone: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}

			var commentHtml = $('<div>' + data.comment + '</div>').find('[data-controller="core.front.core.comment"]').html();

			this.scope
				.html( commentHtml )
				.closest('.ipsComment')
					.removeClass('ipsComment_popular')
					.find('.ipsComment_popularFlag')
						.remove();
					

			// Flash message
			ips.ui.flashMsg.show( ips.getString( 'commentUnrecommended' ) );

			// Tell the recommended overview to remove it
			this.trigger('removeRecommendation', {
				commentID: data.unrecommended
			});
		},

		/**
		 * Unrecommending a comment failed
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns	{void}
		 */
		unrecommendCommentError: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}

			window.reload();
		},

		/**
		 * Figure out if our selected text has changed
		 *
		 * @returns	{void}
		 */
		_checkQuoteStatus: function () {
			var selectedText = ips.utils.selection.getSelectedText( '[data-role="commentContent"]', this.scope.find('[data-role="commentContent"]').parent() );
			var ancestor = ips.utils.selection.getCommonAncestor();

			if( selectedText.trim() == '' ){
				this._hideQuoteTooltip();
				return;
			}

			if( ancestor && ancestor.closest('.ipsCode').length ){
				// If we've selected inside a code block, wrap our selected text too
				selectedText = "<pre class='ipsCode prettyprint'>" + selectedText + "</pre>";
			} else if( !selectedText.startsWith('<') ) {
				// If the user selects a bunch of text that contains HTML, the browser will automatically wrap it for us. But if the user
				// just selects some plain text, that's all we get. So in that case, we'll wrap it ourselves.
				selectedText = '<p>' + selectedText + '</p>';
			}

			if( this._selectedText == selectedText ){
				return;
			}
			
			this._selectedText = selectedText;
			
			this._showQuoteTooltip();
		},

		/**
		 * Builds & shows the selective quoting tooltip, displaying it just above the user's cursor
		 *
		 * @returns	{void}
		 */
		_showQuoteTooltip: function () {
			var selection = ips.utils.selection.getSelection();
			var range = ips.utils.selection.getRange( this.scope.find('[data-role="commentContent"]') );
			var tooltip = this.scope.find('[data-role="inlineQuoteTooltip"]');
			var scopeOffset = this.scope.offset();
			var position = {
				left: 0,
				top: 0
			};

			if( range === false || !_.isObject( range ) || _.isUndefined( range.type ) ){ // No selection found
				Debug.log("No selection found");
				return;
			}

			// Create the new tooltip if needed
			if( !tooltip.length ){
				this.scope.append( ips.templates.render('core.selection.quote', {
					direction: 'bottom'//ips.utils.events.isTouchDevice() ? 'bottom' : 'top'
				}) );	
				tooltip = this.scope.find('[data-role="inlineQuoteTooltip"]');
				$( document ).on( 'click dblclick', this._clickHandler );
			}

			if ( range.type === 'outside' ){ 
				// Selection was beyond our content area, so we need to limit it to end of the content
				// Get the bounding of the selection to use as the basis of our tooltip position
				var boundingBox = range.range.getBoundingClientRect();
				var offset = this.scope.offset();

				position.left = boundingBox.left + ( boundingBox.width / 2 ) + $( window ).scrollLeft() - offset.left;
				position.top = boundingBox.top + boundingBox.height + $( window ).scrollTop() - offset.top;
			} else { 
				// Normal selection, inside content, so we can position based on the Range directly
				// Clone the range, and insert an element containing an invisible character which we'll use
				// to fetch the position
				var cloneRange = range.range.cloneRange();
				var invisibleElement = document.createElement('span');
				invisibleElement.appendChild( document.createTextNode('\ufeff') );

				// Collapse the range so that we only care about the end position
				cloneRange.collapse( false );

				// Insert our invisible element into the range
				cloneRange.insertNode( invisibleElement );

				// Get the position of the invisible element we created
				var tmpPosition = ips.utils.position.getElemPosition( $( invisibleElement ) );

				position.left = tmpPosition.absPos.left - scopeOffset.left;
				position.top = tmpPosition.absPos.top - scopeOffset.top + 25;

				// Remove the invisible element (fixes #1758)
				invisibleElement.parentNode.removeChild(invisibleElement);
			}	

			var tooltipSize = {
				width: tooltip.show().outerWidth(),
				height: tooltip.show().outerHeight()
			};

			// Set the position. On touch devices, move it a little further to the left to avoid the OS's touch handle
			var leftAdjustment = ips.utils.events.isTouchDevice() ? tooltipSize.width : ( tooltipSize.width / 2 );

			tooltip.css({
				position: 'absolute',
				left: Math.round( position.left - leftAdjustment ) + 'px',
				top: Math.round( position.top ) + 'px',
				zIndex: ips.ui.zIndex()
			});

			// If the tooltip isn't already shown, fade it in
			if( !tooltip.is(':visible') ){
				tooltip.hide().fadeIn('fast');
			} else {
				tooltip.show();
			}
		},

		/**
		 * Hide the selective quote tooltip
		 *
		 * @returns	{void}
		 */
		_hideQuoteTooltip: function () {
			$( document ).off( 'click dblclick', this._clickHandler );
			clearInterval( this._quoteTimeout );
			this.scope.find('[data-role="inlineQuoteTooltip"]').fadeOut('fast');
			this._selectedText = '';
		},

		/**
		 * Event handler for clicking 'quote this' in the selective quote tooltip
		 * Triggers an event sent to the comment feed
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns	{void}
		 */
		quoteSelection: function (e) {
			e.preventDefault();

			this._getQuoteData();

			if( this._selectedText ){
				this.trigger('quoteComment.comment', {
					userid: this._quoteData.userid,
					username: this._quoteData.username,
					timestamp: this._quoteData.timestamp,
					contentapp: this._quoteData.contentapp,
					contenttype: this._quoteData.contenttype,
					contentclass: this._quoteData.contentclass,
					contentid: this._quoteData.contentid,
					contentcommentid: this._quoteData.contentcommentid,
					quoteHtml: this._selectedText
				});
			}

			this._hideQuoteTooltip();
		},

		/**
		 * Triggered when the moderation checkbox is changed
		 *	
		 * @param 		{event}		e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		commentCheckbox: function (e) {
			var checked = $( e.currentTarget ).is(':checked');
			this.scope.closest('.ipsComment').toggleClass( 'ipsComment_selected', checked );

			this.trigger('checkedComment.comment', {
				commentID: this._commentID,
				actions: $( e.currentTarget ).attr('data-actions'),
				checked: checked
			});
		},

		/**
		 * The comment feed has told us we can't support quoting
		 *	
		 * @returns 	{void}
		 */
		disableQuoting: function () {
			this._quotingDisabled = true;
			this.scope.find('[data-ipsQuote-editor]').remove();
		},
		
		/**
		 * Event handler for the Helpful/Unhelpful buttons.
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		rateReview: function (e) {
			e.preventDefault();
			var self = this;

			ips.getAjax()( $( e.currentTarget ).attr('href') )
				.done( function (response) {
					var content = $("<div>" + response + "</div>");
					self.scope.html( content.find('[data-controller="core.front.core.comment"]').contents() );

					$( document ).trigger( 'contentChange', [ self.scope ] );
				})
				.fail( function (err) {
					window.location = $( e.currentTarget ).attr('href');
				});
		},

		/**
		 * User has clicked the share button within the comment; we'll select the text to make it easy to copy
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		shareCommentDialog: function (e, data) {
			if( data.dialog ){
				data.dialog.find('input[type="text"]').get(0).select();
			}
		},

		/**
		 * Event fired on this controller by a core.commentFeed controller to tell us which
		 * multiquote buttons are enabled presently. Here we check whether this applies to us, and toggle
		 * the button if so.
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		setMultiQuote: function (e, data) { 
			var selector = '[data-commentApp="' + data.contentapp + '"]';
				selector += '[data-commentType="' + data.contenttype + '"]';
				selector += '[data-commentID="' + data.contentcommentid + '"]';

			if( this.scope.is( selector ) ){
				if( !_.isNull( e ) && e.type == 'setMultiQuoteEnabled') {
					
					this.scope.find('[data-action="multiQuoteComment"]')
						.removeClass('ipsButton_simple')
						.addClass('ipsButton_alternate')
						.attr( 'data-mqActive', true )
						.html( ips.templates.render('core.posts.multiQuoteOn') );

				} else if( _.isNull( e ) || e.type == 'setMultiQuoteDisabled' ) {

					this.scope.find('[data-action="multiQuoteComment"]')
						.addClass('ipsButton_simple')
						.removeClass('ipsButton_alternate')
						.removeAttr( 'data-mqActive' )
						.html( ips.templates.render('core.posts.multiQuoteOff') );

				}
			}
		},

		/**
		 * Event handler for the Quote button. Triggers a quoteComment event for the
		 * commentFeed controller to handle.
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		quoteComment: function (e) {
			e.preventDefault();
			
			if( !this._getQuoteData() ){
				Debug.error("Couldn't get quote data");
				return;
			}

			var html = this._prepareQuote( $('<div/>').html( this.scope.find('[data-role="commentContent"]').html() ) );
			
			// Send the event up the chain to the commentFeed controller for handling
			this.trigger( 'quoteComment.comment', {
				userid: this._quoteData.userid,
				username: this._quoteData.username,
				timestamp: this._quoteData.timestamp,
				contentapp: this._quoteData.contentapp,
				contenttype: this._quoteData.contenttype,
				contentclass: this._quoteData.contentclass,
				contentid: this._quoteData.contentid,
				contentcommentid: this._quoteData.contentcommentid,
				quoteHtml: html.html()
			});
		},

		/**
		 * MultiQuote comment handler
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		multiQuoteComment: function (e) {
			e.preventDefault();

			if( !this._getQuoteData() ){
				Debug.error("Couldn't get quote data");
				return;
			}
			
			var button = $( e.currentTarget );
			var mqActive = button.attr('data-mqActive');

			var html = this._prepareQuote( $('<div/>').html( this.scope.find('[data-role="commentContent"]').html() ) );

			this.trigger( ( mqActive ) ? 'removeMultiQuote.comment' : 'addMultiQuote.comment', {
				userid: this._quoteData.userid,
				username: this._quoteData.username,
				timestamp: this._quoteData.timestamp,
				contentapp: this._quoteData.contentapp,
				contenttype: this._quoteData.contenttype,
				contentclass: this._quoteData.contentclass,
				contentid: this._quoteData.contentid,
				contentcommentid: this._quoteData.contentcommentid,
				quoteHtml: html.html(),
				button: button.attr('data-mqId')
			});

			if( mqActive ){
				button
					.removeClass('ipsButton_alternate')
					.addClass('ipsButton_simple')
					.removeAttr('data-mqActive')
					.html( ips.templates.render('core.posts.multiQuoteOff') );
			} else {
				button
					.removeClass('ipsButton_simple')
					.addClass('ipsButton_alternate')
					.attr( 'data-mqActive', true )
					.html( ips.templates.render('core.posts.multiQuoteOn') );
			}
		},

		/**
		 * Edit comment handler
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		editComment: function (e) {
			e.preventDefault();
			
			this._commentContents = this.scope.find('[data-role="commentContent"]').html();
			
			var url = $( e.currentTarget ).attr('href');

			this.trigger( 'getEditForm.comment', {
				url: url,
				commentID: this._commentID
			});
		},
		
		/**
		 * Called when a cancel link is clicked
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		cancelEditComment: function (e) {
			e.preventDefault();
			
			var self = this;
			
			ips.ui.alert.show( {
				type: 'verify',
				icon: 'warn',
				message: ips.getString('cancel_edit_confirm'),
				subText: '',
				buttons: { yes: ips.getString('yes'), no: ips.getString('no') },
				callbacks: {
					yes: function () {
						ips.ui.editor.destruct( self.scope.find('[data-ipseditor]') );
						self.scope.find('[data-role="commentContent"]').html( self._commentContents );
						self.scope.find('[data-role="commentControls"], [data-action="expandTruncate"]').show();
						self.scope.find('[data-action="editComment"]').parent('li').show();
						$( document ).trigger( 'contentChange', [ self.scope ] );
					}
				}
			});
		},
		
		/**
		 * Called when a comment edit button is clicked
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		submitEdit: function (e) {
			e.preventDefault();
			e.stopPropagation(); // This is a form within a form, so we have to prevent it bubbling up otherwise IE gets confused and will try to submit the moderation actions form too
			
			var instance;
			var empty = false;

			for( instance in CKEDITOR.instances ){
				CKEDITOR.instances[ instance ].updateElement();
			}

			if ( typeof CKEDITOR.instances['comment_value'] !== 'undefined' ) {
				var postBody = CKEDITOR.instances['comment_value'].editable().getData().replace(/&nbsp;/g, '').trim();

				if (postBody == '' || postBody.match(/^<p>(<p>|<\/p>|\s)*<\/p>$/)) {
					ips.ui.alert.show({
						type: 'alert',
						icon: 'warn',
						message: ips.getString('cantEmptyEdit'),
						subText: ips.getString('cantEmptyEditDesc')
					});
					return;
				}
			}

			var form = this.scope.find('form');
			var url = form.attr('action');				
			var data = form.serialize();
			
			form.find('[data-action="cancelEditComment"]').remove();
			form.find('[type="submit"]').prop( 'disabled', true ).text( ips.getString('saving') );
						
			this.trigger( 'saveEditComment.comment', {
				form: data,
				url: url,
				commentID: this._commentID
			});
		},

		/**
		 * Model event: something is loading
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		commentLoading: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}

			var commentLoading = this.scope.find('[data-role="commentLoading"]');
			
			commentLoading
				.removeClass('ipsHide')
				.find('.ipsLoading')
					.removeClass('ipsLoading_noAnim');

			ips.utils.anim.go( 'fadeIn', commentLoading );
		},

		/**
		 * Model event: something is done loading
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		commentDone: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}


			this.scope
				.find('[data-role="commentLoading"]')
					.addClass('ipsHide')
					.find('.ipsLoading')
						.addClass('ipsLoading_noAnim');
		},

		/**
		 * Model event: edit form has loaded
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		getEditFormDone: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}

			var self = this;
			var showForm = _.once( function () {
				self._isEditing = true;
				self.scope.find('[data-action="expandTruncate"], [data-role="commentControls"]').hide();
				self.scope.find('[data-action="editComment"]').parent('li').hide();
				self.scope.find('[data-role="commentContent"]').html( data.response );

				$( document ).trigger( 'contentChange', [ self.scope.find('[data-role="commentContent"]') ] );
			});

			// Scroll to the comment
			var elemPosition = ips.utils.position.getElemPosition( this.scope );
			var windowScroll = $( window ).scrollTop();
			var viewHeight = $( window ).height();

			// Only scroll if it isn't already on the screen
			if( elemPosition.absPos.top < windowScroll || elemPosition.absPos.top > ( windowScroll + viewHeight ) ){
				$('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' }, function () {
					showForm();
				});	
			} else {
				showForm();
			}
		},

		/**
		 * Model event: error loading edit form
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		getEditFormError: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}

			window.location = data.url;
		},

		/**
		 * Model event: saving an edit is finished
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		saveEditCommentDone: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}
			
			ips.ui.editor.destruct( this.scope.find('[data-ipseditor]') );

			this._isEditing = false;
			this.scope.find('[data-role="commentContent"]').replaceWith( $('<div>' + data.response + '</div>').find('[data-role="commentContent"]') );
			this.scope.trigger('refreshContent');
			this.scope.find('[data-action="expandTruncate"], [data-role="commentControls"]').show();
			this.scope.find('[data-action="editComment"]').parent('li').show();
			
			$( document ).trigger( 'contentChange', [ this.scope ] );
		},

		/**
		 * Model event: saving an edit failed
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		saveEditCommentError: function (e, data) {
			
			if( data.commentID != this._commentID ){
				return;
			}
			
			ips.ui.alert.show( {
				type: 'alert',
				icon: 'warn',
				message: ips.getString('editCommentError'),
			});
			//this.scope.find('form').submit();
		},
			
		/**
		 * Handler for approving a comment
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		approveComment: function (e) {
			e.preventDefault();

			var url = $( e.currentTarget ).attr('href');

			this.trigger( 'approveComment.comment', {
				url: url,
				commentID: this._commentID
			});
		},

		/**
		 * Model indicates it's starting to approve the comment
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Data object from model
		 * @returns 	{void}
		 */
		approveCommentLoading: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}

			this.scope
				.find('[data-role="commentControls"]')
					.addClass('ipsFaded')
						.find('[data-action="approveComment"]')
							.addClass( 'ipsButton_disabled' )
							.text( ips.getString( 'commentApproving' ) );
		},

		/**
		 * Model returned success for approving the comment
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Data object from model
		 * @returns 	{void}
		 */
		approveCommentDone: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}

			var commentHtml = $('<div>' + data.response + '</div>').find('[data-controller="core.front.core.comment"]').html();

			// Remove moderated classes and update HTML
			this.scope
				.html( commentHtml )
				.removeClass('ipsModerated')
				.closest( '.ipsComment' )
					.removeClass('ipsModerated');

			// Let document know
			$( document ).trigger( 'contentChange', [ this.scope ] );

			// Set up multiquote in this comment
			if( ips.utils.db.isEnabled() ){
				this.scope.find('[data-action="multiQuoteComment"]').removeClass('ipsHide');
			}

			// And show a flash message
			ips.ui.flashMsg.show( ips.getString( 'commentApproved' ) );
		},

		/**
		 * Model returned an error for approving the comment
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Data object from model
		 * @returns 	{void}
		 */
		approveCommentError: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}

			window.location = data.url;
		},

		/**
		 * Handler for delete comment
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		deleteComment: function (e) {
			e.preventDefault();
			
			var self = this;
			var url = $( e.currentTarget ).attr('href');
			var commentData = this._getQuoteData();

			var eventData = _.extend( commentData, {
				url: url,
				commentID: this._commentID
			});
			
			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'warn',
				message: ips.getString('delete_confirm'),
				callbacks: {
					ok: function(){
						self.trigger( 'deleteComment.comment', eventData );
					}
				}
			});
		},

		/**
		 * Model event: delete comment finished
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		deleteCommentDone: function (e, data) {

			if( data.commentID != this._commentID ){
				return;
			}

			var deleteLink = this.scope.find('[data-action="deleteComment"]');

			// Stuff to HIDE elements on delete
			var toHide = null;
			var toShow = null;

			if( deleteLink.attr('data-hideOnDelete') ){
				toHide = this.scope.find( deleteLink.attr('data-hideOnDelete') );
			} else {
				toHide = this.scope.closest('article');
			}

			toHide.animationComplete( function () {
				toHide.remove();
			});

			ips.utils.anim.go( 'fadeOutDown', toHide );

			// Update count
			if ( deleteLink.attr('data-updateOnDelete') ) {
				$( deleteLink.attr('data-updateOnDelete') ).text( parseInt( $( deleteLink.attr('data-updateOnDelete') ).text() ) - 1 );
			}

			// Stuff to SHOW elements on delete
			if( deleteLink.attr('data-showOnDelete') ) {
				toShow = this.scope.find( deleteLink.attr('data-showOnDelete') );
				ips.utils.anim.go( 'fadeIn', toShow );
			}

			this.trigger( 'deletedComment.comment', {
				commentID: this._commentID,
				response: data.response
			});
		},

		/**
		 * Model event: delete comment failed
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		deleteCommentError: function (e, data) {
			if( data.commentID != this._commentID ){
				return;
			}

			window.location = data.url;
		},

		/**
		 * Prepares post data for quoting
		 *	
		 * @param 		{string} 	html 	Post contents
		 * @returns 	{string} 	Transformed post contents
		 */
		_prepareQuote: function (html) {
			
			/* Remove nested quotes */
			if( html.find('blockquote.ipsQuote') &&
				html.find('blockquote.ipsQuote').parent() && html.find('blockquote.ipsQuote').parent().get( 0 ) &&
				html.find('blockquote.ipsQuote').parent().get( 0 ).tagName == 'DIV' && html.find('blockquote.ipsQuote').siblings().length == 0 )
			{
				var div = html.find('blockquote.ipsQuote').closest('div');
				div.next('p').find("br:first-child").remove();
				div.remove();
			}
			else
			{
				html.find('blockquote.ipsQuote').remove();
			}
			
			/* Expand spoilers */			
			html.find('.ipsStyle_spoilerFancy,.ipsStyle_spoiler').replaceWith( ips.templates.render( 'core.posts.quotedSpoiler' ) );

			/* Remove data-excludequote (used for "edited by" byline presently, but can be used by anything) */
			html.find("[data-excludequote]").remove();
			
			/* Remove the citation */
			html.find('.ipsQuote_citation').remove();
			
			/* Set the quote value */
			html.find( '[data-quote-value]' ).each( function () {
				$( this ).replaceWith( '<p>' + $( this ).attr('data-quote-value') + '</p>' );
			});

			return html;
		},

		/**
		 * Parses the JSON object containing quote data for the comment
		 *	
		 * @returns 	{object} 	Quote data, or else an empty object
		 */
		_getQuoteData: function () {
			if( !this._quoteData ){
				try {
					this._quoteData = $.parseJSON( this.scope.attr('data-quoteData') );
					return this._quoteData;
				} catch (err) {
					Debug.log("Couldn't parse quote data");
					return {};
				}
			}

			return this._quoteData;
		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.commentFeed.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.commentFeed.js - Controller for a stream of comments (e.g. a topic, conversation, etc.)
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.commentFeed', {

		_overlay: null,
		_commentFeedID: 0,
		_newRepliesFlash: null,
		_maximumMultiQuote: 50, // Maximum number of items that can be multiquoted
		_pageParam: 'page',
		_seoPagination: false,
		_urlParams: {},
		_baseURL: '',
		_doneInitialState: false,
		_initialURL: '',

		// Polling vars
		_pollingEnabled: true, // Is polling enabled at all?
		_pollingActive: false, // Is polling running right now?
		_pollingPaused: false, // Have we paused polling?
		_initialPoll: 60000, // Our base polling frequency (1 minute)
		_currentPoll: 60000, // The current interval
		_decay: 20000, // Decay (amount added to interval on each false response)
		_maxInterval: ( 30 * 60 ) * 1000, // Maximum interval possible (30 mins)
		_pollingTimeout: null, // timeout obj
		_pollAjax: null, // ajax obj
		_pollOnUnpaused: false, // If true, when window is focused a poll will fire immediately
		_notification: null,
		_lastSeenTotal: 0,

		initialize: function () {
			this._containerID = this.scope.closest('[data-commentsContainer]').length ? this.scope.closest('[data-commentsContainer]').attr('data-commentsContainer') : this.scope.identify().attr('id');

			this.on( 'submit', '[data-role="replyArea"]', this.quickReply );
			this.on( 'quoteComment.comment', this.quoteComment );
			this.on( 'addMultiQuote.comment', this.addMultiQuote );
			this.on( 'removeMultiQuote.comment deleteComment.comment', this.removeMultiQuote );
			this.on( 'click', '[data-action="filterClick"]', this.filterClick );
			this.on( 'menuItemSelected', '[data-role="signatureOptions"]', this.signatureOptions );
			//this.on( 'change', '[data-role="moderation"]', this.selectRow );
			this.on( 'editorCompatibility', this.editorCompatibility );
			this.on( 'checkedComment.comment', this.checkedComment );

			this._boundMQ = _.bind( this.doMultiQuote, this );
			this._boundCMQ = _.bind( this.clearMultiQuote, this );
			
			$( document ).on( 'click', '[data-role="multiQuote_' + this._containerID + '"]', this._boundMQ );
			$( document ).on( 'click', '[data-action="clearQuoted_' + this._containerID + '"]', this._boundCMQ );
			$( document ).on( 'moderationSubmitted', this.clearLocalStorage );

			this.on( 'paginationClicked paginationJump', this.paginationClick );

			// Watch events on the document that are actually triggered from within this.quickReply
			this.on( document, 'addToCommentFeed', this.addToCommentFeed );
			this.on( 'deletedComment.comment', this.deletedComment );

			// Window events for polling purposes
			//$( window ).on( 'blur', _.bind( this.windowBlur, this ) );
			//$( window ).on( 'focus', _.bind( this.windowFocus, this ) );

			// Event we watch for on flash messages
			this.on( document, 'click', '[data-action="loadNewPosts"]', this.loadNewPosts );

			// Watch for state updates
			this.on( window, 'statechange', this.stateChange );
			// Since history.js doesn't fire statechange when the URL contains a hash, we need to
			// watch for anchorchange too, and then try and determine if we should respond
			this.on( window, 'anchorchange', this.anchorChange );

			// Socket events
			this.on(document, "socket.new_comment", this.handleSocketCommentTrigger);

			this.setup();
		},

		/**
		 * Setup method for comment feeds
		 *
		 * @returns {void}
		 */
		setup: function () {
			var self = this;
			var replyForm = this.scope.find('[data-role="replyArea"] form');
			
			this._commentFeedID = this.scope.attr('data-feedID');
			this._urlParams = this._getUrlParams();
			this._baseURL = this.scope.attr('data-baseURL');
			this._initialURL = window.location.href;
			this._currentPage = ips.utils.url.getPageNumber( this._pageParam );
			this._urlParams[ this._pageParam ] = this._currentPage;

			if( this._baseURL.match(/\?/) ) {
				if( this._baseURL.slice(-1) != '?' ){
					this._baseURL += '&';	
				}				
			} else {
				this._baseURL += '?';
			}

			if( replyForm.attr('data-noAjax') ){
				this._pollingEnabled = false;
			}

			if( !_.isUndefined( this.scope.attr('data-lastPage') ) && this._pollingEnabled ){
				this._startPolling();
			}

			$( document ).ready( function () {
				self._setUpMultiQuote();
				self._findCheckedComments();
			});
		},

        /**
         * Clear local storage after form is submitted
         *
         * @returns {void}
         */
        clearLocalStorage: function () {
            ips.utils.db.remove( 'moderation', $( document ).find("[data-feedID]").attr('data-feedID') );
        },

		/**
		 * Destroy method
		 *
		 * @returns {void}
		 */
		destroy: function () {
			$( document ).off( 'click', '[data-role="multiQuote_' + this._containerID + '"]', this._boundMQ );
			$( document ).off( 'click', '[data-action="clearQuoted_' + this._containerID + '"]', this._boundCMQ );
			this._stopPolling();
		},

		/**
		 * Returns an object containing URL parameters
		 *
		 * @returns {object}
		 */
		_getUrlParams: function () {
			var sort = this._getSortValue();
			var obj = {	
				sortby: sort.by || '',
				sortdirection: sort.order || '',
			};

			obj[ this._pageParam ] = ips.utils.url.getPageNumber( this._pageParam ) || 1;

			return obj;
		},

		/**
		 * Returns the current sort by and sort order value
		 *
		 * @returns {object}	Object containing by and order keys
		 */
		_getSortValue: function () {
			return { by: '', order: '' };
		},

		/**
		 * Responds to anchor changes triggered by History.js
		 * History.js doesn't fire statechange if the URL contains a hash - which impacts our 'last post' URL (e.g. #comment-123)
		 * So, we need to use anchorChange, see if the hash contains a comment anchor, and if so, find the most recent state
		 * with that anchor, then use that to reload the content. Fun and games!
		 * See https://github.com/browserstate/history.js/issues/276
		 *
		 * @returns {void}
		 */
		anchorChange: function () {
			var hash = History.getHash();
			var prevState = null;

			if( !hash.startsWith('comment') ){
				return;
			}

			// Find the most recent history with this hash
			var currentIndex = History.getCurrentIndex() - 1;

			// Search backwards through our state indexes to find the one with the matchng hash
			for( var i = currentIndex; i >= 0; i-- ){
				var tmpState = History.getStateByIndex( i );

				if( tmpState.url.indexOf('#' + hash) !== -1 ){
					prevState = tmpState;
					break;
				}
			}

			if( !prevState ){
				return;
			}

			// If we have a matching state, use that to reload the page
			this._urlParams = prevState.data;
			this._getResults( prevState.url );
		},

		/**
		 * Responds to state changes triggered by History.js
		 *
		 * @returns {void}
		 */
		stateChange: function () {
			var state = History.getState();

			// Make sure the state we're working with belongs to this controller/feed.
			// When our controllers request new pages, they provide a 'controller' param we use to identify which controller made the request.
			// However, on the initial page load, there is no controller state - meaning if a user clicks page 2, and then click the Back button,
			// there'll be no state stored for that initial page. Instead, what we do is try and determine if we should reload that inital page by
			// comparing it to the page URL we stored when the controller was first initialized.
			if( ( _.isUndefined( state.data.controller ) || state.data.controller != this.controllerID || state.data.feedID != this._commentFeedID ) ) {
				if( _.isUndefined( state.data.controller ) && state.url == this._initialURL ){
					// If there's no controller info in the state, but the state URL matches our initial URL, we'll reload that initial page content
					Debug.log("No controller state, but state URL matched initial URL");
				} else {
					return;
				}
			}

			// Update data
			this._urlParams = state.data;

			// Register page view
			ips.utils.analytics.trackPageView( state.url );

			// Get le new results
			// If the initial URL matches the URL for this state, then we'll load results by URL instead 
			// of by object (since we don't have an object for the URL on page load)
			if( this._initialURL == state.url ){
				this._getResults( state.url );
			} else {
				this._getResults();
			}
		},

		/**
		 * Fetches new results from the server, then calls this._updateTable to update the
		 * content and pagination. Simply redirects to URL on error.
		 *
		 * @param 	{string} 	[url] 	Optional URL to fetch the results from. If omitted
		 								 the URL will be built based on the current params object
		 * @returns {void}
		 */
		_getResults: function (url) {
			var self = this;
			var fetchURL = url || this._baseURL + this._getURL();

			this._setLoading( true );

			ips.getAjax()( fetchURL, {
				showLoading: true
			} )
				.done( _.bind( this._getResultsDone, this ) )
				.fail( _.bind( this._getResultsFail, this ) )
				.always( _.bind( this._getResultsAlways, this ) );
		},

		/**
		 * New results have finished loading
		 *
		 * @param 	{string}	Results HTML from ajax request
		 * @returns {void}		
		 */
		_getResultsDone: function (response) {
			var tmpElement = $( '<div>' + response + '</div>' ).find( '[data-feedID="' + this.scope.attr('data-feedID') + '"]' );
			var newContents = tmpElement.html();
			tmpElement.remove();

			this.cleanContents();
			//ips.ui.destructAllWidgets( this.scope );

			this.scope.hide().html( newContents );

			// Show content and hide loading
			ips.utils.anim.go( 'fadeIn', this.scope );
			this._overlay.hide();

			// Last page check:
			// Check whether we're on the last page, because if we are we need to start polling (or stop if not last page)
			var currentPageNo	= ips.utils.url.getPageNumber( this._pageParam, window.location.href );
			var lastPageNo		= this.scope.find('li.ipsPagination_last > a').first().attr('data-page');

			if( currentPageNo != lastPageNo ){
				this.scope.removeAttr( 'data-lastPage' );
				this._stopPolling();
			} else {
				this.scope.attr( 'data-lastPage', true );

				if( this._pollingEnabled ){
					this._currentPoll = this._initialPoll;
					this._startPolling();
				}
			}

			// Update multiquote, let document know, then highlight checked comments
			this._setUpMultiQuote();
			$( document ).trigger( 'contentChange', [ this.scope ] );
			this._findCheckedComments();
		},

		/**
		 * Callback when the results ajax fails
		 *
		 * @param 	{object} 	jqXHR			jQuery XHR object
		 * @param	{string} 	textStatus		Error message
		 * @param 	{string}	errorThrown
		 * @returns {void}
		 */
		_getResultsFail: function (jqXHR, textStatus, errorThrown) {
			if( Debug.isEnabled() ){
				Debug.error( "Ajax request failed (" + textStatus + "): " + errorThrown );
				Debug.error( jqXHR.responseText );
			} else {
				// rut-roh, we'll just do a manual redirect
				window.location = this._baseURL + this._getURL();
			}
		},

		/**
		 * Callback always called after ajax request to load results
		 *
		 * @returns {void}
		 */
		_getResultsAlways: function () {
			//
		},

		/**
		 * Callback always called after ajax request to load results
		 *
		 * @returns {void}
		 */
		_setLoading: function (status) {
			var scope = this.scope;
			var self = this;
			var commentFeed = this.scope.find('[data-role="commentFeed"]');

			if( status ){
				if( !this._overlay ){
					this._overlay = $('<div/>').addClass('ipsLoading').hide();
					ips.getContainer().append( this._overlay );
				}

				// Get dims & position			
				var dims = ips.utils.position.getElemDims( commentFeed );
				var position = ips.utils.position.getElemPosition( commentFeed );

				this._overlay.show().css({
					left: position.viewportOffset.left + 'px',
					top: position.viewportOffset.top + $( document ).scrollTop() + 'px',
					width: dims.width + 'px',
					height: dims.height + 'px',
					position: 'absolute',
					zIndex: ips.ui.zIndex()
				});

				commentFeed.animate({
					opacity: "0.5"
				});

				// Get top postition of feed
				var elemPosition = ips.utils.position.getElemPosition( this.scope );
				$('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' } );
			} else {
				// Stop loading
			}
		},

		/**
		 * Responds to a pagination click
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		paginationClick: function (e, data) {
			data.originalEvent.preventDefault();

			if( data.pageNo != this._urlParams[ this._pageParam ] ) {
				//var newObj = {};
				//newObj[ this._pageParam ] = data.pageNo;
				var urlObj = ips.utils.url.getURIObject( data.href );
				var queryKey = urlObj.queryKey;

				// If this is coming from a page jump, the page number may not be in the href passed
				// through. So check whether it exists, and manually add it to the object if needed.
				if( _.isUndefined( queryKey[ this._pageParam ] ) ){
					queryKey[ this._pageParam ] = data.pageNo;
				}
				
				this._seoPagination = data.seoPagination;

				// If we're now on the last page, start polling again
				// Removed 06/25/20 - This is now handled in _getResultsDone post-update so that it works both on clicks and statechanges
				/*if( data.lastPage && !this._pollingActive ){
					this.scope.attr( 'data-lastPage', true );
					this._currentPoll = this._initialPoll;
					this._startPolling();
				} else if( !data.lastPage ) {
					this.scope.removeAttr( 'data-lastPage' );
					this._stopPolling();
				}*/

				this._updateURL( queryKey );
			}
		},

		/**
		 * Pushes a new URL state to the browser
		 *	
		 * @param 		{object} 	newParams 	Object to be added to the state
		 * @returns 	{void}
		 */
		_updateURL: function (newParams) {

			// We don't insert a record into the history when the page loads. That means when a user
			// goes to page 1 -> page 2 then hits back, there's no record of 'page 1' in the history, and it doesn't work.
			// To fix that, we're tracking a 'doneInitialState' flag in this controller. The first time this method is called
			// and doneInitialState == false, we insert the *current* url into the stack before changing the URL. This gives
			// the History manager something to go back to when the user clicks back to the initial page.
			/*if( !this._doneInitialState ){
				History.replaceState(
					_.extend( _.clone( this._urlParams ), { controller: this.controllerID, feedID: this._commentFeedID, bypassState: true } ),
					document.title,
					document.location
				);

				this._doneInitialState = true;
			}*/

			_.extend( this._urlParams, newParams );

			var tmpStateData	= _.extend( _.clone( this._urlParams ), { controller: this.controllerID, feedID: this._commentFeedID } );
			var newUrl			= this._baseURL + this._getURL();

			if( newUrl.slice(-1) == '?' ){
				newUrl = newUrl.substring( 0, newUrl.length - 1 );
			}

			if ( this._seoPagination == true ) {
				newUrl = ips.utils.url.pageParamToPath( newUrl, this._pageParam, newParams[ this._pageParam ] );
			}

			History.pushState( 
				tmpStateData,
				document.title,
				newUrl
			);
		},

		/**
		 * Builds a param string from values in this._urlParams, excluding empty values
		 *
		 * @returns {string}	Param string
		 */
		_getURL: function () {
			var tmpUrlParams = {};

			for( var i in this._urlParams ){
				if( this._urlParams[ i ] != '' && i != 'controller' && i != 'feedID' && i != 'bypassState' && ( i != 'page' || ( i == 'page' && this._urlParams[ i ] > 1 ) ) ){
					tmpUrlParams[ i ] = this._urlParams[ i ];
				}
			}

			return $.param( tmpUrlParams );
		},

		/**
		 * An editor in this feed has indicated its compatibility
		 *
		 * @param	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		editorCompatibility: function (e, data) {
			if( !data.compatible ){
				this.triggerOn( 'core.front.core.comment', 'disableQuoting.comment' );
			}
		},

		/**
		 * A comment controller triggered an event indicating it was selected
		 * Adds the comment ID and actions to localStorage so it can be tracked across
		 * pages of the feed
		 *
		 * @param	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		checkedComment: function (e, data) {
			var dataStore = ips.utils.db.get( 'moderation', this._commentFeedID ) || {};

			if( data.checked ){
				if( _.isUndefined( dataStore[ data.commentID ] ) ){
					dataStore[ data.commentID ] = data.actions;
				}
			} else {
				delete dataStore[ data.commentID ];
			}

			// Store the updated value, or delete if it's empty  now
			if( _.size( dataStore ) ){
				ips.utils.db.set( 'moderation', this._commentFeedID, dataStore );	
			} else {
				ips.utils.db.remove( 'moderation', this._commentFeedID );
			}			
		},

		/**
		 * Called on setup, loops through the selected comments for this feedID from localstorage,
		 * and checks any that are present on this page. For others, instructs the pageAction
		 * widget to add the ID to its store manually so that they can still be worked with.
		 *
		 * @returns {void}
		 */
		_findCheckedComments: function () {
			// Bail if there's no checkboxes anyway
			if( !this.scope.find('input[type="checkbox"]').length ){
				return;
			}

			// Fetch the checked comments for this feedID
			var dataStore = ips.utils.db.get( 'moderation', this._commentFeedID ) || {};
			var self = this;
			var pageAction = this.scope.find('[data-ipsPageAction]');

			if( _.size( dataStore ) ){
				var sizeOtherPage = 0;

				_.each( dataStore, function (val, key) {
					if( self.scope.find('[data-commentID="' + key + '"]').length ){
						self.scope
							.find('[data-commentID="' + key + '"] input[type="checkbox"][data-role="moderation"]')
							.attr( 'checked', true )
							.trigger('change');
					} else {
						sizeOtherPage++;

						pageAction.trigger('addManualItem.pageAction', {
							id: 'multimod[' + key + ']',
							actions: val
						});
					}
				});

				if( this.scope.find('[data-ipsAutoCheck]') )
				{
					this.scope.find('[data-ipsAutoCheck]').trigger( 'setInitialCount.autoCheck', { count: sizeOtherPage } );
				}
			}
		},
	
		/**
		 * Options 
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */		
		signatureOptions: function (e, data) {
			data.originalEvent.preventDefault();

			if( data.selectedItemID == 'oneSignature' ){
				this._ignoreSingleSignature( $( e.currentTarget ).attr('data-memberID') );
			} else {
				this._ignoreAllSignatures();
			}
		},

		/**
		 * Fires a request to hide all signatures in the feed
		 *
		 * @returns 	{void}
		 */	
		_ignoreAllSignatures: function () {
			var self = this;
			var url = ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=settings&do=toggleSigs';
			var signatures = this.scope.find('[data-role="memberSignature"]');

			// Hide all signatures on the page
			signatures.slideUp();

			ips.getAjax()( url )
				.done( function (response) {
					ips.ui.flashMsg.show( ips.getString('signatures_hidden') );
					signatures.remove();
				})
				.fail( function () {
					signatures.show();

					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.getString('signatures_error'),
						callbacks: {}
					});
				});
		},

		/**
		 * Fires a request to hide a single signature (i.e. a single member's signature)
		 *	
		 * @param 		{number} 	memberID 	Member ID's signature to hide
		 * @returns 	{void}
		 */	
		_ignoreSingleSignature: function (memberID) {
			var self = this;
			var url = ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=ignore&do=ignoreType&type=signatures';
			var signatures = this.scope.find('[data-role="memberSignature"]').find('[data-memberID="' + memberID + '"]').closest('[data-role="memberSignature"]');

			signatures.slideUp();

			ips.getAjax()( url, {
				data: {
					member_id: parseInt( memberID )
				}
			})
				.done( function (response) {
					ips.ui.flashMsg.show( ips.getString('single_signature_hidden') );
					signatures.remove();
				})
				.fail( function () {
					signatures.show();

					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.getString('single_signature_error'),
						callbacks: {}
					});
				});
		},

		/**
		 * Filter click
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		filterClick: function(e) {
			e.preventDefault();

			var urlObj = ips.utils.url.getURIObject( $( e.target ).attr('href') );
			var queryKey = urlObj.queryKey;

			this._updateURL( queryKey );
		},

		/**
		 * Responds to a quote event from a comment controller
		 * Finds the reply box for this feed, and triggers a new event instructing the 
		 * editor to insert the quote
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object (which should contain all of the properties necessary for a quote)
		 * @returns 	{void}
		 */
		quoteComment: function (e, data) {
			ips.ui.editor.getObjWithInit( this.scope.find('[data-role="replyArea"] [data-ipsEditor]'), function(editor){
				editor.insertQuotes( [ data ] );
			} );
		},

		/**
		 * If the window blurs, we will pause polling, but if a poll is skipped, we'll immediately poll on window focus
		 *	
		 * @returns 	{void}
		 */
		windowBlur: function (e) {
			if( this._pollingEnabled ){
				Debug.log( 'Window blurred, pausing polling...' );
				this._pollingPaused = true;
			}
		},

		/**
		 * Window focus - if polling was paused and a poll was skipped, trigger it immediately now
		 *	
		 * @returns 	{void}
		 */
		windowFocus: function (e) {
			if( this._pollingEnabled && this._pollingPaused ){
				Debug.log( 'Window focused...' );

				this._pollingPaused = false;

				if( this._pollOnUnpaused ){
					this._pollOnUnpaused = false;
					this.pollForNewReplies();
				}
			}
		},

		/**
		 * Socket has told us there may be new replies, so we'll trigger a manual check here
		 */
		handleSocketCommentTrigger: function () {
			if( !_.isUndefined( this.scope.attr('data-lastPage') ) && this._pollingEnabled ){
				this.pollForNewReplies();
			}
		},

		/**
		 * Set a timeout for the new post polling process
		 *	
		 * @returns 	{void}
		 */
		_startPolling: function () {
			var self = this;
			this._pollingActive = true;

			Debug.log('Starting polling with interval ' + ( this._currentPoll / 1000 ) + 's' );

			this._pollingTimeout = setTimeout( function (){
				self.pollForNewReplies();
			}, this._currentPoll );
		},

		/**
		 * Clear the new post poll timeout
		 *	
		 * @returns 	{void}
		 */
		_stopPolling: function () {
			this._pollingActive = false;

			if( this._pollingTimeout ){
				clearTimeout( this._pollingTimeout );
			}

			Debug.log("Stopped polling for new replies in comment feed.");
		},
		
		/**
		 * Checks for new replies since we opened the page
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		pollForNewReplies: function () {					
			var self = this;
			var replyForm = this.scope.find('[data-role="replyArea"] form');
			var commentsOnThisPage = this.scope.find('[data-commentid]');

			if( !commentsOnThisPage.length ) {
				return;
			}

			var lastSeenId = this._getLastSeenID( commentsOnThisPage );
			var type = $( commentsOnThisPage[ commentsOnThisPage.length - 1 ] ).attr('data-commentType');

			if ( type.match( /-review$/ ) ) {
				Debug.log("Polling disabled for reviews");
				this._stopPolling();
				return;
			}
			
			if( this._pollingPaused ){
				Debug.log('Window blurred, delaying poll until focused...');
				this._pollOnUnpaused = true;
				return;
			}
			// Abort any running ajax
			if( this._pollAjax && !_.isUndefined( this._pollAjax.abort ) ){
				this._pollAjax.abort();
			}

			this._pollAjax = ips.getAjax();
			this._pollAjax( replyForm.attr('action'), {
				dataType: 'json',
				data: 'do=checkForNewReplies&type=count&lastSeenID=' + lastSeenId + '&csrfKey=' + ips.getSetting('csrfKey'),
				type: 'post'
			})
				.done( function (response) {

					// If auto-polling is now disabled, stop everything
					if( response.error && response.error == 'auto_polling_disabled' ){
						self._stopPolling();
						return;
					}

					if( parseInt( response.count ) > 0 ) {
						// Reset the poll interval
						self._currentPoll = self._initialPoll;
						self._buildFlashMsg( response );

						if( parseInt( response.totalCount ) > parseInt( self._lastSeenTotal ) ){
							self._buildNotifications( response );
							self._lastSeenTotal = parseInt( response.totalCount );
						}				
					} else {
						// Add 20 seconds to the poll interval, up to a max of 5 minutes
						if( ( self._currentPoll + self._decay ) < self._maxInterval ){
							self._currentPoll += self._decay;
						} else {
							self._currentPoll = self._maxInterval;
						}
					}

					// Start again if we're on the last page
					if( !_.isUndefined( self.scope.attr('data-lastPage') ) ){
						self._startPolling();
					}
				});
		},

		/**
		 * Builds a flash message to alert user of new posts
		 *	
		 * @param 		{object} 	response 		Information object
		 * @returns 	{void}
		 */
		_buildFlashMsg: function (response) {
			var html = '';
			var self = this;
			var itemsInFeed = this.scope.find('[data-commentid]').length;
			var spaceForMore = ( parseInt( response.perPage ) - itemsInFeed );

			// Build our flash message HTML
			if( parseInt( response.count ) > spaceForMore ) {
				html = ips.templates.render( 'core.postNotify.multipleSpillOver', {
					text: ips.pluralize( ips.getString( 'newPostMultipleSpillOver' ), [ response.totalNewCount ] ),
					canLoadNew: ( spaceForMore > 0 ),
					showFirstX: ips.pluralize( ips.getString( 'showFirstX' ), [ spaceForMore ] ),
					spillOverUrl: response.spillOverUrl
				});
			} else if( parseInt( response.count ) === 1 && !_.isUndefined( response.photo ) && !_.isUndefined( response.name ) ){
				html = ips.templates.render( 'core.postNotify.single', {
					photo: response.photo,
					text: ips.getString( 'newPostSingle', { name: response.name } )
				});
			} else {
				html = ips.templates.render( 'core.postNotify.multiple', {
					text: ips.pluralize( ips.getString( 'newPostMultiple' ), [ response.count ] )
				});
			}

			if( $('#elFlashMessage').is(':visible') && $('#elFlashMessage').find('[data-role="newPostNotification"]').length ){
				$('#elFlashMessage').find('[data-role="newPostNotification"]').replaceWith( html );
			} else {
				ips.ui.flashMsg.show( 
					html,
					{ 
						sticky: true, 
						position: 'bottom', 
						extraClasses: 'cPostFlash ipsPadding:half',
						dismissable: function () {
							self._stopPolling();
						},
						escape: false
					}
				);
			}
		},

		/**
		 * Builds browser notifications to alert users of new posts
		 *	
		 * @param 		{object} 	response 		Information object
		 * @returns 	{void}
		 */
		_buildNotifications: function (response) {
			var self = this;
			var hiddenProp = ips.utils.events.getVisibilityProp();

			// Build our browser notification if the window isn't in focus *and* we support them
			if( _.isUndefined( hiddenProp ) || !document[ hiddenProp ] || !ips.utils.notification.hasPermission() ){
				return;
			}

			var notifyData = {
				onClick: function (e) {

					// Try and focus the window (security settings may prevent it, though)
					try {
						window.focus();
					} catch (err) {}

					// And load in those posts
					self.loadNewPosts( e );
				}
			};		

			// If we already have a notification, then hide it
			if( self._notification ){
				self._notification.hide();
			}

			if( parseInt( response.count ) === 1 && !_.isUndefined( response.photo ) && !_.isUndefined( response.name ) ){
				notifyData = _.extend( notifyData, {
					title: ips.getString('notificationNewPostSingleTitle', {
						name: response.name
					}),
					body: ips.getString('notificationNewPostSingleBody', {
						name: response.name,
						title: response.title
					}),
					icon: response.photo
				});
			} else {
				notifyData = _.extend( notifyData, {
					title: ips.pluralize( ips.getString('notificationNewPostMultipleTitle'), [ response.count ] ),
					body: ips.pluralize( ips.getString('notificationNewPostMultipleBody', {
						title: response.title
					}), [ response.count ] )
				});
			}

			// Create the new one
			self._notification = ips.utils.notification.create( notifyData );
			self._notification.show();
		},

		/**
		 * Adds new replies to the display
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_importNewReplies: function () {
			var form = this.scope.find('[data-role="replyArea"] form');
			var commentsOnThisPage = this.scope.find('[data-commentid]');
			var _lastSeenID = this._getLastSeenID( commentsOnThisPage );
			var self = this;

			ips.getAjax()( form.attr('action'), {
				data: 'do=checkForNewReplies&type=fetch&lastSeenID=' + _lastSeenID + '&showing=' + commentsOnThisPage.length + '&csrfKey=' + ips.getSetting('csrfKey'),
				type: 'post'
			}).done( function (response) {
				// If we have more comments to load than we allow per page, then reload the page to show them plus the pagination
				if( commentsOnThisPage.length + parseInt( response.totalNewCount ) > response.perPage ){
					if( response.spillOverUrl ){
						window.location = response.spillOverUrl;
					} else {
						window.location.reload();
					}
				} else {
					if( _.isArray( response.content ) ) {
						_.each( response.content, function (item) {
							self.trigger( 'addToCommentFeed', {
								content: item,
								feedID: self._commentFeedID,
								resetEditor: false,
								totalItems: response.totalCount
							});
						});
					} else {
						self.trigger( 'addToCommentFeed', {
							content: response.content,
							feedID: self._commentFeedID,
							resetEditor: false,
							totalItems: response.totalCount
						});
					}
				}

				self._clearNotifications();
			});
		},

		/**
		 * Close the flash message for new post notifications
		 *
		 * @returns 	{void}
		 */
		_clearNotifications: function () {
			if( this._notification && _.isFunction( this._notification.hide() ) ){
				this._notification.hide();
			}

			if( $('#elFlashMessage').find('[data-role="newPostNotification"]').length ){
				$('#elFlashMessage').find('[data-role="newPostNotification"]').trigger('closeFlashMsg.flashMsg');
			}
		},

		/**
		 * Handles quick-reply functionality for this comment feed. Post the content via ajax,
		 * and trigger events to handle showing the new post (or redirecting to a new page)
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		quickReply: function (e) {
		
			var form = this.scope.find('[data-role="replyArea"] form');
			if ( form.attr('data-noAjax') ) {
				return;
			}
			
			e.preventDefault();
			e.stopPropagation();

			var self = this;
			var replyArea = this.scope.find('[data-role="replyArea"]');
			var submit = form.find('[type="submit"]');
			var autoFollow = this.scope.find('input[name$="auto_follow_checkbox"]');
			var commentsOnThisPage = this.scope.find('[data-commentid]');
			var _lastSeenID = this._getLastSeenID( commentsOnThisPage );

			// Set the form to loading
			var initialText = submit.text();
			submit
				.prop( 'disabled', true )
				.text( ips.getString('saving') );

			var page = ips.utils.url.getPageNumber( this._pageParam );

			if( !page ){
				page = 1;
			}
			
			this._clearNotifications();

			ips.getAjax()( form.attr('action'), {
				data: form.serialize() + '&currentPage=' + page + '&_lastSeenID=' + _lastSeenID,
				type: 'post'
			})
				.done( function (response) {
					if ( response.type == 'error' ) {
						if ( response.form ) {
							ips.ui.editor.getObj( replyArea.find('[data-ipsEditor]') ).destruct();
							form.replaceWith( $(response.form) );
							$( document ).trigger( 'contentChange', [ self.scope ] );
						} else {
							ips.ui.alert.show( {
								type: 'alert',
								icon: 'warn',
								message: response.message,
								callbacks: {}
							});
						}
					}
					else if( response.type == 'redirect' ) {
						self.paginationClick( e, {
							href: response.url,
							originalEvent: e
						});
					} else if( response.type == 'merge' ) {
						var comment = self.scope.find('[data-commentid="' + response.id + '"]');
						comment.find('[data-role="commentContent"]').html( response.content );
												
						if( comment.find('pre.prettyprint').length ){
							comment.find('pre.prettyprint').each( function () {
								$( this ).html( window.PR.prettyPrintOne( _.escape( $( this ).text() ) ) );
							});
						}
						
						ips.ui.editor.getObj( self.scope.find('[data-role="replyArea"] [data-ipsEditor]') ).reset();

						if( self.scope.find('[data-role="replyArea"] input[name="guest_name"]').length )
						{
							self.scope.find('[data-role="replyArea"] input[name="guest_name"]').val('');
						}
						form.find("[data-role='commentFormError']").each(function() {
						  $( this ).remove();
						});
						
						var container = comment.closest('.ipsComment');
						if ( container.length ) {
							ips.utils.anim.go( 'pulseOnce', container );
						} else {
							ips.utils.anim.go( 'pulseOnce', comment );
						}
						ips.ui.flashMsg.show( ips.getString('mergedConncurrentPosts') );
						
						$( document ).trigger( 'contentChange', [ self.scope ] );
					} else {

						/* add the datalayer event if we posted it */
						if ( response.postedByLoggedInMember ) {
							self.trigger( 'ipsDataLayer', { _key: 'content_comment', _properties: response.dataLayer } );
						}

						self.trigger( 'addToCommentFeed', {
							content: response.content,
							totalItems: response.total,
							feedID: self._commentFeedID,
							scrollToItem: true
						});
						
						if ( response.message ) {
							ips.ui.flashMsg.show( response.message );
						}
						
						ips.ui.editor.getObj( self.scope.find('[data-role="replyArea"] [data-ipsEditor]') ).reset();

						if( self.scope.find('[data-role="replyArea"] input[name="guest_name"]').length )
						{
							self.scope.find('[data-role="replyArea"] input[name="guest_name"]').val('');
							self.scope.find('[data-role="replyArea"] [data-ipsEditor]')
								.find('.ipsComposeArea_dummy')
									.hide()
								.end()
								.find('[data-role="mainEditorArea"]')
									.show()
								.end()
								.closest('.ipsComposeArea')
									.removeClass('ipsComposeArea_minimized')
									.find('[data-ipsEditor-toolList]')
										.show();
						}
						form.find("[data-role='commentFormError']").each(function() {
						  $( this ).remove();
						});

						// If the user is following this item, we can update the follow button too
						if( autoFollow.length ){
							self.trigger( 'followingItem', {
								feedID: self.scope.attr('data-feedID'),
								following: autoFollow.is(':checked')
							});
						}
					}
					
					self._clearNotifications();
				})
				.fail( function (jqXHR, textStatus, errorThrown) {
					if( Debug.isEnabled() ){
						Debug.error("Posting new reply failed: " + textStatus);
						Debug.log( jqXHR );
						Debug.log( errorThrown );
					} else {
						form.attr('data-noAjax', 'true');
						form.attr('action', form.attr('action') + ( ( ! form.attr('action').match( /\?/ ) ) ? '?failedReply=1' : '&failedReply=1' ) );
						form.submit();
					}
				})
				.always( function () {
					submit
						.prop( 'disabled', false )
						.text( initialText ? initialText : ips.getString('submit_reply') );
				});
		},

		/**
		 * Event handler for the 'load new posts' link in flash messages
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		loadNewPosts: function (e) {
			e.preventDefault();
			this._importNewReplies();
		},

		/**
		 * Responds to an event (trigger within this controller) indicating a new comment has been added
		 * Show it, and reset the contents of ckeditor
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		addToCommentFeed: function (e, data) {
			if( !data.content || data.feedID != this._commentFeedID ){
				return;
			}
			
			var textarea = this.scope.find('[data-role="replyArea"] textarea');
			var content = $('<div/>').append( data.content );
			var comment = content.find('.ipsComment');

			var commentFeed = this.scope.find('[data-role="commentFeed"]');

			if( commentFeed.find('[data-role="moderationTools"]').length ){
				commentFeed = commentFeed.find('[data-role="moderationTools"]');
			}

			// Hide the 'no comment' text
			this.scope.find('[data-role="noComments"]').remove();

			// Add comment content
			commentFeed.append( comment.css({ opacity: "0.001" }) );

			var newItemTop = comment.offset().top;
			var windowScroll = $( window ).scrollTop();
			var viewHeight = $( window ).height();

			var _showComment = function () {
				comment.css({ opacity: "1" });
				ips.utils.anim.go( 'fadeInDown', comment.filter(':not(.ipsHide)') );
			};

			// If needed, scroll to the correct location before showing the comment
			if( !_.isUndefined( data.scrollToItem ) && data.scrollToItem && ( newItemTop < windowScroll || newItemTop > ( windowScroll + viewHeight ) ) ){
				$('html, body').animate( { scrollTop: newItemTop + 'px' }, 'fast', function () {
					setTimeout( _showComment, 100 ); // Short delay before fading in comment for pleasantness
				} );
			} else {
				_showComment();
			}			

			if( _.isUndefined( data.resetEditor ) || data.resetEditor !== false ){
				ips.ui.editor.getObj( this.scope.find('[data-role="replyArea"] [data-ipsEditor]') ).reset();
			}			

			if( ips.utils.db.isEnabled() ){
				var buttons = comment.find('[data-action="multiQuoteComment"]');

				buttons.hide().removeClass('ipsHide');
				ips.utils.anim.go('fadeIn', buttons);
			}
						
			this._updateCount(data.totalItems);

			$( document ).trigger( 'contentChange', [ this.scope ] );
		},
		
		/**
		 * Responds to an event indicating thay a comment has been deleted
		 * Show it, and reset the contents of ckeditor
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		deletedComment: function( e, data ) {
			data = $.parseJSON( data.response );
			var self = this;

			if( data.type == 'redirect' ) {
				window.location = data.url;
			}
			else {
				this._updateCount( data.total );
			}

		},
		
		/**
		 * Update comment count
		 *
		 * @param	{int}	newTotal	The new total
		 * @returns 	{void}
		 */
		_updateCount: function(newTotal) {
			if ( this.scope.find('[data-role="comment_count"]') ) {
				var langString = 'js_num_comments';
				if ( this.scope.find('[data-role="comment_count"]').attr('data-commentCountString') ) {
					langString = this.scope.find('[data-role="comment_count"]').attr('data-commentCountString');
				}
				this.scope.find('[data-role="comment_count"]').text( ips.pluralize( ips.getString( langString ), newTotal ) );
			}
		},

		/**
		 * Event handler for the 'Quote x posts' button in multiquote popup
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		doMultiQuote: function (e) {

			var mqData = this._getMultiQuoteData();
			var replyArea = this.scope.find('[data-role="replyArea"]');
			var output = [];
			var self = this;

			if( !_.size( mqData ) || !replyArea.is(':visible') ){
				return;
			}

			// Build quote array and trigger event for the editor widget to deal with
			_.each( mqData, function (value){
				output.push( value );
			});

			ips.ui.editor.getObjWithInit( this.scope.find('[data-role="replyArea"] [data-ipsEditor]'), function(editor) {
				editor.insertQuotes( output );
			});

			this._removeAllMultiQuoted();
		},

		/**
		 * Event handler for the 'clear' button in the multiquote popup
		 * Simply calls _removeAllMultiQuoted to do the clear
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		clearMultiQuote: function (e) {
			e.preventDefault();
			this._removeAllMultiQuoted();
		},

		/**
		 * Removes all quoted posts
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		_removeAllMultiQuoted: function () {
			var mqData = this._getMultiQuoteData();
			var self = this;
			
			// Delete all the multi-quoted posts from DB
			ips.utils.db.set( 'mq', 'data', {} );

			// Hide popup
			this._buildMultiQuote(0);

			if( !_.size( mqData ) ){
				return;
			}

			// Loop through each quoted posts and see if it exists on this page by building a selector,
			// then updating classnames on elements that match it
			_.each( mqData, function (value) {
				self.triggerOn( 'core.front.core.comment', 'setMultiQuoteDisabled.comment', {
					contentapp: value.contentapp,
					contenttype: value.contenttype,
					contentcommentid: value.contentcommentid
				});
			});
		},

		/**
		 * Responds to an addMultiQuote event
		 * Adds the provided post data to the multiquote DB entry and updates the popup
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		addMultiQuote: function (e, data) {
			var mqData = this._getMultiQuoteData();
			var key = data.contentapp + '-' + data.contenttype + '-' + data.contentcommentid;
			
			// Have we hit a limit?
			if( _.size( mqData ) == this._maximumMultiQuote )
			{
				ips.ui.alert.show( {
					type: 'alert',
					icon: 'warn',
					message: ips.pluralize( ips.getString( 'maxmultiquote' ), this._maximumMultiQuote ),
					callbacks: {
						ok: function () {
							$("button[data-mqId='" + data.button + "']").removeClass('ipsButton_alternate')
								.addClass('ipsButton_simple')
								.removeAttr('data-mqActive')
								.html( ips.templates.render('core.posts.multiQuoteOff') );
						}
					}
				});
				return false;
			}

			mqData[ key ] = data;

			ips.utils.db.set( 'mq', 'data', mqData );

			this._buildMultiQuote( _.size( mqData ) );
		},

		/**
		 * Responds to a removeMultiQuote event
		 * Removes the provided post data from the multiquote DB entry and updates the popup
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		removeMultiQuote: function (e, data) {
			var mqData = this._getMultiQuoteData();
			var key = data.contentapp + '-' + data.contenttype + '-' + data.contentcommentid;

			if( !_.isUndefined( mqData[ key ] ) ){
				mqData = _.omit( mqData, key );

				ips.utils.db.set( 'mq', 'data', mqData );

				this._buildMultiQuote( _.size( mqData ) );
			}
		},

		/**
		 * Returns the current multiquote data from the localStorage
		 *	
		 * @returns 	{object} 	Multiquote data from localStorage
		 */
		_getMultiQuoteData: function () {
			// Get the IDs we already have saved
			var mqData = ips.utils.db.get('mq', 'data');

			if( _.isUndefined( mqData ) || !_.isObject( mqData ) ){
				return {};
			}

			return mqData;
		},

		/**
		 * Called when the controller is initialized
		 * Checks whether there's any mq data, and shows the popup if so
		 *	
		 * @returns 	{void}
		 */
		_setUpMultiQuote: function () {

			if( !ips.utils.db.isEnabled() ){
				return;
			}

			var buttons = this.scope.find('[data-action="multiQuoteComment"]');
			var self = this;
			var mqData = this._getMultiQuoteData();

			buttons.show();

			if( _.size( mqData ) ){
				this._buildMultiQuote( _.size( mqData ) );

				// Loop through each quoted posts and see if it exists on this page by building a selector,
				// then updating classnames on elements that match it
				_.each( mqData, function (value) {
					self.triggerOn( 'core.front.core.comment', 'setMultiQuoteEnabled.comment', {
						contentapp: value.contentapp,
						contenttype: value.contenttype,
						contentcommentid: value.contentcommentid
					});
				});
			}			
		},

		/**
		 * Builds the multiquote popup, either from a template if this is the first time,
		 * or updates the value if it already exists.
		 *	
		 * @param 		{number} 	count 		Count of quoted posts
		 * @returns 	{void}
		 */
		_buildMultiQuote: function (count) {
			var quoterElem = $('#ipsMultiQuoter');

			if( !quoterElem.length && count ){
				ips.getContainer().append( ips.templates.render('core.posts.multiQuoter', {
					count: ips.getString('multiquote_count', {
						count: ips.pluralize( ips.getString( 'multiquote_count_plural' ), [ count ] )
					}),
					commentFeedId: this._containerID
				}));

				ips.utils.anim.go( 'zoomIn fast', $('#ipsMultiQuoter') );
			} else {

				// Since we only show one global multiquote button (not per-feed), the existing button
				// might have been created/shown by another feed on the same page. We'll assume the latest 
				// comment feed to show is the one the user cares about, and update the button attributes 
				// so that this controller properly handles clicks on it.
				if( quoterElem.attr('data-commentsContainer') !== this._containerID ){
					quoterElem
						.attr('data-commentsContainer', this._containerID )
						.find('[data-role^="multiQuote_"]')
							.attr('data-role', 'multiQuote_' + this._containerID)
						.end()
						.find('[data-action^="clearQuoted_"]')
							.attr('data-action', 'clearQuoted_' + this._containerID);
				}

				quoterElem.find('[data-role="quotingTotal"]').text( 
					ips.pluralize( ips.getString( 'multiquote_count_plural' ), [ count ] )
				);

				if( count && quoterElem.is(':visible') ){
					ips.utils.anim.go( 'pulseOnce fast', quoterElem );	
				} else if( count && !quoterElem.is(':visible') ){
					ips.utils.anim.go( 'zoomIn fast', quoterElem );
				} else {
					ips.utils.anim.go( 'zoomOut fast', quoterElem );
				}
				
			}
		},

		/**
		 * Returns the largest ID number on the page, taking into account the fact that the topic
		 * wrapper may specify a higher ID (in cases where a question's answer is on a different page)
		 *	
		 * @param 		{array} 	commentsOnThisPage 		Array of comments on this page, based on having the [data-commentid] attribute
		 * @returns 	{number}
		 */
		_getLastSeenID: function (commentsOnThisPage) {
			var commentFeed = this.scope.find('[data-role="commentFeed"]');
			var maxComment = _.max( commentsOnThisPage, function (comment) {
				return parseInt( $( comment ).attr('data-commentid') );
			});
			var max = $( maxComment ).attr('data-commentid');

			// If the topic feed 
			if( commentFeed.attr('data-topicAnswerID') && parseInt( commentFeed.attr('data-topicAnswerID') ) > max ){
				max = parseInt( commentFeed.attr('data-topicAnswerID') );
			}

			Debug.log("Max comment ID is " + max);
			return max;
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.commentsWrapper.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.commentWrapper.js
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.commentsWrapper', {
		
		initialize: function () {
			this.on( document, 'addToCommentFeed', this.addToCommentFeed );
			this.on( 'deletedComment.comment', this.deletedComment );
		},
		
		/**
		 * Responds to an event (trigger within this controller) indicating a new comment has been added
		 * Show it, and reset the contents of ckeditor
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		addToCommentFeed: function(e, data) {
			this._updateCount( $(e.target).attr('data-commentsType'), data.totalItems );
		},
		
		/**
		 * Responds to an event indicating thay a comment has been deleted
		 * Show it, and reset the contents of ckeditor
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		deletedComment: function(e, data) {
			try {
				var newTotal = $.parseJSON( data.response ).total;
			} catch (err) {
				var newTotal = 0;
			}

			this._updateCount( $(e.target).closest('[data-commentsType]').attr('data-commentsType'), newTotal );
		},
		
		/**
		 * Update comment count
		 *
		 * @param	{int}	newTotal	The new total
		 * @returns 	{void}
		 */		
		_updateCount: function( type, number ) {
			var langString = 'js_num_' + type;
			var elem = $( '#' + $(this.scope).attr('data-tabsId') + '_tab_' + type );
			elem.text( ips.pluralize( ips.getString( langString ), number ) );
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.contentMessage.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100">/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.contentMessage
 *
 * Author: Matt Mecham
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.contentMessage', {

		initialize: function () {
			this.on( 'change', '#check_message_is_public', this.updateEditorBorder );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			this.scope.find('.ipsComposeArea_editor').addClass('cContentMessageEditor');
			this.updateEditorBorder();
		},

		/**
		 * Add a context sensitive border around the editor
		 *
		 * @returns {void}
		 */
		updateEditorBorder: function () {
			if ( $('#check_message_is_public_wrapper').hasClass('ipsToggle_on') ) {
				this.scope.find('.ipsComposeArea_editor').removeClass('cContentMessageEditor--private').addClass('cContentMessageEditor--public');
			} else {
				this.scope.find('.ipsComposeArea_editor').removeClass('cContentMessageEditor--public').addClass('cContentMessageEditor--private');
			}
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.dataLayer.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community v4+
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.dataLayer.js - dataLayer support.
 *
 * Author: Matt Finger
 */
;( function($, _, undefined){
    "use strict";

    ips.controller.register('core.front.core.dataLayer', {
        eventHandlers: [],
        propertiesHandlers: [],

        initialize: function () {
            if ( this.verify() ) {
                // Subscribe our closure to handle events
                this.on('ipsDataLayer', this.handleEvent);

                // Subscribe our closure to handle properties
                this.on('ipsDataLayerProperties', this.handleProperties);

                this.setup();
                Debug.log( 'Pushing events to the dataLayer' );
            }
        },

        setup: function() {
            // Get our event handlers
            for ( let i in IpsDataLayerEventHandlers ) {
                let handler = IpsDataLayerEventHandlers[i];
                if ( handler instanceof Function ) {
                    try {
                        let callback = handler();
                        if ( callback instanceof Function ) {
                            this.eventHandlers.push( callback );
                        } else {
                            Debug.error( `Invalid Handler Callback Found: Returned value of type '${typeof (callback || undefined)}', expected a callback Function!` );
                        }
                    } catch (e) {
                        Debug.error(e);
                    }
                }
            }

            // Get our property handlers
            for ( let i in IpsDataLayerPropertiesHandlers ) {
                let handler = IpsDataLayerPropertiesHandlers[i];
                if ( handler instanceof Function ) {
                    try {
                        let callback = handler();
                        if ( callback instanceof Function ) {
                            this.propertiesHandlers.push( callback );
                        } else {
                            Debug.error( `Invalid Handler Callback Found: Returned value of type '${typeof (callback || undefined)}', expected a callback Function!` );
                        }
                    } catch (e) {
                        Debug.error(e);
                    }
                }
            }

            this.unsetOldUniqueKeys();

            // Our login/logout event. Delay slightly to prevent collisions with the handler's head script (events pushed to GTM before GTM is loaded might not be recognized properly)
            setTimeout( _.bind( this.loginLogout, this ), 200 );

            //Events included in the page. Delay slightly to prevent collisions with the handler's head script (events pushed to GTM before GTM is loaded might not be recognized properly)
            setTimeout( _.bind( this.handleInitialEvents, this ), 202 );

            // Some interactions (like downloading a file) need to be verified a second or so after they occur
            this.on( 'ipsDataLayerSync', this.remoteFetchEvents );

            this.scope.find('[data-datalayer-postfetch]').on( 'click', function(e) {
                setTimeout( function() { $(e.currentTarget).trigger( 'ipsDataLayerSync' ); }, 1500 );
            } );
        },

        /**
         * Handle events
         *
         * @return  void
         */
        handleEvent: function(evt, event) {
            let Events = IpsDataLayerConfig._events;
            let Properties = IpsDataLayerConfig._properties;
            let PII = IpsDataLayerConfig._pii;

            // This method will do nothing if we don't recognize events OR have no handlers to use
            if (!Object.keys(Events).length || !this.eventHandlers.length) {
                return;
            }

            try {
                if (event._key && event._properties instanceof Object) {
                    // Make sure its enabled
                    if (!Events[event._key] || !Events[event._key].enabled) return;

                    // Check for duplicate keys
                    if (event._uniquekeys && event._uniquekeys instanceof Object) {
                        try {
                            let same = true;
                            let saved = ips.utils.db.get('ipsDataLayer', event._key) || {};
                            saved.exp = saved.exp || 0;
                            event._uniquekeys.exp = event._uniquekeys.exp || (Math.floor(Date.now() / 1000) + 300);

                            // If the saved keys to check are expired, this is not a duplicate
                            if ((saved.exp) <= Math.floor(Date.now() / 1000)) {
                                same = false;
                            }

                            // Not expired, compare each key
                            if (same) {
                                for (let savedKey in saved) {
                                    if (savedKey === 'exp') {
                                        continue;
                                    }
                                    if (saved[savedKey] !== event._uniquekeys[savedKey]) {
                                        same = false;
                                        break;
                                    }
                                }
                            }

                            // If this is the same as a previous one, update the expiration and stop processing
                            if (same) {
                                saved.exp = Math.max(event._uniquekeys.exp, saved.exp);
                                ips.utils.db.set('ipsDataLayer', event._key, saved, false);
                                return;
                            }

                            // Otherwise save now
                            ips.utils.db.set('ipsDataLayer', event._key, event._uniquekeys, false);
                        } catch (e) {
                            console.log(e);
                        }
                    }

                    // Create the event
                    let _event = {
                        '_key': Events[event._key].formatted_name,
                        '_properties': {}
                    };

                    // Go through all properties we know
                    for (let propertyKey in Properties) {

                        // We don't care if it's disabled or it has pii and pii is not allowed
                        let property = Properties[propertyKey];
                        if (property.enabled && !(!PII && property.pii)) {

                            // Skip if this property shouldn't be used with this event
                            let validForEvent = false;
                            for (let j in property.event_keys) {
                                let pattern = property.event_keys[j].replaceAll('*', '.*');
                                if (event._key.match(pattern)) {
                                    validForEvent = true;
                                    break;
                                }
                            }
                            if (!validForEvent) {
                                continue;
                            }

                            // What value should we use?
                            let formatted = property.formatted_name;
                            let value = '';

                            // This must be unique so we generate it client side to avoid using a cached key
                            if (propertyKey === 'ips_key') {
                                value = this.uniqueId();
                            } else if ( Object.keys( event._properties ).includes(propertyKey) ) {
                                value = event._properties[propertyKey];
                            } else if (property.custom) {
                                value = (property.value === null || property.value === undefined) ? undefined : property.value;
                            } else if (propertyKey === 'ips_time') { // Add the time if needed
                                value = Math.floor(Date.now() / 1000);
                            } else if (IpsDataLayerContext[formatted]) {
                                value = IpsDataLayerContext[formatted];
                            } else {
                                value = property.default || undefined;
                            }

                            // Some shallow type-checking, set to undefined if its type isn't allowed
                            if (value !== null && value !== undefined) {
                                let types = property.type.toLowerCase().split(' ');
                                if (types.includes('number')) {
                                    try {
                                        if (!isNaN(value)) {
                                            value = Number(value);
                                        }
                                    } catch (e) {
                                    }
                                }

                                // If this type an array but we got an empty object, there's a chance PHP JSON encoded an empty array as associative, so make it an empty array
                                if (typeof value === 'object' &&
                                    Object.keys(value).length === 0 &&
                                    Object.getPrototypeOf(value) === Object.prototype &&
                                    types.includes('array')) {
                                    value = [];
                                } else if (!( // If this is not instanceof array and arrays are allowed nor any other type specified, make it undefined
                                    (types.includes('array') && value instanceof Array) || types.includes(typeof value)
                                )) {
                                    Debug.error(`Invalid Data Layer Property Type: Event property "${propertyKey}" was overridden to undefined because it could not be cast from "${typeof value}" to an allowed type`);
                                    value = undefined;
                                }
                            } else {
                                value = undefined;
                            }

                            // Actually set the property
                            _event._properties[formatted] = value;
                        }
                    }

                    // Call each handler on this event
                    Debug.log('Pushing an event to the Data Layer Event Handlers');
                    Debug.log(_event)
                    for (let i in this.eventHandlers) {
                        try {
                            this.eventHandlers[i](_event);
                        } catch (e) {
                            Debug.error('Bad Data Layer Event Handler: An event handler failed to handle an event!');
                        }
                    }
                }
            } catch (e) {
                Debug.log(e);
            }
        },

        /**
         * Handle our properties using the callbacks
         *
         * @return Function
         */
        handleProperties: function(evt, event) {
            let Properties  = IpsDataLayerConfig._properties;
            let PII         = IpsDataLayerConfig._pii;

            if ( this.propertiesHandlers.length && event._properties instanceof Object) {
                let properties = {};
                for ( let propertyKey in event._properties ) {
                    let property = Properties[propertyKey];

                    // Filter out properties if we don't know them, the have to be tied to an event, or contain PII and pii is not allowed
                    if ( !( property || property === null ) || !property.enabled || !property.page_level || (!PII && property.pii) ) {
                        continue;
                    }

                    // Some shallow type-checking, set to undefined if its type isn't allowed
                    let value = event._properties[propertyKey];
                    if ( value !== null && value !== undefined ) {
                        let types = property.type.toLowerCase().split(' ');
                        if ( types.includes('number') ) {
                            try {
                                if ( !isNaN(value) ) {
                                    value = Number(value);
                                }
                            } catch (e) {}
                        }

                        // If this type an array but we got an empty object, there's a chance PHP JSON encoded an empty array as associative, so make it an empty array here
                        if (typeof value === 'object' &&
                            Object.keys(value).length === 0 &&
                            Object.getPrototypeOf(value) === Object.prototype &&
                            types.includes('array')) {
                            value = [];
                        } else if (!( // If this is not instanceof array and arrays are allowed nor any other type specified, make it undefined
                            (types.includes('array') && value instanceof Array) || types.includes(typeof value)
                        )) {
                            Debug.error(`Invalid Data Layer Property Type: Property "${propertyKey}" was overridden to undefined because it could not be cast from "${typeof value}" to an allowed type`);
                            value = undefined;
                        }
                    } else {
                        value = undefined;
                    }

                    properties[propertyKey] = value;
                }

                if ( Object.keys( properties ).length ) {
                    Debug.log( 'Pushing properties to the Data Layer Properties Handlers' );
                    Debug.log( properties );
                    for ( let i in this.propertiesHandlers ) {
                        try {
                            this.propertiesHandlers[i](properties);
                        } catch (e) {
                            Debug.error( 'Bad Data Layer Properties Handler: A properties handler failed to handle a collection (object) of properties' );
                        }
                    }
                }
            }
        },

        handleInitialEvents: function() {
            let self = this;
            IpsDataLayerEvents.forEach( function ( event ) {
                self.handleEvent( {}, event );
            } );
        },

        /**
         *  Verify the correct constants exist with the appropriate properties, does not validate them though
         */
        verify: function() {
            try {
                return (
                    // If these are const, then they will exist but not as a property of window
                    (!window.IpsDataLayerContext && IpsDataLayerContext) &&
                    (!window.IpsDataLayerConfig && IpsDataLayerConfig) &&
                    (!window.IpsDataLayerEventHandlers && IpsDataLayerEventHandlers) &&
                    (!window.IpsDataLayerPropertiesHandlers && IpsDataLayerPropertiesHandlers) &&
                    IpsDataLayerContext instanceof Object &&
                    IpsDataLayerEvents instanceof Array &&
                    IpsDataLayerEventHandlers instanceof Array &&
                    IpsDataLayerPropertiesHandlers instanceof Array &&
                    IpsDataLayerConfig instanceof Object &&
                    IpsDataLayerConfig._properties instanceof Object &&
                    IpsDataLayerConfig._events instanceof Object &&
                    IpsDataLayerConfig._pii !== undefined
                );
            } catch (e) {
                return false;
            }
        },

        /**
         * Add a login/logout event if needed. This checks local storage to see if they just logged in, or if they were just logged in
         */
        loginLogout: function() {
            // What is stored in local storage
            let stored = ips.utils.db.get( 'ipsDataLayer', 'login' ) || {};

            if ( ipsSettings.memberID ) {
                // If this is 0, not null, we know the current use viewed a page as a guest
                if ( stored.logged_in === 0 ) {
                    this.scope.trigger('ipsDataLayer', { _key: 'account_login', _properties: {} });
                }

                // Flag they visited logged in
                ips.utils.db.set( 'ipsDataLayer', 'login', {logged_in : 1}, false )
            } else {
                // If this is 1, we know they viewed a page as a logged in member
                if ( stored.logged_in === 1 ) {
                    this.scope.trigger('ipsDataLayer', {_key: 'account_logout', _properties: {} });
                }

                // Flag they visited logged out
                ips.utils.db.set( 'ipsDataLayer', 'login', {logged_in: 0}, false );
            }
        },

        /**
         * Create a pseudo-random (practically unique) id similar to php's uniqueid() method
         */
        uniqueId: function() {
            let s = i => {
                return Math.floor((1 + Math.random()) * Math.pow(16, i))
                    .toString(16)
                    .substring(1);
            };

            let time = Date.now();
            let sec = Math.floor(time / 1000);
            let secString = sec.toString(16);
            secString = secString.substring( secString.length - 8 );
            let ms  = ( time - ( sec * 1000 ) ) * 1000; // micro
            let msString = (ms + 0x100000).toString(16).substring(1);

            return secString + msString + s(1) + '.' + s(4) + s(4);
        },

        /**
         * Unsets the out-dated event keys so they don't remain in persistent user storage indefinitely
         */
        unsetOldUniqueKeys: function() {
            let Events = IpsDataLayerConfig._events;
            for( let eventKey in Events ) {
                let saved = ips.utils.db.get('ipsDataLayer', eventKey) || {};
                if ( Object.keys(saved).length ) {
                    Debug.log( `Found stored unique keys for the event '${eventKey}'` );
                    Debug.log( saved );

                    if ( !saved.exp || saved.exp <= (Date.now() / 1000) ) {
                        Debug.log( `Removing expired stored unique keys for the event '${eventKey}'` );
                        ips.utils.db.set('ipsDataLayer', eventKey, 0, false);
                    }
                }
            }
        },

        /**
         * Get events from the ajax endpoint
         */
        remoteFetchEvents: function() {
            let self = this;
            ips.getAjax()( '?app=core&module=system&controller=ajax&do=getDataLayerEvents')
                .done( function(response) {
                    for ( let i in response ) {
                        self.handleEvent( {}, response[i] );
                    }
                } );
        },

    });
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.followButton.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.followButton.js - Controller for follow button
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.followButton', {

		initialize: function () {
			this.setup();
			this.on( document, 'followingItem', this.followingItemChange );
		},

		setup: function () {
			this._app = this.scope.attr('data-followApp');
			this._area = this.scope.attr('data-followArea');
			this._id = this.scope.attr('data-followID');
			this._feedID = this._area + '-' + this._id;
			this._button = this.scope.find('[data-role="followButton"]');
		},

		/**
		 * Responds to events indicating the follow status has changed
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		followingItemChange: function (e, data) {
			if( data.feedID == this._feedID ){
				this._reloadButton();
			}
		},

		/**
		 * Gets a new follow button from the server and replaces the current one with the response
		 *
		 * @returns 	{void}
		 */
		_reloadButton: function () {
			// Show button as loading
			this._button.addClass('ipsFaded ipsFaded_more');
			
			var self = this;
			var pos = ips.utils.position.getElemPosition( this._button );
			var dims = ips.utils.position.getElemDims( this._button );

			this.scope.append( ips.templates.render('core.follow.loading') );

			// Adjust sizing
			this.scope
				.css({
					position: 'relative'
				})
				.find('.ipsLoading')
					.css({
						width: dims.outerWidth + 'px',
						height: dims.outerHeight + 'px',
						top: "0",
						left: "0",
						position: 'absolute',
						zIndex: ips.ui.zIndex()
					});

			// Load new contents
			ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=notifications&do=button', {
				data: _.extend({
					follow_app: this._app,
					follow_area: this._area,
					follow_id: this._id
				}, ( this.scope.attr('data-buttonType') ) ? { button_type: this.scope.attr('data-buttonType') } : {} )
			})
				.done( function (response) {
					self.scope.html( response );
					$( document ).trigger( 'contentChange', [ self.scope ] );
					
					/* Any auto follow toggles on the page? */
					if ( $('input[data-toggle-id="auto_follow_toggle"]').length ) {
						var val = self.scope.find('[data-role="followButton"]').attr('data-following');
						if ( val == 'false' && $('input[data-toggle-id="auto_follow_toggle"]').is(':checked') ) {
							$('input[data-toggle-id="auto_follow_toggle"]').prop('checked', false).change();
						} else if (val == 'true' && ! $('input[data-toggle-id="auto_follow_toggle"]').is(':checked') ){
							$('input[data-toggle-id="auto_follow_toggle"]').prop('checked', true).change();
						}
					}
				})
				.fail( function () {
					self._button.removeClass('ipsFaded ipsFaded_more');
				})
				.always( function () {
					self.scope.find('.ipsLoading').remove();
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.followForm.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.followButton.js - Controller for follow button
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.followForm', {

		initialize: function () {
			this.on( 'submit', this.submitForm );
			this.on( 'click', '[data-action=&quot;unfollow&quot;]', this.unfollow );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this._app = this.scope.attr('data-followApp');
			this._area = this.scope.attr('data-followArea');
			this._id = this.scope.attr('data-followID');
		},

		/**
		 * Event handler for unfollowing an item
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		unfollow: function (e) {
			e.preventDefault();
			this._doFollowAction( $( e.currentTarget ).attr('href'), {}, true );
		},

		/**
		 * Event handler for submitting the follow form
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		submitForm: function (e) {
			e.preventDefault();
			this._doFollowAction( this.scope.attr('action'), this.scope.serialize(), false );
		},

		/**
		 * Performs an ajax action. Shows the hovercard as loading, and calls the URL
		 *
		 * @param 		{string} 	url		URL to call
		 * @param 		{object} 	data 	Object of data to include in the request
		 * @returns 	{void}
		 */
		_doFollowAction: function (url, data, unfollow) {
			var self = this;
			var dims = ips.utils.position.getElemDims( this.scope.parent('div') );

			// Set it to loading
			this.scope
				.hide()
				.parent('div')
					.css({
						width: dims.outerWidth + 'px',
						height: dims.outerHeight + 'px'
					})
					.addClass('ipsLoading');

			// Update follow preference via ajax
			ips.getAjax()( url, {
				data: data,
				type: 'post'
			})
				.done( function (response) {
					// Success, so trigger event to update button
					if( unfollow ){
						self.trigger('followingItem', {
							feedID: self._area + '-' + self._id,
							unfollow: true
						});	
					} else {
						self.trigger('followingItem', {
							feedID: self._area + '-' + self._id,
							notificationType: self.scope.find('[name=&quot;follow_type&quot;]:checked').val(),
							anonymous: !self.scope.find('[name=&quot;follow_public_checkbox&quot;]').is(':checked')
						});
					}				

					ips.ui.flashMsg.show( ips.getString('followUpdated') ); 
				})
				.fail( function (jqXHR, textStatus, errorThrown) {
					window.location = url;
				})
				.always( function () {
					// If we're in a hovercard, remove it
					self.scope.parents('.ipsHovercard').remove();
				});
		}
	});
}(jQuery, _));
</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.guestTerms.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100">/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.guestTerms.js - Guest terms bar
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.guestTerms', {

		initialize: function () {
			this.on( 'click', '[data-action=&quot;dismissTerms&quot;]', this.dismissBar );
			this.setup();
		},

		setup: function () {
			// If guest caching is enabled, the bar HTML will exist in the page even if this
			// user has accepted terms. We'll hide it with JS if that happens.
			this.scope.toggle( !ips.utils.cookie.get('guestTermsDismissed') );
			$('body').toggleClass('cWithGuestTerms', !ips.utils.cookie.get('guestTermsDismissed') );
		},

		/**
		 * Event handler for dismiss button in bar
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		dismissBar: function (e) {
			e.preventDefault();

			var self = this;

			// Set the cookie so it doesn't show again
			ips.utils.cookie.set( 'guestTermsDismissed', 1 );

			// Hide the bar
			self.scope.animate({
				opacity: &quot;0&quot;
			}, 'fast', function () {
				$('body').removeClass('cWithGuestTerms');

				// Destruct the sticky
				if( self.scope.is('[data-ipsSticky]') ){
					ips.ui.sticky.destruct( self.scope );
				}

				self.scope.remove();
			});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.ignoredComments.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.ignoredComments.js - Controller to handle ignored comments
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.ignoredComments', {

		initialize: function () {
			this.on( 'menuItemSelected', '[data-action="ignoreOptions"]', this.commentIgnore );
		},

		/**
		 * Ignore options
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		commentIgnore: function (e, data) {
			switch( data.selectedItemID ){
				case 'showPost':
					data.originalEvent.preventDefault();
					this._showHiddenPost( e, data );
				break;
				case 'stopIgnoring':
					data.originalEvent.preventDefault();
					this._stopIgnoringFromComment( e, data );
				break;
			}
		},
		
		/**
		 * Shows a hidden post
		 *	
		 * @param 		{event} 	e 		Event object from the event handler
		 * @param 		{object}	data 	Event data object from the event handler
		 * @returns 	{void}
		 */
		_showHiddenPost: function (e, data) {
			// Hide the ignore row
			var ignoreRow = $( data.triggerElem ).closest('.ipsComment_ignored');
			var commentID = ignoreRow.attr('data-ignoreCommentID');
			var comment = this.scope.find( '#' + commentID );

			ignoreRow.remove();
			comment.removeClass('ipsHide');
		},

		/**
		 * Stops ignoring posts by a user
		 *	
		 * @param 		{event} 	e 		Event object from the event handler
		 * @param 		{object}	data 	Event data object from the event handler
		 * @returns 	{void}
		 */
		_stopIgnoringFromComment: function (e, data) {
			var ignoreRow = $( data.triggerElem ).closest('.ipsComment_ignored');
			var userID = ignoreRow.attr('data-ignoreUserID');
			var self = this;
			var posts = this.scope.find('[data-ignoreUserID="' + userID + '"]');

			posts.each( function () {
				self.scope.find( '#' + $( this ).attr('data-ignoreCommentID') ).removeClass('ipsHide');
				$( this ).remove();
			});

			var url = ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=ignore&do=ignoreType&type=topics&off=1';

			ips.getAjax()( url, {
				data: {
					member_id: parseInt( userID )
				}
			})
				.done( function () {
					ips.ui.flashMsg.show( ips.getString('ignore_prefs_updated') );
				})
				.fail( function () {
					window.location = ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=ignore&do=ignoreType&off=1type=topics&member_id=' + userID;
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.instantNotifications.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.instantNotifications.js - Instant notifications controller
 *
 * Explanation of logic
 * ---------------------------
 * Every 20 seconds, this controller will check localStorage and determine if the last poll was > 20s ago.
 * If it was, it will fire an ajax request to get any new notifcations, and presnt those to the user. It will
 * then store this as the latest result in localStorage. We use localStorage so that multiple browser tabs
 * aren't all doing their own poll.
 * When the user first loads the page, we'll also compare the current message/notification count to what's in
 * localStorage. If there's new notifications, we'll fetch them and show them, to make it feel 'instanty'.
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.instantNotifications', {

		_pollTimeout: 60, // Seconds
		_windowInactivePoll: 0,
		_pollMultiplier: 1, // multiplier for decay
		_messagesEnabled: null,
		_ajaxObj: null,
		_debugPolling: true,
		_browserNotifications: {},
		_paused: false,
		_interval: null,

		initialize: function () {
			this.on( document, ips.utils.events.getVisibilityEvent(), this.windowVisibilityChange );
			this.on( window, 'storage', this.storageChange );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			if( !ips.utils.db.isEnabled() || !_.isFunction( JSON.parse ) ){
				Debug.warn("Sorry, your browser doesn't support localStorage or JSON so we can't load instant notifications for you.");
				return;
			}

			this._messagesEnabled = this.scope.find('[data-notificationType="inbox"]').length;
			this._setInterval( this._pollTimeout);

			this._doInitialCheck();
		},

		/**
		 * Responds to window storage event so we can update instantly if any other tab
		 * changes our data
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void} 
		 */
		storageChange: function (e) {
			var event = e.originalEvent;

			if( event.key !== 'notifications.' + ips.getSetting('baseURL') ){
				return;
			}

			if( this._debugPolling ){
				Debug.log('Notifications: updating instantly from storage event');
			}			

			try {
				var data = JSON.parse( event.newValue );
				var counts = this._getCurrentCounts();

				this._updateIcons( {
					messages: parseInt( data.messages ),
					notifications: parseInt( data.notifications )
				}, counts );
			} catch(err) {}
		},

		/**
		 * Handles window visibiliy changes; removes count from title bar
		 *
		 * @returns {void} 
		 */
		windowVisibilityChange: function () {
			var hiddenProp = ips.utils.events.getVisibilityProp();

			if( !_.isUndefined( hiddenProp ) && !document[ hiddenProp] ){
				// Document is now in focus
				this._updateBrowserTitle( 0 );
				this._pollMultiplier = 1;
				this._windowInactivePoll = 0;

				if( this._paused ){
					document.title = document.title.replace( "❚❚ ", '' );
					this._checkNotifications(); // Do an immediate check
					this._setInterval( this._pollTimeout );
				}

				if( this._debugPolling ){
					Debug.log( "Notifications: Resetting inactive poll.");
				}
			}
		},

		/**
		 * Handles setting up our interval poll
		 *
		 * @param 	{number} 	timeoutInSecs 	Seconds between polls
		 * @returns {void} 
		 */
		_setInterval: function (timeoutInSecs) {
			clearInterval( this._interval );
			this._interval = setInterval( _.bind( this._checkNotifications, this ), timeoutInSecs * 1000 );
		},

		/**
		 * On page load, does an initial check to see if we need to call the server
		 *
		 * @returns {void} 
		 */
		_doInitialCheck: function () {
			// Fetch the latest poll from localStorage
			var storage = ips.utils.db.get( 'notifications', ips.getSetting('baseURL') );
			var counts = this._getCurrentCounts();

			if( !storage || !_.isObject( storage ) ){
				return;
			}

			// If our bubble is reporting more notifications or messages than we have in storage,
			// we'll fetch them immediately
			if( ( this._messagesEnabled && counts.messages > storage.messages ) || counts.notifications > storage.notifications ){
				if( this._debugPolling ){
					Debug.log("Notifications: bubbles reporting higher counts for notifications or messages.");
				}

				var dataToSend = {
					notifications: storage.notifications
				};

				if( this._messagesEnabled ){
					dataToSend = _.extend( dataToSend, {
						messages: storage.messages
					});
				}

				this._doAjaxRequest( dataToSend );
			}
		},		

		/**
		 * The main method to check notification status
		 * Checks in localstorage to see if the last poll was < 20s ago
		 *
		 * @returns {void}
		 */
		_checkNotifications: function () {
			// Fetch the latest poll from localStorage
			var storage = ips.utils.db.get( 'notifications', ips.getSetting('baseURL') );
			var timestamp = ips.utils.time.timestamp();
			var counts = this._getCurrentCounts();
			var currentTimeout = this._pollTimeout * this._pollMultiplier;

			// If our window is inactive, increase the count
			if( document[ ips.utils.events.getVisibilityProp() ] ){
				if( this._windowInactivePoll >= 3 && this._pollMultiplier === 1 ){ // 0-3 minutes @ 60s poll
					if( this._debugPolling ){
						Debug.log( "Notifications: Polled over 3 minutes, increasing multiplier to 2");
					}
					this._pollMultiplier = 2;
					this._setInterval( this._pollTimeout * this._pollMultiplier );
				} else if( this._windowInactivePoll >= 7 && this._pollMultiplier === 2 ) { // 4-11 minutes @ 120s poll
					if( this._debugPolling ){
						Debug.log( "Notifications: Polled over 10 minutes, increasing multiplier to 3");
					}
					this._pollMultiplier = 3;
					this._setInterval( this._pollTimeout * this._pollMultiplier );
				} else if( this._windowInactivePoll >= 25 && this._pollMultiplier === 3 ) { // > 60 mins stop
					if( this._debugPolling ){
						Debug.log( "Notifications: Polled over 60 mins, stopping polling");
					}
					this._stopPolling();
					return;
				}

				this._windowInactivePoll++;
			}

			// Do we need to poll?
			// the -1 in the below logic gives us a little fuzziness to account for the delay in processing the script
			if( ( storage && _.isObject( storage ) ) && parseInt( storage.timestamp ) > ( timestamp - ( ( currentTimeout - 1 ) * 1000 ) ) ){ 
				// We *don't* need to poll, it has been less than 20s
				this._updateIcons( storage, counts );

				if( this._debugPolling ){
					Debug.log("Notifications: fetching from localStorage");
				}
			} else {
				
				// We send our currently-displayed bubble count to the backend
				// to find out if there's any change in number
				var dataToSend = {
					notifications: counts.notifications
				};

				if( this._messagesEnabled ){
					dataToSend = _.extend( dataToSend, {
						messages: counts.messages
					});
				}

				this._doAjaxRequest( dataToSend );				
			}
		},

		/**
		 * Calls the backend to get new notification data
		 *
		 * @param	{object} 	dataToSend 	 	Object containing current message and notification counts
		 * @returns {void} 
		 */
		_doAjaxRequest: function (dataToSend) {
			var self = this;
			var url = ips.getSetting( 'baseURL' ) + '?app=core&module=system&controller=ajax&do=instantNotifications';

			if( this._debugPolling ){
				Debug.log("Notifications: sending ajax request");
			}

			// We'll update the timestamp before polling so that other windows
			// don't start polling before this one is finished
			this._updateTimestamp();

			// We do need to poll, it's been more than 20s
			this._ajaxObj = ips.getAjax()( url, {
				data: dataToSend
			})
				.done( _.bind( this._handleResponse, this ) )
				.fail( function () {
					self._stopPolling(true);
					Debug.error("Problem polling for new notifications; stopping.");
				});
		},

		/**
		 * Processes an ajax response
		 *
		 * @param	{object} 	response 	 	Server response
		 * @returns {void} 
		 */
		_handleResponse: function (response) {

			try {
				// If auto-polling is now disabled, stop everything
				if( response.error && response.error == 'auto_polling_disabled' ){
					this._stopPolling( true );
					return;
				}

				var counts = this._getCurrentCounts();

				if( response.notifications.count > counts.notifications && this._debugPolling ){
					Debug.log("Notifications: I'm the winner! I found there's " + response.notifications.count + " new notifications");
				}

				this._updateIcons( {
					messages: response.messages.count,
					notifications: response.notifications.count
				}, counts );

				// Update localStorage with the new count
				ips.utils.db.set( 'notifications', ips.getSetting('baseURL'), {
					timestamp: ips.utils.time.timestamp(),
					messages: response.messages.count,
					notifications: response.notifications.count
				});

				var total = response.messages.data.length + response.notifications.data.length;

				// How many NOTIFICATIONS do we have to show?
				if( response.notifications.data.length ){
					this._showNotification( this._buildNotifyData( response.notifications.data, 'notification' ), 'notification' );
				}

				// How many MESSAGES do we have to show?
				if( response.messages.data.length ){
					this._showNotification( this._buildNotifyData( response.messages.data, 'message' ), 'message' );
				}
			} catch (err) {
				this._stopPolling( true );
				return;
			}
			
			// Do we need to play a sound?
			if( total > 0 ){
				// Do we need to update the browser title?
				if( document[ ips.utils.events.getVisibilityProp() ] ){
					this._updateBrowserTitle( total );
				}
			}		
		},

		/**
		 * Updates the browser title bar with a new count (or removes it if 0)
		 *
		 * @param 	{number} 	count 		Count to show in the browser title bar
		 * @returns {void}
		 */
		_updateBrowserTitle: function (count) {
			var cleanTitle = document.title.replace( /^\(\d+\)/, '' ).trim();

			if( count ){
				document.title = "(" + count + ") " + cleanTitle;	
			} else {
				document.title = cleanTitle;
			}
		},

		/**
		 * Builds notification data for the given items based on type
		 *
		 * @param 	{array} 	items 			Array of items from the backend
		 * @param	{string} 	type 	 		Type of notification being build (message or notification)
		 * @returns {object}	Object of notification data 
		 */
		_buildNotifyData: function (items, type) {
			var self = this;
			var notifyData = {
				count: items.length
			};

			if( items.length === 1 ){
				notifyData = _.extend( notifyData, {
					title: ips.getString( type + 'GeneralSingle'),
					icon: items[0].author_photo,
					body: items[0].title,
					url: items[0].url,
					onClick: function () {
						// Try and focus the window (security settings may prevent it, though)
						try {
							window.focus();
						} catch (err) {}

						window.location = items[0].url;
					}
				});
			} else {
				notifyData = _.extend( notifyData, {
					title: ips.pluralize( ips.getString( type + 'GeneralMultiple'), [ items.length ] ),
					body: items[0].title,
					icon: ips.getSetting( type + '_imgURL'),
					onClick: function () {
						// Try and focus the window (security settings may prevent it, though)
						try {
							window.focus();
						} catch (err) {}

						self._getIcon( ( type == 'message' ) ? 'inbox' : 'notify' ).click();
					}
				});
			}

			return notifyData;
		},

		/**
		 * Determines which is the appropriate notification method to use to let the user know about new data
		 *
		 * @param 	{object} 	notifyData 		Notification data to use when building the notification
		 * @param	{string} 	type 	 		Type of notification being build (message or notification)
		 * @returns {void}
		 */
		_showNotification: function (notifyData, type) {
			if( !document[ ips.utils.events.getVisibilityProp() ] ){
				// When the window is ACTIVE
				// Show a flash message
				this._showFlashMessage( notifyData, type );
			}
		},

		/**
		 * Shows a flash message at the bottom of the user's window
		 *
		 * @param 	{object} 	notifyData 		Object containing notification data
		 * @param 	{string}	type 			Type of notification (notification or message)
		 * @returns {void}
		 */
		_showFlashMessage: function (notifyData, type) {
			var html = '';
			var self = this;

			if( notifyData.count === 1 ){
				notifyData = _.extend( notifyData, { text: ips.getString( type + 'FlashSingle') } );
				html = ips.templates.render( 'core.notification.flashSingle', notifyData );	
			} else {
				notifyData = _.extend( notifyData, { text: ips.pluralize( ips.getString( type + 'FlashMultiple'), [ notifyData.count ] ) } );
				html = ips.templates.render( 'core.notification.flashMultiple', notifyData );
			}						

			if( $('#elFlashMessage').is(':visible') && $('#elFlashMessage').find('[data-role="newNotification"]').length ){
				$('#elFlashMessage').find('[data-role="newNotification"]').replaceWith( html );
			} else {
				ips.ui.flashMsg.show( html, { 
					timeout: 8,
					position: 'bottom',
					extraClasses: 'cNotificationFlash ipsPadding:half',
					dismissable: function () {
						self._stopPolling();
					},
					escape: false
				});
			}
		},

		/**
		 * Updates our storage timestamp to now
		 *
		 * @returns {void}
		 */
		_updateTimestamp: function () {
			var storage = ips.utils.db.get( 'notifications', ips.getSetting('baseURL') );

			storage = _.extend( storage, {
				timestamp: ips.utils.time.timestamp()
			});

			ips.utils.db.set( 'notifications', ips.getSetting('baseURL'), storage );
		},

		/**
		 * Updates the bubble on both icons if the count differs from what's alrady displayed
		 *
		 * @param	{object} 	newData 	 	The latest counts (either from storage or ajax response)
		 * @param 	{object} 	oldData 		Existing counts from the bubbles
		 * @returns {void}
		 */
		_updateIcons: function (newData, oldData) {
			var reportBadge = this.scope.find('[data-notificationType="reports"]');
			var reportCount = reportBadge.length ? parseInt( reportBadge.text() ) : 0;

			// Some data we'll pass in an event
			var notifyData = {
				total: parseInt( newData.notifications ) + reportCount,
				notifications: parseInt( newData.notifications ),
				reports: reportCount
			};

			if( parseInt( newData.notifications ) !== oldData.notifications ){
				this._updateIcon( 'notify', newData.notifications );
				this.scope.trigger( 'clearUserbarCache', { type: 'notify' } );
			}

			if( this._messagesEnabled ){
				if( parseInt( newData.messages ) !== oldData.messages ){
					this._updateIcon( 'inbox', newData.messages );
					this.scope.trigger( 'clearUserbarCache', { type: 'inbox' } );
				}

				notifyData.total += parseInt( newData.messages );
				notifyData.messages = parseInt( newData.messages );
			}

			// Trigger an event to let the document know
			this.scope.trigger( 'notificationCountUpdate', notifyData );
		},

		/**
		 * Updates a bubble on an icon, and uses the appropriate animation to show it
		 *
		 * @param	{string} 	type 	 	notify or inbox
		 * @param 	{number} 	count 		The new count to show
		 * @returns {void}
		 */
		_updateIcon: function (type, count) {
			var icon = this._getIcon( type );

			icon.attr( 'data-currentCount', count ).text( count );

			if( parseInt( count ) ){
				ips.utils.anim.go( ( !icon.is(':visible') ) ? 'zoomIn' : 'pulseOnce', icon.removeClass('ipsHide') );
			} else {
				icon.fadeOut();
			}
		},

		/**
		 * Returns a reference to the icon of the given type
		 *
		 * @param	{string} 	type 	 	notify or inbox
		 * @returns {element} 	jQuery element
		 */
		_getIcon: function (type) {
			return $('body').find('[data-notificationType="' + type + '"]');
		},

		/**
		 * Gets the current counts from the bubbles on-screen
		 *
		 * @returns {object} 	Contains two keys, messages & notifications, containing the currently-displayed counts
		 */
		_getCurrentCounts: function () {
			var messages = this.scope.find('[data-notificationType="inbox"]');
			var notifications = this.scope.find('[data-notificationType="notify"]');

			return {
				notifications: parseInt( notifications.attr('data-currentCount') ),
				messages: ( messages.length ) ? parseInt( messages.attr('data-currentCount') ) : null
			};
		},

		/**
		 * Stops our internal loop from polling for any more notifications
		 *
		 * @returns {void}
		 */
		_stopPolling: function (fatal) {
			Debug.info("Stopping instant notification polling");
			clearInterval( this._interval );
			this._paused = true;
			document.title = "❚❚ " + document.title.replace("❚❚ ", "");
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.lightboxedImages.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.lightboxedImages.js - As of 4.4 this is more of a general UGC controller. We've opted not to rename it
 * so that mass template changes don't become necessary, so that's a @future todo
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.lightboxedImages', {

		_random: null,

		initialize: function () {
			var self = this;
			this.on( 'initializeImages', this.refreshContent );
			this.on( 'refreshContent', this.refreshContent );
			this.on( document, 'imageRotated', function(e, data){
				var attachment = $( self.scope ).find( '.ipsAttachLink_image img[data-fileId=' + data.fileId + ']' );
				self._updateAttachmentImage( attachment, data );
			});
			this.setup();
		},

		/**
		 * Setup method
		 *	
		 * @returns 	{void}
		 */
		setup: function () {
			this._random = 'g' + ( Math.round( Math.random() * 100000 ) );
			this._initializeAttachments();
			this._initializeImages();
		},

		/**
		 * Refresh the content in this container
		 *	
		 * @returns 	{void}
		 */
		refreshContent: function (e) {
			Debug.log("Refreshing content in lightboxedImages");
			this.scope.removeAttr('data-loaded');
			this._initializeAttachments();
			this._initializeImages();

			e.stopPropagation();
		},

		/**
		 * Build the attachment display inside content
		 *	
		 * @returns 	{void}
		 */
		_initializeAttachments: function () {
			var fileIDsToFetch = {};
			var self = this;
			var attachments = this.scope.find('[data-fileid]').not( function (idx, elem) {
				// We don't want to change any image/video attachments, so exclude those here
				var elem = $(elem);
				//return elem.is('img, source, video') || elem.find('img, source, video').length;
				return elem.is('source, video') || elem.find('source, video').length;
			});

			if( !attachments.length ){
				return;
			}

			// Loop through each attachment and build the initial HTML for it
			attachments.each( function () {
				var attachment = $(this);

				if( !_.isUndefined( attachment.attr('data-loaded') ) ){
					return;
				}

				// we can skip all of this for images because we are not loading metadata here
				if( !( attachment.is( 'img, .ipsAttachLink_image' ) ) ){
					var parent = attachment.parent();
					var clone = parent.clone();

					// To figure out if this attachment is on a line either by itself or only with other attachments,
					// we'll clone the parent, remove all child elements, remove whitespace, and see if we
					// have any text left. If we do, we know it's inline.
					clone.children().remove();
					clone.text( clone.text().replace(/\s/g, '') );

					// If this attachment is part of a list
					if( !clone.text().length && attachment.parentsUntil( this.scope, 'li' ).length === 0 ){
						// This is a BLOCK attachment
						attachment.addClass('ipsAttachLink_block');

						if( attachment.children().length ){
							return;
						}

						var title = attachment.text();
						attachment.html( ips.templates.render('core.attachments.attachmentPreview', {
							title: title
						}));
					} else {
						// This is an INLINE attachment
						attachment.addClass('ipsAttachLink_inline');
						attachment.attr('title', ips.getString('attachmentPending'));
						attachment.attr('data-ipstooltip', true);
					}
				}

				fileIDsToFetch[ attachment.attr('data-fileid') ] = true;
			});

			// If we have no files to fetch, we can bail
			if( !_.size( fileIDsToFetch ) ){
				return;
			}

			// Now set up our lazy load observer which will load file info 	
			ips.utils.lazyLoad.observe( this.scope.get(0), {
				loadCallback: function () {
					// Get the file info for each block attachment in this post
					ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=ajax&do=attachmentInfo', {
						dataType: 'json',
						data: {
							attachIDs: fileIDsToFetch
						}
					})
						.done( function (response) {
							attachments.each( function () {
								var attachment = $(this);
								var attachmentID = attachment.attr('data-fileid');

								if( _.isUndefined( response[ attachmentID ] ) ){
									self._updateAttachmentMetaDataError( attachment );
								} else if( !_.isUndefined( response[attachmentID].rotate ) ) {
									self._updateAttachmentImage( attachment, response[ attachmentID ] );
								} else {
									self._updateAttachmentMetaData( attachment, response[ attachmentID ] );
								}
							});
						})
						.fail( function () {
							attachments.each( function () {
								var attachment = $(this);
								self._updateAttachmentMetaDataError( attachment );
							});
						});
				}
			});
		},

		/**
		 * Update an attachment with the provided meta data
		 *	
		 * @returns 	{void}
		 */
		_updateAttachmentMetaData: function (attachment, response) {
			var attachmentID = attachment.attr('data-fileid');

			if( attachment.hasClass('ipsAttachLink_block') ){
				attachment.find('.ipsAttachLink_metaInfo').html( ips.templates.render('core.attachments.metaInfo', {
					size: response.size,
					downloads: ips.pluralize( ips.getString('attachmentDownloads'), response.downloads )
				}));
			} else {
				attachment.attr('title', response.size + ' - ' + ips.pluralize( ips.getString('attachmentDownloads'), response.downloads ));
			}

			attachment.attr('data-loaded', true);
		},

		/**
		 * Update an image attachment with provided info
		 *
		 * @return 		{void}
		 */
		_updateAttachmentImage: function (attachment, response){
			return;
			
			if( ! ( attachment.is( 'img' ) ) ){
				return;
			}

			if( !_.isUndefined(response.rotate) && response.rotate !== null ){
				attachment.attr( 'data-rotate', response.rotate )
					.css( 'transform', '' );
				attachment.parents( 'a.ipsAttachLink_image' ).css( {
					'transform': 'rotate(' + response.rotate + 'deg)',
					'position': 'absolute',
					'top': 0
				} );

				if( response.rotate == 90 || response.rotate == -270 ){
					if( attachment.width() > attachment.height() ){
						attachment.parents( 'a.ipsAttachLink_image' ).css({
							'right': '40%',
							'height': '100%'
						});
					} else {
						attachment.parents( 'a.ipsAttachLink_image' ).css({
							'left': '5%',
							'transform-origin': 'right'
						});
					}
				} else if( response.rotate == -90 || response.rotate == 270 ){
					if( attachment.width() > attachment.height() ){
						attachment.parents('a.ipsAttachLink_image').css({
							'right': '40%',
							'height': '100%'
						} );
					} else {
						attachment.parents( 'a.ipsAttachLink_image' ).css({
							'left': '40%',
							'transform-origin': 'left'
						});
					}
				}

				/* get the current height of the parent and adjust if necessary */
				var containerHeight = attachment.height();
				if( response.rotate != 0 && response.rotate != 180 && response.rotate != -180 ){
					containerHeight = attachment.width();
				}
				var parent = attachment.parents( 'p:first' );
				if( $( parent ).height() < containerHeight ){
					$( parent ).css( { 'height': parseInt( containerHeight + 5 ).toString() + 'px', 'position': 'relative' } );
				}
			}
		},

		/**
		 * Update an attachment with an 'unavailable' message
		 *	
		 * @returns 	{void}
		 */
		_updateAttachmentMetaDataError: function (attachment) {
			if( attachment.hasClass('ipsAttachLink_block') ){
				attachment.find('.ipsAttachLink_metaInfo').html( ips.getString('attachmentUnavailable') );
			} else {
				attachment.attr('title', ips.getString('attachmentUnavailable') );
			}

			attachment.attr('data-loaded', true);
		},

		/**
		 * Pre-initialize an element.
		 * This is called on page load, possibly *before* images have been lazy-loaded. Used to set up
		 * any behaviors not requiring the image itself to be loaded yet (e.g. lightbox)
		 *	
		 * @returns 	{void}
		 */
		_preLazyLoadInit: function (elem) {
			// Since we're supplying a custom preload handler to lazyload, we should
			// still call the default preload handler manually
			ips.utils.lazyLoad.preload(elem);
			this._nonLazyLoadInit(elem);
		},

		/**
		 * Image init method used for both legacy content and when lazy-loading is disabled on a site.
		 *
		 * @returns 	{void}
		 */
		_nonLazyLoadInit: function (image) {
			if( image instanceof $ ){
				var rawImage = image.get(0);
			} else {
				var rawImage = image;
				image = $( image );
			}
			
			if( ( !image.is('img') || image.is('[data-emoticon], .ipsEmoji') ) && !image.hasClass('ipsImage_thumbnailed') ){
				return;
			}

			image.addClass('ipsImage_thumbnailed');

			// Wrap image in a link
			this._addOrUpdateWrappingLink( image );			
		},
		
		/**
		 * Given an image, either updates the wrapping link or adds one, before adding lightbox attrs
		 *
		 * @returns 	{void}
		 */
		_addOrUpdateWrappingLink: function (image) {
			var closestLink = image.closest('a');
			var imageSrc = image.attr('data-src') ? image.attr('data-src') : image.attr('src');
			var fileId = image.attr('data-fileid');
			
			if( closestLink.length && closestLink.hasClass('ipsAttachLink') && closestLink.hasClass('ipsAttachLink_image') ){
				var href = closestLink.attr('href');
				var ext = href.substr( href.lastIndexOf('.') + 1 ).toLowerCase();

				if( ['gif', 'jpeg', 'jpe', 'jpg', 'png'].indexOf( ext ) !== -1 ){
					closestLink
						.attr( 'data-fileId', fileId )
						.attr( 'data-fullURL', closestLink.attr('href') )
						.attr( 'data-ipsLightbox', '' )
						.attr( 'data-ipsLightbox-group', this._random );
				}
			} else if( !closestLink.length ) {
				image.wrap( $( '<a href="' + imageSrc + '"' + " title='" + ips.getString('enlargeImage') + "' data-fileId='" + fileId + "'data-wrappedLink data-ipsLightbox data-ipsLightbox-group='" + self._random + "'></a>" ) );
			}
		},

		/**
		 * Event handler for main event
		 *	
		 * @returns 	{void}
		 */
		_initializeImages: function () {
			var self = this;
			var toLazyLoad = this.scope.find( ips.utils.lazyLoad.contentSelector );

			if( toLazyLoad.length ){
				if( ips.getSetting('lazyLoadEnabled') ){
					//var _postLoadBound = _.bind( this._postLazyLoadInit, this );
					var _preloadBound = _.bind( this._preLazyLoadInit, this );

					toLazyLoad.each( function () {
						ips.utils.lazyLoad.observe( this, { // this == the raw dom node, not this controller
							preloadCallback: _preloadBound,
							//imgLoadedCallback: _postLoadBound
						});
					});
				} else {
					var _nonLazyLoadBound = _.bind( this._nonLazyLoadInit, this );
					ips.utils.lazyLoad.loadContent(this.scope.get(0), _nonLazyLoadBound); // load immediately
				}
			}

			// Handle legacy images that don't have the lazy load attributes applies
			// Only select non-lazy-load images	
			var nonLazyImages = this.scope.find('img:not([data-src])'); 
			if( nonLazyImages.length ){
				this.scope.imagesLoaded( function (imagesLoaded) {
					if( !imagesLoaded.images.length ){
						return;
					}
					_.each( imagesLoaded.images, function (image, i) {
						self._nonLazyLoadInit(image.img);
					});
				});
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.markRead.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.markRead.js - Controller for moderation actions in content listings
 *
 * Author: Matt Mecham; Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.markRead', {

		initialize: function () {
			this.on( 'click', this.markSiteRead );
		},

		/**
		 * Event handler for marking site as read
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		markSiteRead: function (e) {
			e.preventDefault();
			
			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'question',
				message: ips.getString('markAsReadConfirm'),
				subText: '',
				callbacks: {
					ok: function () {
						var url = $( e.currentTarget ).attr('href');

						ips.getAjax()( url, {
							showLoading: true
						})
							.done( function () {
								$( document ).trigger( 'markAllRead' );
							})
							.fail( function (jqXHR, textStatus, errorThrown) {
								window.location = url;
							});
					}
				}
			});
			
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.messengerMenu.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.messengerMenu.js - Messenger menu popup to control drawer
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.messengerMenu', {

		initialize: function () {
			this.setup();
			this.on( 'click', '#elMessengerPopup_compose', this.clickCompose );
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			// Remove the dialog trigger if we're on mobile
			if( ips.utils.responsive.currentIs('phone') ){
				this.scope.find('#elMessengerPopup_compose').removeAttr('data-ipsDialog');
			}
		},

		/**
		 * Event handler for clicking the 'compose' button inside this popup.
		 * Find the drawer X button and clicks it, hiding the drawer while the compose popup is open
		 * Also backup protection for redirecting to the compose page if we're on mobile
		 *
		 * @returns {void}
		 */
		clickCompose: function (e) {
			if( ips.utils.responsive.currentIs('phone') ){
				e.preventDefault();
				window.location = $( e.currentTarget ).attr('href');
			} else {
				$('body').find('#elMobileDrawer .ipsDrawer_close').click();
			}
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.mobileNav.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.mobileNav.js - Mobile navigation controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.mobileNav', {

		initialize: function () {
			this.on( document, 'notificationCountUpdate', this.updateCount );
		},

		/**
		 * Update the badge when we have a different notification count
		 *
		 * @param	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		updateCount: function (e, data) {
			if( !_.isUndefined( data.total ) ){
				if( data.total <= 0 ){
					this.scope.find('[data-notificationType="total"]').hide();
				} else {
					this.scope.find('[data-notificationType="total"]').text( parseInt( data.total ) );
				}
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.moderation.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.moderation.js - Controller for moderation actions in content listings
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.moderation', {
		
		_editTimeout: 0,
		_editingTitle: false,

		initialize: function () {
			this.on( 'submit', '[data-role="moderationTools"]', this.moderationSubmit );
			this.on( 'mousedown', '[data-role="editableTitle"]', this.editTitleMousedown );
			this.on( 'mouseup mouseleave', '[data-role="editableTitle"]', this.editTitleMouseup );
			this.on( 'click', '[data-role="editableTitle"]', this.editTitleMouseclick );
		},
		
		/**
		 * Event handler called when the user clicks down an editable title
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		editTitleMousedown: function(e) {
			var self = this;

			if( e.which !== 1 ){ // Only care if it's the left mouse button
				return;
			}

			this._editTimeout = setTimeout(function(){
				
				self._editingTitle = true;
				clearTimeout( this._editTimeout );
				
				var anchor = $( e.currentTarget );
				
				anchor.hide();
				var inputNode = $('<input/>').attr( { type: 'text' } ).attr( 'data-role', 'editTitleField' ).val( anchor.text().trim() );
				anchor.after(inputNode);
				inputNode.focus();
				
				inputNode.on('blur', function(){
					inputNode.addClass('ipsField_loading');
					if ( inputNode.val() == '' )
					{
                        inputNode.remove();
                        anchor.show();
                        self._editingTitle = false;
					}
					else
					{
                        ips.getAjax()( anchor.attr('href'), { method: 'post', data: { do: 'ajaxEditTitle', newTitle: inputNode.val() } } )
                            .done(function(response){
                                anchor.text( response );
                            })
                            .fail(function(response){
                                ips.ui.alert.show( {
                                    type: 'alert',
                                    icon: 'warn',
                                    message: response.responseJSON,
                                });
                            })
                            .always(function(){
                                inputNode.remove();
                                anchor.show();
                                self._editingTitle = false;
                            });
					}

				});
				
				inputNode.on('keypress', function(e){
					if( e.keyCode == ips.ui.key.ENTER ){
						e.stopPropagation();
						e.preventDefault();
						inputNode.blur();
						return false;
					}
				});

				// Chrome requires checking keydown instead for escape
				inputNode.on('keydown', function(e){
					if( e.keyCode == ips.ui.key.ESCAPE ){
						inputNode.remove();
						anchor.show();
						self._editingTitle = false;
						return false;
					}
				});
			}, 1000);
		},
		
		/**
		 * Event handler called when the user clicks up an editable title
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		editTitleMouseup: function(e) {
 			clearTimeout( this._editTimeout );
		},
		
		/**
		 * Event handler called when the user clicks up an editable title
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		editTitleMouseclick: function(e) {
 			if ( this._editingTitle ) {
	 			e.preventDefault();
	 		}
		},
				
		/**
		 * Event handler called when the moderation bar submits
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		moderationSubmit: function (e) {
			
			if ( this._editingTitle ) {
				e.preventDefault();
			}

			var action = this.scope.find('[data-role="moderationAction"]').val();

			switch (action) {
				case 'delete':
					this._modActionDelete(e);
				break;
				case 'move':
					this._modActionDialog(e, 'move', 'narrow');
				break;
				case 'hide':
					this._modActionDialog(e, 'hide', 'narrow');
				break;
				case 'split':
					this._modActionDialog(e, 'split', 'wide');
				break;
				case 'merge':
					this._modActionDialog(e, 'merge', 'medium');
				break;
				default:
					 $( document ).trigger('moderationSubmitted');
				break;
			}
		},

		/**
		 * Handles a delete action from the moderation bar
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		_modActionDelete: function (e) {
			var self = this;
			var form = this.scope.find('[data-role="moderationTools"]');

			if( self._bypassDeleteCheck ){
				return;
			}

			e.preventDefault();

			// How many are we deleting?
			var count = parseInt( this.scope.find('[data-role="moderation"]:checked').length ) + parseInt( this.scope.find('[data-role="moderation"]:hidden').length );

			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'warn',
				message: ( count > 1 ) ? ips.pluralize( ips.getString( 'delete_confirm_many' ), count ) : ips.getString('delete_confirm'),
				callbacks: {
					ok: function () {
                        $( document ).trigger('moderationSubmitted');
						self._bypassDeleteCheck = true;
						self.scope.find('[data-role="moderationTools"]').submit();
					}
				}
			});
		},

		/**
		 * Handles a move/split action from the moderation bar
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		_modActionDialog: function (e, title, size) {
			e.preventDefault();
			
			var form = this.scope.find('[data-role="moderationTools"]');
			
			// Create dialog to show the form
			var moveDialog = ips.ui.dialog.create({
				url: form.attr('action') + '&' + form.serialize().replace( /%5B/g, '[' ).replace( /%5D/g, ']' ),
				modal: true,
				title: ips.getString(title),
				forceReload: true,
				size: size
			});

			moveDialog.show();
			$( document ).trigger('moderationSubmitted');
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.navBar.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.navBar.js - Controller for managing the nav bar
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.navBar', {
		
		_defaultItem: null,
		_usingSubBars: true,

		initialize: function () {
			var debounce = _.debounce( this.resizeWindow, 300 );

			this.on( window, 'resize', debounce );
			this.on( 'mouseleave', this.mouseOutScope );
			this.on( 'mouseenter', this.mouseEnterScope );
			
			if( !$('body').attr('data-controller') || $('body').attr('data-controller').indexOf('core.global.customization.visualLang') == -1 ){
				this.setup();
			} else {
				var self = this;
				$('body').on( 'vleDone', function(){
					self.setup();
				});
			}
		},

		/**
		 * Setup method
		 *	
		 * @returns 	{void}
		 */
		setup: function () {			
			this.scope.identify();

			if( this.scope.find('[data-role="secondaryNavBar"]').length < 2 ){
				this._usingSubBars = false;
			}

			// If we have two items active, remove one if it belongs to a drop down list (affects stream items duplicated outside of the menu)
			if( this._usingSubBars && this.scope.find('.ipsNavBar_secondary > li.ipsNavBar_active').length > 1 ){
				$.each( this.scope.find('.ipsNavBar_secondary > li.ipsNavBar_active'), function( i, elem ) {
					if ( $(elem).find('a[data-ipsmenu]').length ) {
						$(elem).removeClass('ipsNavBar_active');
					}
				} );
			}

			// Add a caret to the More menu and move the dropdown if we're not using sub menus
			if( !this._usingSubBars ){
				this.scope.find('#elNavigationMore_dropdown')
					.append(" <i class='fa fa-caret-down'></i>")
					.after( 
						this.scope.find('#elNavigationMore_more_dropdown_menu')
							.attr('id', 'elNavigationMore_dropdown_menu')
					);
			}
			
			// If we have secondary menus, we'll do the normal tab-style navigation. Otherwise,
			// don't bother with the hover functionality
			if( this._usingSubBars ){
				if( ips.utils.events.isTouchDevice() ){
					this.on( 'click', '[data-role="primaryNavBar"] > li > a', this.intentOver );
				} else {
					this.scope.hoverIntent( _.bind( this.intentOver, this ), $.noop, '[data-role="primaryNavBar"] > li' );
				}
			}			

			this._defaultItem = this.scope.find('[data-role="primaryNavBar"] > li > [data-navDefault]').attr('data-navitem-id');

			this._mushAllMenus();
		},

		/**
		 * When the user mouses out of the scope completely, then we remove the active class
		 * on any tabs and hide the submenu within it. Then we immediately show the default
		 * item and its menu. The effect is 'resetting' the menu after mousing out.
		 *	
		 * @returns 	{void}
		 */
		mouseOutScope: function () {
			var self = this;

			if( ips.utils.events.isTouchDevice() ){
				return;
			}

			this._mouseOutTimer = setTimeout( function () {
				self._makeDefaultActive();
				self.scope.find('[data-ipsMenu]').trigger('closeMenu');
			}, 500 );
		},

		/**
		 * When the user mouses over our scope, we'll cancel any timer that's about to reset the menu
		 *	
		 * @returns 	{void}
		 */
		mouseEnterScope: function () {
			clearTimeout( this._mouseOutTimer );
		},

		/**
		 * HoverIntent event handler. Here we fade out all submenus and then fade in the requested menu,
		 * except if the requested is already currently shown.
		 *	
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		intentOver: function (e) {
			var li = $( e.currentTarget );
			var link = li.find('> a');
			var allItems = this.scope.find('[data-role="primaryNavBar"] > li');

			// On touch devices our event handler is on the a instead, so we need to switch
			// out our references here so the following code makes sense.
			if( li.is('a') ){
				li = li.closest('li');
				link = li.find('> a');
			}

			// If we're already active and this is a touch device, then allow the browser to navigate to it
			if( ips.utils.events.isTouchDevice() && li.hasClass('ipsNavBar_active') ){				
				return;
			}
			
			if( ips.utils.events.isTouchDevice() ){
				e.preventDefault();
			}
			
			this.scope.find('[data-ipsMenu]').trigger('closeMenu');

			allItems.removeClass('ipsNavBar_active').find('> a').removeAttr('data-active');
			li.addClass('ipsNavBar_active');
			link.attr('data-active', true);
		},

		/**
		 * Event handler for resizing the window
		 *	
		 * @returns 	{void}
		 */
		resizeWindow: function () {
			this._mushAllMenus();
		},

		_makeDefaultActive: function () {
			// Switch to the default item when we mouse out of the menu completely
			var link = this.scope.find('[data-navitem-id="' + this._defaultItem + '"]');
			var list = link.closest('li');
			var allItems = this.scope.find('[data-role="primaryNavBar"] > li');

			allItems.removeClass('ipsNavBar_active').find('> a').removeAttr('data-active');
			list.addClass('ipsNavBar_active').find('> a').attr('data-active', true);

			// The active item may now be in the more menu
			if( link.closest('[data-role="secondaryNavBar"]').length ){
				link.closest('[data-role="secondaryNavBar"]').closest('li').addClass('ipsNavBar_active').find('> a').attr('data-active', true);
			}
		},

		/**
		 * Mushes the given menu bar
		 *	
		 * @param 		{element} 	bar 				the menu bar being mushed
		 * @param 		{number} 	widthAdjustment		A value to subtract from the available space in the bar
		 * @returns 	{void}
		 */
		_mushMenu: function (bar, widthAdjustment) {
			var self = this;
			var padding = parseInt( this.scope.css('padding-left') ) + parseInt( this.scope.css('padding-right') );
			var availableSpace = this._getNavElement().width() - widthAdjustment - padding;
			var moreItem = bar.find('> [data-role="navMore"]');
			var moreMenuSize = moreItem.outerWidth();
			var menuItems = bar.find('> li[data-role="navBarItem"]');
			var sizeIncrement = 0;
			var dropdown = bar.find('[data-role="moreDropdown"]');

			if( !moreItem.is(':visible') ){
				moreMenuSize = moreItem.removeClass('ipsHide').outerWidth();
				moreItem.addClass('ipsHide');
			}

			menuItems.each( function () {
				var item = $( this );
				var itemSize = 0;

				// We set the original width on an item so that we can easily
				// sum the width of the menu. Even if we don't mush now, we'll set it
				// for easy use later
				if( item.attr('data-originalWidth' ) ){
					itemSize = parseInt( item.attr('data-originalWidth') );
				} else {
					var o = item.outerWidth() + parseInt( item.css('margin-right') ) + parseInt( item.css('margin-left') );
					item.attr( 'data-originalWidth', o );
					itemSize = o;
				}
				
				// If this item will push us over our available size, then mush it
				// We add the more menu manually because we *have* to show that one of course
				if( ( sizeIncrement + itemSize + moreMenuSize ) > availableSpace ){
					
					// Have we been mushed already?
					if( !item.attr('data-mushed') ){

						// Build a new list item containing the contents of our menu item
						var newLI = $('<li/>')
										.attr('data-originalItem', item.identify().attr('id') )
										.append( item.contents() );
						
						if( self._usingSubBars ){
							// If this is the primary nav, then we put it in the sub menu; otherwise,
							// build a dropdown
							if( bar.is('[data-role="primaryNavBar"]') ){
								bar.find('> [data-role="navMore"] > [data-role="secondaryNavBar"]').append( newLI );

								//--------------
								// If this item has a submenu, we need to move those into a dropdown
								if( newLI.find('> [data-role="secondaryNavBar"] > li').length ){
									var newA = newLI.find('> a');
									var newDropdown = $('<ul/>')
											.addClass('ipsMenu ipsMenu_auto ipsHide')
											.attr( 'id', newA.identify().attr('id') + '_menu' )
											.attr('data-mushedDropdown', item.identify().attr('id') );
									
									// Move items from the submenu to the new dropdown
									newLI.find('> [data-role="secondaryNavBar"] > li').each( function () {
										if( $( this ).is('[data-role="navMore"]') ){
											return;
										}

										var newMenuItem = $('<li/>').addClass('ipsMenu_item');

										// If this item itself has a submenu, then add the class to make it work
										if( $( this ).find('.ipsMenu').length ){
											newMenuItem.addClass('ipsMenu_subItems');
										}

										var menuContent = $( this ).contents();
										menuContent.find('.fa.fa-caret-down').addClass('ipsHide');
										newDropdown.append( newMenuItem.append( menuContent ).attr('data-originalItem', $( this ).identify().attr('id') ) );
									});

									// Now 
									newA
										.attr('data-ipsMenu', '')
										.attr('data-ipsMenu-appendTo', '#' + self.scope.identify().attr('id') )
										.append("<i class='fa fa-caret-down' data-role='mushedCaret'></i>");

									newLI.append( newDropdown );
								}
								//--------------

							} else {
								newLI.addClass('ipsMenu_item');

								// If we have a dropdown inside of this one, add the sub items class to show the >
								if( newLI.find('.ipsMenu').length ){
									newLI.addClass('ipsMenu_subItems');
								}

								dropdown.append( newLI );
							}
						} else {
							// Not using sub bars, so put it in the More dropdown
							self.scope.find('#elNavigationMore_dropdown_menu').append( newLI.addClass('ipsMenu_item') );

							if( newLI.find('.ipsMenu').length ){
								newLI.addClass('ipsMenu_subItems');
							}
						}

						// If the menu item is itself a dropdown menu, we need to adjust the appendTo
						// option for it, otherwise it will try and append it to the now-hidden menu tab.
						var linkInList = newLI.children('a');
						if( linkInList.is('[data-ipsMenu]') ){
							linkInList.attr('data-ipsMenu-appendTo', '#' + newLI.identify().attr('id') );
						}
						
						item.addClass('ipsHide').attr('data-mushed', true);
					}

				} else if( item.attr('data-mushed') ) {

					var mushedParent = null;
					var mushedItem = null;

					// If we're in the primary nav bar, our item will be in the secondayr nav bar; otherwise,
					// the item will be in a dropdown
					if( !self._usingSubBars ){
						mushedParent = self.scope.find('#elNavigationMore_dropdown_menu');
					} else if( bar.is('[data-role="primaryNavBar"]') ){
						mushedParent = bar.find('> [data-role="navMore"] > [data-role="secondaryNavBar"]');
					} else {
						mushedParent = dropdown;
					}

					// If this item has previously been mushed, we can unmush it by moving the contents
					// back to its original location
					var mushedItem = mushedParent.find('[data-originalItem="' + item.identify().attr('id') + '"]');

					// If the menu item itself is a dropdown, we previously adjusted the appendTo option.
					// We now need to set that back to the correct ID
					if( mushedItem.children('a').is('[data-ipsMenu]') ){
						mushedItem.children('a').attr('data-ipsMenu-appendTo', '#' + item.identify().attr('id') );
					}

					// If we found the mushed item, move the contents back to the original place
					if( mushedItem.length ){
						item.append( mushedItem.contents() ).removeClass('ipsHide');
					}

					// If we've moved secondary nav items into a dropdown, we need to move them back
					if( self._usingSubBars && bar.is('[data-role="primaryNavBar"]') ){
						var mushedDropdown = self.scope.find('[data-mushedDropdown="' + item.attr('id') + '"]');
						var secondaryMenu = item.find('> [data-role="secondaryNavBar"]');

						if( mushedDropdown.length ){

							// Move each item in the dropdown back to its correct place
							mushedDropdown.find('> .ipsMenu_item').each( function () {
								var originalItem = self.scope.find( '#' + $( this ).attr('data-originalItem') );
								originalItem.append( $( this ).contents() );
							});

							// Now remove the dropdown
							mushedDropdown.remove();
						}

						item.find('[data-role="mushedCaret"]').remove();
					}

					mushedItem.remove();
					item.removeAttr('data-mushed');
					item.find('.fa.fa-caret-down').removeClass('ipsHide');
				}

				sizeIncrement += itemSize;
			});
			
			// Show/hide the "More" item as needed
			if( bar.is('[data-role="primaryNavBar"]') ){
				if( this._usingSubBars ){
					moreItem.toggleClass('ipsHide', bar.find('> [data-role="navMore"] > [data-role="secondaryNavBar"] > li').length <= 1 );	
				} else {
					moreItem.toggleClass('ipsHide', !this.scope.find('#elNavigationMore_dropdown_menu > li').length );
				}				
			} else {
				moreItem.toggleClass('ipsHide', dropdown.find('> li').length < 1 );
			}

			this._makeDefaultActive();
		},

		/**
		 * Handles mushing the primary menu and the currently-visible secondary menu
		 *	
		 * @returns 	{void}
		 */
		_mushAllMenus: function () {
			this._mushMenu( this.scope.find('[data-role="primaryNavBar"]'), this.scope.find('#elSearch').outerWidth() );
			this._mushMenu( this.scope.find('[data-role="secondaryNavBar"]:visible'), 0 );
		},
		
		/**
		 * Returns the correct element used for determining nav width
		 *	
		 * @returns 	{element}
		 */
		_getNavElement: function () {
			if( this.scope.hasClass('ipsNavBar_primary') ){
				return this.scope;
			} else {
				return this.scope.find('.ipsNavBar_primary');
			}			
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.notifications.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.notifications.js - Browser notifications prompt
 *
 * Author: Stuart Silvester
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.notifications', {

		initialize: function () {
			this.on( document, 'menuOpened', this.menuOpened );
			this.on( document, 'permissionDenied.notifications', this.hideNotice );
			this.on( document, 'subscribePending.notifications', this.subscribePending );
			this.on( document, 'subscribeSuccess.notifications', this.subscribeSuccess );
			this.on( document, 'subscribeFail.notifications', this.subscribeFail );
			this.on( 'click', '[data-action=browserNotificationPrompt]', this.requestPermission );
			this.on( 'click', '[data-role=dismissNotification]', this.dismissNotification );
			this.on( 'click', '[data-action="rejectPush"]', this.rejectPush );

			if( ips.getSetting('memberID') && ips.utils.notification.supported && ips.utils.serviceWorker.supported ){
				this.setup();
			}
		},

		setup: function() {
			this._timeout = null;
			this._buttonText = '';
			this._missingSubscription = false;

			if( ips.utils.notification.needsPermission() && _.isUndefined( ips.utils.cookie.get('browserNotificationDismiss') ) ) {
				this.scope.html( ips.templates.render( 'core.browserNotification.prompt' ) ).hide();
			} else if( ips.utils.notification.hasPermission() && _.isUndefined( ips.utils.cookie.get('notificationPushRejected') ) ) {
				// If we have permission but no subscription, prompt the user - likely an upgrade where they've granted permission
				// but we haven't received a push subscription token yet
				ips.utils.notification.getSubscription()
					.then( subscription => {
						if( subscription ){
							return; // They already have a subscription, so no need to do anything here
						}
						
						this._missingSubscription = true;
						this.scope.html( ips.templates.render( 'core.browserNotification.missingSubscription' ) ).hide();
					})
					.catch( err => {
						Debug.log("getSubscription failed - browser may not support pushManager");
						Debug.log(err);
						return;
					});
			}
		},

		destroy: function () {
			clearTimeout( this._timeout );
		},

		/**
		 * Called when the notifications menu is opened
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data
		 * @returns {void}
		 */
		menuOpened: function (e, data) {
			const showPrompt = () => {
				this._timeout = setTimeout( () => {
					this.scope.slideDown('fast');
					ips.utils.cookie.unset('notificationMenuShown');
				}, 750 );
			};

			if( data.elemID == 'elFullNotifications' || data.elemID == 'elMobNotifications' ) {
				if( this._missingSubscription ){
					showPrompt(); // In cases where they've given permission but not yet subscribed, just show immediately
				} else {
					// To prevent annoyance for new users, we'll only show the callout after 2+ days
					if( !_.isUndefined( ips.utils.cookie.get( 'notificationMenuShown' ) ) ){
						var date = parseInt( ips.utils.cookie.get( 'notificationMenuShown' ) );

						if( date && Date.now() >= date ){
							showPrompt();
						}
					} else {
						var date = new Date();
						date.setDate( date.getDate() + 2 );
						ips.utils.cookie.set('notificationMenuShown', date.getTime(), true );
					}
				}
			}
		},

		/**
		 * Event handler for a pending subscription - update button to show something is happening
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data
		 * @returns {void}
		 */
		subscribePending: function (e, data) {
			// Save current button text so we can switch it back later
			const button = this.scope.find('[data-action="browserNotificationPrompt"]');
			this._buttonText = button.text();
			button.prop('disabled', true).text( ips.getString('notificationsEnabling') );
		},

		/**
		 * Event handler for a successful subscription - let the user know
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data
		 * @returns {void}
		 */
		subscribeSuccess: function (e, data) {
			const button = this.scope.find('[data-action="browserNotificationPrompt"]');
			button.prop('disabled', true).text( ips.getString('notificationsSubscribed') );
		},

		/**
		 * Event handler for a failed subscription - let the user know
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data
		 * @returns {void}
		 */
		subscribeFail: function (e, data) {
			this.scope
				.find('[data-action="browserNotificationPrompt"]')
				.prop( 'disabled', false )
				.text( this._buttonText );

			this.scope.find('[data-role="promptMessage"]').text( ips.getString('notificationsSubscribeFailed') ).slideDown();	
		},

		/**
		 * Called when the user clicks the button to init the notification popup
		 *
		 * @returns {void}
		 */
		requestPermission: function() {
			this.scope.find( '[data-role="promptMessage"]').text( ips.getString('notificationsAllowPrompt') ).slideDown();
			$(document).trigger('requestPermission.notifications');
		},

		/**
		 * User does not 
		 *
		 * @returns {void}
		 */
		rejectPush: function (e) {
			e.preventDefault();
			ips.utils.cookie.set('notificationPushRejected', true, true );
			this.hideNotice();
		},

		/**
		 * Event handler called when the browser (via ips.utils.notifications) changes notification status
		 * Whatever the user decides, hide the message now so we arent annoying
		 *
		 * @returns {void}
		 */
		hideNotice: function() {
			this.scope.slideUp('fast');
		},

		/**
		 * Allows the user to dismiss the callout in the notifications menu
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		dismissNotification: function(e) {
			if( e ) {
				e.preventDefault();
			}

			var date = new Date();
			date.setDate( date.getDate() + 100 );
			ips.utils.cookie.set( 'browserNotificationDismiss', true, date.toUTCString() );

			this.scope.slideUp( {
				duration: 400,
				complete: function() {
					$(this).remove();
				}
			});
		}

	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.onlineUsersWidget.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://invisioncommunity.com
 *
 * ips.core.onlineUsersWidget.js - Widget block controller for handling online users
 *
 * Author: Stuart Silvester
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.onlineUsersWidget', {

		initialize: function () {

			if( !ips.getSetting('member_url') || this.scope.find('[data-memberId=' + ips.getSetting('member_id') + ']').length )
			{
				return;
			}

			var memberRowHtml = ips.templates.render('core.onlineUser.linked', {
				memberUrl: ips.getSetting('member_url'),
				memberHovercardUrl: ips.getSetting('member_hovercardUrl'),
				formattedName: ips.getSetting('member_formattedName'),
			});

			this.scope.find('ul').prepend( memberRowHtml );

			var numOnline = this.scope.find( 'span[data-memberCount]' );
			numOnline.text( ips.pluralize( ips.getString('widget_onlineusers_membercount'), parseInt( numOnline.attr('data-memberCount') ) + 1 ) );
			this.scope.find('li[data-noneOnline]').remove();
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.pagination.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.pagination.js - Pagination controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.pagination', {

		initialize: function () {
			this.on( 'paginationClicked paginationJump', this.paginationClick );
		},
		
		paginationClick: function (e, data) {
			var self = this;

			if( !data.href ){
				return;
			}

			ips.getAjax()( data.href )
				.done( function (response) {
					self.scope.hide().html( response );
					ips.utils.links.updateExternalLinks();
					ips.utils.anim.go('fadeIn', self.scope);
				})
				.fail( function () {
					window.location = data.href;
				});
		}

	});
}(jQuery, _));
</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.poll.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.poll.js - Poll controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.poll', {

		initialize: function () {
			this.on( 'submit', 'form', this.submitPoll );
			this.on( 'click', '[data-action="viewResults"]', this.viewResults );
		},

		/**
		 * Event handler for clicking a link to view results
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		viewResults: function (e) {
			e.preventDefault();
						
			var url = $( e.currentTarget ).attr('href') + '&fetchPoll=1&viewResults=1';
			if ( $(e.currentTarget).attr('data-viewResults-confirm') ) {
				var self = this;
				ips.ui.alert.show( {
					type: 'confirm',
					icon: 'warn',
					message: ips.getString('generic_confirm'),
					subText: ips.getString('warn_allow_result_view'),
					callbacks: {
						ok: function () {
							self._viewResults( url + '&nullVote=1' );
						}
					}
				});
			} else {
				this._viewResults( url );
			}
		},
		
		_viewResults: function( url ) {
			var self = this;
			self._setContentsLoading();
			ips.getAjax()( url )
				.done( function (response) {
					self.cleanContents();
					self.scope.html( response );
					
					$( document ).trigger( 'contentChange', [ self.scope ] );
				});
		},

		/**
		 * Sets the poll container to loading state
		 *
		 * @returns 	{void}
		 */
		_setContentsLoading: function () {
			var container = this.scope.find('[data-role="pollContents"]');
			var height = container.outerHeight();

			container
				.css({
					height: height + 'px'
				})
				.html('')
				.addClass('ipsLoading');
		},

		/**
		 * Event handler for submitting the poll form to vote
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		submitPoll: function (e) {
			var form = $( e.currentTarget );

			if( form.attr('data-bypassAjax') ){
				return
			}
			
			e.preventDefault();
			var url = form.attr('action');
			var self = this;

			// Set button to voting
			this.scope.find('button[type="submit"]').prop( 'disabled', true ).text( ips.getString('votingNow') );

			if ( url.match(/\?/) ) {
				url += '&';
			} else {
				url += '?';
			}
			
			ips.getAjax()( url + 'fetchPoll=1', {
				data: form.serialize(),
				type: 'POST'
			})
				.done( function (response) {
					self.cleanContents();
					self.scope.html( response );

					$( document ).trigger( 'contentChange', [ self.scope ] );
					ips.ui.flashMsg.show( ips.getString('thanksForVoting') );
				})
				.fail( function () {
					form
						.attr( 'data-bypassAjax', true )
						.submit();	
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.pollEditor.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.pollEditor.js - Controller for follow button
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.pollEditor', {

		initialize: function () {
			this.on( 'click', '[data-action="removeChoice"]', this.removeChoice );
			this.on( 'click', '[data-action="addChoice"]', this.addChoice );
			this.on( 'click', '[data-action="addQuestion"]', this.addQuestion );
			this.on( 'click', '[data-action="removeQuestion"]', this.removeQuestion );

			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this._maxQuestions = this.scope.attr('data-maxQuestions');
			this._maxChoices = this.scope.attr('data-maxChoices');
			this._name = this.scope.attr('data-pollName');

			var pollData = ips.getSetting('pollData');

			// Build the existing options
			if( _.isArray( pollData ) && pollData.length ){
				for( var i = 0; i < pollData.length; i++ ){
					this._buildQuestion( pollData[ i ], i + 1 );
				}
			} else if ( _.isObject( pollData ) && ! _.isEmpty( pollData ) ) {
				for( var i in pollData ){
					this._buildQuestion( pollData[ i ], i );
				}
			} else {
				this._addQuestion( 1 );
				this._checkQuestionButton();
				this._checkChoiceButton( this.scope.find('[data-questionID="1"]') );
			}
		},

		/**
		 * Event handler for the Add Question button
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		addQuestion: function (e) {
			e.preventDefault();

			// Get maximum question ID
			var maxQid = _.max( this.scope.find('[data-questionID]'), function (item) {
				return parseInt( $( item ).attr('data-questionID') );
			});

			maxQid = parseInt( $( maxQid ).attr('data-questionID') );

			if( !_.isNumber( maxQid ) || _.isNaN( maxQid ) ){
				maxQid = 0;
			}

			var questions = this.scope.find('[data-questionID]');
			if( questions.length >= this._maxQuestions ){
				ips.ui.alert.show( {
					type: 'alert',
					icon: 'warn',
					message: ips.getString('noMoreQuestionsMlord'),
					callbacks: {
						ok: $.noop
					}
				});

				return;
			}

			this._addQuestion( maxQid + 1 );

			ips.utils.anim.go( 'fadeIn', this.scope.find('[data-questionID="' + ( maxQid + 1 ) + '"]') );

			this._checkQuestionButton();
		},

		/**
		 * Event handler for the Remove Question button
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		removeQuestion: function (e) {
			e.preventDefault();

			var self = this;
			var question = $( e.currentTarget ).closest('[data-questionid]');
			var removeQuestion = function () {
				question.replaceWith('<div data-questionid="' + question.attr('data-questionid') + '"></div>');
				self._checkQuestionButton();
			};

			if( question.find('[data-role="questionTitle"]').val() !== '' ){
				ips.ui.alert.show( {
					type: 'confirm',
					icon: 'question',
					message: ips.getString('removeQuestionConfirm'),
					callbacks: {
						ok: removeQuestion
					}
				});	
			} else {
				removeQuestion();
			}			
		},

		/**
		 * Event handler for adding a new choice to a question
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		addChoice: function (e) {
			e.preventDefault();

			var question = $( e.currentTarget ).closest('[data-questionID]');

			// How many choices?
			var maxCid = _.max( question.find('[data-choiceID]'), function (item) {
				return parseInt( $( item ).attr('data-choiceID') );
			});

			maxCid = parseInt( $( maxCid ).attr('data-choiceID') );

			if( !_.isNumber( maxCid ) || _.isNaN( maxCid ) ){
				maxCid = 0;
			}

			if( maxCid >= this._maxChoices ){
				ips.ui.alert.show( {
					type: 'alert',
					icon: 'warn',
					message: ips.getString('noMoreChoices'),
					callbacks: {
						ok: $.noop
					}
				});

				return;
			}

			this._addChoice( question, maxCid + 1 );

			ips.utils.anim.go( 'fadeIn', question.find('[data-choiceID="' + ( maxCid + 1 ) + '"]') );

			this._checkChoiceButton( question );
		},

		/**
		 * Event handler for removing a choice
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		removeChoice: function (e) {
			e.preventDefault();

			var self = this;
			var choice = $( e.currentTarget ).closest('[data-choiceID]');
			var question = choice.closest( '[data-questionID]' );

			// Check this isn't the only choice left
			if( question.find('[data-choiceID]').length <= 2 ){
				ips.ui.alert.show( {
					type: 'alert',
					icon: 'warn',
					message: ips.getString('cantRemoveOnlyChoice'),
					callbacks: {
						ok: $.noop
					}
				});

				return;
			}

			// Animation complete handler to remove the choice
			choice.animationComplete( function () {
				choice.remove();

				// Need to readjust all the choice numbers for this question
				_.each( question.find('[data-choiceID]'), function (item, idx) {
					$( item )
						.attr( 'data-choiceID', idx + 1 )
						.find('[data-role="choiceNumber"]')
							.text( idx + 1 );
				});

				self._checkChoiceButton( question );
			});

			ips.utils.anim.go( 'fadeOut fast', choice );
		},

		/**
		 * Builds a question based on existing data
		 *
		 * @param 		{object} 	data 		Data object containing title, multiple choice, etc
		 * @param 		{number} 	qid 		Question ID
		 * @returns 	{void}
		 */
		_buildQuestion: function (data, qid) {
			var choices = [];

			if( _.isArray( data.choices ) && data.choices.length ){
				for( var i = 0; i < data.choices.length; i++ ){
					choices.push( this._getChoiceHTML( i + 1, qid, data.choices[ i ].title ) );
				}
			} else if ( _.isObject( data.choices ) ) {
				for( var i in data.choices ){
					choices.push( this._getChoiceHTML( i, qid, data.choices[ i ].title ) );
				}
			}

			this.scope.find('[data-role="pollContainer"]').append( ips.templates.render('core.pollEditor.question', {
				pollName: this._name,
				multiChoice: data.multiChoice,
				questionID: qid,
				question: data.title,
				choices: choices.join(''),
				removeQuestion: !( qid === 1 )
			}));
		},

		/**
		 * Adds an empty question block to the form
		 *
		 * @param 		{object} 	data 		Message data
		 * @returns 	{void}
		 */
		_addQuestion: function (qid) {
			var choices = [];

			choices.push( this._getChoiceHTML( 1, qid ) );
			choices.push( this._getChoiceHTML( 2, qid ) );

			this.scope.find('[data-role="pollContainer"]').append( ips.templates.render('core.pollEditor.question', {
				pollName: this._name,
				questionTitle: ips.getString( 'questionTitle', { id: qid } ),
				questionID: qid,
				choices: choices.join(''),
				removeQuestion: !( qid === 1 )
			}));
		},

		/**
		 * Adds a new choice to the given question
		 *
		 * @param 		{element} 	question 		Question block we're adding to
		 * @param 		{number} 	cid 			ID of new choice
		 * @returns 	{void}
		 */
		_addChoice: function (question, cid) {
			var html = this._getChoiceHTML( cid, question.attr('data-questionID'), '' );
			question.find('[data-role="choices"]').append( html );
		},

		/**
		 * Returns the HTML for a choice row
		 *
		 * @param 		{object} 	data 		Message data
		 * @returns 	{void}
		 */
		_getChoiceHTML: function (cid, qid, name) {
			return ips.templates.render('core.pollEditor.choice', {
				choiceID: cid,
				questionID: qid,
				pollName: this._name,
				choiceTitle: name
			});
		},

		/**
		 * Enables or disables the Add Question button depending on current number of questions
		 *
		 * @returns 	{void}
		 */
		_checkQuestionButton: function () {
			var questions = this.scope.find('[data-questionID]');
			this.scope.find('[data-action="addQuestion"]').toggleClass( 'ipsButton_disabled ipsFaded', ( questions.length >= this._maxQuestions ) );
		},

		/**
		 * Enables or disables the Add Choice button depending on current number of choices in the given question
		 *
		 * @param 		{element} 	questionScope 		The question being worked with
		 * @returns 	{void}
		 */
		_checkChoiceButton: function (questionScope) {
			var choices = questionScope.find('[data-choiceID]');

			questionScope.find('[data-action="addChoice"]').toggleClass( 'ipsButton_disabled ipsFaded', ( choices.length >= this._maxChoices ) );
			questionScope.find('[data-choiceID] [data-action="removeChoice"]').toggleClass( 'ipsButton_disabled ipsFaded', ( choices.length === 2 ) );
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.profileCompletion.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.profileCompletion.js - Controller for profile completion sidebar widget
 *
 * Author: Ryan Ashbrook
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.profileCompletion', {
	
		initialize: function () {
			this.on( 'click', '[data-role="dismissProfile"]', this.dismissProfile );
		},

		dismissProfile: function(e) {
			e.preventDefault();

			var self = this;
			
			ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=settings&do=dismissProfile' )
				.done( function(response) {
					self.scope.animate({
						opacity: "0"
					}, 'fast', function() {
						self.scope.hide();
					} );
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.quickSearch.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.quickSearch.js - Controller for search in header
 *
 * Author: Ehren Harber
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.quickSearch', {
		
		initialize: function () {
			this.on( 'mouseup', '.cSearchFilter__menu', this.updateAndClose );
			this.on( 'change', 'input[name="type"]', this.updateFilter );
			this.on( 'focus', '.cSearchSubmit', this.a11yFocusSubmit );
			this.on( 'keypress', '.cSearchFilter__text', this.a11yOpenDetails );
			this.setup();
		},

		/* Populate the search filter with the default filter */
		setup: function () {
			document.querySelector('.cSearchFilter__text').innerText = document.querySelector('.cSearchFilter__menu input:checked + .cSearchFilter__menuText').innerHTML;
		},
		
		/* Update the search filter when a new filter is selected */
		updateFilter: function(e){
			document.querySelector('.cSearchFilter__text').innerText = e.target.nextElementSibling.innerHTML;
		},

		/* Close the menu and add focus back to the search form when a new filter is selected */
		updateAndClose: function(e){
			setTimeout(() => {
				document.querySelector('.cSearchFilter').open = false;
				document.querySelector('#elSearchField').focus();
			}, "500");
		},

		/* Automatically focus the selected filter when opened using keyboard */
		a11yOpenDetails: function(e){
			if(e.key === "Enter"){
				e.preventDefault();
				document.querySelector('.cSearchFilter').open = true;
				document.querySelector('.cSearchFilter__menu input:checked').focus();
			}
		},

		/* Hide the dropdown menu when the submit button is focused using keyboard */
		a11yFocusSubmit: function(e){
			document.querySelector('.cSearchFilter').open = false;
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.rating.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.quickSearch.js - Controller for search in header
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.rating', {
	
		initialize: function () {
			this.on( 'ratingSaved', '[data-ipsRating]', this.ratingClick );
			var scope = this.scope;
		},
		
		ratingClick: function(e, data){
			var scope = $(this.scope);
			ips.getAjax()( scope.attr('action'), {
				data: scope.serialize(),
				type: 'post'
			})
				.done( function (response, textStatus, jqXHR) {	
					// Don't need to actually do anything here
				})
				.fail(function(){
					scope.submit();
				});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.reaction.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.reaction.js - Reaction handler HAHA THANKS
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.reaction', {

		_reactTypeContainer: null,
		_reactButton: null,
		_reactTypes: null,
		_reactClose: null,
		_ajaxObj: null,

		initialize: function () {
			this.on( 'click', '[data-role="reactionInteraction"]', this.clickLaunchReaction );
			this.on( 'mouseenter', '[data-role="reactionInteraction"]', this.launchReaction ); 
			this.on( 'mouseleave', '[data-role="reactionInteraction"]', this.unlaunchReaction );
			this.on( 'click', '[data-role="reaction"]', this.clickReaction );
			this.on( 'click', '[data-action="unreact"]', this.unreact );
			this.setup();
		},

		setup: function () {
			this._reactTypeContainer = this.scope.find('[data-role="reactionInteraction"]');
			this._reactTypes = this._reactTypeContainer.find('[data-role="reactTypes"]');
			this._reactButton = this._reactTypeContainer.find('[data-action="reactLaunch"]');
			this._reactClose = this._reactTypeContainer.find('[data-action="unreact"]');
			this._reactBlurb = this.scope.find('[data-role="reactionBlurb"]');
			this._reactCount = this.scope.find('[data-role="reactCount"]');
			this._singleReaction = !( this._reactTypes.length );
		},
		
		/**
		 * Click handler for the react button - only relevant on mobile
		 *
		 * @returns 	{void}
		 */
		clickLaunchReaction: function (e) {
			if( !ips.utils.events.isTouchDevice() || this._singleReaction ){
				return;
			}
			
			this._reactTypeContainer.addClass('ipsReact_types_active');
			this._launchReaction();
		},

		/**
		 * Launch event handler for mouseenter event
		 *
		 * @returns 	{void}
		 */
		launchReaction: function () {
			// Ignore these on mobile
			if( ips.utils.events.isTouchDevice() ){
				return;
			}

			this._launchReaction();
		},

		/**
		 * Handler for clickLaunchReaction and launchReaction to open the flyout
		 *
		 * @returns 	{void}
		 */
		_launchReaction: function () {
			var self = this;
			this._reactTypes.show().removeClass('ipsReact_hoverOut').addClass('ipsReact_hover');
		},

		/**
		 * Handler for hiding the reaction flyout
		 *
		 * @returns 	{void}
		 */
		unlaunchReaction: function () {
			var self = this;

			this._reactTypes.animationComplete( function () {
				if( self._reactTypes.hasClass('ipsReact_hoverOut') ){
					self._reactTypes.removeClass('ipsReact_hoverOut').hide();
				}
			});

			this._reactTypes.removeClass('ipsReact_hover').addClass('ipsReact_hoverOut');
			this._reactTypeContainer.removeClass('ipsReact_types_active');
		},

		/**
		 * Handler for unreacting to a post
		 *
		 * @param 		{event} 	[e] 	Event object
		 * @returns 	{void}
		 */
		unreact: function (e) {
			if( e ){
				e.preventDefault();
				e.stopPropagation();
			}

			var self = this;
			var defaultReaction = this.scope.find('[data-defaultReaction]');
			var	url = this._reactTypeContainer.attr('data-unreact');

			// If the user's reaction isn't the default one, we need to swap them around
			if( !defaultReaction.closest('[data-action="reactLaunch"]').length ){
				// We need to swap the buttons
				var currentReaction = this._reactButton.find('[data-role="reaction"]');
				var defaultReactionCopy = defaultReaction.clone();
				var currentReactionCopy = currentReaction.clone();

				currentReaction.replaceWith( defaultReactionCopy.removeClass('ipsReact_active') );
				defaultReaction.replaceWith( currentReactionCopy.removeClass('ipsReact_active') );
			}

			// Remove the reacted class
			this._reactButton.removeClass('ipsReact_reacted');

			// Hide the close button
			self._reactClose.fadeOut();

			// And trigger the close event
			self.unlaunchReaction();

			// Fire the ajax request
			ips.getAjax()( url )
				.done( function (response) {
					self._updateReaction( response, ips.getString('removedReaction') );
				});
		},

		_updateReaction: function (response, flashMsg) {
			// Are we only showing the score?
			if( this._reactCount.hasClass('ipsReact_reactCountOnly') ){
				this._reactCount.find('[data-role="reactCountText"]').text( response.score ).removeClass('ipsAreaBackground_positive ipsAreaBackground_negative ipsAreaBackground_light');

				if( parseInt( response.score ) >= 1 ){
					this._reactCount.addClass('ipsAreaBackground_positive');
				} else if( parseInt( response.score ) < 0 ){
					this._reactCount.addClass('ipsAreaBackground_negative');
				} else {
					this._reactCount.addClass('ipsAreaBackground_light');
				}

				// Hide the count if there's no reactions; otherwise show
				if( response.count == 0 ){
					this._reactCount.hide();
				} else {
					this._reactCount.show();
				}
			} else {	
				this._reactBlurb.html( response.blurb );
				this._reactCount.text( response.count );

				if( parseInt( response.count ) > 0 ){
					this._reactBlurb.removeClass('ipsHide').fadeIn();
				} else {
					this._reactBlurb.fadeOut();
				}
			}

			this._reactTypeContainer.removeClass('ipsReact_types_active');

			// Let the user know
			if( flashMsg ){
				ips.ui.flashMsg.show( flashMsg );
			}
		},

		/**
		 * Handler for clicking a reaction
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		clickReaction: function (e) {
			e.preventDefault();

			// If this is a single reaction, and we're active, then we'll treat it as an 'unreact' action
			if( this._singleReaction && this._reactButton.hasClass('ipsReact_reacted') ){
				this.unreact(null);
				return;
			}

			// Mobile support - check whether we've activated the flyout first
			// Or, if this is a single reaction, ignore the flyout and just proceed with reacting
			if( ips.utils.events.isTouchDevice() && ( !this._singleReaction && !this._reactTypeContainer.hasClass('ipsReact_types_active') ) ){
				return;
			}

			var self = this;
			var reaction = $( e.currentTarget );
			var url = reaction.attr('href');
			var currentButton = this.scope.find('[data-action="reactLaunch"] > [data-role="reaction"]');
			var newReaction = ( !$( e.currentTarget ).closest('[data-action="reactLaunch"]').length || !this._reactButton.hasClass('ipsReact_reacted') ); 

			// Remove all 'active' classes to reset their states
			this._removeActiveReaction();

			// Trigger a pulse animation on the selected reaction
			reaction.addClass('ipsReact_active');

			// Add 'reacted' class to button
			this._reactButton.addClass('ipsReact_reacted');

			// If this isn't the current button already...
			if( reaction.closest('[data-action="reactLaunch"]').length == 0 ){

				var _complete = function () {
					// Clone and swap the current/new reaction
					var currentButtonCopy = currentButton.clone();
					var reactionCopy = reaction.clone();
					currentButton.replaceWith( reactionCopy.removeClass('ipsReact_active') );
					reaction.replaceWith( currentButtonCopy.removeClass('ipsReact_active') );

					// Show the x button, hide the flyout and remove active styles
					setTimeout( function () {
						self._reactClose.fadeIn();
					}, 400 );
					self.unlaunchReaction();
					self._removeActiveReaction();
				};
			} else {
				var _complete = function () {
					// Show the x button, hide the flyout and remove active styles
					setTimeout( function () {
						self._reactClose.fadeIn();
					}, 400 );
					self.unlaunchReaction();
					self._removeActiveReaction();
				};
			}

			// Use a timeout here to allow time for the 'pulse' animation to finish
			setTimeout( _complete, 400 );

			// Only bother with an ajax request if we're updating the reaction
			if( newReaction ){
				// Fire our ajax request to actually react
				if( this._ajaxObj && _.isFunction( this._ajaxObj.abort ) ){
					this._ajaxObj.abort();
				}

				let reactionTitle = reaction.innerText;
				let reactionIcon = reaction.find('img[data-ipsTooltip]');
				if ( reactionIcon && reactionIcon.attr('_title') ) {
					reactionTitle = reactionIcon.attr('_title');
				}
				reactionTitle = reactionTitle || undefined;
				
				this._ajaxObj = ips.getAjax()( url )
					.done( function (response) {
						self._updateReaction( response );

						/* Data Layer Event */
						try {
							if ( IpsDataLayerConfig && IpsDataLayerConfig._events.content_react.enabled ) {
								let context = IpsDataLayerContext || {};

								$('body').trigger('ipsDataLayer', {
									_key : 'content_react',
									_properties : {...context, 'reaction_type': reactionTitle, ...(response.datalayer || {})},
								});
							}
						} catch (e) {}
					} )
					.fail( function (jqXHR, textStatus, errorThrown) {
						Debug.log('fail');

						if( !_.isUndefined( jqXHR.responseJSON ) && jqXHR.responseJSON.error == 'react_daily_exceeded' ){
							ips.ui.alert.show( {
								type: 'alert',
								icon: 'warn',
								message: ips.getString('reactDailyExceeded'),
								callbacks: {}
							});
						} else {
							ips.ui.alert.show( {
								type: 'alert',
								icon: 'warn',
								message: ips.getString('reactError'),
								callbacks: {}
							});
						}

						// Undo all the hard work we did to make the reaction active :(
						self._reactButton.removeClass('ipsReact_reacted');
						self._reactClose.remove();
					} );
			}
		},

		/**
		 * Removes the active classname from all reactions to reset the animation
		 *
		 * @returns 	{void}
		 */
		_removeActiveReaction: function () {
			this._reactTypeContainer.find('.ipsReact_active').removeClass('ipsReact_active');
		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.recommendedComments.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.recommendedComments.js - Controller for recommended comments
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.recommendedComments', {
	
		initialize: function () {
			this.on( document, 'refreshRecommendedComments', this.refresh );
			this.on( document, 'removeRecommendation', this.removeRecommendation );
		},
		
		/**
		 * Refresh the recommended comments area (primary to add a new comment). Can optionally scroll to the
		 * recommended comments area firat
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns	{void}
		 */
		refresh: function (e, data){
			var self = this;

			if( data.scroll ){
				if( !this.scope.is(':visible') ){
					this.scope.show();
				}

				var once = _.bind( _.once( self._doRefresh ), this );

				$('html, body').animate({
					scrollTop: this.scope.offset().top + 'px'
				}, function () {
					once( data.recommended );
				});
			} else {
				self._doRefresh( data.recommended );
			}
		},

		/**
		 * Removes a recommended comment
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns	{void}
		 */
		removeRecommendation: function (e, data) {
			var self = this;
			var comment = this.scope.find('[data-commentID="' + data.commentID + '"]');

			if( comment.length ){
				comment.fadeOut().slideUp( function () {
					comment.remove();
					
					if( !self.scope.find('[data-commentID]').length ){
						self.scope.hide();
					}
				});
			}
		},

		/**
		 * Fires the ajax request to get the recommended comments
		 *
		 * @param 	{string} 	newId 	The ID of the new comment that was recommended
		 * @returns	{void}
		 */
		_doRefresh: function (newId) {
			var self = this;

			// Fetch the recommended comments
			ips.getAjax()( this.scope.attr('data-url') )
				.done( function (response) {
					self._handleResponse( response, newId );
				})
				.fail( function () {
					window.reload();
				});
		},

		/**
		 * Handles the server response when adding a new comment recommendation
		 *
		 * @param 	{object} 	response 		JSON returned from server
		 * @param 	{string} 	newId 			New comment ID recommendation
		 * @returns	{void}
		 */
		_handleResponse: function (response, newId ) {
			var content = $('<div>' + response.html + '</div>').find('[data-controller="core.front.core.recommendedComments"]');
			
			// Show/hide if needed
			if( parseInt( response.count ) > 0 ){
				this.scope.show();
			} else {
				this.scope.hide();
			}

			if( !response.count ){
				return;
			}

			// If we have a new ID, we don't need to replace the whole lot - we can insert it inline
			// Do we have an ID to hide and show?
			if( newId ){
				var newComment = content.find('[data-commentID="' + newId + '"]');
				newComment.hide();

				if( newComment.is(':last-child') ){
					this.scope.find('[data-role="recommendedComments"]').append( newComment );
				} else if( newComment.is(':first-child') ){
					this.scope.find('[data-role="recommendedComments"]').prepend( newComment );
				} else {
					var prev = newComment.prev('[data-commentID]');
					prev.after( newComment );
				}

				$( document ).trigger( 'contentChange', [ newComment ] );
				newComment.fadeIn().slideDown();
			} else {
				this.scope.html( content );
				$( document ).trigger( 'contentChange', [ this.scope ] );
			}			
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.reputation.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.reputation.js - Controller for reputation controls
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.reputation', {

		initialize: function () {
			this.on( 'click', '[data-action="giveReputation"]', this.giveReputation ); 
		},
		
		/**
		 * Event handler for the reputation buttons.
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		giveReputation: function (e) {
			e.preventDefault();
			
			var self = this;
			var url = $( e.currentTarget ).attr('href');
			var thisParent = this.scope.parent();

			this.scope.css({ opacity: "0.5" });
			
			ips.getAjax()( url )
				.done( function (response) {
					var newHTML = $('<div>' + response + '</div>').find('[data-controller="core.front.core.reputation"]').html();
					self.scope
						.html( newHTML )
						.css({
							opacity: "1"
						});
				})
				.fail( function ( jqXHR, textStatus, errorThrown ) {
					if ( jqXHR.responseJSON['error'] ) {
						ips.ui.alert.show( {
							type: 'alert',
							icon: 'warn',
							message: jqXHR.responseJSON['error'],
							callbacks: {}
						});
					} else {
						window.location = url;
					}
				});
		}

	});
}(jQuery, _));
]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.reviewForm.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.reviewForm.js - Review form controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.reviewForm', {
		initialize: function () {
			this.on( 'click', '[data-action=&quot;writeReview&quot;]', this.toggleReview );
		},

		toggleReview: function (e) {
			e.preventDefault();

			this.scope.find('[data-role=&quot;reviewIntro&quot;]').hide();
			this.scope.find('[data-role=&quot;reviewForm&quot;]').show();
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.sharelink.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.sharelink.js - Controller to launch link in small window
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.sharelink', {

		/**
		 * Initialize the events that this controller will respond to
		 *
		 * @returns 	{void}
		 */
		initialize: function () {
			this.on( 'click', '[data-role="shareLink"]', this.launchWindow );
		},
		
		/**
		 * Filter click
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		launchWindow: function(e) {
			e.preventDefault();
			var url = $( e.currentTarget ).attr('href');
			if ( !ips.utils.url.getParam( 'url', url ) )
			{
				url += "&url=" + encodeURIComponent( location.href );
			}
			if ( !ips.utils.url.getParam( 'title', url ) )
			{
				url += "&title=" + encodeURIComponent( document.title );
			}
			
			window.open( url, 'delicious','toolbar=no,width=550,height=550' );
		},
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.statuses.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.statuses.js - Controller for status updates
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.statuses', {

		/**
		 * Initialize the events that this controller will respond to
		 *
		 * @returns 	{void}
		 */
		initialize: function () {

			this._hideReplyFields();

			// Events that originate here
			this.on( 'click', '[data-action="delete"]', this.deleteStatus );
			this.on( 'click', '[data-action="lock"]', this.lockStatus );
			this.on( 'click', '[data-action="unlock"]', this.unlockStatus );
			this.on( 'click', '[data-action="reply"]', this.replyStatus );
			this.on( 'click', '[data-action="loadPreviousComments"]', this.loadPrevious );
			this.on( 'blur', '[data-role="replyComment"] input[type="text"]', this.blurCommentField );
			this.on( 'keydown', '[data-role="replyComment"] input[type="text"]', this.keydownCommentField );
			//this.on( 'focus', '[data-role="replyComment"] input[type="text"]', this.focusCommentField );


			// Events we watch for here
			this.on( document, 'lockingStatus', this.togglingStatus );
			this.on( document, 'lockedStatus', this.lockedStatus );

			this.on( document, 'unlockingStatus', this.togglingStatus );
			this.on( document, 'unlockedStatus', this.unlockedStatus );

			this.on( document, 'deletingStatus deletingComment', this.deletingStatus );
			this.on( document, 'deletedStatus deletedComment', this.deletedStatus );

			this.on( document, 'loadingComments', this.loadingComments );
			this.on( document, 'loadedComments', this.loadedComments );

			this.on( document, 'addingComment', this.addingComment );
			this.on( document, 'addedComment', this.addedComment );
		},

		_requestCount: {},
		_offsets: {},

		_hideReplyFields: function () {
			$( this.scope )
				.find('[data-statusid]')
					.not('.ipsComment_hasChildren')
					.find('.ipsComment_subComments')
						.hide()
					.end()
				.end()
				.find('[data-role="submitReply"]')
					.hide();
		},

		/**
		 * Display previous comments on a status
		 *
		 * @param 	{event} 	e 		Event
		 * @fires 	core.statuses#loadComments
		 * @returns {void}
		 */
		loadPrevious: function (e) {
			e.preventDefault();

			// Get status ID
			var link = $( e.currentTarget ),
				statusElem = link.parents( '[data-statusid]' ),
				statusID = $( statusElem ).data('statusid');

			// Count how many we're showing already
			this._offsets[ statusID ] = ( statusElem.find('[data-commentid]').length ) * -1;

			this.trigger( 'loadComments', { statusID: statusID, offset: this._offsets[ statusID ] } );
		},

		/**
		 * Model is loading comments
		 *
		 * @param 	{event} 	e 		Event
		 * @param 	{object}	data 	Event data object
		 * @returns {void}
		 */
		loadingComments: function (e, data) {
			// Find relevant status
			var status = $( this.scope ).find( '[data-statusid="' + data.statusID + '"]' );

			status
				.find('[data-action="loadPreviousComments"]')
				.html( ips.templates.render('core.statuses.loadingComments') );
		},

		/**
		 * Comments have been loaded
		 *
		 * @param 	{event} 	e 		Event
		 * @param 	{object}	data 	Event data object
		 * @returns {void}
		 */
		loadedComments: function (e, data) {
			// Find relevant status
			var status = $( this.scope ).find( '[data-statusid="' + data.statusID + '"]' ),
				loadingRow = status.find('[data-action="loadPreviousComments"]');

			loadingRow.after( data.comments );

			var totalShown = status.find('[data-commentid]').length;

			if( data.total <= totalShown ){
				loadingRow.remove();
			} else {
				loadingRow
					.html( ips.templates.render('core.statuses.loadMore') )
					.find("[data-role='remainingCount']")
					.text( data.total - totalShown );
			}

			// Let everyone know
			$( document ).trigger( 'contentChange', [ status ] );
		},


		/**
		 * User has clicked a delete link
		 *
		 * @param 	{event} 	e 		Event
		 * @fires 	core.statuses#deleteComment
		 * @fires 	core.statuses#deleteStatus
		 * @returns {void}
		 */
		deleteStatus: function (e) {
			e.preventDefault();

			// Get status ID
			var link = $( e.currentTarget ),
				statusElem = link.parents('[data-statusid]'),
				commentElem = link.parents('[data-commentid]'),
				statusID = $( statusElem ).data('statusid'),
				commentID = $( commentElem ).data('commentid');

			if( commentElem ){
				if( confirm( ips.getString('confirmStatusCommentDelete') ) ){
				
					/**
					 * Requests that a model deletes this status
					 *
					 * @event 		core.statuses#deleteComment
					 * @type 		{object}
					 * @property	{number}	statusID 	The ID of the parent status
					 * @property	{number}	commentID 	The ID of the comment to delete
					 */
					this.trigger( 'deleteComment', { statusID: statusID, commentID: commentID } );
				}
			} else { 	
				if( confirm( ips.getString('confirmStatusDelete') ) ){
					
					/**
					 * Requests that a model deletes this status
					 *
					 * @event 		core.statuses#deleteStatus
					 * @type 		{object}
					 * @property	{number}	statusID 	The ID of the status to delete
					 */
					this.trigger( 'deleteStatus', { statusID: statusID } );
				}
			}
		},

		/**
		 * A delete request is currently being handled by the model
		 *
		 * @param 	{event} 	e 		Event
		 * @param 	{object}	data 	Event data object
		 * @returns {void}
		 */
		deletingStatus: function (e, data) {
			// Find relevant status or comment
			if( data.commentID ){
				$( this.scope )
					.find( '[data-commentid="' + data.commentID + '"]' )
					.animate( { opacity: "0.5" } );
			} else {
				$( this.scope )
					.find( '[data-statusid="' + data.statusID + '"]' )
					.animate( { opacity: "0.5" } );
			}
		},

		/**
		 * Respond to the model deleting a status
		 *
		 * @param 	{event} 	e 		Event
		 * @param 	{object}	data 	Event data object
		 * @returns {void}
		 */
		deletedStatus: function (e, data) {
			// Find relevant status or comment
			if( data.commentID ){
				$( this.scope )
					.find( '[data-commentid="' + data.commentID + '"]' )
					.remove();
			} else {
				$( this.scope )
					.find( '[data-statusid="' + data.statusID + '"]' )
					.remove();
			}
		},

		/**
		 * User has clicked a lock status link
		 *
		 * @param 	{event} 	e 		Event
		 * @fires 	core.statuses#lockStatus
		 * @returns {void}
		 */
		lockStatus: function (e) {
			e.preventDefault();

			// Get status ID
			var link = $( e.currentTarget ),
				statusElem = link.parents( '[data-statusid]' ),
				statusID = $( statusElem ).data('statusid');

			/**
			 * Requests that a model locks this status
			 *
			 * @event 		core.statuses#lockStatus
			 * @type 		{object}
			 * @property	{number}	statusID 	The ID of the status to lock
			 */
			this.trigger( 'lockStatus', { statusID: statusID } );
		},

		/**
		 * User has clicked an unlock status link
		 *
		 * @param 	{event} 	e 		Event
		 * @fires 	core.statuses#unlockStatus
		 * @returns {void}
		 */
		unlockStatus: function (e) {
			e.preventDefault();

			// Get status ID
			var link = $( e.currentTarget ),
				statusElem = link.parents( '[data-statusid]' ),
				statusID = $( statusElem ).data('statusid');

			/**
			 * Requests that a model locks this status
			 *
			 * @event 		core.statuses#unlockStatus
			 * @type 		{object}
			 * @property	{number}	statusID 	The ID of the status to unlock
			 */
			this.trigger( 'unlockStatus', { statusID: statusID } );
		},

		/**
		 * Responds to the model locking a status
		 *
		 * @param 	{event} 	e 		Event
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		lockedStatus: function (e, data) {
			// Find relevant status
			var status = $( this.scope ).find( '[data-statusid="' + data.statusID + '"]' );

			// Find loading element
			$( status )
				.find('[data-action="lock"]')
				.first()
					.replaceWith( ips.templates.render('core.statuses.unlock') );

			this._finishedAction( e, data );
		},

		/**
		 * Responds to the model unlocking a status
		 *
		 * @param 	{event} 	e 		Event
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		unlockedStatus: function (e, data) {
			// Find relevant status
			var status = $( this.scope ).find( '[data-statusid="' + data.statusID + '"]' );

			// Find loading element
			$( status )
				.find('[data-action="unlock"]')
				.first()
					.replaceWith( ips.templates.render('core.statuses.lock') );

			this._finishedAction( e, data );
		},

		/**
		 * A request is currently being handled by the model
		 *
		 * @param 	{event} 	e 		Event
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		togglingStatus: function (e, data) {
			// Find relevant status
			var status = $( this.scope ).find( '[data-statusid="' + data.statusID + '"]' ),
				loadingThingy = status.find('.cStatusTools_loading');

			if( !loadingThingy.length ){
				// Add the loading thingy
				status
					.find('.cStatusTools')
					.first()
						.append( ips.templates.render('core.statuses.statusAction') );
			} else {
				loadingThingy.show();
			}

			// Update number of requests we're dealing with
			if( !this._requestCount[ data.statusID ] ){
				this._requestCount[ data.statusID ] = 1;
			} else {
				this._requestCount[ data.statusID ]++;
			}
		},

		/**
		 * Hides the loading thingy, if necessary. Called when we've finished handling a
		 * response from the model.
		 *
		 * @param 	{event} 	e 		Event
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		_finishedAction: function (e, data) {
			// Find relevant status
			var status = $( this.scope ).find( '[data-statusid="' + data.statusID + '"]' ),
				loadingThingy = status.find('.cStatusTools_loading');

			this._requestCount[ data.statusID ]--;

			if( this._requestCount[ data.statusID ] == 0 ){
				loadingThingy.remove();
			}
		},

		/**
		 * Shows and/or focuses the comment reply box for a status
		 *
		 * @param 	{event} 	e 		Event
		 * @returns {void}
		 */
		replyStatus: function (e) {
			e.preventDefault();

			// Get status ID
			var link = $( e.currentTarget ),
				statusElem = link.parents( '[data-statusid]' );

			if( statusElem.find('[data-commentid]').length > 0 ){
				statusElem
					.find('[data-role="replyComment"] input[type="text"]')
						.focus();

				return;
			}

			Debug.log( statusElem.find('.ipsComment_subComments').is(':visible') );

			if( !statusElem.find('.ipsComment_subComments').is(':visible') ){
				ips.utils.anim.go('fadeIn', statusElem.find('.ipsComment_subComments') );

				statusElem
					.addClass('ipsComment_hasChildren')
					.find('[data-role="replyComment"] input[type="text"]')
						.focus();
			} else {

				if( statusElem.find('[data-commentid]').length == 0 && field.val() == '' ){
					statusElem
						.removeClass('ipsComment_hasChildren')
						.find('.ipsComment_subComments, [data-role="submitReply"]')
							.hide();
				}
			}
			
		},

		/**
		 * User has blurred from the reply text field. Remove the comment box if a) there's no existing comments
		 * b) they haven't typed anything
		 *
		 * @param 	{event} 	e 		Event
		 * @returns {void}
		 */
		blurCommentField: function (e) {
			e.preventDefault();

			// Get status ID
			var field = $( e.currentTarget ),
				statusElem = field.parents( '[data-statusid]' ),
				replyButton = statusElem.find('[data-role="submitReply"]');

			if( statusElem.find('[data-commentid]').length == 0 && field.val() == '' ){
				statusElem
					.removeClass('ipsComment_hasChildren')
					.find('.ipsComment_subComments')
						.hide();
			}
		},

		/**
		 * User has blurred from the reply text field. Remove the comment box if a) there's no existing comments
		 * b) they haven't typed anything
		 *
		 * @param 	{event} 	e 		Event
		 * @returns {void}
		 */
		keydownCommentField: function (e) {

			var field = $( e.currentTarget ),
				statusElem = field.parents( '[data-statusid]' ),
				statusID = statusID = $( statusElem ).data('statusid');

			if( e.keyCode == ips.ui.key.ENTER ){
				
				/**
				 * Adds a new reply to a status
				 *
				 * @event 		core.statuses#addComment
				 * @type 		{object}
				 * @property	{string}	content 	The text of the reply
				 * @property	{number}	statusID 	The ID of the parent status				 
				 */
				this.trigger('addComment', {
					content: field.val(),
					statusID: statusID
				});
			}
		},

		/**
		 * The model is saving a comment
		 *
		 * @param 	{event} 	e 		Event
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		addingComment: function (e, data) {
			// Find relevant status
			var statusElem = $( this.scope ).find( '[data-statusid="' + data.statusID + '"]' ),
				replyRow = statusElem.find('[data-role="replyComment"]');

			replyRow
				.find('input[type="text"]')
				.prop('disabled', true)
				.addClass('ipsField_disabled');
		},

		/**
		 * A comment has been added by the model
		 *
		 * @param 	{event} 	e 		Event
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		addedComment: function (e, data) {
			// Find relevant status
			var statusElem = $( this.scope ).find( '[data-statusid="' + data.statusID + '"]' ),
				replyRow = statusElem.find('[data-role="replyComment"]'),
				subComments = statusElem.find('.ipsComment_subComments');

			if( replyRow.length ){
				replyRow.before( data.comment );
			} else if( subComments.length ){
				subComments.append( data.comment );
			}

			statusElem
				.find('[data-role="replyComment"] input[type="text"]')
					.val('')
					.blur()
					.prop('disabled', false)
					.removeClass('ipsField_disabled');
		},

		/**
		 * User has focused on the reply field, so we show the reply button
		 *
		 * @param 	{event} 	e 		Event
		 * @returns {void}
		 */
		/*focusCommentField: function (e) {
			e.preventDefault();

			// Get status ID
			var field = $( e.currentTarget ),
				statusElem = field.parents( '[data-statusid]' ),
				replyButton = statusElem.find('[data-role="submitReply"]');

			if( !replyButton.is(':visible') ){
				replyButton.show();
			}
		}*/
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.statusFeedWidget.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.statusFeedWidget.js - Controller for status sidebar widget
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.statusFeedWidget', {
	
		initialize: function () {
			this.on( 'editorWidgetInitialized', '[data-role="statusFormArea"]', this.editorReady );
			this.on( 'focus', '[data-role="statusFormArea"] .ipsComposeArea_dummy', this.focusNewStatus );
			this.on( 'submit', '[data-role="statusFormArea"] form', this.submitNewStatus );
			this.setup();
		},

		setup: function () {

		},

		focusNewStatus: function (e) {
			e.preventDefault();
			var self = this;

			$( e.currentTarget ).text( ips.getString('loading') + "..." );

			// Fetch the form
			ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=status&controller=ajaxcreate')
				.done( function (response) {
					self.scope.find('[data-role="statusEditor"]').html( response );
					$( document ).trigger( 'contentChange', [ self.scope.find('[data-role="statusEditor"]') ] );
				});
		},

		editorReady: function (e, data) {
			this.scope.find('[data-role="statusEditor"]').show();
			this.scope.find('[data-role="statusDummy"]').hide().find('.ipsComposeArea_dummy').text( ips.getString('whatsOnYourMind') );

			try {
				CKEDITOR.instances[ data.id ].focus();
			} catch (err) {
				Debug.log( err );
			}
		},

		submitNewStatus: function (e) {
			e.preventDefault();

			var self = this;
			var form = $( e.currentTarget );

			// Set the button loading
			form.find('button[type="submit"]').prop( 'disabled', true ).text( ips.getString('updatingStatus') );

			ips.getAjax()( form.attr('action'), {
				data: form.serialize(),
				type: 'post',
				bypassRedirect: true
			} )
				.done( function (response) {
					var newStatus = $( response.content );

					self.scope.find('[data-role="statusDummy"]').show();
					self.scope.find('[data-role="statusEditor"]').hide();
					self.scope.find('[data-role="statusFeedEmpty"]').hide();

					// Add the content, find the new status, hide it, then animate it
					self.scope.find('[data-role="statusFeed"]')
						.prepend( newStatus )
						.find('[data-statusID="' + response.id + '"]')
							.hide()
							.slideDown();

					$( document ).trigger( 'contentChange', [ self.scope.find('[data-role="statusFeed"]') ] );
				})
				.always( function () {
					form.find('button[type="submit"]').prop( 'disabled', false ).text( ips.getString('submitStatus') );
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.tagEditor.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.tagEditor.js - Quick tag editing
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.tagEditor', {

		_minTags: null,
		_maxTags: null,
		_count: 0,
		_tagEditID: '',

		initialize: function () {
			this.on( 'click', '[data-action="removeTag"]', this.removeTag );
			this.on( document, 'tagsUpdated', this.tagsUpdated );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			this._tagEditID = this.scope.attr('data-tagEditID');

			// How many tags do we have?
			this._minTags = this.scope.attr('data-minTags') || null;
			this._maxTags = this.scope.attr('data-maxTags') || null;
			this._setCount();
			this._checkMinMax();
		},

		/**
		 * If this instance is benig destroyed, see if we have already shown a menu for it, and if so remove it
		 * This is necessary in situtions where a tag editor might be shown more than once on the page, e.g. gallery lightbox
		 *
		 * @returns {void}
		 */
		_destroy: function () {
			if( $('#elTagEditor_' + this._tagEditID + '_menu').length ){
				$('#elTagEditor_' + this._tagEditID + '_menu').remove();
			}
		},

		/**
		 * Event handler for tagsUpdated method, triggered by the tagEditorForm controller inside the dropdown menu
		 * Lets us know that tags have changed so that we can update the UI
		 *
		 * @param	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		tagsUpdated: function (e, data) {
			if( data.tagEditID !== this._tagEditID ){
				return;
			}

			// Remove existing tags, and then reapply with new HTML
			this.scope.find('.ipsTag').closest('li').remove();
			this.scope.prepend( data.tags );

			// Is there an editable prefix?
			var editablePrefix = $('body').find('[data-editablePrefix]');

			if( editablePrefix.length ){
				if( data.prefix ){
					editablePrefix.html( data.prefix ).removeClass('ipsHide');
				} else {
					editablePrefix.html('').addClass('ipsHide');
				}
			}

			// Count tags
			this._setCount();
			this._checkMinMax();

			// Show flash message
			ips.ui.flashMsg.show( ips.getString('tagsUpdated') );
		},

		/**
		 * Event handler for clicking the 'x' on a tag to remove it
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		removeTag: function (e) {
			e.preventDefault();

			var self = this;
			var remove = $( e.currentTarget );
			var url = remove.attr('href');
			var tagContainer = remove.closest('li');
			var tag = tagContainer.find('.ipsTag');

			// Fade it out since we'll assume we can remove it
			tagContainer.fadeOut('fast');

			// Adjust count
			this._count--;
			this._checkMinMax();

			ips.getAjax()( url, {
				bypassRedirect: true
			})
				.done( function () {
					ips.ui.flashMsg.show( ips.getString('tagRemoved') );

					// Add a small timeout on actually removing it from the dom to allow animation to finish
					setTimeout( function () {
						tagContainer.remove();
					}, 200 );
				})
				.fail( function (jqXHR, textStatus, errorThrown) {
					tagContainer
						.stop()
						.show()
						.css({
							opacity: "1"
						});

					self._count++;

					// Error will indicate what happened, e.g. minimum number of tags required
					if( jqXHR.responseJSON ){
						ips.ui.alert.show( {
							type: 'alert',
							icon: 'warn',
							message: jqXHR.responseJSON,
							callbacks: {}
						});
					}
				});
		},

		/**
		 * Hides the 'x' or add tag button appropriately depending on the current status of tags
		 *
		 * @returns {void}
		 */
		_checkMinMax: function () {
			var allowRemove = !( this._minTags && this._count <= this._minTags );

			// Hide the remove links if needed			
			this.scope
				.find('[data-action="removeTag"]')
					.toggle( allowRemove )
				.end()
				.find('.ipsTags_deletable')
					.toggleClass( 'ipsTags_deletable', allowRemove );

			// Hide the add link if needed
			this.scope.find('.ipsTags_edit').toggle( !( this._maxTags && this._count >= this._maxTags ) );
		},

		_setCount: function () {
			var prefix = this._getPrefix();
			var count = this.scope.find('.ipsTag').length;

			if( prefix.length && prefix.is(':visible') ){
				count++;
			}

			this._count = count;
		},

		_getPrefix: function () {
			return $('body').find('[data-editablePrefix]');
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.tagEditorForm.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.tagEditorForm.js - Controller for the tag editing form within a content item
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.tagEditorForm', {
		_placeholder: null,
		_menuID: '',
		_tagEditID: '',

		initialize: function () {
			this.on( document, 'menuOpened', this.menuOpened );
			this.on( document, 'menuClosed', this.menuClosed );
			this.on( 'submit', 'form', this.submitForm );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			this._menuID = this.scope.closest('.ipsMenu').attr('id').replace('_menu', '');
			this._tagEditID = this._menuID.replace('elTagEditor_', '');
		},

		/**
		 * Event handler for 'menuClosed' event. We'll check this is the menu we care about and 
		 * then clear the tag edit HTML
		 *
		 * @param	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		menuClosed: function (e, data) {
			if( data.elemID != this._menuID ){
				return;
			}

			// Wipe out the HTML
			this.scope.html( ips.templates.render('core.edittags.default') );
		},

		/**
		 * Event handler for 'menuOpened' event. We'll check this is the menu we care about and 
		 * then load the tag editor form if so.
		 *
		 * @param	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		menuOpened: function (e, data) {
			if( data.elemID != this._menuID ){
				return;
			}

			var self = this;
			var url = $( data.originalEvent.currentTarget ).attr('href');

			ips.getAjax()( url )
				.done( function (response) {
					self._setLoading( false );
					self.scope.html( response );
					$( document ).trigger('contentChange', [ self.scope ] );
				})
				.fail( function () {
					window.location = url;
				});
		},

		/**
		 * Event handler for submitting the tag edit form.
		 * On success trigger an event to which the tagEditor controller will respond
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		submitForm: function (e) {
			e.preventDefault();

			// Submit the form
			var self = this;
			var form = $( e.currentTarget );
			var autoComplete = this.scope.find('[data-ipsAutocomplete]');

			// Trigger blur on the autocomplete box
			autoComplete.trigger('blur');

			// This isn't ideal, but to prevent a race condition with the autocomplete where
			// it doesn't tokenify a typed tag in time before this form submits, we need to add
			// a delay.
			setTimeout( function () {
				if( ips.ui.autocomplete.getObj( autoComplete ).hasErrors() ){
					e.preventDefault();
					return;
				}

				self._setLoading( true );

				ips.getAjax()( form.attr('action'), {
					type: 'post',
					data: form.serialize(),
					dataType: 'json'
				})
					.done( function (response) {
						self.scope.trigger('tagsUpdated', {
							tagEditID: self._tagEditID,
							tags: response.tags,
							prefix: response.prefix
						});
						self.scope.trigger('closeMenu');
						setTimeout( function () {
							self._setLoading( false );
						}, 200);
					})
					.fail( function (jqXHR, textStatus, errorThrown) {
						// Error will indicate what happened, e.g. minimum number of tags required
						if( jqXHR.responseJSON ){
							ips.ui.alert.show( {
								type: 'alert',
								icon: 'warn',
								message: jqXHR.responseJSON,
								callbacks: {}
							});
						}
					});
				}, 500);			
		},

		/**
		 * Set the menu into loading state (i.e. show a spinner)
		 *
		 * @param	{boolean} 	loading 		Are we loading?
		 * @returns {void}
		 */
		_setLoading: function (loading) {
			if( loading ){
				if( !this._placeholder ){
					this._buildPlaceholder();
				}	

				// Measure size of form
				var width = this.scope.outerWidth();
				var height = this.scope.outerHeight();

				this.scope.hide();

				this._placeholder
					.show()
					.css({
						width: width + 'px',
						height: height + 'px'
					});
			} else {
				if( this._placeholder ){
					this._placeholder.hide();
					this.scope.show();	
				}				
			}
		},

		/**
		 * Builds an element that will cover the menu contents to show the loading state
		 *
		 * @returns {void}
		 */
		_buildPlaceholder: function () {
			this._placeholder = $('<div/>').addClass('ipsLoading').hide();
			this.scope.after( this._placeholder );
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.userbar.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.userbar.js - Controller for userbar (inbox, notifications, etc.)
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.core.userbar', {

		loaded: {},

		/**
		 * Initialize controller events
		 * Sets up the events from the view that this controller will handle
		 *
		 * @returns 	{void}
		 */
		initialize: function () {
			// Events initiated here
			this.on( document, 'menuOpened', this.menuOpened );
			this.on( document, 'clearUserbarCache', this.clearUserbarCache );
		},

		/**
		 * Event handler for menus being opened. Pass off to the correct method to handle
		 *
		 * @param 		{event} 	e 		Event
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		menuOpened: function (e, data) {
			if( data.elemID == 'elFullInbox' || data.elemID == 'elMobInbox' ){
				this._loadMenu( 'inbox', ips.getSetting('baseURL') + 'index.php?app=core&module=messaging&controller=messenger&overview=1&_fromMenu=1', 'inbox' );
			} else if( data.elemID == 'elFullNotifications' || data.elemID == 'elMobNotifications' ){
				this._loadMenu( 'notify', ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=notifications', 'notify' );
			} else if( data.elemID == 'elFullReports' || data.elemID == 'elMobReports' ){
				this._loadMenu( 'reports', ips.getSetting('baseURL') + 'index.php?app=core&module=modcp&controller=modcp&tab=reports&overview=1', 'reports' );
			}
		},
		
		/**
		 * Event handler to clear the cache of loaded windows
		 *
		 * @param 		{event} 	e 		Event
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		clearUserbarCache: function (e, data) {
			this.loaded[ data.type ] = false;
		},

		/**
		 * Loads one of the nav bar menus
		 *
		 * @param 		{string} 	type		Type of content being loaded
		 * @param 		{string} 	url 		URL to fetch the content
		 * @param 		{string} 	contentID 	Prefix used for the elements for this type (e.g. elInbox)
		 * @returns 	{void}
		 */
		_loadMenu: function (type, url, contentID) {
			if( !this.loaded[ type ] ){
				var self = this;
				var ajaxObj = ips.getAjax();

				$('[data-role="' + contentID + 'List"]')
					.html('')
					.css( { height: '100px' } )
					.addClass('ipsLoading');

				ajaxObj( url, { dataType: 'json' } )
					.done( function (returnedData) {
	 					
	 					// Add this content to the menu
						$('[data-role="' + contentID + 'List"]')
							.css( { height: 'auto' } )
							.removeClass('ipsLoading')
							.html( returnedData.data );

						// Remember we've loaded it
						self.loaded[ type ] = true;

						// Remove the notification count
						if( contentID != 'reports' ){
							var thisTotal = $('[data-notificationType="' + contentID + '"]').html();
							var globalCount = parseInt( $('[data-notificationType="total"]').html() );

							ips.utils.anim.go( 'fadeOut', $('[data-notificationType="' + contentID + '"]') );

							$('[data-notificationType="total"]').html( globalCount - parseInt( thisTotal ) );

							if( globalCount - parseInt( thisTotal ) <= 0 ){
								ips.utils.anim.go( 'fadeOut', $('[data-notificationType="total"]') );
							}
						}						

						$( document ).trigger( 'contentChange', [ $('[data-role="' + contentID + 'List"]') ] );
					})
					.fail( function () {
						//self.trigger('topicLoadError');
					});
			}
		}

	});

}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.webshare.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.webshare.js - Controller for WebShare API
 *
 * Author: Ryan Ashbrook
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.core.webshare', {
		/**
		 * Initialize controller events
		 * Sets up the events from the view that this controller will handle
		 *
		 * @returns 	{void}
		 */
		initialize: function () {
			if ( navigator.share ){
				this._render();
				this.on( 'click', this.initShare );
			}
		},
		
		/**
		 * Render share API
		 *
		 * @returns	{void}
		 */
		_render: function() {
			$('[data-role=&quot;webShare&quot;]').removeClass( 'ipsHide' );
		},
		
		/**
		 * Event handler for WebShare
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		initShare: function (e) {
			try {
				//var data = $.parseJSON( this.scope.attr( 'data-webShareData' ) );
				navigator.share( {
					title: this.scope.attr( 'data-webShareTitle' ),
					text: this.scope.attr( 'data-webShareText' ),
					url: this.scope.attr( 'data-webShareUrl' )
				} );
			} catch (err) {
				Debug.log(&quot;Failed to use web share API: &quot;, err);
			}
		}

	});

}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.2fa.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.2fa.js - Two-factor authentication controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.global.core.2fa', {

		initialize: function () {
			this.on( 'tabShown', this.tabShown );
			this.on( 'tabChanged', this.tabChanged );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			// Give ourselves an appropriate z-index
			this.scope.css({
				zIndex: ips.ui.zIndex()
			});

			// Focus into the first visible text box
			this.scope.find('input[type=&quot;text&quot;]:visible').first().focus();
		},

		/**
		 * Event handler for tab being toggled. Used to focus first text field in the current tab.
		 *
		 * @returns {void}
		 */
		tabShown: function (e, data) {
			this.scope.find('input[type=&quot;text&quot;]:visible').first().focus();
		},

		/**
		 * Event handler for tab being changed.
		 * Allows us to check the correct radio button for the method
		 *
		 * @returns {void}
		 */
		tabChanged: function (e, data) {
			if( data.tab ){
				data.tab.find('input[name=&quot;mfa_method&quot;]').prop('checked', true);
			}
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.authyOneTouch.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.authyOneTouch.js - Authy OneTouch controller
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.global.core.authyOneTouch', {
		initialize: function () {
			var scope = $(this.scope);
			setInterval( function(){
				ips.getAjax()( scope.closest('form').attr('action'), { data: { 'onetouchCheck': scope.find('[data-role=&quot;onetouchCode&quot;]').val() } } )
					.done(function( response ) {
						if ( response.status == 1 ) {
							scope.closest('form').submit();
						}
					});
			}, 3000 );
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.coverPhoto.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.coverPhoto.js - Controller for cover photos
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.core.coverPhoto', {

		_image: null,
		_repositioning: false,
		_existingPosition: 0,
		_tooltip: null,
		_expandedCover: false,
		_containerHeight: 0,

		initialize: function () {
			var self = this;
			this.on( 'menuItemSelected', function(e, data){
				switch( $( data.originalEvent.target ).attr('data-action') ){
					case 'removeCoverPhoto':
						self.removePhoto(data);
						break;
					case 'positionCoverPhoto':
						self.positionPhoto(data.originalEvent);
						break;
				}
			});
			
			this.on( 'click', '[data-action="savePosition"]', this.savePosition );
			this.on( 'click', '[data-action="cancelPosition"]', this.cancelPosition );
			$( window ).on( 'resize', _.bind( this.resizeWindow, this ) );

			this.on( 'click', '[data-action="toggleCoverPhoto"]', this.toggleCoverPhoto );

			this.setup();
		},

		/**
		 * Setup method.
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this._initCoverPhotoImage();
			this._containerHeight = this.scope.outerHeight();

			// Remove manage button if showing in a widget
			if ( this.scope.closest('.cWidgetContainer').length ){
				$('#elEditPhoto').hide();
			}

			// Get the URL bits and see if we're immediately going into position mode
			var doPosition = ips.utils.url.getParam('_position');

			if( !_.isUndefined( doPosition ) ){
				this.positionPhoto();
			}
						
			this.scope.find('a[data-action="positionCoverPhoto"]').parent().removeClass('ipsHide');
		},

		/**
		 * Initialize the cover photo image. Calls this._positionImage when the cover photo is loaded.
		 *
		 * @returns 	{void}
		 */
		_initCoverPhotoImage: function() {
			var self = this;
			this._image = this.scope.find('.ipsCoverPhoto_photo');
			this._offset = this.scope.attr('data-coverOffset') || 0;

			if( !this._image.attr('data-positioned') ){
				this._image.css({ opacity: "0.0001" });
			}

			// Set up lazy loaded cover photo, providing a callback to fade
			// in the image after loading
			if( this._image.length ){
				var position = _.bind( this._positionImage, self );

				if( this._image.is('[data-src]') && !this._image.is('[data-loaded]') ){
					ips.utils.lazyLoad.observe( this.scope.find('.ipsCoverPhoto_photo'), {
						imgLoadedCallback: function () {
							position();
						} 
					});
				} else {
					this._image.imagesLoaded( position );
				}
			}
		},

		/**
		 * Event handler for the window resizing
		 *
		 * @returns 	{void}
		 */
		resizeWindow: function () {
			if ( this._expandedCover )
			{
				this.toggleCoverPhoto();
			}

			this._initCoverPhotoImage();
		},

		/**
		 * Removes the cover photo
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		removePhoto: function (data) {			
			data.originalEvent.preventDefault();
			var self = this;

			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'warn',
				message: ips.getString('confirmRemoveCover'),
				callbacks: {
					ok: function () {
						ips.getAjax()( $( data.originalEvent.target ).attr('href') + '&wasConfirmed=1' )
							.done( function () {

								ips.utils.anim.go( 'fadeOut', self._image )
									.done( function () {
										ips.ui.flashMsg.show( ips.getString('removeCoverDone') );
									});

								data.menuElem.find('[data-role="photoEditOption"]').hide();
							})
							.fail( function (err) {
								window.location = $( data.originalEvent.target ).attr('href');
							});
					}
				}
			});
		},

		/**
		 * Save a new position of the cover photo
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		savePosition: function (e) {
			e.preventDefault();

			// Get the natural size
			var natHeight = ips.utils.position.naturalHeight( this._image );
			var realHeight = this._image.outerHeight();
			var topPos = parseInt( this._image.css('top') ) * -1;
			var percentage = ( topPos / realHeight ) * 100;
			var newOffset = Math.floor( ( natHeight / 100 ) * percentage );

			this._offset = newOffset;
			this.scope.attr( 'data-coverOffset', newOffset );
			
			ips.getAjax()( this.scope.attr('data-url') + '&do=coverPhotoPosition' + '&offset=' + newOffset )
				.fail( function (err) {
					this.scope.attr('data-url') + '&do=coverPhotoPosition' + '&offset=' + newOffset;
				});
							
			this._resetImage();
		},

		/**
		 * Cancels changing the position of the image
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		cancelPosition: function (e) {
			e.preventDefault();

			this._image.css( {
				top: this._existingPosition + 'px',
			});

			this._resetImage();
		},

		/**
		 * Starts the 'editing' state of the cover photo
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		positionPhoto: function (e) {

			if( !_.isUndefined( e ) ){
				e.preventDefault();	
			}
			
			var self = this;

			this.scope.find('[data-hideOnCoverEdit]').css({ visibility: 'hidden' });

			this._image.css({
				cursor: 'move'
			});

			this._repositioning = true;
			this._existingPosition = parseInt( this._image.css('top') ) || 0;

			this.scope.find('.ipsCoverPhoto_container').append( ips.templates.render('core.coverPhoto.controls') );

			this._showTooltip();

			ips.loader.get( ['core/interface/jquery/jquery-ui.js'] ).then( function () {
				self._image.draggable({ axis: 'y', scroll: false, stop: _.bind( self._dragStop, self ) });
			});
		},

		/**
		 * Positions the image so that the offset stays correct regardless of actual image size
		 *
		 * @returns 	{void}
		 */
		_positionImage: function () {

			if( !this._image.length ){
				return;
			}
			
			// Get the natural size
			var natHeight = ips.utils.position.naturalHeight( this._image );
			var realHeight = this._image.outerHeight();

			if( this._offset === 0){
				this._image.animate({
					opacity: "1"
				}, 'fast');

				return;
			}

			// The provided offset is relative to the natural width, so we need to work out what it is for the actual width
			var percentage = ( ( this._offset * 1 ) / natHeight ) * 100;
			var adjustedOffset = ( Math.floor( ( realHeight / 100 ) * percentage ) * -1 );

			// set bounds so image is never higher than bottom of the viewing area
			var minBottom = ( realHeight - this.scope.outerHeight() ) * -1;
			
			if( adjustedOffset < minBottom ){
				adjustedOffset = minBottom;
			}

			this._image
				.attr('data-positioned', true)
				.css({
					position: 'absolute',
					left: "0",
					top: adjustedOffset + 'px',
				})
				.animate({
					opacity: "1"
				}, 'fast');
		},

		/**
		 * Cancels the 'editing' state of the cover photo
		 *
		 * @returns 	{void}
		 */
		_resetImage: function () {
			if( this._image.draggable ){
				this._image.draggable('destroy');
			}

			this._image.css( {
				cursor: 'default'
			});

			this.scope.find('.ipsCoverPhoto_container [data-role="coverPhotoControls"]').remove();
			this.scope.find('[data-hideOnCoverEdit]').css({ visibility: 'visible' });

			this._hideTooltip();

			// Reset the URL so refreshing the page does not re-trigger repositioning
			History.pushState( "", document.title, ips.utils.url.removeParam( 'csrfKey', this.scope.attr('data-url') ) );
		},

		/**
 		 * Shows a tooltip on the autocomplete with the provided message
		 *
		 * @param 	{string} 	msg 	Message to show
		 * @returns {void}
		 */
		_showTooltip: function (msg) {
			if( !this._tooltip ){
				this._buildTooltip();
			}

			this._tooltip.hide().text( ips.getString('dragCoverPhoto') );

			this._positionTooltip();
		},

		/**
 		 * Hides the tooltip
		 *
		 * @returns {void}
		 */
		_hideTooltip: function () {
			if( this._tooltip && this._tooltip.is(':visible') ){
				ips.utils.anim.go( 'fadeOut', this._tooltip );
			}
		},

		/**
 		 * Positions the tooltip over the autocomplete
		 *
		 * @returns {void}
		 */
		_positionTooltip: function () {
			var positionInfo = {
				trigger: this.scope.find('.ipsCoverPhoto_container'),
				target: this._tooltip,
				center: true,
				above: true
			};

			var tooltipPosition = ips.utils.position.positionElem( positionInfo );

			this._tooltip.css({
				left: tooltipPosition.left + 'px',
				top: tooltipPosition.top + 'px',
				position: ( tooltipPosition.fixed ) ? 'fixed' : 'absolute',
				zIndex: ips.ui.zIndex()
			});

			if( tooltipPosition.location.vertical == 'top' ){
				this._tooltip.addClass('ipsTooltip_top');
			} else {
				this._tooltip.addClass('ipsTooltip_bottom');
			}

			this._tooltip.show();
		},

		/**
 		 * Builds the tooltip element
		 *
		 * @param 	{string} 	msg 	Message to show
		 * @returns {void}
		 */
		_buildTooltip: function () {
			// Build it from a template
			var tooltipHTML = ips.templates.render( 'core.tooltip', {
				id: 'elCoverPhotoTooltip'
			});

			// Append to body
			ips.getContainer().append( tooltipHTML );

			this._tooltip = $('#elCoverPhotoTooltip');
		},

		/**
		 * Event handler for when dragging stops
		 * Checks that the cover photo is within the bounds of the header (i.e. still visible)
		 *
		 * @returns 	{void}
		 */
		_dragStop: function () {
			var imageTop = parseInt( this._image.css('top') );

			if( imageTop > 0 ) {
				this._image.css({ top: "0", bottom: 'auto', position: 'absolute' });
			} else {
				var containerHeight = this.scope.find('.ipsCoverPhoto_container').outerHeight();
				var imageHeight = this._image.outerHeight();

				if( ( imageTop + imageHeight ) < containerHeight ){
					this._image.css({ top: 'auto', bottom: "0", position: 'absolute' });
				}
			}
		},

		/**
		 * Toggles cover photo to full height
		 *
		 * @returns 	{void}
		 */
		toggleCoverPhoto: function () {
			var imageHeight = this._image.outerHeight();

			if( this._expandedCover == false ) {
				this._existingPosition = parseInt( this._image.css('top') ) || 0;

				this.scope.animate({
					height: ( imageHeight + this._existingPosition ) + 'px',
				});

				this._expandedCover = true;
			} else {
				this.scope.animate({
					height: this._containerHeight + 'px'
				});

				this._expandedCover = false;
			}

			this.scope.toggleClass('ipsCoverPhotoMinimal');
		},
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.cropper.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.cropper.js - Cropping controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.global.core.cropper', {

		_image: null,
		_coords: {},

		initialize: function () {
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			var self = this;

			this._image = this.scope.find('[data-role=&quot;profilePhoto&quot;]');
			this._coords = {
				topLeftX: this.scope.find('[data-role=&quot;topLeftX&quot;]'),
				topLeftY: this.scope.find('[data-role=&quot;topLeftY&quot;]'),
				bottomRightX: this.scope.find('[data-role=&quot;bottomRightX&quot;]'),
				bottomRightY: this.scope.find('[data-role=&quot;bottomRightY&quot;]'),
			};

			this._image.css({
				maxWidth: '100%'
			});

			ips.loader.get( ['core/interface/cropper/cropper.min.js'] ).then( function () {
				self._image.imagesLoaded( _.bind( self._startCropper, self ) );
 			});
		},

		/**
		 * Starts the cropping function, called after the image has loaded
		 *
		 * @returns {void}
		 */
		_startCropper: function () {
			var self = this;

			var width = this._image.width();
			var height = this._image.height();

			// Resize the wrapper
			this._image.closest('[data-role=&quot;cropper&quot;]').css({
				width: width + 'px',
				height: height + 'px'
			});

			// Initialize cropper
			var cropper = new Cropper( this._image.get(0), {
				aspectRatio: 1 / 1,
				autoCropArea: 0.9,
				responsive: true,
				zoomOnWheel: false,
				crop: function ( e ) {					
					self._coords.topLeftX.val( e.detail.x );
					self._coords.topLeftY.val( e.detail.y );
					self._coords.bottomRightX.val( e.detail.width + e.detail.x );
					self._coords.bottomRightY.val( e.detail.height + e.detail.y );
				}
			});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.datetime.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.datetime.js - Controller to update the contents of cached <time> tags with the appropriate timezone
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.core.datetime', {

		initialize: function () {
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			var formatObject = { format: $(this.scope).attr('data-format') };
			
			var localeTimeFormat = ips.utils.time.localeTimeFormat( $('html').attr('lang') );
			if ( localeTimeFormat.meridiem ) {
				formatObject.meridiem = localeTimeFormat.meridiem;
			}
			
			$(this.scope).text( ips.utils.time.formatTime( new Date( $(this.scope).attr('data-time') ), formatObject ) );
		}
		
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.embeddedvideo.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * IPS Social Suite 4
 * (c) 2018 Invision Power Services - http://www.invisionpower.com
 *
 * ips.core.embeddedVideo.js - Simple controller to swap an embedded video out for the link if the source is not supported
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.global.core.embeddedvideo', {
		
		initialize: function () {
			// Normally, videos will be lazy loaded automatically in content. However,
			// we need to retain this controller for legacy content, as well as saved
			// editor content which won't yet have the lazy load attributes applied.

			// The code here is slightly different, since we dealing directly with the src 
			// attribute here rather than our data-video-src lazyload attributes. Be sure
			// any future functionality changes are applied in both areas.
			var video = this.scope.get(0);
			var canPlay = false;
			
			this.scope.find('source').each( function () {
				if( video.canPlayType( $(this).attr('type') ) ){
					canPlay = true;
				}
			});	
			
			if( !canPlay ) {
				if( this.scope.find('embed').length ){
					this.scope.replaceWith( this.scope.find('embed') );
				} else {
					this.scope.replaceWith( $(this.scope).find('a') );
				}
			}
		}		
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.framebust.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * IPS Social Suite 4
 * (c) 2013 Invision Power Services - http://www.invisionpower.com
 *
 * ips.core.framebust.js - Frame Busting
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.global.core.framebust', {

		initialize: function () {
			if ( top != self ) {
				$(this.scope).html('');
			}
		}
		
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.genericTable.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.genericTable.js - Controller for ACP tables that can be filtered and live-searched
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.core.genericTable', {

		_curSearchValue: '',
		_urlParams: {},
		_baseURL: '',
		_searchField: null,
		_timer: null,
		_currentValue: '',

		initialize: function () {
			this.on( 'paginationClicked paginationJump', this.paginationClicked );
			this.on( 'click', '[data-action="tableFilter"]', this.changeFiltering );
			this.on( 'menuItemSelected', '[data-role="tableFilterMenu"]', this.changeFilteringFromMenu );
			this.on( 'focus', '[data-role="tableSearch"]', this.startLiveSearch );
			this.on( 'blur', '[data-role="tableSearch"]', this.endLiveSearch );
			this.on( 'click', '[data-action="tableSort"]', this.changeSorting );
			this.on( 'menuItemSelected', '#elSortMenu', this.sortByMenu );
			this.on( 'menuItemSelected', '#elOrderMenu', this.orderByMenu );
			this.on( 'refreshResults', this._getResults );
			this.on( 'buttonAction', this.buttonAction );

			History.Adapter.bind( window, 'statechange', _.bind( this.stateChange, this ) );

			this.setup();	
		},

		/**
		 * Setup method
		 * Builds the initial page parameters, and replaces the current state with these initial
		 * values.
		 *
		 * @returns {void}
		 */
		setup: function () {
			this._baseURL = this.scope.attr('data-baseurl');
			if ( this.scope.attr('data-baseurl').match(/\?/) ) {
				this._baseURL += '&';
			} else {
				this._baseURL += '?';
			}
			
			
			this._searchField = this.scope.find('[data-role="tableSearch"]');

			// Get the initial page parameters
			var sort = this._getSortValue();

			this._urlParams = {
				filter: this._getFilterValue() || '',
				sortby: sort.by || '',
				sortdirection: sort.order || '',
				quicksearch: this._getSearchValue() || '',
				page: ips.utils.url.getParam('page') || 1
			};

			// Replace the current state to store our params object
			History.replaceState( this._urlParams, document.title, window.location.href );

			// Show the search box
			this.scope.find('[data-role="tableSearch"]').removeClass('ipsHide').show();
		},

		buttonAction: function (e, data) {
			this._getResults();
		},

		/**
		 * Handles events from the sort menu (shown only on mobile)
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		sortByMenu: function (e, data) {
			data.originalEvent.preventDefault();

			this._updateSort( {
				by: data.selectedItemID
			});
		},

		/**
		 * Handles events from the order menu (shown only on mobile)
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		orderByMenu: function (e, data) {
			data.originalEvent.preventDefault();

			this._updateSort( {
				order: data.selectedItemID
			});
		},

		/**
		 * Responds to state changes triggered by History.js
		 *
		 * @returns {void}
		 */
		stateChange: function () {
			var state = History.getState();

			// Because tables can exist alongside other widgets that manage the URL, we use the controller property
			// of the state data to identify states set by this controller only.
			// If that property doesn't exist, or if it doesn't match us, just ignore it.
			if( _.isUndefined( state.data.controller ) || state.data.controller != 'genericTable' ) {
				return;
			}

			// See what's changed so we can update the display
			if( !_.isUndefined( state.data.filter ) && state.data.filter != this._urlParams.filter ){
				this._updateFilter( state.data.filter );
			}

			if( ( !_.isUndefined( state.data.sortby ) && !_.isUndefined( state.data.sortdirection ) ) && 
					( state.data.sortby != this._urlParams.sortby || state.data.sortdirection != this._urlParams.sortdirection ) ){
				this._updateSort( {
					by: state.data.sortby,
					order: state.data.sortdirection 
				});
			}

			if( !_.isUndefined( state.data.quicksearch ) && state.data.quicksearch != this._urlParams.quicksearch ){
				this._updateSearch( state.data.quicksearch );
			}

			if( !_.isUndefined( state.data.page ) && state.data.page != this._urlParams.page ){
				this._updatePage( state.data.page );
			}

			// Update data
			this._urlParams = state.data;

			// Get le new results
			this._getResults();
		},

		/**
		 * Update the current URL
		 *
		 * @param	{object} 	newParams 		New values to use in the search
		 * @returns {void}
		 */
		updateURL: function (newParams) {
			_.extend( this._urlParams, newParams );
			
			var tmpStateData = _.extend( _.clone( this._urlParams ), { controller: 'genericTable' } );
			var newUrlParams = this._getURL();
			
			if ( newUrlParams.match( /page=\d/ ) ){
				this._baseURL = this._baseURL.replace( /page=\d+?(&|\s)/, '' );
			}
			
			var newUrl = this._baseURL + newUrlParams;

			if( newUrl.slice(-1) == '?' )
			{
				newUrl = newUrl.substring( 0, newUrl.length - 1 );
			}

			History.pushState( 
				tmpStateData,
				document.title,
				newUrl
			);
		},

		/**
		 * Builds a param string from values in this._urlParams, excluding empty values
		 *
		 * @returns {string}	Param string
		 */
		_getURL: function () {
			var tmpUrlParams = {};

			for( var i in this._urlParams ){
				if( this._urlParams[ i ] != '' && i != 'controller' && ( i != 'page' || ( i == 'page' && this._urlParams[ i ] != 1 ) ) ){
					tmpUrlParams[ i ] = this._urlParams[ i ];
				}
			}

			return $.param( tmpUrlParams );
		},

		/**
		 * Event handler for pagination widget
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		paginationClicked: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}
			
			if( data.pageNo != this._urlParams.page ){
				this.updateURL( { page: data.pageNo } );
			}
		},

		/**
		 * Update classname on new active page. Pagination actually gets overwritten
		 * by the ajax response, but by updating the class here, it feels more immediate
		 * for the user.
		 *
		 * @param	{number} 	newPage 		New active page number
		 * @returns {void}
		 */
		_updatePage: function (newPage) {
			this.scope
				.find('[data-role="tablePagination"] [data-page]')
					.removeClass('ipsPagination_pageActive')
				.end()
				.find('[data-page="' + newPage + '"]')
					.addClass('ipsPagination_pageActive');
		},

		/**
		 * Event handler for choosing a new filter
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		changeFiltering: function (e) {
			e.preventDefault();
			var newFilter = $( e.currentTarget ).attr('data-filter');

			// Select the one that was clicked, unselect others
			this._updateFilter( newFilter );

			if( newFilter != this._urlParams.filter ){
				this.updateURL( { 
					filter: newFilter,
					page: 1
				});
			}
		},
		
		/**
		 * Event handler for choosing a new filter from a dropdown menu
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		changeFilteringFromMenu: function (e,data) {
			var newFilter = $( data.originalEvent.target ).closest('li').attr('data-filter');
			
			// Select the one that was clicked, unselect others
			this._updateFilter( newFilter );

			if( newFilter != this._urlParams.filter ){
				this.updateURL( { 
					filter: newFilter,
					page: 1
				});
			}
		},

		/**
		 * Updates element classnames for filtering
		 *
		 * @param	{string} 	newFilter 		Filter ID of new filter to select
		 * @returns {void}
		 */
		_updateFilter: function (newFilter) {
			this.scope
				.find('[data-role="tableSortBar"] [data-action="tableFilter"] a')
					.removeClass('ipsButtonRow_active')
				.end()
				.find('[data-action="tableFilter"][data-filter="' + newFilter + '"] a')
					.addClass('ipsButtonRow_active');
		},

		/**
		 * Focus event handler for live search box
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		startLiveSearch: function (e) {
			this._timer = setInterval( _.bind( this._checkSearchValue, this ), 500 );
		},

		/**
		 * Blur event handler for live search box
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		endLiveSearch: function (e) {
			clearInterval( this._timer );
		},

		/**
		 * Determines whether the search field value has changed from the last loop run,
		 * and updates the URL if it has
		 *
		 * @returns {void}
		 */
		_checkSearchValue: function () {
			var val = this._searchField.val();

			if( this._currentValue != val ){
				this.updateURL({
					quicksearch: val,
					page: 1
				});

				this._currentValue = val;
			}
		},

		/**
		 * Updates the search field with a provided value
		 *
		 * @param	{string} 	searchValue 		Value to update
		 * @returns {void}
		 */
		_updateSearch: function (searchValue) {
			this._searchField.val( searchValue );
		},

		/**
		 * Event handler for choosing new sort column/order
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		changeSorting: function (e) {
			e.preventDefault();
			var cell = $( e.currentTarget );
			var order = '';

			// Apply asc or desc classnames to the cell, depending on its current state
			if( cell.hasClass('ipsTable_sortableActive') ){
				order = ( cell.hasClass('ipsTable_sortableDesc') ) ? 'asc' : 'desc';
			} else {
				order = ( cell.hasClass('ipsTable_sortableDesc') ) ? 'desc' : 'asc';
			}

			this._updateSort( {
				by: cell.attr('data-key'),
				order: order
			});
		},

		/**
		 * Updates the sorting order classnames
		 *
		 * @param 	{string} 	by 			Key name of sort by value
		 * @param 	{string} 	direction	asc or desc order value
		 * @returns {void}
		 */
		_updateSort: function ( data ) {
			var directions = 'ipsTable_sortableAsc ipsTable_sortableDesc';
			var current = this._getSortValue();

			if( !data.by ){
				data.by = current.by;
			}

			if( !data.order ){
				data.order = current.order;
			}

			// Do the cell headers
			this.scope
				.find('[data-role="table"] [data-action="tableSort"]')
					.removeClass('ipsTable_sortableActive')
					.removeAttr('aria-sort')
				.end()
				.find('[data-action="tableSort"][data-key="' + data.by + '"]')
					.addClass('ipsTable_sortableActive')
					.removeClass( directions )
					.addClass( 'ipsTable_sortable' + data.order.charAt(0).toUpperCase() + data.order.slice(1) )
					.attr( 'aria-sort', ( data.order == 'asc' ) ? 'ascending' : 'descending' );

			// Do the menus
			$('#elSortMenu_menu, #elOrderMenu_menu')
				.find('.ipsMenu_item')
					.removeClass('ipsMenu_itemChecked')
				.end()
				.find('[data-ipsMenuValue="' + data.by + '"], [data-ipsMenuValue="' + data.order + '"]')
					.addClass('ipsMenu_itemChecked');

			this.updateURL( {
				sortby: data.by,
				sortdirection: data.order,
				page: 1
			});
		},

		/**
		 * Fetches new results from the server, then calls this._updateTable to update the
		 * content and pagination. Simply redirects to URL on error.
		 *
		 * @returns {void}
		 */
		_getResults: function () {
			var self = this;

			ips.getAjax()( this._baseURL + this._getURL() + '&' + this.scope.attr('data-resort') + '=1', {
				dataType: 'json',
				showLoading: true
			})
				.done( function (response) {
					self._updateTable( response );
				})
				.fail( function (jqXHR, textStatus, errorThrown) {
					if( Debug.isEnabled() ){
						Debug.error( "Ajax request failed (" + status + "): " + errorThrown );
						Debug.error( jqXHR.responseText );
					} else {
						// rut-roh, we'll just do a manual redirect
						window.location = self._baseURL + self._getURL();
					}
				});
		},

		/**
		 * Update the content and pagination elements
		 *
		 * @param	{object} 	response 		JSON object containing new HTML pieces
		 * @returns {void}
		 */
		_updateTable: function (response) {
			// Table body
			this.scope.find('[data-role="tableRows"]').html( response.rows );
			// Pagination
			this.scope.find('[data-role="tablePagination"]')
				.toggle( ( response.pagination && response.pagination.trim() !== "" ) || !_.isUndefined( this.scope.find('[data-role="tablePagination"]').attr('data-showEmpty') ) )
				.html( response.pagination || "" );

			// New content loaded, so trigger contentChange event
			$( document ).trigger( 'contentChange', [ this.scope ] );
		},

		/**
		 * Returns the current filter value
		 *
		 * @returns {string}
		 */
		_getFilterValue: function () {
			var sortBar = this.scope.find('[data-role="tableSortBar"]');

			if( !sortBar.length ){
				return '';
			}

			return sortBar.find('.ipsButtonRow_active').closest('[data-filter]').attr('data-filter');
		},

		/**
		 * Returns the current sort by and sort order value
		 *
		 * @returns {object}	Object containing by and order keys
		 */
		_getSortValue: function () {
			var sortBy = this.scope.find('[data-role="table"] thead .ipsTable_sortable.ipsTable_sortableActive');
			var sortOrder = 'desc';

			if( sortBy.hasClass('ipsTable_sortableAsc') ){
				sortOrder = 'asc';
			}

			return { by: sortBy.attr('data-key'), order: sortOrder };
		},

		/**
		 * Gets the current search value, either from the URL or contents of the search box
		 *
		 * @returns {string}
		 */
		_getSearchValue: function () {
			if( ips.utils.url.getParam('quicksearch') ){
				return ips.utils.url.getParam('quicksearch');
			}

			return this.scope.find('[data-role="tableSearch"]').val();
		},

		/**
		 * Replaces a row in the table with the provided contents
		 *
		 * @param 	{element} 	target 		The element used as our reference inside the row we're replacing, or the row itself
		 * @param 	{string}	contents 	The HTML with which the row will be replaced
		 * @returns {void}
		 */
		_actionReplace: function (target, contents) {
			// Find the table row this applies to
			var tr = $( target ).closest( 'tr' );
			var prevElem = tr.prev();

			tr.replaceWith( contents );

			// Let document know. We can't use our tr variable here, because that references the old (removed) row.
			// So trigger it on prevElem.next() instead
			$( document ).trigger( 'contentChange', [ prevElem.next() ] );
		}
	});
}(jQuery, _));	]]></file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.googleAuth.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.googleAuth.js - Google Authenticator controller
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.core.googleAuth', {
		initialize: function () {
			this.on( 'click', '[data-action="showManual"]', this.showManual );
			this.on( 'click', '[data-action="showBarcode"]', this.showBarcode );
			
			var waitUntil = $(this.scope).attr('data-waitUntil');
			if ( waitUntil > Math.floor( Date.now() / 1000 ) ) {
				this.showWait();
			}
		},

		/**
		 * Show the manual instructions for Google Auth
		 *
		 * @returns {void}
		 */
		showManual: function () {
			this.scope.find('[data-role="barcode"]').hide();
			this.scope.find('[data-role="manual"]').show();
		},
		
		/**
		 * Show the barcode for for Google Auth
		 *
		 * @returns {void}
		 */
		showBarcode: function () {
			this.scope.find('[data-role="barcode"]').show();
			this.scope.find('[data-role="manual"]').hide();
		},
		
		/**
		 * Show the waiting bar
		 *
		 * @returns {void}
		 */
		showWait: function () {
			this.scope.find('[data-role="codeWaiting"]').show();
			this.scope.find('[data-role="codeInput"]').hide();
			
			var waitUntil = $(this.scope).attr('data-waitUntil') * 1000;
			var start = Date.now();
			
			var progressBar = $(this.scope).find('[data-role="codeWaitingProgress"]');
			var interval = setInterval( function(){
				if ( Date.now() >= waitUntil ) {
					clearInterval(interval);
					this.showInput();
				}
				
				progressBar.css( 'width', ( ( 100 - ( 100 / ( waitUntil - start ) * ( waitUntil - Date.now() ) ) ) ) + '%' );
			}.bind(this), 100 );
		},
		
		/**
		 * Show the input box
		 *
		 * @returns {void}
		 */
		showInput: function () {
			this.scope.find('[data-role="codeWaiting"]').hide();
			this.scope.find('[data-role="codeInput"]').show();
			this.scope.find('input').focus();
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.googlemap.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.overview.nearMe.js - Controller for near me
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined) {
    "use strict";

    ips.controller.register('ips.core.map.googlemap', {
        initialize: function() {
            this.setup();
        },

        setup: function() {
            let mapData = this.scope.data().mapData;
            if ( typeof mapData === 'string' ) {
                mapData = JSON.parse(mapData);
            }
            if ('key' in mapData) {
                this._mapData = mapData;
                window.ipsGoogleMapsCallback = this.setupGoogleMaps.bind(this);
                window.ips.loader.get([`https://maps.googleapis.com/maps/api/js?key=${mapData.key}&callback=ipsGoogleMapsCallback&v=weekly`]); // will call this.setupGoogleMaps() when loaded
            }
        },

        setupGoogleMaps: function() {
            let position = { lat: this._mapData.lat, lng: this._mapData.long };
            let elem = this.scope.find('[data-role="mapContainer"]').get(0);
            let maptype = 'ROADMAP';
            if ( this._mapData.maptype && window.google.maps.MapTypeId[this._mapData.maptype.toUpperCase()] ) {
                maptype = this._mapData.maptype.toUpperCase();
            }

            let map = new window.google.maps.Map( elem, {
                center: position,
                zoom: this._mapData.zoom ? this._mapData.zoom * 8 : 15,
                scale: this._mapData.scale || undefined,
                mapTypeId: window.google.maps.MapTypeId[maptype.toUpperCase()]
            } );
            let marker = new window.google.maps.Marker({
                position,
                map
            });
        }
    });
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.licenseRenewal.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.global.licenseRenewal.js - License Renewal message
 *
 * Author: Stuart Silvester
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.global.core.licenseRenewal', {
				
		initialize: function () {
			this.on( 'click', '[data-action=&quot;notNow&quot;]', this.renewalPrompt );
			this.on(document, 'click', '[data-action=&quot;closeLicenseRenewal&quot;]', this.close);
		},

		renewalPrompt: function(e) {
			e.preventDefault();
			
			this._modal = ips.ui.getModal();
			
			if ( !$('body').find('[data-role=&quot;licenseRenewal&quot;]').length ) {
				$('body').append( ips.templates.render('licenseRenewal.wrapper') );
			}
			this._container = $('body').find('[data-role=&quot;licenseRenewal&quot;]').css({ opacity: &quot;0.001&quot;, transform: &quot;scale(0.8)&quot; });
			
			// Set the survey URL
			$('body').find( '[data-role=&quot;survey&quot;]').attr( 'href', $( this.scope ).attr( 'data-surveyUrl' ) );

			this._modal.css( { zIndex: ips.ui.zIndex() } );
			var self = this;

			// Animate the modal in
			setTimeout( function () {
				self._container.css( { zIndex: ips.ui.zIndex() } );
				self._container.animate({
					opacity: &quot;1&quot;,
					transform: &quot;scale(1)&quot;
				}, 'fast');
			}, 500);
			
			ips.utils.anim.go('fadeIn', this._modal);

		},

		/**
		 * Close the popup
		 *
		 * @returns {void}
		 */
		close: function (e) {
						
			if( $('body').find('[data-role=&quot;licenseRenewal&quot;]').find( 'input[type=checkbox][name=hideRenewalNotice]' ).is(':checked') )
			{
				var notification = $(this.scope).closest('.cNotification,.cAcpNotificationBanner');
				
				ips.getAjax()( $(this.scope).find('[data-action=&quot;notNow&quot;]').attr('href') ).done( function(response) {
					ips.utils.anim.go( 'fadeOut', notification );
					
					if ( !notification.closest('.cNotificationList').children().count ) {
						ips.utils.anim.go( 'fadeIn',  notification.closest('.cNotificationList').find('[data-role=&quot;empty&quot;]').removeClass('ipsHide') );
					}
					
					$('body').trigger('updateNotificationCount');
				});
			}

			$('body').find('[data-role=&quot;licenseRenewal&quot;]').animate({
				transform: &quot;scale(0.7)&quot;,
				opacity: &quot;0&quot;
			}, 'fast');

			ips.utils.anim.go('fadeOut', this._modal);
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.multipleRedirect.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.multipleRedirect.js - Facilitates multiple redirects
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.core.multipleRedirect', {
		_iterator: 0,
		
		initialize: function () {
			var self = this;
			this.setup();
		},

		setup: function () {
			this.scope.find('.ipsRedirect').removeClass('ipsHide');
			$('.ipsRedirect_manualButton').hide();
			this.step( this.scope.attr('data-url') + '&mr=0&_mrReset=1' );
		},
		
		step: function (url) {		
			this._iterator++;	
			var elem = this.scope;
			var self = this;
			ips.getAjax()( url )
				.done(function( response ) {
																									
					if( _.isObject( response ) && response.custom ){
						var originalContent = $( elem.html() ).removeClass('ipsHide');
						var newContent = elem.html(response.custom);
						newContent.find( '[data-action="redirectContinue"]' ).click(function(e){
							e.preventDefault();
							elem.html( originalContent );
							self.step( $(this).attr('href') );
						});
						$( document ).trigger( 'contentChange', [ elem ] );
						return;
					}

					// If a json object is returned with a redirect key, send the user there
					if( _.isObject( response ) && response.redirect ){
						window.location = response.redirect;
						return;
					}
					
					elem.find('[data-loading-text]').attr( 'data-loading-text', response[1] );

					/* The percent completion doesn't make logical sense if it exceeds 100% */
					if ( response[2] && response[2] < 100 ) {
						elem.find('[data-role="progressBarContainer"]').removeClass('ipsHide');
						elem.find('[data-role="loadingIcon"]').addClass('ipsHide');
						elem.find('[data-role="progressBar"]').css({ width: ( response[2] + '%' ) }).attr('data-progress', +( Math.round( response[2] + "e+2" )  + "e-2") + '%' );
					} else {
						elem.find('[data-role="progressBarContainer"]').addClass('ipsHide');
						elem.find('[data-role="loadingIcon"]').removeClass('ipsHide');
						elem.find('[data-role="progressBar"]').removeAttr('data-progress');
					}
										
					var newurl = elem.attr('data-url') + '&mr=' + self._iterator;

					if ( response.done && response.done == true ) {
						window.location = newurl;	
					} else if ( response.close && response.close == true ) {
						self.trigger( 'closeDialog' );
					} else {
						self.step( newurl );
					}
				})
				.fail(function(err){
					window.location = url;
				});
		}		
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.notificationList.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.notificationList.js - Controller for the notification list
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.global.core.notificationList', {

		initialize: function () {
			this.on( 'click', '[data-action=&quot;dismiss&quot;]', this.dismiss );
		},
		
		dismiss: function (e) {
			e.preventDefault();
			var notification = $(e.target).closest('[data-role=&quot;notificationBlock&quot;],.cAcpNotificationBanner');
			ips.ui.alert.show({
				type: 'confirm',
				message: ips.getString('acp_notification_hide_confirm'),
				icon: 'question',
				callbacks: {
					ok: function(){
						ips.getAjax()( notification.find('[data-action=&quot;dismiss&quot;]').attr('href') ).done( function(response) {
							notification.addClass('cNotification_hidden');
							ips.utils.anim.go( 'fadeOut', notification ).done(function(){
								if ( !notification.closest('.cNotificationList').children('[data-role=&quot;notificationBlock&quot;]:not(.cNotification_hidden)').length ) {
									ips.utils.anim.go( 'fadeIn',  notification.closest('.cNotificationList').find('[data-role=&quot;empty&quot;]').removeClass('ipsHide') );
								}
							});
							$('body').trigger('updateNotificationCount');
						});
					}
				}
			});
		}
	});
}(jQuery, _));
</file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.optionalAutocomplete.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.prefixedAutocomplete.js - Controller for prefix functionality
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.core.optionalAutocomplete', {

		_autoComplete: null,
		_closedTagging: false,

		initialize: function () {
			this.setup();
			this.on('click', '[data-action="showAutocomplete"]', this.showAutocomplete);
		},

		/**
		 * Setup method
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this._autoComplete = this.scope.find('[data-ipsAutocomplete]');

			if( !_.isUndefined( this._autoComplete.attr('data-ipsAutocomplete-minimized') ) ){
				return;
			}

			// Wrap the contents of the row so that we can hide it, and show on demand
			var div = $('<div data-role="autoCompleteWrapper" />').html( this.scope.contents() ).hide();
			this.scope.html( div );
			this.scope.append( ips.templates.render('core.autocomplete.optional', { langString: ips.getString( this._autoComplete.attr('data-ipsAutocomplete-lang') ) } ) );
			this.scope.closest('.ipsFieldRow').find('.ipsFieldRow_label').hide();

			// Get the options the autocomplete uses
			if( this._autoComplete.attr('data-ipsAutocomplete-freeChoice') && this._autoComplete.attr('data-ipsAutocomplete-freeChoice') == 'false' ){
				this._closedTagging = true;
			}
		},

		/**
		 * Toggles showing the autocomplete field
		 *
		 * @returns 	{void}
		 */
		showAutocomplete: function (e) {
			if( e ){
				e.preventDefault();
			}
			
			var self = this;
			var autoCompleteObj = ips.ui.autocomplete.getObj( this._autoComplete );

			this.scope.find('[data-action="showAutocomplete"]').hide();
			this.scope.find('[data-role="autoCompleteWrapper"]').show();
			this.scope.closest('.ipsFieldRow').find('.ipsFieldRow_label').show();

			setTimeout( function () {
				if( self._closedTagging ){
					self.scope.find('[data-action="addToken"]').click();
				} else {
					autoCompleteObj.focus();
				}
			}, 100);
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.prefixedAutocomplete.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.prefixedAutocomplete.js - Controller for prefix functionality
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.core.prefixedAutocomplete', {

		initialize: function () {
			this.setup();

			this.on( 'autoCompleteReady', this.autoCompleteReady );
			this.on( 'tokenAdded', this.tokensChanged );
			this.on( 'tokenDeleted', this.tokensChanged );
			this.on( 'menuItemSelected', '[data-role="prefixButton"]', this.prefixSelected );

			// In case the UI module already issued the ready command, reissue it
			this.scope.find('[data-ipsAutocomplete]').trigger('reissueReady');
		},

		/**
		 * Setup method
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this._prefixRow = this.scope.find('[data-role="prefixRow"]');
			this._prefixValue = this.scope.find('[data-role="prefixValue"]');
			this._prefixButton = this.scope.find('[data-role="prefixButton"]');
			this._prefixMenu = this.scope.find('[data-role="prefixMenu"]');
		},

		/**
		 * Event handler called when autocomplete is ready and has generated all existing tokens
		 *
		 * @param 		{event} 	e 		Event object
		 * @param		{object}	data	Data object from the autocomplete widget
		 * @returns 	{void}
		 */
		autoCompleteReady: function (e, data) {
			var tokens = data.currentValues;

			// Do we need to show the prefix menu immediately?
			if( this._prefixValue && tokens.length ){
				this._prefixMenu.html( this._buildTokenList( tokens, this._prefixValue.val() ) );
				this._prefixButton.find('span').html( this._getPrefixText( _.escape( this._prefixValue.val() ) ) );
				this._prefixRow.show();
			}
		},
		
		/**
		 * Event handler for the autocomplete adding/removing tokens. Updates the menu, and shows the row if needed
		 *
		 * @param 		{event} 	e 		Event object
		 * @param		{object}	data	Data object from the menu widget
		 * @returns 	{void}
		 */
		tokensChanged: function (e, data) {			
			if( data.totalTokens > 0 && !this._prefixRow.is(':visible') ){
				ips.utils.anim.go( 'fadeIn', this._prefixRow );
			} else if( data.totalTokens === 0 && this._prefixRow.is(':visible') ){
				ips.utils.anim.go( 'fadeOut', this._prefixRow );
				this._prefixRow.find('input[type="checkbox"]').prop( 'checked', false );
			}

			// Update button
			if( e && e.type == 'tokenDeleted' && data.token == this._prefixValue.val() ){
				this._prefixButton.find('span').html( ips.getString('selectPrefix') );
				this._prefixValue.val('');
			}

			// Get current value
			var value = this._prefixValue.val();
			var list = this._buildTokenList( data.tokenList, value );

			// Update list contents
			this._prefixMenu.html( list );
		},

		/**
		 * Event handler for when a prefix menu item is selected
		 *
		 * @param 		{event} 	e 		Event object
		 * @param		{object}	data	Data object from the menu widget
		 * @returns 	{void}
		 */
		prefixSelected: function (e, data) {
			data.originalEvent.preventDefault();

			var itemValue = ( data.selectedItemID == '-' ) ? '' : data.selectedItemID;
			var selectedText = this._getPrefixText( data.selectedItemID );

			this._prefixButton.find('span').html( selectedText );
			this._prefixValue.val( itemValue );

			this._prefixRow.find('input[type="checkbox"]').prop( 'checked', true );
		},

		/**
		 * Loops through provided tokens, building menu items for each
		 *
		 * @param 		{array} 	tokens 		Tokens array from the autocomplete widget
		 * @param		{string}	value 		Currently-selected item
		 * @returns 	{string}	Menu HTML
		 */
		_buildTokenList: function (tokens, value) {
			var output = '';
			
			output += ips.templates.render('core.menus.menuItem', {
				value: '',
				title: ips.getString('selectedNone'),
				checked: ( value == '' )
			});

			output += ips.templates.render('core.menus.menuSep');

			$.each( tokens, function (i, item) {
				output += ips.templates.render('core.menus.menuItem', {
					value: item,
					title: _.unescape( item ),
					checked: ( item == value )
				});
			});
			
			Debug.log( output );

			return output;
		},

		/**
		 * Gets the string for the prefix selector
		 *
		 * @param 		{string} 	prefix 		A selected prefix
		 * @returns 	{string}	
		 */
		_getPrefixText: function (prefix) {
			var selectedText = '';

			if( prefix && prefix != '-' ){
				selectedText = ips.getString( 'selectedPrefix', { tag: prefix } );
			} else {
				selectedText = ips.getString( 'selectedPrefix', { tag: ips.getString('selectedNone') } );
			}

			return selectedText;
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.table.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.table.js - Basic table controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.core.table', {

		_urlParams: {},
		_baseURL: '',
		_otherParams: [],
		_pageParam: 'page',
		_updateURL: true,
		_currentPage: 1,
		_seoPagination: false,
		_initialURL: '',
		_ajax: null,

		initialize: function () {
			this.on( 'paginationClicked paginationJump', this.paginationClicked );
			this.on( 'refreshResults', this.refreshResults );
			this.on( 'buttonAction', this.buttonAction );
			this.on( 'click', '[data-action="tableFilter"]', this.changeFiltering );
			this.on( 'menuItemSelected', '[data-role="tableFilterMenu"]', this.changeFilteringFromMenu );
			this.on( window, 'statechange', this.stateChange );
			this.on( 'click', 'tr[data-tableClickTarget]', this.rowClick );

			this.setup();	
		},

		setup: function () {
			if( this.scope.attr('data-pageParam') && this.scope.attr('data-pageParam') != 'page' ){
				this._pageParam = this.scope.attr('data-pageParam');
			}

			this._otherParams.push( this._pageParam );			
			this._baseURL = this.scope.attr('data-baseurl');
			this._originalBaseURL = this._baseURL;
			this._currentPage = ips.utils.url.getPageNumber( this._pageParam, this._baseURL );
			this._cleanUpBaseURL();

			if( this._baseURL.match(/\?/) ) {
				if( this._baseURL.slice(-1) != '?' ){
					this._baseURL += '&';	
				}				
			} else {
				this._baseURL += '?';
			}

			this._urlParams = this._getUrlParams();
			this._urlParams[ this._pageParam ] = this._currentPage;
			this._initialURL = window.location.href;

			Debug.log( this._currentPage );

			if( this.scope.closest('[data-disableTableUpdates]').length ){
				this._updateURL = false;
			}

			var tmpStateData = _.extend( _.clone( this._urlParams ), { controller: this.controllerID } );
			
			// Replace the current state to store our params object
			//History.replaceState( tmpStateData, document.title, window.location.href );

			/* Data Layer Stuff */
			try {
				if ( IpsDataLayerConfig && !window.IpsDataLayerConfig ) {
					/* Data Layer Page Number Property */
					this.scope.find( '[data-role="tablePagination"] [data-page]' ).click( function (e) {
						let target = e.currentTarget;
						if ( target.parentNode.classList.contains('ipsPagination_active') ) {
							return;
						}
						let page = Number( e.currentTarget.dataset['page'] );

						if ( isNaN(page) ) return;

						$('body').trigger('ipsDataLayerProperties', { _properties: {page_number: page} });
					});
				}
			} catch (e) {}
		},

		/**
		 * Responds to state changes triggered by History.js
		 *
		 * @returns {void}
		 */
		stateChange: function () {
			var state = History.getState();

			// Because tables can exist alongside other widgets that manage the URL, we use the controller property
			// of the state data to identify states set by this controller only.
			// If that property doesn't exist, or if it doesn't match us, just ignore it.
			if( ( _.isUndefined( state.data.controller ) || state.data.controller != this.controllerID ) && this._initialURL != state.url ) {
				return;
			}

			/*if( state.data.bypassState ){
				Debug.log('got state, but bypassing update');
				//Debug.log( state );
				//return;
			}*/

			Debug.log("stateChange:");
			Debug.log(state);

			this._handleStateChanges( state );

			// Update data
			this._urlParams = _.omit( state.data, 'bypassStateAdjustment' );

			// Gallery for instance stores a state change when closing the lightbox to adjust the URL, but this should not cause the table to reload
			if( !_.isUndefined( state.data.bypassStateAdjustment ) && state.data.bypassStateAdjustment ){
				Debug.log('got state, but bypassing update');
				return;
			}

			// Get the new results
			// If the initial URL matches the URL for this state, then we'll load results by URL instead 
			// of by object (since we don't have an object for the URL on page load)
			if( this._initialURL == state.url ){
				// Load our initial URL since the user clicked Back
				this._getResults( this._originalBaseURL );
			} else {
				this._getResults();
			}
		},

		/**
		 * Refresh table contents
		 *
		 * @returns {void}
		 */
		buttonAction: function () {
			this._getResults();
		},

		/**
		 * Refresh table contents
		 *
		 * @returns {void}
		 */
		refreshResults: function () {
			this._getResults();
		},

		/**
		 * Update the current URL
		 *
		 * @param	{object} 	newParams 		New values to use in the search
		 * @returns {void}
		 */
		updateURL: function (newParams) {
			_.extend( this._urlParams, newParams );

			var tmpStateData	= _.extend( _.clone( this._urlParams ), { controller: this.controllerID } );
			var newUrl			= this._baseURL + this._getURL();

			if( newUrl.slice(-1) == '?' ){
				newUrl = newUrl.substring( 0, newUrl.length - 1 );
			}

			if ( this._seoPagination == true ) {
				newUrl = ips.utils.url.pageParamToPath( newUrl, this._pageParam, newParams[ this._pageParam ] );
			}
			
			//Debug.log( tmpStateData );
			History.pushState( 
				tmpStateData,
				document.title,
				newUrl
			);
		},

		/**
		 * Event handler for pagination widget
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		paginationClicked: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}
			
			if( data.pageNo != this._urlParams[ this._pageParam ] ){
				var newObj = {};
				newObj[ this._pageParam ] = data.pageNo;
				this._seoPagination = data.seoPagination;
				
				this.updateURL( newObj );
			}
		},

		/**
		 * Event handler for choosing a new filter
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		changeFiltering: function (e) {
			e.preventDefault();
			var newFilter = $( e.currentTarget ).attr('data-filter');

			// Select the one that was clicked, unselect others
			this._updateFilter( newFilter );

			if( newFilter != this._urlParams.filter ){
				this.updateURL( { 
					filter: newFilter,
					page: 1
				});
			}
		},

		/**
		 * Event handler for choosing a new filter from a dropdown menu
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		changeFilteringFromMenu: function (e,data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}

			//var newFilter = $( data.originalEvent.target ).closest('li').attr('data-filter');
			var newFilter = data.selectedItemID || '';
			
			// Select the one that was clicked, unselect others
			this._updateFilter( newFilter );

			if( newFilter != this._urlParams.filter ){
				this.updateURL( { 
					filter: newFilter,
					page: 1
				});
			}
		},

		/**
		 * Cleans the base url of our default params
		 *
		 * @returns {void}
		 */
		_cleanUpBaseURL: function () {
			var urlObj = ips.utils.url.getURIObject( this._baseURL );
			
			var params = _.clone( urlObj.queryKey );
			var self = this;
			
			this._baseURL = urlObj.protocol + '://' + urlObj.host + ( urlObj.port ? ( ':' + urlObj.port ) : '' ) + urlObj.path + '?';
						
			// If we're using friendly URLs *without* rewriting, we need to 
			if( urlObj.file == 'index.php' ){
				_.each( params, function (val, key) {
					if( key.startsWith('/') ){
						self._baseURL += encodeURIComponent( key ).replace( /%2f/ig, '/' ); // We don't want '/' being encoded or it breaks URLs
						delete params[ key ];					
					}
				});

				this._baseURL += '&';
			}

			// Remove our default URL params
			_.each( _.extend( ['sortby', 'sortdirection', 'filter'], this._otherParams ), function (val) {
				if( !_.isUndefined( params[ val ] ) ){
					delete params[ val ];
				}
			});
			
			// Decode params as $.param() will encode it again (double encode)
			_.each( params, function( v, k ){
				delete params[k];
				params[ decodeURIComponent( k ).replace( /\+/g, ' ') ] = v.replace(/\+/g, ' ');
			});
		
			// When using index.php? URLs, a param key is the path /forums/2-forum/ but as the value is empty, params.length returns false
			if( ! _.isEmpty( params ) ){
				this._baseURL += decodeURIComponent( $.param( params ) );
			}

			// If the last character is &, we can remove that because it'll be added back later
			if( this._baseURL.slice(-1) == '&' ){
				this._baseURL = this._baseURL.slice( 0, -1)
			}
		},

		/**
		 * Checks whether any values in the provided state are different and need updating
		 *
		 * @param 	{object} 	state 	State from history.js
		 * @returns {void}
		 */
		_handleStateChanges: function (state) {
			// See what's changed so we can update the display
			if( !_.isUndefined( state.data.filter ) && state.data.filter != this._urlParams.filter ){
				this._updateFilter( state.data.filter );
			}

			if( ( !_.isUndefined( state.data.sortby ) && !_.isUndefined( state.data.sortdirection ) ) && 
					( state.data.sortby != this._urlParams.sortby || state.data.sortdirection != this._urlParams.sortdirection ) ){
				this._updateSort( {
					by: state.data.sortby,
					order: state.data.sortdirection 
				});
			}

			if( !_.isUndefined( state.data[ this._pageParam ] ) && state.data[ this._pageParam ] != this._urlParams[ this._pageParam ] ){
				this._updatePage( state.data[ this._pageParam ] );
			}
		},

		/**
		 * Fetches new results from the server, then calls this._updateTable to update the
		 * content and pagination. Simply redirects to URL on error.
		 *
		 * @returns {void}
		 */
		_getResults: function (forceURL) {
			var self = this;
			var urlBits = this._getURL();
			var url = '';

			try {
				if( this._ajax && _.isFunction( this._ajax.abort ) ){
					this._ajax.abort();
					this._ajax = null;
				}
			} catch(err) {}

			// Figure out which URL we should be using
			if( forceURL ){
				url = forceURL;
			} else {
				if(urlBits) {
					url = this._baseURL + this._getURL() + '&';
				} else {
					url = this._baseURL;
				}

				// If SEO pagination is enabled, we need to include the page in the URL and then
				// append other URL bits.
				if ( this._seoPagination ) {
					url = ips.utils.url.pageParamToPath( url, this._pageParam, this._urlParams[ this._pageParam ] );
				}
			}
			
			if( !_.isUndefined( this.scope.attr('data-resort') ) ){
				url += ( ( url.indexOf('?') == -1 ) ? '?' : '&' ) + this.scope.attr('data-resort') + '=1';
			}

			this._ajax = ips.getAjax()( url.replace( /\+/g, '%20' ), {
				dataType: 'json',
				showLoading: this._showLoading()
			})
				.done( _.bind( this._getResultsDone, this ) )
				.fail( _.bind( this._getResultsFail, this ) )
				.always( _.bind( this._getResultsAlways, this ) );
		},

		/**
		 * Should the default loading throbber be used?
		 *
		 * @returns {boolean}
		 */
		_showLoading: function () {
			return true;
		},

		/**
		 * Callback when the results ajax is successful
		 *
		 * @param	{object} 	response 		JSON object containing new HTML pieces
		 * @returns {void}
		 */
		_getResultsDone: function (response) {
			this._updateTable( response );
		},

		/**
		 * Callback when the results ajax fails
		 *
		 * @param 	{object} 	jqXHR			jQuery XHR object
		 * @param	{string} 	textStatus		Error message
		 * @param 	{string}	errorThrown
		 * @returns {void}
		 */
		_getResultsFail: function (jqXHR, textStatus, errorThrown) {
			if( Debug.isEnabled() || textStatus == 'abort' ){
				Debug.error( "Ajax request failed (" + textStatus + "): " + errorThrown );
				Debug.error( jqXHR.responseText );
			} else {
				// rut-roh, we'll just do a manual redirect
				window.location = this._baseURL + this._getURL();
			}
		},

		/**
		 * Update the content and pagination elements
		 *
		 * @param	{object} 	response 		JSON object containing new HTML pieces
		 * @returns {void}
		 */
		_updateTable: function (response) {

			var rows = this.scope.find('[data-role="tableRows"]');
			var pagination = this.scope.find('[data-role="tablePagination"]');
			var extra = this.scope.find('[data-role="extraHtml"]');
			var autoCheck = this.scope.find('[data-ipsAutoCheck]');

			// Check the required elements are in the page
			if( !rows.length ){
				window.location = this._baseURL + this._getURL();
				return;
			}

			// Table body
			rows.html( response.rows ).trigger('tableRowsUpdated');
			// Pagination
			// If there's pagination content to show, make sure pagination container is shown
			pagination
				.toggle( ( response.pagination && response.pagination.trim() !== "" ) || !_.isUndefined( pagination.attr('data-showEmpty') ) )
				.html( response.pagination || "" )
				.trigger('tablePaginationUpdated');

			// Eztra
			extra.html( response.extraHtml );
			// Autocheck
			autoCheck.trigger('refresh.autoCheck');

			// New content loaded, so trigger contentChange event
			$( document ).trigger( 'contentChange', [ this.scope ] );

			/* Data Layer page property */
			try {
				if ( IpsDataLayerConfig && !window.IpsDataLayerConfig ) {
					this.scope.find( '[data-role="tablePagination"] [data-page]' ).click( function (e) {
						let target = e.currentTarget;
						if ( target.parentNode.classList.contains('ipsPagination_active') ) {
							return;
						}
						let page = Number( e.currentTarget.dataset['page'] );

						if ( isNaN(page) ) return;

						$('body').trigger('ipsDataLayerProperties', { _properties: {page_number: page} });
					});
				}
			} catch (e) {}
		},

		/**
		 * Update classname on new active page. Pagination actually gets overwritten
		 * by the ajax response, but by updating the class here, it feels more immediate
		 * for the user.
		 *
		 * @param	{number} 	newPage 		New active page number
		 * @returns {void}
		 */
		_updatePage: function (newPage) {
			this.scope
				.find('[data-role="tablePagination"] [data-page]')
					.removeClass('ipsPagination_pageActive')
				.end()
				.find('[data-page="' + newPage + '"]')
					.addClass('ipsPagination_pageActive');
		},

		/**
		 * Updates the sorting order classnames
		 *
		 * @param 	{string} 	by 			Key name of sort by value
		 * @param 	{string} 	direction	asc or desc order value
		 * @returns {void}
		 */
		_updateSort: function ( data ) {
			var current = this._getSortValue();

			if( !data.by ){
				data.by = current.by;
			}

			if( !data.order ){
				data.order = current.order;
			}

			var obj = {
				sortby: data.by,
				sortdirection: data.order,
			};

			obj[ this._pageParam ] = 1;

			this.updateURL( obj );
		},

		/**
		 * Builds a param string from values in this._urlParams, excluding empty values
		 *
		 * @returns {string}	Param string
		 */
		_getURL: function () {
			var tmpUrlParams = {};

			for( var i in this._urlParams ){
				if( this._urlParams[ i ] != '' && i != 'controller' && i != 'bypassState' && ( i != 'page' || ( i == 'page' && this._urlParams[ i ] != 1 ) ) ){
					tmpUrlParams[ i ] = this._urlParams[ i ];
				}
			}

			return $.param( tmpUrlParams );
		},

		/**
		 * Returns current parameters to be used in URLs
		 *
		 * @returns {object}
		 */
		_getUrlParams: function () {
			var sort = this._getSortValue();
			var obj = {	
				filter: this._getFilterValue() || '',
				sortby: sort.by || '',
				sortdirection: sort.order || '',
			};

			obj[ this._pageParam ] = ips.utils.url.getParam( this._pageParam ) || 1

			return obj;
		},
		
		/**
		 * Event handler for clicking a clickable row
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		rowClick: function (e) {
			var target = $( e.target );

			// Ignore if we clicked something clickable (besides the row)
			if ( target.is('a') || target.is('i') || target.is('input') || target.is('textarea') || target.is('code') || target.closest('a').length || target.closest('.ipsMenu').length ) {
				return;
			}
			
			// Ignore if we didn't use the left mouse button. 1 is left mouse button, 2 is middle
			// We allow 2 through here because we'll treat it differently shortly
			if( e.which !== 1 && e.which !== 2 ){
				return;
			}

			// Ignore if special keys are pressed
			if( e.altKey || e.shiftKey ){
				return;
			}
			
			// If we clicked into a cell with a checkbox, check that checkbox rather than redirect
			if ( target.is('td') ) {
				var checkbox = target.find('input[type="checkbox"]');
				if ( checkbox.length ) {
					checkbox.prop( 'checked', !checkbox.prop( 'checked' ) );
					return;
				}
			}
						
			var link = $( e.currentTarget )
 				.find('[data-ipscontrolstrip]').parent()
 					.find( '[data-controlStrip-action="' + $( e.currentTarget ).attr('data-tableClickTarget') + '"]' );

			// If we are using the meta key or middle mouse button, we're going to adjust the link
			// to include _blank, so that it opens in a new tab
			if( e.metaKey || e.ctrlKey || e.which == 2 ){
				link.attr('target', '_blank');
 				link.get(0).click();
 				link.attr('target', '');
			} else {
				// Okay, we can go...
 				link.get(0).click();
			}			
		},


		/**
		 * Abstract
		 */
		_getSortValue: $.noop,
		_getFilterValue: $.noop,
		_getResultsAlways: $.noop
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.updateBanner.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.license.js - License message
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.global.core.updateBanner', {
		initialize: function () {
			this.on( 'click', '[data-role=&quot;closeMessage&quot;]', this.hideMessage );
		},

		hideMessage: function () {
			var date = new Date();
			date.setTime( date.getTime() + ( 7 * 86400000 ) );
			ips.utils.cookie.set( 'updateBannerDismiss', true, date.toUTCString() );
			this.scope.slideUp();
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.forms.ftp.js" javascript_type="controller" javascript_version="107643" javascript_position="1000050"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.forms.ftp.js
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.core.ftp', {
		
		/**
		 * Init
		 */
		initialize: function () {
			var scope = $(this.scope);
			scope.find('[data-role="portToggle"]').change(function(){
				scope.find('[data-role="portInput"]').val( $(this).attr('data-port') );
			});
			
			scope.find('[data-role="serverInput"]').keyup(function(){				
				var matches = $(this).val().match( /^((.+?):\/\/)?((.+?)(:(.+?)?)@)?(.+?\..+?)(:(\d+)?)?(\/.*)?$/ );
				if ( matches && ( matches[1] || matches[3] || matches[8] || matches[10] ) ) {
					if ( matches[2] ) {
						console.log(scope.find('[data-role="portToggle"][value="' + matches[2] + '"]'));
						scope.find('[data-role="portToggle"][value="' + matches[2] + '"]').prop( 'checked', true );
					}
					if ( matches[3] ) {
						if ( matches[4] ) {
							scope.find('[data-role="usernameInput"]').val( matches[4] );
							scope.find('[data-role="usernameInput"]').focus();
						}
						if ( matches[6] ) {
							scope.find('[data-role="passwordInput"]').val( matches[6] );
							scope.find('[data-role="passwordInput"]').focus();
						}
					}
					if ( matches[8] ) {
						scope.find('[data-role="portInput"]').val( matches[9] );
						scope.find('[data-role="portInput"]').focus();
					}
					if ( matches[10] ) {
						scope.find('[data-role="pathInput"]').val( matches[10] );
						scope.find('[data-role="pathInput"]').focus();
					}
					$(this).val( matches[7] );
				}
				
			});
		},
				
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/customization" javascript_name="ips.customization.editorToolbars.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/* global ips, _, Debug, CKEDITOR */
/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.customization.editorToolbars.js - Controller for editor toolbar configuration screen
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.customization.editorToolbars', {

		initialize: function () {
			this.on( 'click', '[data-buttonKey]', this.openPreferences );
			this.on( 'click', '[data-action="addToolbar"]', this.addToolbar );
			this.on( 'click', '[data-action="addSep"]', this.addSep );

			this.on( document, 'editorWidgetInitialized', this.setUpEditor );
			this.setup();
		},

		/**
		 * Setup method
		 * When the document is ready, CKEditor is set up, and the dummy toolbars are made sortable
		 *
		 * @returns {void}
		 */
		setup: function () {
			// Set up ckeditor
			var self = this;

			//$( document ).ready( function () {

				/*var init = function () {
					self._setUpCKEditor();
				};

				if( ips.getSetting('useCompiledFiles') !== true ){
					ips.loader.get( ['core/dev/ckeditor/ckeditor.js'] ).then( init );	
				} else {
					ips.loader.get( ['core/interface/ckeditor/ckeditor.js'] ).then( init );
				}*/

				// Make the toolbars sortable
				self.scope.find('[data-role="dummyToolbar"]').sortable({
					connectWith: '[data-role="dummyToolbar"]',
					update: _.bind( self._saveToolbars, self ),
					appendTo: document.body
				});
			//});			
		},

		setUpEditor: function () {
			this._setUpCKEditor();
		},

		/**
		 * Redirects to page that allows button permissions to be edited
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		openPreferences: function (e) {
			var elem = $( e.currentTarget );
			var title = !_.isUndefined( elem.attr('title') ) ? elem.attr('title') : elem.find('a').attr('title');
			
			window.location = this.scope.attr('data-url') + '&do=permissions&button=' + elem.attr('data-buttonKey') + '&title=' + title;
		},

		/**
		 * Event handler for clicking the Add Toolbar button
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		addToolbar: function (e) {
			e.preventDefault();
			var elem = $( e.currentTarget );

			var list = $('<ul class="dummy_toolbar clearfix" data-role="dummyToolbar" style="min-height:40px" />');
			list.sortable({
				connectWith: '[data-role="dummyToolbar"]',
				update: _.bind( this._saveToolbars, this ),
				appendTo: document.body
			});

			this.scope.find('[data-role="dummyToolbar"]').sortable( 'option', 'connectWith', list );
			this.scope.find('#' + elem.attr('data-deviceKey') + '_editor_toolbars').append( list );
		},

		/**
		 * Event handler for clicking the Add Separator button
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		addSep: function (e) {
			e.preventDefault();
			var elem = $( e.currentTarget );
			
			var list = $('#' + elem.attr('data-deviceKey') + '_editor_toolbars')
				.children()
				.last();
			list.append( $('<li><span class="cke_toolbar_separator"></span></li>') );
			list.sortable({
				connectWith: '[data-role="dummyToolbar"]',
				update: _.bind( this._saveToolbars, this ),
				appendTo: document.body
			});
		},

		/**
		 * Sets up CKEditor by cloning each button and inserting it into the dummy toolbars
		 *
		 * @returns {void}
		 */
		_setUpCKEditor: function () {
			var self = this;
			var instance = null;

			for( var i in CKEDITOR.instances ){
				instance = CKEDITOR.instances[ i ];
			}

			//instance.on( 'instanceReady', function(){
				var items = CKEDITOR.ui( instance ).items;

				// Loop through all toolbar items in ckeditor
				// If it's a button or combo, we then clone the button onto dummy toolbars
				for( var i in items ){
					var elem = null;

					switch ( items[i].type ) {
						case 'button':
						case 'panelbutton':
							if( !$( '.' + instance.id ).find('.cke_button__' + items[i].name).length ){
								var button = new CKEDITOR.ui.button( items[i] );
								var output = [];
								button.render( instance, output );
								elem = $( output.join('') );
							} else {
								elem = $( '.' + instance.id ).find('.cke_button__' + items[i].name);
							}
							break;
						case 'richcombo':
							elem = $( '.' + instance.id ).find('.cke_combo__' + items[i].name );
							break;
						case 'separator':
							break;
					}

					if ( elem !== null ) {
						self.scope.find('[data-role="dummyEditor"]').each( function () {
							var deviceKey = $( this ).attr('data-deviceKey');
							// Clone the element
							var elemClone = elem.clone().attr( 'data-buttonKey', i );
							// Remove onclick from clone and children to prevent ckeditor from taking over
							elemClone.removeAttr('onclick').children().removeAttr('onclick');

							// If the button wrap already exists, clone the button into it, 
							// otherwise, create a new wrapper
							// and append it to the 'unused' toolbar
							if( self.scope.find('#' + deviceKey + '_editorButton_' + i ).length ){
								self.scope.find('#' + deviceKey + '_editorButton_' + i ).append( elemClone );
							} else {
								self.scope.find('#' + deviceKey + '_editor_unusedButtons').append(
									$('<li/>').attr('id', deviceKey + '_editorButton_' + i ).append( elemClone )
								);
							}
						});
					}
				}
			//});
		},

		/**
		 * Save the toolbar arrangement
		 *
		 * @returns {void}
		 */
		_saveToolbars: function () {
			var _save = {
				desktop: [],
				tablet: [],
				phone: []
			};

			this.scope.find('[data-role="devicePanel"]').each( function () {
				var deviceKey = $( this ).attr('data-deviceKey');
				var save = [];
				var i = 1;
				
				$( this ).find('[data-role="dummyToolbar"]').each( function () {
					var _id = 'row_' + i;
					i++;

					if( !$( this ).hasClass( 'editor_unusedButtons' ) ) {
						var toolbar = [];
						$( this ).children().each( function () {
							var buttonKey = null;

							if( $( this ).attr('id') ) {
								buttonKey = $( this ).attr('id').substr( 14 + deviceKey.length );
							} else {
								buttonKey = '-';
							}
							toolbar.push( buttonKey );
						});

						save.push( toolbar );
					}
				});

				_save[ deviceKey ] = save;
			});
			
			ips.getAjax()( this.scope.attr('data-url') + '&do=save', {
				type: 'post',
				data: {
					toolbars: JSON.stringify( _save ),
				}
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/customization" javascript_name="ips.customization.emoticons.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.customization.emoticons.js - Emoticons controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.customization.emoticons', {

		initialize: function () {
			this.on( 'blur', '[data-role="emoticonTyped"]', this.checkTypedValue );
			this.on( 'submit', this.submit );
			this.setup();
		},

		/**
		 * Setup method
		 * Makes the emoticon list sortable and sets an event handler for saving the order
		 *
		 * @returns {void}
		 */
		setup: function () {
			this.scope.find('[data-role="setList"]').sortable({
				update: _.bind( this._saveSetOrder, this )
			});
			this.scope.find('[data-role="emoticonsList"]').sortable({
				connectWith: this.scope.find('[data-role="emoticonsList"]'),
				handle: '[data-role="dragHandle"]',
				update: _.bind( this._saveOrder, this )
			});
		},

		/**
		 * Submit form handler; squash values into a single param
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		submit: function (e) {
			if( !window.JSON ){
				return;
			}

			var formElements = this.scope.find(':input:enabled:not([name="csrfKey"])');
			var output = ips.utils.form.serializeAsObject( formElements );
			var newInput = $('<input />').attr('type', 'hidden').attr('name', 'emoticons_squashed');

			// JSON encode the data
			Debug.log("Before encoding, emoticon data is:");
			Debug.log( output );
			output = JSON.stringify( output );

			this.scope.prepend( newInput.val( output ) );

			// Disable all of the elements we squashed so that they don't get sent
			formElements.prop('disabled', true);
		},
		
		/**
		 * Checks typed entry to ensure it is valid (no spaces)
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		checkTypedValue: function (e) {
			var elem = $( e.currentTarget );
			var val = elem.val();
			
			elem.val( val.replace( /\s/g, '' ) );
			
			if ( val.match( /\s/ ) ) {
				ips.ui.alert.show({
					type: 'alert',
					message: ips.getString('emoticon_no_spaces'),
					icon: 'warn'
				});
			}
		},
		
		/**
		 * Saves the new emoticon order
		 *
		 * @returns {void}
		 */
		_saveSetOrder: function () {
			var setOrder = [];

			this.scope.find('[data-emoticonSet]').each( function () {
				setOrder.push( $( this ).attr('data-emoticonSet') );
			});

			ips.getAjax()( this.scope.attr('action'), {
				type: 'post',
				data: { setOrder: setOrder }
			});
		},
		
		/**
		 * Saves the new emoticon order
		 *
		 * @returns {void}
		 */
		_saveOrder: function (e, ui) {
			var output = {};
			var item = ui.item;

			// Update the group key for this item after it has been moved
			var group = ui.item.closest('[data-emoticonSet]').attr('data-emoticonSet');
			ui.item.find('.cEmoticons_input > input[type="hidden"]').val( group );

			// Build the array of ordering
			this.scope.find('[data-emoticonGroup]').each( function () {
				var itemOrder = [];

				$( this ).find('[data-emoticonID]').each( function () {
					itemOrder.push( parseInt( $( this ).attr('data-emoticonID') ) );
				});

				output[ $( this ).attr('data-emoticonGroup') ] = itemOrder;
			});

			ips.getAjax()( this.scope.attr('action'), {
				type: 'post',
				data: output
			});
		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/customization" javascript_name="ips.customization.themes.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.mobileNav.js - ACP mobile navigation
 *
 * Author: Rikki Tissier
 */

;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.customization.themes', {

		initialize: function () {
			this.on( 'click', this.revertSetting );
		},

		
		/**
		 * Reverts a setting
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		revertSetting: function (e) {
			var self = this;

			e.preventDefault();

			ips.ui.alert.show({
				type: 'confirm',
				message: ips.getString('theme_revert_setting'),
				icon: 'fa fa-question',
				buttons: {
					ok: ips.getString('ok'),
					cancel: ips.getString('cancel')
				},
				callbacks: {
					ok: function () {
						ips.getAjax()( self.scope.attr('href') + '&wasConfirmed=1' )
							.done( function (response) {
								var obj = $('#theme_setting_' + self.scope.attr('data-ipsThemeSetting') + ' input[name^=core_theme_setting_title_]');
								obj.val( response.value );
								obj.focus().blur();
								self.scope.hide();
							});	
					},
				}
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="global" javascript_path="controllers/customization" javascript_name="ips.customization.visualLang.js" javascript_type="controller" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.customization.visualLang.js - Visual language editor controller
 *
 * Author: Mark Wade & Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.customization.visualLang', {

		timeout: null,

		initialize: function () {
			this.on( document, 'mousedown', 'span[data-vle]', this.mouseDownLang );
			this.on( document, 'mouseup mouseleave', 'span[data-vle]', this.mouseUpLang );
			this.on( document, 'keypress', 'input[type="text"][data-role="vle"]', this.keyPressEditBox );
			this.on( document, 'blur', 'input[type="text"][data-role="vle"]', this.blurEditBox );
			this.on( document, 'contentChange', this.contentChange );
			this.setup();
		},

		/**
		 * Set up visual editor
		 * Prepares text nodes for editing, and removes any stragglers
		 *
		 * @returns {void}
		 */
		setup: function () {
			var self = this;

			this._boundHandler = _.bind( this._preventDefaultHandler, this );

			// Remove the VLE tag from the title
			this._removeLangTag('title');

			$( document ).ready( function () {
				self._setUpTextNodes('body');
				self._removeLangTag('body');
				self.scope.trigger('vleDone');
			});
		},

		/**
		 * Inits VLE on a changed dom element
		 *
		 * @returns {void}
		 */
		contentChange: function (e, data) {
			this._setUpTextNodes( data );
			this._removeLangTag( data );
		},

		/**
		 * Event handler for mousedown on document
		 * Sets a timeout so that editing only happens after 1 second
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		mouseDownLang: function (e) {
			this.timeout = setTimeout( _.partial( this._enableLangEditing, e), 1000 );
		},

		/**
		 * Event handler for mouseup on document
		 * Clears timeout
		 *
		 * @returns {void}
		 */
		mouseUpLang: function () {
			clearTimeout( this.timeout );
		},

		/**
		 * Event handler for keypress in an editing input box
		 * Blurs input if enter is pressed
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		keyPressEditBox: function (e) {
			if( e.keyCode == ips.ui.key.ENTER ){
				e.stopPropagation();
				$( e.currentTarget ).blur();
				return false;
			}
		},

		/**
		 * Event handler for blur on editing input box
		 * Sends the new value via ajax, and removes the editing box
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		blurEditBox: function (e) {
			var inputNode = $( e.currentTarget );
			var value = inputNode.val();
			var safeValue = encodeURIComponent( value );
			var elem = inputNode.closest('[data-vle]');
			var url = '?app=core&module=system&controller=vle&do=set';

			if( value == elem.attr('data-original') || value == '' ){
				elem.html( elem.attr('data-original') );				
			} else {
				inputNode
					.val('')
					.addClass('ipsField_loading');
								
				ips.getAjax()( url + '&key=' + elem.attr('data-vle') + '&value=' + safeValue )
					.done( function (response) {
						$(document).find('[data-vle="' + elem.attr('data-vle') + '"]').html( response );
					})
					.fail( function () {
						Debug.log( url + '&key=' + elem.attr('data-vle') + '&value=' + safeValue );
					 	
					 	elem.html( inputNode.attr('data-original') );

						ips.ui.alert.show( {
							type: 'alert',
							icon: 'warn',
							message: ips.getString('js_login_both'),
						});
					});
			}

			var parentLink = elem.closest('a');

			if( parentLink.length ){
				parentLink.off( 'click', this._boundHandler );

				if( parentLink.attr('data-vleHref') ){
					parentLink.attr('href', parentLink.attr('data-vleHref') ).removeAttr('data-vleHref');
				}
			}
		},

		/**
		 * Event handler we can assign to prevent links from navigating
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_preventDefaultHandler: function (e) {
			e.preventDefault();
		},

		/**
		 * Called when mouse has clicked on a string for 1 second
		 * Replaces the elem with a textbox containing the value to allow editing
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_enableLangEditing: function (e) {
			var elem = $( e.currentTarget );
			var parentLink = elem.closest('a');

			if( parentLink.length ){
				parentLink
					.on( 'click', this._boundHandler )
					.attr( 'data-vleHref', parentLink.attr('href') )
					.attr( 'href', '#' );
			}

			var inputNode = $('<input/>')
								.attr( { type: 'text' } )
								.addClass( 'ipsField_loading ipsField_vle' )
								.attr( 'data-role', 'vle' );

			elem.html('').append( inputNode );

			// Fire an ajax request to get the raw language string, then update the text box with the returned value
			ips.getAjax()( '?app=core&module=system&controller=vle&do=get&key=' + elem.attr('data-vle') )
				.done( function (response) {
					console.log( elem.attr('data-vle') );
					inputNode
						.val( response )
						.attr( { 'data-original': response } )
			 			.removeClass('ipsField_loading')
			 			.focus()
			 			.select()
				})
				.fail( function () {
					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.getString('js_login_both'),
					});
				});

		},

		/**
		 * Removes stray language tags from the provided element
		 *
		 * @param 	{element} 	element 	Element from which to remove tags
		 * @returns {void}
		 */
		_removeLangTag: function (element) {
			// element may be undefined
			if( _.isUndefined( element ) )
			{
				return;
			}

			var elem = $( element );
			elem.contents().filter(function() { return this.nodeType === 3 || this.tagName === "LABEL" || this.tagName === "SPAN"; }).each(function(){
				$(this).replaceWith( $(this).text().replace( /\#VLE\#.+?#!#\[(.+?)\]#!##/gm, '$1' ) );
			});
			elem.find('i[class]').each(function(){
				$(this).attr( 'class', $(this).attr('class').replace( /\#VLE\#.+?#!#\[(.+?)\]#!##/gm, '$1' ) );
			});
			elem.find('[placeholder]').each(function(){
				$(this).attr( 'placeholder', $(this).attr('placeholder').replace( /\#VLE\#.+?#!#\[(.+?)\]#!##/gm, '$1' ) );
			});
			elem.find('[title]').each(function(){
				$(this).attr( 'title', $(this).attr('title').replace( /\#VLE\#.+?#!#\[(.+?)\]#!##/gm, '$1' ) );
			});
			elem.find('[aria-label]').each(function(){
				$(this).attr( 'aria-label', $(this).attr('aria-label').replace( /\#VLE\#.+?#!#\[(.+?)\]#!##/gm, '$1' ) );
			});
		},

		/**
		 * Turns strings into editable spans in the provided element
		 *
		 * @param 	{element} 	element 	Element in which to replace language strings
		 * @returns {void}
		 */
		_setUpTextNodes: function ( element ) {
			// element may be undefined
			if( _.isUndefined( element ) )
			{
				return;
			}

			var regex = /\#VLE\#(.+?)#!#\[(.+?)\]#!##/gm;		

			$( element )
				.find('*')
				.contents()
					.filter( function () {
						var elem = $( this );
						return !elem.is('iframe') && !elem.closest('[data-ipsEditor]').length && !elem.is('textarea') && ( elem.is('[value]') || this.nodeType == 3 );
					})
				    	.each( function (idx, elem) {
				    		var elem = $( elem );
				    		if( elem.get(0).nodeType == 3 ){
				    			// Text inputs
								elem.replaceWith( elem.text().replace( regex, '<span data-vle="$1" data-original="$2">$2</span>' ) );
							} else if( elem.is('[value]') ){
								// Inputs
								if( elem.val() != '' ){
									elem.attr( 'data-vle', elem.val().replace( regex, '$1' ) ).val( elem.val().replace( regex, '$2' ) );	
								}							
							}
						});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/dashboard" javascript_name="ips.dashboard.adminNotes.js" javascript_type="controller" javascript_version="107643" javascript_position="1000150">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.dashboard.adminNotes.js - Admin notes controller for the admin notes widget
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.dashboard.adminNotes', {

		initialize: function () {
			this.on( 'submit', 'form', this.saveNotes );
		},

		saveNotes: function (e) {
			e.preventDefault();

			var url = $( e.currentTarget ).attr('action');
			var self = this;

			// Show loading
			this.scope.find('[data-role=&quot;notesInfo&quot;]').hide();
			this.scope.find('[data-role=&quot;notesLoading&quot;]').removeClass('ipsHide');

			ips.getAjax()( url, { type: 'post', data: $('#admin_notes').serialize() } )
				.done( function (response) {
					self.scope.find('[data-role=&quot;notesInfo&quot;]').html( response );
				})
				.fail( function () {

				})
				.always( function () {
					self.scope.find('[data-role=&quot;notesInfo&quot;]').show();
					self.scope.find('[data-role=&quot;notesLoading&quot;]').addClass('ipsHide');
				});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/dashboard" javascript_name="ips.dashboard.main.js" javascript_type="controller" javascript_version="107643" javascript_position="1000150"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.dashboard.main.js - Admin dashboard controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.dashboard.main', {

		_managing: false,

		initialize: function () {
			this.on( 'click', '.acpWidget_close', this.closeWidget );
			this.on( 'click', '[data-widgetCollapse]', this.collapseWidget );
			this.on( document, 'menuItemSelected', '#elAddWidgets:not( .ipsButton_disabled )', this.addWidget );
			this.on( 'refreshWidget', '[data-widgetKey]', this.refreshWidget );
			this.setup();
		},

		/**
		 * Setup method
		 * Adds sortable functionality to our two columns
		 *
		 * @returns {void}
		 */
		setup: function () {
			this.mainColumn = this.scope.find('[data-role="mainColumn"]');
			this.sideColumn = this.scope.find('[data-role="sideColumn"]');

			// Set up our sortables
			this.scope.find('[data-role="sideColumn"]').sortable({
				handle: '.acpWidget_reorder',
				forcePlaceholderSize: true,
				placeholder: 'acpWidget_emptyHover',
				connectWith: '[data-role="mainColumn"]',
				tolerance: 'pointer',
				start: this.startDrag,
				stop: _.bind( this.stopDrag, this ),
				update: _.bind( this.update, this )
			});

			this.scope.find('[data-role="mainColumn"]').sortable({
				handle: '.acpWidget_reorder',
				forcePlaceholderSize: true,
				placeholder: 'acpWidget_emptyHover',
				connectWith: '[data-role="sideColumn"]',
				tolerance: 'pointer',
				start: this.startDrag,
				stop: _.bind( this.stopDrag, this ),
				update: _.bind( this.update, this )
			});

			// Go through collapsed/not collapsed
			this.scope.find('[data-widgetCollapsed="true"][data-widgetCollapse-content]').hide();
		},

		/**
		 * start method for jquery UI
		 * Stops the tooltip from showing while we drag
		 *
		 * @returns {void}
		 */
		startDrag: function (e, ui) {
			$('body')
				.attr('data-dragging', true)
				.css({
					overflow: 'scroll'
				});

			ui.item.css({
				zIndex: ips.ui.zIndex()
			});
		},

		/**
		 * stop method for jquery UI
		 * Lets the tooltip show agian
		 *
		 * @param 	{event} 	e 		Event object
		 * @param	{object} 	ui 		jQuery UI data object
		 * @returns {void}
		 */
		stopDrag: function (e, ui) {
			
			$('body')
				.removeAttr('data-dragging')
				.css({
					overflow: 'auto'
				});

			// Let the widget know it has been sorted
			$( ui.item ).trigger( 'sorted.dashboard', {
				ui: ui
			});
						
			this._loadWidget( $( ui.item ).attr('data-widgetkey') );

			$('#ipsTooltip').hide();
		},

		/**
		 * update method for jquery UI
		 *
		 * @returns {void}
		 */
		update: function () {
			this._savePositions();
		},

		/**
		 * Handler for the close widget button
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		closeWidget: function (e) {
			e.preventDefault();

			var self = this;
			var widget = $( e.currentTarget ).closest('[data-widgetKey]');

			// Get widget info
			var key = widget.attr('data-widgetKey');
			var name = widget.attr('data-widgetName');

			widget.animationComplete( function () {
				widget.remove();
				self.mainColumn.sortable('refresh');
				self.sideColumn.sortable('refresh');
				self._savePositions();
			});

			widget.animate({ height: "0" });
			ips.utils.anim.go( 'zoomOut fast', widget );

			$('#elAddWidgets_menu').find('[data-ipsMenuValue="' + key + '"]').removeClass('ipsHide');

			this.scope
				.find('#elAddWidgets_button')
					.removeClass('ipsButton_disabled')
					.removeAttr('data-disabled');
		},

		/**
		 * Handler for the collapse widget button
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		collapseWidget: function (e) {
			e.preventDefault();
			
			// If we clicked on one of the anchors, just return
			if( $( e.target ).is('i') )
			{
				return;
			}

			var self = this;
			var widget = $( e.currentTarget ).closest('[data-widgetKey]');

			// Figure out if we are hidden or showing to start with
			var collapsed = widget.find('[data-role="widgetContent"]').attr('data-widgetCollapsed');

			// Toggle the data flag
			widget.find('[data-role="widgetContent"]').attr( 'data-widgetCollapsed', ( collapsed == 'true' ) ? 'false' : 'true' );

			// Toggle the divs
			widget.find('[data-widgetCollapse-content]').slideToggle();

			// Toggle the icon
			widget.find('.acpWidget_collapse i').removeClass('fa-caret-right').removeClass('fa-caret-down').addClass( ( collapsed == 'true' ) ? 'fa-caret-down' : 'fa-caret-right' );

			// Save position/collapse data
			this._savePositions();
		},

		/**
		 * Event handler for clicking an item in the 'add widget' menu
		 * Finds an available gap for the new widget, then inserts it into the page
		 *
		 * @param	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object from the menu widget
		 * @returns {void}
		 */
		addWidget: function (e, data) {
			data.originalEvent.preventDefault();

			// Find menu item
			var item = data.menuElem.find('[data-ipsMenuValue="' + data.selectedItemID + '"]');
			var key = item.attr('data-ipsMenuValue');
			var name = item.attr('data-widgetName');

			// Build widget template
			var newWidget = ips.templates.render('dashboard.widget', {
				key: key,
				name: name
			});

			// Insert it into the main column
			this.mainColumn.prepend( newWidget );
			var newWidgetElem = this.mainColumn.find( '#elWidget_' + key );
			ips.utils.anim.go( 'fadeIn', newWidgetElem );

			// Load it
			this._loadWidget( key );

			// Save it
			this._savePositions();

			// Hide it in the main menu
			setTimeout( function () {
				item.addClass('ipsHide');	
			}, 500);
			

			if( !data.menuElem.find('[data-ipsMenuValue]:not( .ipsHide ):not( [data-ipsMenuValue="' + data.selectedItemID + '"] )').length ){
				this.scope
					.find('#elAddWidgets_button')
						.addClass('ipsButton_disabled')
						.attr( 'data-disabled', true );
			}
		},

		/**
		 * Fetches the contents of a widget from the backend
		 *
		 * @param 	{string} 	key 		Key of widget to load
		 * @returns {void}
		 */
		_loadWidget: function (key) {			
			var widget = this.scope.find( '[data-widgetKey="' + key + '"]' );

			if( !widget.length ){
				return;
			}

			widget.find('[data-role="widgetContent"]')
				.css({
					height: widget.find('[data-role="widgetContent"]').outerHeight() + 'px',
				})
				.html('')
				.addClass('ipsLoading');

			// Start the request
			ips.getAjax()( '?app=core&module=overview&controller=dashboard&do=getBlock', {
				data: {
					appKey: key.substr( 0, key.indexOf( '_' ) ),
					blockKey: key
				}
			})
				.done( function (response) {
					widget.find('[data-role="widgetContent"]')
						.css({
							height: 'auto'
						})
						.html( response )
						.removeClass('ipsLoading');

					// Inform the document
					$( document ).trigger( 'contentChange', [ widget ] );
				});
		},

		/**
		 * Refreshes the contents of a widget
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		refreshWidget: function (e) {
			var key = $( e.currentTarget ).attr('data-widgetKey');
			this._loadWidget( key );
		},

		/**
		 * Saves the widget positions back to the backend
		 *
		 * @returns {void}
		 */
		_savePositions: function () {
			// Get the serialized positions for both columns
			var main = this.mainColumn.sortable( 'toArray', { attribute: 'data-widgetKey' } );
			var side = this.sideColumn.sortable( 'toArray', { attribute: 'data-widgetKey' } );

			// Get list of collapsed blocks
			var collapsed = _.map( this.scope.find('[data-widgetCollapsed="true"]'), function( elem ){
				return $(elem).closest('[data-widgetKey]').attr('data-widgetKey');
			});

			/*Debug.log("Current widget list:");
			Debug.log( main );
			Debug.log( side );*/

			ips.getAjax()( '?app=core&module=overview&controller=dashboard&do=update', {
				data: {
					blocks: { 'main': main, 'side': side, 'collapsed': collapsed }
				}
			})
				.done( function () {
					// No need to do anything
				})
				.fail( function () {
					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.getString('dashboard_cant_save'),
						callbacks: {}
					});
				});
		}

	});
}(jQuery, _));


]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/dashboard" javascript_name="ips.dashboard.onboard.js" javascript_type="controller" javascript_version="107643" javascript_position="1000150">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.dashboard.onboard.js - Onboarding setup controller
 *
 * Author: Brandon Farber
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.dashboard.onboard', {

		initialize: function () {
			this.on( 'click', '[data-role=&quot;sectionToggle&quot;]', this.toggleSection );
			this.on( 'click', '[data-action=&quot;nextStep&quot;]', this.nextStep );
			this.on( 'click', '[data-action=&quot;skipStep&quot;]', this.skipStep );

			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
		
		},

		/**
		 * Toggle a section open and closed
		 *
		 * @param	{event}		e	Event
		 * @returns	{void}
		 */
		toggleSection: function( e ) {
			e.preventDefault();

			this.scope.find('[data-role=&quot;sectionWrap&quot;]').addClass('cOnboard__section--closed');
			$( e.currentTarget ).closest('.cOnboard__section').toggleClass('cOnboard__section--closed');
			$(document).trigger( 'contentChange', [ $( e.currentTarget ).closest('.cOnboard__section') ] );
		},

		nextStep: function (e) {
			e.preventDefault();

			var wrap = $( e.currentTarget ).closest('.cOnboard__section');
			var nextWrap = wrap.next('.cOnboard__section');

			if( nextWrap.length ){
				$('html, body').animate({ scrollTop: String(wrap.position().top - 70) }, function () {
					setTimeout( function () {
						wrap.addClass('cOnboard__section--closed cOnboard__section--done');
						nextWrap.removeClass('cOnboard__section--closed');
						$(document).trigger( 'contentChange', [ nextWrap ] );
					}, 200);
				});
			} else {
				wrap.addClass('cOnboard__section--closed cOnboard__section--done');
			}
		}

	});
}(jQuery, _));


</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/dashboard" javascript_name="ips.dashboard.validation.js" javascript_type="controller" javascript_version="107643" javascript_position="1000150">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.dashboard.validation.js - AdminCP users awaiting validation widget
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.dashboard.validation', {

		initialize: function () {
			this.on( 'click', '[data-action=&quot;approve&quot;], [data-action=&quot;ban&quot;]', this.validateUser );
		},

		/**
		 * Event handler for the approve/ban buttons
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		validateUser: function (e) {
			e.preventDefault();
			var self = this;
			var button = $( e.currentTarget );
			var url = button.attr('href');
			var type = button.attr('data-action');
			var row = button.closest('[data-role=&quot;validatingRow&quot;]');
			var name = row.find('[data-role=&quot;userName&quot;]').text();
			var toggles = button.closest('[data-role=&quot;validateToggles&quot;]');
						
			ips.ui.alert.show({
				type: 'confirm',
				callbacks: {
					'ok': function() {
						toggles.find('a').addClass('ipsButton_disabled');
						
						ips.getAjax()( url )
							.done( function ( response ) {
								
								// Show flash msg
								ips.ui.flashMsg.show( ips.getString( type == 'approve' ? 'userApproved' : 'userBanned', {
									name: name
								}));
								
								// Update HTML
								if ( response ) {
									var newElement = $(response);
									$(self.scope).replaceWith( newElement );
									$( document ).trigger( 'contentChange', [ newElement ] );
								} else {
									ips.utils.anim.go( 'fadeOut', $(self.scope).closest('.cNotification') );
									$('body').trigger('updateNotificationCount');
								}
							});
					}
				}
			});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.censorBlock.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.censorBlock.js - Controller for the censor block feature
 *
 * Author: Matt Mecham
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.censorBlock', {

		initialize: function () {
			/* Set up case-insensitive matching */
			jQuery.expr[':'].icontains = function(a, i, m) {
				return jQuery(a).text().toUpperCase()
					.indexOf(m[3].toUpperCase()) >= 0;
			};			

			this._words = $.parseJSON( this.scope.attr('data-censorBlockWords') );
			this._editorId = $( this.scope ).data('editorid');
			
			this.on( document, 'editorWidgetInitialized', this.setup );
		},
		_editorId: null,
		_editor: null,
		
		/**
		 * Perform some set up after the editor has initialised
		 *
		 */
		setup: function (e) {
			this._editor = CKEDITOR.instances[ this._editorId ];
			this._form = $( this._editor.container.$ ).closest('form');
			
			/* Add submit listener */
			this._form.on( 'submit', this.checkCensorBlock.bind(this) );
		},
		
		/**
		 * On form submit, check the contents for any words we want to block
		 *
		 */
		checkCensorBlock: function (e) {
			this._editor.updateElement();
			var value = this._editor.getData();
			var found = [];

			this.scope.find('[data-role="editorCensorBlockMessageInternal"]').html( value );
			var display = this.scope.find('[data-role="editorCensorBlockMessageInternal"]');

			if ( this._words.length ) {
				/* Knock out quote/code blocks so we effectively ignore them then force into text to remove everything else */
				$( display ).html( XRegExp.replace( $( display ).html(), new RegExp( "<(pre|blockquote).+?</\\1>", "sig" ), '' ) ).html( $(display).text() );
				
				found = 0;
				var index = 0;
				var exactWords = [];
				var looseWords = [];
				var reggie = null;
				for( var i in this._words ) {
					var word = this._words[i]['word'];
					var type = this._words[i]['type'];
					if( $( value ).is(':icontains("' + word + '")' ) ){
						if ( type == 'exact' ) {
							exactWords.push( word );
						}
						else {
							looseWords.push( word );
						}
					}
				}

				if ( looseWords.length && exactWords.length ) {
					reggie = new RegExp( "((?:\\b|\\s|^)([^\\b]*" + looseWords.join('|') + "[^\\b]*)(?:\\b|\\s|$)|(?:\\b|\\s|^)(" + exactWords.join('|') + ")(?:\\b|\\s|$))", "ig" );
				} else if ( looseWords.length ) {
					reggie = new RegExp( "(?:\\b|\\s|^)([^\\b]*" + looseWords.join('|') + "[^\\b]*)(?:\\b|\\s|$)", "ig" );
				} else if ( exactWords.length ) {
					reggie = new RegExp( "(?:\\b|\\s|^)(" + exactWords.join('|') + ")(?:\\b|\\s|$)", "ig" );
				}
				Debug.log(reggie);
				if ( $(value).text().match( reggie ) ) {
					$(display).each( function() {
						$( this ).contents().filter(
							function() { return this.nodeType === 3 }
						).each( function(){
							$(this).replaceWith( _.escape( XRegExp.replace( $( this ).text(), reggie, '<mark class="ipsMatchWarning ipsType_bold ipsType_large ipsMatch' + ( index ) + '">' + "$1" + '</mark>' ) ).replace( new RegExp("&lt;mark class=&quot;ipsMatchWarning ipsType_bold ipsType_large ipsMatch" + ( index ) + "&quot;&gt;", 'ig'), "<mark class='ipsMatchWarning ipsType_bold ipsType_large ipsMatch" + ( index ) + "'>" ).replace( new RegExp("&lt;/mark&gt;", 'ig'), "</mark>" ).trim() );
						} );
						found++;
					} );
				}
			}

			if ( found > 0 ) {
				e.preventDefault();
				this._form.find('input[type="submit"],button[type="submit"]').prop( 'disabled', false );
				$(display).html( XRegExp.replace( $( display ).html(), new RegExp( "((\s+?)?(\r\n|\r|\n)(\s+?)?){1,}", "g" ), '<br><br>' ) );
				this.scope.show();
				var elemPosition = ips.utils.position.getElemPosition( this.scope );
	
				// Is it on the page?
				var windowScroll = $( window ).scrollTop();
				var viewHeight = $( window ).height();
	
				// Only scroll if it isn't already on the screen
				if( elemPosition.absPos.top < windowScroll || elemPosition.absPos.top > ( windowScroll + viewHeight ) ){
					$('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' } );	
				}

				return false;
			} else {
				this.scope.find('[data-role="editorCensorBlockMessage"]').html('<div data-role="editorCensorBlockMessageInternal"></div>');
				this.scope.hide();
				return true;
			}
		},
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.code.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.link.js - Controller for code panel in editor
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.code', {
		
		languageMap: {
			'clike': 'c',
			'coffeescript': 'coffee',
			'css': 'css',
			'dart': 'dart',
			'erlang': 'erlang',
			'go': 'go',
			'haskell': 'hs',
			'htmlmixed': 'html',
			'javascript': 'javascript',
			'lua': 'lua',
			'mumps': 'mumps',
			'pascal': 'pascal',
			'r': 'r',
			'perl': 'perl',
			'php': 'php',
			'python': 'py',
			'ruby': 'ruby',
			'scala': 'scala',
			'shell': 'bash',
			'sql': 'sql',
			'swift': 'swift',
			'tcl': 'tcl',
			'vbscript': 'vb',
			'vhdl': 'vhdl',
			'xml': 'xml',
			'xquery': 'xq',
			'yaml': 'yaml',
			'stex': 'latex'
		},
		
		instance: null,
		
		initialize: function () {
			this.setup();
			this.on( 'click', '.cEditorURLButtonInsert', this.formSubmit );
			this.on( 'change', '[data-role="codeModeSelect"]', this.changeMode );
		},
		
		setup:function(){
			var self = this;
			ips.loader.get( ['core/interface/codemirror/diff_match_patch.js','core/interface/codemirror/codemirror.js'] ).then( function () {
				
				var selectedMode = '';
				for ( var i in self.languageMap ) {
					if ( self.languageMap[i] == self.scope.find('[data-role="codeModeSelect"]').attr('data-codeLanguage') ) {
						selectedMode = i;
						break;
					}
				}
				self.scope.find('[data-role="codeModeSelect"]').empty();
				for ( var i in CodeMirror.modes ) {
					var languageName = ips.getString( 'editor_code_' + i );
					if ( !languageName ) {
						languageName = i.toUpperCase();
					}
					var option = $('<option/>').attr( 'name', i ).text( languageName );
					if ( selectedMode == i ) {
						option.attr('selected', 'selected');
					}
					self.scope.find('[data-role="codeModeSelect"]').append( option );
				}
				
				self.instance = CodeMirror.fromTextArea( document.getElementById( 'elCodeInput' + self.scope.attr('data-randomstring') ), {
					autofocus: true,
					mode: selectedMode,
					lineWrapping: true
				} );

				// We need to take special care with CKEditor's special invisible U+FEFF span
				self.instance.on( "beforeChange", function( codemirrorInstance, changeObj ){
					var newText		= changeObj.text;
					var modified	= false;

					_.each( changeObj.text, function( text, index ) {
						if( text.match( /[\ufeff]/g ) )
						{
							modified	= true;
							newText[ index ] = text.replace( /[\ufeff]/g, '' );
						}
					});

					if( modified )
					{
						changeObj.update( changeObj.from, changeObj.to, newText );
					}
				});

				self.scope.find('[data-role="codeLoading"]').remove();
				self.scope.find('[data-role="codeContainer"]').removeClass('ipsLoading');
			});
		},

		/**
		 * Event handler for changing the mode
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		changeMode: function (e) {
			this.instance.setOption( 'mode', this.scope.find('[data-role="codeModeSelect"] option:selected').attr('name') );
		},
				
		/**
		 * Event handler for submitting the form
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		formSubmit: function (e) {
			e.preventDefault();
			$(e.target).prop('disabled', true);
			this.insertCode(e);
		},

		/**
		 * Event handler for 'insert' button
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		insertCode: function (e) {
			var value = this.instance.getValue();			
			var editor = CKEDITOR.instances[ $( this.scope ).data('editorid') ];
			
			if ( this.scope.find('[data-role="codeModeSelect"] option:selected').attr('name') == 'null' ) {
				var element = CKEDITOR.dom.element.createFromHtml( "<pre class='ipsCode'></pre>" );
			} else {
				var lang = '';
				for ( var i in this.languageMap ) {
					if ( i == this.scope.find('[data-role="codeModeSelect"] option:selected').attr('name') ) {
						lang = 'lang-' + this.languageMap[i];
						break;
					}
				}
				var element = CKEDITOR.dom.element.createFromHtml( "<pre class='ipsCode prettyprint " + lang + "'></pre>" );
			}
			element.setText( value );
							
			this.scope.find('textarea').val('');
			editor.insertElement( element );
			editor.widgets.initOn( element, 'ipscode' );

			this.trigger('closeDialog');

			// Trigger a content change on the element so that it gets highlighted immediately
			$( document ).trigger( 'contentChange', [ $( element.$ ) ] );
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.codePreview.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450">/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.codePreview.js - Codemirror preview panel
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.global.editor.codePreview', {

		_origin: '',

		initialize: function () {
			this.on( 'click', '[data-action=&quot;preview&quot;]', this.fetchPreview );
		},

		fetchPreview: function (data) {
			var scope = $(this.scope);
			$('#' + scope.attr('data-name') + '_preview').addClass('ipsLoading').html('');
			ips.getAjax()( scope.attr('data-preview-url'), {
				type: 'POST',
				data: {
					'value': scope.find('textarea').data('CodeMirrorInstance').getValue()
				}
			} ).done( function (response) {
				$('#' + scope.attr('data-name') + '_preview').removeClass('ipsLoading').html( response );
			});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.customtags.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.customtags.js - Controller for inserting custom tags into a text/editor element
 *
 * Author: Rikki Tissier & Brandon Farber
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.customtags', {

		editorWrap: null,
		editorSidebar: null,
		editorSidebarHeader: null,
		editorSidebarList: null,

		initialize: function () {
			this.on( 'click', '[data-tagKey]', this.insertTag );
			this.on( 'click', '[data-action="tagsToggle"]', this.toggleSidebar );
			this.setup();
		},

		/**
		 * Setup method. Sets an interval that checks the height of the editor and sets the sidebar
		 * to the same height
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		setup: function () {
			var self = this;

			this.editorWrap = this.scope.find('[data-role="editor"]');
			this.editorSidebar = this.scope.find('.ipsComposeArea_sidebar');
			this.editorSidebarList = this.editorSidebar.find('[data-role="tagsList"]');
			this.editorSidebarHeader = this.editorSidebar.find('[data-role="tagsHeader"]');
			
			this.reloadTags();

			setInterval( function () {
				var editorHeight = self.editorWrap.outerHeight();
				var headerHeight = self.editorSidebarHeader.outerHeight();

				self.editorSidebarList.css({
					height: ( editorHeight - headerHeight ) + 'px'
				});
			}, 300);
		},

		/**
		 * Event handler for toggling the sidebar on and off
		 * Also set a cookie so that the choice is remembered
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		toggleSidebar: function (e) {
			e.preventDefault();

			if( this.editorSidebar.hasClass('ipsComposeArea_sidebarOpen') ) {
				this.editorSidebar
					.removeClass('ipsComposeArea_sidebarOpen')
					.addClass('ipsComposeArea_sidebarClosed');

				ips.utils.cookie.unset('tagSidebar');
			} else {
				this.editorSidebar
					.removeClass('ipsComposeArea_sidebarClosed')
					.addClass('ipsComposeArea_sidebarOpen');

				ips.utils.cookie.set('tagSidebar', true, true);
			}
		},

		/**
		 * Event handler for inserting custom tags defined on the page
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		insertTag: function (e) {
			var content = $( e.currentTarget ).attr('data-tagKey');

			if( this.scope.attr('data-tagFieldType') == 'editor' ){
				$( 'textarea[name="' + this.scope.attr('data-tagFieldID') + '"]' ).closest('[data-ipsEditor]').data('_editor').insertHtml( content );
			} else if( this.scope.attr('data-tagFieldType') == 'codemirror' ) {
				this.scope.trigger('codeMirrorInsert', { elemID: $( e.currentTarget ).closest('[data-codemirrorid]').attr('data-codemirrorid'), tag: content } );
			} else {
				var textField = $('#' + this.scope.attr('data-tagFieldID') );

				textField
					.focus()
					.insertText( content, textField.getSelection().start, 'collapseToEnd' );
			}
		},
		
		/**
		 * Reload tags list from source
		 *
		 * @return	{void}
		 */
		reloadTags: function() {
			if ( this.scope.attr('data-tagSource' ) )
			{
				ips.getAjax()( this.scope.attr('data-tagSource') )
					.done( function (response, status, jqXHR) {
						$('ul[data-role="tagsList"]').html( response )
					} );
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.emoticons.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.emoticons.js - Controller for emoticons panel
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.emoticons', {
						
		initialize: function () {
			this.on( 'click', '[data-emoji]', this.insertEmoji );
			this.on( 'menuItemSelected', '[data-role="skinToneMenu"]', this.changeSkinTone );
			
			this.on( document, 'menuOpened', this.menuOpened );
			this.on( document, 'menuClosed', this.menuClosed );
			this.on( 'focus', '[data-role="emoticonSearch"]', this.searchEmoticons );
			this.on( 'blur', '[data-role="emoticonSearch"]', this.stopSearchEmoticons );
			
			this.on( 'menuItemSelected', '[data-role="categoryTrigger"]', this.changeCategory );
			
			this.setup();
		},

		/**
		 * Setup when the controller is initialized
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this.editorID = this.scope.attr('data-editorID');
			
			ips.utils.emoji.getEmoji(function(emoji,categories){
				setTimeout(function(){
					this._buildEmoji( emoji, ips.utils.cookie.get('emojiSkinTone'), null, categories );
				}.bind(this),100);
			}.bind(this));
		},
		
		/**
		 * Insert emoji
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		insertEmoji: function(e) {
			/* Insert */
			this.trigger( $( document ), 'insertEmoji', {
				editorID: this.editorID,
				emoji: $( e.currentTarget ).attr('data-emoji'),
			});
			
			/* Clear search box */
			this.scope.find('[data-role="emoticonSearch"]').val('');
			
			/* Close menu */
			this.scope.trigger( 'closeMenu' );
			
			/* Rebuild so Recently Used is correct */
			ips.utils.emoji.getEmoji(function(emoji,categories){
				this._buildEmoji( emoji, ips.utils.cookie.get('emojiSkinTone'), null, categories );
			}.bind(this));
		},
		
		/* !Main panel */
				
		/**
		 * Show emoji
		 *
		 * @param 		{object}		emoji		Emoji data
		 * @param 		{string}		tone		Skin tone
		 * @param 		{RegExp|null}	search		RegEx for searching
		 * @param 		{array}			categories	The categories in the correct order (JS objects are unordered so may be incorrectly ordered in the emoji variable)
		 * @returns 	{void}
		 */
		_buildEmoji: function(emoji, tone, search, categories) {
						
			ips.controller.cleanContentsOf( this.scope.find('.ipsMenu_innerContent') );

			/* Init */
			var finalHtml = '';
			var categoryHtml = '';
			var pos = 0;
			var emojiForThisRow = '';
			var menuContent = [];
						
			// Start with recently used
			if ( !search && ips.utils.cookie.get( 'recentEmoji' ) ) {
				var recentlyUsed = ips.utils.cookie.get( 'recentEmoji' ).split(',');
				var newRecentlyUsed = [];
				if ( recentlyUsed.length ) {
					for ( var i = 0; i < recentlyUsed.length; i++ ) {
						if ( ips.utils.emoji.canRender( recentlyUsed[i] ) ) {
							var displayHtml = ips.utils.emoji.preview( recentlyUsed[i] );
							if ( displayHtml ) {
								newRecentlyUsed.push( recentlyUsed[i] );
								
								emojiForThisRow += ips.templates.render('core.editor.emoji', {
									display: displayHtml,
									name: null,
									code: recentlyUsed[i]
								} );					
								
								// Once we've reached the limit per line, add the line 
								if( newRecentlyUsed.length == 8 || newRecentlyUsed.length == 16 ) {
									categoryHtml += ips.templates.render('core.editor.emoticonRow', { emoticons: emojiForThisRow } );
									emojiForThisRow = '';
								}
							}
						}
					}
					if( emojiForThisRow ){
						categoryHtml += ips.templates.render('core.editor.emoticonRow', { emoticons: emojiForThisRow } );
					}
					if ( categoryHtml ) {
						finalHtml += ips.templates.render('core.editor.emoticonCategory', { title: ips.getString( 'emoji-category-recent' ), categoryID: category, emoticons: categoryHtml } );
						categoryHtml = '';
						emojiForThisRow = '';
					}
				}
				if ( newRecentlyUsed != recentlyUsed ) {
					ips.utils.cookie.set( 'recentEmoji', newRecentlyUsed.join(','), true );
				}
			}
		
			/* Loop all the emoji categories */
			for ( var i in categories ) {
				var category = categories[i];
				var categoryCount = 0;
								
				/* Loop each emoji... */
				for ( var i = 0; i < emoji[category].length; i++ ) {
					
					/* Include in search results? */
					if ( search ) {
						var match = false;
						if ( emoji[category][i].name.match( search ) ) {
							match = true;
						}
						if ( !match && emoji[category][i].shortNames ) {
							for ( var j = 0; j < emoji[category][i].shortNames.length; j++ ) {
								if ( emoji[category][i].shortNames[j].match( search ) ) {
									match = true;
								}
							}
						}
						if ( !match && emoji[category][i].ascii ) {
							for ( var j = 0; j < emoji[category][i].ascii.length; j++ ) {
								if ( emoji[category][i].ascii[j].match( search ) ) {
									match = true;
								}
							}
						}
						if ( !match ) {
							continue;
						}
					}
										
					/* Get which code we'll use */
					var codeToUse = emoji[category][i].code;
					if ( emoji[category][i].skinTone && tone && tone != 'none' ) {
						codeToUse = ips.utils.emoji.tonedCode( codeToUse, tone );
					}
					
					/* Display */
					emojiForThisRow += ips.templates.render('core.editor.emoji', {
						display: ips.utils.emoji.preview( codeToUse ),
						name: ips.haveString( 'emoji-' + emoji[category][i].name ) ? ips.getString( 'emoji-' + emoji[category][i].name ) : emoji[category][i].name,
						code: codeToUse
					} );
					
					/* Once we've reached the limit per line, add the line */
					pos++;
					categoryCount++;
					if( pos == 8 ) {
						categoryHtml += ips.templates.render('core.editor.emoticonRow', { emoticons: emojiForThisRow } );
						pos = 0;
						emojiForThisRow = '';
					}
				}
				
				/* Add the HTML */
				if ( !search ) {
					if( pos ){
						categoryHtml += ips.templates.render('core.editor.emoticonRow', { emoticons: emojiForThisRow } );
					}
					if ( categoryHtml ) {
						var categoryTitle = ['smileys_emotion','people_body','animals_nature','food_drink','activities','travel_places','objects','symbols','flags'].indexOf(category) == -1 ? emoji[category][0].categoryName : ips.getString( 'emoji-category-' + category );
						
						finalHtml += ips.templates.render('core.editor.emoticonCategory', { title: categoryTitle, categoryID: category, emoticons: categoryHtml } );
						categoryHtml = '';
						pos = 0;
						emojiForThisRow = '';
						
						menuContent.push( ips.templates.render('core.editor.emoticonMenu', { 
							title: categoryTitle,
							count: categoryCount,
							categoryID: category
						}));
					}
				}
			}
			if ( search ) {
				this.scope.find('[data-role="categoryTrigger"]').hide();
				if( pos ){
					categoryHtml += ips.templates.render('core.editor.emoticonRow', { emoticons: emojiForThisRow } );
				}
				if ( categoryHtml ) {
					finalHtml = ips.templates.render('core.editor.emoticonSearch', { emoticons: categoryHtml } );
				} else {
					finalHtml = ips.templates.render('core.editor.emoticonNoResults');
				}
			} else {
				this.scope.find('[data-role="categoryTrigger"]').show();
				this.scope.find('[data-role="categoryMenu"]').get(0).innerHTML = menuContent.join('');
			}
			
			/* Display */
			this.scope.find('.ipsEmoticons_content').get(0).innerHTML = finalHtml;
			
			/* Show the skin tone indicator */
			if ( ips.getSetting('emoji_style') != 'disabled' && ( ips.getSetting('emoji_style') != 'native' || ips.utils.emoji.canRender( '1F44D-1F3FB' ) ) ) {
				this.scope.find("[data-role='skinToneMenu']").show();
				switch ( tone ) {
				case 'light':
					this.scope.find("[data-role='skinToneIndicator']").text( String.fromCodePoint( parseInt( '1F44D', 16 ) ) + String.fromCodePoint( parseInt( '1F3FB', 16 ) ) );
					break;
				case 'medium-light':
					this.scope.find("[data-role='skinToneIndicator']").text( String.fromCodePoint( parseInt( '1F44D', 16 ) ) + String.fromCodePoint( parseInt( '1F3FC', 16 ) ) );
					break;
				case 'medium':
					this.scope.find("[data-role='skinToneIndicator']").text( String.fromCodePoint( parseInt( '1F44D', 16 ) ) + String.fromCodePoint( parseInt( '1F3FD', 16 ) ) );
					break;
				case 'medium-dark':
					this.scope.find("[data-role='skinToneIndicator']").text( String.fromCodePoint( parseInt( '1F44D', 16 ) ) + String.fromCodePoint( parseInt( '1F3FE', 16 ) ) );
					break;
				case 'dark':
					this.scope.find("[data-role='skinToneIndicator']").text( String.fromCodePoint( parseInt( '1F44D', 16 ) ) + String.fromCodePoint( parseInt( '1F3FF', 16 ) ) );
					break;
				default:
					this.scope.find("[data-role='skinToneIndicator']").text( String.fromCodePoint( parseInt( '1F44D', 16 ) ) );
					break;
				}
			}
		},
		
		/**
		 * Event handler called when the skin tone is change
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		changeSkinTone: function (e, data) {
			ips.utils.emoji.getEmoji(function(emoji,categories){
				this._buildEmoji( emoji, data.selectedItemID, this._lastVal ? new RegExp( this._lastVal.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" ), 'i' ) : null, categories );
			}.bind(this));
			ips.utils.cookie.set( 'emojiSkinTone', data.selectedItemID, true );
		},
		
		/* !Search */

		_typeTimer: null,
		_lastVal: '',
		
		/**
		 * Event handler called when the emoticons menu is opened.
		 * Iniializes the menu if it hasn't already been done
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		menuOpened: function (e, data) {
			if( data.menu.attr('data-controller') == 'core.global.editor.emoticons' ){
				setTimeout(function(){
					this.scope.find('.ipsEmoticons_content').show();
					this.scope.find('.ipsMenu_innerContent').css({ height: 'auto' });
					this.scope.find('[data-role="emoticonSearch"]').focus();
				}.bind(this),100);
			}
		},

		/**
		 * Event handler called when the emoticons menu is closed.
		 * Hide the content area but set a fixed height on the innerContent elem to preserve position
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		menuClosed: function (e, data) {
			if( data.menu.attr('data-controller') == 'core.global.editor.emoticons' ){
				var inner = this.scope.find('.ipsMenu_innerContent');
				var content = this.scope.find('.ipsEmoticons_content');
				inner.css({ height: content.outerHeight() + 'px' });
				content.hide();
			}
		},

		/**
		 * The search box has received focus
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		searchEmoticons: function (e) {
			this._typeTimer = setInterval( _.bind( this._typing, this ), 200 );
		},

		/**
		 * The search box has blurred
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		stopSearchEmoticons: function (e) {
			if( this._typeTimer ){
				clearInterval( this._typeTimer );
				this._typeTimer = null;
			}
		},

		/**
		 * Runs a continuous interval to check the current search value, and call the search function
		 *
		 * @param 		{event} 	e 		Event object	
		 * @returns 	{void}
		 */
		_typing: function () {
			var textElem = this.scope.find('[data-role="emoticonSearch"]');

			if( this._lastVal == textElem.val() ){
				return;
			}

			if( textElem.val() == '' ){
				this._clearSearch();
			} else {
				this._doSearch( textElem.val() );
			}

			this._lastVal = textElem.val();
		},

		/**
		 * Clears the search panel (called when value is empty)
		 *
		 * @returns 	{void}
		 */
		_clearSearch: function () {
			ips.utils.emoji.getEmoji(function(emoji,categories){
				this._buildEmoji( emoji, ips.utils.cookie.get('emojiSkinTone'), null, categories );
			}.bind(this));
		},

		/**
		 * Finds emoticons matching the value
		 *
		 * @param 		{string} 	value  		Search value	
		 * @returns 	{void}
		 */
		_doSearch: function (value) {
			ips.utils.emoji.getEmoji(function(emoji,categories){
				this._buildEmoji( emoji, ips.utils.cookie.get('emojiSkinTone'), new RegExp( value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" ), 'i' ), categories );
			}.bind(this));
		},
		
		/* !Categories */
		
		/**
		 * Event handler called when the a category is selected
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		changeCategory: function (e, data) {
			this.scope.find('.ipsMenu_innerContent').scrollTop( this.scope.find('.ipsMenu_innerContent').scrollTop() + this.scope.find('[data-categoryid="' + data.selectedItemID + '"]').position().top - 85 );
		}
	
	});
	
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.giphy.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.giphy.js - Controller for Giphy Actions
 *
 * Author: Daniel Fatkic
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.giphy', {

		initialize: function () {
			this.on( 'click', '.ipsGiphyImage', this.insertImage );
			this.on( 'focus', '[data-role="giphySearch"]', this.searchGiphy );
			this.on( 'blur', '[data-role="giphySearch"]', this.stopSearchGiphy );
			$('[data-role="giphyResults"]').on( 'scroll', this.scrollEvent.bind(this) );

			this.on( document, 'menuOpened', this.menuOpened );
			this.setup();
		},
		_typeTimer: null,
		_lastVal: '',
		_perPage: 30,
		_status: 'init',
		
		setup: function () {
			this._editorId = $( this.scope ).data('editorid');
			this._editor = CKEDITOR.instances[ this._editorId ];
		},


		searchGiphy: function (e) {
			this._typeTimer = setInterval( _.bind( this._typing, this ), 1500 );
		},
		
		/**
		 * Event handler for scrolling
		 * 
		 * @param 		{event}	 	e 		Event object
		 * @returns 	{void}
		 */
		scrollEvent: function (e) {
			
			var scrollScope = $('[data-role="giphyResults"]');
			var scrollHeight = scrollScope[0].scrollHeight;
			var distanceFromBottom = scrollHeight - scrollScope.height() - scrollScope.scrollTop();
			
			if ( this._status != 'ready' ) {
				return;
			}
			
			if( distanceFromBottom <= 150 ){
				this._status = 'loading';
				
				var offset = parseInt( this.scope.find('[data-role="giphyMore"]').attr('data-offset') );
				this.scope.find('[data-role="giphyMore"]').attr('data-offset', offset + this._perPage );
				
				this.scope.find('[data-role="giphyMoreLoading"]').removeClass('ipsHide');
				
				this._doSearch( this._lastVal );
			}
		},
		
		/**
		 * The search box has blurred
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		stopSearchGiphy: function (e) {
			if( this._typeTimer ){
				clearInterval( this._typeTimer );
				this._typeTimer = null;
			}
			/* Clear Results */
		},

		/**
		 * Runs a continuous interval to check the current search value, and call the search function
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_typing: function () {
			var textElem = this.scope.find('[data-role="giphySearch"]');

			if( this._lastVal == textElem.val() ){
				return;
			}
			
			this.scope.find('[data-role="giphyMore"]').attr('data-offset', 0);
			this.scope.find('.ipsMenu_innerContent').animate({
				scrollTop: "0"
			}, 250);
			
			this._doSearch( textElem.val() );
			
			this._lastVal = textElem.val();
		},

		/**
		 * AJAX call to fetch the images
		 *
		 * @param 		{string} 	value  		Search value
		 * @returns 	{void}
		 */
		_doSearch: function (value) {
			var resultsbox = this.scope.find('[data-role="giphyLoading"]');
			var offset = parseInt( this.scope.find('[data-role="giphyMore"]').attr('data-offset') );
			
			Debug.log( offset + ',' + this._perPage );
			
			resultsbox.addClass('ipsLoading');
			var _self = this;
									
			this._status = 'loading';
			ips.getAjax()( this._editor.config.controller + '&do=giphy&offset=' + offset + '&limit=' + this._perPage, {
				type: 'POST',
				data: {
					'search': value
				}
			} ).done( function (response) {
				var data = response;
				var result = ( offset > 0 ) ? resultsbox.html() : '';
				var pos = 0;
				var gifsForThisRow = '';

				if ( data.error )
				{
				   result = data.error;
				}
				else
				{
					_.each( data.images, function (term) {
						gifsForThisRow += ips.templates.render('core.editor.giphyThumb', {
							thumb: term.thumb,
							url: term.url,
							title: term.title
						} );

						/* Once we've reached the limit per line, add the line */
						pos++;
						if( pos == 3 ) {
							result += ips.templates.render('core.editor.giphyRow', { gifs: gifsForThisRow } );
							pos = 0;
							gifsForThisRow = '';
						}
					} );
					Debug.log( data.pagination.total_count );
					Debug.log( offset + _self._perPage );
					if ( offset > 0 || data.pagination.total_count > offset + _self._perPage ) {
						_self.scope.find('[data-role="giphyMoreLoading"]').addClass('ipsHide');
						_self._status = 'ready';
					}

					// No more available
					if ( data.pagination.total_count <= offset + _self._perPage ) {
						_self.scope.find('[data-role="giphyMoreLoading"]').addClass('ipsHide');
						_self._status = 'done';
					}
				}
								
				resultsbox.removeClass('ipsLoading').html( result );
			} );
		},
		
		insertImage: function(e)
		{
			var image = $( e.target );
			var element = CKEDITOR.dom.element.createFromHtml( '<img src="' + image.attr('data-url') + '">' );
			this._editor.insertElement( element );

			element.$.alt = image.attr('alt');
			element.$.title = image.attr('title');

			ips.utils.lazyLoad.applyLazyLoadAttributes( element.$ );
			ips.utils.lazyLoad.loadContent( element.$ );

			/* Now clear search and close the menu */
			this.scope.find('[data-role="giphySearch"]').val('');
			this.scope.find('[data-role="giphyLoading"]').addClass('ipsLoading').html('');
			this.scope.trigger( 'closeMenu' );
		},

		/**
		 * Event handler called when the emoticons menu is opened.
		 * Initializes the menu if it hasn't already been done
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		menuOpened: function (e, data) {
			if( data.menu.attr('data-controller') == 'core.global.editor.giphy' ){
				/* Submit an empty search when we open the menu, this will return the top trending gifs */
				this._doSearch("");
				setTimeout(function(){
					this.scope.find('[data-role="giphySearch"]').focus();
				}.bind(this),100);
			}
		},
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.image.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.image.js - Controller for image properties in editor
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.image', {
		
		_typingTimer: null,
		_textTypingTimer: null,
		_ajaxObj: null,
		_imageWidth: null,
		_ratioWidth: 1,
		_ratioHeight: 1,
		
		initialize: function () {
			this.on( 'submit', 'form', this.formSubmit );
			this.on( 'change', '[data-role="imageHeight"]', this.changeHeight );
			this.on( 'change', '[data-role="imageWidth"]', this.changeWidth );
			this.on( 'click', 'label[for^="image_align"]', this.toggleAlign );
			this.on( 'change', 'input[name="image_aspect_ratio"]', this.toggleRatio );
			this.setup();
		},

		/**
		 * Setup
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this._ratioWidth = this.scope.attr('data-imageWidthRatio');
			this._ratioHeight = this.scope.attr('data-imageHeightRatio');

			if( this.scope.find('input[name="image_aspect_ratio"]').is(':checked') ){
				this.scope.find('[data-role="imageHeight"]').prop('disabled', true);
			}
		},

		/**
		 * Toggles between the alighment options, highlighting the one the user clicked
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		toggleAlign: function (e) {
			var thisLabel = $( e.currentTarget );
			this.scope.find('label[for^="image_align"]').removeClass('ipsButton_primary').addClass('ipsButton_light');
			thisLabel.removeClass('ipsButton_light').addClass('ipsButton_primary');
		},

		/**
		 * Event handler for toggling the 'preserve aspect ratio' option
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		toggleRatio: function (e) {
			var sameRatio = $( e.currentTarget ).is(':checked');

			if( sameRatio ){
				this.changeWidth();
			}

			this.scope.find('[data-role="imageHeight"]').prop('disabled', sameRatio);
		},

		/**
		 * Event handler for changing the height
		 * Keeps the width in ratio if enabled
		 *
		 * @returns 	{void}
		 */
		changeHeight: function () {
			var sameRatio = this.scope.find('input[name="image_aspect_ratio"]').is(':checked');
			var thisVal = parseInt( this.scope.find('[data-role="imageHeight"]').val() );
			var width = this.scope.find('[data-role="imageWidth"]');
			var widthVal = parseInt( width.val() );

			if( sameRatio ){
				width.val( Math.floor( thisVal * this._ratioWidth ) );
			}
		},

		/**
		 * Event handler for changing the width
		 * Keeps the height in ratio if enabled
		 *
		 * @returns 	{void}
		 */
		changeWidth: function () {
			var sameRatio = this.scope.find('input[name="image_aspect_ratio"]').is(':checked');
			var thisVal = parseInt( this.scope.find('[data-role="imageWidth"]').val() );
			var height = this.scope.find('[data-role="imageHeight"]');
			var heightVal = parseInt( height.val() );

			if( sameRatio ){
				height.val( Math.floor( thisVal * this._ratioHeight ) );
			}
		},

		/**
		 * Event handler for submitting the form
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		formSubmit: function (e) {
			e.preventDefault();
			e.stopPropagation();
			this._updateImage(e);
		},

		/**
		 * Event handler for 'insert' url button
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_updateImage: function (e) {
			
			var widthInput = this.scope.find('[data-role="imageWidth"]');
			var heightInput = this.scope.find('[data-role="imageHeight"]');
			var sameRatio = this.scope.find('input[name="image_aspect_ratio"]').is(':checked');
			var error = false;

			if ( parseInt( widthInput.val() ) > parseInt( widthInput.attr('max') ) ) {
				error = true;
				widthInput.closest('.ipsFieldRow').addClass('ipsFieldRow_error');
				this.scope.find('[data-role="imageSizeWarning"]').text( ips.getString( 'editorImageMaxWidth', { 'maxwidth': widthInput.attr('max') } ) );
			}
			if ( parseInt( heightInput.val() ) > parseInt( heightInput.attr('max') ) ) {
				error = true;
				widthInput.closest('.ipsFieldRow').addClass('ipsFieldRow_error');
				this.scope.find('[data-role="imageSizeWarning"]').text( ips.getString( 'editorImageMaxHeight', { 'maxheight': heightInput.attr('max') } ) );
			}
			
			if ( !error ) {
				// editorUniqueId will be something like cke_1, which is unique on the page
				var editor = $('.cke.' + this.scope.attr('data-editorUniqueId')).closest('[data-ipsEditor]').data('_editor');
				editor.updateImage( widthInput.val(), ( sameRatio ? 'auto' : heightInput.val() ), this.scope.find('[data-role="imageAlign"]:checked').val(), this.scope.find('[data-role="imageLink"]').val(), this.scope.find('[data-role="imageAlt"]').val() );
								
				this.trigger('closeDialog');
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.insertable.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.insertable.js - Allows items to be inserted into the editor
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.insertable', {

		_editorID: '',
		_selectedItems: {},
		_tooltip: null,
		_tooltipTimer: null,

		initialize: function () {
			this.on( 'click', '[data-action="insertFile"]', this.insertFile );
			this.on( 'click', '[data-action="selectFile"]', this.selectFile );
			this.on( 'click', '[data-action="insertSelected"]', this.insertSelected );
			this.on( 'click', '[data-action="clearAll"]', this.clearSelection );
			this.on( 'fileInjected', this.fileInjected );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns	{void}
		 */
		setup: function () {
			this._editorID = this.scope.attr('data-editorID');
			this._selectedItems = {};
		},

		destruct: function () {
			Debug.log('destruct insertable');
		},

		/**
		 * Toggle a file selection
		 *
		 * @param	{event}	e	Event Object
		 * @returns	{void}
		 */
		selectFile: function (e) {
			e.preventDefault();

			var thisAttach = $( e.currentTarget ).closest('.ipsAttach');
			var thisToggle = thisAttach.find('[data-action="selectFile"]');
			var thisDataRow = thisAttach.closest('.ipsDataItem');
			
			if( thisToggle.hasClass('ipsAttach_selectionOn') ){
				thisToggle.removeClass('ipsAttach_selectionOn');
				
				if( thisDataRow.length ){
					thisDataRow.removeClass('ipsDataItem_selected');
				} else {
					thisAttach.removeClass('ipsAttach_selected');
				}

				this._removeSelectedItem( thisAttach );
			} else {
				thisToggle.addClass('ipsAttach_selectionOn');
				
				if( thisDataRow.length ){
					thisDataRow.addClass('ipsDataItem_selected');
				} else {
					thisAttach.addClass('ipsAttach_selected');
				}

				this._addSelectedItem( thisAttach );
			}
		},
		
		/**
		 * Clears currently-selected items
		 *
		 * @param	{event}		e	Event data
		 * @returns	{void}
		 */
		clearSelection: function (e) {
			if( e ){
				e.preventDefault();
			}

			if( !_.size( this._selectedItems ) ){
				return;
			}

			// Empty object
			this._selectedItems = {};

			// Remove class from all elements in the dom
			this.scope
				.find('.ipsAttach_selectionOn')
					.removeClass('ipsAttach_selectionOn')
					.closest('.ipsAttach')
						.removeClass('ipsAttach_selected')
					.end()
					.closest('.ipsDataItem')
						.removeClass('ipsDataItem_selected');

			// Update buttons
			this._checkSelectedButton();
		},

		/**
		 * Inserts selected files into the editor
		 *
		 * @param	{event}		e	Event data
		 * @returns	{void}
		 */
		insertSelected: function (e) {
			e.preventDefault();

			var self = this;

			if( !_.size( this._selectedItems ) ){
				return;
			}
			
			if( !this.scope.closest('[data-role="attachmentArea"]').length ){
				this.trigger('closeDialog');
			}

			var editor = $( 'textarea[name="' + this._editorID + '"]' ).closest('[data-ipsEditor]').data('_editor');
			_.each( this._selectedItems, function (item) {
				editor.insertHtml( self._buildInsert( item ) );
			});

			this.clearSelection();
		},

		/**
		 * Allows attachments to be inserted into the editor individually
		 *
		 * @param	{event}	e	Event Object
		 * @returns	{void}
		 */
		insertFile: function(e) {			
			if( e ){
				e.preventDefault();
			}
			
			var editor = $( 'textarea[name="' + this._editorID + '"]' ).closest('[data-ipsEditor]').data('_editor');
			var insertData = this._buildInsertData( $( e.target ) );
			var insertHtml = this._buildInsert( insertData );

			editor.insertHtml( insertHtml );

			if( !this.scope.closest('[data-role="attachmentArea"]').length ){
				this.trigger('closeDialog');
			}

			if( insertData.type == 'image' ){
				// Add a tooltip to let users know they can double click it
				this._showImageTooltip( insertData.fileID );
			}
		},
				
		/**
		 * File injected
		 *
		 * @param	{event}		e		Event Object
		 * @param	{event}		data	Event data object
		 * @returns	{void}
		 */
		fileInjected: function (e, data) {
			$(this.scope).trigger( 'injectedFileReadyForInsert', { content: this._buildInsert( this._buildInsertData( data.fileElem ) ), data: data.data } );
		},

		_showImageTooltip: function (fileID) {
			if( !this._tooltip ){
				// Build it from a template
				var tooltipHTML = ips.templates.render( 'core.tooltip', {
					id: 'elEditorImageTooltip_' + this.controllerID,
					content: ips.getString('editorEditImageTip')
				});

				// Append to body
				ips.getContainer().append( tooltipHTML );

				this._tooltip = $('#elEditorImageTooltip_' + this.controllerID );
			} else {
				this._tooltip.hide();
			}

			if( this._tooltipTimer ){
				clearTimeout( this._tooltipTimer );
			}

			// Get image
			var imageFile = $('#cke_' + this._editorID ).find('[data-fileID="' + fileID + '"]').last();
			var self = this;

			// Now position it
			var positionInfo = {
				trigger: imageFile,
				target: this._tooltip,
				center: true,
				above: true
			};

			var tooltipPosition = ips.utils.position.positionElem( positionInfo );

			$( this._tooltip ).css({
				left: tooltipPosition.left + 'px',
				top: tooltipPosition.top + 'px',
				position: ( tooltipPosition.fixed ) ? 'fixed' : 'absolute',
				zIndex: ips.ui.zIndex()
			});

			if( tooltipPosition.location.vertical == 'top' ){
				this._tooltip.addClass('ipsTooltip_top');
			} else {
				this._tooltip.addClass('ipsTooltip_bottom');
			}

			this._tooltip.show();

			setTimeout( function () {
				if( self._tooltip && self._tooltip.is(':visible') ){
					ips.utils.anim.go( 'fadeOut', self._tooltip );
				}
			}, 3000);
		},

		/**
		 * Adds an item to the selected items list
		 *
		 * @param	{element}	element		The file element to be added
		 * @returns	{void}
		 */
		_addSelectedItem: function (element) {
			var fileID = element.attr('data-fileid');
			this._selectedItems[ fileID ] = this._buildInsertData( element );
			this._checkSelectedButton();
		},

		/**
		 * Removes an item from the selected items list
		 *
		 * @param	{element}	element		The file element to be removed
		 * @returns	{void}
		 */
		_removeSelectedItem: function (element) {
			var fileID = element.attr('data-fileid');

			if( !_.isUndefined( this._selectedItems[ fileID ] ) ){
				delete this._selectedItems[ fileID ];
			}

			this._checkSelectedButton();
		},

		/**
		 * Enables the 'clear selection' and 'insert selected files' buttons if there's any selected items
		 *
		 * @returns	{void}
		 */
		_checkSelectedButton: function () {
			var button = this.scope.find('[data-action="insertSelected"]');

			this.scope.find('[data-action="clearAll"]').toggleClass('ipsButton_disabled', !( _.size( this._selectedItems ) > 0 ) );
			button.toggleClass('ipsButton_disabled', !( _.size( this._selectedItems ) > 0 ) );

			if( !_.size( this._selectedItems ) ){
				button.text( ips.getString('insertSelected') );
			} else {
				button.text( ips.pluralize( ips.getString('insertSelectedNum'), _.size( this._selectedItems ) ) );
			}
		},

		/**
		 * Builds insertable element data based on the provided attached file element
		 *
		 * @param	{element}	element		The element on which the insert is based
		 * @returns	{void}
		 */
		_buildInsertData: function (element) {
			var element = element.closest('.ipsAttach');
			var fileID = element.attr('data-fileid');
			var fileKey = element.attr('data-filekey');
			var type = ( element.attr('data-fileType') ) ? element.attr('data-fileType') : 'file';
			var url = '';
			var image = '';
			var extension = '';
			var mimeType = '';
			
			if( type == 'image' ){
				url = ( element.attr('data-thumbnailurl') ) ? element.attr('data-thumbnailurl') : element.attr('data-fullsizeurl');

				if( url != element.attr('data-fullsizeurl') ){
					image = element.attr('data-fullsizeurl');
				}
			} else if ( type == 'video' ) {
				image = element.attr('data-fullsizeurl');
				mimeType = element.attr('data-mimeType');
			} else if ( type == 'audio' ) {
				mimeType = element.attr('data-mimeType');
			} else {
				url = ( element.attr('data-filelink') ) ? element.attr('data-filelink') : '';
			}
			
			extension = element.closest('[data-extension]').attr('data-extension');

			return {
				fileID: fileID,
				fileKey: fileKey,
				type: type,
				title: ( type != 'image' ) ? element.find('[data-role="title"]').html() : '',
				link: url,
				fullImage: image,
				extension: extension,
				mimeType: mimeType
			};
		},

		/**
		 * Builds an element that can be inserted into the editor
		 *
		 * @param	{object}	item		Item data used to build element
		 * @returns	{string}
		 */
		_buildInsert: function (item) {
			var element = null;

			if( item.type == 'image' ){
				// Give the img a unique ID, otherwise removing image in editor when added more than once will only remove one
				element = $('<img/>').attr({
					'data-fileid':	item.fileID,
					'src': item.link,
					'data-unique': Math.random().toString(36).substr(2, 9)
				}).addClass('ipsImage ipsImage_thumbnailed');
				
				if ( item.extension ) {
					element.attr( 'data-extension', item.extension );
				}
					
				if( item.fullImage ){
					var link = $('<a>').attr( 'href', item.fullImage ).addClass('ipsAttachLink ipsAttachLink_image');
					element.addClass( 'ipsImage_thumbnailed');
					link.append( element );

					element	= link;
				}
			} else if( item.type == 'video' ){
								
				var element = $('<video controls>').attr({
					'class': 'ipsEmbeddedVideo',
					'data-controller': 'core.global.core.embeddedvideo',
					'data-fileid':	item.fileID,
					'data-unique': Math.random().toString(36).substr(2, 9)
				});
				
				var sourceElement = $('<source>').attr({
					'src': item.fullImage,
					'type':	item.mimeType
				});
				element.append( sourceElement );
				
				var fallbackLink = $('<a>').addClass('ipsAttachLink').attr( 'href', ips.getSetting('baseURL') + 'applications/core/interface/file/attachment.php?id=' + item.fileID + ( item.fileKey ? '&key=' + item.fileKey : '' ) ).html( item.title );
				element.append( fallbackLink );
				
			} else if( item.type == 'audio' ){

				var element = $('<audio controls>').attr({
					'data-controller': 'core.global.core.embeddedaudio',
					'src': ips.getSetting('baseURL') + 'applications/core/interface/file/attachment.php?id=' + item.fileID + ( item.fileKey ? '&key=' + item.fileKey : '' ),
					'data-fileid':	item.fileID,
					'data-unique': Math.random().toString(36).substr(2, 9),
					'type': item.mimeType
				});
				
				var fallbackLink = $('<a>').addClass('ipsAttachLink').attr( 'href', ips.getSetting('baseURL') + 'applications/core/interface/file/attachment.php?id=' + item.fileID + ( item.fileKey ? '&key=' + item.fileKey : '' ) ).html( item.title );
				element.append( fallbackLink );

			} else {
				element = $('<a>').addClass('ipsAttachLink').html( item.title ).attr('data-fileid', item.fileID).attr('data-fileext', item.extension);

				if( item.link ){
					element.attr( 'href', item.link );
				} else {
					var url = ips.getSetting('baseURL') + 'applications/core/interface/file/attachment.php?id=' + item.fileID;
					if ( item.fileKey )
					{
						url = url + '&key=' + item.fileKey;
					}
					element.attr( 'href', url );
				}
			}
			
			if ( item.extension ){
				element.attr( 'data-extension', item.extension );
			}

			let html = $('<div/>').append( element ).html();

			if( item.type === 'video' || item.type === 'audio' ){
				html += "&nbsp;<p>&nbsp;</p>";
			}
			
			return html;
		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.link.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.link.js - Controller for link panel in editor
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.link', {
		
		_typingTimer: null,
		_textTypingTimer: null,
		_ajaxObj: null,
		
		initialize: function () {
			this.on( 'submit', 'form', this.formSubmit );
			this.on( 'click', '.cEditorURLButtonInsert', this.formSubmit );
			this.on( 'click', '[data-action="linkRemoveButton"]', this.removeLink );

			this.scope.find('[data-role="linkURL"]').focus();
		},

		/**
		 * Event handler for submitting the form
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		formSubmit: function (e) {
			e.preventDefault();
			this.insertLink(e);
		},

		/**
		 * Event handler for 'insert' url button
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		insertLink: function (e) {
			var url = this.scope.find('[data-role="linkURL"]').val().replace(/'/g, '%27').replace(/"/g, '%22').replace(/</g, '%3C').replace(/>/g, '%3E');
			
			if ( !url ) {
				$(this.scope).find('.ipsFieldRow.ipsFieldRow_fullWidth').addClass('ipsFieldRow_error');
				return;
			} else {
				$(this.scope).find('.ipsFieldRow.ipsFieldRow_fullWidth').removeClass('ipsFieldRow_error');
			}

			$(this.scope).find('.elLinkError').remove();

			if ( !url.match( /^[a-z]+\:\/\//i ) && !url.match( /^mailto\:/i ) && !url.match( /^\#/ ) ) {
				url = 'http://' + url.replace( /^\/*/, '' );
			}
			
			var editor = CKEDITOR.instances[ $( this.scope ).data('editorid') ];
			var selection = editor.getSelection();

			if ( !_.isUndefined( editor._linkBookmarks) ) {
				selection.selectBookmarks( editor._linkBookmarks );
				delete editor._linkBookmarks;
			}
			
			var selectedElement = selection.getSelectedElement();
			if ( selectedElement && selectedElement.is('img') ) {
				var selectedElement = $( selection.getSelectedElement().$ );

				if( !selectedElement.parent().is('a') )
				{
					var element = CKEDITOR.dom.element.createFromHtml( "<a href='" + url + "'>" + selectedElement[0].outerHTML + "</a>" );

					editor.insertElement( element );
				}
				else
				{
					selectedElement.parent().attr( 'href', url ).removeAttr('data-cke-saved-href');
				}
				this.scope.find('input.cEditorURL').val('');
				this.trigger('closeDialog');
			} else if ( selectedElement && ( selectedElement.is('a') && $( selection.getSelectedElement().$ ).children().is('img') ) ) { 
				selectedElement.setAttribute( 'href', url ).removeAttribute('data-cke-saved-href');

				this.scope.find('input.cEditorURL').val('');
				this.trigger('closeDialog');
			} else {
				if ( $( this.scope ).data('image') ) {
					
					this.scope.find('[data-role="linkURL"]').addClass('ipsField_loading');
					this.scope.find('[data-action="linkButton"]').prop('disabled', true);
					
					var scope = this.scope;
					var self = this;
					
					var img = new Image();
					img.onerror = function(){
						scope.find('[data-role="linkURL"]').removeClass('ipsField_loading');
						scope.find('[data-action="linkButton"]').prop('disabled', false);
						scope.find('.ipsFieldRow.ipsFieldRow_fullWidth').addClass('ipsFieldRow_error');
					};
					img.onload = function(){
						
					    var ajaxUrl = editor.config.controller + '&do=validateLink'
						if ( $(this.scope).attr('data-image') ) {
							ajaxUrl += '&image=1';
						}
						
						ips.getAjax()( ajaxUrl, {
							data: {
								url: url,
								width: img.width,
								height: img.height,
								image: 1
							},
							type: 'post'
						})
						.done(function( response ){
							if ( response.embed ) {
								scope.find('[data-role="linkURL"]').removeClass('ipsField_loading');
								scope.find('[data-action="linkButton"]').prop('disabled', false);
								scope.find('input.cEditorURL').val('');
								editor.insertHtml( response.preview );
								self.trigger('closeDialog');
							} else {
								scope.find('[data-role="linkURL"]').removeClass('ipsField_loading');
								scope.find('[data-action="linkButton"]').prop('disabled', false);
								scope.find('.ipsFieldRow.ipsFieldRow_fullWidth').addClass('ipsFieldRow_error');

								if( !_.isUndefined( response.errorMessage ) )
								{
									scope.find('.ipsFieldRow.ipsFieldRow_fullWidth').append( "<span class='elLinkError ipsType_warning'>" + response.errorMessage + "</span>" );
								}
							}
						})
						.fail(function(){
							scope.find('[data-role="linkURL"]').removeClass('ipsField_loading');
							scope.find('[data-action="linkButton"]').prop('disabled', false);
							scope.find('.ipsFieldRow.ipsFieldRow_fullWidth').addClass('ipsFieldRow_error');
						});
					}
					img.src = url;
				} else {
					// Normal link
					if( this.scope.find('[data-role="linkText"]').length )
					{
						var title = this.scope.find('[data-role="linkText"]').val().replace( / {2}/g,' &nbsp;' );
						if ( !title ) {
							title = decodeURI( url );
						}
						title = title.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

						var element = CKEDITOR.dom.element.createFromHtml( "<a>" + title + "</a>" );
					}
					// Something (i.e. an img tag) is selected
					else
					{
						element = selectedElement;
					}

					element.setAttribute( 'href', url );

					editor.insertElement( element );
					this.scope.find('input.cEditorURL').val('');
					this.trigger('closeDialog');
				}
			}
		},
		
		/**
		 * Event handler for remove link button
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		removeLink: function (e) {			
			e.preventDefault();
			e.stopPropagation();
			
			var editor = CKEDITOR.instances[ $( this.scope ).data('editorid') ];
			editor.focus();
			editor.execCommand( 'ipsLinkRemove' );
			
			this.trigger('closeDialog');
			
		},
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.mymedia.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.mymedia.js - My media controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.global.editor.mymedia', {

		initialize: function () {
			this.on( window, 'resize', _.bind( this._resizeContentArea, this ) );
			this.setup();
		},

		setup: function () {
			this._resizeContentArea();
		},

		/**
		 * Resizes the mymedia content area to be the correct height for the dialog
		 *
		 * @returns	{void}
		 */
		_resizeContentArea: function () {
			// Get size of dialog content
			var dialogHeight = this.scope.closest('.ipsDialog_content').outerHeight();
			var controlsHeight = this.scope.find('.cMyMedia_controls').outerHeight();

			// Set the content area to that height
			this.scope.find('[data-role=&quot;myMediaContent&quot;]').css({
				height: ( dialogHeight - controlsHeight - 10 ) + 'px'
			});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.mymediasection.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.mymediasection.js - My media section
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.mymediasection', {

		_timer: null,
		_ajax: null,
		_value: '',

		initialize: function () {
			//this.on( 'input', '[data-role="myMediaSearch"]', this.myMediaSearch );
			this.on( 'focus', '[data-role="myMediaSearch"]', this.focusMediaSearch );
			this.on( 'blur', '[data-role="myMediaSearch"]', this.blurMediaSearch );
			this.on( 'paginationClicked paginationJump', this.paginationClicked );
		},

		paginationClicked: function (e, data) {
			var self = this;
			var results = this.scope.find('[data-role="myMediaResults"]');
			var url = data.href;

			data.originalEvent.preventDefault();

			if( url == '#' ){
				// Manually build URL if we're using the pagejump
				url = data.paginationElem.find('[data-role="pageJump"]').attr('action') + '&page=' + data.pageNo;
			}

			// Load another page
			this._ajax = ips.getAjax()( url, {
				showLoading: true,
				data: {
					search: this._value
				}
			} )
				.done( function (response) {
					results.html( response );
					$( document ).trigger( 'contentChange', [ results ] );
				});
		},

		/**
		 * Event handler for focusing the search box
		 *
		 * @returns	{void}
		 */
		focusMediaSearch: function () {
			// Start the timer going
			this._timer = setInterval( _.bind( this._checkValue, this ), 700 );
		},

		/**
		 * Event handler for blurring the search box
		 *
		 * @returns	{void}
		 */
		blurMediaSearch: function () {
			clearInterval( this._timer );
		},

		/**
		 * If the current value is different to the previous value, run the search
		 *
		 * @returns	{void}
		 */
		_checkValue: function () {
			var value = this.scope.find('[data-role="myMediaSearch"]').val();

			if( value == this._value ){
				return;
			}

			this._value = value;
			this._loadResults();
		},

		/**
		 * Runs a search
		 *
		 * @returns	{void}
		 */
		_loadResults: function () {
			var self = this;
			var url = this.scope.attr('data-url');
			
			// Abort any requests running now
			if( this._ajax && this._ajax.abort ){
				this._ajax.abort();
			}

			this.scope.find('[data-role="myMediaSearch"]').addClass('ipsField_loading');

			this._ajax = ips.getAjax()( url, {
				data: {
					search: this._value
				}
			})
				.done( function (response) {
					self.scope.find('[data-role="myMediaResults"]').html( response );
					$( document ).trigger( 'contentChange', [ self.scope.find('[data-role="myMediaResults"]') ] );
				})
				.always( function () {
					self.scope.find('[data-role="myMediaSearch"]').removeClass('ipsField_loading');
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.preview.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.preview.js - Editor preview panel
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.preview', {

		_origin: '',

		initialize: function () {
			this.on( window, 'message', this.processMessage );
			this.on( 'click', 'a', this.handleLinks );
			this.setup();
		},

		setup: function () {
			// Build a loading div that we can show
			this.scope.find('[data-role="previewContainer"]').html( ips.templates.get('core.editor.previewLoading') );

			this._origin = ips.utils.url.getOrigin();
			this._editorID = this.scope.attr('data-editorID');
			this._sendMessage({
				message: "iframeReady",
			});

			this._startTimer();
		},

		/**
		 * Don't allow links to go anywhere in the preview
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns void
		 */
		handleLinks: function (e) {
			// Allow the [page] links to work
			if( $( e.target ).is('[data-page]') ){
				return;
			}

			// Allow # links to work and assume a controller is going to do something with them
			if( $( e.target ).attr('href') && $( e.target ).attr('href') == '#' ){
				return;
			}

			// Otherwise, stop the link
			e.preventDefault();
		},

		/**
		 * Handles a message posted to us by the parent controller
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object}	data 	Data object
		 * @returns void
		 */
		processMessage: function (e, data) {			
			var oE = e.originalEvent;
			var json = $.parseJSON( oE.data );

			// Security: ignore requests not from the same origin so 3rd party frames can't tamper
			if( oE.origin !== this._origin ){
				return;
			}

			if( _.isUndefined( json.message ) ){
				return;
			}

			switch( json.message ){
				case 'fetchPreview':
					this._fetchPreview( json );
				break;
				case 'previewClosed':
					this._closedPreview();
				break;
			}
		},

		/**
		 * Sends a message to the parent controller
		 *
		 * @param 	{object}	data 	Data to send
		 * @returns void
		 */
		_sendMessage: function (data) {
			window.parent.postMessage( JSON.stringify( _.extend( data, { editorID: this._editorID } ) ), this._origin );
		},

		/**
		 * Pauses the interval timer
		 *
		 * @returns void
		 */
		_closedPreview: function () {
			if( this._timer ){
				clearInterval( this._timer );	
			}			
		},

		/**
		 * Starts the interval timer
		 *
		 * @returns void
		 */
		_startTimer: function () {
			this._timer = setInterval( _.bind( this._sendHeight, this ), 150 );
		},

		/**
		 * Fires an ajax request to get the preview contents
		 *
		 * @param 	{object}	data 	Editor data object
		 * @returns void
		 */
		_fetchPreview: function (data) {
			// Empty the content we already have
			this.cleanContents();
			this.scope.find('[data-role="previewContainer"]').html('');
			this._startTimer();

			var self = this;
			var ajaxData = {
				type: 'POST',
				data: {
					_previewField: this._editorID
				}
			};

			ajaxData.data[ this._editorID ] = data.editorContent;

			ips.getAjax()( data.url, ajaxData )
				.done( function (response) {
					var preview = self.scope.find('[data-role="previewContainer"]');
					preview.html( self._processResponse( response ) );
					ips.utils.lazyLoad.loadContent( preview );
				});
		},

		/**
		 * Processes the returned HTML before it's inserted into the dom
		 *
		 * @returns void
		 */
		_processResponse: function (response) {
			response = response.replace( /data\-ipshover/, '' );

			return response;
		},

		/**
		 * Sends the current height of the editor to the parent frame
		 *
		 * @returns void
		 */
		_sendHeight: function () {
			this._sendMessage({
				message: 'previewHeight',
				height: $( document ).height()
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.stockReplies.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.stockReplies.js - Controller for editor templates/saved replies
 *
 * Author: Matt Mecham
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.stockReplies', {

		initialize: function () {
			this.on( 'click', '.ipsStockReplies_row', this.insertTemplate );
			this.on( document, 'menuOpened', this.menuOpened );
			this.setup();
		},
		
		setup: function () {
			this._editorId = $( this.scope ).data('editorid');
			this._editor = CKEDITOR.instances[ this._editorId ];
		},
		
		insertTemplate: function(e)
		{
			e.stopPropagation();
			e.preventDefault();
			
			var template = $( e.target );
			var _self = this;
			ips.getAjax()( this._editor.config.controller + '&do=storedReplies&id=' + template.attr('data-templatesId'), {
				type: 'POST'
			} ).done( function (response) {
				if ( ! response.error )
				{
				   _self._editor.insertElement( CKEDITOR.dom.element.createFromHtml( '<div>' + response.reply + '</div>') );
				}
			} );
			
			this.scope.trigger( 'closeMenu' );
		},

		/**
		 * Event handler called when the emoticons menu is opened.
		 * Initializes the menu if it hasn't already been done
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		menuOpened: function (e, data) {
			if( data.menu.attr('data-controller') == 'core.global.editor.stockReplies' ){
				setTimeout(function(){
					var resultsbox = this.scope.find('[data-role="stockRepliesLoading"]');
					if ( resultsbox.hasClass('ipsLoading') ) {
						var _self = this;
						ips.getAjax()( this._editor.config.controller + '&do=storedReplies', {
							type: 'POST'
						} ).done( function (response) {
							var data   = response;
							var result = '';
							if ( data.error )
							{
							   result = data.error;
							}
							else
							{
								_.each( data, function (template) {
									result += ips.templates.render('core.editor.editorStockRepliesRow', {
										title: template.title,
										id: template.id
									} );
								} );
								
							}
											
							resultsbox.removeClass('ipsLoading').html( ips.templates.render('core.editor.editorStockRepliesWrap', { content: result } ) );
						} );
					}
				}.bind(this),100);
			}
		},
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.uploader.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.editor.uploader.js - Editor uploader controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.editor.uploader', {

		initialize: function () {
			this.on( 'addUploaderFile', this.addUploaderFile );
			this.on( 'removeAllFiles', this.removeAllFiles );
			this.on( 'fileDeleted', this.fileDeleted );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns	{void}
		 */
		setup: function () {
			this.scope.find('[data-role="fileContainer"]').each( function(){
				if( $( this ).children().length > 0 ){
					$( this ).parent().removeClass('ipsHide');
				}
			});
		},

		/**
		 * Intercepts the addUploaderFile event from ips.ui.uploader, so that we can show
		 * the attachment differently depending on whether it's a file or image.
		 *
		 * @param	{event}		e		Event Object
		 * @param	{event}		data	Event data object
		 * @returns	{void}
		 */
		removeAllFiles: function (e, data) {
			this.scope.find('[data-role="files"], [data-role="images"], [data-role="videos"], [data-role="audio"]').hide();
			this.scope.find('[data-role="fileList"]').hide();
		},

		/**
		 * Intercepts the addUploaderFile event from ips.ui.uploader, so that we can show
		 * the attachment differently depending on whether it's a file or image.
		 *
		 * @param	{event}		e		Event Object
		 * @param	{event}		data	Event data object
		 * @returns	{void}
		 */
		addUploaderFile: function (e, data) {
			e.stopPropagation();

			var container = null;
			var template = 'core.attachments.';

			this.scope.find('[data-role="fileList"]').show();
			
			// Show the appropriate container for this kind of file
			if( data.isImage ){
				container = this.scope.find('[data-role="images"]');
				template += 'imageItem';
			} else if( data.isVideo ){
				container = this.scope.find('[data-role="videos"]');
				template += 'videoItem';
			} else if ( data.isAudio ) {
				container = this.scope.find('[data-role="audio"]');
				template += 'audioItem';
		    } else {
				container = this.scope.find('[data-role="files"]');
				template += 'fileItem';
			}

			data.extIcon = ips.ui.uploader.getExtensionIcon( data.title );
			
			container
				.show()
				.find('[data-role="fileContainer"]')
					.append( ips.templates.render( template, data ) );
		},

		/**
		 * Event handler for the fileDeleted event from ips.ui.uploader. Hides our
		 * attachment container if no more files exist.
		 *
		 * @param	{event}		e		Event Object
		 * @param	{event}		data	Event data object
		 * @returns	{void}
		 */
		fileDeleted: function (e, data) {
			var count = 0;

			// See if we need to hide either of the containers
			this.scope.find('[data-role="fileContainer"]').each( function () {
				if( !$( this ).find('.ipsAttach').length ){
					$( this ).closest('[data-role="files"], [data-role="images"], [data-role="videos"], [data-role="audio"]').hide();
					count++;
				}
			});
			
			if( count == 4 ){
				this.scope.find('[data-role="fileList"]').hide();
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/files" javascript_name="ips.files.form.js" javascript_type="controller" javascript_version="107643" javascript_position="1000200">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.files.form.js - ACP Files Form Stuffs
 *
 * Author: MTM
 */

;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.files.form', {

		initialize: function () {
			if ( this.scope.find('input[name=filestorage_move]') )
			{
				$('#form_filestorage_move').hide();
				this.on( 'submit', 'form.ipsForm_horizontal', this.submitForm );
			}
		},

		
		/**
		 * Check if move is needed
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		submitForm: function (e) {
			var self = this;
			
			if( $( e.currentTarget ).attr('data-bypassValidation') ){
				return true;
			}
			
			e.preventDefault();
			
			$( e.currentTarget ).attr('data-bypassValidation', true);
			
			// Start the request
			ips.getAjax()( $( e.currentTarget ).attr('action').replace('do=configurationForm', 'do=checkMoveNeeded'), {
				data: $( e.currentTarget ).serialize(),
				type: 'post'
			})
			.done( function (response) {
				if ( response.needsMoving )
				{
					ips.ui.alert.show({
						type: 'confirm',
						message: ips.getString('files_overview_move_desc'),
						icon: 'fa fa-warning',
						buttons: {
							ok: ips.getString('files_overview_move'),
							cancel: ips.getString('files_overview_leave')
						},
						callbacks: {
							ok: function () {
								$('input[name=filestorage_move_checkbox]').prop('checked', true);
								$( e.currentTarget ).submit();
							},
							cancel: function () {
								$('input[name=filestorage_move_checkbox]').prop('checked', false);
								$( e.currentTarget ).submit();
							}
						}
					});
				}
				else {
					$( e.currentTarget ).submit();
				}
			});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/files" javascript_name="ips.files.multimod.js" javascript_type="controller" javascript_version="107643" javascript_position="1000200"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.support.multimod.js - Controller for moderation actions for the attachments list
 *
 * Author: Daniel Fatkic
 */
;( function($, _, undefined){
    "use strict";

    ips.controller.register('ips.admin.files.multimod', {

        initialize: function () {
            this.on( 'submit', '[data-role="moderationTools"]', this.moderationSubmit );
            this.on( 'menuItemSelected', this.itemSelected );
        },

        /**
         * Event handler called when the moderation bar submits
         *
         * @param	{event} 	e 		Event object
         * @returns {void}
         */
        moderationSubmit: function (e) {
            var action = this.scope.find('[data-role="moderationAction"]').val();

            switch (action) {
                case 'delete':
                    this._modActionDelete(e);
                    break;
                default:
                    $( document ).trigger('moderationSubmitted');
                    break;
            }
        },

        /**
         * Handles a delete action from the moderation bar
         *
         * @param	{event} 	e 		Event object
         * @returns {void}
         */
        _modActionDelete: function (e) {
            var self = this;
            var form = this.scope.find('[data-role="moderationTools"]');

            if( self._bypassDeleteCheck ){
                return;
            }

            e.preventDefault();

            // How many are we deleting?
            var count = parseInt( this.scope.find('[data-role="moderation"]:checked').length );

            ips.ui.alert.show( {
                type: 'confirm',
                icon: 'warn',
                message: ( count > 1 ) ? ips.pluralize( ips.getString( 'delete_confirm_many' ), count ) : ips.getString('delete_confirm'),
                callbacks: {
                    ok: function () {
                        $( document ).trigger('moderationSubmitted');
                        self._bypassDeleteCheck = true;
                        self.scope.find('[data-role="moderationTools"]').submit();
                    }
                }
            });
        }
    });
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/ignore" javascript_name="ips.ignore.existing.js" javascript_type="controller" javascript_version="107643" javascript_position="1000150"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ignore.existing.js - Controller for an ignored user on ignore preferences page
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.ignore.existing', {

		initialize: function () {
			this.on( 'menuItemSelected', '[data-action="ignoreMenu"]', this.ignoreMenu );
			this.on( 'submitDialog', this.editedUser );
		},

		/**
 		 * Event handler for edit dialog saving
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Data object from the event. Contains token information.
		 * @returns 	{void}
		 */
		editedUser: function (e, data) {
			this.trigger('refreshResults');

			ips.ui.flashMsg.show( ips.getString('editedIgnore') );
		},

		/**
 		 * Event handler for the ignore menu
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Data object from the event. Contains token information.
		 * @returns 	{void}
		 */
		ignoreMenu: function (e, data) {
			data.originalEvent.preventDefault();

			switch (data.selectedItemID) {
				case 'remove':
					this._removeIgnore(e, data);
				break;
			}
		},

		/**
 		 * Removes the ignore from this user
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Data object from the event. Contains token information.
		 * @returns 	{void}
		 */
		_removeIgnore: function (e, data) {
			var url =  data.menuElem.find('[data-ipsMenuValue="remove"] a').attr('href');
			var self = this;

			Debug.log('here');

			// Confirm it
			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'question',
				message: ips.getString('confirm_unignore'),
				subText: ips.getString('confirm_unignore_desc'),
				callbacks: {
					ok: function () {
						ips.getAjax()( url + '&wasConfirmed=1' )
							.done( function (response) {
								self.trigger('refreshResults');
							});
					}
				}
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/ignore" javascript_name="ips.ignore.new.js" javascript_type="controller" javascript_version="107643" javascript_position="1000150">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ignore.new.js - Manage Ignored Users controller
 *
 * Author: Rikki Tissier / Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.ignore.new', {
	
		/**
 		 * Initialize controller events
		 * Sets up the events from the view that this controller will handle
		 *
		 * @returns 	{void}
		 */
		initialize: function () {
			this.on( 'submit', '#elIgnoreForm', this.addIgnoredUser );
			this.on( 'tokenAdded', this.showExtraControls );
			this.setup();
		},

		/**
 		 * Non-event-based setup
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			if ( parseInt( this.scope.attr('data-id') ) === 0 ) {
				$('#elIgnoreTypes, #elIgnoreSubmitRow').hide();
			}	
		},
				
		/**
 		 * Submit handler for add user form. Gathers the types of content to be ignored, and emits an
 		 * event allowing the model to handle it
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		addIgnoredUser: function (e) {
			var form = this.scope.find('#elIgnoreForm');
			
			if( form.attr('data-bypassValidation') ){
				return;
			}
			
			e.preventDefault();
				
			var self = this;
			var scope = this.scope;

			ips.getAjax()( form.attr('action'), {
				data: form.serialize(),
				type: 'post'
			}).done( function (response, textStatus, jqXHR) {
				if ( jqXHR.responseJSON ) {
					ips.utils.anim.go( 'fadeOut', $('#elIgnoreTypes, #elIgnoreSubmitRow') ).then( function () {
						var field = scope.find('[name=&quot;member&quot;]');
						ips.ui.autocomplete.getObj( field ).removeAll();
						form.find( &quot;[type='checkbox']&quot; ).attr( 'checked', '' ).change();
					});

					// Show confirmation
					ips.ui.flashMsg.show( 
						ips.getString('addedIgnore', {
							user: response.name
						})
					);

					// Find the table, and refresh
					self.triggerOn( 'core.global.core.table', 'refreshResults' );	
					
				} else {
					form.attr('data-bypassValidation', true).submit();
				}
			}).fail( function (jqXHR, textStatus, errorThrown) {
				form.attr('data-bypassValidation', true).submit();
			});
		},
		
		/**
 		 * Triggered when the autocomplete field has added a token. Shows the extra options
 		 * on the form.
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Data object from the event. Contains token information.
		 * @returns 	{void}
		 */
		showExtraControls: function (e, data) {
			
			var field = this.scope.find('[name=&quot;member&quot;]');
			var wrapper = $( '#' + field.attr('id') + '_wrapper' );
			wrapper.addClass('ipsField_loading');
						
			var form = this.scope.find('#elIgnoreForm');
			
			ips.getAjax()( form.attr('action'), {
				type: 'post',
				data: {
					do: 'add',
					name: this.scope.find('[name=&quot;member&quot;]').val()
				}
			} )
				.done( function( response ) {
					var i;
					for ( i in response ) {
						form.find( &quot;[name='ignore_&quot; + i + &quot;_checkbox']&quot; ).attr( 'checked', response[i] == 1 ).change();
					}
					
					ips.utils.anim.go( 'fadeIn', $('#elIgnoreTypes, #elIgnoreSubmitRow') );
				})
				.fail( function( jqXHR, textStatus, errorThrown ) {
					ips.ui.alert.show({
						message: jqXHR.responseJSON['error']
					});

					field.data('_autocomplete').removeAll();
				}).always(function(){
					wrapper.removeClass('ipsField_loading');
				});
		}		

	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/marketplace" javascript_name="ips.marketplace.launcher.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.marketplace.authentication.js - Handles signing in to the marketplace
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.marketplace.launcher', {

		_termsAndConditions: false,
		_termsFromPurchase: false,
		_install: false,
		_dialog: null,

		initialize: function () {
			this._termsAndConditions = $('#downloadTerms').length ? true : false;

			this.on('click', '[data-action="signIn"]', this.signIn );
			this.on('click', '[data-role="update"], [data-role="install"], [data-action="launchWindow"]', this.termsAndConditions );
			this.on('click', '[data-role="renew"]', this.renewFromInvoice );
			this.on( 'menuItemSelected', '#elFileRenew', this.renewFromInvoice );

			$(document).on('click', '[data-action="acceptDisclaimer"]', _.bind( this.acceptTermsAndConditions, this ) );
			$(document).on( 'searchResultSelected', function(e,data) {
				window.location = data.url;
			});

			this.on( 'click', '[data-action="showError"]', this.showError );
			window.addEventListener( 'message', this.receivedMessage );
		},

		signIn: function(e) {
			window.open( this.scope.data('url'), '_blank', 'height=' + this.scope.attr('data-height') + ',width=575,menubar=0,status=0,titlebar=0' );
		},

		termsAndConditions: function(e) {
			e.preventDefault();
			var disclaimer = this.scope.data('disclaimer-location');
			this._termsFromPurchase = $( e.target ).data('role') == 'purchase' ? true : false;
			this._install = $( e.target ).data('role') == 'install' ? true : false;

			if ( this._termsAndConditions && ( disclaimer == 'both' || this._termsFromPurchase == false && disclaimer == 'download' || this._termsFromPurchase == true && disclaimer == 'purchase' ) ) {
				this._dialog = ips.ui.dialog.create({
					content: '#downloadTerms',
					title: $('#downloadTerms').data('title')
				});

				this._dialog.show();
			}
			else {
				// Continue, we'll just call the next step for them.
				this.acceptTermsAndConditions( new Event('tAndCs'), this );
			}
		},

		acceptTermsAndConditions: function(e) {
			e.preventDefault();

			if( this._termsFromPurchase ) {
				window.open( $('[data-purchase-url]').attr('data-purchase-url') + "&confirm=1", '_blank', 'height=' + $('[data-purchase-url]').attr('data-height') + ',width=575,menubar=0,status=0,titlebar=0');
			}
			else {
				if( !_.isNull( this._dialog ) ) {
					this._dialog.hide();
					this._dialog.destruct();
				}

				var dialogRef = ips.ui.dialog.create({
					url: $('[data-role="install"],[data-role="update"]').attr('href') + "&confirm=1",
					remoteVerify: true,
					extraClass: 'elMarketplaceInstallerDialog',
					size: 'narrow',
					close: false,
					title: ips.getString( this._install ? 'marketplace_installing' : 'marketplace_updating' )
				});

				dialogRef.show();
				return;
			}
		},

		showError: function (e) {
			ips.ui.alert.show( {
				type: 'alert',
				icon: 'warn',
				message: this.scope.attr('data-error'),
			});
		},
		
		receivedMessage: function (e) {
			if ( e.data === 'OK' ) {
				$('button[data-action="signIn"]').attr('disabled', true ); // Disable so it doesn't get clicked again
				window.location = window.location;
			}
			else if ( e.data.error ) {
				switch ( e.data.error ) {
					case 'access_denied':
						// User cancelled the authentication flow, we don't need to do anything.
						break;
					case '_not_account_holder':
						ips.ui.alert.show( {
							type: 'alert',
							icon: 'warn',
							message: ips.getString('marketplace_authentication_bad_account'),
						});
						break;
					default:
						ips.ui.alert.show( {
							type: 'alert',
							icon: 'warn',
							message: ips.getString('marketplace_communication_error_js'),
							subText: e.data.error_description ? ( e.data.error_description + " (" + e.data.error + ")" ) : e.data.error
						});
				}
			}
		},

		renewFromInvoice: function (e, data) {
			if( !_.isUndefined( data ) ){
				data.originalEvent.preventDefault();
				var target = $( data.originalEvent.target ).attr('href');
			} else {
				e.preventDefault();
				var target = $( e.target ).attr('href');
			}

			window.open( target, '_blank', 'height=900,width=575,menubar=0,status=0,titlebar=0');
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/marketplace" javascript_name="ips.marketplace.onboard.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.marketplace.onboard.js - Handles looking up an existing app/plugin/theme
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.marketplace.onboard', {
		
		initialize: function () {
			this.on( 'onboardMatch', this.check );
			this.check();
		},
		
		check: function () {
			var haveAllMatches = true;
			this.scope.find('[data-role=&quot;confirm&quot;]').each(function(){
				if ( $(this).val() == 0 ) {
					haveAllMatches = false;
				}
			});
			
			if ( haveAllMatches ) {
				this.scope.find('[data-role=&quot;continueButton&quot;]').removeClass('ipsButton_disabled').prop('disabled', false);
			} else {
				this.scope.find('[data-role=&quot;continueButton&quot;]').addClass('ipsButton_disabled').prop('disabled', true);
			}
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/marketplace" javascript_name="ips.marketplace.onboardRow.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.marketplace.onboard.js - Handles looking up an existing app/plugin/theme
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.marketplace.onboardRow', {
		
		_searchDialogs: [],

		initialize: function () {
			this.on( 'click', '[data-action="searchMarketplace"]', this._searchMarketplace );
			this.on( 'click', '[data-action="confirmMatch"]', this._confirmMatch );
			this.on( 'menuItemSelected', this._menuItemSelected );
			this.on( document, 'searchResultSelected', this._searchResultSelected );
			this.setup();
		},
		
		setup: function (e) {
			if ( this.scope.attr('data-marketplaceId') ) {
				ips.getAjax()( '?app=core&module=marketplace&controller=marketplace&do=apiLookup&id=' + this.scope.attr('data-marketplaceId') )
					.done(function(response){
						this.scope.find('[data-role="onboardLoading"]').hide();
						if( !_.isUndefined( response.html ) ) {
							this.scope.find('[data-role="matchedFile"]').html( response.html ).show();
						} else {
							this.scope.find('[data-role="matchedFile"]').hide();
							this.scope.find('[data-role="noMatchedFile"]').show();
							this.scope.find('[data-role="confirmed"]').hide();
							this.scope.find('[data-role="confirm_nomatch"]').show();
						}
					}.bind(this))
					.fail(function(){
						this.scope.find('[data-role="onboardLoading"]').hide();
						this.scope.find('[data-role="noMatchedFile"]').show();
						this.scope.find('[data-role="confirmed"]').hide();
						this.scope.find('[data-role="confirm_nomatch"]').show();
					}.bind(this));
			} else {
				ips.getAjax()( '?app=core&module=marketplace&controller=marketplace&do=apiSearch&title=' + encodeURIComponent( '"' + this.scope.attr('data-title') + '"' ) + "&category=" + this.scope.attr('data-category') + "&single=1" )
					.done(function(response){
						this.scope.find('[data-role="onboardLoading"]').hide();
						if ( response ) {
							$(this.scope).find('[data-role="id"]').val( response.id );
							this.scope.find('[data-role="matchedFile"]').html( response.html ).show();
							this.scope.find('[data-role="confirm_match"]').show();
						} else {
							this.scope.find('[data-role="noMatchedFile"]').show();
							this.scope.find('[data-role="confirm_nomatch"]').show();
						}
					}.bind(this))
					.fail(function(response){
						this.scope.find('[data-role="onboardLoading"]').hide();
						this.scope.find('[data-role="noMatchedFile"]').show();
						this.scope.find('[data-role="confirm_nomatch"]').show();
					}.bind(this));
			}
		},
		
		_searchMarketplace: function(e) {
			if ( e ) {
				e.preventDefault();
				e.stopPropagation();
			}
			
			this._searchDialogs[ this.scope.attr('data-id') ] = ips.ui.dialog.create( { title: ips.getString('marketplace_search'), url: '?app=core&module=marketplace&controller=marketplace&do=search&category=' + this.scope.attr('data-category') } );
			this._searchDialogs[ this.scope.attr('data-id') ].show();
		},
		
		_searchResultSelected: function(e,data){
			if ( this._searchDialogs[ this.scope.attr('data-id') ] && this._searchDialogs[ this.scope.attr('data-id') ].dialogID == data.dialogId ) {
				this._searchDialogs[ this.scope.attr('data-id') ].destruct();

				this.scope.find('[data-role="onboardLoading"]').show();
				this.scope.find('[data-role="confirm_match"]').hide();
				this.scope.find('[data-role="confirm_nomatch"]').hide();
				this.scope.find('[data-role="noMatchedFile"]').hide();
				this.scope.find('[data-role="matchedFile"]').hide();
				
				ips.getAjax()( '?app=core&module=marketplace&controller=marketplace&do=apiLookup&id=' + data.id )
					.done(function(response){
						this.scope.find('[data-role="onboardLoading"]').hide();
						if( !_.isUndefined( response.html ) ) {
								$(this.scope).find('[data-role="id"]').val( data.id );
								$(this.scope).find('[data-role="confirm"]').val('1');

							this.scope.find('[data-role="matchedFile"]').html( response.html ).show();
							this.scope.find('[data-role="confirm_match"]').hide();
							this.scope.find('[data-role="confirmed"]').show();
							
							this.trigger('onboardMatch');
						} else {
							this.scope.find('[data-role="noMatchedFile"]').show();
							this.scope.find('[data-role="confirm_nomatch"]').show();
						}
					}.bind(this))
					.fail(function(){
						this.scope.find('[data-role="onboardLoading"]').hide();
						this.scope.find('[data-role="noMatchedFile"]').show();
						this.scope.find('[data-role="confirm_nomatch"]').show();
					}.bind(this));
			}
		},
		
		_confirmMatch: function(e) {
			e.preventDefault();
			e.stopPropagation();
			$(this.scope).find('[data-role="confirm"]').val('1');
			this.scope.find('[data-role="confirm_match"]').hide();
			this.scope.find('[data-role="noMatchedFile"]').hide();
			this.scope.find('[data-role="confirmed"]').show();
			
			this.trigger('onboardMatch');
		},
		
		_menuItemSelected: function(e,data) {
			data.originalEvent.preventDefault();
			if ( data.selectedItemID == 'confirmCustom' ) {
				this._confirmCustom();
			} else if ( data.selectedItemID == 'searchMarketplace' ) {
				this._searchMarketplace();
			}
		},
		
		_confirmCustom: function() {
			$(this.scope).find('[data-role="id"]').val('0');
			$(this.scope).find('[data-role="confirm"]').val('1');
			this.scope.find('[data-role="confirm_match"]').hide();
			this.scope.find('[data-role="confirm_nomatch"]').hide();
			this.scope.find('[data-role="matchedFile"]').hide();
			this.scope.find('[data-role="noMatchedFile"]').show();
			this.scope.find('[data-role="confirmed"]').show();
			
			this.trigger('onboardMatch');
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/marketplace" javascript_name="ips.marketplace.search.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.marketplace.search.js - Handles searching marktplace
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.marketplace.search', {
		
		_timer: null,
		_textField: null,
		_lastValue: '',
		_ajax: null,

		initialize: function () {
			this._textField = $(this.scope).find('[data-role="searchBox"]');
			
			this.on( document, 'focus', '[data-role="searchBox"]', this.fieldFocus );
			this.on( document, 'blur', '[data-role="searchBox"]', this.fieldBlur );
			this.on( 'click', '[data-file]', this.selectFile )
			
			this.setup();
		},
		
		setup: function (e) {
			this.scope.find('[data-role="searchBox"]').focus();
		},
		
		/**
		 * Select a file
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		selectFile: function (e) {
			e.preventDefault()
			e.stopPropagation();

			this.trigger( 'searchResultSelected', { id: $(e.currentTarget).attr('data-file'), url: $(e.currentTarget).attr('href'), dialogId: $(e.currentTarget).closest('.ipsDialog').attr('id') } );
		},
		
		/**
		 * Event handler for focusing in the search box
		 * Set a timer going that will watch for value changes. If there's already a value,
		 * we'll show the results immediately
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		fieldFocus: function (e) {
			this._timer = setInterval( _.bind( this._timerFocus, this ), 700 );
		},

		/**
		 * Event handler for field blur
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		fieldBlur: function (e) {
			clearInterval( this._timer );
		},
		
		/**
		 * Timer callback from this.fieldFocus
		 * Compares current value to previous value, and shows/loads new results if it's changed
		 *
		 * @returns {void}
		 */
		_timerFocus: function () {
			var currentValue = this._textField.val();
			
			if( currentValue == this._lastValue ){
				return;
			}

			this._lastValue = currentValue;
			
			this._loadResults();
		},
		
		/**
		 * Load results from the server
		 *
		 * @returns {void}
		 */
		_loadResults: function () {			
			if( this._ajax ){
				this._ajax.abort();
			}
			
			if ( this._lastValue ) {
				this._textField.addClass('ipsField_loading');
				this.scope.find('[data-role="results"]').html('');
										
				var self = this;
				this._ajax = ips.getAjax()( '?app=core&module=marketplace&controller=marketplace&do=apiSearch&title=' + encodeURIComponent( this.scope.attr('data-search-literal') ? ( '"' + this._lastValue + '"' ) : this._lastValue ) + '&category=' + this.scope.attr('data-category') + '&compatible=' + this.scope.attr('data-compatible')  ).done(function(response){
	 				self.scope.find('[data-role="results"]').html( response.html );
	 				self.scope.find('[data-role="hideWhenSearching"]').hide();
					self._textField.removeClass('ipsField_loading');
				}).fail( function( failResponse ) {
					if( !_.isUndefined( failResponse.responseJSON ) ) {
						self.scope.find('[data-role="results"]').html( failResponse.responseJSON.error );
						self.scope.find('[data-role="hideWhenSearching"]').hide();
						self._textField.removeClass('ipsField_loading');
					}
				} );
			} else {
				this.scope.find('[data-role="results"]').html('');
				this.scope.find('[data-role="hideWhenSearching"]').show();
				this._textField.removeClass('ipsField_loading');
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/members" javascript_name="ips.members.achievementRuleForm.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.members.achievementRuleForm.js - Controller for restrictions screen
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.members.achievementRuleForm', {

		initialize: function () {
			this.on( 'change', 'select[name="achievement_rule_action"]', this.changeRule );
			this.on( 'click', '[data-action="filterReveal"]', this.showFilter );
			this.on( 'click', '[data-action="filterCollapse"]', this.hideFilter );
			this.on( 'change keyup', 'input[type="number"]', this.changeNumber );
			this.on( 'change', 'select[name$="[badge]"]', this.changeBadge );

			this.setup();
		},
		
		setup: function () {
			this._select = this.scope.find('select[name="achievement_rule_action"]');
			this.changeRule();
		},

		changeBadge: function (e) {
			this._changeBadge( $( e.currentTarget ).closest('select[name$="[badge]"]') );
		},

		_changeBadge: function( badgeSelectObj ) {
			const data = badgeSelectObj.attr('name').split('_');
			const translatable = $('#' + data[0] + '_' + data[1] + '_award_' + data[3].replace( '[badge]', '' ) + '_badge' );
			if ( badgeSelectObj.val() > 0 ) {
				translatable.show();
			} else {
				translatable.hide();
			}
		},

		changeRule: function (e) {
			if(e) {
				e.preventDefault();
			}

			const ruleToShow = this.scope.find(`#rule_${this._select.val()}`);
			this.scope.find('[data-role="ruleWrap"]').hide(); // Hide all rule wraps

			ruleToShow.attr('data-ipsForm', true); // Apply the form widget to each individual rule wrapper
			ruleToShow.show();

			this._changeBadge( $('select[name="' + this._select.val() + '_award_subject[badge]"]' ) );
			const otherBadge = $('select[name="' + this._select.val() + '_award_other[badge]"]' );
			if ( otherBadge.length ) {
				this._changeBadge( otherBadge );
			}
			$( document ).trigger('contentChange', [ ruleToShow ] ); // Trigger form init
		},

		showFilter: function (e) {
			const ruleWrap = $( e.currentTarget ).closest('[data-role="ruleWrap"]');
			const toggleWrap = $( e.currentTarget ).closest('[data-role="toggleFilter"]');
			const check = toggleWrap.find('input[type="checkbox"]');
			const toggleRow = toggleWrap.closest('[data-role="conditionButtons"]');

			check.prop('checked', true).trigger('change');
			toggleWrap.hide();
			toggleRow.toggle( toggleRow.find('[data-role="toggleFilter"] input[type="checkbox"]:visible').length > 0 );
			ruleWrap.find(`#${check.attr('data-filter')}`).show();

			if( ruleWrap.find('input[type="number"]') ){
				const self = this;
				ruleWrap.find('input[type="number"]').each( function () {
					self._pluralizeNumber( $( this ) );
				});
			}
		},

		hideFilter: function (e) {
			e.preventDefault();
			const filter = $( e.currentTarget ).closest('[data-role="filterField"]');
			const filterName = filter.attr('id');
			const ruleWrap = filter.closest('[data-role="ruleWrap"]');
			const check = ruleWrap.find(`#${filterName}Checkbox`);
			const toggleWrap = check.closest('[data-role="toggleFilter"]');
			
			check.prop('checked', false).trigger('change');
			toggleWrap.show().closest('[data-role="conditionButtons"]').show();
			filter.hide();
		},

		changeNumber: function (e) {
			this._pluralizeNumber( $( e.currentTarget ) );
		},

		_pluralizeNumber: function (input) {
			const span = input.next('[data-role="th"]');

			if( span.length ){
				span.text( ips.pluralize( ips.getString('numberSuffix'), input.val() ) );
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/members" javascript_name="ips.members.allowedCharacters.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.members.allowedCharacters.js - Controller for setting to control which characters are allowed in usernames
 *
 * Author: Mark Eade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.members.allowedCharacters', {

		initialize: function () {
			this.on( 'click', '[data-action=&quot;easy&quot;]', this.showEasy );
			this.on( 'click', '[data-action=&quot;regex&quot;]', this.showRegex );

			this.setup();
		},
		
		showEasy: function(e){
			if (e) {
				e.preventDefault();
			}
			this.scope.find('[data-role=&quot;easy&quot;]').show();
			this.scope.find('[data-role=&quot;regex&quot;]').hide();
			this.scope.find('[data-role=&quot;easyInput&quot;]').val('1');
		},
		
		showRegex: function(e){
			if (e) {
				e.preventDefault();
			}
			this.scope.find('[data-role=&quot;regex&quot;]').show();
			this.scope.find('[data-role=&quot;easy&quot;]').hide();
			this.scope.find('[data-role=&quot;easyInput&quot;]').val('0');
		},

		setup: function () {
			this.scope.find('[data-action=&quot;easy&quot;]').show();
			this.scope.find('[data-action=&quot;regex&quot;]').show();
			if ( this.scope.attr('data-easy') == 1 ) {
				this.showEasy();
			}
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/members" javascript_name="ips.members.form.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.members.form.js - ACP Files Form Stuffs
 *
 * Author: MTM
 */

;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.members.form', {

		initialize: function () {
			this.on( 'submit', this.submitForm );
		},
		
		/**
		 * Check if move is needed
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		submitForm: function (e) {
			var self = this;
			
			if( $( e.currentTarget ).attr('data-bypassValidation') ){
				return true;
			}
			
			e.preventDefault();
			e.stopPropagation();
		
			var isInAdminGroup = false;
			var mainGroup = this.scope.find('select[name=group]').val();
			var secondaryGroups = _.map( this.scope.find('select[name="secondary_groups[]"]').val(), function( val ){
				return parseInt( val );
			} );
			var adminGroups = $.parseJSON( this.scope.attr('data-adminGroups') );
			if( adminGroups.length ){
				for( var i = 0; i < adminGroups.length; i++ ) {
					var testId = adminGroups[i];
					if ( secondaryGroups != null && secondaryGroups.length && _.indexOf( secondaryGroups, testId ) != -1 ) {
						isInAdminGroup = true;
					}
					if ( testId == mainGroup ) {
						isInAdminGroup = true;
					}
					if ( $( "input#elCheckbox_secondary_groups_" + testId + ":checked" ).length ) {
						isInAdminGroup = true;
					}
				}
			}
			
			if ( isInAdminGroup ) {
				ips.ui.alert.show({
					type: 'confirm',
					message: ips.getString('member_edit_is_admin'),
					icon: 'fa fa-warning',
					buttons: {
						ok: ips.getString('member_edit_ok'),
						cancel: ips.getString('member_edit_cancel')
					},
					callbacks: {
						ok: function () {
							$( e.currentTarget ).attr('data-bypassValidation', true);
							$( e.currentTarget ).submit();
						},
						cancel: function () {
							return false;
						}
					}
				});
			} else {
				$( e.currentTarget ).attr('data-bypassValidation', true);
				$( e.currentTarget ).submit();
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/members" javascript_name="ips.members.history.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.members.history.js - Filters for member history log
 *
 * Author: Mark Wade
 */

;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.members.history', {

		initialize: function () {
			this.on( 'menuItemSelected', this.filterSelected );
		},
		
		/**
		 * Event handler for when a filter option is chosen
		 *
		 * @param	{event} 	e		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		filterSelected: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}
												
			if( data.triggerID == 'memberHistoryFilters' ){
				$(this.scope).find('[data-role=&quot;historyTitle&quot;]').text( data.menuElem.find('[data-ipsMenuValue=&quot;' + data.selectedItemID + '&quot;]').text() );
				$(this.scope).find('[data-role=&quot;historyDisplay&quot;]').addClass('ipsLoading ipsLoading_dark').html('');
								
				ips.getAjax()( data.menuElem.find('[data-ipsMenuValue=&quot;' + data.selectedItemID + '&quot;] a').attr('href') ).done(function(response){
					$(this.scope).find('[data-role=&quot;historyDisplay&quot;]').html( response ).removeClass('ipsLoading ipsLoading_dark');
					$( document ).trigger( 'contentChange', [ this.scope ] );
				}.bind(this));
			}
		}
		
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/members" javascript_name="ips.members.lazyLoadingProfileBlock.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.members.lazyLoadingProfileBlock.js
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.members.lazyLoadingProfileBlock', {
		
		/**
		 * Init
		 */
		initialize: function () {
			var scope = $(this.scope);
			ips.getAjax()( scope.attr('data-url') ).done(function(response){
				scope.html( response );
				$( document ).trigger( 'contentChange', [ scope ] );
			});
		},
				
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/members" javascript_name="ips.members.listFlagSpammer.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://invisioncommunity.com
 *
 * ips.members.listFlagSpammer.js - Member list spam flagging
 *
 * Author: Stuart Silvester
 */

;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.members.listFlagSpammer', {

		initialize: function () {
			this.on('click', this.toggleFlagSpammer  );
		},

		toggleFlagSpammer: function( e ) {
			e.preventDefault();

			var elem = $( e.currentTarget );
			var icon = elem.find( '.fa-flag' );

			ips.ui.alert.show({
				'type': 'confirm',
				'icon': 'warn',
				'message': icon.hasClass( 'ipsType_spammer' ) ? ips.getString( 'confirmUnFlagAsSpammer' ) : ips.getString('confirmFlagAsSpammer'),
				'subText': icon.hasClass( 'ipsType_spammer' ) ? ips.getString( 'confirmUnFlagAsSpammerDesc' ) : '',
				callbacks: {
					ok: function () {
						ips.getAjax()( elem.attr('href'), { dataType: 'json' } )
							.done( function (response) {
								ips.ui.flashMsg.show( response );
								icon.toggleClass( 'ipsType_spammer');

								// adjust url
								var newStatus = ips.utils.url.getParam( 'status', elem.attr('href') ) == 1 ? 0 : 1;
								var newUrl = ips.utils.url.removeParam( 'status', elem.attr('href') );
								elem.attr( 'href', newUrl + '&status=' + newStatus );

								// Labels
								elem.attr( '_title', newStatus ? ips.getString( 'flagAsSpammer' ) : ips.getString( 'unflagAsSpammer' ) );
								elem.attr( 'aria-label', newStatus ? ips.getString( 'flagAsSpammer' ) : ips.getString( 'unflagAsSpammer' ) );
							})
							.fail( function (jqXHR) {
								if( Debug.isEnabled() ){
									Debug.error( jqXHR.responseText );
								} else {
									window.location = elem.attr('href');
								}
							});
					}
				}
			});
		}

	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/members" javascript_name="ips.members.moderatorPermissions.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.members.moderatorPermissions.js - 
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.members.moderatorPermissions', {

		initialize: function () {
			this.on( 'change', '#ipsTabs_tabs_form_form_tab_modperms__core_Content_panel input[type="checkbox"]', this.toggle );
			this.on( 'click', '[data-role="checkAll"]', this.checkAll );
			this.on( 'click', '[data-role="uncheckAll"]', this.checkAll );
			this.setup();
		},

		/**
		 * Setup method; checks initial states of toggles
		 *
		 * @returns {void}
		 */
		setup: function () {
			var mainPanel = this.scope.find('#ipsTabs_tabs_form_form_tab_modperms__core_Content_panel');
			var self = this;

			mainPanel.find('input[type="checkbox"]').each( function () {
				self._toggleChanged( $( this ) );
			});
			
			$(this.scope).find('.ipsTabs_panel').each(function(){
				var controls = $( ips.templates.render( 'moderatorPermissions.checkUncheckAll' ) );
				controls.find('a').attr( 'data-scope', $(this).attr('id') );
				$(this).children('ul').prepend( controls );
			});

			this._checkEachTab();
		},
		
		/**
		 * Check/uncheck all
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		checkAll: function (e) {
			e.preventDefault();
			var check = $(e.currentTarget).attr('data-role') == 'checkAll';
			
			if ( $(e.currentTarget).attr('data-scope') ) {
				var scope = $( '#' + $(e.currentTarget).attr('data-scope') );
			} else {
				var scope = $(this.scope);
			}

			if( check && !$(e.currentTarget).attr('data-scope') )
			{
				$('input[name="mod_use_restrictions"][value="no"]').prop( 'checked', true ).change();
			}
			
			var self = this;
			scope.find('input[type="checkbox"]').each(function(){
				if ( check && !$(this).is(':checked') ) {
					$(this).prop( 'checked', true ).change();
				} else if ( !check && $(this).is(':checked') ) {
					$(this).prop( 'checked', false ).change();
				} 
			});
		},

		/**
		 * Toggle event handler
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		toggle: function (e) {
			this._toggleChanged( $( e.currentTarget ) );
			this._checkEachTab();
		},

		/**
		 * Called when a toggle changes in the main panel. If checked, other toggles of this type in other panels are hidden
		 *
		 * @param	{element} 	thisToggle 		The toggle that has changed
		 * @returns {void}
		 */
		_toggleChanged: function (thisToggle) {
			var id = thisToggle.closest('.ipsFieldRow').attr('id').replace('_content', '');
			var panels = this.scope.find('.ipsTabs_panel:not( #ipsTabs_tabs_form_form_tab_modperms__core_Content_panel )');
			var otherToggles =  panels.find('.ipsFieldRow[id^="' + id + '"]').not( thisToggle.closest('.ipsFieldRow') );

			if( thisToggle.is(':checked') ){
				// Find all other toggles of this type and hide them
				otherToggles.hide();
				otherToggles.find('input[type="checkbox"]').prop( 'disabled', true );
			} else {
				otherToggles.show();
				otherToggles.find('input[type="checkbox"]').prop( 'disabled', false );
			}
		},

		/**
		 * Checks each tab on the form to see whether any permissions are showing, hiding it if not
		 *
		 * @returns {void}
		 */
		_checkEachTab: function () {
			var self = this;
			var panels = this.scope.find('.ipsTabs_panel:not( #ipsTabs_tabs_form_form_tab_modperms__core_Content_panel )');

			// Now check each tab to make sure there's some to show
			panels.each( function () {
				var count = $( this ).find('input[type="checkbox"]:enabled:not( [data-role="zeroVal"] )').length;
				var id = $( this ).attr('id').replace('ipsTabs_tabs_form_', '').replace('_panel', '');

				if( !count ){
					self.scope.find('#' + id).closest('li').hide();
				} else {
					self.scope.find('#' + id).closest('li').show();
				}
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/members" javascript_name="ips.members.restrictions.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.members.restrictions.js - Controller for restrictions screen
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.members.restrictions', {

		initialize: function () {
			this.on( 'click', '.acpRestrictions_subHeader h3', this.toggleSubHeader );
			this.on( 'change', '.acpRestrictions_header input[type=&quot;checkbox&quot;]', this.toggleHeader );
			this.on( 'change', '.acpAppRestrictions_header input[type=&quot;checkbox&quot;]', this.toggleAppHeader );
			this.on( 'click', '[data-action=&quot;checkAll&quot;]', this.checkAll );
			this.on( 'click', '[data-action=&quot;checkNone&quot;]', this.checkNone );
			this.on( 'click', '[data-action=&quot;expandAll&quot;], [data-action=&quot;collapseAll&quot;]', this.toggleDisplay );

			this.setup();
		},

		/**
		 * Event handler for both the expand and collapse links
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		toggleDisplay: function (e) {
			e.preventDefault();

			var row = $( e.currentTarget ).closest('.acpRestrictions_header');
			var subHeaders = row.next().find('.acpRestrictions_subHeader');
			var self = this;
			var action = ( $( e.currentTarget ).attr('data-action') == 'expandAll' ) ? 'expand' : 'collapse';

			subHeaders.each( function () {
				if( action == 'expand') {
					self._expandSection( $( this ) );	
				} else {
					self._collapseSection( $( this ) );
				}				
			});
		},

		/**
		 * Disables all the toggles in an app when the app header is unchecked
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		toggleAppHeader: function (e) {
			var check = $( e.currentTarget );
			var row = check.closest('.acpAppRestrictions_header');

			if( !check.is(':checked') ){
				row
					.siblings('.acpAppRestrictions_panel')
					.find('.acpRestrictions_header input[type=&quot;checkbox&quot;]')
						.each( function () {
							// Loops through each checkbox, disables it, stores the original state as an attr,
							// unchecks it, and triggers a change event to update the JS toggle widget.
							$( this )
								.prop('disabled', true)
								.attr( 'data-originalState', $( this ).is(':checked') )
								.attr( 'checked', false )
								.trigger('change');
						});
			} else {
				// Top panel rows
				row
					.siblings('.acpAppRestrictions_panel')
					.find('input[type=&quot;checkbox&quot;]')
						.each( function () {
							var thisCheck = $( this );
							thisCheck.prop( 'disabled', false );

							if( thisCheck.attr('data-originalState') == 'true' ){
								thisCheck
									.prop( 'checked', true )
									.trigger('change');
							}
						});

				// Sub panel rows
				row
					.siblings('.acpAppRestrictions_panel')
					.find('.acpRestrictions_panel input[type=&quot;checkbox&quot;]')
						.each( function () {
							var thisCheck = $( this );
							var checked = thisCheck.closest('.acpRestrictions_panel').siblings('.acpRestrictions_header').find('input[type=&quot;checkbox&quot;]').is(':checked');

							if( !checked ){
								thisCheck.prop('disabled', true);
							} else {
								thisCheck.prop('disabled', false);
							}

							if( thisCheck.attr('data-originalState') == 'true' ){
								thisCheck
									.prop( 'checked', true )
									.trigger('change');
							}
						});
			}
		},

		/**
		 * Disables all the toggles in a section when the section header is unchecked
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		toggleHeader: function (e) {
			var check = $( e.currentTarget );
			var row = check.closest('.acpRestrictions_header');
			var unChecked = !check.is(':checked');

			row
				.next()
				.find('input[type=&quot;checkbox&quot;]')
					.each( function () {
						var thisCheck = $( this );

						if( unChecked ){
							thisCheck
								.attr( 'data-originalState', thisCheck.is(':checked') )
								.prop( 'checked', false );
						} else if ( ( thisCheck.attr('data-originalState') == 'true' ) ){
							thisCheck.prop( 'checked', true );
						}
						
						thisCheck
							.prop( 'disabled', unChecked )
							.trigger('change');
					});
		},

		/**
		 * Toggles the display of a section when a subheader is clicked
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		toggleSubHeader: function (e) {
			var header = $( e.currentTarget ).parent();

			if( header.hasClass('acpRestrictions_open') ){
				this._collapseSection( header );
			} else {
				this._expandSection( header );
			}
		},

		/**
		 * Displays a section with animation
		 *
		 * @param	{element} 	section 	The section to show
		 * @returns {void}
		 */
		_expandSection: function (section) {
			var next = section.next('ul');

			section
				.addClass('acpRestrictions_open')
				.removeClass('acpRestrictions_closed');

			ips.utils.anim.go( 'fadeInDown fast', next );
		},

		/**
		 * Hides a section
		 *
		 * @param	{element} 	section  	The section to hide
		 * @returns {void}
		 */
		_collapseSection: function (section) {
			section
				.removeClass('acpRestrictions_open')
				.addClass('acpRestrictions_closed');
		},

		/**
		 * Checks all toggles in the section, opening the section too if necessary
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		checkAll: function (e) {
			e.preventDefault();
			var self = this;
			var header = $( e.currentTarget ).parents('.acpRestrictions_subHeader');
			var next = header.next('ul');

			// If the section isn't visible, do the toggling after the section has
			// animated in, so that the user can see the change happen. Otherwise, just do it immediately
			if( !next.is(':visible') ){
				next.animationComplete( function () {
					self._togglePermissions( true, next );
				});

				this._expandSection( header );	
			} else {
				this._togglePermissions( true, next );
			}
			
		},

		/**
		 * Unchecks all toggles in the section, opening the section too if necessary
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		checkNone: function (e) {
			e.preventDefault();
			var self = this;
			var header = $( e.currentTarget ).parents('.acpRestrictions_subHeader');
			var next = header.next('ul');

			// If the section isn't visible, do the toggling after the section has
			// animated in, so that the user can see the change happen. Otherwise, just do it immediately
			if( !next.is(':visible') ){
				next.animationComplete( function () {
					self._togglePermissions( false, next );
				});

				this._expandSection( header );	
			} else {
				this._togglePermissions( false, next );
			}
			
		},

		/**
		 * Sets all checkboxes to the given state in the given container
		 *
		 * @param	{boolean} 	state 		The state to which checkboxes will be set
		 * @param 	{element} 	container 	The container in which the checkboxes must exist
		 * @returns {void}
		 */
		_togglePermissions: function (state, container) {
			container.find('input[type=&quot;checkbox&quot;]:not( [disabled] )').prop('checked', state).change();
		},

		/**
		 * Setup method
		 * Collapses all sections initially
		 *
		 * @returns {void}
		 */
		setup: function () {
			this.scope
				.find('.acpRestrictions_open')
					.removeClass('acpRestrictions_open')
					.addClass('acpRestrictions_closed');
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/messages" javascript_name="ips.messages.folderDialog.js" javascript_type="controller" javascript_version="107643" javascript_position="1000200">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.messages.folderDialog.js - Folder naming dialog controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.messages.folderDialog', {

		_events: {
			add: 'addFolder',
			rename: 'renameFolder'
		},

		initialize: function () {
			this.on( 'submit', 'form', this.submitName );
		},

		/**
		 * Responds to the model event indicating the folder has been marked as read
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		submitName: function (e) {
			e.preventDefault();
			e.stopPropagation();

			var type = this.scope.attr('data-type');
			var field = this.scope.find('[data-role=&quot;folderName&quot;]');
			var val = field.val();
			var folderID = field.attr('data-folderID');

			this.trigger( this._events[ type ] + '.messages', {
				folder: folderID,
				name: val
			});

			this.trigger('closeDialog');
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/messages" javascript_name="ips.messages.list.js" javascript_type="controller" javascript_version="107643" javascript_position="1000200"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.messages.list.js - Messages list in messenger
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.messages.list', {

		_messageList: null,
		_searchTimer: null,
		_currentFolder: null,
		_currentMessageID: null,
		_currentOptions: {
			sortBy: 'mt_last_post_time',
			filter: 'all'
		},
		_infScrollURL: null,

		initialize: function () {
			// Main controller events
			this.on( document, 'messengerReady.messages', this.messengerReady );

			// Menu events
			this.on( 'menuItemSelected', '#elSortByMenu', this.changeSort );
			this.on( 'menuItemSelected', '#elFilterMenu', this.changeFilter );
			this.on( 'menuItemSelected', '#elSearchTypes', this.selectedMenuItem );
			
			// Message list events
			this.on( 'click', '[data-messageid]', this.clickMessage );
			this.on( 'submit', '[data-role="moderationTools"]', this.moderationSubmit );

			// Search field
			this.on( 'input', '[data-role="messageSearchText"]', this.inputSearch );
			this.on( 'click', '[data-action="messageSearchCancel"]', this.cancelSearch );
			//this.on( 'focus', '[data-role="messageSearchText"]', this.focusSearch );
			//this.on( 'blur', '[data-role="messageSearchText"]', this.blurSearch );

			// Folder model events
			this.on( document, 'loadFolderDone.messages', this.loadFolderDone );
			this.on( document, 'loadFolderLoading.messages, searchFolderLoading.messages', this.loadFolderLoading );
			this.on( document, 'loadFolderFinished.messages', this.loadFolderFinished );
			this.on( document, 'searchFolderLoading.messages', this.searchFolderLoading );
			this.on( document, 'searchFolderDone.messages', this.searchFolderDone );
			this.on( document, 'searchFolderFinished.messages', this.searchFolderFinished );
			this.on( document, 'markFolderDone.messages', this.markFolderDone );
			this.on( document, 'deleteMessagesDone.messages', this.deleteMessagesDone );
			this.on( document, 'loadMessageDone.messages', this.markMessageRead );
			
			// Message model events
			this.on( document, 'deleteMessageDone.messages', this.deleteMessageDone );
			this.on( document, 'moveMessageDone.messages', this.moveMessageDone );
			this.on( document, 'addToCommentFeed', this.newMessage );
			this.on( document, 'deletedComment.comment', this.deletedMessage );

			// Message view events
			//this.on( document, 'updateReplyCount.messages', this.updateReplyCount );
			//this.on( 'deletedReply.messages', this.deletedReply );

			// Primary event that watches for URL changes
			History.Adapter.bind( window, 'statechange', _.bind( this.stateChange, this ) );

			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this._messageList = this.scope.find('[data-role="messageList"]');
			this._currentFolder = this.scope.attr('data-folderID');

			this.trigger('setInitialFolder.messages', {
				folderID: this._currentFolder
			});
		},

		/**
		 * Handles submitting the moderation form (which lets uses mass-delete messages)
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		moderationSubmit: function (e, data) {
			e.preventDefault();

			var self = this;
			var form = this.scope.find('[data-role="moderationTools"]');

			// How many are we deleting?
			var count = parseInt( this.scope.find('[data-role="moderation"]:checked').length );
		
			if ( this.scope.find('[data-role="pageActionOptions"]').find('select option:selected').val() == 'move' ) {
				var dialog = ips.ui.dialog.create( { remoteVerify: false, size: 'narrow', remoteSubmit: false, title: ips.getString('messagesMove'), url: form.attr('action') + '&do=moveForm&ids=' + _.map( self.scope.find('[data-role="moderation"]:checked'), function (item) {
					return $( item ).closest('[data-messageid]').attr('data-messageid');
				}).join(',') } );
				dialog.show();
			} else {
				ips.ui.alert.show( {
					type: 'confirm',
					icon: 'question',
					message: ( count > 1 ) ? ips.pluralize( ips.getString( 'messagesDeleteMany' ), count ) : ips.getString('messagesDelete'),
					subText: ( count > 1 ) ? ips.getString( 'messagesDeleteManySubText' ) : ips.getString('messagesDeleteSubText'),
					callbacks: {
						ok: function () {
							// Get IDs
							var ids = _.map( self.scope.find('[data-role="moderation"]:checked'), function (item) {
								return $( item ).closest('[data-messageid]').attr('data-messageid');
							});
	
							self.trigger('deleteMessages.messages', {
								id: ids
							});
						}
					}
				});
			}
		},

		/**
		 * Deleted multiple messages using the pageAction widget
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		deleteMessagesDone: function (e, data) {
			// Build a selector to find the messages
			var selector = _.map( data.id, function (item) {
				return '[data-messageid="' + item + '"]';
			}).join(',');

			// Get the messages
			var self = this;
			var messages = this._messageList.find( selector );

			if( messages.length ){
				messages.slideUp( {
					complete: function () {
						messages.remove();

						// Is our selected message one of those deleted?
						if( data.id.indexOf( self._currentMessageID ) !== -1 ){
							self._currentMessageID = null;	

							// Are there any other messages we can show?
							if( self._messageList.find('[data-messageid]').length ){
								self._messageList.find('[data-messageid]').first().click();
							} else {
								self.trigger( 'getFolder', {
									folderID: self._currentFolder 
								});
							}
						}

						// Refresh the page action so it hides
						self._resetListActions();
					},
					queue: false
				}).fadeOut({
					queue: false
				});	
			}
		},

		/**
		 * Event handler for the search box. Starts a timer so that a search happens 500ms after
		 * the user stops typing
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		inputSearch: function (e) {
			clearTimeout( this._searchTimer );
			this._searchTimer = setTimeout( _.bind( this._startSearch, this ), 500 );
		},

		/**
		 * Event handler for model search loading event
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		searchFolderLoading: function (e, data) {
			this.scope.find('[data-role="messageSearchText"]').addClass('ipsField_loading');
		},

		/**
		 * Event handler for model search done event
		 * Updates the message list, hides the filders and shows the cancel button
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		searchFolderDone: function (e, data) {
			//this.cleanContents();
			this._messageList
				.html( data.data )
				.show()
				.end()
				.find('[data-role="messageListPagination"]')
					.html( data.pagination )
				.end()
				.find('[data-role="loading"]')
					.hide()
				.end()
				.find('[data-role="messageListFilters"]')
					.hide();

			this.scope.find('[data-action="messageSearchCancel"]').show();

			// Update the infinite scroll URL
			if( this.scope.is('[data-ipsInfScroll]') ){
				var params = decodeURIComponent( $.param( ips.utils.form.serializeAsObject( $('[data-role="messageSearch"]') ) ) );
				var base = this.scope.find('#elMessageList > form').attr('action');

				this._infScrollURL = this.scope.attr('data-ipsInfScroll');
				this.scope.attr('data-ipsInfScroll-url', base + '&' + params + '&folder=' + this._currentFolder );
				this.scope.trigger('refresh.infScroll');
			}

			$( document ).trigger( 'contentChange', [ this._messageList ] );
			this._resetListActions();
		},

		/**
		 * Event handler for model search finished event
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		searchFolderFinished: function (e, data) {
			this.scope.find('[data-role="messageSearchText"]').removeClass('ipsField_loading');
		},

		/**
		 * Event handler for clicking the cancel search button
		 *
		 * @param 		{event} 	e 		Event object	
		 * @returns 	{void}
		 */
		cancelSearch: function (e) {

			if( !_.isUndefined( e ) )
			{
				e.preventDefault();
			}
			this._resetSearch();
			this._getFolder( this._currentFolder );
		},

		/**
		 * A reply in a message was deleted
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		deletedReply: function (e, data) {
			var count = this._messageList.find('[data-messageid="' + data.messageID + '"] .ipsCommentCount').text();
			this._messageList.find('[data-messageid="' + data.messageID + '"] .ipsCommentCount').text( parseInt( count ) - 1 );
		},

		/**
		 * Updates the reply count for a message
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		updateReplyCount: function (e, data) {
			this._messageList
				.find('[data-messageid="' + data.messageID + '"] .ipsCommentCount')
					.text( data.count );
		},

		/**
		 * Responds to the model event indicating the folder has been marked as read
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		markFolderDone: function (e, data) {
			if( data.folder == this._currentFolder ){
				this._messageList
					.find('[data-messageid]')
						.removeClass('ipsDataItem_unread')
						.find('.ipsItemStatus')
							.remove();
			}
		},

		/**
		 * Responds to the model event indicating an individual message has been deleted
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		deleteMessageDone: function (e, data) {
			// See if the deleted message exists in the list
			var message = this._messageList.find('[data-messageid="' + data.id + '"]');

			if( message.length ){
				ips.utils.anim.go( 'fadeOutDown', message ).done( function () {
					message.remove();
				});

				this._currentMessageID = null;
			}
		},

		/**
		 * Responds to model event indicating message has moved. If the message is in this list, we remove it.
		 * If the message is the selected message, we also select the next or previous message
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		moveMessageDone: function (e, data) {

			// If this message is in the list, remove it
			var message = this._messageList.find('[data-messageid="' + data.id + '"]');
			var next = null;

			if( this._currentMessageID == data.id ){
				// Get the prev or next message
				if( message.prev('[data-messageid]').length ){
					next = message.prev('[data-messageid]');
				} else if( message.next('[data-messageid]').length ){
					next = message.next('[data-messageid]');
				}
			}

			if( message.length && data.to != this._currentFolder ){
				ips.utils.anim.go( 'fadeOutDown', message ).done( function () {
					message.remove();
				});

				this._currentMessageID = null;
			}

			ips.ui.flashMsg.show( ips.getString('conversationMoved') );

			if( next ){
				next.click();
			}
		},

		/**
		 * Responds to the model event indicating a folder has been successfully loaded into the list
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		loadFolderDone: function (e, data) {
			//this.cleanContents();
			this.scope
				.attr( 'data-ipsInfScroll-url', data.listBaseUrl )
				.find('#elMessageList')
					.scrollTop(0);

			this._messageList
					.html( data.data )
					.show()
				.end()
				.find('[data-role="messageListPagination"]')
					.html( data.pagination )
				.end()
				.find('[data-role="loading"]')
					.hide();

			this.scope.trigger('refresh.infScroll');
			$( document ).trigger( 'contentChange', [ this._messageList ] );

			this._resetListActions();
		},

		/**
		 * Responds to the model indicating new results are loading
		 * Shows a loading thingy in place of the list
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		loadFolderLoading: function (e, data) {
			if( !this.scope.find('[data-role="loading"]').length ){
				this._messageList.after( 
					$('<div/>')
						.addClass('ipsLoading')
						.html('&nbsp;')
						.css( { minHeight: '150px' } )
						.attr('data-role', 'loading')
				);
			}

			this._messageList.hide();
			this._hideEmpty();
			this.scope.find('[data-role="loading"]').show();
		},

		/**
		 * Responds to the model indicating loading a folder has finished
		 * Shows the list again
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		loadFolderFinished: function (e, data) {
			this._messageList.show();
			this._resetSearch();
		},

		/**
		 * Responds when all messenger setup is complete.
		 * Triggers an event that lets the main and view controllers know which is the current message
		 *	
		 * @returns 	{void}
		 */
		messengerReady: function () {
			this._currentMessageID = this._messageList.find('.cMessage_active').attr('data-messageid');
			this.trigger( 'setInitialMessage.messages', {
				messageID: this._currentMessageID
			});
		},

		/**
		 * Event handler for clicking on a message in the list
		 * If it's a single message, we trigger an event to load it, and highlight it
		 * If a meta key is pressed, we select multiple messages, as well as triggering the event
		 *
		 * @param 		{event} 	e 		Event object	
		 * @returns 	{void}
		 */
		clickMessage: function (e) {
			if( $( e.target ).is('input[type="checkbox"]') ){
				return;
			}

			e.preventDefault();

			var messageID = $( e.currentTarget ).attr('data-messageid');
			var messageURL = $( e.currentTarget ).find('[data-role="messageURL"]').attr('href');
			var messageTitle = $( e.currentTarget ).find('[data-role="messageURL"]').text();

			// Selecting one message
			//if( ( !e.shiftKey && !e.metaKey ) || this._currentMessageID == null ){
				this.trigger( 'selectedMessage.messages', { 
					messageID: messageID,
					messageURL: messageURL,
					messageTitle: messageTitle
				});

				this.trigger('switchTo.filterBar', {
					switchTo: 'filterContent'
				});
				
				this._selectMessage( messageID );
				return;
			//}

			// Selecting multiple messages
			// Get all messages
			/*var messages = this._messageList.find('[data-messageid]');
			var currentMessage = this._messageList.find('[data-messageid="' + this._currentMessageID + '"]');
			var newMessage = $( e.currentTarget );
			var currentIndex = messages.index( currentMessage );
			var newIndex = messages.index( newMessage );

			if( currentIndex < newIndex ){
				var collection = currentMessage.nextUntil( newMessage );
			} else {
				var collection = currentMessage.prevUntil( newMessage );
			}

			collection = collection.add( currentMessage ).add( newMessage );

			var IDs = [];

			collection.each( function (idx, item) {
				IDs.push( $(item).attr('data-messageid') );
			});

			this.trigger( 'selectedMessage.messages', { 
				messageID: IDs
			});

			this.trigger('switchTo.filterBar', {
				switchTo: 'filterContent'
			});

			this._selectMessages( IDs );*/
		},
		
		/**
		 * Event handler for when a new message is sent
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		newMessage: function (e, data) {
			this._updateRow( data.feedID.substr( data.feedID.indexOf('-') + 1 ) );
		},
		
		/**
		 * Event handler for when a message is deleted
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		deletedMessage: function (e, data) {
			var feedId = $(e.target).closest('[data-feedid]').attr('data-feedid');	
			this._updateRow( feedId.substr( feedId.indexOf('-') + 1 ) );
		},
		
		/**
		 * Refresh row in list
		 *
		 * @param	{int}	conversationId	The conversation ID
		 * @returns	{void}
		 */
		_updateRow: function(conversationId) {
			var scope = $(this.scope);
			ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=messaging&controller=messenger&id=' + conversationId + '&getRow=1' ).done(function(response){
				scope.find('[data-messageid="'+conversationId+'"]').replaceWith( response );
				$( document ).trigger( 'contentChange', [ scope ] );
			});
		},

		/**
		 * Event handler for the 'sort' menu
		 * Triggers an event which loads new items into the list based on new sort order
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		changeSort: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}

			var sort = data.selectedItemID;

			if( sort ){
				/*this.trigger('loadFolder.messages', {
					sortBy: sort,
					folder: this._currentFolder,
					filterBy: this._currentOptions.filter,
				});*/

				this.trigger('changeSort.messages', {
					param: 'sortBy',
					value: sort
				});
			}
		},

		/**
		 * Event handler for the 'filter' menu
		 * Triggers an event which loads new items into the list based on the new filter
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		changeFilter: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}
			
			var filter = data.selectedItemID;

			if( filter ){
				/*this.trigger('loadFolder.messages', {
					sortBy: this._currentOptions.sort,
					folder: this._currentFolder,
					filterBy: filter
				});*/

				this.trigger('changeFilter.messages', {
					param: 'filter',
					value: filter
				});
			}
		},

		/**
		 * Responds to URL state changes
		 * Checks whether the folder or current message ID has changed
		 *
		 * @returns 	{void}
		 */
		stateChange: function () {
			var state = History.getState();

			if( _.isUndefined( state.data.controller ) || state.data.controller != 'messages' ){
				return;
			}

			var newFilters = false;

			if( state.data.params && ( state.data.params.sortBy != this._currentOptions.sortBy || state.data.params.filter != this._currentOptions.filter ) ){
				this._currentOptions.sortBy = state.data.params.sortBy;
				this._currentOptions.filter = state.data.params.filter;

				newFilters = true;
			}
			
			if( state.data.folder != this._currentFolder || newFilters ){
				this._getFolder( state.data.folder );
			}

			if( state.data.mid != this._currentMessageID ){
				if( _.isArray( state.data.mid ) ){
					this._selectMessages( state.data.mid );
				} else {
					this._selectMessage( state.data.mid );
				}
			}
		},
		
		/**
		 * Internal method which marks the message as read
		 *
		 * @param 		{number} 	id 		ID of message to highlight
		 * @returns 	{void}
		 */
		markMessageRead: function( e, data ) {
			this._messageList.find('[data-messageid="' + data.id + '"] a.cMessageTitle').removeClass('cMessageTitle_unread');
			this._messageList.find('[data-messageid="' + data.id + '"]').removeClass('ipsDataItem_unread').find('.ipsItemStatus').remove();
		},

		/**
		 * Starts a search by triggering on the model
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_startSearch: function (e) {
			var serialized = ips.utils.form.serializeAsObject( $('[data-role="messageSearch"]') );
			
			// If we deleted the search term, treat that the same as if we clicked the 'x' icon
			if( !serialized.q.length )
			{
				this.cancelSearch();
				return;
			}

			// If we deleted the search term, treat that the same as if we clicked the 'x' icon
			if( !serialized.q.length )
			{
				this.cancelSearch();
				return;
			}

			var gotSomething = false;
			_.each( [ 'topic', 'post', 'recipient', 'sender' ], function( item ) {
				if ( _.has( serialized.search, item ) ) {
					gotSomething = true;
				}
			} );
			
			if ( ! gotSomething ) {
				var self = this;
				ips.ui.alert.show( {
					type: 'alert',
					icon: 'warn',
					message: ips.getString('messageSearchFail'),
					subText: ips.getString('messageSearchFailSubText'),
					callbacks: {
						ok: function () {
							self._resetSearch();
							return false;
						}
					}
				});
			} else {
				this.trigger('searchFolder.messages', _.extend( {
					folder: this._currentFolder
				}, serialized ) );
			}
		},

		/**
		 * Resets changes made for searching
		 *
		 * @returns 	{void}
		 */
		_resetSearch: function () {
			// Reset the search box
			this.scope.find('[data-role="messageSearchText"]')
				.removeClass('ipsField_loading')
				.val('');

			// Hide the cancel button
			this.scope.find('[data-action="messageSearchCancel"]').hide();

			// Show the filter bar
			this.scope.find('[data-role="messageListFilters"]').show();

			// Reset page actions
			this._resetListActions();

			// Reset infinite scroll url
			this.scope.attr('data-ipsInfScroll', this._infScrollURL);
			this.scope.trigger('refresh.infScroll');
		},

		/**
		 * Internal method which highlights the message with the given ID
		 *
		 * @param 		{number} 	id 		ID of message to highlight
		 * @returns 	{void}
		 */
		_selectMessage: function (id) {
			this._messageList
				.find('[data-messageid]')
					.removeClass('cMessage_active ipsDataItem_selected')
				.end()
				.find('[data-messageid="' + id + '"]')
					.addClass('cMessage_active ipsDataItem_selected');
			
			this._currentMessageID = id;
		},

		/**
		 * Internal method which highlights multiple messages with the given IDs
		 *
		 * @param 		{array} 	IDs 	Array of IDs of messages to select
		 * @returns 	{void}
		 */
		_selectMessages: function (IDs) {
			var self = this;

			this._messageList
				.find('[data-messageid]')
					.removeClass('cMessage_active ipsDataItem_selected');

			_.each( IDs, function (id) {
				self._messageList
					.find('[data-messageid="' + id + '"]')
						.addClass('cMessage_active ipsDataItem_selected');
			});

			this._currentMessageID = IDs;
		},

		/**
		 * Internal handler which triggers an event to get the contents of a folder
		 *
		 * @param 		{string} 	newFolder 		ID of the new folder to get
		 * @returns 	{void}
		 */
		_getFolder: function ( newFolder ) {
			this.trigger('loadFolder.messages', {
				folder: newFolder,
				filter: this._currentOptions.filter,
				sortBy: this._currentOptions.sortBy
			});
			
			this._currentFolder = newFolder;
		},

		/**
		 * Hides the 'no messages' text
		 *	
		 * @returns 	{void}
		 */
		_hideEmpty: function () {
			this.scope.find('[data-role="emptyMsg"]').hide();
		},

		/**
		 * Reset page action/auto check boxes
		 *	
		 * @returns 	{void}
		 */
		_resetListActions: function () {
			// Refresh the page action so it hides
			try {
				ips.ui.pageAction.getObj( this.scope.find('[data-ipsPageAction]') ).reset();
				ips.ui.autoCheck.getObj( this.scope.find('[data-ipsAutoCheck]') ).refresh();
			} catch (err) {}
		},
		
		/**
		 * Prevents default event when a menu item is selected
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object}	data 	Event data object
		 * @returns {void}
		 */
		selectedMenuItem: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}			
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/messages" javascript_name="ips.messages.main.js" javascript_type="controller" javascript_version="107643" javascript_position="1000200"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.messages.main.js - Main messenger controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.messages.main', {

		_currentMessageID: null,
		_ready: {},
		_protectedFolders: ['myconvo'],
		_params: { 'sortBy': 'mt_last_post_time', 'filter': 'all' },
		_currentFolder: null,

		initialize: function () {
			// Main interface events
			this.on( 'menuItemSelected', '#elMessageFolders', this.changeFolder );
			this.on( 'menuItemSelected', '#elFolderSettings', this.folderAction );
			this.on( 'click', '[data-action="addFolder"]', this.addFolder );

			// Model events
			this.on( document, 'addFolderLoading.messages renameFolderLoading.messages ' + 
						'markFolderLoading.messages emptyFolderLoading.messages ' +
						'deleteMessageLoading.messages deleteMessagesLoading.messages moveMessageLoading.messages ' +
						'deleteFolderLoading.messages', this.folderActionLoading );
			this.on( document, 'addFolderFinished.messages renameFolderFinished.messages ' + 
						'markFolderFinished.messages emptyFolderFinished.messages ' +
						'deleteMessageFinished.messages deleteMessagesFinished.messages moveMessageFinished.messages ' + 
						'deleteFolderFinished.messages', this.folderActionDone );

			this.on( document, 'deleteFolderDone.messages deleteMessageDone.messages deleteMessagesDone.messages ' +
						'emptyFolderDone.messages moveMessageDone.messages', this.updateCounts );
			//--
			this.on( document, 'addFolderDone.messages', this.addFolderDone );
			this.on( document, 'renameFolderDone.messages', this.renameFolderDone );
			this.on( document, 'markFolderDone.messages', this.markFolderDone );
			this.on( document, 'emptyFolderDone.messages', this.emptiedFolder );
			this.on( document, 'deleteFolderDone.messages', this.deletedFolder );

			// Events from the list
			this.on( 'setInitialMessage.messages', this.setInitialMessage );
			this.on( 'setInitialFolder.messages', this.setInitialFolder );
			this.on( 'changeSort.messages changeFilter.messages', this.updateParam );
			//this.on( 'selectMessage.messages', this.selectMessage );
			this.on( 'loadMessage.messages', this.loadMessage );

			// Events from the view
			this.on( 'changePage.messages', this.changePage );

			// Document events
			this.on( document, 'controllerReady', this.controllerReady );
			this.on( document, 'openDialog', '#elAddFolder', this.addFolderDialogOpen );
			this.on( document, 'openDialog', '#elFolderRename', this.renameFolderDialogOpen );

			// Primary event that watches for URL changes
			History.Adapter.bind( window, 'statechange', _.bind( this.stateChange, this ) );
		},

		/**
		 * Responds to sub-controllers indicating they are initialized
		 * Allows us to check all sub-controllers are initialized before going any further
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		controllerReady: function (e, data) {
			this._ready[ data.controllerType ] = true;

			if( this._ready['messages.list'] && this._ready['messages.view'] &&
					data.controllerType == 'core.front.messages.list' || data.controllerType == 'core.front.messages.view' ){
				this.trigger('messengerReady.messages');
			}
		},

		/**
		 * Responds to an event from the list controller informing us of the initial message ID that's selected
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		setInitialMessage: function (e, data) {
			this._currentMessageID = data.messageID;
		},

		/**
		 * Responds to an event from the list controller informing us of the initial folder ID
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		setInitialFolder: function (e, data) {
			Debug.log( data );
			this._currentFolder = data.folderID;
		},

		/**
		 * Responds to event from view controller indicating the message page has changed (from pagination)
		 * Updates the URL with the new page number
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		changePage: function (e, data) {
			this._updateURL({
				id: data.id,
				page: data.pageNo
			}, {
				id: data.id, // reset message id
				page: data.pageNo
			});
		},

		/**
		 * Event handler for the folder action menu
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		folderAction: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}

			if( this._currentFolder == null ){

			}

			// Can't delete or rename protected folders
			if( _.indexOf( this._protectedFolders, this._currentFolder ) !== -1 && 
					_.indexOf( ['delete', 'rename'], data.selectedItemID ) !== -1 ){
				return;
			}

			switch( data.selectedItemID ){
				case 'markRead':
					this._actionMarkRead( data );
				break;
				case 'delete':
					this._actionDelete( data );
				break;
				case 'empty':
					this._actionEmpty( data );
				break;
				case 'rename':
					this._actionRename( data );
				break;
			}
		},

		/**
		 * Event handler for all 'folder action' loading events
		 * Displays a loading thingy in the messenger header
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		folderActionLoading: function (e, data) {
			var loading = this.scope.find('[data-role="loadingFolderAction"]');
			ips.utils.anim.go( 'fadeIn', loading );
		},

		/**
		 * Event handler for all 'folder action' loading done events
		 * Hides the loading thingy
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		folderActionDone: function (e, data) {
			var loading = this.scope.find('[data-role="loadingFolderAction"]');
			ips.utils.anim.go( 'fadeOut', loading );
		},

		/**
		 * Method to handle adding a folder
		 * Displays the dialog which contains the form
		 *	
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		addFolder: function (e) {
			var button = $( e.currentTarget );

			if( ips.ui.dialog.getObj( button ) ){
				ips.ui.dialog.getObj( button ).show();
			} else {
				button.ipsDialog( {
					content: '#elAddFolder_content',
					title: ips.getString('addFolder'),
					size: 'narrow'
				});
			}
		},

		/**
		 * Responds to event from model indicating a new folder has been added
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		addFolderDone: function (e, data) {
			var newItem = ips.templates.render('messages.main.folderMenu', {
				key: data.key,
				count: 0,
				name: data.folderName
			});

			// Find last menu item
			$('#elMessageFolders_menu')
				.find('[data-ipsMenuValue]')
					.last()
					.after( newItem );
				
			$('#elMessageFolders_menu')
				.find('[data-ipsMenuValue="' + data.key + '"]')
					.click(); // Find this item then click it to navigate
		},

		/**
		 * Responds to event from model indicating a folder has been renamed
		 * Updates the relevant menu item, and main messenger title
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		renameFolderDone: function (e, data) {
			var realFolderName = this._getRealFolder( data.folder );

			// Rename the menu item
			$('#elMessageFolders_menu')
				.find('[data-ipsMenuValue="' + data.folder + '"]')
					.find('[data-role="folderName"]')
						.text( data.folderName );

			// Rename the main title
			this.scope
				.find('[data-role="currentFolder"]')
					.text( data.folderName );

			// Show message
			ips.ui.flashMsg.show( ips.getString('renamedTo', {
				folderName: realFolderName,
				newFolderName: data.folderName
			}) );
		},

		/**
		 * Responds to model event indicating a folder is marked read
		 * Displays a message box
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		markFolderDone: function (e, data) {
			var realFolderName = this._getRealFolder( data.folder );
			ips.ui.flashMsg.show( ips.getString('messengerMarked', {
				folderName: realFolderName
			}) );
		},

		/**
		 * Responds to model event indicating a folder has been emptied
		 * Updates the count value in the folder menu
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		emptiedFolder: function (e, data) {
			var menuItem = $('#elMessageFolders_menu').find('[data-ipsMenuValue="' + data.folder + '"]');
			menuItem.find('.ipsMenu_itemCount').html('0');

			this.trigger( 'loadFolder', {
				folder: this._currentFolder,
				sortBy: this._params['sortBy'],
				filter: this._params['filter']
			});
		},

		/**
		 * Responds to model event indicating a folder has been deleted
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		deletedFolder: function (e, data) {
			// Remove the folder from the folder list, then click the myconvos folder to load it.
			// Our event handlers will handle it from there.
			this.scope
				.find('#elMessageFolders_menu')
					.find('[data-ipsMenuValue="' + data.folder + '"]')
						.remove()
					.end()
					.find('[data-ipsMenuValue="myconvo"]')
						.click();

			// Show a flash message
			ips.ui.flashMsg.show( ips.getString('folderDeleted') );
		},

		/**
		 * Responds to the list event triggered when a new message needs to be loaded
		 * Updates the URL with the new message ID
		 * The view controller handles actually loading the message from the model
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		loadMessage: function (e, data) {
			if( !data.messageID ){
				return;
			}

			this._newMessageID = data.messageID;
			this._updateURL( {
				id: data.messageID,
				url: data.messageURL
			}, {}, data.messageTitle );
		},

		/**
		 * Responds to the list event informing us that a sort/filter param has changed
		 * Stores this for later use in URL updates
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		updateParam: function (e, data) {
			if( !_.isUndefined( data.param ) && !_.isUndefined( data.value ) ){
				this._params[ data.param ] = data.value;
			}

			this._updateURL( false, this._params );
		},

		/**
		 * Event handler for the folder navigation menu
		 * Updates the URL with the new folder ID so we can navigate to a new folder
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		changeFolder: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}

			var folderID = data.selectedItemID;
			var folderURL = data.menuElem.find('[data-ipsMenuValue="' + data.selectedItemID + '"] a').attr('href');
			var folderName = data.menuElem.find('[data-ipsMenuValue="' + data.selectedItemID + '"]').find('[data-role="folderName"]').text();

			if( _.isUndefined( folderID ) ){
				return;
			}

			this._currentMessageID = null;

			this.scope.find('[data-ipsFilterBar]').trigger('switchTo.filterBar', {
				switchTo: 'filterBar'
			});

			this._updateURL( _.extend( { 
				folder: folderID,
				url: folderURL
			}, this._params ), {
				folder: folderID,
				id: null, // reset message id
				page: null
			}, folderName );
		},

		/**
		 * Handles an event.openDialog event for the add folder dialog
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		addFolderDialogOpen: function (e, data) {
			$( data.dialog )
				.find('input[type="text"]')
					.attr('data-folderID', this._currentFolder )
					.val('')
					.focus();
		},

		/**
		 * Handles an event.openDialog event for the rename folder dialog
		 *
		 * @param 		{event} 	e 		Event object	
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		renameFolderDialogOpen: function (e, data) {
			var realFolderName = this._getRealFolder( this._currentFolder );
			$( data.dialog )
				.find('[data-role="folderName"]')
					.attr('data-folderID', this._currentFolder )
					.val( _.unescape( realFolderName ) )
					.focus();
		},

		/**
		 * Responds to URL state changes
		 * Check whether the folder has changed, and load a new one if necessary
		 *	
		 * @returns 	{void}
		 */
		stateChange: function () {
			var state = History.getState();

			if( _.isUndefined( state.data.controller ) || state.data.controller != 'messages' ){
				return;
			}

			// Folder change?
			if( state.data.folder != this._currentFolder ){
				this._updateFolder( state.data.folder );
			}
		},

		/**
		 * Updates the browser URL
		 *	
		 * @param 		{object} 	urlParams 		Values which will be inserted into the URL
		 * @param 		{object} 	newValues 		Values which will be passed into the data object stored with the state
		 * @returns 	{void}
		 */
		_updateURL: function ( urlParams, newValues, newTitle ) {
			var url = '';
			var title = newTitle || document.title;
						
			if( urlParams === false ){
				url = window.location.href;
				if ( window.location.hash ) {
					url = url.substr( 0, url.length - window.location.hash.length );
				}
			} else if( urlParams.url ){
				url = urlParams.url;
			} else {
				var url = [];

				url.push( '?app=core&module=messaging&controller=messenger' );
				
				_.each( urlParams, function (value, idx) {
					if( idx != 'page' || ( idx == 'page' && value != 1 ) ){
						url.push( idx + "=" + value );
					}
				});

				url = url.join('&');
			}
			
			var defaultObj = {
				id: this._newMessageID,
				folder: this._currentFolder,
				params: this._params,
				controller: 'messages',
			};

			History.pushState( _.extend( defaultObj, newValues || {} ), newTitle, url );
		},

		/**
		 * Updates the quota progressbar and tooltip, and the folder counts
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		updateCounts: function (e, data) {			
			// Update quota tooltip text and width
			this.scope
				.find('[data-role="quotaTooltip"]')
					.attr('data-ipsTooltip-label', data.quotaText )
					.find('[data-role="quotaWidth"]')
						.animate({
							width: parseInt( data.quotaPercent ) + '%'
						})
					.end()
					.find('[data-role="quotaValue"]')
						.text( parseInt( data.quotaPercent ) );
			
			// Update folder counts
			$('#elMessageFolders_menu').find('[data-ipsMenuValue]').each( function () {
				if( data.counts )
				{
					$( this ).find('.ipsMenu_itemCount').text( parseInt( data.counts[ $( this ).attr('data-ipsMenuValue') ] ? data.counts[ $( this ).attr('data-ipsMenuValue') ] : '0' ) );
				}
			});
		},

		/**
		 * Handles changing to a new folder.
		 * Updates the name of the folder in the header, and enables/disables action menu options as needed
		 * Actually loading a new folder is handled in the list/view controllers
		 *	
		 * @param 		{string} 	newFolder 		New folder name
		 * @returns 	{void}
		 */
		_updateFolder: function (newFolder) {

			var folderName = $('[data-ipsMenuValue="' + newFolder + '"]').find('[data-role="folderName"]').text();
			var self = this;

			// Remove all disabled states
			$('#elFolderSettings_menu')
				.find('.ipsMenu_item')
					.removeClass('ipsMenu_itemDisabled')
					.show();

			// Update the settings menu URLs with the new folder (the JS handles the correct ajax URL, but
			// updating it here prevents any issues if there's a JS error)
			$('#elFolderSettings_menu .ipsMenu_item a').each( function () {
				$( this ).attr( 'href', $( this ).attr('href').replace( '&folder=' + self._currentFolder, '&folder=' + newFolder ) );
			});

			// See if we need to apply them again
			if( _.indexOf( this._protectedFolders, newFolder ) !== -1 ){
				$('#elFolderSettings_menu')
					.find('[data-ipsMenuValue="delete"], [data-ipsMenuValue="rename"]')
						.addClass('ipsMenu_itemDisabled')
						.hide();
			}

			// Update folder name
			this.scope.find('[data-role="currentFolder"]').text( folderName );

			this._currentFolder = newFolder;
		},

		/**
		 * Method to handle folder renaming
		 * Displays the dialog which contains the form
		 *	
		 * @param 		{object} 	data 	Event data object from this.folderAction
		 * @returns 	{void}
		 */
		_actionRename: function (data) {
			var dialog = $('#elFolderSettings_menu').find('[data-ipsMenuValue="rename"]');

			if( ips.ui.dialog.getObj( dialog ) ){
				ips.ui.dialog.getObj( dialog ).show();
			} else {
				dialog.ipsDialog( {
					content: '#elFolderRename_content',
					title: ips.getString('renameFolder'),
					size: 'narrow'
				});
			}
		},

		/**
		 * Method to handle folder deleting
		 *	
		 * @param 		{object} 	data 	Event data object from this.folderAction
		 * @returns 	{void}
		 */
		_actionDelete: function (data) {
			var self = this;

			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'question',
				message: ips.getString('messengerDeleteConfirm'),
				subText: ips.getString('cantBeUndone'),
				callbacks: {
					ok: function () {
						self.trigger( 'deleteFolder.messages', {
							folder: self._currentFolder
						});
					}
				}
			});
		},

		/**
		 * Method to handle marking a folder as reason
		 * Displays a confirmation box, and on success triggers an event for the model
		 *	
		 * @param 		{object} 	data 	Event data object from this.folderAction
		 * @returns 	{void}
		 */
		_actionMarkRead: function (data) {
			var realFolderName = this._getRealFolder( this._currentFolder );
			var self = this;
			
			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'question',
				message: ips.getString('messengerMarkRead', {
					folderName: realFolderName
				}),
				callbacks: {
					ok: function () {
						self.trigger( 'markFolder.messages', {
							folder: self._currentFolder
						});
					}
				}
			});	
		},

		/**
		 * Method to handle folder emptying
		 * Displays a confirmation box, and on success triggers an event for the model
		 *	
		 * @param 		{object} 	data 	Event data object from this.folderAction
		 * @returns 	{void}
		 */
		_actionEmpty: function (data) {
			var realFolderName = this._getRealFolder( this._currentFolder );
			var self = this;

			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'question',
				message: ips.getString('messengerDeleteContents', {
					folderName: realFolderName
				}),
				subText: ips.getString('cantBeUndone'),
				callbacks: {
					ok: function () {
						self.trigger( 'emptyFolder.messages', {
							folder: self._currentFolder
						});
					}
				}
			});		
		},

		/**
		 * Returns the real folder name based on the folder key
		 *	
		 * @param 		{string} 	folder 		Folder key
		 * @returns 	{string} 	Real folder name
		 */
		_getRealFolder: function (folder) {
			var menuItem = $('#elMessageFolders_menu').find('[data-ipsMenuValue="' + folder + '"]');
			return menuItem.find('[data-role="folderName"]').html();
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/messages" javascript_name="ips.messages.view.js" javascript_type="controller" javascript_version="107643" javascript_position="1000200"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.messages.view.js - Controller for message view pane in messenger
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.messages.view', {

		_currentMessageID: null,
		_currentPage: 1,

		initialize: function () {
			// Events from within
			this.on( 'paginationClicked paginationJump', this.paginationClicked );
			this.on( 'addToCommentFeed', this.addToCommentFeed );
			this.on( 'deletedComment.comment', this.deleteComment );

			this.on( document, 'menuItemSelected', '#elConvoMove', this.moveConversation );
			this.on( document, 'click', '[data-action="deleteConversation"]', this.deleteConversation );

			this.on( 'menuOpened', "[data-action='inviteUsers']", this.inviteMenuOpened );

			this.on( document, 'menuItemSelected', '[data-role="userActions"]', this.userAction );

			this.on( 'submit', '[data-role="addUser"]', this.addUsersSubmit );

			// Events bubbled from the list
			this.on( document, 'selectedMessage.messages', this.selectedMessage );
			this.on( document, 'setInitialMessage.messages', this.setInitialMessage );

			// Events from the main controller
			this.on( document, 'getFolder.messages', this.getFolder );

			// Model events
			this.on( document, 'loadMessageLoading.messages', this.loadMessageLoading );
			this.on( document, 'loadMessageDone.messages', this.loadMessageDone );
			this.on( document, 'deleteMessageDone.messages', this.deleteMessageDone );
			this.on( document, 'blockUserDone.messages', this.blockUserDone );
			this.on( document, 'addUserDone.messages', this.addUserDone );
			this.on( document, 'addUserError.messages', this.addUserError );

			// Primary event that watches for URL changes
			History.Adapter.bind( window, 'statechange', _.bind( this.stateChange, this ) );
			this.setup();

		},
		/**
		 * Setup method
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			if ( this.scope.attr('data-current-id') )
			{
				this._currentMessageID = this.scope.attr('data-current-id');
			}
		},

		/**
		 * A reply to the conversation
		 *
		 * @param 		{event} 	e 		Event object	
		 * @param 		{object} 	data	Data object	from model
		 * @returns 	{void}
		 */
		addToCommentFeed: function (e, data) {
			if( data.totalItems ){
				this.trigger( 'updateReplyCount.messages', {
					messageID: this._currentMessageID,
					count: data.totalItems
				});
			}
		},

		/**
		 * Adding a user to the conversation failed
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Data object from model
		 * @returns 	{void}
		 */
		addUserError: function (e, data) {
			if( data.error ){
				ips.ui.alert.show( {
					type: 'alert',
					icon: 'warn',
					message: data.error,
					callbacks: {}
				});
				return;
			}
		},

		/**
		 * One or more users have been added to this conversation
		 * Inserts or replaces new HTML in the participants list and shows a flashMsg
		 *
		 * @param 		{event} 	e 		Event object	
		 * @param 		{object} 	data	Data object	from model
		 * @returns 	{void}
		 */
		addUserDone: function (e, data) {
			if( data.id != this._currentMessageID ){
				return;
			}

			if( data.error ){
				ips.ui.alert.show( {
					type: 'alert',
					icon: 'warn',
					message: data.error,
					callbacks: {}
				});
				return;
			}

			var numberMembers = _.size( data.members );

			if( data.members && numberMembers ){
				for( var i in data.members ){
					var participant = this.scope.find('.cMessage_members').find('[data-participant="' + i + '"]');

					Debug.log('Ajax response:');
					Debug.log( data.members[ i ] );

					// If this user already exists, replace them
					if( participant.length ){
						participant.replaceWith( data.members[ i ] );	
					} else {
						// New record, so append it
						this.scope.find('.cMessage_members [data-role="addUserItem"]').before( data.members[ i ] );
					}					
				}
			}

			var message = ips.getString('messageUserAdded');

			if( numberMembers > 1 ){
				message = ips.pluralize( ips.getString( 'messageUsersAdded' ), numberMembers );
			}

			ips.ui.flashMsg.show( message );

			if( data.failed && parseInt( data.failed ) > 0 ){
				ips.ui.flashMsg.show( ips.getString('messageNotAllUsers') );
			}

			// Hide the 'add' menu
			this.scope.find('#elInviteMember' + this._currentMessageID).trigger('closeMenu');

			// Clear the autocomplete
			var autocomplete = ips.ui.autocomplete.getObj( this.scope.find('input[name="member_names"]') );

			autocomplete.removeAll();
		},

		/**
		 * Triggered by the invite user menu being opened
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		inviteMenuOpened: function (e) {
			this.scope.find('[data-role="addUser"] input[type="text"][id$="dummyInput"]').focus();
		},

		/**
		 * Event handler for submitting the 'invite users' form
		 * Triggers the addUser event
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		addUsersSubmit: function (e) {
			e.preventDefault();
			
			var names = $( e.currentTarget ).find('[name="member_names"]').val();

			this.trigger( 'addUser.messages', {
				id: this._currentMessageID,
				names: names
			});
		},

		/**
		 * The model has blocked a user
		 *
		 * @param 		{event} 	e 		Event object	
		 * @param 		{object} 	data 	Data object	from model
		 * @returns 	{void}
		 */
		blockUserDone: function (e, data) {
			if( data.id != this._currentMessageID ){
				return;
			}

			// Find participant & replace
			var participant = this.scope.find('.cMessage_members').find('[data-participant="' + data.member + '"]');
			participant.replaceWith( data.response );

			ips.ui.flashMsg.show( ips.getString('messageRemovedUser') );
		},

		/**
		 * Event handler for the user actions menu
		 *
		 * @param 		{event} 	e 		Event object	
		 * @param 		{object} 	data	Data object	from model
		 * @returns 	{void}
		 */
		userAction: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}

			var userID = $( data.triggerElem ).closest('[data-participant]').attr('data-participant');

			switch( data.selectedItemID ){
				case 'block':
					this.trigger('blockUser.messages', {
						member: userID,
						id: this._currentMessageID
					});
				break;
				case 'unblock':
					this.trigger('addUser.messages', {
						member: userID,
						id: this._currentMessageID,
						unblock: true
					});
				break;
			}
		},

		/**
		 * The model has deleted a message. If it's the one we're viewing, then remove the content and
		 * show the placeholder.
		 *
		 * @param 		{event} 	e 		Event object	
		 * @param 		{object} 	data	Data object	from model
		 * @returns 	{void}
		 */
		deleteMessageDone: function (e, data) {
			var url = ipsSettings['baseURL'] + '?app=core&module=messaging&controller=messenger'
			window.location = url;
		},

		/**
		 * Event handler for selecting a folder into which this conversation will be moved
		 *
		 * @param 		{event} 	e 		Event object	
		 * @param 		{object} 	data	Data object	from menu widget
		 * @returns 	{void}
		 */
		moveConversation: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}

			var self = this;

			// Get real name of folder
			var realName = $('#elConvoMove_menu').find('[data-ipsMenuValue="' + data.selectedItemID + '"] a').html();

			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'question',
				message: ips.getString('conversationMove', { name: realName } ),
				callbacks: {
					ok: function () {
						self.trigger( 'moveMessage.messages', { 
							id: self._currentMessageID,
							folder: data.selectedItemID
						});
					}
				}
			});
		},

		/**
		 * Event handler for clicking the delete conversation button.
		 * Confirms the user actually wants to delete it
		 *
		 * @param 		{event} 	e 		Event object	
		 * @returns 	{void}
		 */
		deleteConversation: function (e) {
			e.preventDefault();

			var self = this;

			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'question',
				message: ips.getString('messagesDelete'),
				subText: ips.getString('messagesDeleteSubText'),
				callbacks: {
					ok: function () {
						self.trigger( 'deleteMessage.messages', { 
							id: self._currentMessageID
						});
					}
				}
			});
		},

		/**
		 * Responds to the model loading message event
		 * Shows the loading thingy in the message pane
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		loadMessageLoading: function (e, data) {
			this.cleanContents();
			this.scope.html( 
				$('<div/>')
					.addClass('ipsLoading')
					.html('&nbsp;')
					.css( { minHeight: '150px' } )
			);
		},

		/**
		 * Responds to the model loaded message event
		 * Displays the loaded message in the message pane
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		loadMessageDone: function (e, data) {
			//this.cleanContents();
			this.scope.html( data.response );
			$( document ).trigger( 'contentChange', [ this.scope ] );
		},

		/**
		 * Responds to pagination event in conversation
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		paginationClicked: function (e, data){
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}

			/*this.trigger('changePage.messages', {
				pageNo: data.pageNo,
				perPage: data.perPage,
				id: this._currentMessageID
			});*/
		},

		/**
		 * Responds to event from main messages controller, informing us a message (or messages)
		 * have been selected. For a single message, we emit an event here to load the contents
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		selectedMessage: function (e, data) {
			//if( _.isArray( data.messageID ) ){
				//this.scope.html('');
			//} else {
				this.trigger( 'loadMessage.messages', { 
					messageID: data.messageID,
					messageURL: data.messageURL,
					messageTitle: data.messageTitle
				});
			//}

			this._currentMessageID = data.messageID;
		},

		/**
		 * Responds to the browser url changing
		 * We're only interested in watching for the message ID here. If it changes, we fetch a new message
		 *	
		 * @returns 	{void}
		 */
		stateChange: function () {
			var state = History.getState();

			if( _.isUndefined( state.data.controller ) || state.data.controller != 'messages' ){
				return;
			}

			if( state.data.id == null ){
				this.cleanContents();
				this.scope.html( ips.templates.render('messages.view.placeholder') );
				
				// Reset values
				this._currentMessageID = null;
				this._currentPage = null;
				return;
			}

			if( state.data.id != this._currentMessageID ){
				// Get message from le model
				this.trigger( 'fetchMessage.messages', {
					id: state.data.id,
					page: state.data.page || 1
				});

				// Track page view
				ips.utils.analytics.trackPageView( state.url );
				
				// Reset values
				this._currentMessageID = state.data.id;
				this._currentPage = state.data.page || 1;
				return;
			} else if( state.data.page != this._currentPage ){
				this.trigger( 'fetchMessage.messages', {
					id: this._currentMessageID,
					page: state.data.page
				});

				this._currentPage = state.data.page;
			}
		},

		/**
		 * Responds to an event from the main controller letting us know the initially-selected message ID
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		setInitialMessage: function (e, data) {
			this._currentMessageID = data.messageID;
		},

		/**
		 * Responds to an event from the main controller indicating the selected folder has changed
		 * We remove any message present and replace it with the placeholder
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		getFolder: function (e, data) {
			this.cleanContents();
			this.scope.html( ips.templates.render('messages.view.placeholder') );
			ips.utils.anim.go( 'fadeIn', this.scope );
		},
	});
}(jQuery, _));
]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/modcp" javascript_name="ips.modcp.announcementForm.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://invisioncommunity.com
 *
 * ips.modcp.announcementForm.js - Controller for the announcement add/edit form
 *
 * Author: Stuart Silvester
 */
;( function($, _, undefined){
    "use strict";

    ips.controller.register('core.front.modcp.announcementForm', {

        _lastValue: '',
        _textField: null,
        _timer: 0,

        initialize: function () {

            this._textField = $('#elInput_announce_url');
			this.on( 'blur', '#elInput_announce_url', this.fieldKeyUp );
        },

        fieldKeyUp: function() {
			// Reset timer
			clearTimeout( this._timer );
			this._timer = setTimeout( _.bind( this._checkPermissions, this ), 700 );
        },

        _checkPermissions: function () {
            var value = this._textField.val().trim();

            // Must have more than 3 characters
            if( value.length < 3 )
            {
                return;
            }


            ips.getAjax()('?app=core&module=system&controller=announcement&do=permissionCheck', {
                dataType: 'json',
                data: {
                    url:  value
                }
            }).done( function (response) {

                // Remove any current warning if the latest response doesn't have one
                if( _.isUndefined( response.html ) && $('#elAnnouncementGroupWarning').length )
                {
					$('#elAnnouncementGroupWarning').remove();
                }

                // Replace existing, or create warning
                if( $('#elAnnouncementGroupWarning').length )
                {
                    $('#elAnnouncementGroupWarning').replaceWith( response.html );
                }
                else
                {
                    $( response.html ).insertAfter( '#elInput_announce_url' );
                }
            }).fail( function (err) {
				// fail gets called when it's aborted, so deliberately do nothing here
			});
        }

    });
}(jQuery, _));
]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/modcp" javascript_name="ips.modcp.approveQueue.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.modcp.approveQueue.js - Controller for using approval queue
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.modcp.approveQueue', {
	
		initialize: function () {
			this.on( 'click', '[data-action=&quot;approvalQueueNext&quot;]', this.doConfirm );
		},
		
		/**
		 * Confirm
		 *
		 * @param 		{event} 	e	Event object
		 * @param		{object}	data	Event data
		 * @returns 	{void}
		 */
		doConfirm: function(e,data) {
			e.preventDefault();
			
			var self = this;
			var action = $( e.currentTarget ).attr('data-type');
			
			if( action !== 'delete' )
			{
			    self.doAction( e, data );
			    return;
			}
			
			var alert = {
				type: 'confirm',
				icon: 'warn',
				message: ips.getString('generic_confirm'),
				subText: '',
				callbacks: {
					ok: function () {
						self.doAction( e, data );
					},
					no: function () {
						return;
					}
				}
			};
			
			ips.ui.alert.show( alert );
		},
		
		/**
		 * Respond when an action button is clicked
		 *
		 * @param 		{event} 	e 		Event object
		 * @param		{object}	data	Event data
		 * @returns 	{void}
		 */
		doAction: function(e,data) {
			e.preventDefault();
			
			var scope = $(this.scope);
			
			if ( $( e.currentTarget ).hasClass('ipsButton_disabled') ) {				
				ips.ui.alert.show({
					type: 'alert',
					icon: 'warn',
					message: ips.getString('approvalQueueNoPerm')
				});
				return;
			}
			
			var height = $('#elApprovePanel').height();
			$('#elApprovePanel').html('').css( 'height', height ).addClass('ipsLoading');
						
			ips.getAjax()( $( e.currentTarget ).attr('href'), { bypassRedirect: true } )
				.done(function(){
					ips.getAjax()( scope.attr('data-url') )
						.done(function(response){
							scope.html( response.html );
							$('#elModCPApprovalCount').html( response.count );
						})
						.fail(function(failresponse){
							window.location = scope.attr('data-url');
						});
				})
				.fail(function(){
					window.location = $( e.currentTarget ).attr('href');
				});
		}
		
	});
}(jQuery, _));
</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/modcp" javascript_name="ips.modcp.report.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.modcp.report.js - Controller for viewing a report
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.modcp.report', {
	
		initialize: function () {
			this.on( document, 'submitDialog', '[data-role=&quot;warnUserDialog&quot;]', this.dialogSubmitted );
			this.on( 'menuItemSelected', this.menuItemSelected );
		},
		
		/**
		 * Respond when a menu item is selected
		 *
		 * @param 		{event} 	e 		Event object
		 * @param		{object}	data	Event data
		 * @returns 	{void}
		 */
		menuItemSelected: function(e, data) {
			data.originalEvent.preventDefault();

			var link = data.menuElem.find('[data-ipsMenuValue=&quot;' + data.selectedItemID + '&quot;] a');
			var langString = ( data.selectedItemID == 'spamFlagButton' ) ? ips.getString( 'confirmFlagAsSpammer' ) : ips.getString( 'confirmUnFlagAsSpammer' );
			var descString = ( data.selectedItemID == 'spamUnFlagButton' ) ? ips.getString( 'confirmUnFlagAsSpammerDesc' ) : '';
			var self = this;

			if( data.selectedItemID == 'spamFlagButton' || data.selectedItemID == 'spamUnFlagButton' ){
				ips.ui.alert.show({
					type: 'confirm',
					message: langString,
					subText: descString,
					callbacks: {
						ok: function () {
							self._startLoading();

							ips.getAjax()( link.attr('href'), {
								bypassRedirect: true
							} )
								.done( function (response) {
									self._refreshPanel();
								});
						},
					}
				});
			}
		},
		
		/**
		 * Respond when a dialog is submitted
		 *
		 * @param 		{event} 	e 		Event object
		 * @param		{object}	data	Event data
		 * @returns 	{void}
		 */
		dialogSubmitted: function(e, data) {
			this._startLoading();
			this._refreshPanel();
		},
		
		/**
		 * Start Loading
		 */
		_startLoading: function() {
			this.scope
				.find('[data-role=&quot;authorPanel&quot;]')
					.css( 'height', this.scope.find('[data-role=&quot;authorPanel&quot;]').height() + 'px' )
					.addClass('ipsLoading')
					.find('*')
						.hide();
		},
		
		/**
		 * Refresh Panel
		 */
		_refreshPanel: function() {
			var self = this;
			ips.getAjax()( window.location, {
				bypassRedirect: true
			} )
				.done( function(response){
					self.scope
						.find('[data-role=&quot;authorPanel&quot;]')
							.css( 'height', 'auto' )
							.removeClass('ipsLoading')
							.html( response );
				} );
		}
		
	});
}(jQuery, _));
</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/modcp" javascript_name="ips.modcp.reportList.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.modcp.reportList.js - Report list controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.modcp.reportList', {

		initialize: function () {
			this.on( 'menuItemSelected', '[data-action=&quot;changeStatus&quot;]', this.changeReportStatus );
		},

		/**
		 * When a reports status is changed, check whether the row needs changing
		 *
		 * @param 		{event} 	e 		Event object
		 * @param		{object}	data	Event data
		 * @returns 	{void}
		 */
		changeReportStatus: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}

			var row = $( e.currentTarget ).closest('.ipsDataItem');

			row.removeClass('ipsDataItem_new ipsDataItem_warning');

			switch( data.selectedItemID ){
				case '1':
					row.addClass('ipsDataItem_new');
				break;
				case '2':
					row.addClass('ipsDataItem_warning');
				break;
			}
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/modcp" javascript_name="ips.modcp.reportToggle.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.modcp.reportToggle.js - Controller for report toggling
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.modcp.reportToggle', {

		initialize: function () {
			this.on( 'menuItemSelected', this.reportToggled );
		},

		/**
		 * Report status has been changed
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		reportToggled: function (e, data) {
			// Get the menu elem
			var item = data.menuElem.find('[data-ipsmenuvalue=&quot;' + data.selectedItemID + '&quot;]');

			var icon = item.find('[data-role=&quot;ipsMenu_selectedIcon&quot;]').attr('class');
			var status = item.find('[data-role=&quot;ipsMenu_selectedText&quot;]').text();

			this.scope.find('[data-role=&quot;reportIcon&quot;]').get(0).className = icon;
			this.scope.find('[data-role=&quot;reportStatus&quot;]').text( status );

			// And show a flash message
			ips.ui.flashMsg.show( ips.getString('reportStatusChanged') );
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/modcp" javascript_name="ips.modcp.warnForm.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.modcp.warnForm.js - Controller for the add warning form
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.modcp.warnForm', {
		pointsEdited: false,
		expirationEdited: false,
		editorSetTo: '',

		initialize: function () {
			var self = this;
			this.on( 'change', '[name="warn_reason"]', this.changeReason ); 
			this.on( 'change', '[name="warn_points"]', this.changePoints );
			this.on( 'change', '[name="warn_remove"],[name="warn_remove_time"]', function( e ) {
				self.expirationEdited = true;
			} );
			this.on( 'editorWidgetInitialized', this.editorInitialized );
		},

		editorInitialized: function (e, data) {
			if( data.id == 'warn_member_note' ){
				$('[name="warn_reason"]').change(); // Set for initial value

				// check if we have a default note
				var editor = ips.ui.editor.getObj( $( 'textarea[name="warn_member_note"]' ).closest('[data-ipsEditor]') );
				this.editorSetTo = editor.getInstance().getData();
			}
		},
		
		/**
		 * Change reason handler
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		changeReason: function(e) {
			var scope = this.scope;
			var self  = this;

			ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=warnings&do=reasonAjax&id=' + $( e.target ).val() ).done( function(response) {

				// If the points have been changed AND we can override the points, do not adjust
				if( self.pointsEdited == false || response.points_override == 0 )
				{
					var pointsNow = scope.find('[name="warn_points"]').val();
                	scope.find('[name="warn_points"]').val( response.points ).prop( 'disabled', response.points_override == 0 );

                	if( pointsNow != response.points )
                	{
                		scope.find('[name="warn_points"]').change();
                	}

                	// Flag that we're back to default
                	self.pointsEdited = false;
                }
                
                var removePointsUnlimited = scope.find('[name="warn_remove_unlimited"]');

                if ( response.remove.unlimited ) {
                	if( self.expirationEdited == false || response.remove_override == 0 )
                	{
						if( removePointsUnlimited.prop( 'checked' ) == false )
						{
							if( removePointsUnlimited.prop('disabled') == true ){
								removePointsUnlimited.prop('disabled', false).click().prop('disabled', true);
							} else {
								removePointsUnlimited.click();
							}
						}
					}
				} else if( self.expirationEdited == false || response.remove_override == 0 ) {
					removePointsUnlimited.prop( 'checked', false );
					scope.find('[name="warn_remove"]').val( response.remove.date ).prop( 'disabled', response.remove_override == 0 );
					scope.find('[name="warn_remove_time"]').val( response.remove.time ).prop( 'disabled', response.remove_override == 0 );
				}
				removePointsUnlimited.prop( 'disabled', response.remove_override == 0 );
				
				var cheevPoints = scope.find('[name="warn_cheeve_point_reduction"]');
				if( response.cheev_override == 0 ) {
					cheevPoints.prop('disabled', true);
				} else {
					cheevPoints.prop('disabled', false);
				}
				cheevPoints.val( response.cheev_point_reduction );
				
				var editor = ips.ui.editor.getObj( $( 'textarea[name="warn_member_note"]' ).closest('[data-ipsEditor]') );

				if( response.notes ){
					var currentContents = editor.getInstance().getData();
					var previousContents = self.editorSetTo;
					editor.unminimize( function(){
						if ( currentContents == previousContents ) {
							editor.reset();
						} else {
							editor.insertHtml('<p></p>');
						}
						editor.insertHtml( response.notes );
						editor.resetDirty();
						self.editorSetTo = editor.getInstance().getData();
					});
				}
				
			} );
		},
		
		/**
		 * Change points handler
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		changePoints: function(e) {
			this.pointsEdited	= true;
			var scope = this.scope;

			ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=warnings&do=actionAjax&points=' + $( e.target ).val() + '&member=' + scope.attr('data-member') ).done( function(response) {
				var types = [ 'mq', 'rpa', 'suspend' ];

				scope.find( 'ul#elWarningPenalties').remove();
				if( parseInt( response.override ) ) {
					scope.find( 'li#form_warn_punishment .ipsFieldRow_content' ).show();
				}
				else {
					scope.find( 'li#form_warn_punishment .ipsFieldRow_content' ).hide();
					var enforcedPenalties = [];
					for( var i = 0; i < 3; i++ ) {
						if( parseInt( response.actions[ types[i] ].unlimited ) ) {
							enforcedPenalties.push( ips.getString( 'warningPunishmentIndefinitely', { type: ips.getString( 'warningPunishment_' + types[i] ) } ) );
						}
						else if( response.actions[ types[i] ].date != "" ) {
							var date = new Date( response.actions[ types[i] ].date + ' ' + response.actions[ types[i] ].time );
							enforcedPenalties.push( ips.getString( 'warningPunishmentDate', { type: ips.getString( 'warningPunishment_' + types[i] ), date: ips.utils.time.localeDateString( date, { dateStyle: "long", timeStyle: "short" } ) } ) );
						}

						// uncheck checkbox to hide toggled on fields
						scope.find( '[name="warn_punishment[' + types[i] + ']"]' ).prop( 'checked', false ).change();
					}

					// Generate penalty list
					scope.find( 'li#form_warn_punishment').append( ips.templates.render('system.warningpenalty.nomodify', { penalties: enforcedPenalties } ) );
				}

				for( var i = 0; i < 3; i++ ) {
					// Only check this checkbox if it can be overridden
					if( parseInt( response.override ) )
					{
						scope.find( '[name="warn_punishment[' + types[i] + ']"]' ).prop( 'checked', ( response.actions[ types[i] ].date || response.actions[ types[i] ].unlimited ) ).change();
					}
					scope.find( '[name="warn_' + types[i] + '"]' ).val( response.actions[ types[i] ].date ).prop( 'disabled', !parseInt( response.override ) );
					scope.find( '[name="warn_' + types[i] + '_time"]' ).val( response.actions[ types[i] ].time ).prop( 'disabled', !parseInt( response.override ) );
					scope.find( '[name="warn_' + types[i] + '_unlimited"]' ).prop( 'checked', response.actions[ types[i] ].unlimited ).prop( 'disabled', !parseInt( response.override ) );
				}
			} );
		}
		

	});
}(jQuery, _));
]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/modcp" javascript_name="ips.modcp.warnPopup.js" javascript_type="controller" javascript_version="107643" javascript_position="1000250"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.modcp.warnPopup.js - Warning popup controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.modcp.warnPopup', {

		initialize: function () {
			this.on( 'click', '[data-action="revoke"]', this.revokeWarning );
		},

		/**
		 * Revoke warning
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		revokeWarning: function (e) {
			e.preventDefault();

			var url = $( e.currentTarget ).attr('href');

			ips.ui.alert.show( {
				type: 'verify',
				icon: 'question',
				message: ips.getString('revokeWarning'),
				buttons: {
					yes: ips.getString('reverseAndDelete'),
					no: ips.getString('justDelete'),
					cancel: ips.getString('cancel')
				},
				callbacks: {
					yes: function () {
						window.location = url + '&undo=1';
					},
					no: function () {
						window.location = url + '&undo=0';
					}
				}
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/profile" javascript_name="ips.profile.body.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.profile.body.js - Profile body controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.profile.body', {

		/**
 		 * Initialize controller events
		 * Sets up the events from the view that this controller will handle
		 *
		 * @returns 	{void}
		 */
		initialize: function () {
			this.on( 'click', '[data-action=&quot;showRecentWarnings&quot;]', this.showRecentWarnings );
			this.setup();
		},

		/**
 		 * Non-event-based setup
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			
		},

		showRecentWarnings: function (e) {
			e.preventDefault();
			
			this.scope.find('[data-action=&quot;showRecentWarnings&quot;]').hide();
			ips.utils.anim.go( 'fadeIn fast', this.scope.find('[data-role=&quot;recentWarnings&quot;]') );
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/profile" javascript_name="ips.profile.followers.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.profile.followers.js - Follower JS
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.profile.followers', {

		_feedID: null,

		initialize: function () {
			this.on( document, 'followingItem', this.followUser );
			this.on( 'menuItemSelected', "[data-role='followOption']", this.toggleFollowOption );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */	 
		setup: function () {
			this._feedID = this.scope.attr('data-feedID');
		},

		/**
		 * Event handler for document-wide followingItem event
		 * Checks if the event is for this member (based on 'feedID'), and fetches new HTML
		 * for the followers block
		 *
		 * @param 	{event} 	Event object
		 * @param 	{object} 	Event data object
		 * @returns {void}
		 */	 
		followUser: function (e, data) {
			if( data.feedID != this._feedID ){
				return;
			}

			var self = this;
			var memberID = data.feedID.replace('member-', '');

			// Get the new followers
			// If there's an error we can just ignore it, it's not a big deal
			ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=members&controller=profile&do=followers&id=' + parseInt( memberID ) )
				.done( function (response) {
					self.scope.html( response );
				})
				.fail( function () {
					Debug.log('Error fetching follower HTML');
				});
		},

		/**
		 * Event handler for changing the follower preference (for profile owner)
		 *
		 * @param 	{event} 	Event object
		 * @param 	{object} 	Event data object
		 * @returns {void}
		 */	 
		toggleFollowOption: function (e, data) {
			data.originalEvent.preventDefault();

			var url = data.menuElem.find('[data-ipsMenuValue="' + data.selectedItemID + '"] a').attr('href');

			// Ping
			ips.getAjax()( url )
				.done( function (response) {
					ips.ui.flashMsg.show( ips.getString('followerSettingToggled') );
				})
				.fail( function () {
					window.location = url;
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/profile" javascript_name="ips.profile.main.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.profile.main.js - Main profile wrapper
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.profile.main', {
		
		contentArea: null,

		/**
 		 * Initialize controller events
		 * Sets up the events from the view that this controller will handle
		 *
		 * @returns 	{void}
		 */
		initialize: function () {
			this.on( 'click', '[data-action="goToProfile"]', this.changeType );
			this.on( 'click', '[data-action="browseContent"]', this.changeType );
			this.on( 'click', '[data-action="repLog"]', this.changeType );
			this.on( 'click', '[data-action="badgeLog"]', this.changeType );
			this.on( 'click', '[data-action="solutionLog"]', this.changeType );

			// Primary event that watches for URL changes
			History.Adapter.bind( window, 'statechange', _.bind( this.stateChange, this ) );

			this.setup();
		},

		/**
 		 * Non-event-based setup
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this.contentArea = this.scope.find('[data-role="profileContent"]');
			this.contentHeader = this.scope.find('[data-role="profileHeader"]');

			var state = History.getState();
			var url = ips.utils.url.getURIObject();

			if( !state.data.section && !url.queryKey.tab ) {
				History.replaceState({ section : 'goToProfile'}, state.title);
			}
		},

		/**
		 * Called when History.js state changes
		 *	
		 * @returns 	{void}
		 */
		stateChange: function () {
			var state = History.getState();

			Debug.log( state.data.section );

			switch( state.data.section ){
				case 'goToProfile':
					this._showProfile( state.url );
				break;
				case 'browseContent':
					this._showContent( state.url );
				break;
				case 'repLog':
				case 'solutionLog':
				case 'badgeLog':
					this._showGenericLog( state.url );
					this._showGenericLog( state.url );
				break;
			}
		},

		/**
		 * User clicked something that changes the profile view
		 * Just change the URL, the state change handler does the rest
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		changeType: function (e) {
			e.preventDefault();
			var target = $( e.currentTarget );

			if( !target.is('a') ){
				target = target.find('a');
			}

			this._changeURL( 
				{ section: $( e.currentTarget ).attr('data-action') },
				target.attr('title'),
				target.attr('href') 
			);			
		},

		/**
		 * Shows the user's reputation or solution log
		 *	
		 * @param 		{string} 	url 		URL to load the content
		 * @returns 	{void}
		 */
		_showGenericLog: function (url) {
			var self = this;
			this._changeContent( true, url );
			this._showProfileButton();

			if( ips.utils.responsive.enabled() && ips.utils.responsive.currentIs('phone') ){
				$('#elProfileStats').addClass('cProfileHeaderContent');
			}
		},

		/**
		 * Shows the user's content
		 *	
		 * @param 		{string} 	url 		URL to load the content
		 * @returns 	{void}
		 */
		_showContent: function (url) {
			var self = this;
			
			$( '[data-action="goToProfile"]' ).attr( "disabled", true );
			this._showProfileButton();
			this._changeContent( true, url );

			if( ips.utils.responsive.enabled() && ips.utils.responsive.currentIs('phone') ){
				$('#elProfileStats').addClass('cProfileHeaderContent');
			}
		},

		/**
		 * Shows the 'view profile' button in the header
		 *	
		 * @returns 	{void}
		 */
		_showProfileButton: function () {
			var self = this;

			// Hide browse button
			this.contentHeader.find('[data-action="browseContent"]').each( function () {
				var elem = $( this );

				if( elem.is(':visible') ){
					elem.animationComplete( function () {
						ips.utils.anim.go( 'fadeIn fast', self.contentHeader.find('[data-action="goToProfile"][data-type="' + elem.attr('data-type') + '"]') );
					 });
					 
					ips.utils.anim.go( 'fadeOut fast', elem );
				} else {
					elem.hide();
					self.contentHeader.find('[data-action="goToProfile"][data-type="' + elem.attr('data-type')  + '"]').show();
				}
			});
		},

		/**
		 * Shows the user's profile
		 *	
		 * @param 		{string} 	url 		URL to load the content
		 * @returns 	{void}
		 */
		_showProfile: function (url) {
			var self = this;

			$( '[data-action="browseContent"]' ).attr( "disabled", true );
			this._showContentButton();
			this._changeContent( false, url );

			$('#elProfileStats').removeClass('cProfileHeaderContent');
		},

		/**
		 * Shows the 'view profile' button in the header
		 *	
		 * @returns 	{void}
		 */
		_showContentButton: function () {
			var self = this;

			// Hide browse button
			this.contentHeader.find('[data-action="goToProfile"]').each( function () {
				var elem = $( this );

				if( elem.is(':visible') ){
					elem.animationComplete( function () {
						ips.utils.anim.go( 'fadeIn fast', self.contentHeader.find('[data-action="browseContent"][data-type="' + elem.attr('data-type') + '"]') );
					});
					
					ips.utils.anim.go( 'fadeOut fast', elem );
				} else {
					elem.hide();
					self.contentHeader.find('[data-action="browseContent"][data-type="' + elem.attr('data-type') + '"]').show();
				}
			});
		},

		/**
		 * Changes the content in the content section
		 *	
		 * @param 		{boolean} 	small 		Show the header in its minimal state?
		 * @param 		{string} 	url 		The URL to load
		 * @returns 	{void}
		 */
		_changeContent: function (small, url) {
			var self = this;

			ips.controller.cleanContentsOf( this.contentArea );

			// Get height and set it, so that it doesn't jolt the page
			this.contentArea.css({
				height: String(this.contentArea.outerHeight())
			});

			// Remove content and set to loading
			this.contentArea.html( 
				$('<div/>').addClass('ipsLoading').css({
					height: '300px'
				})
			);

			// Add class to the header to shrink it
			this.contentHeader.find('#elProfileHeader').toggleClass( 'cProfileHeaderMinimal', small );

			// Load the content
			ips.getAjax()( url )
				.done( function (response) {
					self.contentArea
						.hide()
						.html( response )
						.css({
							height: 'auto'
						});

					self.contentHeader.find("#elProfileStats a").each( function () {
						$( this ).removeAttr( "disabled" );
					});

					ips.utils.anim.go( 'fadeIn fast', self.contentArea );

					$( document ).trigger( 'contentChange', [ self.contentArea ] );
				})
				.fail( function () {
					window.location = url;
				});
		},

		/**
		 * Push a new URL to the browser
		 *	
		 * @param 		{object} 	data 		Object to save as the state data
		 * @param 		{string} 	title 		Page title
		 * @param 		{string} 	url 		Page URL
		 * @returns 	{void}
		 */
		_changeURL: function (data, title, url) {
			History.pushState( data, title, url );
		}

	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/profile" javascript_name="ips.profile.toggleBlock.js" javascript_type="controller" javascript_version="107643" javascript_position="1000300">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.profile.toggleBlock.js - Toggle blocks on the profile
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.profile.toggleBlock', {

		initialize: function () {
			this.on( 'click', '[data-action=&quot;disable&quot;]', this.toggleBlock );
			this.on( 'click', '[data-action=&quot;enable&quot;]', this.toggleBlock );
		},

		/**
		 * Toggles a block on the profile, loading the new contents via ajax from the target URL
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		toggleBlock: function (e) {
			e.preventDefault();

			var self = this;

			this.scope.css({
				opacity: &quot;0.6&quot;
			});

			ips.getAjax()( $( e.currentTarget ).attr('href'), {
				showLoading: true
			} )
				.done( function (response) {
					self.scope.html( response );
					$( document ).trigger( 'contentChange', [ self.scope ] );
				})
				.fail( function () {
					window.location = $( e.currentTarget ).attr('href');
				})
				.always( function () {
					self.scope.css({ 
						opacity: &quot;1&quot;
					});
				});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/promote" javascript_name="ips.system.promote.js" javascript_type="controller" javascript_version="107643" javascript_position="1000350"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.promote.js - Promotion controller
 *
 * Author: Matt Mecham
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.system.promote', {

		_twitterContent: '',
		_storedContent: {},
		
		initialize: function () {
			this.on( window, 'resize', this.resizeContentArea );
			this.on( 'click', '[data-action="selectImage"]', this.selectImage );
			this.on( 'click', '[data-action="cancelShare"]', this.cancelShare );
			this.on( 'click', '[data-action="enableShare"]', this.enableShare );
			this.on( 'focus', '[name="promote_custom_date"], [name="promote_custom_date_time"]', this.toggleFutureSchedule );
			this.on( 'change', '[name="promote_schedule"]', this.changeSchedule );
			this.on( 'keyup', '[name="promote_social_content_twitter"]', this.twitterContentChange );
			this.on( 'click', '[data-action="expandTextarea"]', this.expandTextarea );
			
			var self = this;
			this.scope.find('[data-action="counter"]').each( function () {
				// Initialise
				self.updateCounter( self.scope.find('[name="' + $(this).attr('data-count-field') + '"]') );
				
				// Watch
				self.on( 'keyup', '[name="' + $(this).attr('data-count-field') + '"]', self.changeCounter );
			} );

			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			this.resizeContentArea();
			this.changeSchedule();
			this._saveTwitterContent();
		},
		
		/**
		 * Expands the original content text area
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		expandTextarea: function (e) {
			e.preventDefault();
			
			$('#eOriginalText').removeClass('cPromote_text_fade').addClass('cPromote_text_expanded');
			this.scope.find('[data-action="expandTextarea"]').hide();
		},

		/**
		 * Store the initial twitter content so we know if it's changed
		 *
		 * @returns {void}
		 */
		_saveTwitterContent: function () {
			if( this.scope.find('[name="promote_social_content_twitter"]').length ){
				this._twitterContent = this.scope.find('[name="promote_social_content_twitter"]').val();
			}
		},

		/**
		 * Called on keyup; if it's the same, show the warning message
		 *
		 * @returns {void}
		 */
		twitterContentChange: function () {
			var val = this.scope.find('#elTextarea_promote_social_content_twitter').val();

			if( val == this._twitterContent ){
				this.scope.find('[data-role="twitterDupe"]').slideDown();
			} else if( this.scope.find('[data-role="twitterDupe"]').is(':visible') ){
				this.scope.find('[data-role="twitterDupe"]').slideUp();
			}
		},

		/**
		 * Hide a share type
		 * Empties the textbox, since this is how the backend handles not sending to a particular service
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		cancelShare: function (e) {
			e.preventDefault();

			var closeButton = $( e.currentTarget );
			var row = closeButton.closest('.ipsFieldRow');
			var textarea = row.find('.ipsFieldRow_content').find('textarea');
			this._storedContent[ textarea.attr('name') ] = textarea.val();
			
			row
				.addClass('cPromoteRow_minimized')
				.find('.ipsFieldRow_content')
					.fadeOut('fast')
				.end()
				.find('textarea')
					.val('')
					.slideUp( function () {				
						closeButton.hide();
					})
				.end();

			if( row.find('[data-action="enableShare"]').length ){
				row.find('[data-action="enableShare"]').fadeIn();
			} else {
				row.append( $('<div/>').addClass('ipsButton ipsButton_veryLight ipsButton_small cPromoteEnable').attr('data-action', 'enableShare').text( ips.getString('enablePromote') ) );
			}
		},

		/**
		 * Re-enables a share type
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		enableShare: function (e) {
			e.preventDefault();

			var enableButton = $( e.currentTarget );
			var row = enableButton.closest('.ipsFieldRow');
			var closeButton = row.find('[data-action="cancelShare"]');
			var textarea = row.find('.ipsFieldRow_content').find('textarea');
			var restoreVal = ! _.isUndefined( this._storedContent[ textarea.attr('name') ] ) ? this._storedContent[ textarea.attr('name') ] : '';
			
			row
				.find('.ipsFieldRow_content')
					.fadeIn('fast')
				.end()
				.find('textarea')
					.val( restoreVal )
					.slideDown( function () {
						closeButton.show();
					})
				.end()
				.find('[data-action="enableShare"]')
					.hide();
		},

		/**
		 * Check an image in the attachment panel
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		selectImage: function (e) {
			e.preventDefault();

			var image = $( e.currentTarget );
			var check = image.find('input[type="checkbox"]');
			var select = image.find('.ipsAttach_selection');
			var wrap = image.closest('.cPromote_attachImage');

			select.toggleClass('ipsAttach_selectionOn', !check.is(':checked') );
			wrap.toggleClass('cPromote_attachImageSelected', !check.is(':checked') );

			check.prop('checked', !check.is(':checked') ).trigger('change');
		},

		/**
		 * Auto-check the 'custom' radio when user focuses in to date/time field
		 *
		 * @returns {void}
		 */
		toggleFutureSchedule: function () {
			this.scope.find('[name="promote_schedule"][value="custom"]').prop('checked', true).trigger('change');
		},

		/**
		 * Dynamically update schedule button text based on selected schedule
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		changeSchedule: function () {
			var val = this.scope.find('[name="promote_schedule"]:checked').val();
			var newString = '';

			switch (val) {
				case 'now':
					newString = ips.getString('promoteImmediate');
				break;
				case 'auto':
					newString = ips.getString('promoteAuto');
				break;
				case 'custom':
					newString = ips.getString('promoteCustom');
				break;
			}

			this.scope.find('[data-role="promoteSchedule"]').text( newString );
		},

		/**
		 * Resizes the mymedia content area to be the correct height for the dialog
		 *
		 * @returns	{void}
		 */
		resizeContentArea: function () {

			if( !this.scope.closest('.ipsDialog').length ){
				return;
			}
			
			// Get size of dialog content
			var dialogHeight = this.scope.closest('.ipsDialog_content').outerHeight();
			var controlsHeight = this.scope.find('.cPromoteSubmit').outerHeight();

			// Set the content area to that height
			this.scope.find('[data-role="promoteDialogBody"]').css({
				paddingBottom: controlsHeight + 30 + 'px',
				height: ( dialogHeight  - 80 ) + 'px',
				overflow: 'auto'
			});
		},
		
		/**
		 * Event to update counter
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		changeCounter: function (e) {
			this.updateCounter( $(e.currentTarget) );
		},
		
		/**
		 * Change the characters remaining box
		 *	
		 * @param 		{object} 	object 		Object
		 * @returns 	{void}
		 */
		updateCounter: function (object) {
			var counter = this.scope.find('[data-count-field="' + object.attr('name') + '"]' );
			var count = parseInt( counter.attr('data-limit') ) - parseInt( object.val().length );
			
			/* Twitter auto links and counts each autolink as 23 characters */
			if ( object.attr('name').match( /twitter/ ) ) {
				var links = linkify.find( object.val() );
				if ( links.length ) {
					$( links ).each( function( k, link ) {
						if ( link.type == 'url' ) {
							count += link.value.length;
							count -= 23;
						}
					} );
				}
			}
			
			// Update
			counter.text( count ).removeClass('ipsType_negative ipsType_issue');

			if( count <= 0 ){
				counter.addClass('ipsType_negative');
			} else if( count < 15 ){
				counter.addClass('ipsType_issue');
			}
		}

	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/promote" javascript_name="ips.system.promoteList.js" javascript_type="controller" javascript_version="107643" javascript_position="1000350">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.promote.js - Promotion controller
 *
 * Author: Matt Mecham
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.system.promoteList', {

		initialize: function () {
			this.on( 'click', '[data-action=&quot;delete&quot;]', this.delete );
		},
		
		/**
		 * Event handler for clicking a delete button
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		delete: function (e) {
			var a = $( e.currentTarget );
			
			ips.ui.alert.show({
				type: 'confirm',
				message: ips.getString('promote_confirm_delete'),
				subText: ips.getString('promote_confirm_delete_desc'),
				icon: 'info',
				callbacks: {
					ok: function () {
						window.location = a.attr('href');
					}
				}
			});
			
			return false;
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/search" javascript_name="ips.search.filters.js" javascript_type="controller" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.search.filters.js - Filters form for search
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.search.filters', {

		initialize: function () {
			this.on( 'click', '[data-action="showFilters"]', this.showFilters );
			this.on( 'click', '[data-action="searchByTags"]', this.toggleSearchFields );
			this.on( 'click', '[data-action="searchByAuthors"]', this.toggleSearchFields );
			this.on( 'click', '[data-action="cancelFilters"]', this.cancelFilters );
			this.on( 'change', 'input[name="type"]', this.toggleFilterByCounts );
			this.on( 'itemClicked.sideMenu', '[data-filterType="dateCreated"]', this.filterDate );
			this.on( 'itemClicked.sideMenu', '[data-filterType="dateUpdated"]', this.filterDate );
			this.on( 'itemClicked.sideMenu', '[data-filterType="joinedDate"]', this.filterDate );
			this.on( 'change', '[name^="search_min_"]', this.changeValue );
			this.on( 'tokenDeleted tokenAdded', this.tokenChanged );
			this.on( 'resultsLoading.search', this.resultsLoading );
			this.on( 'resultsDone.search', this.resultsDone );
			this.on( 'cancelResults.search', this.cancelResults );
			this.on( 'submit', this.submitForm );
			this.on( 'tabShown', this.tabShown );
			this.on( 'nodeInitialValues', this.setup );
			
			if( !this.scope.find('[data-role="hints"] ul li').length ){
				this.scope.find('[data-role="hints"]').hide();
			}
			
			this.setup();
		},

		setup: function () {
			var data = this.scope.find('form').serializeArray();

			this.trigger( 'initialData.search', {
				data: data
			});

			this.toggleFilterByCounts();
		},
		
		/**
		 * Remove the "Filter by number of..." header as the form toggles takes care of the rest
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		toggleFilterByCounts: function () {
			var type = this.scope.find('input[name="type"]:checked').val();
			
			if ( !type ) {
				$('#elSearch_filter_by_number').hide();
			} else {
				$('#elSearch_filter_by_number').show();
			}
		},

		/**
		 * Triggered by search wrapper, resets search form back to initial state (i.e. no results showing)
		 *
		 * @returns 	{void}
		 */
		cancelResults: function () {
			this.showFilters();
			this.scope.find('[data-role="hints"]').remove();
			this.scope.find('#elMainSearchInput').val('').focus();
			this.scope.find('[data-action="cancelFilters"], [data-action="searchAgain"]').hide();
		},
		
		/**
		 * Event handler watching for changes on 'minimum' search fields. Shows a bubble
		 * when a positive value is applied.
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		changeValue: function (e) {
			var field = $( e.currentTarget ); 
			var name = field.attr('name');
			var bubble = this.scope.find('[data-role="' + name + '_link"] [data-role="fieldCount"]');

			if( field.val() == 0 ){
				bubble.text('0').addClass('ipsHide');
			} else {
				bubble.text( field.val() ).removeClass('ipsHide');
			}
		},

		/**
		 * Watches for token changes in tags field so we can show the and/or option
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from autocomplete
		 * @returns 	{void}
		 */
		tokenChanged: function (e, data) {
			var tags = this.scope.find('input[name="tags"]');
			var term = this.scope.find('input[name="q"]');
			var andOr = this.scope.find('[data-role="searchTermsOrTags"]');
			
			// If we have a term and a token, show the and/or radios, otherwise hide them
			if( tags.val() && term.val() && !andOr.is(':visible') ){
				andOr.slideDown();
			} else if ( ( !tags.val() || !term.val() ) && andOr.is(':visible') ){
				andOr.slideUp();
			}
		},

		/**
		 * Watches for tab changes. When the 'member search' tab is focused, we select the
		 * hidden radio box that sets search to members
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from tab widget
		 * @returns 	{void}
		 */
		tabShown: function (e, data) {			
			if( data.tabID == 'elTab_searchMembers' ){
				this.scope
					.find('input[name="type"][value="core_members"]')
						.prop( 'checked', true )
						.change()
					.end()
					.find('[data-action="updateResults"]')
						.text( ips.getString('searchMembers') );
			} else {
				this.scope
					.find('[data-role="searchApp"] .ipsSideMenu_itemActive input[type="radio"]')
						.prop( 'checked', true )
						.change()
					.end()
					.find('[data-action="updateResults"]')
						.text( ips.getString("searchContent") );
			}
		},

		/**
		 * Hides filters
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		cancelFilters: function (e) {
			var self = this;
			this.scope.find('[data-role="searchFilters"]').slideUp('fast', function () {
				self.scope.find('[data-action="showFilters"]').slideDown();
			});
		},

		/**
		 * Shows advanced filters
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		showFilters: function (e) {
			if( e ){
				e.preventDefault();
			}

			this.scope.find('[data-action="showFilters"]').hide();
			this.scope.find('[data-role="searchFilters"]').slideDown();

			/* We do this so the form toggles (i.e. show forums if you choose to search in forums content type) will reinitialize */
			$(document).trigger('contentChange', [ this.scope ] );
		},

		/**
		 * Event handler from main controller indicating results have loaded
		 * Remove loading mode, and hide the filters
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from main controller
		 * @returns 	{void}
		 */
		resultsDone: function (e, data) {
			var searchButton = this.scope.find('[data-action="updateResults"]');

			// Reset loading state
			searchButton.prop( 'disabled', false ).text( searchButton.attr('data-originalText') );
			
			// Hide filters
			this.scope.find('[data-role="searchFilters"]').hide();
			// Unhide 'more options' link
			this.scope.find('[data-action="showFilters"]').removeClass('ipsHide').show();
			// Unhide 'search again' button
			this.scope.find('[data-action="searchAgain"]').removeClass('ipsHide ipsButton_disabled').show();
			
			if( ! _.isUndefined( data.hints ) ){
				this.scope.find('[data-role="hints"]').html( data.hints ).show();
			}
			
			if( ! this.scope.find('[data-role="hints"] ul li').length ){
				this.scope.find('[data-role="hints"]').hide();
			}
			
			$( document ).trigger( 'contentChange', [ this.scope ] );
		},

		/**
		 * Event handler from main controller indicating results are loading
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from main controller
		 * @returns 	{void}
		 */
		resultsLoading: function (e, data) {
			var searchButton = this.scope.find('[data-action="updateResults"]');

			this.scope.find('[data-action="searchAgain"]').addClass('ipsButton_disabled');
			searchButton.prop( 'disabled', true ).attr( 'data-originalText', searchButton.text() ).text( ips.getString("searchFetchingResults") );
		},

		/**
		 * Event handlers for tags/author links to show a form filter
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		toggleSearchFields: function (e) {
			e.preventDefault();
			var link = $( e.currentTarget );
			var opens = link.attr('data-opens').split(',');
			var i;
			
			for( i = 0; i < opens.length; i++ ) {
				this.scope.find('[data-role="' + opens[i] + '"]').slideDown( function () {
					if( !link.closest('ul').find('li').length ){
						link.closest('ul').remove();
					}
	
					$( this ).find('input[type="text"]').focus();
				});
			}

			link.closest('li').hide();
		},

		/**
		 * Event handler for date filters, showing date fields when 'custom' is selected
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from side meun widget
		 * @returns 	{void}
		 */
		filterDate: function (e, data) {
			var elem = $( e.currentTarget );

			if( data.selectedItemID == 'custom' ){
				elem.find('[data-role="dateForm"]').slideDown();
			} else {
				elem.find('[data-role="dateForm"]').slideUp();
			}
		},

		/**
		 * Event handler for submitting the form. Triggers an event containing the data which
		 * the main controller will handle
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		submitForm: function (e) {
			e.preventDefault();

			// Make sure keyboard is hidden
			this.scope.find('#elMainSearchInput').blur();

			var self = this;
			var app = this.scope.find('[data-role="searchApp"] .ipsSideMenu_itemActive');
			var appKey = app.attr('data-ipsMenuValue');
			var appTitle = app.find('[data-role="searchAppTitle"]').text();
			var isMemberSearch = $('#elTab_searchMembers').hasClass('ipsTabs_activeItem');
			
			// Make sure we have at least one key field entered (a term, or tags)
			var searchTerm = this.scope.find('#elMainSearchInput').val().trim();
			var tagExists = ( this.scope.find('#elInput_tags').length && this.scope.find('#elTab_searchContent').hasClass('ipsTabs_activeItem') );

			if( tagExists ){
				var tagField = ips.ui.autocomplete.getObj( this.scope.find('#elInput_tags') );
				var tokens = tagField.getTokens();
			}
			
			if ( ! isMemberSearch )
			{
				if( !searchTerm && !tagExists || !searchTerm && tagExists && tokens.length === 0 ){
					ips.ui.alert.show( {
						type: 'alert',
						message: ( !searchTerm && !tagExists ) ? ips.getString('searchRequiresTerm') : ips.getString('searchRequiresTermTags'),
						icon: 'info',
						callbacks: {
							ok: function () {
								setTimeout( function () {
									self.scope.find('#elMainSearchInput').focus();
								}, 300 );
							}
						}
					});
					return;
				}
			}
			
			// Everything good? Trigger the event for the main controller to handle
			this.trigger( 'formSubmitted.search', {
				data: this.scope.find('form').serializeArray(),
				appKey: appKey,
				tabType: this.scope.closest('data-tabType').attr('data-tabType'),
				appTitle: appTitle
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/search" javascript_name="ips.search.main.js" javascript_type="controller" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.search.main.js - Main search JS controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.search.main', {

		_content: null,
		_formData: {},
		_loadingDiv: null,
		_initialURL: '',
		_initialData: {},

		initialize: function () {
			this.on( 'initialData.search', this.initialData );
			this.on( 'formSubmitted.search', this.submittedSearch );
			this.on( 'paginationClicked paginationJump', this.paginationClicked );

			// Primary event that watches for URL changes
			History.Adapter.bind( window, 'statechange', _.bind( this.stateChange, this ) );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this._content = this.scope.find('#elSearch_main');
			this._baseURL = this.scope.attr('data-baseURL');

			if ( this._baseURL.match(/\?/) ) {
				this._baseURL += '&';
			} else {
				this._baseURL += '?';
			}

			// If the last character is &, we can remove that because it'll be added back later
			if( this._baseURL.slice(-1) == '&' ){
				this._baseURL = this._baseURL.slice( 0, -1)
			}

			this._initialURL = window.location.href;
		},

		/**
		 * Filters have sent up their initial data
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		initialData: function (e, data) {
			this._formData = this._getFormData( data.data );
			this._initialData = _.clone( this._formData );
		},

		/**
		 * Main state change event handler that responds to URL changes
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		stateChange: function () {
			var state = History.getState();

			if( ( !state.data.controller || state.data.controller != 'core.front.search.main' ) && this._initialURL !== state.url ){
				return;
			}

			if( this._initialURL == state.url && !_.isUndefined( state.data ) && !_.size( state.data ) ){
				// If our URLs match but we have no state data, we can assume we've gone back to the intital search form, so let's reset
				this._cancelSearch();
			} else if( this._initialURL == state.url && _.isUndefined( state.data.url ) ){
				// If we don't have a URL, get it from our initial data
				this._loadResults( this._getUrlFromData( this._initialData ) );
			} else {
				// Otherwise use the state url
				this._loadResults( state.data.url );	
			}			
		},

		/**
		 * Responds to event from filters indicating the filter form has been submitted
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		submittedSearch: function (e, data) {
			this._formData = this._getFormData( data.data );
			var url = this._getUrlFromData( this._formData );

			/* DataLayer event */
			try {
				if ( IpsDataLayerContext && !window.IpsDataLayerContext ) {
					let unique = '';
					let self = this;
					Object.keys( this._formData ).sort().forEach(function (index) {
						let value = self._formData[index];
						if (value.join) {
							value = value.join();
						}
						if (value.toString) {
							value = value.toString();
						}
						if (value && (typeof value === 'string')) {
							unique += value;
						}
					});

					$('body').trigger('ipsDataLayer', {
						_key: 'search',
						_properties: {
							query: this._formData.q || null
						},
						_uniquekeys: {'key':unique}
					});
				}
			} catch (e) {}

			History.pushState( {
				controller: 'core.front.search.main',
				url: url,
				filterData: this._formData
			}, this._getBrowserTitle(), url );	
		},

		/**
		 * Cancel the search results and reset the form
		 *
		 * @returns 	{void}
		 */
		_cancelSearch: function () {
			var results = this.scope.find('[data-role="filterContent"]');
			var blurb = this.scope.find('[data-role="searchBlurb"]');

			// Remove controller/widgets in results area before we empty it
			ips.controller.cleanContentsOf( results );

			// Tell filters to reset
			this.triggerOn( 'core.front.search.filters', 'cancelResults.search' );

			// Empty relevant elements
			results.html('');
			blurb.html('').hide();
		},

		/**
		 * Builds a search URL from the provided data
		 *
		 * @param 		{object} 	data 	Object containing search data
		 * @returns 	{string} 	url
		 */
		_getUrlFromData: function (data) {
			var params = [];

			// Basic params
			_.each( ['q', 'type', 'page', 'quick'], function (val) {
				if( !_.isUndefined( data[ val ] ) && data[ val ] !== '' ){
					params.push( val + '=' + encodeURIComponent( data[ val ] ) );
				}
			});

			// Are we searching content or members?
			if( data['type'] == 'core_members' ){

				// Joined date
				if( !_.isUndefined( data['joinedDate'] ) ){
					if( data['joinedDate'] !== 'custom' ){
						params.push( 'joinedDate=' + data['joinedDate'] );
					} else {
						if( !_.isUndefined( data['joinedDateCustom[start]'] ) ){
							params.push( 'start_after=' + encodeURIComponent( new Date( data['joinedDateCustom[start]'] ).getTime() / 1000 ) );	
						}
						if( !_.isUndefined( data['joinedDateCustom[end]'] ) ){
							params.push( 'start_before=' + encodeURIComponent( new Date( data['joinedDateCustom[end]'] ).getTime() / 1000 ) );	
						}						
					}
				}

				// Member group
				if( !_.isUndefined( data['group'] ) ){
					if( !_.isArray( data['group'] ) ){
						data['group'] = [ data['group'] ];
					}

					for( var i = 0; i < data['group'].length; i++ ){
						params.push( 'group[' + data['group'][ i ] + ']=1' );
					}
				}

				// Custom profile fields
				_.each( data, function (val, key){
					if( !key.startsWith('core_pfield') || val === 0 || val === '' ){
						return;
					}

					params.push( key + '=' + val );
				});
				
			} else {
				// Content-specific basic params
				_.each( ['item', 'author', 'search_min_replies', 
						'search_min_views', 'search_min_comments', 'search_min_reviews'], function (val) {
					if( !_.isUndefined( data[ val ] ) && data[ val ] !== '' && parseInt( data[ val ] ) !== 0 ){
						if( val === 'author' )
						{
							// Author names need treating slightly differently, since they may contain some HTML entities
							params.push( val + '=' + encodeURIComponent( data[ val ] ) );
							return;
						}
						params.push( val + '=' + data[ val ] );
					}
				});

				if( !_.isUndefined( data['tags'] ) ){
					params.push( 'tags=' + data['tags'].replace(/\n/g, ',') );
				}

				// Are we searching nodes?
				if( !_.isUndefined( data['nodes'] ) ){
				    params.push( 'nodes=' + data['nodes'].replace(/\n/g, ',') );
				}
				else if( !_.isUndefined( data[ data['type'] + '_node' ] ) ){
					params.push( 'nodes=' + data[ data['type'] + '_node' ] );
				}
				if( !_.isUndefined( data['club[]'] ) ){
					if ( _.isArray( data['club[]'] ) ) {
						params.push( 'club=' + data['club[]'].filter(function(v){
							return v != '__EMPTY';
						}));
					} else if ( data['club[]'].replace( '__EMPTY', '' ) ) {
						params.push( 'club=' + data['club[]'].replace( '__EMPTY', '' ) );
					}
				}

				// Only include eitherTermsOrTags if there's a term AND some tags
				if( !_.isUndefined( data['eitherTermsOrTags'] ) ){
					if( !_.isUndefined( data['q'] ) && data['q'].trim() !== '' && !_.isUndefined( data['tags'] ) && data['tags'].trim() !== '' ){
						params.push( 'eitherTermsOrTags=' + data['eitherTermsOrTags'] );
					}
				}

				// Only include search_and_or if its 'or' or 'and'
				if( !_.isUndefined( data['search_and_or'] ) && ( data['search_and_or'] == 'or' || data['search_and_or'] == 'and' ) ){
					params.push( 'search_and_or=' + data['search_and_or'] );
				}
				
				// Only include search_in if its 'title'
				if( !_.isUndefined( data['search_in'] ) && data['search_in'] == 'titles' ){
					params.push( 'search_in=' + data['search_in'] );
				}

				// Date params
				var datesSet = { startDate: false, updatedDate: false };
				_.each( [ ['startDate', 'start_after'], ['updatedDate', 'updated_after'] ], function (val) {
					if( !_.isUndefined( data[ val[0] ] ) ){
						if( data[ val[0] ] !== 'any' && data[ val[0] ] !== 'custom' ){
							params.push( val[1] + '=' + data[ val[0] ] );

							datesSet[ val[0] ] = true;
						} else if( data[ val[0] ] === 'any' ) {
							datesSet[ val[0] ] = true;
						}
					}
				});

				// Custom date param
				_.each( [ ['startDateCustom[start]', 'start_after'], ['startDateCustom[end]', 'start_before'],
						['updatedDateCustom[start]', 'updated_after'], ['updatedDateCustom[end]', 'updated_before'] ], function (val) {
					var thisType = ( val[0].indexOf('startDate') != -1 ) ? 'startDate' : 'updatedDate';
					if( !_.isUndefined( data[ val[0] ] ) && !datesSet[ thisType ] ){
						// If we have selected 'any' for dates', do not add these
						if ( ( val[0] == 'startDateCustom[start]' || val[0] == 'startDateCustom[end]' ) && !_.isUndefined( data['startDate'] ) && data['startDate'] == 'any' ) {
							// Do nothing
						} else if ( ( val[0] == 'updatedDateCustom[start]' || val[0] == 'updatedDateCustom[end]' ) && !_.isUndefined( data['updatedDate'] ) && data['updatedDate'] == 'any' ) {
							// Do nothing
						} else {
							// We have to pass the form field to getDateFromInput() to account for polyfill
							params.push( val[1] + '=' + encodeURIComponent( ips.utils.time.getDateFromInput( $('[name="' + val[0] + '"]') ).getTime() / 1000 ) );
						}
					}
				});
			}
			
			// Sort
			if( !_.isUndefined( data['sortby'] ) ){
				params.push( 'sortby=' + data['sortby'] );
			}
			
			if( !_.isUndefined( data['sortdirection'] ) ){
				params.push( 'sortdirection=' + data['sortdirection'] );
			}

			return this._baseURL + '&' + params.join('&');
		},

		/**
		 * Main method to load new results from the server
		 *
		 * @param 		{string} 	url 				URL to load
		 * @param 		{boolean} 	showFiltersLoading 	Show the loading indicator on the filter bar?
		 * @returns 	{void}
		 */
		_loadResults: function (url ) {
			var self = this;

			this.triggerOn( 'core.front.search.filters', 'resultsLoading.search' );
			this._setContentLoading( true );

			ips.getAjax()( url )
				.done( function (response) {
									
					if ( typeof response !== 'object' ) {
						window.location = url;
					}
					
					if( response.css ){
						self._addCSS( response.css );	
					}

					// Hide filters
					self.triggerOn( 'core.front.search.filters', 'resultsDone.search', {
						contents: response.filters,
						hints: response.hints
					});	

					// Update content
					self._content.html( response.content );
					$( document ).trigger( 'contentChange', [ self._content ] );

					// Update title
					self.scope.find('[data-role="searchBlurb"]').show().html( response.title );

					// Make sure cancel button is shown
					self.scope.find('[data-action="cancelFilters"]').show();
					
					// Animate new items
					var newItems = self.scope.find('[data-role="resultsArea"] [data-role="activityItem"]').css({
						opacity: "0"
					});
					var delay = 100;

					// Slide down to make space for them
					newItems.slideDown( function () {
						// Now fade in one by one, with a delay
						newItems.each( function (index) {
							var d = ( index * delay );
							$( this ).delay( ( d > 1200 ) ? 1200 : d ).animate({
								opacity: "1"
							});
						});	
					});
				})
				.fail( function (jqXHR, textStatus) {
					window.location = url;
				})
				.always( function () {
					self._setContentLoading( false );
				});
		},

		/**
		 * Responds to pagination event in conversation
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object	
		 * @returns 	{void}
		 */
		paginationClicked: function (e, data){
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}		

			this._formData['page'] = data.pageNo;
						
			var url = this._getUrlFromData( this._formData );

			History.pushState( {
				controller: 'core.front.search.main',
				url: url
			}, document.title, url );
			
			var elemPosition = ips.utils.position.getElemPosition( this.scope );
			$('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' } );
		},

		/**
		 * Returns form data, converting jquery serializedArray objects into
		 * simple key/value pairs
		 *
		 * @param 		{object} 	data 	Form data object
		 * @returns 	{object}
		 */
		_getFormData: function (data) {
			if( !_.isObject( data ) ){
				return;
			}

			var returnData = {};
			var skipData = [ 'page', 'csrfKey' ];

			for( var i = 0; i < data.length; i++ ){
				if( _.indexOf( skipData, data[ i ].name ) === -1 && data[ i ].value !== '' ){

					// If we already have a value set for this param and it isn't an array, juggle it
					// so that it is an array and push the existing value into it
					if( !_.isUndefined( returnData[ data[ i ].name ] ) && !_.isArray( returnData[ data[ i ].name ] ) ){
						var tmp = returnData[ data[ i ].name ];
						returnData[ data[ i ].name ] = [];
						returnData[ data[ i ].name ].push( tmp );
					}

					// If we're an array, push it into it, otherwise just set the value
					if( !_.isUndefined( returnData[ data[ i ].name ] ) ){
						returnData[ data[ i ].name ].push( data[ i ].value );
					} else {
						returnData[ data[ i ].name ] = data[ i ].value;	
					}
					
					// Unlimited checkbox? Overwrite value
					if ( data[i].name != 'club[]' ) {
						if ( $('#' + data[i].name + '-unlimitedCheck').length )
						{
							if ( ! $('#' + data[i].name + '-unlimitedCheck:checked').length )
							{
								returnData[ data[ i ].name ] = $('input[type=number][name=' + data[ i ].name + ']').val();
							}
							else
							{
								delete( returnData[ data[ i ].name ] );
							}
						}
					}
				}
			}
			
			if( ! _.isUndefined( data['type'] ) && data['type'] != 'core_members' ){
				if ( ! _.isUndefined( data['sortby'] ) ){
					delete( data['sortby'] );
				}
				
				if ( ! _.isUndefined( data['sortdirection'] ) ){
					delete( data['sortdirection'] );
				}
			}

			return returnData;
		},

		/**
		 * Builds a string to use in the browser titlebar
		 *
		 * @returns 	{string}
		 */
		_getBrowserTitle: function () {
			var title = ips.getString('searchTitle');
			var currentType = this.scope.find('input[type="radio"][name="type"]:checked');
			var q = this._formData['q'];
			if ( _.isUndefined( q ) ) {
				q = '';
			}
			
			if( q !== '' && !currentType.length ){
				title = ips.getString('searchTitleTerm', {
					term: q
				});
			} else if( q !== '' && currentType.length ){
				title = ips.getString('searchTitleTermType', {
					term: q,
					type: currentType.next('[data-role="searchAppTitle"]').text()
				});
			} else if( q === '' && this._currentType !== '' ){
				title = ips.getString('searchTitleType', {
					type: currentType.next('[data-role="searchAppTitle"]').text()
				});
			}
			
			return title;
		},

		/**
		 * Adds css files to the page header
		 *
		 * @param 		{array} 	css 	Array of CSS urls to add
		 * @returns 	{void}
		 */
		_addCSS: function (css) {
			var head = $('head');

			if( css && css.length ){
				for( var i = 0; i < css.length; i++ ){
					head.append( $('<link/>').attr( 'href', css[i] ).attr( 'type', 'text/css' ).attr('rel', 'stylesheet') );
				}
			}
		},

		/**
		 * Toggles the loading state on the main search body area
		 *
		 * @param 		{boolean} 	showLoading 		Enable loading state?
		 * @returns 	{void}
		 */
		_setContentLoading: function (state) {
			var results = this.scope.find('[data-role="resultsContents"]');

			if( !results.length ){
				if( this._loadingDiv ){
					this._loadingDiv.hide();
				}

				return;
			}

			var dims = ips.utils.position.getElemDims( results );
			var position = ips.utils.position.getElemPosition( results );
			

			if( !this._loadingDiv ){
				this._loadingDiv = $('<div/>').append( 
					$('<div/>')
						.css({
							height: _.min( [ 200, results.outerHeight() ] ) + 'px'
						})
						.addClass('ipsLoading')
				);

				ips.getContainer().append( this._loadingDiv );
			}

			this._loadingDiv
				.show()
				.css({
					left: position.viewportOffset.left + 'px',
					top: position.viewportOffset.top + $( document ).scrollTop() + 'px',
					width: dims.width + 'px',
					height: dims.height + 'px',
					position: 'absolute',
					zIndex: ips.ui.zIndex()
				})
				

			if( state ){
				results
					.animate({
						opacity: "0.6"
					})
					.css({
						height: results.height() + 'px'
					});				
			} else {
				results.css({
					height: 'auto',
					opacity: "1"
				});

				this._loadingDiv.hide();
			}	
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/search" javascript_name="ips.search.results.js" javascript_type="controller" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.search.results.js - Search results controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.search.results', {

		_resultLength: 300,
		_terms: [],

		initialize: function () {
			this.setup();
			this.on( document, 'contentChange', _.bind( this.contentChange, this ) );
		},

		/**
		 * Setup methods. Adds case-insensitive jQuery expression.
		 *
		 * @param 		{string} 	term 	The word(s) to highlight
		 * @returns 	{void}
		 */
		setup: function () {
			// Add case-insensitive contains expression to jquery
			jQuery.expr[':'].icontains = function(a, i, m) {
			  return jQuery(a).text().toUpperCase()
			      .indexOf(m[3].toUpperCase()) >= 0;
			};

			var self = this;

			try {
				this._terms = JSON.parse( this.scope.attr('data-term') );
			} catch(err) {
				Debug.log("Error parsing search terms");
				return;
			}

			// Process each result
			this.scope.find('[data-role="activityItem"]').each( function () {
				self._processResult( $( this ) );
			});
		},

		/**
		 * Event handler for document content change
		 *
		 * @returns 	{void}
		 */
		contentChange: function () {
			var self = this;

			this.scope.find('[data-role="activityItem"]').each( function () {
				self._processResult( $( this ) );
			});
		},

		/**
		 * Processes the given element as a search result
		 *
		 * @param 		{element} 	result 		The search result element
		 * @returns 	{void}
		 */
		_processResult: function (result) {
			// Don't process it twice
			if( result.attr('data-processed') ){
				return;
			}

			// Start by locating the first hit
			var findWords = result.find('[data-findTerm]');

			if( findWords.length ){
				this._findWords( findWords );
			}

			// Now highlight the terms
			this._highlight( result );

			result.attr('data-processed', true);
		},


		/**
		 * Takes a search result, and finds the match within it and then reduces the text
		 * to just the characters surrounding the match
		 *
		 * @param 		{element} 	result 		The element containing the text to work on
		 * @returns 	{void}
		 */
		_findWords: function (result) {
			var text = result.text().trim();
			var firstMatch = text.length;
			var startPoint = 0;
			var foundMatches = false;

			//-----------
			// Step 1: Find the first occurrence of each term
			for( var i = 0; i < this._terms.length; i++){

				// Note: regexp used here because simple indexOf isn't case insensitive
				// and toLowercase doesn't work well with some languages
				var indexOf = text.search( new RegExp( ips.utils.escapeRegexp( this._terms[i] ), 'i' ) );

				if( indexOf !== -1 ){
					foundMatches = true;

					if( indexOf < firstMatch ){
						firstMatch = indexOf;
					}
				}
			}

			//-----------
			// Step 2: Search backwards to find the closest puncutation mark, which is where we'll start our result snippet
			// We'll go back up to half of our result length, but stop if we hit the beginning
			var punctuationMarks = ['.', ',', '?', '!'];
			var searchBack = ( firstMatch - ( this._resultLength / 2 ) < 0 ) ? 0 : firstMatch - ( this._resultLength / 2 ); 

			// if there were no matches, then we'll just set manual values
			if( !foundMatches ){
				startPoint = 0;
			} else {
				for( var j = firstMatch; j > searchBack; j-- ){
					if( punctuationMarks.indexOf( text[j] ) !== -1 ){
						startPoint = j + 1;
						break;
					}
				}
			}

			//-----------
			// Step 3: Count forward from the starting point to get our snippet
			var finalSnippet = text.substring( startPoint, startPoint + 300 ).trim();

			if( startPoint > 0 && foundMatches ){
				finalSnippet = '...' + finalSnippet;
			}

			if( startPoint + this._resultLength < text.length || ( !foundMatches && text.length > this._resultLength ) ){
				finalSnippet = finalSnippet + '...';
			}

			result.text( finalSnippet );
		},

		/**
		 * Highlight search results
		 *
		 * @param 		{string} 	term 	The word(s) to highlight
		 * @returns 	{void}
		 */
		_highlight: function (result) {
			// Find the elements we're searching in
			var self = this;
			var elements = result.find('[data-searchable]');
			
			_.each( this._terms, function (term, index) {
				elements.each( function () {
					if( !$( this ).is(':icontains("' + term + '")' ) ){
						return;
					}
										
					$( this ).contents().filter(
						function() { return this.nodeType === 3 }
					).each( function(){
						$( this ).replaceWith( _.escape( XRegExp.replace( $( this ).text(), new RegExp( "(\\b|\\s|^)(" + term + "\\w*)(\\b|\\s|$)", "ig" ), '<mark class="ipsMatch' + ( index + 1 ) + '">' + "$2 " + '</mark>' ) ).replace( new RegExp("&lt;mark class=&quot;ipsMatch" + ( index + 1 ) + "&quot;&gt;", 'ig'), " <mark class='ipsMatch" + ( index + 1 ) + "'>" ).replace( new RegExp("&lt;/mark&gt;", 'ig'), "</mark>" ) );
					} );
				});
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/settings" javascript_name="ips.settings.advanced.js" javascript_type="controller" javascript_version="107643" javascript_position="1000350">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.settings.advanced.js
 *
 * Author: Brandon Farber
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.settings.advanced', {
		defaultStatus: false,

		initialize: function () {
			this.defaultStatus = $( '#check_lazy_load_enabled' ).is(':checked');
			this.on( 'change', '#check_lazy_load_enabled', this.promptRebuildPreference );
		},

		promptRebuildPreference: function (e) {
			/* Do not prompt if the setting is toggled to the default */
			if( this.defaultStatus == $( '#check_lazy_load_enabled' ).is(':checked') )
			{
				/* Disable any rebuild process */
				$('input[name=rebuildPosts]').val( 0 );
				return;
			}

			/* Show Rebuild Prompt */
			ips.ui.alert.show({
				type: 'confirm',
				message: ips.getString('imageProxyRebuild'),
				subText: $( '#check_lazy_load_enabled' ).is(':checked') ? ips.getString('imageLazyLoadRebuildBlurbEnable') : ips.getString('imageLazyLoadRebuildBlurbDisable'),
				icon: 'question',
				buttons: {
					ok: ips.getString('imageProxyRebuildYes'),
					cancel: ips.getString('imageProxyRebuildNo')
				},
				callbacks: {
					ok: function(){
						$('input[name=rebuildPosts]').val( 1 );
					},
					cancel: function(){
						$('input[name=rebuildPosts]').val( 0 );
					}
				}
			});
		}

	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/stats" javascript_name="ips.stats.filtering.js" javascript_type="controller" javascript_version="107643" javascript_position="1000400">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.stats.filtering.js - Statistics filtering controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.stats.filtering', {

		initialize: function () {
			this.on( 'click', '[data-role=&quot;toggleGroupFilter&quot;]', this.toggleGroupFilter );

			// And hide by default
			if( $('#elGroupFilter').attr('data-hasGroupFilters') == 'true' )
			{
				$('#elGroupFilter').show();
			}
		},

		/**
		 * Toggle filtering by groups
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		toggleGroupFilter: function (e) {
			e.preventDefault();
			
			if( $('#elGroupFilter').is(':visible') )
			{
				// If we are hiding the filter, we will assume they want to search everything and ensure all checkboxes are checked
				$('#elGroupFilter').find('input[type=&quot;checkbox&quot;]').prop('checked', true);
				$('#elGroupFilter').slideUp();
			}
			else
			{
				$('#elGroupFilter').slideDown();
			}
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/stats" javascript_name="ips.stats.nodeFilters.js" javascript_type="controller" javascript_version="107643" javascript_position="1000400">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.stats.overviewBlock.js - Overview statistics block controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.stats.nodeFilters', {

		url: null,
		dateFilters: { 'start': null, 'end': null, 'range': null },

		initialize: function () {
			this.on( 'submit', this.reloadBlock );

			$(document).on( 'stats.setDateFilters', _.bind( this.statsReady, this ) );
			$(document).on( 'click.hovercard', _.bind( function(){ this.trigger( 'reloadStatsDateFilters' ); }, this ) );

			this.trigger( 'reloadStatsDateFilters' );
		},

		/**
		 * Stats overview is ready, so store the values we need
		 *
		 * @param	{e}		event	Event
		 * @param	{data}	object	Data
		 * @returns {void}
		 */
		 statsReady: function( e, data ) {
			this.url = data.url;
			this.dateFilters = data.dateFilters;
		 },

		/**
		 * Store the nodes we want to filter by and reload the block
		 *
		 * @param	{e} 	event	 Submit event
		 * @returns {void}
		 */
		reloadBlock: function (e) {
			var blockKey = $(e.currentTarget).attr('data-block');
			var subblock = $(e.currentTarget).attr('data-subblock');

			var blockElement = $( '[data-role=&quot;statsBlock&quot;][data-block=&quot;' + blockKey + '&quot;][data-subblock=&quot;' + subblock.replace( /\\/g, '\\\\' ) + '&quot;]' );

			blockElement.attr( 'data-nodeFilter', $(e.currentTarget).find('[data-role=&quot;nodeValue&quot;]').val() );

			e.preventDefault();
			e.stopPropagation();

			this.trigger( 'stats.nodeFilters', {
				blockToRefresh: blockKey,
				subblockToRefresh: subblock,
				url: this.url,
				dateFilters: this.dateFilters
			} );

			// Close the hovercard
			$(document).trigger('click');
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/stats" javascript_name="ips.stats.overview.js" javascript_type="controller" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.stats.overview.js - Overview statistics controller
 *
 * Author: Brandon Farber
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.stats.overview', {

		dateFilters: { 'start': null, 'end': null, 'range': null },

		initialize: function () {
			this.on( 'submit', '[data-role="dateFilter"]', this.updateDateFilter );
			this.on( 'change', '[data-action="toggleApp"]', this.toggleApp );
			this.on( 'change', '[name="predate"]', this.submitForm );
			this.on( 'click', '[data-action="cancelDateRange"]', this.cancelDateRange );
			this.on( 'stats.ready', this.blockReady );
			$(document).on( 'reloadStatsDateFilters', _.bind( this.resendDateFilters, this ) );
			this.setup();
		},

		setup: function () {
			this.url = this.scope.attr('data-url');
			this.dateFilters.range = this.scope.find('[data-role="dateFilter"]').attr('data-defaultRange');
		},

		resendDateFilters: function (e) {
			this.trigger( 'stats.setDateFilters', {
				dateFilters: this.dateFilters,
				url: this.url
			});
		},

		cancelDateRange: function (e) {
			e.preventDefault();
			this.scope.find('select[name="predate"]').val( this.scope.find('.cStatsFilters').attr('data-defaultRange') ).change();
		},

		submitForm: function (e) {
			var select = $( e.currentTarget );

			if( select.val() === '-1' ){
				this.scope.find('.cStatsFilters [data-role="formTitle"], select[name="predate"]').hide();
				this.scope.find('.cStatsFilters button').show();
			} else {
				this.scope.find('.cStatsFilters [data-role="formTitle"], select[name="predate"]').show();
				this.scope.find('.cStatsFilters button').hide();
				select.closest('form').submit();
			}
		},

		/**
		 * A block is ready to be loaded
		 *
		 * @param	{event} 	e		Event
		 * @param 	{object}	data 	Event data
		 * @returns {void}
		 */
		blockReady: function (e, data) {
			$(e.target).trigger('stats.loadBlock', {
				dateFilters: this.dateFilters,
				url: this.url
			});
		},

		/**
		 * Event handler for toggling which apps to see
		 *
		 * @returns {void}
		 */
		toggleApp: function () {
			// Get selected apps
			var enabledApps = _.map( this.scope.find('[data-action="toggleApp"]:checked'), function(app) { 
				return $( app ).attr('data-toggledApp') 
			});
			var disabledApps = _.map( this.scope.find('[data-action="toggleApp"]:not( :checked )'), function (app) {
				return $( app ).attr('data-toggledApp');
			});

			// Select all tiles, except those that are enabled
			this.scope.find('.cStatTile[data-app]').each( function () {
				var tile = $(this);
				if( tile.hasClass('ipsHide') && enabledApps.indexOf( $( this ).attr('data-app') ) !== -1 ){
					tile.css({ transform: 'scale(0.7)', opacity: "0" }).removeClass('ipsHide').animate({ transform: 'scale(1)', opacity: "1" });
				} else if ( enabledApps.indexOf( $( this ).attr('data-app') ) === -1 ){
					tile.addClass('ipsHide');
				}
			});

			// Write the cookie to set disabled apps
			ips.utils.cookie.set('overviewExcludedApps', JSON.stringify( disabledApps ), true );
		},

		/**
		 * Update blocks on the page to use the new date filters
		 *
		 * @param	{event} 	e	Event
		 * @returns {void}
		 */
		updateDateFilter: function (e) {
			e.preventDefault();
			e.stopPropagation();

			this.dateFilters = { 'start': null, 'end': null, 'range': this.scope.find( '[name="predate"]' ).val() };

			// Are we specifying a custom date range?
			if( this.dateFilters.range == '-1' ){
				this.dateFilters.start = this.scope.find( '[name="date[start]"]' ).val();
				this.dateFilters.end = this.scope.find( '[name="date[end]"]' ).val();
			}
			
			this.triggerOn('core.admin.stats.overviewBlock', 'stats.loadBlock', {
				dateFilters: this.dateFilters,
				url: this.url
			});

			this.triggerOn('core.admin.stats.nodeFilters', 'stats.setDateFilters', {
				dateFilters: this.dateFilters,
				url: this.url
			});
		},
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/stats" javascript_name="ips.stats.overviewBlock.js" javascript_type="controller" javascript_version="107643" javascript_position="1000400"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.stats.overviewBlock.js - Overview statistics block controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.stats.overviewBlock', {

		initialize: function () {
			Debug.log('init bloc');
			this.on('stats.loadBlock', this.loadBlock);
			$(document).on( 'stats.nodeFilters', _.bind( this.loadBlock, this ) );
			this.setup();
		},
		
		setup: function () {
			this.refresh = null;
			this.loaded = false;
			this.currentCounts = [];
			this.url = null;
			this.block = this.scope.attr('data-block');
			this.subblock = this.scope.attr('data-subblock');
			this.refreshInterval = this.scope.attr('data-refresh') ? parseInt( this.scope.attr('data-refresh') ) : false;
			this.trigger('stats.ready');
		},

		/**
		 * Return a normalized array of counts this block has shown, used to compare prev/next values
		 *
		 * @param	{element} 	elem	  Element to check for values 
		 * @returns {void}
		 */
		getCounts: function (elem) {
			return _.map( elem.find('[data-number]'), function (thisElem) { return parseInt( $( thisElem ).attr('data-number') ) } );
		},

		startInterval: function () {
			if( !this.refreshInterval || !this.url ){
				return;
			}

			clearInterval( this.refresh );
			this.refresh = setInterval( _.bind( this.fetchUpdate, this ), this.refreshInterval * 1000 );
		},

		fetchUpdate: function () {
			var self = this;

			ips.getAjax()( this.url, {
				type: 'get'
			} )
				.done( function (response) {
					var newContent = $("<div>" + response + "</div>");
					var counts = self.getCounts( newContent );
					var hasDifference = false;

					// Is there a difference?					
					if( counts.length !== self.currentCounts.length ){
						hasDifference = true;
					} else {
						for( var i = 0; i < counts.length; i++ ){
							if( counts[ i ] !== self.currentCounts[ i ] ){
								hasDifference = true;
								break;
							} 
						}
					}

					if( hasDifference ){
						self.scope.addClass('cStatTile--updated').find('[data-role="statBlockContent"]').html( response );
						self.currentCounts = counts;

						setTimeout( function () {
							self.scope.removeClass('cStatTile--updated');
						}, 2200);

						$( document ).trigger( 'contentChange', [ self.scope ] );
					} else {
						Debug.log("No change in values in " + self.block);
					}
				});
		},

		/**
		 * Load the block
		 *
		 * @param	{event} 	e	    Event
		 * @param   {object}    data    Event data
		 * @returns {void}
		 */
		loadBlock: function (e, data) {
			// We might only want to refresh one block, in which case we can skip this
			if( !_.isUndefined( data.blockToRefresh ) && ( data.blockToRefresh != this.block || data.subblockToRefresh != this.subblock ) )
			{
				Debug.log( "Skipping because " + data.blockToRefresh + " does not match " + this.block + " or " + data.subblockToRefresh + " does not match " + this.subblock );
				return;
			}

			var self = this;
			this.url = data.url + '&blockKey=' + this.block;

			clearInterval( this.refresh );
			
			if( this.loaded ){
				this.loaded = false;
				
				this.scope
					.removeClass('cStatTile--loaded')
					.find('.cStatTile__body')
						.addClass('ipsLoading')
					.end()
					.find('[data-role="statBlockContent"]')
						.hide()
						.html('');
			}

			if( !_.isUndefined( this.subblock ) ){
				this.url = this.url + '&subblock=' + this.subblock;
			}

			if( data.dateFilters.range != '-1' && data.dateFilters.range != '0' ) {
				this.url = this.url + '&range=' + data.dateFilters.range;
			} else if ( data.dateFilters.range == '-1' && !_.isNull( data.dateFilters.start ) ) {
				this.url = this.url + '&start=' + data.dateFilters.start + '&end=' + data.dateFilters.end;
			}

			if( this.scope.attr('data-nodeFilter') )
			{
				this.url = this.url + '&nodes=' + this.scope.attr('data-nodeFilter');
			}

			ips.getAjax()( this.url, {
				type: 'get'
			} )
				.done( function (response) {
					self.scope
						.addClass( 'cStatTile--loaded' )
						.find('.cStatTile__body')
							.removeClass('ipsLoading')
						.end()
						.find('[data-role="statBlockContent"]')
							.hide()
							.html( response );

					self.scope.find('[data-role="statBlockContent"]').fadeIn('fast');
					self.loaded = true;
					self.currentCounts = self.getCounts( self.scope );
					self.startInterval();

					$( document ).trigger( 'contentChange', [ self.scope ] );
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/statuses" javascript_name="ips.statuses.status.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.statuses.status.js - Status controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.statuses.status', {

		_commentStatusID: null,

		initialize: function () {
			this.on( 'click', '[data-action="loadPreviousComments"], [data-action="loadNextComments"]', this.paginate );
			this.on( 'submit', '[data-role="replyComment"]', this.quickReply );
			this.on( document, 'addToStatusFeed', this.addToStatusFeed );
			this.setup();
		},

		setup: function () {
			this._commentStatusID = this.scope.attr('data-statusID');
		},

		/**
		 * Load more status comments
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		paginate: function (e) {
			e.preventDefault();

			var feed = $( e.currentTarget ).closest('[data-role="statusComments"]');
			var paginateRow = $( e.currentTarget ).closest( '.cStatusUpdates_pagination' );

			// Put the list in loading state
			paginateRow.html( ips.templates.render('core.statuses.loadingComments') );

			// Load the new comments
			ips.getAjax()( $( e.currentTarget ).attr('href') )
				.done( function (response) {
					paginateRow.replaceWith( response );

					// Remove any pagination rows which aren't at the start or end of the list
					feed
						.find('meta')
							.remove()
						.end();

					$( document ).trigger( 'contentChange', [ feed ] );
				})
				.fail(function(response){
					paginateRow.replaceWith( '' );
					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: _.isUndefined( response.responseJSON ) ? ips.getString( 'errorLoadingContent' ) : response.responseJSON,
					});
				});
		},

		/**
		 * Reply to a status
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		quickReply: function (e) {

			var form = this.scope.find('[data-role="replyComment"] form');

			if ( form.attr('data-noAjax') ) {
				return;
			}

			e.preventDefault();
			e.stopPropagation();

			var self = this;
			var feed = $( e.currentTarget ).closest('[data-role="statusComments"]');
			var replyArea = this.scope.find('[data-role="replyComment"]');
			var submit = this.scope.find('[type="submit"]');
			
			var page = feed.attr('data-currentPage');
			
			if( !page ){
				page = 1;
			}

			var currentText = submit.text();
			
			// Set the form to loading
			submit
				.prop( 'disabled', true )
				.text( ips.getString('saving') );

			ips.getAjax()( form.attr('action'), {
				data: form.serialize() + '&submitting=1&currentPage=' + page,
				type: 'post'
			})
				.done( function (response) {
					self.trigger( 'addToStatusFeed', {
						content: response.content,
						statusID: self._commentStatusID
					});
				})
				.fail( function () {
					form.attr('data-noAjax', 'true');
					form.submit();
				})
				.always( function () {
					submit
						.prop( 'disabled', false )
						.text( currentText );
				});
		},

		/**
		 * Adds new comment to the feed and resets the editor
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		addToStatusFeed: function (e, data) {
			if( !data.content || data.statusID != this._commentStatusID ){
				return;
			}
			
			var content = $('<div/>').append( data.content );

			this.scope.find('[data-role="statusComments"]').append( content );

			ips.utils.anim.go( 'fadeInDown', content );

			ips.ui.editor.getObj( this.scope.find('[data-role="replyComment"] [data-ipsEditor]') ).reset();

			$( document ).trigger( 'contentChange', [ this.scope ] );
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/statuses" javascript_name="ips.statuses.statusFeed.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.statuses.statusFeed.js - Status feed controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.statuses.statusFeed', {

		initialize: function () {
			this.on( 'submit', '[data-role="newStatus"] form', this.submitNewStatus );
			this.setup();
		},

		setup: function () {
			
		},

		/**
		 * Adds a new status to the feed
		 *	
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		submitNewStatus: function (e) {
			e.preventDefault();
			e.stopPropagation();

			var self = this;
			var feed = this.scope.find('[data-role="activityStream"]');
			var replyArea = this.scope.find('[data-role="replyComment"]');
			var form = this.scope.find('[data-role="newStatus"] form');
			var submit = this.scope.find('[data-action="submitComment"]');
			var textarea = form.find('textarea');

			var currentText = submit.text();
			
			if ( !textarea.val() ) {
				ips.ui.alert.show( {
					type: 'alert',
					message: ips.getString('validation_required'),
					icon: 'warn'
				});
				return;
			}
			
			// Set the form to loading
			submit
				.prop( 'disabled', true )
				.text( ips.getString('saving') );

			ips.getAjax()( form.attr('action'), {
				data: form.serialize(),
				type: 'post',
				bypassRedirect: true
			})
				.done( function (response) {
					var content = $('<div/>').append( response );
					var comment = content.find('.ipsComment,.ipsStreamItem').first(); // Must select first(), because statuses can contain sub-comments

					feed.prepend( comment );

					ips.utils.anim.go( 'fadeInDown', comment );
					
					$( 'textarea[name="' + textarea.attr('name') + '"]' ).closest('[data-ipsEditor]').data('_editor').reset();
					$( document ).trigger( 'contentChange', [ comment ] );
				})
				.fail( function ( xhr ) {
					self.scope.find('#elStatusSubmit').parent().html( xhr.responseText );
					$( document ).trigger( 'contentChange', [ self.scope.find('#elStatusSubmit').parent() ] );
				})
				.always( function () {
					submit
						.prop( 'disabled', false )
						.text( currentText );
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="controllers/stockart" javascript_name="ips.stockart.pixabay.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500"><![CDATA[/**
 * IPS Social Suite 4
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.stockart.pixabay.js - Controller for Pixabay Actions
 *
 * Author: Daniel Fatkic
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.global.stockart.pixabay', {

		initialize: function () {
			this.on( 'click', '.ipsPixabayImage', this.selectImage );
			this.on( 'focus', '[data-role="pixabaySearch"]', this.searchPixabay );
			this.on( 'blur', '[data-role="pixabaySearch"]', this.stopSearchPixabay );
			$('[data-role="pixabayResults"]').on( 'scroll', this.scrollEvent.bind(this) );
			this.setup();
		},
		_typeTimer: null,
		_lastVal: '',
		_perPage: 20,
		_status: 'init',
		
		setup: function () {
			this.uploader = $(document).find('[data-ipsUploader-name="'+ this.scope.attr('data-uploader') + '"]');
			this._doSearch(null);
		},


		searchPixabay: function (e) {
			this._typeTimer = setInterval( _.bind( this._typing, this ), 1500 );
		},
		
		/**
		 * Event handler for scrolling
		 * 
		 * @param 		{event}	 	e 		Event object
		 * @returns 	{void}
		 */
		scrollEvent: function (e) {
			
			var scrollScope = $('[data-role="pixabayResults"]');
			var scrollHeight = scrollScope[0].scrollHeight;
			var distanceFromBottom = scrollHeight - scrollScope.height() - scrollScope.scrollTop();
			
			if ( this._status != 'ready' ) {
				return;
			}
			
			if( distanceFromBottom <= 150 ){
				this._status = 'loading';
				
				var offset = parseInt( this.scope.find('[data-role="pixabayMore"]').attr('data-offset') );
				this.scope.find('[data-role="pixabayMore"]').attr('data-offset', offset + this._perPage );
				
				this.scope.find('[data-role="pixabayMoreLoading"]').removeClass('ipsHide');
				
				this._doSearch( this._lastVal );
			}
		},
		
		/**
		 * The search box has blurred
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		stopSearchPixabay: function (e) {
			if( this._typeTimer ){
				clearInterval( this._typeTimer );
				this._typeTimer = null;
			}
			/* Clear Results */
		},

		/**
		 * Runs a continuous interval to check the current search value, and call the search function
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_typing: function () {
			var textElem = this.scope.find('[data-role="pixabaySearch"]');

			if( this._lastVal == textElem.val() ){
				return;
			}
			
			this.scope.find('[data-role="pixabayMore"]').attr('data-offset', 0);
			this.scope.find('.ipsMenu_innerContent').animate({
				scrollTop: "0"
			}, 250);
			
			this._doSearch( textElem.val() );
			
			this._lastVal = textElem.val();
		},

		/**
		 * AJAX call to fetch the images
		 *
		 * @param 		{string} 	value  		Search value
		 * @returns 	{void}
		 */
		_doSearch: function (value) {
			var resultsbox = this.scope.find('[data-role="pixabayLoading"]');
			var offset = parseInt( this.scope.find('[data-role="pixabayMore"]').attr('data-offset') );
			
			Debug.log( offset + ',' + this._perPage );
			
			resultsbox.addClass('ipsLoading');
			var _self = this;
									
			this._status = 'loading';
			ips.getAjax()( ips.getSetting('baseURL') + '?app=core&module=system&controller=pixabay&do=search&offset=' + offset + '&limit=' + this._perPage, {
				type: 'POST',
				data: {
					'search': value
				}
			} ).done( function (response) {
				var data = response;
				var result = ( offset > 0 ) ? resultsbox.html() : '';
				var pos = 0;
				var gifsForThisRow = '';

				if ( data.error )
				{
				   result = data.error;
				}
				else
				{
					_.each( data.images, function (term) {
						gifsForThisRow += ips.templates.render('core.editor.pixabayThumb', {
							thumb: term.thumb,
							url: term.url,
							imgid: term.imgid
						} );

						/* Once we've reached the limit per line, add the line */
						pos++;
						if( pos == 3 ) {
							result += ips.templates.render('core.editor.pixabayRow', { images: gifsForThisRow } );
							pos = 0;
							gifsForThisRow = '';
						}
					} );
					Debug.log( data.pagination.total_count );
					Debug.log( offset + _self._perPage );
					if ( offset > 0 || data.pagination.total_count > offset + _self._perPage ) {
						_self.scope.find('[data-role="pixabayMoreLoading"]').addClass('ipsHide');
						_self._status = 'ready';
					}

					// No more available
					if ( data.pagination.total_count <= offset + _self._perPage ) {
						_self.scope.find('[data-role="pixabayMoreLoading"]').addClass('ipsHide');
						_self._status = 'done';
					}
				}
								
				resultsbox.removeClass('ipsLoading').html( result );
			} );
		},
		
		selectImage: function(e)
		{
			var image = $( e.target );

			var pluploadObj = this.uploader;
			

			pluploadObj.find('.ipsAttachment_loading').show();
			pluploadObj.find('.ipsAttachment_dropZoneSmall_info').hide();
			
			ips.getAjax()( ips.getSetting('baseURL') + '?app=core&module=system&controller=pixabay&do=getById&id=' + image.attr('data-id') ).done( function (response) {
				var randomId = Math.random().toString(36).substring(7);
				var bstr = atob( response.content );
				var n = bstr.length;
				var u8arr = new Uint8Array(n);
				while(n--) {
					u8arr[n] = bstr.charCodeAt(n);
				}
				var file = new File( [u8arr], response.filename, { type: response.type } );

				pluploadObj.trigger( 'injectFile', { file: file } );
			} );
			
			
			/* Now clear search and close the menu */
			this._status = 'init';
			this.scope.find('[data-role="pixabaySearch"]').val('');
			this.scope.find('[data-role="pixabayLoading"]').addClass('ipsLoading').html('');
			this.scope.trigger( 'closeDialog' );
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/streams" javascript_name="ips.streams.form.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.streams.form.js - Streams filter form
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.streams.form', {
		loaded: {},
		formSnapshot: false,
		reloaded: false,
		_loadedContainer: {},
		_loadedClubs: false,
		_formData: {},
		
		initialize: function () {
			this.on( 'streamStateUpdate.streamForm', this.streamStateUpdate );
			this.on( 'itemClicked.sideMenu', '[data-filterType="type"]', this.selectedType );
			this.on( 'menuItemSelected', '#elStreamReadStatus, #elStreamShowMe', this.checkFormUpdate );
			this.on( 'tokenAdded tokenDeleted', this.tokensChanged );
			this.on( 'menuItemSelected', '#elStreamSortEdit', this.changeSort );
			this.on( 'click', '[data-action="saveStream"], [data-action="newStream"]', this.submitForm );
			this.on( 'click', '[data-role="streamContainer"]', this.openStreamContainer );
			this.on( 'click', '[data-role="streamClubs"]', this.openStreamClubs );
			this.on( 'click', '[data-action="dismissSave"]', this.dismissSave );
			this.on( 'keydown', 'input', this.inputKeydown );
			
			if ( this.scope.attr('data-formType') == 'createStream' ) {
				this.on( 'itemClicked.sideMenu', '[data-filterType="date"]', this.selectedDate );
				this.on( 'itemClicked.sideMenu', '[data-filterType="ownership"]', this.selectedOwnership );
			} else {
				this.on( 'menuItemSelected', '#elStreamFollowStatus', this.selectedFollowStatus );
				this.on( 'menuItemSelected', '#elStreamTimePeriod', this.selectedDate );
				this.on( 'menuItemSelected', '#elStreamOwnership', this.selectedOwnership );
			}
			
			this.on( 'change', '#elSelect_stream_club_filter', this.clubSelectionChanged );
			this.on( 'change', '#stream_club_filter_unlimited', this.clubSelectionChanged );
			
			/* Show/hide apply changes button: Node selectors */
			this.on( 'nodeItemSelected', this.toggleApplyFilterButton );
			this.on( 'nodeItemUnselected', this.toggleApplyFilterButton );
			
			this.on( 'click', '[data-action="applyFilters"]', this.applyFilters );
			
			this.on( 'menuItemSelected', '#elStreamSortEdit, #elStreamFollowStatus', this.selectedMenuItem );
			this.on( 'menuItemSelected', '#elStreamTimePeriod, #elStreamOwnership, #elStreamShowMe', this.selectedMenuItem );
			
			this.on( 'menuItemSelected', '#elStreamReadStatus', this.selectedReadStatus );

			this.on( 'menuClosed', this.menuClosed );
			
			this.setup();
		},
	
		setup: function () {
			// Custom serialization for our form
			this._serializeConfig = {
				'stream_date_range[start]': this._serializeDate,
				'stream_date_range[end]': this._serializeDate
			};

			// If we're just creating a stream, we have no configuration yet
			if( this.scope.attr('data-formType') != 'createStream' ){
				this._formData = ips.getSetting('stream_config');

				// Are we looking for unread things and data?
				this._changeReadStatus( this._formData['stream_read'] );

				this._updateFilterOverview();

				this.takeFormSnapshot();
			}

			this.trigger( 'initialFormData.stream', {
				data: this._formData
			});
		},

		/**
		 * Handles keydown on inputs inside the stream menus. 
		 * Prevents enter key from creating a new stream (which is a behavior that isn't obvious to users)
		 *
		 * @param 		{event} 	e 		Event Object
		 * @returns 	{void}
		 */		
		inputKeydown: function (e) {
			if( e.which == 13 ){
				e.preventDefault();
			}
		},

		/**
		 * Toggles the apply filter button if requred
		 *
		 * @param 		{event} 	e 		Event Object
		 * @returns 	{void}
		 */
		toggleApplyFilterButton: function (e) {
			var button = $('.ipsMenu').filter(':visible').first().find('[data-action="applyFilters"]');
			if ( this.hasFormChanged() ) {
				button.removeClass('ipsButton_disabled');
			} else {
				button.addClass('ipsButton_disabled');
			}
		},
		
		/**
		 * Responds to the button click 'Apply Filters'. All we really
		 * need to do here is trigger the menu closed event, which we
		 * already listen for (and will also close the menu for us)
		 *
		 * @param 		{event} 	e 		Event Object
		 * @returns 	{void}
		 */
		applyFilters: function( e )
		{
			var button = $( e.currentTarget );
			button.closest('.ipsMenu').trigger('closeMenu');
		},
		
		/**
		 * Responds to an event from the main controller which tells us
		 * that the page state has changed, and we need to update our form field
		 * values to show the updated values
		 *
		 * @param 		{event} 	e 		Event Object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		streamStateUpdate: function (e, data) {
			this._formData = data.filterData;
			this._updateFieldValues();

			// Do we need to show the Save bar?
			if( !_.isUndefined( data.hideSaveBar ) && data.hideSaveBar ){
				this.scope.find('[data-role="saveButtonContainer"]').slideUp();
			} else {
				this.scope.find('[data-role="saveButtonContainer"]').slideDown();
			}
		},
		
		/**
		 * Adds a simple overview of each filter setting
		 *
		 * @returns 	{void}
		 */
		_updateFilterOverview: function () {
			
			// Only do this if we aren't creating
			if( this.scope.attr('data-formType') == 'createStream' ){
				return;
			}

			var self = this;
			var values = this._formData;
			
			var _overview = function (type) {
				return self.scope.find('[data-filter="' + type + '"] [data-role="filterOverview"]');
			};
			
			// Simple values
			_.each( ['stream_include_comments', 'stream_read', 'stream_sort', 'stream_solved'], function (val) {
				var lang = 'streamFilter_' + val + '_' + values[ val ];
				_overview( val ).html( ips.getString( lang ) );
			});
			
			// Tags
			if( !_.isUndefined( values['stream_tags'] ) && values['stream_tags'].trim() !== '' ){
				var tags = _.compact( values['stream_tags'].split(/\n/) );
			 	
				if( tags.length <= 2 ){
					var tagLang = ips.getString('streamFilter_stream_tags_tags', { 'tags': _.escape( tags.join(',') ) });
				} else {
					var tagLang = ips.pluralize( ips.getString('streamFilter_stream_tags_count'), tags.length ); 
				}
				
				_overview( 'stream_include_comments' ).append( '; ' + tagLang );
			} else {
				_overview( 'stream_include_comments' ).append( '; ' + ips.getString( 'streamFilter_stream_tags_noTags' ) );
			}
			
			// Ownership
			if ( values['stream_ownership'] == 'custom' && ( _.isUndefined( values['stream_custom_members'] ) || values['stream_custom_members'] == null || values['stream_custom_members'] == '' ) ) {
				values['stream_ownership'] = 'all'
			}
			if( values['stream_ownership'] !== 'custom' ){
				var ownershipLang = 'streamFilter_stream_ownership_' + values[ 'stream_ownership' ];
				_overview( 'stream_ownership' ).html( ips.getString( ownershipLang ) );
			} else if ( !_.isUndefined( values['stream_custom_members'] ) && values['stream_custom_members'] != null ) {
				var names = _.compact( ( _.isObject( values['stream_custom_members'] ) ? values['stream_custom_members'] : values['stream_custom_members'].split(/\n/) ) );
				var ownershipLang = ips.pluralize( ips.getString('streamFilter_stream_ownership_custom'), names.length );  
				_overview( 'stream_ownership' ).text( ownershipLang );
			}
			
			// Following
			if( values['stream_follow'] == 'all' ){
				_overview( 'stream_follow' ).html( ips.getString( 'streamFilter_stream_follow_all' ) );
			} else {
				var value = _.keys( values['stream_followed_types'] );
				var follows = [];
				
				for( var i = 0; i < value.length; i++ ){
					follows.push( ips.getString( 'streamFilter_stream_follow_' + value[i] ) );
				}
				
				_overview( 'stream_follow' ).html( follows.join( ', ' ) );
			}

			// Date range
			if ( values['stream_date_type'] == 'relative' && ( _.isUndefined( values['stream_date_relative_days'] ) || values['stream_date_relative_days'] == null || values['stream_date_relative_days'] == '' ) ) {
				values['stream_date_type'] = 'all';
			} else if( values['stream_date_type'] == 'custom' && ( ( _.isUndefined( values['stream_date_range']['start'] ) || values['stream_date_range']['start'] == null || values['stream_date_range']['start'] == '' ) || ( _.isUndefined( values['stream_date_range']['end'] ) || values['stream_date_range']['end'] == null || values['stream_date_range']['end'] == '' ) ) ) {
				values['stream_date_type'] = 'all';
			}
			if( values['stream_date_type'] == 'all' || values['stream_date_type'] == 'last_visit' ){
				_overview( 'stream_date_type' ).html( ips.getString( 'streamFilter_stream_date_type_' + values['stream_date_type'] ) );
			} else if( values['stream_date_type'] == 'relative' ){

				if( values['stream_date_relative_days']['unit'] == 'w' ) {
					var dateLang = ips.pluralize( ips.getString('streamFilter_stream_date_type_relative_weeks'), ( values['stream_date_relative_days']['val'] ? values['stream_date_relative_days']['val'] : values['stream_date_relative_days'] ) );
				}
				else {
					var dateLang = ips.pluralize( ips.getString('streamFilter_stream_date_type_relative'), ( values['stream_date_relative_days']['val'] ? values['stream_date_relative_days']['val'] : values['stream_date_relative_days'] ) );
				}
				_overview( 'stream_date_type' ).text( dateLang );
			} else {
				// If this is a member-owned stream *or* it's a real date stamp (xx-xx-xxxx), we need to strip out the timezone
				// However, if it's an admin-created stream, there's no timezone in the stamp, so leave as-is
				if( !_.isUndefined( values['__stream_owner'] ) || ( isNaN( values['stream_date_range']['start'] ) && values['stream_date_range']['start'].match('-') ) ){
					var start = ips.utils.time.localeDateString( ips.utils.time.removeTimezone( new Date( isNaN( values['stream_date_range']['start'] ) && values['stream_date_range']['start'].match('-') ? values['stream_date_range']['start'] : values['stream_date_range']['start'] * 1000 ) ) );
					var end = ips.utils.time.localeDateString( ips.utils.time.removeTimezone( new Date( isNaN( values['stream_date_range']['end'] ) && values['stream_date_range']['end'].match('-') ? values['stream_date_range']['end'] : values['stream_date_range']['end'] * 1000 ) ) );
				} else {
					var start = ips.utils.time.localeDateString( new Date( values['stream_date_range']['start'] * 1000 ) );
					var end = ips.utils.time.localeDateString( new Date( values['stream_date_range']['end'] * 1000 ) );
				}

				if( !_.isUndefined( values['stream_date_range']['start'] ) && !_.isUndefined( values['stream_date_range']['end'] ) ){
					var dateLang = ips.getString('streamFilter_stream_date_type_range', { start: start, end: end });
				} else if( !_.isUndefined( values['stream_date_range']['start'] ) ) {
					var dateLang = ips.getString('streamFilter_stream_date_type_start', { start: start });
				} else if( !_.isUndefined( values['stream_date_range']['start'] ) ) {
					var dateLang = ips.getString('streamFilter_stream_date_type_end', { end: end });
				}
				
				_overview( 'stream_date_type' ).text( dateLang );
			}
			
			// Content
			if( !values['stream_classes'] || values['stream_classes_type'] == 0 ){
								
				if ( values['stream_club_select'] == 'none' ) {
					_overview( 'stream_classes' ).html( ips.getString( 'streamFilter_stream_classes_no_clubs' ) );
				} else if ( values['stream_club_select'] == 'all' ) {
					_overview( 'stream_classes' ).html( ips.getString( 'streamFilter_stream_classes_all' ) );
				} else if( values['stream_club_filter'] ) {
					_overview( 'stream_classes' ).text( ips.getString('loading') );
					this._loadClubs(function(){
						var elem = this.scope.find('#elSelect_stream_club_filter');
						var clubIds = values['stream_club_filter'].split(',');
						var clubNames = [];
						for( var i = 0; i < clubIds.length; i++ ){
							var option = elem.find('option[value="' + clubIds[i] + '"]');
		
							if( option.length ){
								clubNames.push( option.text().trim() );
							}
						}
						_overview( 'stream_classes' ).text( clubNames.join( ', ') );
					}.bind(this));
				}
			} else {
				var classKeys = _.keys( values['stream_classes'] );
				var classes = [];
				
				for( var i = 0; i < classKeys.length; i++ ){
					var elem = this.scope.find('[data-class="' + classKeys[i].replace(/\\/g, '\\\\') + '"] > span');

					if( elem.length ){
						classes.push( elem.text().trim() );
					}
				}
				
				_overview( 'stream_classes' ).text( classes.join( ', ') );
			}
		},
		
		/**
		 * Update fields in our form to reflect the values in _formData,
		 * which have likely been updated by the page state changing.
		 *
		 * @returns 	{void}
		 */
		_updateFieldValues: function () {
			
			var self = this;
			var data = this._formData;
			
			if ( data['stream_read'] == 'unread' ) {
				data['stream_include_comments'] = 0;
			}
			
			// Basics
			_.each( ['stream_include_comments', 'stream_read', 'stream_sort', 
				'stream_follow', 'stream_ownership', 'stream_date_type'], function (key) {
				self.scope.find('[name="' + key + '"]')
					.prop( 'checked', false )
					.closest('.ipsMenu_item')
						.removeClass('ipsMenu_itemChecked')
					.end()
					.filter('[value="' + data[ key ] + '"]')
						.prop('checked', true)
						.change()
						.closest('.ipsMenu_item')
							.addClass('ipsMenu_itemChecked');
			});
			
			// Following types			
			var followSelector = _.map( data['stream_followed_types'], function (val, key) {
				return '[name="stream_followed_types[' + key + ']"]';
			});
			
			this.scope.find('[name^="stream_followed_types"]')
				.prop('checked', false)
				.closest('.ipsMenu_item')
					.removeClass('ipsMenu_itemChecked')
				.end()
				.filter( followSelector.join(',') )
					.prop('checked', true)
					.change()
					.closest('.ipsMenu_item')
						.addClass('ipsMenu_itemChecked');
					
			// Ownership
			if( this.scope.find('#elInput_stream_custom_members').length ){
				var ownerAC = ips.ui.autocomplete.getObj( this.scope.find('#elInput_stream_custom_members') );
				ownerAC.removeAll();
				
				if( data['stream_ownership'] == 'custom' ){
					var names = _.compact( data['stream_custom_members'].split(/\n/) );
					
					for( var i = 0; i < names.length; i++ ){
						ownerAC.addToken( names[ i ] );
					}
				}
			}
			
			// Tags
			if ( !_.isUndefined( data['stream_tags'] ) ){
				var tags = _.compact( data['stream_tags'].split(/\n/) );
				var tagAC = ips.ui.autocomplete.getObj( this.scope.find('#elInput_stream_tags') );
				tagAC.removeAll();
				
				if( tags.length ){
					for( var i = 0; i < tags.length; i++ ){
						tagAC.addToken( tags[ i ] );
					}
				}
			}

			// Dates
			this.scope.find('[name="stream_date_relative_days"], [name="stream_date_range[start]"], [name="stream_date_range[end]"]').val('');
			
			if( data['stream_date_type'] == 'relative' ){
				this.scope.find('[name="stream_date_relative_days"]').val( data['stream_date_relative_days'] );
			} else if( data['stream_date_type'] == 'custom' ){
				var html5 = ips.utils.time.supportsHTMLDate();
				
				if( data['stream_date_range']['start'] ){
					var startDateObj = new Date( data['stream_date_range']['start'] );
					
					if( html5 ){
						this.scope.find('[name="stream_date_range[start]"]').get(0).valueAsDate = startDateObj;
					} else {
						this.scope.find('[name="stream_date_range[start]"]').datepicker( 'setDate', ips.utils.time.removeTimezone( startDateObj ) );
					}
				}
				
				if( data['stream_date_range']['end'] ){
					var endDateObj = new Date( data['stream_date_range']['end'] );
					
					if( html5 ){
						this.scope.find('[name="stream_date_range[end]"]').get(0).valueAsDate = endDateObj;
					} else {
						this.scope.find('[name="stream_date_range[end]"]').datepicker( 'setDate', ips.utils.time.removeTimezone( endDateObj ) );
					}
				}
			}
			
			// Classes
			var classChecks = $('#elStreamContentTypes_menu').find('[type="checkbox"][name^="stream_classes"]');
			var classSelector = [];
			
			classChecks
				.prop('checked', false)
				.change()
				.closest('.ipsSideMenu_item')
					.removeClass('ipsSideMenu_itemActive');
			
			if( data['stream_classes_type'] == 0 ){
				this.scope.find('[name="stream_classes_type"]')
					.first()
					.closest('.ipsSideMenu_item')
						.addClass('ipsSideMenu_itemActive');
			} else {
				var classKeys = _.keys( data['stream_classes'] );
				
				// It's inefficient to do a .find on each key separately here, but I simply could not
				// get jQuery to find matches when a combined selector was used. 
				// The backslashes really cause a problem.
				_.each( classKeys, function (val) {
					self.scope.find( '[data-class="' + val.replace(/\\/g, '\\\\') + '"] input[type="checkbox"]' )
						.prop('checked', true)
						.change()
						.closest('.ipsSideMenu_item')
							.addClass('ipsSideMenu_itemActive');
				});
			}
			
			
			this._updateFilterOverview();
		},
		
		/**
		 * Stream container toggle
		 *
		 * @param 		{event} 	e 		Event
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		openStreamContainer: function (e, data) {
			e.preventDefault();
			
			var self = this;
			var link = $( e.currentTarget );
			var className = link.attr('data-class');
			var contentKey = link.attr('data-contentKey');
			var nodeContainer = link.next('.cStreamForm_nodes');
			
			if( !this._loadedContainer[ className ] ){
				// Show node container with loading text
				nodeContainer.slideDown();
				var containers = [];
				
				if ( _.isObject( ips.getSetting('stream_config') ) && ! _.isEmpty( ips.getSetting('stream_config')['containers'] ) ) {
					var keys = _.keys( ips.getSetting('stream_config')['containers'] );
					var values = _.values( ips.getSetting('stream_config')['containers'] );
					
					for( var i = 0; i < keys.length; i++ ){
						containers.push( 'stream_containers[' + keys[i] + ']=' + values[i] );
					}	
				}

				// Load container
				ips.getAjax()( this.scope.find('form').attr('action').replace( 'do=create', '' ), {
						type: 'post',
						data: 'do=getContainerNodeElement&className=' + className + '&key=' + contentKey + '&' + containers.join('&')
					} )
					.done( function (returnedData) {
						// Add this content to the menu
						nodeContainer.html( returnedData.node );

						// Remember we've loaded it
						self._loadedContainer[ className ] = true;
	
						$( document ).trigger( 'contentChange', [ nodeContainer.parent() ] );
					});
			} else {
				nodeContainer.slideToggle();
			}
		},
		
		/**
		 * Stream clubs toggle
		 *
		 * @param 		{event} 	e 		Event
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		openStreamClubs: function (e, data) {
			e.preventDefault();			
			
			var clubContainer = $('#elStreamClubs');
			if ( !this._loadedClubs ) {
				clubContainer.slideDown();
				this._loadClubs(null);
			} else {
				clubContainer.slideToggle();
			}
		},
		
		/**
		 * Stream clubs toggle
		 *
		 * @param 		{event} 	e 		Event
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		_loadClubs: function(callback) {
			if ( !this._loadedClubs ) {
				var clubContainer = $('#elStreamClubs');
				
				var extra = null;
				if ( _.isObject( ips.getSetting('stream_config') ) ) {
					extra = '&stream_club_select=' + ips.getSetting('stream_config')['stream_club_select'] + '&stream_club_filter=' + ips.getSetting('stream_config')['stream_club_filter'];
				}
							
				var self = this;
				ips.getAjax()( this.scope.find('form').attr('action').replace( 'do=create', '' ), {
					type: 'post',
					data: 'do=getClubElement&' + extra
				} )
				.done( function (returnedData) {
					clubContainer.html( returnedData.field );
					self._loadedClubs = true;
					$( document ).trigger( 'contentChange', [ clubContainer.parent() ] );
					
					if ( callback ) {
						callback();
					}
				});
			} else if ( callback ) {
				callback();
			}
		},
		
		/**
		 * Event handler for menus being closed.
		 *
		 * @param 		{event} 	e 		Event
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		menuClosed: function (e, data) {
			if ( data.elemID == 'elStreamContentTypes' || ( $('#' + data.elemID + '_menu' ).length && data.elemID != 'elSaveStream' ) ) {
				_.delay( function( self )
				{
					$('[data-action="applyFilters"]').addClass('ipsButton_disabled');
					self.checkFormUpdate();
					self.reloaded = true;
				}, 500, this );
			}
		},
		
		/**
		 * Check to see if any of the form elements have changed and if so, update listing
		 *
		 * @returns 	{void}
		 */
		checkFormUpdate: function() {
			if( this.hasFormChanged() ){
				this.updateResults();
				this.scope.find('#elSaveStream').removeClass('ipsFaded');
				this.takeFormSnapshot();
			}
		},
		
		/**
		 * Has the form changed?
		 *
		 * @returns {boolean}
		 */
		 hasFormChanged: function() {
			if ( _.isEqual( this.formSnapshot, ips.utils.form.serializeAsObject( this.scope.find('form'), this._serializeConfig ) ) ) {
				return false;
			}
			return true;
		},
		
		/**
		 * Take a form snapshot
		 *
		 * @returns {void}
		 */
		takeFormSnapshot: function() {
			this.formSnapshot = ips.utils.form.serializeAsObject( this.scope.find('form'), this._serializeConfig );
		},
		
		/**
		 * When tokens have changed in an autocomplete, mark the form as dirty
		 *
		 * @returns {void}
		 */
		tokensChanged: function () {
			this.reloaded = false;
		},

		/**
		 * Change the sort
		 *
		 * @returns {void}
		 */
		changeSort: function (e, data) {
			this.checkFormUpdate();
		},
		
		/**
		 * Prevents default event when a menu item is selected
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object}	data 	Event data object
		 * @returns {void}
		 */
		selectedMenuItem: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}			
		},
		
		/**
		 * Handles user selecting read status
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from the menu
		 * @returns 	{void}
		 */
		selectedReadStatus: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}
			
			this._changeReadStatus( data.selectedItemID );
		},
		
		/**
		 *  Toggles stuff when read status is changed
		 *
		 * @param 		{string} 	type 		Type (all/unread)
		 * @returns 	{void}
		 */
		_changeReadStatus: function( type ) {
			if( type == 'unread' ){
				/* Unread must be items only, the back-end does not have logic to filter out read comments from a stream of comments */
				this._formData['stream_include_comments'] = 0;
				this.scope.find('#stream_ownership_0').closest('a').trigger('click');
				this.scope.find('#elStreamShowMe_menu li[data-ipsmenuvalue=1]').addClass('ipsMenu_itemDisabled');
			} else {
				this.scope.find('#elStreamShowMe_menu li[data-ipsmenuvalue=1]').removeClass('ipsMenu_itemDisabled');
			}
		},
		
		/**
		 * Dismiss the save bar
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		dismissSave: function (e) {
			this.scope.find('[data-role="saveButtonContainer"]').slideUp();
		},

		/**
		 * Save the form
		 * We add different params depending on what we're doing - save as new stream,
		 * save & update, or just update.
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		submitForm: function (e) {
			
			var button = $( e.currentTarget );
			var form = button.closest('form');

			// If we are creating a stream, we don't want to handle the form with JS
			if( this.scope.attr('data-formType') == 'createStream' ){
				return;	
			}

			// If we're creating a new stream, just submit the page
			if( button.attr('data-action') == 'newStream' ){
				
				form.prepend( 
					$('<input />')
						.attr('type', 'hidden')
						.attr('name', 'do')
						.attr('value', 'create') 
				);

			} else {
				this._formData = ips.utils.form.serializeAsObject( this.scope.find('form'), this._serializeConfig );

				// Send form data up
				this.trigger('formSubmitted.stream', {
					data: this._formData,
					action: ( button.attr('data-action') == 'newStream' ) ? 'createForm' : 'saveForm'
				});
			}
		},
		
		/**
		 * Update results
		 * Takes the value from the form and updates the result set
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		updateResults: function () {
			this._formData = ips.utils.form.serializeAsObject( this.scope.find('form'), this._serializeConfig );
			
			this._updateFilterOverview();
			
			this.trigger('formSubmitted.stream', {
				data: this._formData,
				action: 'updateForm'
			});
		},

		/**
		 * Handles user selecting 'date' as a filter. Shows the date fields in the form
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from the menu
		 * @returns 	{void}
		 */
		selectedDate: function (e, data) {
			this.reloaded = false;
			
			if( data.selectedItemID == 'custom' ){
				this.scope.find('[data-role="dateRelativeForm"]').slideUp();
				this.scope.find('[data-role="dateForm"]').slideDown();
			} else if( data.selectedItemID == 'relative' ){
				this.scope.find('[data-role="dateForm"]').slideUp();
				this.scope.find('[data-role="dateRelativeForm"]').slideDown();
				this.scope.find('[name="stream_date_relative_days"]').focus();
			} else {
				// Check undefined to prevent actioning when clicking on form fields
				if ( ! _.isUndefined( data.selectedItemID ) ) {
					this.scope.find('[data-role="dateForm"]').slideUp();
					this.scope.find('[data-role="dateRelativeForm"]').slideUp();
					this.checkFormUpdate();
				}
			}
		},
		
		/**
		 * Handles user selecting 'follow status' as a filter.
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from the menu
		 * @returns 	{void}
		 */
		selectedFollowStatus: function (e, data) {
			
			// Get selected items
			var selectedItems = data.selectedItems;
			
			if( !_.size( selectedItems ) ){
				this.scope.find('[name="stream_follow"]').val( 'all' );
			} else {
				this.scope.find('[name="stream_follow"]').val( 'followed' );
			}
			
			this.checkFormUpdate();
		},
		
		/**
		 * Handles user selecting 'ownership' as a filter. Shows the custom member field
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from the menu
		 * @returns 	{void}
		 */
		selectedOwnership: function (e, data) {
			if( data.selectedItemID == 'custom' ){
				this.scope.find('[data-role="ownershipMemberForm"]').slideDown();
			} else {
				// Check undefined to prevent actioning when clicking on form fields
				if ( ! _.isUndefined( data.selectedItemID ) ) {
					this.scope.find('[data-role="ownershipMemberForm"]').slideUp();
					this.checkFormUpdate();
				}
			}
		},
		
		/**
		 * Handles changing the club selectionb
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from the menu
		 * @returns 	{void}
		 */
		clubSelectionChanged: function (e, data) {
						
			if ( this.scope.find('[name="stream_club_filter_dummy_unlimited"]').is(':checked') ) {
				this.scope.find('[name="stream_club_select"]').val( 'all' );
			} else {
				this.scope.find('[name="stream_club_select"]').val( 'select' );
				this.scope.find('[name="stream_club_filter"]').val( $('#elSelect_stream_club_filter').val() );
			}
			
			
			this.toggleApplyFilterButton();
		},

		/**
		 * Responds to clicking a content type in the form. Ensures 'all' is selected
		 * if no other types are.
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object from the menu
		 * @returns 	{void}
		 */
		selectedType: function (e, data) {
			var self = this;
			var typeMenu = this.scope.find('[data-filterType="type"]');
			var all = typeMenu.find('[data-ipsMenuValue="__all"]');
			var allButAll = typeMenu.find('[data-ipsMenuValue]:not( [data-ipsMenuValue="__all"] )');
			var allButAllChecks = allButAll.find('> input[type="checkbox"]');

			this.reloaded = false;
			this.toggleApplyFilterButton();
			
			if( data.selectedItemID == '__all' ){
				// Did the user click 'all'? If so, uncheck everything else and hide all content filters
				allButAll
					.removeClass('ipsSideMenu_itemActive')
					.find('> input[type="checkbox"]')
						.prop( 'checked', false );

				// Make sure 'all' is checked
				all.addClass('ipsSideMenu_itemActive');

				this.scope.find('input[type="radio"][name="stream_classes_type"][value="0"]').prop( 'checked', true );

				// Hide any content filters
				this.scope.find('[data-contentType]').closest('.cStreamForm_nodes').slideUp();

			} else {

				// If we have any checked items, uncheck 'all'
				if( allButAllChecks.filter(':checked').length ){
					all.removeClass('ipsSideMenu_itemActive')
					
					this.scope.find('input[type="radio"][name="stream_classes_type"][value="1"]').prop( 'checked', true );

					// If the selected types have extra filters, show those too
					allButAllChecks.filter(':checked').each( function () {
						var type = $( this ).closest('[data-ipsMenuValue]').attr('data-ipsMenuValue');

						if( self.scope.find('[data-contentType="' + type + '"]').length ){
							self.scope.find('[data-contentType="' + type + '"]').slideDown();
						}
					});

					// ...and hide any which aren't checked
					allButAllChecks.filter(':not( :checked )').each( function () {
						var type = $( this ).closest('[data-ipsMenuValue]').attr('data-ipsMenuValue');

						if( self.scope.find('[data-contentType="' + type + '"]').length ){
							self.scope.find('[data-contentType="' + type + '"]').slideUp();
						}
					})
				} else {
					// Nothing is checked now, so check 'all'
					all.addClass('ipsSideMenu_itemActive')
						.find('> input[type="checkbox"]')
							.prop( 'checked', true );

					this.scope.find('[data-contentType]').slideUp();
				}	
			}
		},

		/**
		 * A function which will be passed into the serializeAsObject function so that
		 * we can format dates consistently as YYYY-MM-DD. 
		 * MUST return a string.
		 *
		 * @param 		{string} 	name	Name of form field
		 * @param 		{string} 	value 	Value of form field as returned by jQuery's serializeArray()
		 * @returns 	{string}
		 */
		_serializeDate: function (name, value) {
			// If we're an HTML5 browser, dates are already in YYYY-MM-DD format, so we can return
			if( ips.utils.time.supportsHTMLDate() ){
				return value;
			}

			var dateObj = ips.utils.time.getDateFromInput( $('input[name=' + ips.utils.css.escapeSelector( name ) + ']') );

			// Nothing if this isn't really a date
			if( !ips.utils.time.isValidDateObj( dateObj ) ){
				return '';
			}

			// Format the date to YYYY-MM-DD
			var month = ( '0' + ( dateObj.getUTCMonth() + 1 ) ).slice( -2 );
			var day = ( '0' + ( dateObj.getUTCDate() ) ).slice( -2 );

			return dateObj.getUTCFullYear() + '-' + month + '-' + day;
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/streams" javascript_name="ips.streams.main.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.streams.main.js - Main stream view
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.streams.main', {

		_streamLoadingOverlay: null,
		_tooltip: null,
		_tooltipTimer: null,
		_baseURL: '',
		_streamID: 0,
		_currentView: '',

		_shortInterval: 15, // Seconds
		_longInterval: 45, // Seconds
		_killUpdate: 45, // Minutes
		_timer: null,
		_killTimer: null,
		_activityUpdate: null,
		_loadMore: null,
		_timestamp: 0,
		_autoUpdate: false,
		_windowFocus: false,

		initialize: function () {
			this.on( 'formSubmitted.stream', this.formSubmitted );
			this.on( 'initialFormData.stream', this.initialFormData );
			this.on( 'loadMoreResults.stream', this.loadMoreResults );
			this.on( 'latestTimestamp.stream', this.latestTimestamp );
			this.on( 'click', '[data-action="switchView"]', this.switchView );
			this.on( 'menuOpened', '#elStreamShare', this.menuOpened );
			this.on( 'click', '[data-action="toggleStreamDefault"]', this.toggleDefault );
			this.on( 'click', '[data-action="removeStream"]', this.removeStream );
			this.on( 'click', '[data-action="loadMore"]', this.loadMoreResults );
			this.on( 'resultsUpdated.stream', this.resultsUpdated );
			this.on( 'stickyStatusChange.sticky', '#elStreamFilterForm', this.stickyChange );
			this.on( 'click', '[data-action="toggleFilters"]', this.toggleFilters );
			this.on( 'click', '[data-action="subscribe"]', this.subscribe );
			this.on( 'click', '[data-action="unsubscribe"]', this.unsubscribe );
			this.on( document, 'breakpointChange', this.breakpointChange );
			this.on( document, 'markAllRead', this.markAllRead );

			// Figure out the visibility event we need
			this.on( document, ips.utils.events.getVisibilityEvent(), this.windowVisibilityChange );

			// Primary event that watches for URL changes
			History.Adapter.bind( window, 'statechange', _.bind( this.stateChange, this ) );
			
			this.setup();
		},

		setup: function () {
			this._streamID = this.scope.attr('data-streamID');			
			this._baseURL = ips.getSetting('stream_config')['url'];

			if ( this._baseURL.match(/\?/) ) {
				this._baseURL += '&';
			} else {
				this._baseURL += '?';
			}

			// If the last character is &, we can remove that because it'll be added back later
			if( this._baseURL.slice(-1) == '&' ){
				this._baseURL = this._baseURL.slice( 0, -1)
			}

			// Get the newest timestamp
			this._timestamp = parseInt( this.scope.find('[data-timestamp]').first().attr('data-timestamp') );
			this._currentView = this.scope.find('[data-action="switchView"].ipsButtonRow_active').attr('data-view');

			if( !_.isUndefined( this.scope.find('[data-role="streamResults"]').attr('data-autoPoll') ) ){
				this._autoUpdate = true;
				this.windowVisibilityChange(); // Call our visibility method manually to determine window visibility
			}

			// Are we on mobile?
			if( ips.utils.responsive.currentIs('phone') ){
				this.scope.find('[data-role="filterBar"]').addClass('ipsHide');
				this.scope.find('[data-action="toggleFilters"]').html( ips.getString('toggleFiltersHidden') );
			}
		},
		
		/**
		 * Event handler for the page state changing
		 * We have a few different states to support:
		 * - Form changes
		 * - Load More button
		 * - View type
		 * Each is handled a little differently but ultimately passes data to this._loadResults
		 *
		 * @returns 	{void}
		 */
		stateChange: function () {
			var state = History.getState();
			var self = this;
			
			if( ( !state.data.controller || state.data.controller != 'core.front.streams.main' ) && this._initialURL !== state.url ){
				return;
			}
			
			if( state.data.action == 'saveForm' || state.data.action == 'updateForm' ){
				
				// If this state alters the form, let the form know
				this.triggerOn( 'core.front.streams.form', 'streamStateUpdate.streamForm', {
					filterData: state.data.filterData,
					hideSaveBar: ( _.isEmpty( this._getUrlDiff( state.data.filterData ) ) || state.data.action == 'saveForm' )
				});

				// Track page view
				ips.utils.analytics.trackPageView( state.url );

				// Are we changing the sorting?
				if( !_.isUndefined( state.data.filterData.stream_sort ) ){
					this._togglePolling( !( state.data.filterData.stream_sort == 'oldest' ) );
				}
				
				var data = _.extend( { view: this._currentView }, state.data.filterData );
				
				switch (state.data.action) {
					case 'updateForm':
						data = _.extend( data, false, { 'updateOnly': 1 } );
					break;
					case 'saveForm':
						data = _.extend( data, false, { 'form': 'save' } );
					break;
				}
				
				this._loadResults( data )
					.done( function (response) {
						self.triggerOn( 'core.front.streams.results', 'updateResults.stream', {
							append: false,
							response: response
						});
						
						// Do we need to reset the URL & config?
						if( state.data.action == 'saveForm' ){
							ips.setSetting( 'stream_config', JSON.parse( response.config ) );
							
							History.replaceState( {
								controller: 'core.front.streams.main',
								url: ips.getSetting( 'stream_config' )['url'],
								skipUpdate: true
							}, document.title, ips.getSetting( 'stream_config' )['url'] );
						}
					})
				
			} else if( state.data.action == 'loadMore' ){
				
				var data = _.extend( { 
					before: state.data.before, 
					view: this._currentView 
				}, state.data.filters );

				// If it's from 'load more', load them and pass the results down
				var before = this.scope.find('[data-role="activityItem"]').last().attr('data-timestamp');
				var _this  = this;
				this._loadResults( data )
					.done( function (response) {
						var content = $('<div>' + response.results + '</div>');
						// We deduct one here because the SQL does a > or < which won't match all items if the timestamp is exact
						var latest = parseInt( content.find('[data-role="activityItem"]').last().attr('data-timestamp') ) - 1;
						History.replaceState( {
							controller: 'core.front.streams.main',
							latest: latest
						}, document.title, _this._buildRequestURL( { before: before, latest: isNaN( latest ) ? 0 : latest } ) );
			
						self.triggerOn( 'core.front.streams.results', 'updateResults.stream', {
							append: true,
							response: response,
							url: _this._buildRequestURL( { before: before, latest: isNaN( latest ) ? 0 : latest } )
						});
					});
					
			} else if( state.data.action == 'viewToggle' ){
				this._setViewType( state.data.view );
				
				// If it's from the view toggle, load them and pass the results down
				this._loadResults( _.extend( { view: state.data.view }, state.data.filters ) )
					.done( function (response) {
						self.triggerOn( 'core.front.streams.results', 'updateResults.stream', {
							response: response
						});
					});
					
			} else {
				
				// Don't do anything
				Debug.log('skip update');
			}	
		},
		
		/**
		 * Event handler for clicking the Load More button
		 *
		 * @param 		{event} 	e 		Event Object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		loadMoreResults: function (e, data) {
			e.preventDefault();
			
			// Get the last timestap being shown
			var timestamp = this.scope.find('[data-role="activityItem"]').last().attr('data-timestamp');
			var url = this._buildRequestURL( { before: timestamp } );

			this.scope
				.find('[data-role="loadMoreContainer"] [data-action="loadMore"]')
					.addClass('ipsButton_disabled')
					.text( ips.getString('loading') );
			
			History.pushState( {
				controller: 'core.front.streams.main',
				before: timestamp,
				filters: this._getUrlDiff( this._formData ),
				action: 'loadMore'
			}, document.title, url );
		},

		/**
		 * Toggles filters on mobile
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		toggleFilters: function (e) {
			e.preventDefault();

			var filterBar = this.scope.find('[data-role="filterBar"]');
			var toggleButton = $( e.currentTarget );

			if( filterBar.is(':visible') ){
				filterBar.slideUp();
				toggleButton.html( ips.getString('toggleFiltersHidden') ).removeClass('cStreamFilter_toggleShown');
				this.scope.find('[data-role="saveButtonContainer"]').addClass('ipsHide');
			} else {
				filterBar.slideDown();
				toggleButton.html( ips.getString('toggleFiltersShown') ).addClass('cStreamFilter_toggleShown');
			}

			$( document ).trigger( 'closeMenu' );
		},

		/**
		 * When the user switches breakpoints, hide/show filters as appropriate
		 *
		 * @param 		{event} 	e 		Event data
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		breakpointChange: function (e, data) {
			if( data.curBreakName == 'phone' ){
				this.scope.find('[data-role="filterBar"]').addClass('ipsHide').hide();
				this.scope.find('[data-action="toggleFilters"]')
					.html( ips.getString('toggleFiltersHidden') )
					.removeClass('cStreamFilter_toggleShown')
			} else {
				this.scope.find('[data-role="filterBar"]').removeClass('ipsHide');
				/* Slide up animation adds display:none to filterbar */
				if ( ! this.scope.find('[data-role="filterBar"]').is(':visible') ) {
					this.scope.find('[data-role="filterBar"]').slideDown();
					this.scope.find('[data-action="toggleFilters"]')
						.html( ips.getString('toggleFiltersShown') )
						.addClass('cStreamFilter_toggleShown');
				}
			}
		},

		/**
		 * Called when the view is changing
		 *
		 * @param 		{string} 	type 		The view type we're switching to
		 * @returns 	{void}
		 */
		_setViewType: function (type) {
			this._currentView = type;
			
			// Update the button
			this.scope
				.find('[data-action="switchView"]')
					.removeClass('ipsButton_primary')
					.addClass('ipsButton_veryLight')
					.filter('[data-view="' + type + '"]')
						.removeClass('ipsButton_veryLight')
						.addClass('ipsButton_primary');
						
			// Set a cookie
			ips.utils.cookie.set( 'stream_view_' + this._streamID, type, true );
		},

		/**
		 * If we switch into sticky mode on the filter bar, hide any menus
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		stickyChange: function (e, data) {
			this.scope.find('[data-ipsMenu]').trigger('closeMenu');
		},
		
		/**
		 * Handles switching views
		 * Sets a cookie so the preference is remembered
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		switchView: function (e) {
			e.preventDefault();
			
			var type = $( e.currentTarget ).attr('data-view');			
			var url = this._buildRequestURL( { view: type } );
			
			History.pushState( {
				controller: 'core.front.streams.main',
				view: type,
				filters: this._getUrlDiff( this._formData ),
				action: 'viewToggle'
			}, document.title, url );
		},
		
		/**
		 * Handler for the filter form being submitted. When this happens we determine
		 * what data is different, then update the page state in order to get new results
		 *
		 * @param 		{event} 	e 		Event Object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		formSubmitted: function (e, data) {
			this._formData = this._getFormData( data.data );
			var url = this._buildRequestURL();

			History.pushState( {
				controller: 'core.front.streams.main',
				url: url,
				filterData: this._formData,
				action: data.action
			}, document.title, url );
		},
		
		/**
		 * The form has passed us its initial values on page load
		 *
		 * @param 		{event} 	e 		Event Object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		initialFormData: function (e, data) {
			this._formData = this._getFormData( data.data );
		},

		/**
		 * The results controller has sent us the latest-shown timestamp
		 *
		 * @param 		{event} 	e 		Event Object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		latestTimestamp: function (e, data) {
			this._timestamp = data.timestamp;
		},

		/**
		 * Event handler for windw visibility changing
		 * Allows us to slow the rate of auto-updates if the user isn't looking at the page
		 *
		 * @returns 	{void}
		 */
		windowVisibilityChange: function () {
			var hiddenProp = ips.utils.events.getVisibilityProp();

			if( !_.isUndefined( hiddenProp ) ){
				if( document[ hiddenProp] ){
					this._windowFocus = false;
					this._startTimer( this._longInterval );
					this._killTimer = setTimeout( _.bind( this._stopPolling, this ), this._killUpdate * 60 * 1000 );
					Debug.log("Set the kill timer");
				} else {
					this._windowFocus = true;
					this._startTimer( this._shortInterval );
					this._updateTitle(0);
					clearTimeout( this._killTimer );
					Debug.log("Cleared the kill timer");
				}
			} else {
				this._startTimer( this._longInterval );
			}
		},
		
		/**
		 * Loads new results from the server
		 *
		 * @param 		{object} 	data 		Params data object
		 * @param 		{string} 	updateType	The type of update we're doing, e.g. 'update' or 'save'
		 * @param 		{boolean} 	silent		Fetch silently? If true, won't show loading or update stream UI
		 * @returns 	{void}
		 */
		_loadResults: function (data, resetConfig, silent) {
			var self = this;			
			var promise = $.Deferred();
			
			if( !silent ){
				this._setStreamLoading( true );	
			}						
			
			ips.getAjax()( ips.getSetting('stream_config')['url'], {
				type: 'post',
				data: data || {}
			})
				.done( function (response) {
					if( !silent ){
						self._setStreamLoading( false );
						self._updateStreamUI( response );	
					}
					
					promise.resolve( response );
				})
				.fail( function (response) {
					if( !silent ){
						ips.ui.alert.show( {
							type: 'alert',
							message: ips.getString('errorLoadingStream'),
							icon: 'warn'
						});	
					}				
					
					promise.reject();
				});
				
			return promise;
		},
		
		/**
		 * Perform actions after the results have been updated and added to the DOM
		 *
		 * @param 		{event} 	e 		Event Object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		resultsUpdated: function(e, data) {
			if ( this.scope.find('[data-role="activityItem"]').length == 0  ) {
				this.scope.find('[data-role="loadMoreContainer"]').hide();
				this.scope.find('[data-role="streamContent"]').removeClass('ipsStream_withTimeline');
			}
			else{
				this.scope.find('[data-role="streamContent"]').addClass('ipsStream_withTimeline');
				this.scope.find('[data-role="loadMoreContainer"]').show();
			}
		},
		
		/**
		 * Processes an update after new results have been loaded
		 * This method is responsible for the stream UI. The actual results
		 * are processed by core.front.streams.results.
		 *
		 * @param 		{object} 	data 	Event data object from form controller
		 * @returns 	{void}
		 */
		_updateStreamUI: function (response) {
			Debug.log("Updating stream UI...");
			
			// Update blurb
			this.scope.find('[data-role="streamBlurb"]').text( response.blurb.replace(/&#039;/g, "'") );
			this.scope.find('[data-role="streamTitle"]').html( response.title );
		
			// Hide or reset "Load more" button if needed
			if( response.count == 0 ){
				this.scope.find('[data-role="loadMoreContainer"]').show().html( ips.templates.render('core.streams.noMore') );
			} else {
				this.scope.find('[data-role="loadMoreContainer"]').show().html( ips.templates.render('core.streams.loadMore') );
			}	
			
			// Update menu
			if( response.id ){
				var menuItem = $('body').find('[data-streamid="' + response.id + '"] > a');
				menuItem.text( response.title );
			}
		},
		
		/**
		 * Takes a raw form data object and returns only the keys we're interested in
		 *
		 * @param 		{object} 	data	Form data object
		 * @returns 	{object}
		 */
		_getFormData: function (data) {			
			var returnValues = {};
			
			// Keep any data prefixed with 'stream'
			_.each( data, function (val, key){
				if( key.startsWith('stream_') ){
					returnValues[ key ] = val;
				}
			});

			// Remove the __EMPTY from classes
			try {
				if( !_.isUndefined( returnValues['stream_classes']['__EMPTY'] ) ){
					returnValues['stream_classes'] = _.omit( returnValues['stream_classes'], '__EMPTY' );
				}	
			} catch (err) { }			
			
			return returnValues;
		},
		
		/**
		 * Returns an object that only contains the params that are
		 * different to our base stream config, allowing us to use them in
		 * the URL but not duplicate the 'default' stream values
		 *
		 * @param 		{object} 	data	Form data object
		 * @returns 	{object}
		 */
		_getUrlDiff: function (data) {

			var returnedValues = {};
			var defaultValues = ips.getSetting( 'stream_config' );

			// Reset any values that have been altered 
			if ( ! _.isUndefined( defaultValues['changed'] ) ) {
				_.each( defaultValues['changed'], function( val, key ) {
					returnedValues[ ( key == 'containers' ) ? 'stream_containers' : key ] = defaultValues[ key ];
				}  );
			}

			if( !_.size( data ) ){
				return returnedValues;
			}

			// Simple values
			_.each( ['stream_sort', 'stream_include_comments', 'stream_read', 'stream_default_view', 'stream_club_select', 'stream_club_filter'], function (val) {
				if( defaultValues[ val ] != data[ val ] && !_.isUndefined( data[ val ] ) ){
					returnedValues[ val ] = data[ val ];
				}
			});
			
			// Ownership
			if( data['stream_ownership'] == 'custom' ){
				// We're filtering by custom members, so work out if there's any differences
				var newNames = _.isObject( data['stream_custom_members'] ) ? data['stream_custom_members'] : data['stream_custom_members'].split(/\n/);
				var oldNames = defaultValues['stream_custom_members'];
				
				if ( ! _.isObject( oldNames ) ) {
					oldNames = [];
				}
				
				var nameIntersection = _.intersection( newNames, oldNames );

				// Got any names now?
				if ( ! newNames.length ) {
					returnedValues['stream_ownership'] = 'all';
				}
				// If the lengths don't match, include them
				else if( newNames.length !== oldNames.length || nameIntersection.length !== newNames.length ){
					returnedValues['stream_ownership'] = 'custom';
					returnedValues['stream_custom_members'] = data['stream_custom_members'];
				}
			} else if( defaultValues['stream_ownership'] !== data['stream_ownership'] ) {
				returnedValues['stream_ownership'] = data['stream_ownership'];
			}
			
			// Follows
			if( !( defaultValues['stream_follow'] == 'all' && data['stream_follow'] == 'all' ) ){
				if( data['stream_follow'] == 'followed' ){
					var newFollowTypes = _.keys( data['stream_followed_types'] );
					var oldFollowTypes = _.keys( defaultValues['stream_followed_types'] );
					var followIntersection = _.intersection( newFollowTypes, oldFollowTypes );
					
					if( newFollowTypes.length !== oldFollowTypes.length || followIntersection.length !== newFollowTypes.length ){
						returnedValues['stream_follow'] = 'followed';
						returnedValues['stream_followed_types'] = data['stream_followed_types'];
					}
				} else {
					returnedValues['stream_follow'] = data['stream_follow'];
				}			
			}
			
			// Tags
			if( !_.isUndefined( data['stream_tags'] ) && !_.isEmpty( data['stream_tags'].trim() ) ){
				var newTags = _.compact( ( data['stream_tags'] || '' ).split(/\n/) );
				var oldTags = _.compact( ( defaultValues['stream_tags'] || '' ).split(/\n/) );
				var tagIntersection = _.intersection( newTags, defaultValues['stream_tags'] );

				if( newTags.length !== oldTags.length || tagIntersection.length !== newTags.length ){
					returnedValues['stream_tags'] = _.map( newTags, function (str) { return str.trim() } ).join(',');
				}
			} else if ( defaultValues['stream_tags'] ) {
				// There were tags
				returnedValues['stream_tags'] = '';
			}
			
			// Dates
			if( data['stream_date_type'] == 'custom' ){
				var startTest = data['stream_date_range']['start'];

				if( startTest.toString().match( /^[0-9]{9,10}$/ ) )
				{
					var start = data['stream_date_range']['start'];
					var end = data['stream_date_range']['end'];
				}
				else
				{
					var start = new Date( data['stream_date_range']['start'] ).getTime() / 1000;
					var end = new Date( data['stream_date_range']['end'] ).getTime() / 1000;
				}
				
				// Check if there's a start date, and if so, is it different to the stream default?
				if( data['stream_date_range']['start'] && 
					( _.isUndefined( defaultValues['stream_date_range'] ) || start !== defaultValues['stream_date_range']['start'] ) ){
					returnedValues['stream_date_type'] = 'custom';
					returnedValues['stream_date_start'] = start;
				}
				// Check if there's an end date, and if so, is it different to the stream default?
				if( data['stream_date_range']['end'] && 
					( _.isUndefined( defaultValues['stream_date_range'] ) || end !== defaultValues['stream_date_range']['end'] ) ){
					returnedValues['stream_date_type'] = 'custom';
					returnedValues['stream_date_end'] = end;
				}
			} else if( data['stream_date_type'] == 'relative' ){
				// Has the number of relative days changed?
				if( defaultValues['stream_date_relative_days'] !== data['stream_date_relative_days'] ){
					returnedValues['stream_date_type'] = 'relative';
					returnedValues['stream_date_relative_days'] = data['stream_date_relative_days'];
				}
			} else if( defaultValues['stream_date_type'] !== data['stream_date_type'] ){
				returnedValues['stream_date_type'] = data['stream_date_type'];
			}
			
			// Classes
			if( data['stream_classes_type'] == 0 && defaultValues['stream_classes_type'] == 1 ){
				returnedValues['stream_classes'] = {};
			} else {
				var newClasses = _.without( _.keys( data['stream_classes'] ), '__EMPTY' );
				var classIntersection = _.intersection( newClasses, _.keys( defaultValues['stream_classes'] ) );
			
				if( classIntersection.length !== newClasses.length ){
					returnedValues['stream_classes'] = _.omit( data['stream_classes'], '__EMPTY' );
				}	
			}			
			
			// Containers
			var containers = {};
			if ( ! _.isUndefined( data['stream_classes'] ) && _.isObject( data['stream_classes'] ) ) {
				_.each( _.without( _.keys( data['stream_classes'] ), '__EMPTY' ), function( val, key ) {
					var contentType = $('div[data-role="streamContainer"][data-className="' + val.replace( /\\/g, '\\\\' ) + '"]').attr('data-contentKey');
					if ( ! _.isUndefined( contentType ) && $('input[name="stream_containers_' + contentType + '"]').length ) {
						containers[ val ] = $('input[name="stream_containers_' + contentType + '"]').val();
					}
				} );
			}
			
			if ( ! _.isEmpty( containers ) ) {
				if ( ! _.isUndefined( defaultValues['containers'] ) && ! _.isEqual( containers, defaultValues['containers'] ) ) {
					returnedValues['stream_containers'] = containers;
				}
			}
			
			return returnedValues;
		},
		
		/**
		 * Builds a request URL by getting a diff of the current stream params,
		 * and adding any extra params we need.
		 *
		 * @param 		{object} 	extraParams 	Any extra request params to use
		 * @returns 	{void}
		 */
		_buildRequestURL: function (extraParams) {			
			var urlDiff = this._getUrlDiff( this._formData );
			var params = [];
			
			// Loop through each 'diffed' param to build the request URL
			_.each( urlDiff, function (val, key) {
				if( _.isObject( val ) ){
					if( !_.size( val ) ){
						params.push( key + '=' );
					} else {
						var keys = _.keys( val );
						var values = _.values( val );
						
						for( var i = 0; i < keys.length; i++ ){
							var paramValue = ( ! _.isUndefined( values[i] ) ? values[i] : 1 );
							params.push( key + '[' + encodeURIComponent( keys[i] ) + ']=' + paramValue );
						}	
					}					
				} else {
					params.push( key + '=' + encodeURIComponent( val ) );
				}
			});
			
			// Do we have extra params?
			if( _.isObject( extraParams ) ){
				_.each( extraParams, function (val, key) {
					params.push( key + '=' + encodeURIComponent( val ) );
				});
			}

			return this._baseURL + '&' + params.join('&');
		},

		/**
		 * Selects the value of the textbox in the share popup
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data	Event data object
		 * @returns 	{void}
		 */
		menuOpened: function (e, data) {
			data.menu.find('input[type="text"]').focus().get(0).select();
		},
		
		/**
		 * Confirms user click to remove this stream
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		removeStream: function (e) {
			e.preventDefault();
			
			ips.ui.alert.show({
				type: 'confirm',
				message: ips.getString('confirmRemoveStream'),
				callbacks: {
					ok: function () {
						window.location = $(e.currentTarget).attr('href') + '&wasConfirmed=1';
					},
				}
			});
		},
		
		/**
		 * Toggles a stream as the shortcut
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		toggleDefault: function (e) {
			e.preventDefault();
		
			var self  = this;
			var link  = $( e.currentTarget );
			var value = link.attr('data-change');
			var url   = link.attr('href');
			
			ips.getAjax()( url )
				.done( function (response) {					
					if( value == 1 ){
						self.scope.find('a[data-action="toggleStreamDefault"][data-change="0"]').removeClass('ipsHide');
						self.scope.find('a[data-action="toggleStreamDefault"][data-change="1"]').addClass('ipsHide');
					} else {
						self.scope.find('a[data-action="toggleStreamDefault"][data-change="0"]').addClass('ipsHide');
						self.scope.find('a[data-action="toggleStreamDefault"][data-change="1"]').removeClass('ipsHide');
					}
						
					if( !response.title ) {
						$('a[data-action="defaultStream"]').hide();
					} else {
						$('a[data-action="defaultStream"]')
							.attr('href', response.url )
							.show()
							.find('span')
								.html( response.title );
					}
	
					if( value == 1 ){
						if( !ips.utils.responsive.enabled || !ips.utils.responsive.currentIs('phone') && $('a[data-action="defaultStream"]').is(':visible') ){
							self._showStreamTooltip( response.tooltip );
						} else {
							ips.utils.anim.go( 'pulseOnce', $('a[data-action="defaultStream"]') );
						}
					}			
			});
		},

		/**
		 * Shows a tooltip on the 'default stream' link to help the user
		 *
		 * @param 		{string} 	title 		TItle of the default stream
		 * @returns 	{void}
		 */
		_showStreamTooltip: function (title) {
			if( !this._tooltip ){
				// Build it from a template
				var tooltipHTML = ips.templates.render( 'core.tooltip', {
					id: 'elDefaultStreamTooltip_' + this.controllerID
				});

				// Append to body
				ips.getContainer().append( tooltipHTML );

				this._tooltip = $('#elDefaultStreamTooltip_' + this.controllerID );
			} else {
				this._tooltip.hide();
			}

			if( this._tooltipTimer ){
				clearTimeout( this._tooltipTimer );
			}

			this._tooltip.text( ips.getString('streamDefaultTooltip', {
				title: title
			}));

			// Get image
			var streamLink = $('a[data-action="defaultStream"]:visible');
			var self = this;

			// Now position it
			var positionInfo = {
				trigger: streamLink.first(),
				target: this._tooltip,
				center: true,
				above: true
			};

			var tooltipPosition = ips.utils.position.positionElem( positionInfo );

			$( this._tooltip ).css({
				left: tooltipPosition.left + 'px',
				top: tooltipPosition.top + 'px',
				position: ( tooltipPosition.fixed ) ? 'fixed' : 'absolute',
				zIndex: ips.ui.zIndex()
			});

			if( tooltipPosition.location.vertical == 'top' ){
				this._tooltip.addClass('ipsTooltip_top');
			} else {
				this._tooltip.addClass('ipsTooltip_bottom');
			}

			this._tooltip.show();

			setTimeout( function () {
				if( self._tooltip && self._tooltip.is(':visible') ){
					ips.utils.anim.go( 'fadeOut', self._tooltip );
				}
			}, 3000);
		},
		
		/**
		 * Puts the stream into loading mode by adding an overlaid loadinb div
		 *
		 * @param 		{boolean} 	loading 		Are we loading?
		 * @returns 	{void}
		 */
		_setStreamLoading: function (loading) {
			var stream = this.scope.find('[data-role="streamContent"]');

			if( !loading ){
				this._streamLoadingOverlay.hide();
				stream.css({
					opacity: "1"
				});
				return;
			} else {

				if( !this._streamLoadingOverlay ){
					this._streamLoadingOverlay = $('<div/>').addClass('ipsLoading');
					ips.getContainer().append( this._streamLoadingOverlay );
				}

				// Get dims & position			
				var dims = ips.utils.position.getElemDims( stream );
				var position = ips.utils.position.getElemPosition( stream );

				this._streamLoadingOverlay.show().css({
					left: position.viewportOffset.left + 'px',
					top: position.viewportOffset.top + $( document ).scrollTop() + 'px',
					width: dims.width + 'px',
					height: dims.height + 'px',
					position: 'absolute',
					zIndex: ips.ui.zIndex()
				});

				stream.css({
					opacity: "0.5"
				});
			}

		},

		/**
		 * Sets or resets the timer to check for new posts
		 *
		 * @param 		{number} 	interval 		Interval (in seconds) between checks
		 * @returns 	{void}
		 */
		_startTimer: function (interval) {
			if( !this._autoUpdate ){
				return;
			}
			
			if( !interval ){
				interval = this._shortInterval;
			}

			if( this._timer ){
				clearInterval( this._timer );
			}

			this._timer = setInterval( _.bind( this._autoFetchNew, this ), interval * 1000 );	
		},

		/**
		 * Toggles auto-polling for new content depending on the status passed in as a param
		 *
		 * @param 		{boolean} 	status 		Enable polling?
		 * @returns 	{void}
		 */
		_togglePolling: function (status) {
			// Only enable if we have polling possible
			if( !this._autoUpdate ){
				clearInterval( this._timer );
				return;
			}

			if( status ){
				this._startTimer();
				this.scope.find('[data-role="updateMessage"]').show();
			} else {
				if( this._timer ){
					clearInterval( this._timer );
					this.scope.find('[data-role="updateMessage"]').hide();
				}
			}
		},

		/**
		 * Method to check for new activity results on the server.
		 *
		 * @returns 	{void}
		 */
		_autoFetchNew: function () {
			var self = this;

			if( !_.isNumber( this._timestamp ) || _.isNaN( this._timestamp ) ){
				Debug.log("Timestamp not a number");
				clearInterval( this._timer );
				return;
			}

			// Fetch the results, then pass them to the results controller to display
			this._loadResults( { after: this._timestamp }, false, true )
				.done( function (response) {

					var count = parseInt( response.count );

					// If auto-polling is now disabled, stop everything
					if( response.error && response.error == 'auto_polling_disabled' ){
						self.scope.find('[data-role="updateMessage"]').remove();
						clearInterval( self._timer );
						return;
					}

					// Nothing returned?
					if( _.isNaN( count ) || count == 0 ){
						return;
					}

					self.triggerOn( 'core.front.streams.results', 'resultsTeaser.stream', {
						response: response
					});
				});
		},

		/**
		 * Stops the auto-updating from running for good
		 *
		 * @returns 	{void}
		 */
		_stopPolling: function () {
			clearInterval( this._timer );
			this._autoUpdate = false;
			this.scope.find('[data-role="updateMessage"]').html( ips.getString('autoUpdateStopped') );
			Debug.log("Stopped polling due to user inactivity");
		},

		/**
		 * Updates the browser title with an 'unseen count' of new items
		 *
		 * @param 		{number} 	count 		Number of unseen items
		 * @returns 	{void}
		 */
		_updateTitle: function (count) {
			// Moved to instant notifications		
		},

		/**
		 * Marks everything in this stream as read
		 *
		 * @returns {void}
		 */
		markAllRead: function () {
			this.scope
				.find('.ipsStreamItem_unread')
				.removeClass('ipsStreamItem_unread')
				.find('.ipsItemStatus:not(.ipsItemStatus_read)')
				.addClass('ipsItemStatus_read');

		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/streams" javascript_name="ips.streams.results.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.streams.results.js - Manages results in a stream
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.streams.results', {		
		_streamID: 0,
		_newUpdates: $('<div/>'),
		_waitingCount: 0,
		_config: {},
		
		initialize: function () {
			this.on( 'updateResults.stream', this.updateResults );
			this.on( 'resultsTeaser.stream', this.resultsTeaser );
			this.on( 'click', '[data-action="insertNewItems"]', this.insertNewItems );
			this.on( 'click', 'a[data-linkType]', this.clickResult );
			
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			if( this.scope.attr('data-streamID') ){
				this._streamID = this.scope.attr('data-streamID');
			}
			
			this._config = ips.getSetting('stream_config');
		},
		
		/**
		 * Determine whether we need to replace all results, or append them
		 *
		 * @param 	{object} 	data 	Data object that originated from the ajax request
		 * @returns {void}
		 */
		updateResults: function (e, data) {
			if( data.append ){
				this._appendNewResults( data );
			} else {
				this._replaceWithNewResults( data );
			}
		},
		
		/**
		 * Replaces all results in the stream with new results
		 *
		 * @param 	{object} 	data 	Data object that originated from the ajax request
		 * @returns {void}
		 */
		_replaceWithNewResults: function (data) {
			this._config = JSON.parse( data.response.config );

			// Do we need to replace the entire HTML
			if( data.response.results.indexOf('core.front.streams.results') !== -1 ){
				var content = $('<div>' + data.response.results + '</div>');
				var root = content.find('[data-controller="core.front.streams.results"]');
				
				this.cleanContents();
				this.scope
					.html( root.html() )
					.attr( 'data-streamReadType', root.attr('data-streamReadType') )
					.attr( 'data-streamURL', root.attr('data-streamURL') )
					.attr( 'data-streamID', root.attr('data-streamID') );				
			} else {
				ips.controller.cleanContentsOf( this.scope.find('[data-role="streamContent"]') );
				this.scope.find('[data-role="streamContent"]').html( data.response.results );
			}
			
			this.trigger('resultsUpdated.stream', {
				response: data.response
			});

			$( document ).trigger( 'contentChange', [ this.scope ] );
		},
		
		/**
		 * Appends new results to the existing results
		 *
		 * @param 	{object} 	data 	Data object that originated from the ajax request
		 * @returns {void}
		 */
		_appendNewResults: function (data) {			
			var content = $('<div>' + data.response.results + '</div>');

			// Get the latest time bubble
			var latestBubble = this.scope.find('[data-timeType]').last().attr('data-timeType');

			// If the new content has that bubble, get rid of it
			content.find('[data-timeType="' + latestBubble + '"]').remove();

			// Get items ready to display
			content
				.find('[data-role="activityItem"]')
					.attr('data-new', true)
					.css({
						opacity: "0"
					});
			
			// Forms created on the next slice post back to the default URL which doesn't have this silce, so updated action here
			if ( ! _.isUndefined( data.url ) ) {
				content
					.find('.ipsComment_content form[action="' + this._config['url'] + '"]')
					.attr('action', data.url );
			}
			
			// Get th count of current items in the feed
			var existingItems = this.scope.find('[data-role="activityItem"]');
			var count = existingItems.length;

			// Get the last activity item and insert new content
			existingItems.last().after( content );

			// Init only the new items
			this.scope.find('[data-role="activityItem"]').slice( count ).each( function () {
				$( document ).trigger( 'contentChange', [ $( this ) ] );	
			});

			this._animateNewItems();
		},
		
		/**
		 * When we click a result, we're going to track it in local storage
		 * so that when the user returns, we can scroll to it and highlight it
		 *
		 * @returns {void}
		 */
		clickResult: function (e, data) {
			
			var item = $( e.target ).closest( '[data-indexID]' );
			var star = item.find('a[data-linkType="star"]');
			
			// Remove 'active' from all items
			this.scope.find('.ipsStreamItem_active').removeClass('ipsStreamItem_active');
			
			// If this result is unread, mark it read.
			// Add a class that we'll use to indicate last click
			item
				.addClass('ipsStreamItem_active')
				.find('.ipsStreamItem_unread')
					.removeClass('ipsStreamItem_unread');
					
			// Hide the unread marker if needed
			if( star.attr('data-iPostedIn') ){
				star.find('span.ipsItemStatus').addClass('ipsItemStatus_read').addClass('ipsItemStatus_posted').unwrap();
			} else {
				star.addClass('ipsHide');
			}
		},

		/**
		 * Event handler for clicking the teaser button when there's new results queued to show
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		insertNewItems: function (e) {
			e.preventDefault();

			var insertBefore = null;

			// First, we need to figure out the timing bubble. Our new results may contain a bubble too,
			// so we have to square that away with the ones already showing in the feed.
			if( this.scope.find('[data-timeType="past_hour"]').length ){
				insertBefore = this.scope.find('[data-timeType="past_hour"]').siblings('[data-role="activityItem"]').first();
				// If we already have a 'past_hour' bubble in the stream, then remove it from the new content
				this._newUpdates.find('[data-timeType]').remove();
			} else {
				insertBefore = this.scope.find('[data-timeType], [data-role="activityItem"]').first();
			}

			// Add a bar at the end of the content to show the user where they've seen up to
			this.scope.find('[data-role="unreadBar"]').remove();
			this._newUpdates.append( ips.templates.render('core.streams.unreadBar') );

			// Lets set some styles on the items to show
			this._newUpdates
				.find('[data-role="activityItem"]')
					.attr('data-new', true)
					.css({
						opacity: "0"
					});

			// Insert the new content
			insertBefore.before( this._newUpdates.html() );

			// Reinit
			$( document ).trigger( 'contentChange', [ this.scope ] );

			// Remove the teaser button and animate the new items in
			this.scope.find('[data-action="insertNewItems"]').remove();
			this._animateNewItems();

			// Reset our tracking vars
			this._newUpdates = $('<div/>');
			this._waitingCount = 0;
		},

		/**
		 * Finds the new items in the feed and animates them into view
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		_animateNewItems: function () {
			var newItems = this.scope.find('[data-new]');
			var delay = 200;

			// Now fade in one by one, with a delay
			newItems.each( function (index) {
				var d = ( index * delay );
				$( this ).delay( ( d > 1200 ) ? 1200 : d ).animate({
					opacity: "1"
				}).removeAttr('data-new');
			});	
		},

		/**
		 * Method to check for new activity results on the server.
		 * We don't show the results immediately, otherwise it would bounce the user around the page.
		 * Instead, we store the new results and show a teaser block at the top of the feed. When
		 * the user clicks the teaser, then we insert the results.
		 *
		 * @param 		{event} 	e 		Event object
		 * @param 		{object} 	data 	Event data object
		 * @returns 	{void}
		 */
		resultsTeaser: function (e, data) {
			var self = this;
			var response = data.response;
			var count = parseInt( response.count );

			// Get the count and latest timestamp
			var tmp = $('<div>' + response.results + '</div>');

			self.trigger( 'latestTimestamp.stream', {
				timestamp: parseInt( tmp.find('[data-timestamp]').first().attr('data-timestamp') )
			});

			self._waitingCount += count;

			// If we have a date bubble in this new content, we need to check any other new content we haven't
			// shown yet has them too, and remove them.
			if( tmp.find('[data-timeType]').length ){
				var type = tmp.find('[data-timeType]').attr('data-timeType');
				self._newUpdates.find('[data-timeType="' + type + '"]').remove();
			}

			self._newUpdates.prepend( tmp.contents() );

			// Build the teaser
			var teaser = ips.templates.render('core.streams.teaser', {
				count: self._waitingCount,
				words: ips.pluralize( ips.getString('newActivityItems'), [ self._waitingCount, self._waitingCount ] )
			});

			// If a teaser already exists, replace it; otherwise, insert at top
			if( self.scope.find('[data-action="insertNewItems"]').length ){
				self.scope.find('[data-action="insertNewItems"]').replaceWith( teaser );
				self.scope.find('[data-action="insertNewItems"]').show();
			} else {
				self.scope.find('[data-role="activityItem"]').first().before( teaser );
				self.scope.find('[data-action="insertNewItems"]').slideDown();
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/support" javascript_name="ips.support.contact.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.support.contact.js
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.support.contact', {

		initialize: function () {
			this.on( 'change', '#elCheckbox_support_request_extra_admin', this.acpAccountCheckboxChange );
			this.on( document, 'refreshSupportSummary', this.embedSummary );

			this.embedSummary();
		},

		/**
		 * Toggle handler for support account function
		 *
		 * @param	{event}	e	Change event
		 * @returns	{void}
		 */
		acpAccountCheckboxChange: function (e) {
			if ( !$('#elCheckbox_support_request_extra_admin').is(':checked') ) {
				ips.ui.alert.show({
					type: 'confirm',
					message: ips.getString('supportAcpAccountHead'),
					subText: ips.getString('supportAcpAccountDisableBlurb'),
					icon: 'warn',
					buttons: {
						ok: ips.getString('supportAcpAccountDisableYes'),
						cancel: ips.getString('supportAcpAccountDisableNo')
					},
					callbacks: {
						ok: function(){
							$('#elCheckbox_support_request_extra_admin').prop( 'checked', true );
						}
					}
				});
			}
		},


		/**
		 * Get the summary and embed on the form
		 *
		 * @returns	{void}
		 */
		embedSummary: function() {
			var summary = $(document).find('[data-controller="core.admin.support.dashboard"] [data-role="summary"]');

			if( summary.length )
			{
				var html = $('<div>').append( summary.clone() );
				html.find( '.ipsPos_right' ).remove();
				html.find('[data-role="summary"]').addClass('ipsMargin_bottom');
				html.find('[data-role="summaryText"]').append( ips.templates.render('support.ticket.supportSummary' ) );
				html = html.html();

				this.scope.find('ul.ipsForm').prepend('<li>' + html + '</li>');
			}
		}	
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/support" javascript_name="ips.support.dashboard.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.support.dashboard.js
 *
 * Author: Brandon Farber
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.support.dashboard', {
		// Counters
		blocksToLoad: 0,
		blocksLoaded: 0,

		criticalIssuesCount: 0,
		recommendedIssuesCount: 0,

		// AJAX guide searching
		results: {},
		_ajax: {},
		_timers: {},
		_lastValue: '',
		_input : null,

		/**
		 * Initialization method
		 *
		 * @returns {void}
		 */
		initialize: function () {
			this.on( 'click', '[data-role="checkAgain"]', this.checkAgain );
			this.on( 'click', '[data-role="clearCaches"]', this.clearCaches );
			this.on( 'click', '[data-role="disableCustomizations"]', this.disableCustomizations );
			this.on( 'click', '[data-role="enableCustomizations"]', this.enableCustomizations );
			this.on( 'click', '[data-action="enableThirdPartyPart"]', this.enableSingleCustomizations );
			this.on( document, 'customizationsEnabled', this.setCustomizationsButton );

			this._input = $('#elInput_support_advice_search');
			this.on( 'focus', '#elInput_support_advice_search', this.fieldFocus );
			this.on( 'blur', '#elInput_support_advice_search', this.fieldBlur );
			this.on( 'submit', '#guidesForm form', this.guideFormSubmit );

			this.initializeBlocks();
		},

		/**
		 * Stop guide search form submissions
		 *
		 * @param	{event}		e	Event object
		 * @returns	{void}
		 */
		guideFormSubmit: function (e) {
			e.preventDefault();
		},

		/**
		 * Event handler for focusing in the search box
		 * Set a timer going that will watch for value changes. If there's already a value,
		 * we'll show the results immediately
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		fieldFocus: function (e) {
			// Set the timer going
			this._timers.focus = setInterval( _.bind( this.guideSearch, this ), 700 );
		},

		/**
		 * Event handler for field blur
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		fieldBlur: function (e) {
			clearInterval( this._timers );
		},

		/**
		 * Guides live search
		 *
		 * @param	{event}	e	Event
		 * @returns	void
		 */
		 guideSearch: function( e ) {
			var searchTerm = this._input.val().trim();

			if( searchTerm == this._lastValue ) {
				return;
			}

			if( searchTerm.length < 3 ) {
				if( !$('#featuredGuides').is(':visible') && searchTerm.length == 0 ) {
					ips.utils.anim.go( 'fadeIn fast', $('#featuredGuides') );
					ips.utils.anim.go( 'fadeOut fast', $('#guideSearchResults') );
				}
				return;
			}

			this._lastValue = searchTerm;

		 	$('#guideSearchResults > ul').html('').parent().addClass('ipsLoading');

		 	if( $('#featuredGuides').is(':visible') ) {
				ips.utils.anim.go( 'fadeOut fast', $('#featuredGuides') );
				ips.utils.anim.go( 'fadeIn fast', $('#guideSearchResults') );
		 	}

			var self = this;

			// Abort any requests running now
			if( _.size( this._ajax ) ){
				_.each( this._ajax, function (ajax) {
					try {
						if( _.isFunction( ajax.abort ) ) {
							ajax.abort();
							Debug.log('aborted ajax');
						}
					} catch (err) { }
				});
			}

			// Check caches
		 	if( !_.isUndefined( this.results[ searchTerm ] ) )
		 	{
		 		this.showResults( this.results[ searchTerm ] );
		 		return;
		 	}

			ips.getAjax()('?app=core&module=support&controller=support&do=guideSearch', {
				dataType: 'json',
				data: {
					search_term: encodeURIComponent( searchTerm )
				}
			}).done( function (response) {
				
				self.results[ searchTerm ] = response;
				
				self.showResults( response );
			}).fail( function (err) {
				// fail gets called when it's aborted, so deliberately do nothing here
			});
		},

		/**
		 * Process the guide search results and display
		 *
		 * @param	{object}	results		Search results
		 * @returns	{void}
		 */
		showResults: function( results ) {
			$('#guideSearchResults').removeClass('ipsLoading');

			var html = '';
			if( results.length )
			{
				_.each( results, function( result ) {
					html += ips.templates.render('support.guideSearch', result );
				});
			}
			else
			{
				html = ips.templates.render('support.guideSearch.noResults' );
			}

			if( html ) {
				$('#guideSearchResults > ul').html( html );
			} else {
				// Show "no results"
			}
		},

		/**
		 * Initializes all blocks
		 *
		 * @returns {void}
		 */
		initializeBlocks: function() {
			var self = this;

			self.blocksToLoad	= 0;
			self.blocksLoaded	= 0;
			self.criticalIssuesCount	= 0;
			self.recommendedIssuesCount	= 0;

			_.each( $(this.scope).find('[data-role="patchworkItem"]'), function( elem ) {
				$(elem).find('[data-role="supportBlock"]').html('').addClass( 'ipsLoading' );
				$(elem).find('[data-iconType]').hide();
				$(elem).removeClass( 'elCritical' );
				self.loadBlock( $(elem).attr('data-blockid') );
				self.blocksToLoad++;
			});
		},

		/**
		 * Callback when we click the "check again" button. Resets our counters and reinitializes.
		 *
		 * @returns {void}
		 */
		checkAgain: function() {
			this.blocksToLoad = 0;
			this.blocksLoaded = 0;

			this.scope.find('[data-role="summary"]').hide();

			this.initializeBlocks();
		},

		/**
		 * Callback when we click the "disable customizations" button.
		 *
		 * @param	{event} 	e 	Click event
		 * @returns {void}
		 */
		disableCustomizations: function( e ) {
			e.preventDefault();

			$(e.target).prop( 'disabled', true ).attr( 'data-oldText', $(e.target).text() ).text( ips.getString('supportDisablingCustomizations') );

			var self = this;
			ips.getAjax()( $(e.target).attr('href') )
				.done( function( response ) {
					self.scope.find('[data-role="customizationsWrapper"]').html( response );

					self.scope.find('[data-role="disableCustomizations"]')
						.text( $(e.target).attr('data-oldText') )
						.prop( 'disabled', false )
						.hide();
					self.scope.find('[data-role="enableCustomizations"]').show();

					$( document ).trigger( 'contentChange', [ self.scope ] );
				});
		},

		/**
		 * Callback when we click the "re-enable customizations" button.
		 *
		 * @param	{event} 	e 	Click event
		 * @returns {void}
		 */
		enableCustomizations: function( e ) {
			e.preventDefault();

			$(e.target).prop( 'disabled', true ).attr( 'data-oldText', $(e.target).text() ).text( ips.getString('supportEnablingCustomizations') );
			
			var self = this;
			ips.getAjax()( $(e.target).attr('href') )
				.done( function( response ) {
					self.scope.find('[data-role="disableCustomizations"]').show();
					self.scope.find('[data-role="enableCustomizations"]').prop( 'disabled', false ).text( $(e.target).attr('data-oldText') ).hide();

					var container = self.scope.find('[data-role="disabledInformation"]');
					container.find('.ipsType_warning').removeClass('ipsType_warning').addClass('ipsType_neutral');
					container.find('.fa-exclamation-triangle').removeClass('fa-exclamation-triangle').addClass('fa-info-circle');
					container.find('.ipsButton_negative').removeClass('ipsButton_negative').addClass('ipsButton_light');
					container.find('[data-role="disabledMessage"]').hide();
					container.find('[data-role="enabledMessage"]').show();
					container.find('[data-action="enableThirdPartyPart"]').remove();

					$( document ).trigger( 'contentChange', [ self.scope ] );
				});
		},

		/**
		 * Callback when we click the "re-enable customizations" button.
		 *
		 * @param	{event} 	e 	Click event
		 * @returns {void}
		 */
		enableSingleCustomizations: function( e ) {
			e.preventDefault();

			$(e.target).prop('disabled', true).text( ips.getString('supportEnablingCustomizations') );
			
			var self = this;
			ips.getAjax()( $(e.target).attr('href') + '&enable=1&type=' + $(e.target).attr('data-type') )
				.done( function( response ) {
					var container = $(e.target).closest('li');
					container.find('.ipsType_warning').removeClass('ipsType_warning').addClass('ipsType_neutral');
					container.find('.fa-exclamation-triangle').removeClass('fa-exclamation-triangle').addClass('fa-info-circle');
					container.find('.ipsButton_negative').removeClass('ipsButton_negative').addClass('ipsButton_light');
					container.find('[data-role="disabledMessage"]').hide();
					container.find('[data-role="enabledMessage"]').show();

					if( self.scope.find('.ipsButton_negative').length < 1 )
					{
						self.scope.find('[data-role="disableCustomizations"]').show();
						self.scope.find('[data-role="enableCustomizations"]').hide();

						$( document ).trigger( 'customizationsEnabled' );
					}
					
					$(e.target).remove();

					$( document ).trigger( 'contentChange', [ self.scope ] );
				});
		},

		/**
		 * Callback when we click the "clear caches" button.
		 *
		 * @returns {void}
		 */
		clearCaches: function( e ) {
			e.preventDefault();
			var button = this.scope.find('[data-role="clearCaches"]');

			if( button.hasClass( 'ipsButton_disabled' ) ) {
				return;
			}

			var self = this;
			ips.getAjax()( '?app=core&module=support&controller=support&do=clearCaches' )
				.done( function( response ) {
					ips.ui.flashMsg.show( ips.getString( 'health_caches_cleared' ) );
				})
				.always( function() {
					self.scope.find('[data-role="clearCaches"]').removeClass( 'ipsButton_disabled' );
				});
		},

		/**
		 * Load an individual block
		 *
		 * @param	{string} 	blockid 	The block id to load
		 * @returns {void}
		 */
		loadBlock: function( blockid ) {
			var loadBlockUrl = '?app=core&module=support&controller=support&do=getBlock&block=' + blockid;
			var self = this;

			ips.getAjax()( loadBlockUrl )
				.done( function( response ) {
					self.scope.find('[data-blockid="' + blockid + '"] [data-role="supportBlock"]').html( response.html ).removeClass( 'ipsLoading' );
				
					$( document ).trigger( 'contentChange', [ self.scope.find('[data-blockid="' + blockid + '"]') ] );

					self.blocksLoaded++;
					self.criticalIssuesCount = self.criticalIssuesCount + parseInt( response.criticalIssues );
					self.recommendedIssuesCount = self.recommendedIssuesCount + parseInt( response.recommendedIssues );

					self.scope.find('[data-blockid="' + blockid + '"] [data-iconType]').hide();

					if( parseInt( response.criticalIssues ) )
					{
						ips.utils.anim.go( 'fadeIn slow', self.scope.find('[data-blockid="' + blockid + '"] [data-iconType="critical"]') );
						self.scope.find('[data-blockid="' + blockid + '"]').addClass( 'elCritical' );
					}
					else if( parseInt( response.recommendedIssues ) )
					{
						ips.utils.anim.go( 'fadeIn slow', self.scope.find('[data-blockid="' + blockid + '"] [data-iconType="recommended"]') );
					}

					if( self.blocksLoaded == self.blocksToLoad )
					{
						self.finishSetup();
					}
				})
				.fail( function(response)
					{
						self.scope.find('[data-blockid="' + blockid + '"] [data-role="supportBlock"]').html( ips.getString("hookscanner_error") ).removeClass( "ipsLoading" );
						self.scope.find('[data-blockid="' + blockid + '"]').addClass( 'elCritical' );
						$( document ).trigger( 'contentChange', [ self.scope.find('[data-blockid="' + blockid + '"]') ] );

						self.blocksLoaded++;
					});
		},

		/**
		 * Callback once all blocks have loaded
		 *
		 * @returns {void}
		 */
		finishSetup: function() {
			this.scope.find('[data-role="summaryText"]').html( ips.pluralize( ips.getString( 'health_check_summary' ), [ this.criticalIssuesCount, this.recommendedIssuesCount ] ) );

			ips.utils.anim.go( 'fadeIn slow', this.scope.find('[data-role="summary"]') );

			$(document).trigger('refreshSupportSummary');
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/support" javascript_name="ips.support.md5.js" javascript_type="controller" javascript_version="107643" javascript_position="1000450">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.support.md5.js
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.support.md5', {

		initialize: function () {
			this.on( 'click', '[data-action=&quot;downloadDelta&quot;]', this.downloadDelta );
		},

		downloadDelta: function (e) {
			e.preventDefault();
			
			$(this.scope).find('[data-role=&quot;initialScreen&quot;]').hide();
			$(this.scope).find('[data-role=&quot;downloadForm&quot;]').show();
			
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.api.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.api.js - API Docs controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.system.api', {

		initialize: function () {
			this.on( 'click', '[data-action="showEndpoint"]', this.showEndpoint );
			this.on( 'click', '[data-action="toggleBranch"]', this.toggleBranch );
		},

		/**
		 * Event handler for clicking a branch in the listing.
		 * Expends or collapses the branch
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		toggleBranch: function (e) {
			e.preventDefault();
			var branchTrigger = $( e.currentTarget );
			var branchItem = branchTrigger.parent();

			if( branchItem.hasClass('cApiTree_inactiveBranch') ){
				ips.utils.anim.go( 'fadeInDown', branchItem.find(' > ul') );

				branchItem
					.removeClass('cApiTree_inactiveBranch')
					.addClass('cApiTree_activeBranch');
			} else {
				branchItem.find(' > ul').hide();

				branchItem
					.removeClass('cApiTree_activeBranch')
					.addClass('cApiTree_inactiveBranch');
			}
		},

		/**
		 * Dynamically loads an endpoint reference
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		showEndpoint: function (e) {
			e.preventDefault();

			var self = this;
			var url = $( e.currentTarget ).attr('href');

			// Make all endpoints inactive
			this.scope.find('.cApiTree_activeNode').removeClass('cApiTree_activeNode');

			// Make this one active
			$( e.currentTarget ).parent('li').addClass('cApiTree_activeNode');

			// Set the content area to loading
			this.scope.find('[data-role="referenceContainer"]')
				.css({
					height: String(this.scope.find('[data-role="referenceContainer"]').height())
				})
				.html( 
					$('<div/>')
						.addClass('ipsLoading')
						.css({ height: '300px' })
				);

			ips.getAjax()( url )
				.done( function (response) {
					self.scope.find('[data-role="referenceContainer"]')
						.html( response )
						.css({ 
							height: 'auto'
						});
				})
				.fail( function () {
					window.location = url;
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.apiPermissions.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.apiPermissions.js - API Permissions form
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.system.apiPermissions', {

		initialize: function () {
			this.on( 'click', '[data-action="toggleSection"]', this.toggleSection );
			this.on( 'click', '.cApiPermissions [data-action="checkAll"]', this.checkAll );
			this.on( 'click', '.cApiPermissions [data-action="checkNone"]', this.checkNone );
			this.on( 'click', '.cApiPermissions_header [data-action="checkAll"]', this.checkAllHeader );
			this.on( 'click', '.cApiPermissions_header [data-action="checkNone"]', this.checkNoneHeader );
			this.setup();
		},

		setup: function () {
			var self = this;
			var sections = this.scope.find('.cApiPermissions > li');

			sections.each( function () {
				self._calculatedCheckedEndpoints( $( this ) );
			});
		},

		/**
		 * Toggles the display of a section when a subheader is clicked
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		toggleSection: function (e) {
			var header = $( e.currentTarget ).parent();

			if( header.hasClass('cApiPermissions_open') ){
				this._collapseSection( header );
			} else {
				this._expandSection( header );
			}
		},

		/**
		 * Checks all toggles for an app
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		checkAllHeader: function (e) {
			e.preventDefault();
			var self = this;
			var header = $( e.currentTarget ).closest('.cApiPermissions_header');
			var sections = header.next('.cApiPermissions').find('> li');

			sections.each( function () {
				var next = $( this ).find('> ul');

				if( !next.is(':visible') ){
					next.animationComplete( function () {
						setTimeout( function () {
							self._togglePermissions( true, next );
						}, 300 );
					});

					self._expandSection( $( this ) );	
				} else {
					self._togglePermissions( true, next );
				}
			})
		},

		/**
		 * Checks all toggles for an app
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		checkNoneHeader: function (e) {
			e.preventDefault();
			var self = this;
			var header = $( e.currentTarget ).closest('.cApiPermissions_header');
			var sections = header.next('.cApiPermissions').find('> li');

			sections.each( function () {
				var next = $( this ).find('> ul');

				if( !next.is(':visible') ){
					next.animationComplete( function () {
						setTimeout( function () {
							self._togglePermissions( false, next );
						}, 300 );
					});

					self._expandSection( $( this ) );	
				} else {
					self._togglePermissions( false, next );
				}	
			})
		},

		/**
		 * Checks all toggles in the section, opening the section too if necessary
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		checkAll: function (e) {
			e.preventDefault();
			var self = this;
			var header = $( e.currentTarget ).parents('li').first();
			var next = header.find('> ul');

			// If the section isn't visible, do the toggling after the section has
			// animated in, so that the user can see the change happen. Otherwise, just do it immediately
			if( !next.is(':visible') ){
				next.animationComplete( function () {
					setTimeout( function () {
						self._togglePermissions( true, next );
					}, 300 );
				});

				this._expandSection( header );	
			} else {
				this._togglePermissions( true, next );
			}	
		},

		/**
		 * Unchecks all toggles in the section, opening the section too if necessary
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		checkNone: function (e) {
			e.preventDefault();
			var self = this;
			var header = $( e.currentTarget ).parents('li').first();
			var next = header.find('> ul');

			// If the section isn't visible, do the toggling after the section has
			// animated in, so that the user can see the change happen. Otherwise, just do it immediately
			if( !next.is(':visible') ){
				next.animationComplete( function () {
					setTimeout( function () {
						self._togglePermissions( false, next );
					}, 300 );
				});

				this._expandSection( header );	
			} else {
				this._togglePermissions( false, next );
			}			
		},

		_calculatedCheckedEndpoints: function (section) {
			var totalEndpoints = section.find('input[name*="access"]');
			var checkedEndpoints = totalEndpoints.filter(':checked');
			var endpointSpan = section.find('[data-role="endpointOverview"]');

			if( section.hasClass('cApiPermissions_open') ){
				endpointSpan.hide();
			} else {
				var text = ips.getString('apiEndpoints_all');

				if( !checkedEndpoints.length ){
					text = ips.getString('apiEndpoints_none');
				} else if( totalEndpoints.length !== checkedEndpoints.length ){
					text = ips.pluralize( ips.getString( 'apiEndpoints_some', { checked: checkedEndpoints.length } ), totalEndpoints.length );	
				}

				endpointSpan.text( text ).show();
			}
		},

		/**
		 * Sets all checkboxes to the given state in the given container
		 *
		 * @param	{boolean} 	state 		The state to which checkboxes will be set
		 * @param 	{element} 	container 	The container in which the checkboxes must exist
		 * @returns {void}
		 */
		_togglePermissions: function (state, container) {
			container.find('input[type="checkbox"]:not( [disabled] )').prop('checked', state).change();
		},

		/**
		 * Displays a section with animation
		 *
		 * @param	{element} 	section 	The section to show
		 * @returns {void}
		 */
		_expandSection: function (section) {
			var next = section.find('> ul');

			section
				.addClass('cApiPermissions_open')
				.removeClass('cApiPermissions_closed');

			ips.utils.anim.go( 'fadeInDown fast', next );

			this._calculatedCheckedEndpoints( section );
		},

		/**
		 * Hides a section
		 *
		 * @param	{element} 	section  	The section to hide
		 * @returns {void}
		 */
		_collapseSection: function (section) {
			section
				.removeClass('cApiPermissions_open')
				.addClass('cApiPermissions_closed');

			this._calculatedCheckedEndpoints( section );
		},
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.autoupgradetimer.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.autoupgradetimer.js - Countdown timer for CIC autoupgrade failures necessitating a full applylatestfiles call, or an upgrade call from Cloud2.
 *
 * Author: Brandon Farber
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.system.autoupgradetimer', {

		initialize: function () {
			var self = this;

			// Set a timer that counts down every second
			var secondsRemaining = 60 * 5;
			
			if ( ips.getSetting('cloud2') ) {
				// If we're on Cloud2, set it to 15 seconds initially instead
				secondsRemaining = 15;
			}
			
			var interval = setInterval( function() {
				// Take a second off
				secondsRemaining--;

				// If we are at 0, we're done
				if( secondsRemaining < 1 )
				{
					clearInterval( interval );
					self.scope.find('[data-role="counter-wrapper"]').hide();
					self.scope.find('[data-role="continue-button"]').show();
				}
				// Otherwise update the displayed countdown
				else
				{
					var minutes = Math.floor( secondsRemaining / 60 );
					var seconds = secondsRemaining % 60;

					self.scope.find('[data-role="counter"]').html( minutes + ':' + ( ( seconds >= 10 ) ? seconds : '0' + seconds ) );
				}
			}, 1000 );
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.codeHook.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.codeHook.js - Handles editing code hooks
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.system.codeHook', {
		
		codeMirror: null,
		
		initialize: function () {
			this.on( 'click', '[data-codeToInject]', this._itemClick );
		},
		
		/**
		 * Event handler for clicking on an item
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		_itemClick: function (e) {
			var codeMirror = $( this.scope ).find('textarea').data('CodeMirrorInstance');
						
			var regex = new RegExp( $.parseJSON( $(e.currentTarget).attr('data-signature') ) );
			
			var found = false;
			var lastLine = codeMirror.doc.lineCount() - 1;
			codeMirror.doc.eachLine(function(line){
				if ( line.text.match( regex ) ) {
					found = true;
					codeMirror.setSelection( { line: codeMirror.doc.getLineNumber( line ), ch: 0 }, { line: codeMirror.doc.getLineNumber( line ), ch: line.text.length } );
					codeMirror.scrollIntoView( { line: codeMirror.doc.getLineNumber( line ), ch: 0 } );
				}
				if ( line.text.match( /^\s*}\s*$/ ) ) {
					lastLine = codeMirror.doc.getLineNumber( line );
				}
			});
			
			if ( !found ) {
				codeMirror.doc.replaceRange( $.parseJSON( $(e.currentTarget).attr('data-codeToInject') ), { line: lastLine - 1, chr: 0 }, { line: lastLine - 1, chr: 0 } );
				codeMirror.scrollIntoView( { line: codeMirror.doc.lineCount(), ch: 0 } );
			}
		}
	});
}(jQuery, _));
</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.langString.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.langString.js - Faciliates editing language strings in the ACP translator
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.system.langString', {

		initialize: function () {
			this.on( 'click', '[data-action="saveWords"]', this.saveWords );
			this.on( 'click', '[data-action="revertWords"]', this.revertWords );
			this.setup();
		},
		
		/**
		 * Setup method
		 * Replaces the scope element with a textbox containing the scope's HTML
		 *
		 * @returns {void}
		 */
		setup: function () {
			var self = this;
			var textArea = $('<textarea />');

			textArea
				.attr( 'data-url', this.scope.attr('href') )
				.val( this.scope.html() )
				.change( function (e) {
					self._change(e);
				});
			
			//this.scope.replaceWith( textArea );
		},
		
		/**
		 * Event handler for content changing in the textbox
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		_change: function (e) {
			var elem = $( e.target );

			elem.addClass( 'ipsField_loading' );

			var url = elem.attr('data-url') + '&form_submitted=1&csrfKey=' + ips.getSetting('csrfKey') + 
						'&lang_word_custom=' + encodeURIComponent( elem.val() );

			// Send the translated string, and show flash message on success
			// On failure we'll reload the page
			ips.getAjax()( url )
				.done( function() {
					elem.removeClass('ipsField_loading');
					ips.ui.flashMsg.show( ips.getString('saved') );
				})
				.fail( function () {
					window.location = url;
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.login.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.login.js - ACP login screen controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.system.login', {

		initialize: function () {
			this.on( 'tabChanged', this.tabChanged );
			this.on( 'click', '[data-action=&quot;upgradeWarningContinue&quot;]', this.upgradeWarningContinue );
			this.setup();
		},

		/**
		 * Setup method
		 * Focuses the first text field on the first visible tab automatically
		 *
		 * @returns {void}
		 */
		setup: function () {
			// find the active tab, if any, then the first text field to focus it
			this.scope
				.find(&quot;#elTabContent .ipsTabs_panel:visible&quot;)
					.first()
					.find('input[type=&quot;text&quot;]')
						.first()
						.focus();
		},

		/**
		 * Event handler for the active tab being changed
		 * Saves the new active tab in a cookie for the next time the login screen is loaded
		 * so that we can show the user their correct login method automatically
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data	Event data object
		 * @returns {void}
		 */
		tabChanged: function (e, data) {
			// Store the tab they've clicked on in the local DB so that we can
			// show it by default next time they log in
			ips.utils.cookie.set( 'acpLoginMethod', data.tabID, 1 );
		},
		
		/**
		 * Event handler for when the upgrade warning is skipped
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data	Event data object
		 * @returns {void}
		 */
		upgradeWarningContinue: function (e, data) {
			e.preventDefault();
			$(this.scope).find('[data-role=&quot;upgradeWarning&quot;]').hide();
			$(this.scope).find('[data-role=&quot;loginForms&quot;]').removeClass('ipsHide').show();
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.menuManager.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.menuManager.js - Menu manager JS
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.system.menuManager', {

		_dropdownManager: null,
		_menuManager: null,
		_editForm: null,
		_previewWindow: null,
		_ajaxObj: null,
		_currentDropdown: 0,
		_previewOpen: false,
		_baseUrl: null,

		initialize: function () {
			this.on( 'click', "[data-action='editDropdown']", this.editDropdown );
			this.on( 'click', "[data-role='menuItem']", this.editItem );
			this.on( 'click', "[data-action='removeItem']", this.removeItem );
			this.on( 'click', "[data-action='newDropdown']", this.newDropdown );
			this.on( 'click', "[data-action='newItem']", this.newItem );
			this.on( 'click', "[data-action='navBack']", this.navBack );
			this.on( 'submit', "[data-role='editForm'] form", this.saveEditForm );
			this.on( 'click', "[data-action='previewToggle']", this.togglePreview );
			this.on( document, 'menuToggle.acpNav', this.menuToggled );
			this.on( document, 'click', '[data-action="publishMenu"]:not( .ipsButton_disabled )', this.publishMenu );
			this.on( document, 'click', '[data-action="restoreMenu"]', this.restoreMenu );
			this.on( window, 'beforeunload', this.windowUnload );
			this.setup();
		},

		/**
		 * Setup method
		 * Sets up the sortable
		 *
		 * @returns {void}
		 */
		setup: function () {
			var self = this;

			this._dropdownManager = this.scope.find('[data-manager="dropdown"]');
			this._menuManager = this.scope.find('[data-manager="main"]');
			this._editForm = this.scope.find("[data-role='editForm']");
			this._previewWindow = this.scope.find('[data-role="preview"]');
			this._baseUrl = this.scope.attr('data-baseUrl');

			// Set up nested sortable for the root items
			var sortOptions = {
				forcePlaceholderSize: true,
				handle: 'div',
				helper: 'clone',
				maxLevels: 2,
				items: '[data-role="menuNode"]',
				isTree: true,
				errorClass: 'cMenuManager_emptyError',
				placeholder: 'cMenuManager_emptyHover',
				start: _.bind( this._startDragging, this ),
				toleranceElement: '> div',
				tabSize: 30,
				update: _.bind( this._update, this )
			};
			if ( this.scope.attr('data-supportsChildren') ) {
				this.scope.find('.cMenuManager_root > ol').nestedSortable( sortOptions );
			} else {
				this.scope.find('.cMenuManager_root > ol').sortable( sortOptions );
			}

			// Append the update logic to the dropdown children
			this._applyDropDownSort();

			// Position the live preview
			this._positionPreviewWindow();
		},

		_applyDropDownSort: function() {
			var self = this;

			// Set up sortables for the dropdown menus
			this._dropdownManager.find('[data-dropdownID] > ol').sortable({
				items: '> li',
				update: function (event, ui) {
					self._changeOccurred();

					var parentID = ui.item.closest('[data-menuid]').attr('data-menuid');
					var items = $( this ).sortable( 'toArray' );
					var result = [];

					for( var i = 0; i < items.length; i++ ){
						result.push( "menu_order[" + items[i].replace('menu_', '') + "]=" + parentID );
					}

					ips.getAjax()( self._baseUrl + '&do=reorder&reorderDropdown=1&' + result.join('&') )
						.done(function(){
							self._updatePreview();
						});
				}
			});
		},

		_update: function () {
			var self = this;
			this._changeOccurred();
			
			if ( this.scope.attr('data-supportsChildren') ) {
				var serialized = this.scope.find('.cMenuManager_root > ol').nestedSortable( 'serialize', { key: 'menu_order' } );
			} else {
				var serialized = this.scope.find('.cMenuManager_root > ol').sortable( 'serialize', { key: 'menu_order[]' } );
			}

			ips.getAjax()( this._baseUrl + '&do=reorder&' + serialized )
				.done(function(){
					self._updatePreview();
				});
		},

		/**
		 * Event handler called when user leaves the page
		 * If there's unsaved changes, we'll let the user know
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		windowUnload: function (e) {
			if( this._hasChanges ){
				return ips.getString('menuPublishUnsaved');
			}
		},

		/**
		 * Warns the user before proceeding with restoring the menu
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		restoreMenu: function (e) {
			e.preventDefault();

			var url = $( e.currentTarget ).attr('href');

			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'warn',
				message: ips.getString('menuRestoreConfirm'),
				subText: ips.getString('menuRestoreConfirmSubtext'),
				callbacks: {
					ok: function () {
						window.location = url;
					}
				}
			});
		},

		/**
		 * Publishes the menu via ajax
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		publishMenu: function (e) {
			e.preventDefault();

			var self = this;
			var button = $( e.currentTarget );
			var url = button.attr('href');

			// Change the text
			button
				.attr('data-currentTitle', button.find('span').text() )
				.addClass('ipsButton_disabled')
				.find('span')
					.text( ips.getString('publishing') );

			// Fire request
			ips.getAjax()( url, {
				bypassRedirect: true
			} )
				.done( function (response) {
					ips.ui.flashMsg.show( ips.getString("publishedMenu") );
					self._hasChanges = false;

					button
						.find('span')
							.text( button.attr('data-currentTitle') );
				})
				.fail( function () {
					window.location = url;
				});
		},

		/**
		 * Toggles the preview panel
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		togglePreview: function (e) {
			e.preventDefault();

			var toggle = $( e.currentTarget );

			if( this._previewOpen ){
				this._previewOpen = false;
				this._previewWindow.stop().animate({
					height: '48px'
				});
			} else {
				this._previewOpen = true;
				this._previewWindow.stop().animate({
					height: '350px'
				});
			}

			toggle.find('[data-role="closePreview"]').toggleClass( 'ipsHide', !this._previewOpen );
			toggle.find('[data-role="openPreview"]').toggleClass( 'ipsHide', this._previewOpen );
		},

		/**
		 * Handles adding a new item to a dropdown menu
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		newDropdown: function (e) {
			e.preventDefault();

			// Add temporary item for now
			var self = this;
			var newItem = ips.templates.render( 'menuManager.temporaryDropdown', {
				selected: true
			});
			var menuID = this._currentDropdown;
			var menu = this._dropdownManager.find( '[data-menuID="' + menuID + '"]' );
			var url = $( e.currentTarget ).attr('href');

			this._unselectAllItems();

			var doNew = function () {
				menu.append( newItem );

				self._checkForEmptyDropdown( menu );

				// Load the edit form
				self._loadEditForm( url, {
					parent: menuID
				});
			};

			this._checkTempBeforeCallback( doNew );
		},

		/**
		 * Handles adding a new item to the main menu
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		newItem: function (e) {
			e.preventDefault();

			// Add temporary item for now
			var self = this;
			var newItem = ips.templates.render( 'menuManager.temporaryMenuItem', {
				selected: true
			});
			var url = $( e.currentTarget ).attr('href');

			this._unselectAllItems();

			var doNew = function () {
				self.scope.find('.cMenuManager_root > ol').prepend( newItem );

				// Load the edit form
				self._loadEditForm( url, {
					parent: 0
				});
			};

			this._checkTempBeforeCallback( doNew );
		},

		/**
		 * Event handler for saving an add/edit form
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		saveEditForm: function (e) {
			e.preventDefault();

			var self = this;
			var form = $( e.currentTarget );
			var url = form.attr('action');
			var id = form.attr('data-itemID');
			var isNew = form.find('input[type="hidden"][name="newItem"]').val();

			ips.getAjax()( url, {
				type: 'post',
				data: form.serialize()
			})
				.done( function (response) {
					
					// If the form has just been returned back, that indicates an error
					if ( typeof response == 'string' ) {
						self._editForm.html( response );
						$( document ).trigger( 'contentChange', [ self._editForm ] );
						return;
					}
					
					// Update the menu item with new HTML
					if( isNew ){
						self.scope.find('[data-itemID="temp"]')
							.closest('[data-role="menuNode"]')
								.attr('id', 'menu_' + response.id )
							.end()
							.replaceWith( response.menu_item );
					} else {
						self.scope.find('[data-itemID="' + id + '"]').replaceWith( response.menu_item );
					}

					// Do we have a dropdown menu to replace?
					if( response.dropdown_menu ){
						var dropdownContent = $('<div>' + response.dropdown_menu + '</div>');

						dropdownContent.find('[data-dropdownid]').each( function () {
							var id = $( this ).attr('data-dropdownid');

							if( self._dropdownManager.find('[data-dropdownid="' + id + '"]').length ){
								self._dropdownManager.find('[data-dropdownid="' + id + '"]').html( $( this ).html() );
							} else {
								var newDropdown = $('<div/>').attr('data-dropdownid', id ).html( $( this ).html() );
								self._dropdownManager.append( newDropdown );
							}
						});
					}

					// Empty the edit form
					self._editForm
						.removeClass('cMenuManager_formActive')
						.find('> div').fadeOut( 'fast', function () {
							$( this ).remove();
						});

					// Animate item
					ips.utils.anim.go( 'pulseOnce', self.scope.find('[data-itemID="' + id + '"]') );

					if( isNew ){
						self.scope.find('.cMenuManager_root > ol').sortable('refreshPositions');

						self._applyDropDownSort();

						self._update();
					}
					
					self._changeOccurred();
					self._updatePreview();
				})
				.fail( function (err) {

				});
		},

		/**
		 * Event handler for removing an item
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		removeItem: function (e) {
			e.preventDefault();
			
			var self = this;
			var removeIcon = $( e.currentTarget );
			var item = removeIcon.closest('[data-role="menuItem"]');
			var li = item.closest('li');
			var menu = item.closest('ol');
			var url = removeIcon.attr('href');

			if( item.attr('data-itemID') == 'temp' ){
				// Just remove and reset the form
				ips.utils.anim.go( 'fadeOutDown', li )
					.done( function () {
						li.remove();
						self._checkForEmptyDropdown( menu );
					});

				this._unselectAllItems();
			}

			var removeItem = function () {
				// Remove item first
				self._changeOccurred();
				ips.utils.anim.go( 'fadeOutDown', li );
				ips.getAjax()( self._baseUrl + '&do=remove&wasConfirmed=1&id=' + item.attr('data-itemID') );
			};

			// Check whether this item has any children
			if( item.find('+ ol > li').length ){
				ips.ui.alert.show( {
					type: 'confirm',
					icon: 'warn',
					message: ips.getString('menuItemHasChildren'),
					callbacks: {
						ok: removeItem
					}
				});
			} else if( item.find('[data-action="editDropdown"]').length ) {
				ips.ui.alert.show( {
					type: 'confirm',
					icon: 'warn',
					message: ips.getString('menuItemHasDropdown'),
					callbacks: {
						ok: removeItem
					}
				});
			} else {
				removeItem();
			}
		},

		/**
		 * Event handler for clicking on an item to edit it
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		editItem: function (e) {
			// Find edit URL
			var self = this;
			var clickFocus = $( e.target );
			var item = $( e.currentTarget );
			var editURL = item.find('[data-action="editItem"]').attr('href');

			var doEdit = function () {
				// Ignore it if we're inside another link
				if( clickFocus.closest('a').length ){
					return;
				}

				// Highlight this item; remove all other highlights
				self._menuManager.find('.cMenuManager_active').removeClass('cMenuManager_active');
				self._dropdownManager.find('.cMenuManager_active').removeClass('cMenuManager_active');
				self._editForm.addClass('cMenuManager_formActive');
				item.addClass('cMenuManager_active');

				// Load the edit form
				self._loadEditForm( editURL, {} );
			};

			this._checkTempBeforeCallback( doEdit );			
		},

		/**
		 * Event handler for clicking on the 'edit dropdown' icon
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		editDropdown: function (e) {
			e.preventDefault();
			var self = this;
			var icon = $( e.currentTarget );
			var dropdownID = icon.closest('[data-itemID]').attr('data-itemID');
			var menuWrapper = this._dropdownManager.find('[data-dropdownID="' + dropdownID + '"]');

			// Main edit functionality
			var doEdit = function () {
				// If we're currently viewing the roots, we'll slide it across
				if( !self._currentDropdown ){
					// Get column ready for animation
					self.scope.find('.cMenuManager_column').addClass('cMenuManager_readyToSlide');

					// Hide all menu wrappers
					self._dropdownManager.find('[data-dropdownID]').hide();

					// Show this menu
					menuWrapper.show();
					menuWrapper.find('[data-menuid="' + dropdownID + '"]').show();

					// Slide columns
					var animations = {};
					if ( $('html').attr('dir') === 'rtl' ) {
						animations.right = "-50%";
					} else {
						animations.left = "-50%";
					}
					self.scope.find('[data-manager="dropdown"], [data-manager="main"]').show().animate(animations, function () {
						self.scope.find('.cMenuManager_column').addClass('cMenuManager_readyToSlide');
					});

					self._currentDropdown = 0;
				} else {

					// If we're already viewing a menu, we'll fade out and in
					var currentMenuWrapper = self._dropdownManager.find('[data-dropdownID="' + self._currentDropdown + '"]');

					currentMenuWrapper.find(' > ol').fadeOut( 'fast', function () {
						currentMenuWrapper.hide();
						menuWrapper
							.find('> ol')
								.hide()
							.end()
							.show()
							.find('> ol')
								.fadeIn();
					});
				}

				// Empty the edit form
				self._editForm
					.removeClass('cMenuManager_formActive')
					.find('> div').fadeOut( 'fast', function () {
						$( this ).remove();
					});

				self._currentDropdown = dropdownID;
				
				// Scroll to the top of the page as we could be two or three pages down so the form is never shown
				$('body').animate({
					scrollTop: String($("#acpContent").offset().top - 40)
				}, 1000);
			};

			this._checkTempBeforeCallback( doEdit );			
		},

		/**
		 * Goes back a level in the dropdown menu editor
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		navBack: function (e) {
			e.preventDefault();

			var self = this;
			var parentID = $( e.currentTarget ).attr('data-parentID');

			if( parentID == 0 ){
				
				var animations = {};
				if ( $('html').attr('dir') === 'rtl' ) {
					animations.right = "0";
				} else {
					animations.left = "0";
				}
				
				// Back to the root
				this.scope.find('.cMenuManager_column').addClass('cMenuManager_readyToSlide');
				this.scope.find('[data-manager="dropdown"], [data-manager="main"]').show().animate(animations, function () {
					self._dropdownManager.find('[data-dropdownID]').hide();
					self.scope.find('.cMenuManager_column').removeClass('cMenuManager_readyToSlide');
				});

				this._currentDropdown = 0;
			} else {
				// Back to a parent dropdown
				var menuWrapper = this._dropdownManager.find('[data-dropdownID="' + parentID + '"]');
				var currentMenuWrapper = this._dropdownManager.find('[data-dropdownID="' + this._currentDropdown + '"]');

				currentMenuWrapper.find(' > ol').fadeOut( 'fast', function () {
					currentMenuWrapper.hide();
					menuWrapper
						.find('> ol')
							.hide()
						.end()
						.show()
						.find('> ol')
							.fadeIn();
				});
			}

			// Empty the edit form
			this._unselectAllItems();
		},

		/**
		 * Event handler that responds to the ACP main menu being toggled
		 * so that we can position the live preview bar
		 *
		 * @returns {void}
		 */
		menuToggled: function () {
			this._positionPreviewWindow();
		},

		/**
		 * Loads a new add/edit form
		 *
		 * @param 	{string} 	url 	URL to the form to load
		 * @param 	{object} 	obj 	Params object to pass to ajax
		 * @returns {void}
		 */
		_loadEditForm: function (url, obj) {
			var self = this;

			if( this._ajaxObj && _.isFunction( this._ajaxObj.abort ) ){
				this._ajaxObj.abort();
			}

			this._editForm
				.addClass('ipsLoading')
				.addClass('cMenuManager_formActive')
				.find('> div')
					.css( { opacity: "0.4" } )
					.after( $('<div/>').addClass('cMenuManager_formLoading ipsLoading') );
			
			this._ajaxObj = ips.getAjax()( url, {
				data: obj
			})
				.done( function (response){
					self._editForm.html( response );
					$( document ).trigger( 'contentChange', [ self._editForm ] );
				})
				.always( function () {
					self._editForm.removeClass('ipsLoading cMenuManager_formLoading');
				});
		},

		/**
		 * Checks whether a dropdown is empty, and removes/adds the empty message
		 *
		 * @param 	{element} 	menu 	Menu element
		 * @returns {void}
		 */
		_checkForEmptyDropdown: function (menu) {
			if( !menu.length ){
				return;
			}

			if( menu.find('[data-itemID]').length || menu.find('.ipsMenu_sep').length ){
				menu.find('.cMenuManager_emptyList').remove();
			} else {
				menu.append( ips.templates.render( 'menuManager.emptyList' ) );
			}
		},

		/**
		 * Checks for unsaved temporary items in the menu manager, and runs a callback after confirming
		 *
		 * @param 	{function} 	callback 	Callback method to run after confirming
		 * @returns {void}
		 */
		_checkTempBeforeCallback: function (callback) {
			var self = this;

			// Are there any temp items to warn about?
			if( this.scope.find('[data-itemID="temp"]').length ){
				ips.ui.alert.show( {
					type: 'confirm',
					icon: 'warn',
					message: ips.getString('menuManagerUnsavedTemp'),
					callbacks: {
						ok: function () {
							// Remove temp items
							self.scope.find('[data-itemID="temp"]').remove();
							callback.apply( self );
						}
					}
				});
			} else {
				callback.apply( self );
			}
		},		

		/**
		 * Clears the add/edit form area
		 *
		 * @returns {void}
		 */
		_unselectAllItems: function () {
			this._editForm.find('> div').fadeOut( 'fast', function () {
				$( this ).remove();
			});

			this._menuManager.find('.cMenuManager_active').removeClass('cMenuManager_active');
			this._dropdownManager.find('.cMenuManager_active').removeClass('cMenuManager_active');
			this._editForm.removeClass('cMenuManager_formActive');
		},

		/**
		 * Called when any data change happens, so that we can illuminate the Publish button
		 * and track state internally
		 *
		 * @returns {void}
		 */
		_changeOccurred: function () {
			this._hasChanges = true;

			// Light up the buttons
			// The buttons are in the header so we need to work out of scope here
			$('[data-action="publishMenu"]').removeClass('ipsButton_disabled');
		},

		/**
		 * Updates the preview panel
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_updatePreview: function (e) {
			this.scope.find("[data-role='previewBody'] iframe").get(0).contentWindow.location.reload( true );
		},

		_startDragging: function () {
			//
		},

		/**
		 * Positions the preview bar so it accounts for the ACP menu
		 *
		 * @returns {void}
		 */
		_positionPreviewWindow: function () {
			if( $('body').find('#acpAppList').is(':visible') ){
				var width = $('body').find('#acpAppList').width();
				var css = {};
				if ( $('html').attr('dir') === 'rtl' ) {
					css.right = width + 'px';
				} else {
					css.left = width + 'px';
				}
				this._previewWindow.css(css);
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.settings.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.settings.js - ACP login screen controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.system.settings', {

		initialize: function () {
			this.on( 'change', 'input[name=datastore_method]', this.datastoreChanged );
			this.on( 'change', 'input[name=cache_method]', this.cacheChanged );
			this.setup();
		},

		/**
		 * Setup method
		 * Focuses the first text field on the first visible tab automatically
		 *
		 * @returns {void}
		 */
		setup: function () {
			if ( $('input[name=datastore_method]:checked').val() == 'Redis' ) {
				$(&quot;input[name=cache_method][value=Redis]&quot;).prop(&quot;checked&quot;, true);
				$('#form_cache_method').slideUp();
				$('li[id^=redis]').slideDown();
			}
		},
		
		/**
		 * Event handler for when the the cache store radio is checked
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data	Event data object
		 * @returns {void}
		 */
		cacheChanged: function (e, data) {
			var value = $(e.currentTarget ).val();
			
			if ( value == 'Redis' ) {
				$(&quot;input[name=datastore_method][value=Redis]&quot;).prop(&quot;checked&quot;, true);
				$('#form_cache_method,#id_datastore_filesystem_path').slideUp();
				$('li[id^=redis]').slideDown();
			} else {
				$('#form_cache_method').slideDown();
				$('li[id^=redis]').slideUp();
				
				if ( $('input[name=datastore_method]:checked').val() == &quot;Redis&quot; ) {
					$(&quot;input[name=datastore_method][value=FileSystem]&quot;).prop(&quot;checked&quot;, true);
				}
			}
		},
		
		/**
		 * Event handler for when the the datastore radio is checked
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data	Event data object
		 * @returns {void}
		 */
		datastoreChanged: function (e, data) {
			var value = $(e.currentTarget ).val();
			
			if ( value == 'Redis' ) {
				$(&quot;input[name=cache_method][value=Redis]&quot;).prop(&quot;checked&quot;, true);
				$('#form_cache_method').slideUp();
				$('li[id^=redis]').slideDown();
			} else {
				$('#form_cache_method').slideDown();
				$('li[id^=redis]').slideUp();
				
				if ( $('input[name=cache_method]:checked').val() == &quot;Redis&quot; ) {
					$(&quot;input[name=cache_method][value=None]&quot;).prop(&quot;checked&quot;, true);
				}
			}
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.themeHook.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.codeHook.js - Handles editing theme hooks
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.system.themeHook', {
				
		initialize: function () {	
			var scope = this.scope;
			scope.find( '[data-action=&quot;showTemplate&quot;]' ).removeClass('ipsHide');
			this.on( 'openDialog', function(e, data) {
				$( '#' + data.dialogID ).on( 'click', 'li[data-selector]', function(e){
					e.stopPropagation();
					ips.ui.dialog.getObj( scope.find( '[data-action=&quot;showTemplate&quot;]' ) ).hide();
					scope.find( 'input[name=&quot;plugin_theme_hook_selector&quot;]' ).val( $(e.currentTarget).attr('data-selector') );
				} )
			});
		},
		
	});
}(jQuery, _));
</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.themeHookEditor.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.codeHook.js - Makes the theme hook editor all fancy like
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.system.themeHookEditor', {
				
		initialize: function () {	
			this.on( 'click', 'a[data-action="templateLink"]', this._itemClick );
		},
		
		/**
		 * Event handler for clicking on an item
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		_itemClick: function (e) {
			e.preventDefault();
			
			var themeHookWindow = $(this.scope).find('[data-role="themeHookWindow"]');
			var sidebar = this.scope.find('[data-role="themeHookSidebar"]');
			var target = $( e.currentTarget );
			var templateName = target.text().trim();

			History.replaceState( {}, document.title, target.attr('href') );
			
			themeHookWindow.children('[data-template],[data-role="themeHookWindowPlaceholder"]').hide();
			themeHookWindow.addClass('ipsLoading');
			sidebar.find('.ipsSideMenu_itemActive').removeClass('ipsSideMenu_itemActive');
						
			if ( themeHookWindow.children( '[data-template="' + templateName + '"]' ).length ) {
				themeHookWindow.children( '[data-template="' + templateName + '"]' ).show();
				themeHookWindow.removeClass('ipsLoading');
			} else {
				ips.getAjax()( target.attr('href') + '&editor=1' )
					.done( function (response) {
						themeHookWindow.append( "<div class='cHookEditor_content' data-template='" + templateName + "'>" + response + '</div>' );
						$( document ).trigger('contentChange', [ themeHookWindow.find('[data-template="' + templateName + '"]') ]);
					})
					.fail( function () {
						window.location = target.attr('href');
					})
					.always( function () {
						themeHookWindow.removeClass('ipsLoading');
					});
			}
			
			target.closest('.ipsSideMenu_item').addClass('ipsSideMenu_itemActive');
		}
		
	});
}(jQuery, _));
]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.themeHookTemplate.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.codeHook.js - Handles editing code hooks
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.system.themeHookTemplate', {
				
		initialize: function () {	
			this.on( 'click', 'li[data-selector]', this._itemClick );
		},
		
		/**
		 * Event handler for clicking on an item
		 *
		 * @param 	{event} 	e 		Event object
		 * @returns {void}
		 */
		_itemClick: function (e) {
			this.trigger( $( e.currentTarget ), 'templateClicked' );
		}
		
	});
}(jQuery, _));
</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.upgradeManualQuery.js" javascript_type="controller" javascript_version="107643" javascript_position="1000500">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.upgradeManualQuery.js - Widget to run a query with a check for timeouts
 *
 * Author: Mark Wade
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.system.upgradeManualQuery', {

		initialize: function () {
			this.scope.find('[data-role=&quot;runManualButton&quot;]').css({'display':'inline'});
			
			this.on( 'click', '[data-action=&quot;runQuery&quot;]', this.runQuery );
		},
		
		runQuery: function (e) {
			this.scope.find('[data-role=&quot;querySuccessButtons&quot;]').hide();
			this.scope.css({'opacity':&quot;0.5&quot;}).addClass('ipsLoading');
			
			ips.getAjax()( this.scope.attr('data-url'), { timeout: 30000 } )
				.done(function(response){
					if ( response.runManualQuery ) {
						this.scope.find('[data-action=&quot;redirectContinue&quot;]').click();
					} else {
						this.runQueryFailed();
					}
				}.bind(this))
				.fail(function(a,b,c){
					this.runQueryFailed();
				}.bind(this));
		},
		
		runQueryFailed: function() {
			this.scope.css({'opacity':&quot;1&quot;}).removeClass('ipsLoading');
			this.scope.find('[data-role=&quot;querySuccessButtons&quot;]').show();
			this.scope.find('[data-role=&quot;runManualButton&quot;]').hide();
			
			ips.ui.alert.show( {
				type: 'alert',
				icon: 'warn',
				message: ips.getString('delta_upgrade_run_manual_query_fail_title'),
				subText: ips.getString('delta_upgrade_run_manual_query_fail_desc')
			});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/system" javascript_name="ips.system.manageFollowed.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.manageFollowed.js - Followed content controll
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.system.manageFollowed', {

		initialize: function () {
			$( document ).on( 'followingItem', _.bind( this.followingItemChange, this ) );
			this.setup();
		},

		setup: function () {
			this._followID = this.scope.attr('data-followID');
		},

		followingItemChange: function (e, data) {
			if( data.feedID != this._followID ){
				return;
			}

			if( !_.isUndefined( data.unfollow ) ){
				this.scope.find('[data-role=&quot;followDate&quot;], [data-role=&quot;followFrequency&quot;]').html('');
				this.scope.find('[data-role=&quot;followAnonymous&quot;]').addClass('ipsHide');
				this.scope.find('[data-role=&quot;followButton&quot;]').addClass('ipsButton_disabled');
				this.scope.addClass('ipsFaded');
				return;
			}

			// Update anonymous badge
			this.scope.find('[data-role=&quot;followAnonymous&quot;]').toggleClass( 'ipsHide', !data.anonymous );

			// Update notification type
			if( data.notificationType ){
				this.scope.find('[data-role=&quot;followFrequency&quot;]').html( ips.templates.render( 'follow.frequency', {
					hasNotifications: ( data.notificationType !== 'none' ),
					text: ips.getString( 'followFrequency_' + data.notificationType )
				} ));	
			}			
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/system" javascript_name="ips.system.metaTagEditor.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.metaTagEditor.js - Live meta tag editor functionality
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.system.metaTagEditor', {

		_changed: false,

		initialize: function () {
			this.on( 'click', '[data-action="addMeta"]', this.addMetaBlock );
			this.on( 'click', '[data-action="deleteMeta"]', this.removeMetaBlock );
			this.on( 'click', '[data-action="deleteDefaultMeta"]', this.removeDefaultMeta );
			this.on( 'click', '[data-action="restoreMeta"]', this.restoreDefaultMeta );
			this.on( 'change', 'input, select', this.changed );
			this.on( 'submit', 'form', this.formSubmit );
			this.on( window, 'beforeunload', this.beforeUnload );

			this.on( 'change', '[data-role="metaTagChooser"]', this.toggleNameField );
			this.setup();
		},

		setup: function () {
			this.scope.css({
				zIndex: "10000"
			});
		},

		/**
		 * Show/hide the meta tag name field as appropriate
		 *
		 * @returns 	{void}
		 */
		toggleNameField: function (e) {
			if( $(e.currentTarget).val() == 'other' )
			{
				$(e.currentTarget).closest('ul').find('[data-role="metaTagName"]').show();
			}
			else
			{
				$(e.currentTarget).closest('ul').find('[data-role="metaTagName"]').hide();
			}
		},

		/**
		 * Restores a previously deleted default meta tag
		 *
		 * @param e
		 */
		restoreDefaultMeta: function(e) {
			var tag = $(e.currentTarget).attr('data-tag');

			// Duplicate the metaTemplate block and append it to the list
			var copy = this.scope.find('[data-role="metaTemplate"]').clone().attr( 'data-role', 'metaTagRow' ).hide();

			if( tag == 'robots' || tag == 'keywords' || tag == 'description' )
			{
				copy.find('select[name="meta_tag_name[]"]').val( tag );
			}
			else
			{
				copy.find('select[name="meta_tag_name[]"]').val( 'other' );
				copy.find('[name="meta_tag_name_other[]"]').val( tag ).parent().removeClass('ipsHide');
			}

			if( this.scope.find('input[name="defaultMetaTag[' + tag + ']"]') )
			{
				copy.find('[name="meta_tag_content[]"]').val( this.scope.find('input[name="defaultMetaTag[' + tag + ']"]').val() );
			}

			copy.find('[data-action="deleteMeta"]').attr( 'data-action', 'deleteDefaultMeta' );

			$('#elMetaTagEditor_defaultTags').append( copy );

			ips.utils.anim.go( 'fadeIn', copy );

			$(document).trigger( 'contentChange', [ this.scope ] );
			
			this._doMetaRemoval( e );
		},

		/**
		 * Removes a default meta tag element
		 *
		 * @param e
		 */
		removeDefaultMeta: function(e) {
			if( $( e.currentTarget ).siblings('select').first().val() == 'other' )
			{
				var name	= $( e.currentTarget ).closest('ul').find('input[name="meta_tag_name_other[]"]').val();
			}
			else
			{
				var name	= $( e.currentTarget ).siblings('select').first().val();
			}

			$( e.currentTarget )
				.closest( 'form' )
				.find( 'input' )
				.first()
				.after( "<input type='hidden' name='deleteDefaultMeta[]' value='" + name + "'>" );
			
			this.removeMetaBlock( e, false );

			var string = ips.getString('meta_tag_deleted', {
				tag: name
			});

			var copy = this.scope.find('[data-role="metaDefaultDeletedTemplate"]').clone().attr( 'data-role', 'metaTagRow' ).hide();

			copy.find('[data-role="metaDeleteMessage"]').html( string );
			copy.find('[data-action="restoreMeta"]').attr( 'data-tag', name );

			$('#elMetaTagEditor_defaultTags').find('.ipsAreaBackground').after( copy );

			ips.utils.anim.go( 'fadeIn', copy );

			$(document).trigger( 'contentChange', [ this.scope ] );

			this.changed();

			this._showHideNoTagsMessage();
		},

		/**
		 * Removes a meta tag element
		 *
		 * @param e
		 */
		removeMetaBlock: function( e, restoreDefault ) {
			// We can't use ECMAScript 2015 yet so no default function parameter values
			if( _.isUndefined( restoreDefault ) )
			{
				restoreDefault = true;
			}

			if( $( e.currentTarget ).siblings('select').first().val() == 'other' )
			{
				var tag	= $( e.currentTarget ).closest('ul').find('input[name="meta_tag_name_other[]"]').val();
			}
			else
			{
				var tag	= $( e.currentTarget ).siblings('select').first().val();
			}

			if( this.scope.find('input[name="defaultMetaTag[' + tag + ']"]').length && restoreDefault )
			{
				$(e.currentTarget).attr( 'data-tag', tag );

				this.restoreDefaultMeta(e);
			}
			else
			{
				this._doMetaRemoval( e );
			}
		},

		/**
		 * Actually remove the row
		 *
		 * @param e
		 */
		 _doMetaRemoval: function(e) {
			e.preventDefault();
			var elem = $( e.currentTarget ).closest('[data-role="metaTagRow"]');
			elem.remove();
			ips.utils.anim.go( 'fadeOut', elem );

			this.changed();

			this._showHideNoTagsMessage();
		 },

		/**
		 * Determine if the "no custom meta tags" message should be shown or hidden
		 *
		 * @returns	{void}
		 */
		_showHideNoTagsMessage: function() {
			if( $('#elMetaTagEditor_customTags').find('li[data-role="metaTagRow"]').length )
			{
				$('#elMetaTagEditor_customTags').find('li[data-role="noCustomMetaTagsMessage"]').hide();
			}
			else
			{
				$('#elMetaTagEditor_customTags').find('li[data-role="noCustomMetaTagsMessage"]').show();
			}
		},

		/**
		 * Event handler for submitting the meta tags form
		 *
		 * @returns 	{void}
		 */
		formSubmit: function (e) {
			var form = $( e.currentTarget );
			
			if ( form.attr('data-noAjax') ) {
				return;
			}

			e.preventDefault();

			var self = this;

			form.find('.ipsButton').prop( 'disabled', true ).addClass('ipsButton_disabled');

			// Send ajax request to save
			ips.getAjax()( form.attr('action'), {
				data: form.serialize(),
				type: 'post'
			})
				.done( function () {
					ips.ui.flashMsg.show( ips.getString('metaTagsSaved') );
					form.find('.ipsButton').prop( 'disabled', false ).removeClass('ipsButton_disabled');
					self._changed = false;

					if( form.find('[name="meta_tag_title"]').val() )
					{
						document.title = form.find('[name="meta_tag_title"]').val();
					}
					else
					{
						document.title = self.scope.attr('data-defaultPageTitle');
					}
				})
				.fail( function () {
					form.attr('data-noAjax', 'true');
					form.submit();
				});
		},

		/**
		 * Warns the user if they've got unsaved changes
		 *
		 * @returns 	{string|null}
		 */
		beforeUnload: function () {
			if( this._changed ){
				return ips.getString('metaTagsUnsaved');
			}
		},

		/**
		 * Clones a new set of meta tag elements
		 *
		 * @returns 	{void}
		 */
		addMetaBlock: function (e) {
			e.preventDefault();

			// Duplicate the metaTemplate block and append it to the list
			var copy = this.scope.find('[data-role="metaTemplate"]').clone().attr( 'data-role', 'metaTagRow' ).hide();

			$('#elMetaTagEditor_customTags').append( copy );

			ips.utils.anim.go( 'fadeIn', copy );

			$(document).trigger( 'contentChange', [ copy ] );

			this.changed();

			this._showHideNoTagsMessage();
		},

		/**
		 * Called when an input changes, so we can later warn the use rif they leave the page
		 *
		 * @returns 	{void}
		 */
		changed: function (e) {
			this._changed = true;
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/system" javascript_name="ips.system.notificationSettings.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.notificationSettings.js - Notification settings controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.system.notificationSettings', {

		initialize: function () {
			this.on( 'click', '[data-action="enablePush"]', this.enablePush );
			this.on( document, 'subscribePending.notifications', this.subscribePending );
			this.on( document, 'subscribeSuccess.notifications', this.subscribeSuccess );
			this.on( document, 'subscribeFail.notifications', this.subscribeFail );
			this.on( document, 'permissionDenied.notifications', this.permissionDenied );


			this.on( 'click', '[data-action="showNotificationSettings"]', this.showNotificationSettings );
			this.on( 'click', '[data-action="closeNotificationSettings"]', this.closeNotificationSettings );
			this.on( 'change', '[data-role="notificationSettingsWindow"]', this.saveNotificationSettings );

			this.on( 'change', '#elBrowserNotifications', this.promptMe );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			this._showNotificationOptions();
		},

		/**
		 * Updates the push toggle link with the relevant content depending on notification status
		 *
		 * @returns 	{void}
		 */
		_showNotificationOptions: function () {
			const pushElement = this.scope.find('[data-action="enablePush"]');

			if( ips.utils.notification.supported && ips.utils.serviceWorker.supported ){
				if( Notification.permission === 'granted' ){
					ips.utils.notification.getSubscription()
						.then( subscription => {
							if( !subscription ){
								// No subscription - leave the enable button showing
								return;
							}

							const enableLink = pushElement.contents();
							pushElement.html( ips.templates.render('core.notifications.checking') );

							// We can end up in a situation where the server has removed the device's keys, but the device
							// still retains a subscription. To handle that situation and give the user the opportunity to 
							// resubscription, we'll ping the server with the key and verify it exists. If not, show the
							// enable button again.
							const jsonSubscription = JSON.parse( JSON.stringify(subscription) );
							const key = jsonSubscription.keys.p256dh;
							
							ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=notifications&do=verifySubscription', {
								type: 'post',
								data: {
									key
								}
							} )
								.done( (response, status, jqXHR) => {
									if( jqXHR.status === 200 ){
										pushElement.html( ips.templates.render('core.notifications.success') );
										return;
									}

									// For any other status, leave the enable button showing
									pushElement.html( enableLink );
								})
								.fail( () => {
									pushElement.html( enableLink );
								});
						})
						.catch( err => {
							console.log( err );
							pushElement.html( ips.templates.render('core.notifications.notSupported') );
						});
				} else if( Notification.permission === 'denied' ){
					pushElement.html( ips.templates.render('core.notifications.fail') );
				}
			} else {
				console.log( "Notifications not supported" );
				pushElement.html( ips.templates.render('core.notifications.notSupported') );
			}
			
			pushElement.slideDown();
		},

		/**
		 * Event handler for enabling push notificiations
		 *
		 * @returns 	{void}
		 */
		enablePush: function (e) {
			e.preventDefault();
			ips.utils.notification.requestPermission();
		},

		/**
		 * Pending subscription: Event handler when browser is trying to subscribe to notifications
		 *
		 * @returns 	{void}
		 */
		subscribePending: function (e, data) {
			this.scope.find('[data-action="enablePush"]').html( ips.templates.render('core.notifications.pending') ).show();
		},

		/**
		 * Sucessful subscription: Event handler when browser has subscribed to notifications
		 *
		 * @returns 	{void}
		 */
		subscribeSuccess: function (e, data) {
			this.scope.find('[data-action="enablePush"]').html( ips.templates.render('core.notifications.success') ).show();
		},

		/**
		 * Failed subscription: Event handler when browser has failed to subscribe to notifications
		 *
		 * @returns 	{void}
		 */
		subscribeFail: function (e, data) {
			this.scope.find('[data-action="enablePush"]').html( ips.templates.render('core.notifications.fail') ).show();
		},

		/**
		 * Permission denied: Event handler for when the notification permission changes
		 *
		 * @returns 	{void}
		 */
		permissionDenied: function () {
			this.scope.find('[data-action="enablePush"]').html( ips.templates.render('core.notifications.fail') ).show();
		},

		/**
		 * Event handler for changing the browser notifications checkbox
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		promptMe: function (e) {
			if ( $(e.target).is(':checked') ) {
				ips.utils.cookie.unset( 'noBrowserNotifications' );
				if( !ips.utils.notification.hasPermission() ){
					ips.utils.notification.requestPermission();
				} else {
					ips.ui.flashMsg.show( ips.getString('saved') );
				}
			} else {
				ips.utils.cookie.set( 'noBrowserNotifications', true, true );
				ips.ui.flashMsg.show( ips.getString('saved') );
			}
		},

		/**
		 * Displays an info panel, with the message depending on whether notifications are enabled in the browser
		 *
		 * @returns 	{void}
		 */
		_showNotificationChoice: function () {
			this.scope.find('[data-role="browserNotifyInfo"]').show();
			var type = ips.utils.notification.permissionLevel();
			switch ( type ) {
				case 'denied':
					$('#elBrowserNotifications').prop( 'checked', false ).prop( 'disabled', true );
					this.scope.find('[data-role="browserNotifyDisabled"]').show();
					break;
				case 'granted':
					$('#elBrowserNotifications').prop( 'checked', !ips.utils.cookie.get('noBrowserNotifications') );
					break;
				default:
					break;
			}
		},
		
		/**
		 * Event handler for when a notification type is clicked
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		showNotificationSettings: function (e) {
			e.preventDefault();
			
			var target = $(e.currentTarget);
			var expandedContainer = target.parent().find('[data-role="notificationSettingsWindow"]');
			
			this.scope.find('.cNotificationTypes__row--selected').removeClass('cNotificationTypes__row--selected');
			this.scope.find('[data-action="showNotificationSettings"]').show();
			this.scope.find('[data-role="notificationSettingsWindow"]').hide();
			
			target.parent().addClass('cNotificationTypes__row--selected');
			target.find('.cNotificationSettings_expand').addClass('ipsLoading ipsLoading_tiny').find('i').addClass('ipsHide');
						
			ips.getAjax()( target.attr('href') ).done(function(response){
				expandedContainer.html(response).show();
				target.hide();
				target.find('.cNotificationSettings_expand').removeClass('ipsLoading').find('i').removeClass('ipsHide');
			}).fail(function(){
				window.location = target.attr('href');
			})
		},
		
		/**
		 * Event handler for when a notification type is clicked
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		closeNotificationSettings: function (e) {
			e.preventDefault();
			
			this.scope.find('.cNotificationTypes__row--selected').removeClass('cNotificationTypes__row--selected');
			this.scope.find('[data-action="showNotificationSettings"]').show();
			this.scope.find('[data-role="notificationSettingsWindow"]').hide();
		},
		
		/**
		 * Event handler for when a notification form is saved
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		saveNotificationSettings: function (e) {
			e.preventDefault();
			
			var target = $(e.target);
			var form = target.closest('form');
			var container = form.closest('[data-role="notificationSettingsWindow"]');
			var containerParent = container.closest('.cNotificationTypes__row');
			var closeIcon = container.find('[data-action="closeNotificationSettings"]');
			
			closeIcon.addClass('ipsLoading ipsLoading_tiny').text('');
						
			ips.getAjax()( form.attr('action'), {
				data: form.serialize(),
				type: 'post'
			} ).done(function(response){
				closeIcon.removeClass('ipsLoading').html('&times;');
				containerParent.find('[data-action="showNotificationSettings"]').html(response);
				ips.ui.flashMsg.show( ips.getString('saved') );
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/system" javascript_name="ips.system.referrals.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.referrals.js - Referrals section
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.front.system.referrals', {

		initialize: function () {
			
			// There can be a delay while the library loads for the first time
			$('.cReferrer_copy').each( function()
			{
				$(this).hide();
			} );
					
			ips.loader.get( ['core/interface/clipboard/clipboard.min.js'] ).then( function()
	        {
		        if ( ClipboardJS.isSupported() ) {
			        $('.cReferrer_copy').each( function()
					{
						$(this).show();
					} );
			
					var clipboard = new ClipboardJS('.cReferrer_copy');
					
					clipboard.on('success', function(e) {
					    ips.ui.flashMsg.show( ips.getString('copied') );
					    e.clearSelection();
					});
				} else {
					$('.cReferrals_directLink_input').removeClass('ipsHide');
					$('.cReferrals_directLink_link').addClass('ipsHide');
				}
			} );
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/system" javascript_name="ips.system.register.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.system.register.js - Registration controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.system.register', {

		usernameField: null,
		timers: { 'username': null, 'email': null },
		ajax: ips.getAjax(),
		popup: null,
		passwordBlurred: true,
		dirty: false,
		initialize: function () {
			this.on( 'keyup', '#elInput_username', this.changeUsername );
			this.on( 'blur', '#elInput_username', this.changeUsername );
			this.on( 'keyup', '#elInput_password_confirm', this.confirmPassword );
			this.on( 'blur', '#elInput_password_confirm', this.confirmPassword );
			this.on( 'click', 'a[data-ipsPbrCancel]', this.cancelPbr );
			this.setup();
		},

		/**
		 * Setup method
		 * Loads the template file for registration, and adds an empty element after the username field
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		setup: function () {
			this.usernameField = this.scope.find('#elInput_username');
			this.passwordField = this.scope.find('#elInput_password');
			this.confirmPasswordField = this.scope.find('#elInput_password_confirm');

			// Build extra element after username
			this.usernameField.after( $('<span/>').attr( 'data-role', 'validationCheck' ) );
			this.confirmPasswordField.after( $('<span/>').attr( 'data-role', 'validationCheck' ) );

			this.convertExistingErrors();
		},

		/**
		 * Looks for any validation errors present when the page was loaded (i.e. errors added by the backend)
		 * and converts them into the dynamic errors we use here.
		 *
		 * @returns 	{void}
		 */
		convertExistingErrors: function () {
			var fields = this.scope.find('#elInput_username, #elInput_password, #elInput_password_confirm');
			var self = this;

			fields.each( function () {
				var elem = $(this);
				var wrapper = elem.closest('.ipsFieldRow');

				// Bail if no errors found
				if( !wrapper.hasClass('ipsFieldRow_error') ){
					return;
				}

				var message = wrapper.find('.ipsType_warning').html();
				self._clearResult( elem );

				wrapper.find('[data-role="validationCheck"]').show().html( ips.templates.render( 'core.forms.validateFailText', { message: message } ) );
				elem.removeClass('ipsField_success').addClass('ipsField_error');
			});
		},

		/**
		 * Cancel the post before register submission
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		cancelPbr: function (e) {
			var url = $(e.target).closest('[data-ipsPbrCancel]').attr('href');
			
			e.preventDefault();
			e.stopPropagation();
			
			/* Show confirmation Prompt */
			ips.ui.alert.show({
				type: 'confirm',
				message: ips.getString('pbr_confirm_title'),
				subText: ips.getString('pbr_confirm_text'),
				icon: 'warn',
				buttons: {
					ok: ips.getString('pbr_confirm_ok'),
					cancel: ips.getString('pbr_confirm_cancel')
				},
				callbacks: {
					ok: function(){
						window.location = url;
					},
					cancel: function(){
						return false;
					}
				}
			});
		},
		
		/**
		 * Event handler for a change on the username field
		 * Waits 700ms, then calls this._doCheck
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		changeUsername: function (e) {
			if( this.timers['username'] ){
				clearTimeout( this.timers['username'] );
			}

			if( this.usernameField.val().length > 4 || e.type != "keyup" ){
				this.timers['username'] = setTimeout( _.bind( this._doCheck, this, this.usernameField ), 700 );
			} else {
				this._clearResult( this.usernameField );
			}
		},

		/**
		 * Event handler for a change on the password field
		 * Waits 200ms, then calls this._doPasswordCheck
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		changePassword: function (e) {
			if( this.timers['password'] ){
				clearTimeout( this.timers['password'] );
			}

			if( this.passwordField.val().length > 2 || e.type != "keyup" ){
				this.timers['password'] = setTimeout( _.bind( this._doPasswordCheck, this, this.passwordField ), 200 );
			} else {
				this._clearResult( this.passwordField );
			}

			this.confirmPassword();
		},

		/**
		 * Event handler for a change on the confirm password field
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		confirmPassword: function (e) {
			var resultElem = this.confirmPasswordField.next('[data-role="validationCheck"]');

			if( this.passwordField.val() && this.passwordField.val() === this.confirmPasswordField.val() ){
				resultElem.hide().html('');
				this.confirmPasswordField.removeClass('ipsField_error').addClass('ipsField_success');
			} else {
				this._clearResult( this.confirmPasswordField );
			}
		},

		/**
		 * Clears a previous validation result
		 *
		 * @returns 	{void}
		 */
		_clearResult: function (field) {
			field
				.removeClass('ipsField_error')
				.removeClass('ipsField_success')
				.next('[data-role="validationCheck"]')
					.html('');

			field
				.closest('.ipsFieldRow')
					.removeClass('ipsFieldRow_error')
					.find('.ipsType_warning, .ipsFieldRow_content br:last')
						.remove();
		},

		/**
		 * Fires an ajax request to check whether the username is already in use
		 * Updates the result element depending on the result
		 *
		 * @returns 	{void}
		 */
		_doCheck: function ( field ) {
			var value = field.val();
			var resultElem = field.next('[data-role="validationCheck"]');
			var self = this;

			if( this.ajax && this.ajax.abort ){
				this.ajax.abort();
			}

			// Set loading
			field.addClass('ipsField_loading');

			// Do ajax
			this.ajax( ips.getSetting('baseURL') + '?app=core&module=system&controller=ajax&do=usernameExists', {
				dataType: 'json',
				data: {
					input: encodeURIComponent( value )
				}
			})
				.done( function (response) {
					if( response.result == 'ok' ){
						resultElem.hide().html('');
						field.removeClass('ipsField_error').addClass('ipsField_success');
					} else {
						resultElem.show().html( ips.templates.render( 'core.forms.validateFailText', { message: response.message } ) );
						field.removeClass('ipsField_success').addClass('ipsField_error');
					}
				})
				.fail( function () {} )
				.always( function () {
					field.removeClass('ipsField_loading');
				});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.addForm.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.templates.addForm.js - Controller for the 'add' form in the template editor
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.templates.addForm', {

		initialize: function () {
			this.on( 'submit', 'form.ipsForm', this.submitForm );
			this.on( document, 'fileListRefreshed.templates', this.closeDialog );
		},

		submitForm: function (e) {
			e.preventDefault();

			var self = this;

			if( !$( e.currentTarget ).attr('data-bypassValidation') ){
				// The form hasn't been validated by the genericDialog controller yet, so bail for now
				return;
			}

			// Gather form values and send them
			ips.getAjax()( $( e.currentTarget ).attr('action'), {
				dataType: 'json',
				data: $( e.currentTarget ).serialize(),
				type: 'post'
			})
				.done( function (response) {

					self.trigger( 'addedFile.templates', {
						type: self.scope.attr('data-type'),
						fileID: response.id,
						name: response.name
					});

				});
		},

		closeDialog: function (e, data) {
			this.trigger('closeDialog');
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.conflict.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.templates.conflict.js - Templates: Parent controller for the template conflict manager
 *
 * Author: Matt Mecham
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.templates.conflict', {

		initialize: function () {			
			this.on( 'click', '.ipsButton', this.makeSelection );
			this.setup();
		},

		setup: function () {
			$('span[data-conflict-name] input[type=radio]').hide();
			
			if ( $('[data-role=&quot;editor&quot;]').length ) {
				_.each( $('[data-role=&quot;editor&quot;]'), function( obj ) {
					var id = $(obj).attr('id');
					var type = $(obj).attr('data-type');
					var editor = CodeMirror.fromTextArea( document.getElementById(id), { 
						mode: (type == 'html' ? 'htmlmixed' : 'css'),
						lineNumbers: true
					} );
					editor.setSize( null, '600px' );
				} );
			}
		},

		/**
		 * &quot;Use this version&quot; button is clicked
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		makeSelection: function (e) {
			var span  = $( e.target ).closest('span');
			var radio = $( span ).find('input[type=radio]');
			var name  = $( span ).attr('data-conflict-name');
			var id    = $( radio ).attr('name').replace( /conflict_/, '' );
			
			/* Button disabled */
			if ( $(span).hasClass('ipsButton_disabled') )
			{
				return false;
			}
			/* Undo selection */
			else if ( $(span).hasClass('ipsButton_alternate') )
			{
				$(radio).removeAttr('checked');
				
				$(span).removeClass('ipsButton_alternate')
					   .addClass('ipsButton_primary')
					   .find('strong').html( ips.getString('sc_use_this_version') );
					   
				$('input[type=radio][name=conflict_' + id + ']').closest('span.ipsButton[data-conflict-name=' + ( name == 'new' ? 'old' : 'new' ) +']').removeClass('ipsButton_disabled');
				
				$('th span[data-conflict-id=' + id + '][data-conflict-name=' + name + ']').removeClass('ipsPos_left ipsBadge ipsBadge_positive');

				ips.utils.anim.go( 'blindDown', this.scope.find('div[data-conflict-id=' + id + ']') );
			}
			/* Make selection */
			else
			{
				$(radio).attr('checked', 'checked');
				$(span).removeClass('ipsButton_primary')
					   .addClass('ipsButton_alternate')
					   .find('strong').html( ips.getString('sc_remove_selection') );
						   
				$('input[type=radio][name=conflict_' + id + ']').closest('span.ipsButton[data-conflict-name=' + ( name == 'new' ? 'old' : 'new' ) +']').addClass('ipsButton_disabled');
				
				$('th span[data-conflict-id=' + id + '][data-conflict-name=' + name + ']').addClass('ipsPos_left ipsBadge ipsBadge_positive');
				
				this.scope.find('div[data-conflict-id=' + id + ']').slideUp();
			}
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.fileEditor.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.templates.fileEditor.js - Templates: controller for the tabbed file editor
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.templates.fileEditor', {

		_tabBar: null,
		_tabContent: null,
		_fileStore: {},
		_ajaxURL: '',
		_cmInstances: {},
		_currentHeight: 0,
		_currentDiff: null,
		_editorPreferences: {
			wrap: true,
			lines: false
		},

		initialize: function () {
			// Events from elsewhere
			this.on( document, 'openFile.templates', this.openFile );
			this.on( document, 'variablesUpdated.templates', this.updateVariables );

			// Events from within
			this.on( 'tabChanged', this.changedTab );
			this.on( 'click', '[data-action="closeTab"]', this.closeTab );
			this.on( 'click', '[data-action="save"]', this.save );
			this.on( 'click', '[data-action="revert"]', this.revert );
			this.on( 'savedFile.templates', this.updateFile );
			this.on( 'revertedFile.templates', this.updateFile );
			this.on( 'openDialog', this.openedDialog );
			this.on( 'menuItemSelected', this.menuSelected );

			var debounce = _.debounce( _.bind( this._recalculatePanelWrapper, this ), 100 );
			this.on( window, 'resize', debounce );


			// Other setup
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			var self = this;

			// Set element references
			this._tabBar = this.scope.find('#elTemplateEditor_tabbar');
			this._tabContent = this.scope.find('#elTemplateEditor_panels');

			// Get the current height of the tab bar
			this._currentHeight = this._tabBar.outerHeight();

			// Set URLs
			this._ajaxURL = this.scope.closest('[data-ajaxURL]').attr('data-ajaxURL');
			this._normalURL = this.scope.closest('[data-normalURL]').attr('data-normalURL');

			// Get the template editor preferences
			this._editorPreferences['wrap'] = ips.utils.db.get('templateEditor', 'wrap');
			this._editorPreferences['lines'] = ips.utils.db.get('templateEditor', 'lines');

			if( this._editorPreferences['wrap'] ){
				$('#elTemplateEditor_preferences_menu').find('[data-ipsMenuValue="wrap"]').addClass('ipsMenu_itemChecked');
			}

			if( this._editorPreferences['lines'] ){
				$('#elTemplateEditor_preferences_menu').find('[data-ipsMenuValue="lines"]').addClass('ipsMenu_itemChecked');
			}			

			// Initialize the initial content
			this._tabContent.find('[data-type]').each( function (i, item) {

				// We need to turn the variables/attributes text input into a hidden field
				// We can't simply change the type because IE8 throws a hissy fit, so we'll make a copy
				// then remove the original
				var original = self._tabContent.find('[data-fileid="' + $( item ).attr('data-fileid') + 
														'"] input[data-role="variables"]');

				if( original.length ){
					original.after( 
						$('<input/>')
							.attr( 'type', 'hidden' )
							.attr( 'name', original.attr('name') )
							.attr( 'value', original.attr('value') )
							.attr( 'data-role', 'variables' )
					)
					
					original.remove();
				}

				ips.loader.get( ['core/interface/codemirror/diff_match_patch.js','core/interface/codemirror/codemirror.js'] ).then( function () {
					self._initCodeMirror( $( item ).attr('data-fileid'), $( item ).attr('data-type') );
				});
			});
		},

		/**
		 * Event handler for the editor preference menu
		 *
		 * @param	{event} 	e		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		menuSelected: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}
						
			if( data.triggerID == 'elTemplateEditor_preferences' ){
				if ( data.selectedItemID == 'diff' ) {
					this.toggleDiff('original');
				}
				else if ( data.selectedItemID == 'diffparent' ) {
					this.toggleDiff('parent');
				}
				else if( data.selectedItemID == 'wrap' ){
					this._changeEditorPreference( !ips.utils.db.get('templateEditor', 'wrap'), 'wrap', 'lineWrapping' );
				} else {
					this._changeEditorPreference( !ips.utils.db.get('templateEditor', 'lines'), 'lines', 'lineNumbers' );
				}
			}
		},

		/**
		 * Method that updates an editor preference
		 *
		 * @param	{object} 	data	Event data object from this.menuSelected
		 * @returns {void}
		 */
		_changeEditorPreference: function (toValue, type, cmName) {
			// Set the menu appropriately
			if( toValue ){
				$('#elTemplateEditor_preferences_menu')
					.find('[data-ipsMenuValue="' + type + '"]').addClass('ipsMenu_itemChecked');
			} else {
				$('#elTemplateEditor_preferences_menu')
					.find('[data-ipsMenuValue="' + type + '"]').removeClass('ipsMenu_itemChecked');
			}

			// Update controller variable
			this._editorPreferences[ type ] = toValue;

			// Update local DB
			ips.utils.db.set( 'templateEditor', type, toValue );

			// Update all CM instances
			for( var i in this._cmInstances ){
				if ( this._cmInstances[ i ] instanceof CodeMirror.MergeView ) {					
					this._cmInstances[ i ].edit.setOption( cmName, toValue );
					this._cmInstances[ i ].left.orig.setOption( cmName, toValue );
				} else {
					this._cmInstances[ i ].setOption( cmName, toValue );
				}
			}
		},

		/**
		 * A dialog has been opened
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		openedDialog: function (e, data) {
			if( data.elemID == 'elTemplateEditor_variables' || data.elemID == 'elTemplateEditor_attributes' ){
				this._insertVariablesIntoDialog( data );
			}
		},

		/**
		 * Inserts the current variables value into the dialog
		 *
		 * @param	{object} 	data 	Event data object from the dialog
		 * @returns {void}
		 */
		_insertVariablesIntoDialog: function (data) {
			// First get the variables
			var active = this._getActiveTab();

			if( !active.tabPanel ){
				return;
			}

			var value = active.tabPanel.find('[data-role="variables"]').val().trim();

			data.dialog
				.find('[data-role="variables"]')
					.val( value )
				.end()
				.find('[name="_variables_fileid"]')
					.val( active.tabPanel.attr('data-fileid') )
				.end()
				.find('[name="_variables_type"]')
					.val( active.tabPanel.attr('data-type') );
		},

		/**
		 * Updates the value of the variables field in the tab with the given file ID
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{event} 	e 		Event data object from templates.variablesDialog
		 * @returns {void}
		 */
		updateVariables: function (e, data) {
			// Find the panel with the correct fileID, and update it's variables value
			this._tabContent
				.find('[data-type="' + data.type + '"][data-fileid="' + data.fileID + '"]')
					.find('[data-role="variables"]')
						.val( data.value );
		},
		
		/**
		 * Toggle diff
		 *
		 * @param	{string}	type	Type of Diff to fetch
		 * @returns {void}
		 */
		toggleDiff: function (type) {
			
			var self = this;			
			var active = this._getActiveTab();
			var panel = active.tabPanel;
			var key = panel.attr('data-fileid');

			if ( this._currentDiff != null && this._currentDiff !== type )
			{
				if ( type == 'parent' ) {
					$('[data-ipsMenuValue="diff"]').click();
				} else {
					$('[data-ipsMenuValue="diffparent"]').click();
				}
			}

			// Toggle off
			if ( self._cmInstances[ key ] instanceof CodeMirror.MergeView ) {
				self.scope.find( '#editor_' + key ).val( this._cmInstances[ key ].edit.doc.getValue() );
				panel.find('.CodeMirror-merge,.cTemplateMergeHeaders').remove();
				self._initCodeMirror( key, panel.attr('data-type') );
				this._currentDiff = null;
			}
			
			// Toggle on
			else {

				this._currentDiff = type;
				// Get the contents from the current CodeMirror instance and remove it
				self._cmInstances[ key ].save();
				panel.find('.CodeMirror').remove();
				panel.addClass('ipsLoading');
						
				// Fire AJAX to get the original content
				ips.getAjax()( this._normalURL + '&do=diffTemplate&type=' + type, {
					dataType: 'json',
					data: this._getParametersFromPanel( panel )
				})
					.done( function (response) {
												
						// Initiate the merge view
						self._cmInstances[ key ] = CodeMirror.MergeView( document.getElementById( panel.attr('id') ), {
							value: self.scope.find( '#editor_' + key ).val(),
						    origLeft: response,
							lineWrapping: self._editorPreferences['wrap'],
							lineNumbers: self._editorPreferences['lines'],
						    mode: ( panel.attr('data-type') == 'templates' ? 'htmlmixed' : 'css' ),
						} );
						panel.removeClass('ipsLoading');
						
						// Add headers
						var headers = ( type === 'parent' ) ? $( ips.templates.render( 'templates.editor.diffHeadersParent' ) ) : $( ips.templates.render( 'templates.editor.diffHeaders' ) );
						panel.prepend( headers );
											
						// Set size
						var height = self._getTabContentHeight() - panel.find('.cTemplateMergeHeaders').outerHeight();
						self._cmInstances[ key ].edit.setSize( null, height );
						self._cmInstances[ key ].left.orig.setSize( null, height );
						$( self._cmInstances[ key ].left.gap ).css( 'height', height );
												
						// Add change handler
						self._cmInstances[ key ].edit.on( 'change', function (doc, cm) {
							self._setChanged( true, key );
						});
					});
			}
		},

		/**
		 * Saves the contents of the editor
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		save: function (e) {
			e.preventDefault();
			var self = this;
			var active = this._getActiveTab();
			var panel = active.tabPanel;
			var key = panel.attr('data-fileid');

			if( !active.tab || !active.tabPanel ){
				Debug.warn('No active tab or tab panel');
				return;
			}

			var save = this._getParametersFromPanel( panel );

			// We call .save() on the CodeMirror instance, which will cause it to update the
			// contents of the original textbox, unless we're in merge view, when we have to
			// do that ourselves
			if ( this._cmInstances[ key ] instanceof CodeMirror.MergeView ) {
				self.scope.find( '#editor_' + key ).val( this._cmInstances[ key ].edit.doc.getValue() );
			} else {
				this._cmInstances[ key ].save();
			}

			// Add it to the save object
			save[ 'editor_' + key ] = this.scope.find( '#editor_' + key ).val();

			// If this is a template, add its variables
			if( panel.attr('data-type') == 'templates' ){
				save[ 'variables_' + save.t_key ] = panel.find('[data-role="variables"]').val();
			}

			// Show loading
			this.trigger( 'savingFile.templates' );

			// Send it
			ips.getAjax()( this._normalURL + '&do=saveTemplate', {
				dataType: 'json',
				data: save,
				type: 'post'
			})
				.done( function (response) {

					// Let everyone know
					self.trigger( 'savedFile.templates', {
						key: key,
						oldID: parseInt( panel.attr('data-itemID') ),
						newID: parseInt( response.item_id ),
						status: 'changed'
					});

					// Remove the unsaved status from the tab
					self._setChanged( false, key );

					// Update the toolbar
					self._updateToolbar( active.tab );
				})
				.fail( function ( jqXHR ) {
					var message = ips.getString('saveThemeError');
					try
					{
						message = $.parseJSON( jqXHR.responseText );
					}
					catch (e) {}

					ips.ui.alert.show( {
						type: 'alert',
						message: message,
						icon: 'warn'
					});
				})
				.always( function () {
					self.trigger( 'saveFileFinished.templates' );
				});
		},

		/**
		 * Reverts or deletes a file
		 * If the bypass parameter is false, this method will confirm the action with the user first
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{boolean} 	bypass	Bypass the user confirmation?
		 * @returns {void}
		 */
		revert: function (e, bypass) {
			e.preventDefault();

			if ( $(e.currentTarget).hasClass('ipsButton_disabled') )
			{
				return false;
			}
			
			var self = this;
			var active = this._getActiveTab();
			var panel = active.tabPanel;
			var key = panel.attr('data-fileid');

			var message = ( $( e.currentTarget ).attr('data-actionType') == 'revert' ) ? 
					ips.getString('skin_revert_confirm') : ips.getString('skin_delete_confirm');

			if( bypass !== true ){
				ips.ui.alert.show({
					type: 'confirm',
					message: message,
					icon: 'warn',
					callbacks: {
						ok: function () {
							self.revert( e, true );
						}
					}
				});

				return;
			}

			var save = this._getParametersFromPanel( panel );

			// Send it
			ips.getAjax()( this._normalURL + '&do=deleteTemplate&wasConfirmed=1', {
				dataType: 'json',
				data: save,
				type: 'post'
			})
				.done( function (response) {
					if( _.isUndefined( response.type ) ){
						self._deletedFile( key, active );
					} else {
						if( response.template_id || response.css_id ){
							self._revertedFile( response, key, active );							
						} else {
							self._deletedFile( key, active );
						}
					}
				});
		},

		/**
		 * Handles updating the editor when a file is reverted
		 *
		 * @param 	{object} 	response 	JSON response object from ajax request
		 * @param	{string} 	key 	 	Key of the file that's been reverted
		 * @param 	{object} 	active 		Object containing keys 'tab' and 'tabPanel' referencing active items
		 * @returns {void}
		 */
		_revertedFile: function (response, key, active) {
			// Let the document know
			this.trigger( 'revertedFile.templates', {
				key: key,
				oldID: parseInt( active.tabPanel.attr('data-itemID') ),
				newID: parseInt( response.template_id || response.css_id ),
				status: response.InheritedValue
			});

			// Update the raw textarea
			$( '#editor_' + key ).val( response.template_content || response.css_content );

			// Update the variables
			// CSS files don't have variables so checl our response has some to set
			if( _.isString( response.template_data ) ){
				active.tabPanel.find('[data-role="variables"]').val( response.template_data.trim() );
				$('#elTemplateEditor_variables_dialog, #elTemplateEditor_attributes').find('[data-role="variables"]').val( response.template_data.trim() );
			}

			// Update codemirror
			if ( this._cmInstances[ key ] instanceof CodeMirror.MergeView ) {
				this._cmInstances[ key ].edit.setValue( response.template_content || response.css_content );
			} else {
				this._cmInstances[ key ].setValue( response.template_content || response.css_content );
			}

			// Remove the unsaved status from the tab
			this._setChanged( false, key );

			// Update the toolbar
			this._updateToolbar( active.tab );
		},

		/**
		 * Handles updating the editor when a file is deleted
		 *
		 * @param	{string} 	key 	 	Key of the file that's been reverted
		 * @param 	{object} 	active 		Object containing keys 'tab' and 'tabPanel' referencing active items
		 * @returns {void}
		 */
		_deletedFile: function (key, active) {
			this.trigger( 'deletedFile.templates', {
				key: key,
				fileID: active.tabPanel.attr('data-itemID'),
				type: active.tabPanel.attr('data-type')
			});

			// Find close link in the tab
			active.tab.find('[data-action="closeTab"]').click();
		},

		/**
		 * Returns an object of parameters used by the ajax requests
		 *
		 * @param	{element} 	panel 	The panel being used as the source
		 * @returns {object}
		 */
		_getParametersFromPanel: function (panel) {
			return { 
				t_type: panel.attr('data-type'),
				t_item_id: panel.attr('data-itemID'),
				t_app: panel.attr('data-app'),
				t_location: panel.attr('data-location'),
				t_group: panel.attr('data-group'),
				t_name: panel.attr('data-name'),
				t_key: panel.attr('data-fileid')
			};
		},

		/**
		 * A file has been saved or reverted
		 * Updates the ID of any element with the old ID, and changes the state
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		updateFile: function (e, data) {
			this.scope
				.find('[data-itemID="' + data.oldID + '"]')
					.attr( 'data-itemID', data.newID )
					.attr( 'data-inherited-value', data.status );
		},

		/**
		 * Event handler for clicking a close tab button
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		closeTab: function (e) {
			var tab = $( e.currentTarget ).closest('.ipsTabs_item');
			this._doCloseTab( tab );
		},

		/**
		 * Handles closing a tab.
		 * We first check if the tab is in an 'unsaved' state, and if so, prompt the user to confirm losing changes.
		 * We then destroy the codemirror instance, remove the tab and panel, and switch to another open tab.
		 *
		 * @param	{element} 	tab 	The tab to be closed
		 * @param	{boolean} 	bypass 	Whether to bypass the unsaved check
		 * @returns {void}
		 */
		_doCloseTab: function (tab, bypass) {
			var self = this;
			var tabParent = tab.closest('[data-fileid]');
			var key = tabParent.attr('data-fileid');
			var allTabs = this._tabBar.find('.ipsTabs_item').closest('[data-fileid]');
			var newTab = null;

			// Check if there's unsaved content
			if( tabParent.attr('data-state') == 'unsaved' && bypass != true ){
				ips.ui.alert.show({
					type: 'confirm',
					message: ips.getString('themeUnsavedContent'),
					icon: 'warn',
					callbacks: {
						ok: function () {
							self._doCloseTab( tab, true );
						}
					}
				});

				return;
			} 

			// Is this tab active?
			var active = tab.hasClass('ipsTabs_activeItem');

			// Let the document know what we're up to
			this.trigger( 'closedTab.templates', {
				fileID: key
			});

			// Remove the codemirrrrr element & instance
			delete( this._cmInstances[ key ] );

			// Find the next or prev tab, if this tab is active, and switch to it
			if( active && allTabs.length > 1 ){
				if( allTabs.first().attr('data-fileid') == tabParent.attr('data-fileid') ){
					newTab = tabParent.next();
				} else {
					newTab = tabParent.prev();
				}
			}

			if( newTab ){
				newTab.find('> a').click();
			}

			// Close the tab
			ips.utils.anim.go('fadeOutDown fast', tabParent)
				.done( function () {
					tabParent.remove();
					self._recalculatePanelWrapper();
				});

			// Remove the panel
			this._tabContent.find('[data-fileid="' + key + '"]').remove();
		},

		/**
		 * Tab widget has indicated that the user has changed tab
		 * If there's a file ID, trigger a new event with it, to enable the file listing to highlight it
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data 	Event data object
		 * @returns {void}
		 */
		changedTab: function (e, data) {
			var tab = data.tab;

			if( !_.isUndefined( tab.closest('[data-fileid]').attr('data-fileid') ) ){
				this.trigger( 'fileSelected.templates', {
					fileID: tab.closest('[data-fileid]').attr('data-fileid')
				});
			}

			this._updateToolbar( tab );
		},

		/**
		 * Updates the toolbar buttons
		 *
		 * @param	{element} 	tab 	The current tab
		 * @returns {void}
		 */
		_updateToolbar: function (tab) {
			var tabParent = tab.closest('[data-fileid]').attr('data-fileid');
			var tabPanel = this._tabContent.find('[data-fileid="' + tabParent + '"]');
			var status = tabPanel.attr('data-inherited-value');
			var type = tabPanel.attr('data-type');
			var revert = this.scope.find('[data-action="revert"]');
			var key = tabPanel.attr('data-fileid');

			switch( status ){
				case 'original':
				case 'inherit':
					revert
						.addClass('ipsButton_disabled')
				break;
				case 'custom':
					revert
						.html( ips.getString('skin_delete') )
						.removeClass('ipsButton_disabled')
						.attr('data-actionType', 'delete')
						.show();
				break;
				case 'changed':
				case 'outofdate':
					revert
						.html( ips.getString('skin_revert') )
						.removeClass('ipsButton_disabled')
						.attr('data-actionType', 'revert')
						.show();
				break;
			}

			if( type == 'templates' ){
				$('#elTemplateEditor_variables').show();
				$('#elTemplateEditor_attributes').hide();
			} else {
				$('#elTemplateEditor_variables').hide();
				$('#elTemplateEditor_attributes').show();
			}
			
			if ( this._cmInstances[ key ] instanceof CodeMirror.MergeView ) {
				if ( this._currentDiff == null || this._currentDiff != 'parent' ) {
					$('[data-ipsmenuvalue="diff"]').addClass('ipsMenu_itemChecked');
				} else {
					$('[data-ipsmenuvalue="diffparent"]').addClass('ipsMenu_itemChecked');
				}
			} else {
				if ( this._currentDiff == null || this._currentDiff != 'parent' ) {
					$('[data-ipsmenuvalue="diff"]').removeClass('ipsMenu_itemChecked');
				} else {
					$('[data-ipsmenuvalue="diffparent"]').removeClass('ipsMenu_itemChecked');
				}
			}
		},

		/**
		 * Reponds to the openFile event, to open a file
		 * Either switch to it if already open, or hand off to _buildTab to load it
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data 	Event data object
		 * @returns {void}
		 */
		openFile: function (e, data) {
			
			// Is this file already open?
			if( !this._tabBar.find('[data-fileid="' + data.meta.key + '"]').length ){
				this._buildTab( data.meta );
			} else {
				this._tabBar.find('[data-fileid="' + data.meta.key + '"] > a').click();
			}
		},

		/**
		 * Builds a tab for the file with the given metadata
		 *
		 * @param	{object} 	meta 	Object of file metadata
		 * @returns {void}
		 */
		_buildTab: function (meta) {
			var self = this;

			// Build the actual tab
			this._tabBar.append( ips.templates.render('templates.editor.newTab', {
				title: meta.title,
				fileid: meta.key,
				id: 'tab_' + meta.key
			}));

			// Build the content container
			this._tabContent.append( ips.templates.render('templates.editor.tabPanel', {
				fileid: meta.key,
				name: meta.name,
				type: meta.type,
				app: meta.app,
				location: meta.location,
				group: meta.group,
				id: meta.id,
				inherited: meta.inherited
			}));

			// We may need to rejig the tab pane wrap to account for wrapped tabs,
			// so do that now both tab and panel have been added
			this._recalculatePanelWrapper();

			// Toggle the new tab
			this._tabBar.find('[data-fileid="' + meta.key + '"] > a').click();

			// Manually set the content area to loading since we aren't using ui.tabbar's load methods
			this._tabContent.addClass('ipsLoading ipsTabs_loadingContent');

			// Load the content
			ips.getAjax()( this._ajaxURL + '&do=loadTemplate', {
				dataType: 'json',
				data: { 
					't_app':      meta.app,
					't_location': meta.location,
					't_group':    meta.group,
					't_name':     meta.name,
					't_key':      meta.key,
					't_type':	  meta.type
				}
			})
				.done( function (response) {
					self._postProcessNewTab( response, meta );
				})
				.always( function () {
					self._tabContent.removeClass('ipsLoading ipsTabs_loadingContent');
				});
		},

		/**
		 * Once tab content has been returned by ajax, this method builds the content of a tab,
		 * and initializes codemirrior for syntax highlighting
		 *
		 * @param	{object} 	response  	Response JSON object from ajax request
		 * @param 	{object} 	meta 		Object of meta data for the tab being created
		 * @returns {void}
		 */
		_postProcessNewTab: function (response, meta) {
			var content = ips.templates.render('templates.editor.tabContent', {
				fileid: meta.key,
				content: response.template_content || response.css_content,
				variables: response.template_data || response.css_attributes
			});

			this._tabContent.find('[data-fileid="' + meta.key + '"]').html( content );
			this._initCodeMirror( meta.key, meta.type );
		},

		/**
		 * Initializes CodeMirror on a textarea with the provided key
		 *
		 * @param 	{string}	key 	Key of the textarea to be turned into codemirrior
		 * @param	{string} 	type 	'templates' or 'css'
		 * @returns {void}
		 */
		_initCodeMirror: function (key, type) {
			var self = this;

			this._cmInstances[ key ] = CodeMirror.fromTextArea( document.getElementById('editor_' + key ), { 
				mode: (type == 'templates' ? 'htmlmixed' : 'css'),
				lineWrapping: this._editorPreferences['wrap'],
				lineNumbers: this._editorPreferences['lines']
			} );
			this._cmInstances[ key ].setSize( null, this._getTabContentHeight() );

			this._cmInstances[ key ].on( 'change', function (doc, cm) {
				self._setChanged( true, key );
			});
		},

		/**
		 * Sets a tab to 'unsaved' state
		 *
		 * @returns {void}
		 */
		_setChanged: function (state, key) {

			if( state == true ){
				// Update 'x' in tab to an unsaved version, then set state on the tab
				this._tabBar
					.find('[data-fileid="' + key + '"]')
						.attr('data-state', 'unsaved')
						.find('[data-action="closeTab"]')
							.html( ips.templates.render('templates.editor.unsaved') );
			} else {
				this._tabBar
					.find('[data-fileid="' + key + '"]')
						.attr('data-state', 'saved')
						.find('[data-action="closeTab"]')
							.html( ips.templates.render('templates.editor.saved') );
			}
		},

		/**
		 * Calculates whether the tab bar has wrapped, and if so, resizes the panel wrapper and updates
		 * CodeMirror instances with the new height
		 *
		 * @returns {void}
		 */
		_recalculatePanelWrapper: function () {
			// Get height of the tab bar
			var tabHeight = this._tabBar.outerHeight();

			/*if( tabHeight == this._currentHeight ){
				return;
			}*/

			// Set the top value of the panel
			this._tabContent.css( { top: tabHeight + 'px' } );

			// Get the height of it
			var contentHeight = this._getTabContentHeight();

			if ( this._tabContent.find('.cTemplateMergeHeaders').length )
			{
				/* We are viewing a diff, so we need to account for this bar too */
				contentHeight -= this._tabContent.find('.cTemplateMergeHeaders').outerHeight();
			}

			Debug.log( contentHeight );
			
			// Find all codemirror instances and resize those
			this._tabContent.find('.CodeMirror').css( { height: contentHeight + 'px' } );

			// .. and any merge gap bars so they don't overrun
			this._tabContent.find('.CodeMirror-merge-gap').css( { height: contentHeight + 'px' } );

			this._currentHeight = tabHeight;
		},

		/**
		 * Returns references to both the active tab and the active tab panel
		 *
		 * @returns {object} 	Contains keys 'tab' and 'tabPanel', which are jQuery objects
		 */
		_getActiveTab: function () {
			var toReturn = {
				tab: null,
				tabPanel: null
			};

			var tab = this._tabBar.find('.ipsTabs_item.ipsTabs_activeItem').first().parent();

			if( !tab.length ){
				return toReturn;
			}

			// Get the associated panel
			toReturn = {
				tab: tab,
				tabPanel: this._tabContent.find('[data-fileid="' + tab.attr('data-fileid') + '"]')
			};

			return toReturn;
		},

		/**
		 * Returns the current height of the tab panel wrapper
		 *
		 * @returns {number}
		 */
		_getTabContentHeight: function () {
			var tabContentTop = this._tabContent.offset().top;
			var windowHeight = $( window ).height();
			var infoHeight = $('#elTemplateEditor_info').outerHeight();		

			this._panelHeight = windowHeight - tabContentTop - infoHeight;
			return this._panelHeight;
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.fileList.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.templates.fileList.js - Templates: controller for the file listing component of the template manager
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.templates.fileList', {

		_tabBar: null,
		_tabContent: null,

		initialize: function () {
			// Events started here			
			this.on( 'click', '[data-action="openFile"]', this.openFile );
			this.on( 'click', '[data-action="toggleBranch"]', this.toggleBranch );

			// Events coming from elsewhere
			this.on( document, 'fileSelected.templates', this.selectFile );
			this.on( document, 'savedFile.templates revertedFile.templates', this.updateItemID );
			this.on( document, 'savedFile.templates revertedFile.templates', this.fileChangedStatus );
			
			this.on( document, 'addedFile.templates', this.refreshFileList );
			this.on( document, 'deletedFile.templates', this.refreshFileList );

			var debounce = _.debounce( _.bind( this.resizeFileList, this ), 100 );
			this.on( window, 'resize', debounce );
			this.on( document, 'contentTruncated', this.resizeFileList );

			// Other setup
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			this._tabBar = this.scope.find('#elTemplateEditor_typeTabs');
			this._tabContent = this.scope.find('#elTemplateEditor_fileList');
			this.resizeFileList();
		},

		/**
		 * Resizes the file list to full height
		 *
		 * @returns {void}
		 */
		resizeFileList: function () {
			// Get height of parts we want to exclude
			var fileListTop = this._tabContent.offset().top;
			var infoHeight = $('#elTemplateEditor_info').height();
			var newButtonHeight = this.scope.find('#elTemplateEditor_newButton').outerHeight();
			var browserHeight = $( window ).height();

			var fileListNewTop = browserHeight - fileListTop - infoHeight - newButtonHeight;

			this._tabContent.css({
				height: fileListNewTop + 'px'
			});
		},

		/**
		 * Something has changed in the file list, so we refresh it and try and remember
		 * the position we were at. If a new file ID is provided, we'll also select it.
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		refreshFileList: function (e, data) {
			var self = this;
			var type = data.type;
			var panel = this._tabContent.find('.cTemplateList[data-type="' + type + '"]');
			var activeItem = panel.find('.cTemplateList_activeNode > a').attr('data-key');

			// If the panel isn't actually open, we'll just show it
			if( !panel.length ){
				this._tabBar.find('[data-type="' + type + '"]').click();
			} else {
				var open = this._getOpenNodes( panel );

				// Now fetch the new list
				var url = this._tabBar.find('[data-type="' + type + '"]').attr('data-tabURL');

				ips.getAjax()( url )
					.done( function (response) {
						panel.html( response );

						// Now reopen all the nodes
						self._openNodes( open, panel, activeItem );

						if( data.fileID ){
							// Click it
							panel.find('[data-itemid="' + data.fileID + '"]').click();
						}

						// Let everyone know
						self.trigger('fileListRefreshed.templates');
					});	
			}
		},

		/**
		 * Opens the nodes provided in the toOpen param
		 *
		 * @param	{object} 	toOpen 		Object of nodes to open, containing three keys: apps, locations, groups
		 * @returns {void}
		 */
		_openNodes: function (toOpen, panel, activeItem) {

			var selector = [];

			// Get the apps
			if( toOpen.apps.length ){	
				for( var i = 0; i < toOpen.apps.length; i++ ){
					selector.push('[data-app="' + toOpen.apps[i] + '"]');
				}
			}

			// Get locations
			if( toOpen.locations.length ){
				for( var i = 0; i < toOpen.locations.length; i++ ){
					selector.push('[data-app="' + toOpen.locations[i][0] + '"] [data-location="' + toOpen.locations[i][1] + '"]');
				}
			}

			// Get groups
			if( toOpen.groups.length ){
				for( var i = 0; i < toOpen.groups.length; i++ ){
					var str = '[data-app="' + toOpen.groups[i][0] + '"] ';
						str += '[data-location="' + toOpen.groups[i][1] + '"] ';
						str += '[data-group="' + toOpen.groups[i][2] + '"]';

					selector.push( str );
				}
			}

			// Now close all branches, then reopen the ones matching our selector
			panel
				.find('.cTemplateList_activeBranch')
					.removeClass('cTemplateList_activeBranch')
					.addClass('cTemplateList_inactiveBranch')
				.end()
				.find( selector.join(',') )
					.removeClass('cTemplateList_inactiveBranch')
					.addClass('cTemplateList_activeBranch');

			// Anything to make active?
			if( activeItem ){
				panel
					.find('[data-key="' + activeItem + '"]')
						.click();
			}
		},

		/**
		 * Returns an object containing the open nodes in the provided panel
		 *
		 * @param	{element} 	panel 	Panel element to fetch from
		 * @returns {object}	Three array keys: apps, locations, groups
		 */
		_getOpenNodes: function (panel) {
			var apps = [];
			var locations = [];
			var groups = [];

			// Fetch all open nodes
			panel.find('.cTemplateList_activeBranch').each( function (i, item) {
				var el = $( item );

				if( el.attr('data-app') ){
					apps.push( el.attr('data-app') );
				}

				if( el.attr('data-location') ){
					locations.push( [ 	
						el.closest('[data-app]').attr('data-app'),
						el.attr('data-location') 
					]);
				}

				if( el.attr('data-group') ){
					groups.push( [ 	
						el.closest('[data-app]').attr('data-app'), 
						el.closest('[data-location]').attr('data-location'),
						el.attr('data-group')
					]);
				}
			});

			return {
				apps: apps,
				locations: locations,
				groups: groups
			};
		},

		/**
		 * The editor controller has indicated that a file tab has been selected
		 * We respond to this event by highlighting the file in the list
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		selectFile: function (e, data) {
			if( data.fileID ){
				this._makeActive( data.fileID );
			}
		},

		/**
		 * Event handler for clicking a file node in the listing.
		 * Gather metadata from the file, then trigger an event so that the editor controller
		 * can load it.
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		openFile: function (e) {
			e.preventDefault();
			var elem = $( e.currentTarget );

			// Get meta data for this file
			var meta = {
				name: elem.attr('data-name'),
				key: elem.attr('data-key'),
				title: elem.text(),
				group: elem.closest('[data-group]').attr('data-group'),
				location: elem.closest('[data-location]').attr('data-location'),
				app: elem.closest('[data-app]').attr('data-app'),
				type: elem.closest('[data-type]').attr('data-type'),
				id: elem.closest('[data-itemID]').attr('data-itemID'),
				inherited: elem.closest('[data-inherited-value]').attr('data-inherited-value')
			};

			Debug.log( meta );

			this.trigger( 'openFile.templates', {
				meta: meta
			});
		},

		/**
		 * Event handler for clicking a branch in the listing.
		 * Expends or collapses the branch
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		toggleBranch: function (e) {
			e.preventDefault();
			var branchTrigger = $( e.currentTarget );
			var branchItem = branchTrigger.parent();

			if( branchItem.hasClass('cTemplateList_inactiveBranch') ){
				ips.utils.anim.go( 'fadeInDown', branchItem.find(' > ul') );

				branchItem
					.removeClass('cTemplateList_inactiveBranch')
					.addClass('cTemplateList_activeBranch');
			} else {
				branchItem.find(' > ul').hide();

				branchItem
					.removeClass('cTemplateList_activeBranch')
					.addClass('cTemplateList_inactiveBranch');
			}
		},

		/**
		 * Updates the ID of any element with the old ID
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		updateItemID: function (e, data) {
			if( data.oldID != data.newID ){
				this.scope
					.find('[data-itemID="' + data.oldID + '"]')
						.attr( 'data-itemID', data.newID );
			}
		},

		/**
		 * A file's status has changed, so we update it with the new status
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		fileChangedStatus: function (e, data) {
			this.scope
				.find('[data-key="' + data.key + '"]')
					.attr( 'data-inherited-value', data.status );
		},

		/**
		 * Finds the provided fileID in the list, highlights it and opens all branches to it
		 *
		 * @param	{string} 	fileID 		fileID of node to higlight
		 * @returns {void}
		 */
		_makeActive: function (fileID) {
			// Find the file entry
			var file = this.scope.find('[data-key="' + fileID +'"]');
			var fileType = file.closest('[data-type]').attr('data-type');
			var currentType = this._currentType();

			// Make all others inactive
			this.scope.find('[data-key]').parent().removeClass('cTemplateList_activeNode');

			// Make this one active
			file.parent().addClass('cTemplateList_activeNode');

			// Get all parent nodes, and show them
			file.parents('li[data-group], li[data-location], li[data-app]').each( function (idx, parent) {
				if( $( parent ).hasClass('cTemplateList_inactiveBranch') ){
					$( parent )
						.removeClass('cTemplateList_inactiveBranch')
						.addClass('cTemplateList_activeBranch')
						.find('> ul')
							.show();
				}
			});

			// Do we need to change tab?
			if( fileType == 'templates' && currentType != 'templates' ){
				this._tabBar.find('[data-type="templates"]').click();
			} else if( fileType == 'css' && currentType != 'css' ){
				this._tabBar.find('[data-type="css"]').click();
			}
		},

		/**
		 * Returns the currently-selected type being shown (templates or css)
		 *
		 * @returns {string}
		 */
		_currentType: function () {
			if( this._tabBar.find('[data-type="templates"]').hasClass('ipsTabs_activeItem') ){
				return 'templates';
			} 
			
			return 'css';
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.main.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.templates.main.js - Templates: Parent controller for the template editor
 * Simply manages showing the loading thingy based on events coming from within
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.admin.templates.main', {
		
		_timer: null,
		_textField: null,
		_lastValue: '',
		_ajax: null,

		initialize: function () {
			this.on( 'savingFile.templates', this.showLoading );
			this.on( 'saveFileFinished.templates', this.hideLoading );
			
			this._textField = $(this.scope).find('[data-role="templateSearch"]');
			this.on( document, 'focus', '[data-role="templateSearch"]', this.fieldFocus );
			this.on( document, 'blur', '[data-role="templateSearch"]', this.fieldBlur );
			this.on( 'menuItemSelected', this.menuSelected );
			this.on( document, 'tabChanged', this._swapTab );
		},

		/**
		 * Shows the loading thingy
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		showLoading: function (e) {
			ips.utils.anim.go( 'fadeIn', this.scope.find('[data-role="loading"]') );
		},

		/**
		 * Hides the loading thingy
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		hideLoading: function (e) {
			ips.utils.anim.go( 'fadeOut', this.scope.find('[data-role="loading"]') );
		},
		
		/**
		 * Event handler for focusing in the search box
		 * Set a timer going that will watch for value changes. If there's already a value,
		 * we'll show the results immediately
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		fieldFocus: function (e) {
			this._timer = setInterval( _.bind( this._timerFocus, this ), 700 );
		},

		/**
		 * Event handler for field blur
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		fieldBlur: function (e) {
			clearInterval( this._timer );
		},
		
		/**
		 * Timer callback from this.fieldFocus
		 * Compares current value to previous value, and shows/loads new results if it's changed
		 *
		 * @returns {void}
		 */
		_timerFocus: function () {
			var currentValue = this._textField.val();
			
			if( currentValue == this._lastValue ){
				return;
			}

			this._lastValue = currentValue;
			
			this._loadResults();
		},
		
		/**
		 * Event when tab is changed
		 *
		 * @returns {void}
		 */
		_swapTab: function (e,data) {
			if ( data.barID == 'elTemplateEditor_typeTabs' ) {
				this._loadResults();
			}
		},
		
		/**
		 * Event handler for the filter menu
		 *
		 * @param	{event} 	e		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		menuSelected: function (e, data) {
			if( data.originalEvent ){
				data.originalEvent.preventDefault();
			}			
			
			this._loadResults();
		},

		/**
		 * Load results from the server
		 *
		 * @returns {void}
		 */
		_loadResults: function () {
			if( this._ajax ){
				this._ajax.abort();
			}

			if ( this._lastValue ) {
				this._textField.addClass('ipsField_loading');
			}
			$('#elTemplateEditor_fileList').find('li').addClass('ipsHide');

			var filters = [];
			$('#elTemplateFilterMenu_menu .ipsMenu_itemChecked').each(function(){
				filters.push( $(this).attr('data-ipsMenuValue') );
			});

			var self = this;
			this._ajax = ips.getAjax()( $(this.scope).attr('data-ajaxURL') + '&do=search' + $('#elTemplateEditor_typeTabs').find('.ipsTabs_activeItem').attr('data-type') + '&term=' + encodeURIComponent( this._lastValue ) + '&filters=' + filters.join(',') ).done(function(response){
				var i;
				for ( i in response ) {
					$('#elTemplateEditor_fileList').find('[data-app="' + i + '"]').removeClass('ipsHide');
					var j;
					for ( j in response[i] )
					{
						$('#elTemplateEditor_fileList').find('[data-app="' + i + '"] [data-location="' + j + '"]').removeClass('ipsHide');
						var k;
						for ( k in response[i][j] )
						{
							$('#elTemplateEditor_fileList').find('[data-app="' + i + '"] [data-location="' + j + '"] [data-group="' + k + '"]').removeClass('ipsHide');
							var l;
							for ( l in response[i][j][k] )
							{
								if( k == '.' ){
									$('#elTemplateEditor_fileList').find('[data-app="' + i + '"] [data-location="' + j + '"] [data-name="' + response[i][j][k][l] + '"]').parent().removeClass('ipsHide');
								} else {
									$('#elTemplateEditor_fileList').find('[data-app="' + i + '"] [data-location="' + j + '"] [data-group="' + k + '"] [data-name="' + response[i][j][k][l] + '"]').parent().removeClass('ipsHide');
								}
							}
						}
					}
				}
				self._textField.removeClass('ipsField_loading');
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.simple.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.templates.simple.js - Templates: controller for the simple editor
 *
 * Author: Matt &quot;Oops I did it again&quot; Mecham
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.templates.simple', {
		
		_cmInstances: {},
		
		initialize: function () {
			this.on( 'click', 'button[type=&quot;submit&quot;]', this.save );
			this.on( 'tabChanged', this.changedTab );
			var debounce = _.debounce( _.bind( this._recalculatePanelWrapper, this ), 100 );
			this.on( window, 'resize', debounce );

			// Other setup
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			var self = this;
			
			ips.loader.get( ['core/interface/codemirror/diff_match_patch.js','core/interface/codemirror/codemirror.js'] ).then( function () {
				self._initCodeMirror( 'theme_simple_header', 'htmlmixed' );
				self._initCodeMirror( 'theme_simple_footer', 'htmlmixed' );
				self._initCodeMirror( 'theme_simple_css', 'css' );
			});
		},
		
		/**
		 * Initializes CodeMirror on a textarea with the provided key
		 *
		 * @param 	{string}	key 	Key of the textarea to be turned into codemirrior
		 * @param	{string} 	type 	'templates' or 'css'
		 * @returns {void}
		 */
		_initCodeMirror: function (key, type) {
			var self = this;
			this._cmInstances[ key ] = CodeMirror.fromTextArea( document.getElementById( 'elTextarea_' + key ), { 
				mode: type,
				lineNumbers: false
			} );
			this._cmInstances[ key ].setSize( null, this._getContentHeight() );

			this.scope.find('#elTextarea_' + key ).parent().addClass('ipsAreaBackground ipsPad_half ipsClearfix');
		},
		
		/**
		 * Tab widget has indicated that the user has changed tab
		 * If there's a file ID, trigger a new event with it, to enable the file listing to highlight it
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data 	Event data object
		 * @returns {void}
		 */
		changedTab: function (e, data) {
			this._recalculatePanelWrapper();
		},
		
		/**
		 * Saves the contents of the editor
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		save: function (e) {
			e.preventDefault();
			var self = this;
			var form = this.scope.find('form[data-formid=&quot;form&quot;]');
			var save = {};
			
			// We call .save() on the CodeMirror instance, which will cause it to update the
			// contents of the original textbox.
			_.each( this._cmInstances, function(key, cm)
			{
				key.save();
			});

			// Get the fields
			form.find('input').each( function() {
				save[ $(this).attr('name') ] = $(this).val();
			} );
			
			form.find('textarea').each( function() {
				save[ $(this).attr('name') ] = $(this).val();
			} );
			
			Debug.log( save );
			
			self.scope.find('button[type=&quot;submit&quot;]').addClass('ipsButton_disabled').removeClass('ipsButton_primary');
			
			// Send it
			ips.getAjax()( form.attr('action'), {
				dataType: 'json',
				data: save,
				type: 'post'
			})
				.done( function (response) {
					ips.ui.flashMsg.show( ips.getString('saved') );
				})
				.fail( function ( jqXHR ) {
					var message = ips.getString('saveThemeError');
					try
					{
						message = $.parseJSON( jqXHR.responseText );
					}
					catch (e) {}

					ips.ui.alert.show( {
						type: 'alert',
						message: message,
						icon: 'warn'
					});
				})
				.always( function () {
					self.scope.find('button[type=&quot;submit&quot;]').removeClass('ipsButton_disabled').addClass('ipsButton_primary');
				});
		},
		
		/**
		 * Returns the current height of the tab panel wrapper
		 *
		 * @returns {number}
		 */
		_getContentHeight: function () {
			return $( window ).height() - this.scope.find('#tabs_form').offset().top - 250;
		},
		
		/**
		 * Calculates whether the tab bar has wrapped, and if so, resizes the panel wrapper and updates
		 * CodeMirror instances with the new height
		 *
		 * @returns {void}
		 */
		_recalculatePanelWrapper: function () {
			// Get the height of it
			var self = this;

			_.each( this._cmInstances, function(key, cm)
			{
				key.setSize( null, self._getContentHeight() );
			});
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.variablesDialog.js" javascript_type="controller" javascript_version="107643" javascript_position="1000550">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.templates.variablesDialog.js - Controller for the variables dialog
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.controller.register('core.admin.templates.variablesDialog', {

		initialize: function () {
			this.on( 'click', 'input[type=&quot;submit&quot;]', this.submitChange );
		},

		/**
		 * Event handler called when the submit button within the dialog is clicked
		 * Fires an event that the editor controller can respond to
		 *
		 * @param	{event} 	e 	Event object
		 * @returns {void}
		 */
		submitChange: function (e) {

			var key = this.scope.find('[name=&quot;_variables_fileid&quot;]').val();
			$('[data-fileid=&quot;' + key + '&quot;]')
				.attr('data-state', 'unsaved')
				.find('[data-action=&quot;closeTab&quot;]')
				.html( ips.templates.render('templates.editor.unsaved') );
				
			this.trigger( 'variablesUpdated.templates', {
				fileID: this.scope.find('[name=&quot;_variables_fileid&quot;]').val(),
				type: this.scope.find('[name=&quot;_variables_type&quot;]').val(),
				value: this.scope.find('[data-role=&quot;variables&quot;]').val()
			});

			this.trigger( 'closeDialog' );
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/vse" javascript_name="ips.vse.colorizer.js" javascript_type="controller" javascript_version="107643" javascript_position="1000600"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.vse.colorizer.js - VSE colorizer controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.vse.colorizer', {

		initialize: function () {
            //this.on( 'change', "input[type='text']", this.colorChanged );
            this.on( 'change.spectrum move.spectrum', this.colorChanged );
			this.on( 'click', '[data-action="revertColorizer"]', this.revertChanges );
			this.on( 'click', '[data-action="invertColors"]', this.invertColors );
			this.setup();
		},

		setup: function () {
            var colors = {};
            var self = this;


			Debug.log( this.scope.data('styleData') );

			_.each( colorizer.startColors, function (value, key) {
				colors[ key ] = '#' + value;
			});

			this.scope.html( ips.templates.render( 'vse.colorizer.panel', colors ) );

            // Set up jscolor on the items
            ips.loader.get( ['core/interface/spectrum/spectrum.js'] ).then( function () {
				
				var inputs = self.scope.find('input[type="text"].color');
				
				inputs.each( function () {
					var options = {
						type: "color",
						clickoutFiresChange: true,
						hideAfterPaletteSelect: true,
						preferredFormat: "hex",
						showAlpha: false,
						allowEmpty: false,
						showInput: true,
						showInitial: true,
						replacerClassName: this.className
					};
					
					$( this ).attr('data-original', $( this ).val()).spectrum( options );	
				})
            } );
		},

		/**
		 * Event handler for a color value being changed
		 * Determines the hue/sat for the selected color, then loops through all styles and applies it to the relevant ones
		 *
		 * @param		{event}		e 	Event object
		 * @returns 	{void}
		 */
		colorChanged: function (e, color) {
			var self = this;
            var type = $( e.target ).attr('data-role');
            var hsl = color.toHsl();

			if( _.isUndefined( colorizer[ type ] ) ){
				Debug.error("Can't find data for " + type);
				return;
			}

            this.trigger( 'colorized', {
                color: color.toHsl(),
                type: type
			});

			// Enable button
			this.scope.find('[data-action="revertColorizer"]').attr('disabled', false);
		},

		/**
		 * Reverts colors back to their default state
		 *
		 * @param		{object}	e 		Event object
		 * @returns 	{void}
		 */
		revertChanges: function (e) {
			var self = this;

			// Confirm with the user this is OK
			ips.ui.alert.show( {
				type: 'confirm',
				icon: 'warn',
				message: ips.getString('vseRevert'),
				subText: ips.getString('vseRevert_subtext'),
				callbacks: {
					ok: function () {
						self.trigger('revertChanges');
						self.trigger('closeColorizer');

						// Revert the colors in our text boxes too
						self.scope.find('.vseColorizer_swatch').each( function () {
							$( this ).spectrum('set', $( this ).attr('data-original') );
						});
					}
				}
			});
		},

		invertColors: function (e) {
			e.preventDefault();

			this.trigger('invertColors');
		},

		/**
		 * Updates a style background (color & gradient)
		 *
		 * @param		{object}	styleData 		Data for this style
		 * @param 		{string}	styleKey		Key in the main object that identifies this style
		 * @param 		{number}	h 				New hew
		 * @param 		{number}	s 				New saturation
		 * @returns 	{void}
		 */
		_updatebackground: function (styleData, styleKey, h, s) {
			if( _.isUndefined( styleData.background ) ){
				return;
			}

			if( styleData.background.color ){
				styleData.background.color = ips.utils.color.convertHex( styleData.background.color, h, s );
			}

			if( styleData.background.gradient ){
				for( var i = 0; i < styleData.background.gradient.stops.length; i++ ){
					styleData.background.gradient.stops[ i ][0] = ips.utils.color.convertHex( styleData.background.gradient.stops[ i ][0], h, s );
				}
			}

			this.trigger( 'styleUpdated', {
				selector: styleData.selector
			});
		},

		/**
		 * Updates a style font (color)
		 *
		 * @param		{object}	styleData 		Data for this style
		 * @param 		{string}	styleKey		Key in the main object that identifies this style
		 * @param 		{number}	h 				New hew
		 * @param 		{number}	s 				New saturation
		 * @returns 	{void}
		 */
		_updatefont: function (styleData, styleKey, h, s) {
			if( _.isUndefined( styleData.font ) || _.isUndefined( styleData.font.color ) ){
				return;
			}

			styleData.font.color = ips.utils.color.convertHex( styleData.font.color, h, s );

			this.trigger( 'styleUpdated', {
				selector: styleData.selector
			});
		}		
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/vse" javascript_name="ips.vse.main.js" javascript_type="controller" javascript_version="107643" javascript_position="1000600"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.vse.main.js - Main VSE controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.vse.main', {

		_mainFrame: null,
		_mainWindow: null,
		_frameReady: false,
		_data: {},
		_originalData: {},
		_xrayOn: false,
		_customCSSOpen: false,
		_url: '',
		_unsaved: false,
		_codeMirror: null,
		_vseData: null,

		/**
		 * Initialization: set up our events
		 *
		 * @returns 	{void}
		 */
		initialize: function () {
			// Interface buttons
			this.on( 'click', '#vseStartXRay', this.toggleXRay );
			this.on( 'click', '#vseColorize', this.startColorizer );
			this.on( 'click', '#vseAddCustomCSS', this.toggleCustomCSS );
			this.on( 'click', '[data-action="buildSkin"]', this.buildSkin );
			this.on( 'click', '[data-action="cancelSkin"]', this.cancelSkin );
			// Class list
			this.on( 'click', '#vseClassList [data-styleID]', this.selectClass );
			this.on( 'click', '[data-action="back"]', this.editorBack );
			this.on( 'change.spectrum move.spectrum', this.colorChange );
			// Messages coming from panels
			this.on( 'colorized', this.styleColorized );
			this.on( 'styleUpdated', this.styleUpdated );
			this.on( 'revertChanges', this.revertChanges );
			this.on( 'invertColors', this.invertColors );
			this.on( 'click', '[data-action="colorizerBack"]', this.colorizerBack );
			this.on( 'closeColorizer', this.colorizerBack );
			this.on( 'change', '#ipsTabs_vseSection_vseSettingsTab_panel :input', this.settingChanged );
			// Window events
			this.on( window, 'message', this.handleCommand );
			this.on( window, 'beforeunload', this.windowBeforeUnload );

			this.setup();
		},
		
		/**
		 * Setup
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			var self = this;

			this._mainFrame = $('#vseMainWrapper');
			this._mainWindow = this._mainFrame.find('iframe').get(0).contentWindow;

			if( !ipsVSEData || !_.isObject( ipsVSEData ) || !colorizer || !_.isObject( colorizer ) ){
				Debug.error("VSE JSON data not found, cannot continue.");
				return;
			}
			
			//this._originalData = $.extend( true, {}, ipsVSEData );
			//this._data = this.getVSEData();

			// Build URL
			var url = ips.utils.url.getURIObject( window.location.href );
			this._url = url.protocol + '://' + url.host;
			if ( url.port && url.port != 80 ) {
				this._url = this._url + ':' + url.port;
			}

			// Set up codemirrior
			this._codeMirror = CodeMirror.fromTextArea( document.getElementById('vseCustomCSS_editor'), { 
				mode: 'css',
				lineWrapping: true,
				lineNumbers: true
			});

			this._codeMirror.setSize( null, 235 );
			this._codeMirror.on( 'change', function (doc, cm) {
				self._updateCustomCSS();
			});

			$('#vseCustomCSS').hide();

			this._buildClassList();
		},

		/**
		 * Event handler for changing a color in a Spectrum color field
		 *
		 * @param 		{event} 	e 		Event
		 * @param 		{object} 	color 	The tinycolor object passed by spectrum
		 * @returns 	{void}
		 */
		colorChange: function (e, color) {
			this._updateCssVar( $( e.target ).attr('data-key'), color );
		},

		/**
		 * Invert every color swatch. Fairly naive implementation, simply flips the luminence value
		 *
		 * @param 		{event} 	e
		 * @returns 	{void}
		 */
		invertColors: function (e) {
			e.preventDefault();

			var swatches = this.scope.find('#vseClassList input.vseClass_swatch');
			var self = this;

			_.each( swatches, function (swatch) {
				var $swatch = $( swatch );
				var setting = $swatch.attr('data-key');
				var color = $swatch.spectrum('get').toHsl();

				color.l = 1 - color.l;

				var updatedColor = $swatch.spectrum('set', color).spectrum('get');

				self._updateCssVar( setting, updatedColor );
			});
		},

		/**
		 * Send a command to the main window to update a particular CSS variable value
		 *
		 * @param 		{string} 	key 	The key to be updated
		 * @param 		{object} 	color 	tinycolor color object to be applied
		 * @returns 	{void}
		 */
		_updateCssVar: function (key, color) {
			if( _.isUndefined( color ) ){
				return;
			}

			this.sendCommand('updateVar', {
				var: key,
				color: color.toRgb()
			});

			this._unsaved = true;
		},

		/**
		 * Merge resume data with vseData
		 *
		 * @return {object}
		 */
		_getVSEData: function()	{
			if ( _.isObject( this._vseData ) ){
				return this._vseData;
			}
			
			this._vseData = ipsVSEData;			
			this._vseData.colors = ipsResumeVse.colors;
			
			return this._vseData;
		},
		
		/**
		 * Builds the class list, from which the user can choose which class to edit
		 *
		 * @returns 	{void}
		 */
		_buildClassList: function () {
			var self = this;
			var output = '';

			_.each( this._getVSEData().sections, function (value, key) {
				output += ips.templates.render('vse.classes.title', {
					title: ips.getString( 'vseSection_' + key ),
					key: key
				});

				if( _.isObject( value ) && _.size( value ) ){
					_.each( value, function (item, itemKey) {
						output += ips.templates.render('vse.classes.item', {
							title: item.title,
							styleid: key + '_' + itemKey,
							swatch: self._buildSwatch( item, true )
						});
					});
				}
			});

			this.scope.find('#vseClassList > ul').html( output );

			
			// Build each color picker
			ips.loader.get( ['core/interface/spectrum/spectrum.js'] ).then( function () {
				
				var inputs = self.scope.find('#vseClassList input.vseClass_swatch');
				
				inputs.each( function () {
					var options = {
						type: "color",
						clickoutFiresChange: true,
						hideAfterPaletteSelect: true,
						preferredFormat: "hex",
						showAlpha: false,
						allowEmpty: false,
						showInput: true,
						showInitial: true,
						replacerClassName: this.className
					};
					
					$( this ).spectrum( options );	
				})
			} );
		},
		
		/**
		 * Builds style properties to be used on the swatch in the class list
		 *
		 * @param		{object} 	data 		Key/value pairs of styles to be changed
		 * @param 		{boolean} 	toString 	If true, object is turned into string to use in style=''
		 * @returns 	{object}	Object containing `back` and `fore` keys
		 */
		_buildSwatch: function (data, toString) {
			
			var toReturn = { back: false, fore: false };
			
			if( !_.isUndefined( data.settings.background ) && !_.isUndefined( this._vseData.colors[ data.settings.background ] ) ){
				toReturn.back = {
					color: "rgb(" + this._vseData.colors[ data.settings.background ] + ")",
					key: data.settings.background
				};
			}

			if( !_.isUndefined( data.settings.foreground ) && !_.isUndefined( this._vseData.colors[ data.settings.foreground ] ) ){
				toReturn.fore = {
					color: "rgb(" + this._vseData.colors[ data.settings.foreground ] + ")",
					key: data.settings.foreground
				};
			}

			return toReturn;
		},

		/**
		* Colorize some settings
		*
		* @param 	{event} 	e 		Event
		* @param 	{object} 	data 	data object, containing `type` (which colorizer group) and `color` (tinycolor object with the source color)
		* @returns 	{void}
		*/
		styleColorized: function (e, data) {
			var hue = data.color.h;
			var type = data.type;
			var settings = colorizer[ type ];
			var self = this;

			var sat = data.color.s * 100;
			var lig = data.color.l * 100;

			_.each( settings, function (setting) {				
				var control = self.scope.find('[data-key="' + setting + '"]');

				if( !control.length ){
					return;
				}
				
				var currentColor = tinycolor( 'rgb(' + self._vseData.colors[ setting ] + ')' ).toHsl();
	
				currentColor.h = data.color.h;
				currentColor.s = Math.max( Math.min( ( sat / 100 ) * currentColor.s, 1 ), 0 );

				var currentLig = currentColor.l * 100;
				var newLig = data.color.l * 100;

				// Difference in lightness
				var lightnessDiff = currentLig - newLig;

				// percentage of difference to apply
				var remainder = 100 - currentLig;
				var percentDiff = ( lightnessDiff / 100 ) * remainder;

				// Returned to a fraction
				var fraction = percentDiff / 100;

				// Applied to L
				currentColor.l = currentColor.l - fraction;

				var updatedColor = control.spectrum('set', currentColor).spectrum('get');

				self._updateCssVar( setting, updatedColor );
			});
	   	},

		
		/**
		 * Sends the provided command to the frame window
		 *
		 * @param 		{string} 	command 		The command to send
		 * @param 		{object} 	data			Data to send
		 * @returns 	{void}
		 */
		sendCommand: function (command, data) {
			this._mainWindow.postMessage( _.extend( data || {}, { command: command } ), this._url );
		},
		
		/**
		* Handles a command from the frame window, running one of our own methods if it exists
		*
		* @param 		{event} 	e 		Event object
		* @returns 	{void}
		*/
	   	handleCommand: function (e) {
		   if( e.originalEvent.origin != this._url ){
			   Debug.error("Invalid origin");
			   return;
		   }

		   var commandName = 'command' + e.originalEvent.data.command.charAt(0).toUpperCase() + e.originalEvent.data.command.slice(1);

		   if( !_.isUndefined( this[ commandName ] ) ){
			   this[ commandName ]( e.originalEvent.data );
		   }
	   	},

		/**
		 * Builds/shows the colorizer panel
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		startColorizer: function (e) {
			e.preventDefault();

			// Disable the two buttons
			$('#vseColorize, #vseStartXRay').attr('disabled', true);

			this.scope.find('#vseClassWrap').hide();
			ips.utils.anim.go('fadeIn', this.scope.find('#vseColorizerPanel') );
		},

		/**
		 * Event handler for back button on the colorizer panel
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		colorizerBack: function (e) {
			e.preventDefault();

			// Disable the two buttons
			$('#vseColorize, #vseStartXRay').attr( 'disabled', false );

			this.scope.find('#vseColorizerPanel').hide();
			ips.utils.anim.go('fadeIn', this.scope.find('#vseClassWrap') );
		},

		/**
		 * Event handler for the 'show custom css' pane
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		toggleCustomCSS: function (e) {
			e.preventDefault();

			if( this._customCSSOpen ){
				$( e.currentTarget ).removeClass('ipsButton_normal').addClass('ipsButton_primary');
				this._customCSSOpen = false;
				$('#vseCustomCSS').hide();
				$('#vseMainWrapper').css({ bottom: '0px' });
			} else {
				$( e.currentTarget ).removeClass('ipsButton_primary').addClass('ipsButton_normal');
				this._customCSSOpen = true;
				ips.utils.anim.go( 'fadeIn', $('#vseCustomCSS') );
				$('#vseMainWrapper').css({ bottom: '300px' });
			}
		},

		/**
		 * Event handler for the 'select element' button. Toggles xray functionality.
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		toggleXRay: function (e) {
			e.preventDefault();

			if( this._xrayOn ){
				$( e.currentTarget ).removeClass('ipsButton_normal').addClass('ipsButton_primary');
				this._xrayOn = false;
				this.stopXRay();
			} else {
				$( e.currentTarget ).removeClass('ipsButton_primary').addClass('ipsButton_normal');
				this._xrayOn = true;
				this.startXRay();
			}
		},

		/**
		 * Sends command to frame to start XRay
		 *
		 * @returns 	{void}
		 */
		startXRay: function () {
			this.sendCommand('xrayStart');
		},

		/**
		 * Sends command to frame to stop XRay
		 *
		 * @returns 	{void}
		 */
		stopXRay: function () {
			this.sendCommand('xrayCancel');

			// Remove results if any
			this.scope
				.find('#vseClassList > ul')
					.find('> li[data-role="xrayResultsTitle"]')
						.remove()
					.end()
					.find('> li')
						.show();
		},

		/**
		 * Tells the window to update the custom CSS contents
		 *
		 * @returns 	{void}
		 */
		_updateCustomCSS: function () {
			this._unsaved = true;
			this._codeMirror.save();
			
			this.sendCommand('updateCustomCSS', {
				css: $('#vseCustomCSS_editor').val()
			});
		},

		/**
		 * Responds to a command from the frame indicating that it is ready
		 *
		 * @returns 	{void}
		 */
		commandWindowReady: function () {
			this._frameReady = true;
            ips.loader.get(["core/interface/spectrum/spectrum.js"]).then( () => {
			    this.sendCommand("varValues", { values: this._getVarValues() });
            });
		},

		/**
		 * Responds to a command from the frame, sending a complete stylesheet object in response
		 *
		 * @returns 	{void}
		 */
		commandGetStylesheet: function () {
			this.sendCommand('createStylesheet', {
				stylesheet: this.scope.find('#vseCustomCSS_editor').val()
			});
		},

		/**
		 * When the xray has determined which css variables apply to the selected element, this
		 * method shows all the relevant rows and hides the others.
		 *
		 * @param 		{object} 	data 	Data from the main window, containing `vars` which is an array of settings that apply to the element
		 * @returns 	{void}
		 */
		commandVarsMatched: function (data) {
			this.scope.find('#vseClassList > ul > li').hide();

			// Find each of the rows that contains our settings
			var selector = _.map( data.vars, function (thisVar) {
				return "[data-key='" + thisVar + "']";
			}).join(',');

			this.scope.find('#vseClassList > ul').find( selector ).closest('li').show();
		},

		/**
		 * Returns an object containing every one of the current color values in rgb format
		 *
		 * @returns 	{array}  Array containing objects, each with `var` (variable name) and `color` (object with r, g, b keys)
		 */
		_getVarValues: function () {
			var swatches = this.scope.find('#vseClassList input.vseClass_swatch');
			var output = [];

			_.each( swatches, function (swatch) {
				output.push({
					var: $( swatch ).attr('data-key'),
					color: $( swatch ).spectrum('get').toRgb()
				});
			});

			return output;
		},

		/**
		 * Event handler for the window unloading.
		 * If we have unsaved changes, warn the user before leaving
		 *
		 * @returns 	{void}
		 */
		windowBeforeUnload: function (e) {
			if( this._unsaved ){
				return "You haven't saved this theme. By leaving this page, you will lose any changes you've made.";
			}
		},

		/**
		 * Builds this skin by sending data to the backend
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		buildSkin: function (e) {
			var self = this;
			
			if( !this._unsaved ){
				ips.ui.alert.show( {
					type: 'alert',
					icon: 'info',
					message: ips.getString('vseNoChanges'),
					callbacks: {}
				});

				return;
			}

			ips.getAjax()( ipsSettings['baseURL'] + '?app=core&module=system&controller=vse&do=build' + '&csrfKey=' + ipsSettings['csrfKey'], {
				type: 'post',
				data: this._buildFinalStyleData(),
				showLoading: true
			})
				.done( function (response) {
					self._unsaved = false;

					ips.ui.alert.show( {
						type: 'verify',
						icon: 'success',
						message: ips.getString('vseSkinBuilt'),
						buttons: { yes: ips.getString('yes'), no: ips.getString('no') },
						callbacks: {
							yes: function () {
								self._closeEditor( ipsSettings['baseURL'] + '?app=core&module=system&controller=vse&do=home&id=' + response.theme_set_id + '&csrfKey=' + ipsSettings['csrfKey'] );
							}
						}
					});
				})
				.fail( function (jqXHR, textStatus, errorThrown) {
					
					Debug.log("Error saving theme:");
					Debug.error( textStatus );

					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.getString('vseSkinBuilt_error'),
						callbacks: {}
					});
				});
		},

		/**
		 * Event handler for 'close editor' button
		 *
		 * @returns 	{void}
		 */
		cancelSkin: function (e) {
			e.preventDefault();

			var self = this;

			if( this._unsaved ){
				ips.ui.alert.show( {
					type: 'confirm',
					icon: 'question',
					message: ips.getString('vseUnsaved'),
					callbacks: {
						ok: function () {
							self._closeEditor( $( e.currentTarget ).attr('href') );
						}
					}
				});
			} else {
				this._closeEditor( $( e.currentTarget ).attr('href') );
			}
		},
		
		/**
		 * A value in the settings tab has been changed
		 *
		 * @returns 	{void}
		 */
		settingChanged: function () {
			this._unsaved = true;
		},
		
		/**
		 * Reverts all changes, changing colors back to their original state when the page was loaded
		 *
		 * @returns 	{void}
		 */
		revertChanges: function () {
			var self = this;
			var swatches = this.scope.find('#vseClassList input.vseClass_swatch');

			_.each( swatches, function (swatch) {
				var $swatch = $( swatch );
				var setting = $swatch.attr('data-key');
				var originalColor = self._vseData.colors[ setting ];
				var updatedColor = $swatch.spectrum('set', 'rgb(' + originalColor + ')' ).spectrum('get');

				self._updateCssVar( setting, updatedColor );
			});

			this._unsaved = false;
		},

		/**
		 * Takes our data object and turns it into a stylesheet and an object representing settings to be updated
		 *
		 * @returns 	{object}
		 */
		_buildFinalStyleData: function () {
			var self = this;
			var settingsObj = {};
			var stylesObj = {};
			var styleBlock = '';
			var orig = this._originalData;

			// Get every swatch and build a settings object
			var swatches = this.scope.find('#vseClassList input.vseClass_swatch');
			var colors = {};

			_.each( swatches, function (swatch) {
				var $swatch = $( swatch );
				colors[ $swatch.attr('data-key') ] = '#' + $swatch.spectrum('get').toHex();
			});

			// Get other form fields
			this.scope.find('#ipsTabs_vseSection_vseSettingsTab_panel :input[name]').each( function () {
				// As we do not use the normal form methods to get the data, we need to tweak
				if ( $( this ).attr('name').match( /_checkbox$/ ) ) {
					settingsObj[ $( this ).attr('name').replace( /_checkbox$/, '' ) ] = $( this ).is(':checked') ? 1 : 0;
				} else if ( $( this ).attr('type') == 'radio' ) {
					settingsObj[ $( this ).attr('name') ] = $('#ipsTabs_vseSection_vseSettingsTab_panel input[name=' + $( this ).attr('name') + ']:checked' ).val();
				} else {
					settingsObj[ $( this ).attr('name') ] = $( this ).val();
				}
			});
			
			return {
				customcss: $('#vseCustomCSS_editor').val(),
				colors: colors,
				settings: settingsObj
			};
		},
		
		/**
		 * Close the editor by calling the close method, then redirecting.
		 *
		 * @returns 	{void}
		 */
		_closeEditor: function ( url ) {
			ips.getAjax()( ipsSettings['baseURL'] + '?app=core&module=system&controller=vse&do=close', {
				showLoading: true
			})
				.always( function () {
					window.location = url || ips.getSetting('baseURL');
				});
		},
	});

}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/vse" javascript_name="ips.vse.window.js" javascript_type="controller" javascript_version="107643" javascript_position="1000600"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.vse.observer.js - Observing VSE controller. Initialized globally and listens for messages from the VSE interface
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.vse.window', {

		_stylesheet: null,
		_variables: [],
		_events: {},
		_exclude: null,
		_xrayElem: null,
		_url: '',

		initialize: function () {
			this.on( window, 'message', this.handleCommand );
			this.setup();
		},

		/**
		 * Setup method - adds our injected stylesheet for later, and sends commands to let the main window know we're ready
		 *
		 * @returns 	{void}
		 */
		setup: function () {
			// Build the initial stylesheet elements
			$('head')
				.append( $('<style/>').attr('type', 'text/css').attr('id', 'elInjectedStyles') )
				.append( $('<style/>').attr('type', 'text/css').attr('id', 'elCustomCSS') );

			this._stylesheet = $('#elInjectedStyles');
			this._custom = $('#elCustomCSS');
			this._root = document.documentElement;

			// Build URL
			var url = ips.utils.url.getURIObject( window.location.href );
			this._url = url.protocol + '://' + url.host;
			if ( url.port && url.port != 80 ) {
				this._url = this._url + ':' + url.port;
			}

			this.sendCommand('windowReady');
			this.sendCommand('getStylesheet'); // Ask the window for the initial stylesheet
		},

		/**
		 * Handles commands from the main window, executing the appropriate method here.
		 *
		 * @param		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		handleCommand: function (e) {
			if( e.originalEvent.origin != this._url ){
				Debug.error("Invalid origin");
				return;
			}

			var pieces = e.originalEvent.data.command.split('.');
			var obj = this;

			for( var i = 0; i < pieces.length - 1; i++ ){
				if( !_.isUndefined( obj[ pieces[ i ] ] ) ){
					obj = obj[ pieces[ i ] ];
				} else {
					Debug.error( "Couldn't run vse.window." + e.originalEvent.data.command );
					return;
				}
			}

			if( obj[ pieces[ pieces.length - 1 ] ] ){
				obj[ pieces[ pieces.length - 1 ] ]( e.originalEvent.data );
			} else {
				Debug.error( "Couldn't run vse.window." + e.originalEvent.data.command );
				return;
			}
		},

		/**
		 * Sends a command to the main window
		 *
		 * @param		{string} 	command 	Command name
		 * @param 		{object} 	data 		Data object
		 * @returns 	{void}
		 */
		sendCommand: function (command, data) {
			top.postMessage( _.extend( data || {}, { command: command } ), this._url );
		},
		
		/**
		 * Command from the main controller letting us know a variable has been updated
		 *
		 * @param 		{object} 	data 		Data object
		 * @returns 	{void}
		 */
		updateVar: function (data) {
			this._root.style.setProperty('--theme-' + data.var, data.color.r + ", " + data.color.g + ", " + data.color.b);
		},

		varValues: function (data) {
			var self = this;

			_.each( data.values, function (value) {
				self.updateVar(value);
			});
		},

		/**
		 * Updates the freeform style tag with custom css
		 *
		 * @param 		{object} 	data 		Data object
		 * @returns 	{void}
		 */
		updateCustomCSS: function (data) {
			this._custom.html( data.css );
		},
		

		/**
		 * The main window has sent us selector data, which the xRay will use to find elements
		 *
		 * @param 		{object} 	data 		Data object
		 * @returns 	{void}
		 */
		selectorData: function (data) {
			//this._selectors = data.selectors;
			this._variables = data.variables;
		},

		/**
		 * Replaces the injected stylesheet contents with new content, built from the data object
		 *
		 * <code>
		 * {
		 *		classes: {
		 *			'body': {
		 *				'background-color': 'red'
		 *			},
		 *			'.ipsButton': {
		 *				'color': 'blue'
		 *			}
		 *		}
		 * 	}
		 * </code>
		 * @param		{object} 	data 	Object of style data
		 * @returns 	{void}
		 */
		createStylesheet: function (data) {
			this._custom.html( data.stylesheet || '' );
		},			

		/**
		 * Starts the xray
		 *
		 * @returns 	{void}
		 */
		xrayStart: function () {
			this._createXRayElem();

			this._events['down'] = $( document ).on( 'mousedown.xray', _.bind( this._doDown, this ) );
			this._events['move'] = $( document ).on( 'mousemove.xray', _.bind( this._doFalse, this ) );
			this._events['over'] = $( document ).on( 'mouseover.xray', _.bind( this._doOver, this ) );
		},

		/**
		 * Cancels the xray
		 *
		 * @returns 	{void}
		 */
		xrayCancel: function () {
			if( this._xrayElem && this._xrayElem.length ){
				this._xrayElem.remove();
			}

			this._stop();
		},

		/**
		 * Stops the xray
		 *
		 * @returns 	{void}
		 */
		_stop: function () {
			$( document ).off('.xray');
		},

		/**
		 * Initializes the xray function by resizing/positioning the xray element to fit the currently-hovered element
		 *
		 * @param		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_doXray: function (e) {
			e.preventDefault();

			var elem = $( e.target );

			if( elem.is( this._exclude ) ){
				return;
			}

			var elemPosition = ips.utils.position.getElemPosition( elem );
			var elemDims = { width: elem.outerWidth(), height: elem.outerHeight() };

			this._xrayElem.css({
				width: elemDims.width + 'px',
				height: elemDims.height + 'px',
				left: elemPosition.absPos.left + 'px',
				top: elemPosition.absPos.top + 'px',
				zIndex: ips.ui.zIndex()
			});
		},

		/**
		 * Event handler for mousedown
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_doDown: function (e) {
			e.preventDefault();
			this._doXray(e);
			this._stop();

			this._findMatchingSelectors( $( e.target ) );
		},

		/**
		 * Event handler for mouseover
		 * Highlight the hovered element
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_doOver: function (e) {
			this._doXray(e);
		},

		/**
		 * Called to prevent xray events from bubbling
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_doFalse: function (e) {
			e.preventDefault();
		},

		/**
		 * Creates the xray element and attaches it to body ready for this._doXray
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		_createXRayElem: function () {
			if( $('#vseXRay').length ){
				$('#vseXRay').remove();
			}

			$('body').append( $('<div/>').attr( 'id', 'vseXRay' ) );

			this._xrayElem = $('#vseXRay');
		},

		/**
		 * Finds the ancestors of the clicked elemented, and matches selectors from our list
		 * Sends command to the main controller with our results
		 *
		 * @param 		{element} 	elem 		Clicked element
		 * @returns 	{void}
		 */
		_findMatchingSelectors: function (elem) {
			
			var self = this;
			var toReturn = [];
			
			var loopSheets = function (thisElem) {
				var rules = self._cssRules( thisElem );
				// If we got rules, loop through them and see if we find any vars
				if( rules.length ){
					for( var i = 0; i < rules.length; i++ ){
						var matches = rules[ i ].match(/\-\-theme\-(\w+)/i);

						if( matches && matches.length ){
							toReturn.push( matches[1] ); 
						}
					}
				}

				if( thisElem.parentNode ){
					loopSheets( thisElem.parentNode );
				}

				return;
			};

			loopSheets( elem.get(0) );

			this.sendCommand( 'varsMatched', {
				vars: toReturn
			});
		},
		
		/**
		 * Fetches all the css rules applying to the given element (but not inherited rules)
		 *
		 * @param 		{element} 	el 		Element
		 * @returns 	{array}
		 */
		_cssRules(el) {
			var sheets = document.styleSheets, ret = [];

			// Skip this check for the root document
			if( el === document ){
				return ret;
			}

			el.matches = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector 
				|| el.msMatchesSelector || el.oMatchesSelector;

			for (var i in sheets) {
				try {
					var rules = sheets[i].rules || sheets[i].cssRules;
					for (var r in rules) {						
						// Safari throws an error if it sees a ::-webkit pseudo selector,
						// so make sure this rule doesn't contain one.
						var rule = this._removeVendorPrefixSelectors(rules[r].selectorText);

						if (el.matches(rule)) {
							console.log(`selector text: ${rules[r].selectorText}`)
							ret.push(rules[r].cssText);
						}
					}
				} catch (err) {
					// Silently ignore errors here.
					// Errors can be thrown if external stylesheets are checked (no permission to read rules)
					// or other edge cases.
				}
			}
			return ret;
		},

		/**
		 * Ignore rules that have vendor-prefixed pseudo selectors.
		 * @see https://stackoverflow.com/questions/45723466/vendor-prefix-pseudo-selector-crashes-element-matches-web-api-in-safari
		 *
		 * @param 		{element} 	el 		Element
		 * @returns 	{array}
		 */
		_removeVendorPrefixSelectors (selectorText) {
			if (/::-/.test(selectorText)) {
				//do nothing
			} else {
				return selectorText;
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/widgets" javascript_name="ips.widgets.area.js" javascript_type="controller" javascript_version="107643" javascript_position="1000650"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.widgets.area.js - Widget area controller
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.widgets.area', {

		_areaID: null,
		_orientation: '',
		_list: null,
		_managing: false,
		_wasUnused: true,
		_readyForDragging: false,

		initialize: function () {
			this.on( 'prepareForDragging.widgets', this.prepareForDragging );
			this.on( 'managingStarted.widgets', this.managingStarted );
			this.on( 'managingFinished.widgets', this.managingFinished );
			this.on( 'loadedWidget.widgets', this.widgetLoaded );
			this.on( 'removeWidget.widgets', this.widgetRemoved );
			this.setup();
		},

		/**
		 * Setup method
		 *
		 * @returns {void}
		 */
		setup: function () {
			this._areaID = this.scope.attr('data-widgetArea');
			this._orientation = this.scope.attr('data-orientation');
			this._list = this.scope.find('> ul');
			this._registerArea();

			// Determine whether we're showing any blocks to start with
			if( this.scope.find('[data-blockID]').length ){
				this._wasUnused = false;
			}
		},

		/**
		 * Called when we're managing widgets
		 *
		 * @returns {void}
		 */
		managingStarted: function () {
			this._managing = true;
			this.scope.addClass('cWidgetContainer_managing');
			this._setWidgetsToManaging( true );
		},

		/**
		 * Called when we're no longer managing widgets
		 *
		 * @returns {void}
		 */
		managingFinished: function () {
			this._managing = false;
			this.scope.removeClass('cWidgetContainer_managing');
			this._setWidgetsToManaging( false );

			if( this._readyForDragging ){
				this._list.sortable('destroy');	
			}			

			// See whether we have anything to display
			this.scope.toggleClass('ipsHide', !this.scope.find('[data-blockID]').length );
		},

		/**
		 * Prepares the area for drag and drop functionality
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		prepareForDragging: function (e, data) {
			var self = this;

			this._list.css({
				zIndex: ips.ui.zIndex(),
			});

			this._list.sortable({
				dragHandle: '.cSidebarBlock_managing',
				receive: _.bind( self.receiveWidget, self ),
				placeholder: 'cSidebarManager_placeholder',
				update: _.bind( self.updateOrdering, self ),
				connectWith: data.selector,
				scroll: true
			});

			this._readyForDragging = true;
		},

		/**
		 * Event handler for the sortable receiving a new widget
		 *
		 * @param 	{event} 	e 	Event object
		 * @param 	{object}	ui 	jQuery UI object
		 * @returns {void}
		 */
		receiveWidget: function (e, ui) {
			var blockID = ui.item.attr('data-blockID');
			var hasConfig = ui.item.attr('data-blockConfig');
			var title	  = ui.item.attr('data-blockTitle');
			var errormsg  = ui.item.attr('data-blockErrorMessage');

			// Do we already have a widget?
			if ( this.scope.find('li[data-blockID=' + blockID + ']').attr('data-hidden') ){
				this.scope.find('li[data-blockID=' + blockID + ']').show().removeAttr('data-hidden');
			} else {
				// Create a new widget
				this._buildNewWidget( blockID, this.scope.find('> ul > li').index( ui.item.get(0) ), hasConfig, title, errormsg );
			}
			
			// Cancel the move because we actually just want to hide it
			ui.sender.sortable('cancel');
			
			if ( ! ui.item.attr('data-allowReuse') )
			{
				ui.item.hide().attr('data-hidden', true);
			}
		},

		/**
		 * Updates the ordering of widgets in this area
		 *
		 * @returns {void}
		 */
		updateOrdering: function (without) {
			if( !this._readyForDragging ){
				Debug.log('trying...');
				setTimeout( _.bind( this.updateOrdering, this ), 500 );
				return;
			}

			var body = $('body');
			var order = this.scope.find('> ul').sortable('toArray', {
				attribute: 'data-blockID'
			});
			
			order = ( without ) ? _.without( _.uniq( order ), without ) : _.uniq( order );

			// Remove hidden blocks as these should not be stored
			var self = this;
			_.each( order, function( value, key )
			{
				if ( self.scope.find('li[data-blockID=' + value + ']').attr('data-hidden') == 'true' )
				{
					order = _.without( order, value );
				}
			} );
			
			ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=widgets&do=saveOrder', {
				method: 'POST',
				data: {
					order: order,
					pageApp: body.attr('data-pageApp'),
					pageModule: body.attr('data-pageModule'),
					pageController: body.attr('data-pageController'),
					area: this._areaID
				}
			})
				.fail( function () {
					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.getString('sidebarError'),
						callbacks: {}
					});
				});
		},

		/**
		 * A widget has finished loading itself - if we're in managing state, tell it to set itself so
		 *
		 * @param	{event} 	e 			The event
		 * @param	{object} 	data		Event data object
		 * @returns {void}
		 */
		widgetLoaded: function (e, data) {
			if( this._managing ) {
				$( e.target ).trigger('startManaging.widgets');
			}
		},

		/**
		 * A widget has been removed
		 *
		 * @param	{event} 	e 			The event
		 * @param	{object} 	data		Event data object
		 * @returns {void}
		 */
		widgetRemoved: function (e, data) {
			this.updateOrdering( data.blockID );
		},

		/**
		 * Builds and loads a new widget into the list
		 *
		 * @param	{string} 	blockID		Block ID to load
		 * @param 	{number} 	idx 		Index of the element we'll place the new widget before
		 * @param	{string}	title		Title of the new widget
		 * @param	{string}	errormsg
		 * @returns {void}
		 */
		_buildNewWidget: function (blockID, idx, hasConfig, title, errormsg) {
			
			/* Does this already have a unique ID? */
			var bits       = blockID.split('_');
			var newBlockID = blockID;
			
			if ( _.isUndefined( bits[3] ) )
			{
				newBlockID =  blockID + '_' + Math.random().toString(36).substr(2, 9);
			}
		
			var newWidget = $('<li/>')
				.attr('data-blockID', newBlockID)
				.attr('data-blockTitle', title )
				.addClass('ipsWidget ipsBox')
				.removeClass('ipsWidget_horizontal ipsWidget_vertical')
				.addClass('ipsWidget_' + this._orientation)
				.attr('data-controller', 'core.front.widgets.block')
				.attr('data-blockErrorMessage', errormsg);

			if( hasConfig ){
				newWidget.attr( 'data-blockConfig', "true" );
			}

			var before = $( this.scope.find('> ul > li:not( .cSidebarBlock_placeholder )').get( idx ) );

			if( !_.isUndefined( idx ) && before.length){
				before.before( newWidget );
			} else {
				this.scope.find('> ul').prepend( newWidget );
			}

			// Init new widget
			$( document ).trigger( 'contentChange', [ newWidget ] );

			// Instruct it to load itself
			newWidget.trigger('reloadContents.sidebar');
		},

		/**
		 * Triggers an event on all widgets, to instruct them to set turn on or off 'managing' state
		 *
		 * @param 	{boolean} 	status 	Managing status
		 * @returns {void}
		 */
		_setWidgetsToManaging: function (status) {
			this.triggerOn( 'core.front.widgets.block', ( status ) ? 'startManaging.widgets' : 'stopManaging.widgets' );
		},

		/**
		 * Fires an event that registers this area with the main manager controller
		 *
		 * @returns {void}
		 */
		_registerArea: function () {
			var usedBlocks = this.scope.find('[data-blockID]');
			var blockIDs = [];

			if( usedBlocks.length ){
				usedBlocks.each( function (idx, val) {
					var blockID = $( val ).attr('data-blockID');

					if( blockID ){
						blockIDs.push( blockID );			
					}
				});
			}

			this.trigger( 'registerArea.widgets', {
				areaID: this._areaID,
				areaElem: this.scope,
				ids: blockIDs
			});
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/widgets" javascript_name="ips.widgets.block.js" javascript_type="controller" javascript_version="107643" javascript_position="1000650"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.widgets.block.js - Widget block controller for handling individual widgets
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.widgets.block', {

		_orientation: '',
		_blockID: '',
		_modalOpen: false,

		initialize: function () {
			this.setup();

			this.on( 'startManaging.widgets', this.startManaging );
			this.on( 'stopManaging.widgets', this.stopManaging );
			this.on( 'reloadContents.sidebar', this.reloadContent );
			this.on( 'click', '[data-action="removeBlock"]', this.removeBlock );
			this.on( 'menuOpened', this.menuOpened );
			this.on( 'menuClosed', this.menuClosed );
			
			$( document ).on( 'submitDialog', _.bind( this.submitDialog, this ) );
			$( document ).on( 'markAllRead', _.bind( this.markAllRead, this ) );
		},

		setup: function () {
			this._blockID = this.scope.attr('data-blockID');
			this._orientation = this.scope.closest('[data-role="widgetReceiver"]').attr('data-orientation');
		},
		
		/**
		 * Triggered by the parent controller, we need to set this block to 'managing' status
		 *
		 * @returns {void}
		 */
		startManaging: function (e, data) {
			if( this.scope.hasClass('ipsWidgetHide') ){
				this.scope.removeClass( 'ipsHide' );
			}
			
			if ( ! this.scope.html() ){
				this.scope.html( ips.templates.render('core.sidebar.blockIsEmpty', {
					text: this.scope.attr('data-blockerrormessage'),
				}) );
			}
			
			if( this.scope.find('.ipsWidgetBlank').length ){
				this.scope.show();
			}
						
			if ( this.scope.attr('data-blockconfig') ) {
				this.scope.append( ips.templates.render('core.sidebar.blockManage', {
					id: this.scope.attr('data-blockID'),
					title: this.scope.attr('data-blockTitle')
				}) );
			} else {
				this.scope.append( ips.templates.render('core.sidebar.blockManageNoConfig', {
					id: this.scope.attr('data-blockID'),
					title: this.scope.attr('data-blockTitle')
				}) );
			}

			$( document ).trigger( 'contentChange', [ this.scope ] );
		},

		/**
		 * Triggered by the parent controller, we need to stop 'managing' this block
		 *
		 * @returns {void}
		 */
		stopManaging: function (e, data) {
			if( this.scope.hasClass('ipsWidgetHide') )
			{
				this.scope.addClass( 'ipsHide' );
			}
			
			if( this.scope.find('.ipsWidgetBlank').length )
			{
				this.scope.hide();
			}

			this.scope.find('.cSidebarBlock_managing').animationComplete( function () {
				this.remove();
			});
			
			/* Make sure any <style>s no longer affect the block until the next page reload */
			if ( this.scope.attr('data-blockBuilder') ) {
				var blockID = this.scope.attr('data-blockID');
				var regex = '\.' + blockID;
				$('style').each( function() {
					if ( $(this).text().match( regex ) ) {
						$(this).text( $(this).text().replace( regex, '\.old' + blockID ) );
					}
				} );
			}

			ips.utils.anim.go('fadeOut fast', this.scope.find('.cSidebarBlock_managing') );
		},

		/**
		 * Event handler for removing this block
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		removeBlock: function (e) {
			e.preventDefault();

			this.scope.animationComplete( function () {
				this.remove();
			});
			
			ips.utils.anim.go( 'zoomOut fast', this.scope );
			
			this.trigger('removeWidget.widgets', {
				blockID: this._blockID
			});
		},

		/**
		 * Event handler/method that reloads the entire contents of this widget
		 *
		 * @returns {void}
		 */
		reloadContent: function () {
			var self = this;

			this._setLoading( true );

			// Get content
			if( this._ajaxObj && this._ajaxObj.abort ){
				this._ajaxObj.abort();
			}
			
			var body = $('body');
			var area = this.scope.closest('[data-widgetArea]').attr('data-widgetArea');
			var url  = ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=widgets&do=getBlock&blockID=' + this._blockID + '&pageApp=' + body.attr('data-pageApp') + '&pageModule=' + body.attr('data-pageModule') + '&pageController=' + body.attr('data-pageController') + '&pageArea=' + area + '&orientation=' + this._orientation;

			this._ajaxObj = ips.getAjax()( url )
				.done( function (response) {
					self.scope.hide().html( response.html );

					self.resetResponsiveClasses( response.devices );

					ips.utils.anim.go('fadeIn', self.scope);

					self.trigger('loadedWidget.widgets', {
						blockID: self._blockID
					});
				})
				.fail( function () {
					self.scope.html('Error');
				})
				.always( function () {
					self._setLoading( false );
				});
		},

		/**
		 * Reset responsive CSS classes on outer object dependent upon list passed in
		 *
		 * @returns {void}
		 */
		 resetResponsiveClasses: function( deviceList ) {
		 	// Remove existing classes
		 	this.scope.removeClass( 'ipsResponsive_hidePhone' )
		 		.removeClass( 'ipsResponsive_hideDesktop' )
		 		.removeClass( 'ipsResponsive_hideTablet' );

		 	// Find the entries missing
		 	var missing = _.filter( [ "Phone", "Tablet", "Desktop" ], function( value ){
		 		return !( deviceList.indexOf( value ) !== -1 );
		 	} );

		 	// And then add the CSS classes to hide those
		 	self = this;
		 	_.each( missing, function( value ){
		 		self.scope.addClass( 'ipsResponsive_hide' + value );
		 	} )
		 },

		/**
		 * When the menu is opened, we need to load the form into it
		 *
		 * @returns {void}
		 */
		 menuOpened: function (e, data) {
		 	/* Don't override menus inside the block */
			if ( ! this.scope.closest('[data-widgetArea]').hasClass('cWidgetContainer_managing') ){
				return;
			}

			var body = $('body');
			var area = this.scope.closest('[data-widgetArea]').attr('data-widgetArea');
			var block = this._blockID;
			var self = this;
			var managerBlock = $('[data-role="availableBlocks"] [data-blockID="' + this._getBlockIDWithoutUniqueKey( this._blockID ) + '"]');
			var menuStyle = managerBlock.attr('data-menuStyle');
		
			if ( menuStyle == 'modal' )
			{
				var dialogRef = ips.ui.dialog.create({
					title: managerBlock.find('h4').html(),
					url: ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=widgets&do=getConfiguration&block=' + block + '&pageApp=' + body.attr('data-pageApp') + '&pageModule=' + body.attr('data-pageModule') + '&pageController=' + body.attr('data-pageController') + '&pageArea=' + area,
					forceReload: true,
					destructOnClose: true,
					remoteSubmit: true
				});
					
				dialogRef.show();
				this._modalOpen = block;
			}
			else
			{
				data.menu.html( $('<div/>').addClass('ipsLoading').css({ height: '100px' }) );
				
				setTimeout( function () {
					ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=widgets&do=getConfiguration', {
						data: {
							block: block,
							pageApp: body.attr('data-pageApp'),
							pageModule: body.attr('data-pageModule'),
							pageController: body.attr('data-pageController'),
							pageArea: area
						}
					} )
					.done( function (response) {
						data.menu
							.html( response )
							.find('form')
							.on( 'submit', _.bind( self._configurationForm, self, data.menu ) );

						$( document ).trigger('contentChange', [ data.menu ] );
					});
				}, 1000);
			}
		},

		/**
		 * Called when menu is closed. We need to empty the form element otherwise we'll get element ID conflicts.
		 *
		 * @param 	{event} 	e 		Event object
		 * @param 	{object} 	data 	Event data object
		 * @returns {void}
		 */
		menuClosed: function (e, data) {
			// Only clear if this is the edit menu
			if( data.menu && data.elemID.substring(data.elemID.length - 4) === 'edit' ){
				ips.controller.cleanContentsOf( data.menu );
				data.menu.html('');
			}
		},
		
		/**
		 * Triggered by a modal form save
		 *
		 */
		submitDialog: function( e, data )
		{
			/* Dialog event triggers in all blocks, is this the one we have open? */
			if ( this._modalOpen == this._blockID )
			{
				this._modalOpen = false;
				this.reloadContent();
			}
		},
		
		/**
		 * Marks lists within this block as read
		 *
		 * @returns {void}
		 */
		markAllRead: function () {
			// Update row
			this.scope
				.find('.ipsDataItem, .ipsDataItem_subList')
					.removeClass('ipsDataItem_unread')
					.find('.ipsItemStatus')
						.addClass('ipsItemStatus_read');
		},
		
		/**
		 * Submit handler for the configuration form
		 *
		 * @returns {void}
		 */
		_configurationForm: function (menu, e) {
			var self = this;
			e.preventDefault();

			ips.getAjax()( $( e.currentTarget ).attr('action'), {
				data: $( e.currentTarget ).serialize(),
				type: 'post'
			})
				.done( function (response) {
					if( response === 'OK' ){
						self.reloadContent();
						menu.trigger('closeMenu');
						menu.remove();
					} else {
						menu.html( response );
					}
				})
				.fail( function () {
					ips.ui.alert.show( {
						type: 'alert',
						icon: 'warn',
						message: ips.getString('sidebarConfigError'),
						callbacks: {}
					});
				});
		},

		/**
		 * Sets the loading status of this widget
		 *
		 * @returns {void}
		 */
		_setLoading: function (status) {
			if( status ){
				this.scope.html('').addClass('ipsLoading cSidebarBlock_loading');
			} else {
				this.scope.removeClass('ipsLoading cSidebarBlock_loading');
			}
		},

		/**
		 * Removes the unique key from the block ID
		 *
		 * @param	{string} 	block 			Block ID with unique key (app_core_whosOnline_4vbvzbw)
		 * @returns {string}
		 */
		_getBlockIDWithoutUniqueKey: function (block) {
			var bits = block.split('_');
			return bits[0] + '_' + bits[1] + '_'  + bits[2];
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/widgets" javascript_name="ips.widgets.manager.js" javascript_type="controller" javascript_version="107643" javascript_position="1000650"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.widgets.manager.js - Widget manager controller
 *
 * Author: Rikki Tissier
 */
	
;( function($, _, undefined){
	"use strict";
		
	ips.controller.register('core.front.widgets.manager', {

		_loadedManager: false,
		_loadingManager: false,
		_dragInitialized: false,
		_inManagingState: false,
		_wasUnused: false,
		_areasInUse: [],
		_blocksInUse: [],

		initialize: function () {
			this.on( 'click', '[data-action="openSidebar"]', this.openSidebarManager );
			this.on( 'click', '[data-action="closeSidebar"]', this.closeSidebarManager );

			this.on( 'removeWidget.widgets', this.widgetRemoved );
			this.on( 'registerArea.widgets', this.registerWidgetArea );

			this.setup();
		},

		/**
		 * Setup method: ensures the block controller is available before we add any blocks
		 *
		 * @returns {void}
		 */
		setup: function () {
			var self = this;

			ips.loader.get( ['core/front/controllers/widgets/ips.widgets.block.js' ] ).then( function () {
				// Should we automatically open?
				if( ips.utils.url.getParam('_blockManager') ){
					self.openSidebarManager();
				}
			});

			// If the sidebar was hidden before we started managing, remember that so we can properly hide it
			// again after
			if( $('body').hasClass('ipsLayout_sidebarUnused') ){
				this._wasUnused = true;
			}
		},

		/**
		 * A widget area has told us it exists
		 *
		 * @param	{event} 	e 			The event
		 * @param 	{object} 	data 		Event data
		 * @returns {void}
		 */
		registerWidgetArea: function (e, data) {
			var self = this;

			this._areasInUse.push( data.areaID );

			if( data.ids ){
				for( var i = 0; i < data.ids.length; i++ ){
					self._blocksInUse.push( data.ids[ i ] );
				};
			}
		},

		/**
		 * Opens the manager panel, building it if necessary
		 *
		 * @param	{event} 	e 			The event
		 * @returns {void}
		 */
		openSidebarManager: function (e) {
			if( e ){
				e.preventDefault();				
			}			

			if( this._inManagingState ){
				return;
			}

			if( !this.scope.find('[data-role="manager"]').length ){
				this._buildSidebar();
			} else {
				// reset css code
				this.scope.find('#elSidebarManager > div:first-child').css({
					overflow: '',
					position: '',
					top: ''
				});
			}

			this.triggerOn( 'core.front.widgets.area', 'managingStarted.widgets');
			this._showManager();
			this.scope.addClass('cWidgetsManaging');
		},

		/**
		 * Closes the manager panel
		 *
		 * @param	{event} 	e 			The event
		 * @returns {void}
		 */
		closeSidebarManager: function (e) {
			e.preventDefault();

			if( this._inManagingState ){
				this._hideManager();
				this._cancelDragging();
				this.triggerOn( 'core.front.widgets.area', 'managingFinished.widgets' );
				this.scope.removeClass('cWidgetsManaging');
			}
		},

		widgetRemoved: function (e, data) {
			ips.utils.anim.go( 'fadeIn', this.scope.find('[data-role="availableBlocks"]').find('[data-blockID="' + this._getBlockIDWithoutUniqueKey( data.blockID ) + '"]').removeAttr('data-hidden') );
		},
		
		/**
		 * Set up drags on the main list and manager lists
		 *
		 * @param	{event} 	e 			The event
		 * @returns {void}
		 */
		_setUpDragging: function () {
			var self = this;
			var managerList = this.scope.find('[data-role="availableBlocks"] ul');
			var selectors = self._buildAreaSelector();

			ips.loader.get( ['core/interface/jquery/jquery-ui.js'] ).then( function () {
				managerList.css({
					zIndex: ips.ui.zIndex(),
				})
				.sortable({
					dragHandle: '.cSidebarManager_block',
					connectWith: selectors,
					placeholder: 'cSidebarManager_placeholder',
					scroll: true,
					start: _.bind( self._startDragging, self ),
					stop: _.bind( self._cancelDragging, self )
				});

				self.triggerOn( 'core.front.widgets.area', 'prepareForDragging.widgets', {
					selector: selectors
				});
			});
		},

		/**
		 * When we start dragging, we need to set the overflow on the list to be 'visible' (rather
		 * than 'hidden') so that items in the list will be visible when they leave the element.
		 * At the same time, we have to adjust the top position of the list to imitate the previous scrolled
		 * position, then refresh the sortable so that the dragged element is in the right place. Phew.
		 *
		 * @returns {void}
		 */
		_startDragging: function () {
			var sidebarPanel = this.scope.find('#elSidebarManager > div:first-child');
			var scrollTop = sidebarPanel.scrollTop();

			sidebarPanel.css({
				overflow: 'visible',
				position: 'relative',
				top: '-' + scrollTop + 'px'
			});

			var managerList = this.scope.find('[data-role="availableBlocks"] ul');
			managerList.sortable('refresh');
		},

		/**
		 * Cancel the sorting, reversing the changes the above method makes.
		 *
		 * @returns {void}
		 */
		_cancelDragging: function () {
			this.scope.find('#elSidebarManager > div:first-child').css({
				overflow: '',
				position: '',
				top: ''
			});
		},

		/**
		 * Builds a selector list for selecting all widget areas
		 *
		 * @returns {void}
		 */
		_buildAreaSelector: function () {
			var output = [];

			for( var i = 0; i < this._areasInUse.length; i++ ){
				output.push( "[data-widgetArea='" + this._areasInUse[ i ] + "'] > ul" );
			}

			return output.join(',');
		},

		/**
		 * Shows the manager panel, loading the contents remotely if needed
		 *
		 * @returns {void}
		 */
		_showManager: function () {
			var self = this;

			this.scope.find('[data-action="openSidebar"]').hide();

			ips.utils.anim.go( 'fadeIn', this.scope.find('[data-role="manager"]') )
				.done( function () {
					// Fade in the submit button
					$('#elSidebarManager_submit').hide().delay(700).fadeIn('fast');
				});

			// Fetch the sidebar list
			if( !this._loadedManager && !this._loadingManager ){
				ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=widgets&do=getBlockList', {
					data: {
						pageApp: $('body').attr('data-pageApp')
					}
				} )
					.done( function (response) {
						self._loadedManager = true;
						self.scope.find('[data-role="availableBlocks"]').html( response );
						self._setUpDragging();
						self._hideUsedBlocks();

						// Content change again
						$( document ).trigger('contentChange', [ $('#elSidebarManager') ] );
					})
					.fail( function (jqXHR, textStatus, errorThrown) {
						ips.ui.alert.show( {
							type: 'alert',
							icon: 'warn',
							message: ips.getString('sidebar_fetch_blocks_error'),
							callbacks: {}
						});
					})
					.always( function () {
						self.scope.find('[data-role="availableBlocks"]').removeClass('ipsLoading ipsLoading_dark');
						self._loadingManager = false;
					});
			} else {
				// If we've loaded the data already, we still need to call these
				this._setUpDragging();
				this._hideUsedBlocks();
			}

			this._inManagingState = true;
		},

		/**
		 * Hides the manager panel
		 *
		 * @returns {void}
		 */
		_hideManager: function () {
			var self = this;

			this.scope.find('[data-action="openSidebar"]').show();
			this.scope.find('#elSidebarManager').hide();

			this._inManagingState = false;
		},

		/**
		 * Hides blocks in the available list which have been used in the live list
		 *
		 * @returns {void}
		 */
		_hideUsedBlocks: function () {
			var self = this;
			var manager = this.scope.find('[data-role="availableBlocks"]');

			// Show all blocks to start
			manager.find('[data-blockID]').show().removeAttr('data-hidden');

			if( this._blocksInUse.length ){
				for( var i = 0; i < this._blocksInUse.length; i++ ){
					var listedBlock = manager.find('[data-blockID="' + this._getBlockIDWithoutUniqueKey( self._blocksInUse[ i ] ) + '"]');
					
					if ( ! listedBlock.attr('data-allowReuse') )
					{
						listedBlock.hide().attr('data-hidden', true);
					}
				}
			}
		},

		/**
		 * Add the manager sidebar HTML to the page
		 *
		 * @param	{event} 	e 			The original event
		 * @returns {void}
		 */
		_buildSidebar: function () {
			this.scope.append( ips.templates.render('core.sidebar.managerWrapper') );
			this.scope.find('[data-role="availableBlocks"]').css({
				zIndex: ips.ui.zIndex()
			});

			$( document ).trigger( 'contentChange', [ this.scope ] );
		},

		/**
		 * Removes the unique key from the block ID
		 *
		 * @param	{string} 	block 			Block ID with unique key (app_core_whosOnline_4vbvzbw)
		 * @returns {string}
		 */
		_getBlockIDWithoutUniqueKey: function (block) {
			var bits = block.split('_');
			return bits[0] + '_' + bits[1] + '_'  + bits[2];
		}
	});
}(jQuery, _));
]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="controllers/widgets" javascript_name="ips.widgets.sidebar.js" javascript_type="controller" javascript_version="107643" javascript_position="1000650"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.widgets.sidebar.js - Special additional controller for the sidebar, to show/hide the whole sidebar as needed
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('core.front.widgets.sidebar', {

		initialize: function () {
			this.on( 'managingStarted.widgets', this.managingStarted );
			this.on( 'managingFinished.widgets', this.managingFinished );
			this.setup();
		},

		setup: function () {
			this._addBodyClass();
		},

		/**
		 * Called when we're managing widgets
		 * Shows the sidebar if it was hidden, and sets the height of the droppable area
		 *
		 * @returns {void}
		 */
		managingStarted: function () {
			var height = this.scope.height() + 'px';

			this.scope
				.removeClass('ipsLayout_sidebarUnused')
				.find('[data-role="widgetReceiver"], [data-role="widgetReceiver"] > ul')
					.css({
						minHeight: height
					});
		},

		/**
		 * Called when we've finished managing widgets
		 * Hides the sidebar completely if there's no widgets or contextual tools displaying
		 *
		 * @returns {void}
		 */
		managingFinished: function () {
			this._addBodyClass();

			this.scope
				.find('[data-role="widgetReceiver"], [data-role="widgetReceiver"] > ul')
					.css({
						height: 'auto'
					});
		},

		/**
		 * Adds a class to the body that indicates if the sidebar is shown
		 *
		 * @returns {void}
		 */
		_addBodyClass: function () {
			if( ! this.scope.find('[data-blockID]:visible').length && !$('#elContextualTools').length && !$('[data-role="sidebarAd"]').length && !$('#cAnnouncementSidebar').length ){
				$('body').addClass('ipsLayout_sidebarUnused').removeClass('ipsLayout_sidebarUsed');
			} else {
				$('body').addClass('ipsLayout_sidebarUsed').removeClass('ipsLayout_sidebarUnused');
			}
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="hammer" javascript_name="hammer.js" javascript_type="framework" javascript_version="107643" javascript_position="1000350"><![CDATA[/*! Hammer.JS - v2.0.8 - 2016-04-23
 * http://hammerjs.github.io/
 *
 * Copyright (c) 2016 Jorik Tangelder;
 * Licensed under the MIT license */
!function(a,b,c,d){"use strict";function e(a,b,c){return setTimeout(j(a,c),b)}function f(a,b,c){return Array.isArray(a)?(g(a,c[b],c),!0):!1}function g(a,b,c){var e;if(a)if(a.forEach)a.forEach(b,c);else if(a.length!==d)for(e=0;e<a.length;)b.call(c,a[e],e,a),e++;else for(e in a)a.hasOwnProperty(e)&&b.call(c,a[e],e,a)}function h(b,c,d){var e="DEPRECATED METHOD: "+c+"\n"+d+" AT \n";return function(){var c=new Error("get-stack-trace"),d=c&&c.stack?c.stack.replace(/^[^\(]+?[\n$]/gm,"").replace(/^\s+at\s+/gm,"").replace(/^Object.<anonymous>\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",f=a.console&&(a.console.warn||a.console.log);return f&&f.call(a.console,e,d),b.apply(this,arguments)}}function i(a,b,c){var d,e=b.prototype;d=a.prototype=Object.create(e),d.constructor=a,d._super=e,c&&la(d,c)}function j(a,b){return function(){return a.apply(b,arguments)}}function k(a,b){return typeof a==oa?a.apply(b?b[0]||d:d,b):a}function l(a,b){return a===d?b:a}function m(a,b,c){g(q(b),function(b){a.addEventListener(b,c,!1)})}function n(a,b,c){g(q(b),function(b){a.removeEventListener(b,c,!1)})}function o(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1}function p(a,b){return a.indexOf(b)>-1}function q(a){return a.trim().split(/\s+/g)}function r(a,b,c){if(a.indexOf&&!c)return a.indexOf(b);for(var d=0;d<a.length;){if(c&&a[d][c]==b||!c&&a[d]===b)return d;d++}return-1}function s(a){return Array.prototype.slice.call(a,0)}function t(a,b,c){for(var d=[],e=[],f=0;f<a.length;){var g=b?a[f][b]:a[f];r(e,g)<0&&d.push(a[f]),e[f]=g,f++}return c&&(d=b?d.sort(function(a,c){return a[b]>c[b]}):d.sort()),d}function u(a,b){for(var c,e,f=b[0].toUpperCase()+b.slice(1),g=0;g<ma.length;){if(c=ma[g],e=c?c+f:b,e in a)return e;g++}return d}function v(){return ua++}function w(b){var c=b.ownerDocument||b;return c.defaultView||c.parentWindow||a}function x(a,b){var c=this;this.manager=a,this.callback=b,this.element=a.element,this.target=a.options.inputTarget,this.domHandler=function(b){k(a.options.enable,[a])&&c.handler(b)},this.init()}function y(a){var b,c=a.options.inputClass;return new(b=c?c:xa?M:ya?P:wa?R:L)(a,z)}function z(a,b,c){var d=c.pointers.length,e=c.changedPointers.length,f=b&Ea&&d-e===0,g=b&(Ga|Ha)&&d-e===0;c.isFirst=!!f,c.isFinal=!!g,f&&(a.session={}),c.eventType=b,A(a,c),a.emit("hammer.input",c),a.recognize(c),a.session.prevInput=c}function A(a,b){var c=a.session,d=b.pointers,e=d.length;c.firstInput||(c.firstInput=D(b)),e>1&&!c.firstMultiple?c.firstMultiple=D(b):1===e&&(c.firstMultiple=!1);var f=c.firstInput,g=c.firstMultiple,h=g?g.center:f.center,i=b.center=E(d);b.timeStamp=ra(),b.deltaTime=b.timeStamp-f.timeStamp,b.angle=I(h,i),b.distance=H(h,i),B(c,b),b.offsetDirection=G(b.deltaX,b.deltaY);var j=F(b.deltaTime,b.deltaX,b.deltaY);b.overallVelocityX=j.x,b.overallVelocityY=j.y,b.overallVelocity=qa(j.x)>qa(j.y)?j.x:j.y,b.scale=g?K(g.pointers,d):1,b.rotation=g?J(g.pointers,d):0,b.maxPointers=c.prevInput?b.pointers.length>c.prevInput.maxPointers?b.pointers.length:c.prevInput.maxPointers:b.pointers.length,C(c,b);var k=a.element;o(b.srcEvent.target,k)&&(k=b.srcEvent.target),b.target=k}function B(a,b){var c=b.center,d=a.offsetDelta||{},e=a.prevDelta||{},f=a.prevInput||{};b.eventType!==Ea&&f.eventType!==Ga||(e=a.prevDelta={x:f.deltaX||0,y:f.deltaY||0},d=a.offsetDelta={x:c.x,y:c.y}),b.deltaX=e.x+(c.x-d.x),b.deltaY=e.y+(c.y-d.y)}function C(a,b){var c,e,f,g,h=a.lastInterval||b,i=b.timeStamp-h.timeStamp;if(b.eventType!=Ha&&(i>Da||h.velocity===d)){var j=b.deltaX-h.deltaX,k=b.deltaY-h.deltaY,l=F(i,j,k);e=l.x,f=l.y,c=qa(l.x)>qa(l.y)?l.x:l.y,g=G(j,k),a.lastInterval=b}else c=h.velocity,e=h.velocityX,f=h.velocityY,g=h.direction;b.velocity=c,b.velocityX=e,b.velocityY=f,b.direction=g}function D(a){for(var b=[],c=0;c<a.pointers.length;)b[c]={clientX:pa(a.pointers[c].clientX),clientY:pa(a.pointers[c].clientY)},c++;return{timeStamp:ra(),pointers:b,center:E(b),deltaX:a.deltaX,deltaY:a.deltaY}}function E(a){var b=a.length;if(1===b)return{x:pa(a[0].clientX),y:pa(a[0].clientY)};for(var c=0,d=0,e=0;b>e;)c+=a[e].clientX,d+=a[e].clientY,e++;return{x:pa(c/b),y:pa(d/b)}}function F(a,b,c){return{x:b/a||0,y:c/a||0}}function G(a,b){return a===b?Ia:qa(a)>=qa(b)?0>a?Ja:Ka:0>b?La:Ma}function H(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return Math.sqrt(d*d+e*e)}function I(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return 180*Math.atan2(e,d)/Math.PI}function J(a,b){return I(b[1],b[0],Ra)+I(a[1],a[0],Ra)}function K(a,b){return H(b[0],b[1],Ra)/H(a[0],a[1],Ra)}function L(){this.evEl=Ta,this.evWin=Ua,this.pressed=!1,x.apply(this,arguments)}function M(){this.evEl=Xa,this.evWin=Ya,x.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}function N(){this.evTarget=$a,this.evWin=_a,this.started=!1,x.apply(this,arguments)}function O(a,b){var c=s(a.touches),d=s(a.changedTouches);return b&(Ga|Ha)&&(c=t(c.concat(d),"identifier",!0)),[c,d]}function P(){this.evTarget=bb,this.targetIds={},x.apply(this,arguments)}function Q(a,b){var c=s(a.touches),d=this.targetIds;if(b&(Ea|Fa)&&1===c.length)return d[c[0].identifier]=!0,[c,c];var e,f,g=s(a.changedTouches),h=[],i=this.target;if(f=c.filter(function(a){return o(a.target,i)}),b===Ea)for(e=0;e<f.length;)d[f[e].identifier]=!0,e++;for(e=0;e<g.length;)d[g[e].identifier]&&h.push(g[e]),b&(Ga|Ha)&&delete d[g[e].identifier],e++;return h.length?[t(f.concat(h),"identifier",!0),h]:void 0}function R(){x.apply(this,arguments);var a=j(this.handler,this);this.touch=new P(this.manager,a),this.mouse=new L(this.manager,a),this.primaryTouch=null,this.lastTouches=[]}function S(a,b){a&Ea?(this.primaryTouch=b.changedPointers[0].identifier,T.call(this,b)):a&(Ga|Ha)&&T.call(this,b)}function T(a){var b=a.changedPointers[0];if(b.identifier===this.primaryTouch){var c={x:b.clientX,y:b.clientY};this.lastTouches.push(c);var d=this.lastTouches,e=function(){var a=d.indexOf(c);a>-1&&d.splice(a,1)};setTimeout(e,cb)}}function U(a){for(var b=a.srcEvent.clientX,c=a.srcEvent.clientY,d=0;d<this.lastTouches.length;d++){var e=this.lastTouches[d],f=Math.abs(b-e.x),g=Math.abs(c-e.y);if(db>=f&&db>=g)return!0}return!1}function V(a,b){this.manager=a,this.set(b)}function W(a){if(p(a,jb))return jb;var b=p(a,kb),c=p(a,lb);return b&&c?jb:b||c?b?kb:lb:p(a,ib)?ib:hb}function X(){if(!fb)return!1;var b={},c=a.CSS&&a.CSS.supports;return["auto","manipulation","pan-y","pan-x","pan-x pan-y","none"].forEach(function(d){b[d]=c?a.CSS.supports("touch-action",d):!0}),b}function Y(a){this.options=la({},this.defaults,a||{}),this.id=v(),this.manager=null,this.options.enable=l(this.options.enable,!0),this.state=nb,this.simultaneous={},this.requireFail=[]}function Z(a){return a&sb?"cancel":a&qb?"end":a&pb?"move":a&ob?"start":""}function $(a){return a==Ma?"down":a==La?"up":a==Ja?"left":a==Ka?"right":""}function _(a,b){var c=b.manager;return c?c.get(a):a}function aa(){Y.apply(this,arguments)}function ba(){aa.apply(this,arguments),this.pX=null,this.pY=null}function ca(){aa.apply(this,arguments)}function da(){Y.apply(this,arguments),this._timer=null,this._input=null}function ea(){aa.apply(this,arguments)}function fa(){aa.apply(this,arguments)}function ga(){Y.apply(this,arguments),this.pTime=!1,this.pCenter=!1,this._timer=null,this._input=null,this.count=0}function ha(a,b){return b=b||{},b.recognizers=l(b.recognizers,ha.defaults.preset),new ia(a,b)}function ia(a,b){this.options=la({},ha.defaults,b||{}),this.options.inputTarget=this.options.inputTarget||a,this.handlers={},this.session={},this.recognizers=[],this.oldCssProps={},this.element=a,this.input=y(this),this.touchAction=new V(this,this.options.touchAction),ja(this,!0),g(this.options.recognizers,function(a){var b=this.add(new a[0](a[1]));a[2]&&b.recognizeWith(a[2]),a[3]&&b.requireFailure(a[3])},this)}function ja(a,b){var c=a.element;if(c.style){var d;g(a.options.cssProps,function(e,f){d=u(c.style,f),b?(a.oldCssProps[d]=c.style[d],c.style[d]=e):c.style[d]=a.oldCssProps[d]||""}),b||(a.oldCssProps={})}}function ka(a,c){var d=b.createEvent("Event");d.initEvent(a,!0,!0),d.gesture=c,c.target.dispatchEvent(d)}var la,ma=["","webkit","Moz","MS","ms","o"],na=b.createElement("div"),oa="function",pa=Math.round,qa=Math.abs,ra=Date.now;la="function"!=typeof Object.assign?function(a){if(a===d||null===a)throw new TypeError("Cannot convert undefined or null to object");for(var b=Object(a),c=1;c<arguments.length;c++){var e=arguments[c];if(e!==d&&null!==e)for(var f in e)e.hasOwnProperty(f)&&(b[f]=e[f])}return b}:Object.assign;var sa=h(function(a,b,c){for(var e=Object.keys(b),f=0;f<e.length;)(!c||c&&a[e[f]]===d)&&(a[e[f]]=b[e[f]]),f++;return a},"extend","Use `assign`."),ta=h(function(a,b){return sa(a,b,!0)},"merge","Use `assign`."),ua=1,va=/mobile|tablet|ip(ad|hone|od)|android/i,wa="ontouchstart"in a,xa=u(a,"PointerEvent")!==d,ya=wa&&va.test(navigator.userAgent),za="touch",Aa="pen",Ba="mouse",Ca="kinect",Da=25,Ea=1,Fa=2,Ga=4,Ha=8,Ia=1,Ja=2,Ka=4,La=8,Ma=16,Na=Ja|Ka,Oa=La|Ma,Pa=Na|Oa,Qa=["x","y"],Ra=["clientX","clientY"];x.prototype={handler:function(){},init:function(){this.evEl&&m(this.element,this.evEl,this.domHandler),this.evTarget&&m(this.target,this.evTarget,this.domHandler),this.evWin&&m(w(this.element),this.evWin,this.domHandler)},destroy:function(){this.evEl&&n(this.element,this.evEl,this.domHandler),this.evTarget&&n(this.target,this.evTarget,this.domHandler),this.evWin&&n(w(this.element),this.evWin,this.domHandler)}};var Sa={mousedown:Ea,mousemove:Fa,mouseup:Ga},Ta="mousedown",Ua="mousemove mouseup";i(L,x,{handler:function(a){var b=Sa[a.type];b&Ea&&0===a.button&&(this.pressed=!0),b&Fa&&1!==a.which&&(b=Ga),this.pressed&&(b&Ga&&(this.pressed=!1),this.callback(this.manager,b,{pointers:[a],changedPointers:[a],pointerType:Ba,srcEvent:a}))}});var Va={pointerdown:Ea,pointermove:Fa,pointerup:Ga,pointercancel:Ha,pointerout:Ha},Wa={2:za,3:Aa,4:Ba,5:Ca},Xa="pointerdown",Ya="pointermove pointerup pointercancel";a.MSPointerEvent&&!a.PointerEvent&&(Xa="MSPointerDown",Ya="MSPointerMove MSPointerUp MSPointerCancel"),i(M,x,{handler:function(a){var b=this.store,c=!1,d=a.type.toLowerCase().replace("ms",""),e=Va[d],f=Wa[a.pointerType]||a.pointerType,g=f==za,h=r(b,a.pointerId,"pointerId");e&Ea&&(0===a.button||g)?0>h&&(b.push(a),h=b.length-1):e&(Ga|Ha)&&(c=!0),0>h||(b[h]=a,this.callback(this.manager,e,{pointers:b,changedPointers:[a],pointerType:f,srcEvent:a}),c&&b.splice(h,1))}});var Za={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},$a="touchstart",_a="touchstart touchmove touchend touchcancel";i(N,x,{handler:function(a){var b=Za[a.type];if(b===Ea&&(this.started=!0),this.started){var c=O.call(this,a,b);b&(Ga|Ha)&&c[0].length-c[1].length===0&&(this.started=!1),this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}}});var ab={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},bb="touchstart touchmove touchend touchcancel";i(P,x,{handler:function(a){var b=ab[a.type],c=Q.call(this,a,b);c&&this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}});var cb=2500,db=25;i(R,x,{handler:function(a,b,c){var d=c.pointerType==za,e=c.pointerType==Ba;if(!(e&&c.sourceCapabilities&&c.sourceCapabilities.firesTouchEvents)){if(d)S.call(this,b,c);else if(e&&U.call(this,c))return;this.callback(a,b,c)}},destroy:function(){this.touch.destroy(),this.mouse.destroy()}});var eb=u(na.style,"touchAction"),fb=eb!==d,gb="compute",hb="auto",ib="manipulation",jb="none",kb="pan-x",lb="pan-y",mb=X();V.prototype={set:function(a){a==gb&&(a=this.compute()),fb&&this.manager.element.style&&mb[a]&&(this.manager.element.style[eb]=a),this.actions=a.toLowerCase().trim()},update:function(){this.set(this.manager.options.touchAction)},compute:function(){var a=[];return g(this.manager.recognizers,function(b){k(b.options.enable,[b])&&(a=a.concat(b.getTouchAction()))}),W(a.join(" "))},preventDefaults:function(a){var b=a.srcEvent,c=a.offsetDirection;if(this.manager.session.prevented)return void b.preventDefault();var d=this.actions,e=p(d,jb)&&!mb[jb],f=p(d,lb)&&!mb[lb],g=p(d,kb)&&!mb[kb];if(e){var h=1===a.pointers.length,i=a.distance<2,j=a.deltaTime<250;if(h&&i&&j)return}return g&&f?void 0:e||f&&c&Na||g&&c&Oa?this.preventSrc(b):void 0},preventSrc:function(a){this.manager.session.prevented=!0,a.preventDefault()}};var nb=1,ob=2,pb=4,qb=8,rb=qb,sb=16,tb=32;Y.prototype={defaults:{},set:function(a){return la(this.options,a),this.manager&&this.manager.touchAction.update(),this},recognizeWith:function(a){if(f(a,"recognizeWith",this))return this;var b=this.simultaneous;return a=_(a,this),b[a.id]||(b[a.id]=a,a.recognizeWith(this)),this},dropRecognizeWith:function(a){return f(a,"dropRecognizeWith",this)?this:(a=_(a,this),delete this.simultaneous[a.id],this)},requireFailure:function(a){if(f(a,"requireFailure",this))return this;var b=this.requireFail;return a=_(a,this),-1===r(b,a)&&(b.push(a),a.requireFailure(this)),this},dropRequireFailure:function(a){if(f(a,"dropRequireFailure",this))return this;a=_(a,this);var b=r(this.requireFail,a);return b>-1&&this.requireFail.splice(b,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(a){return!!this.simultaneous[a.id]},emit:function(a){function b(b){c.manager.emit(b,a)}var c=this,d=this.state;qb>d&&b(c.options.event+Z(d)),b(c.options.event),a.additionalEvent&&b(a.additionalEvent),d>=qb&&b(c.options.event+Z(d))},tryEmit:function(a){return this.canEmit()?this.emit(a):void(this.state=tb)},canEmit:function(){for(var a=0;a<this.requireFail.length;){if(!(this.requireFail[a].state&(tb|nb)))return!1;a++}return!0},recognize:function(a){var b=la({},a);return k(this.options.enable,[this,b])?(this.state&(rb|sb|tb)&&(this.state=nb),this.state=this.process(b),void(this.state&(ob|pb|qb|sb)&&this.tryEmit(b))):(this.reset(),void(this.state=tb))},process:function(a){},getTouchAction:function(){},reset:function(){}},i(aa,Y,{defaults:{pointers:1},attrTest:function(a){var b=this.options.pointers;return 0===b||a.pointers.length===b},process:function(a){var b=this.state,c=a.eventType,d=b&(ob|pb),e=this.attrTest(a);return d&&(c&Ha||!e)?b|sb:d||e?c&Ga?b|qb:b&ob?b|pb:ob:tb}}),i(ba,aa,{defaults:{event:"pan",threshold:10,pointers:1,direction:Pa},getTouchAction:function(){var a=this.options.direction,b=[];return a&Na&&b.push(lb),a&Oa&&b.push(kb),b},directionTest:function(a){var b=this.options,c=!0,d=a.distance,e=a.direction,f=a.deltaX,g=a.deltaY;return e&b.direction||(b.direction&Na?(e=0===f?Ia:0>f?Ja:Ka,c=f!=this.pX,d=Math.abs(a.deltaX)):(e=0===g?Ia:0>g?La:Ma,c=g!=this.pY,d=Math.abs(a.deltaY))),a.direction=e,c&&d>b.threshold&&e&b.direction},attrTest:function(a){return aa.prototype.attrTest.call(this,a)&&(this.state&ob||!(this.state&ob)&&this.directionTest(a))},emit:function(a){this.pX=a.deltaX,this.pY=a.deltaY;var b=$(a.direction);b&&(a.additionalEvent=this.options.event+b),this._super.emit.call(this,a)}}),i(ca,aa,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.scale-1)>this.options.threshold||this.state&ob)},emit:function(a){if(1!==a.scale){var b=a.scale<1?"in":"out";a.additionalEvent=this.options.event+b}this._super.emit.call(this,a)}}),i(da,Y,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[hb]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance<b.threshold,f=a.deltaTime>b.time;if(this._input=a,!d||!c||a.eventType&(Ga|Ha)&&!f)this.reset();else if(a.eventType&Ea)this.reset(),this._timer=e(function(){this.state=rb,this.tryEmit()},b.time,this);else if(a.eventType&Ga)return rb;return tb},reset:function(){clearTimeout(this._timer)},emit:function(a){this.state===rb&&(a&&a.eventType&Ga?this.manager.emit(this.options.event+"up",a):(this._input.timeStamp=ra(),this.manager.emit(this.options.event,this._input)))}}),i(ea,aa,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.rotation)>this.options.threshold||this.state&ob)}}),i(fa,aa,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:Na|Oa,pointers:1},getTouchAction:function(){return ba.prototype.getTouchAction.call(this)},attrTest:function(a){var b,c=this.options.direction;return c&(Na|Oa)?b=a.overallVelocity:c&Na?b=a.overallVelocityX:c&Oa&&(b=a.overallVelocityY),this._super.attrTest.call(this,a)&&c&a.offsetDirection&&a.distance>this.options.threshold&&a.maxPointers==this.options.pointers&&qa(b)>this.options.velocity&&a.eventType&Ga},emit:function(a){var b=$(a.offsetDirection);b&&this.manager.emit(this.options.event+b,a),this.manager.emit(this.options.event,a)}}),i(ga,Y,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[ib]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance<b.threshold,f=a.deltaTime<b.time;if(this.reset(),a.eventType&Ea&&0===this.count)return this.failTimeout();if(d&&f&&c){if(a.eventType!=Ga)return this.failTimeout();var g=this.pTime?a.timeStamp-this.pTime<b.interval:!0,h=!this.pCenter||H(this.pCenter,a.center)<b.posThreshold;this.pTime=a.timeStamp,this.pCenter=a.center,h&&g?this.count+=1:this.count=1,this._input=a;var i=this.count%b.taps;if(0===i)return this.hasRequireFailures()?(this._timer=e(function(){this.state=rb,this.tryEmit()},b.interval,this),ob):rb}return tb},failTimeout:function(){return this._timer=e(function(){this.state=tb},this.options.interval,this),tb},reset:function(){clearTimeout(this._timer)},emit:function(){this.state==rb&&(this._input.tapCount=this.count,this.manager.emit(this.options.event,this._input))}}),ha.VERSION="2.0.8",ha.defaults={domEvents:!1,touchAction:gb,enable:!0,inputTarget:null,inputClass:null,preset:[[ea,{enable:!1}],[ca,{enable:!1},["rotate"]],[fa,{direction:Na}],[ba,{direction:Na},["swipe"]],[ga],[ga,{event:"doubletap",taps:2},["tap"]],[da]],cssProps:{userSelect:"none",touchSelect:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}};var ub=1,vb=2;ia.prototype={set:function(a){return la(this.options,a),a.touchAction&&this.touchAction.update(),a.inputTarget&&(this.input.destroy(),this.input.target=a.inputTarget,this.input.init()),this},stop:function(a){this.session.stopped=a?vb:ub},recognize:function(a){var b=this.session;if(!b.stopped){this.touchAction.preventDefaults(a);var c,d=this.recognizers,e=b.curRecognizer;(!e||e&&e.state&rb)&&(e=b.curRecognizer=null);for(var f=0;f<d.length;)c=d[f],b.stopped===vb||e&&c!=e&&!c.canRecognizeWith(e)?c.reset():c.recognize(a),!e&&c.state&(ob|pb|qb)&&(e=b.curRecognizer=c),f++}},get:function(a){if(a instanceof Y)return a;for(var b=this.recognizers,c=0;c<b.length;c++)if(b[c].options.event==a)return b[c];return null},add:function(a){if(f(a,"add",this))return this;var b=this.get(a.options.event);return b&&this.remove(b),this.recognizers.push(a),a.manager=this,this.touchAction.update(),a},remove:function(a){if(f(a,"remove",this))return this;if(a=this.get(a)){var b=this.recognizers,c=r(b,a);-1!==c&&(b.splice(c,1),this.touchAction.update())}return this},on:function(a,b){if(a!==d&&b!==d){var c=this.handlers;return g(q(a),function(a){c[a]=c[a]||[],c[a].push(b)}),this}},off:function(a,b){if(a!==d){var c=this.handlers;return g(q(a),function(a){b?c[a]&&c[a].splice(r(c[a],b),1):delete c[a]}),this}},emit:function(a,b){this.options.domEvents&&ka(a,b);var c=this.handlers[a]&&this.handlers[a].slice();if(c&&c.length){b.type=a,b.preventDefault=function(){b.srcEvent.preventDefault()};for(var d=0;d<c.length;)c[d](b),d++}},destroy:function(){this.element&&ja(this,!1),this.handlers={},this.session={},this.input.destroy(),this.element=null}},la(ha,{INPUT_START:Ea,INPUT_MOVE:Fa,INPUT_END:Ga,INPUT_CANCEL:Ha,STATE_POSSIBLE:nb,STATE_BEGAN:ob,STATE_CHANGED:pb,STATE_ENDED:qb,STATE_RECOGNIZED:rb,STATE_CANCELLED:sb,STATE_FAILED:tb,DIRECTION_NONE:Ia,DIRECTION_LEFT:Ja,DIRECTION_RIGHT:Ka,DIRECTION_UP:La,DIRECTION_DOWN:Ma,DIRECTION_HORIZONTAL:Na,DIRECTION_VERTICAL:Oa,DIRECTION_ALL:Pa,Manager:ia,Input:x,TouchAction:V,TouchInput:P,MouseInput:L,PointerEventInput:M,TouchMouseInput:R,SingleTouchInput:N,Recognizer:Y,AttrRecognizer:aa,Tap:ga,Pan:ba,Swipe:fa,Pinch:ca,Rotate:ea,Press:da,on:m,off:n,each:g,merge:ta,extend:sa,assign:la,inherit:i,bindFn:j,prefixed:u});var wb="undefined"!=typeof a?a:"undefined"!=typeof self?self:{};wb.Hammer=ha,"function"==typeof define&&define.amd?define(function(){return ha}):"undefined"!=typeof module&&module.exports?module.exports=ha:a[c]=ha}(window,document,"Hammer");
//# sourceMappingURL=hammer.min.js.map]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="IntersectionObserver" javascript_name="IntersectionObserver.js" javascript_type="framework" javascript_version="107643" javascript_position="200"><![CDATA[/**
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE.
 *
 *  https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
 *
 */

!function(d,_){"use strict";if("IntersectionObserver"in d&&"IntersectionObserverEntry"in d&&"intersectionRatio"in d.IntersectionObserverEntry.prototype)"isIntersecting"in d.IntersectionObserverEntry.prototype||Object.defineProperty(d.IntersectionObserverEntry.prototype,"isIntersecting",{get:function(){return 0<this.intersectionRatio}});else{var e=[];t.prototype.THROTTLE_TIMEOUT=100,t.prototype.POLL_INTERVAL=null,t.prototype.USE_MUTATION_OBSERVER=!0,t.prototype.observe=function(e){if(!this._observationTargets.some(function(t){return t.element==e})){if(!e||1!=e.nodeType)throw new Error("target must be an Element");this._registerInstance(),this._observationTargets.push({element:e,entry:null}),this._monitorIntersections(),this._checkForIntersections()}},t.prototype.unobserve=function(e){this._observationTargets=this._observationTargets.filter(function(t){return t.element!=e}),this._observationTargets.length||(this._unmonitorIntersections(),this._unregisterInstance())},t.prototype.disconnect=function(){this._observationTargets=[],this._unmonitorIntersections(),this._unregisterInstance()},t.prototype.takeRecords=function(){var t=this._queuedEntries.slice();return this._queuedEntries=[],t},t.prototype._initThresholds=function(t){var e=t||[0];return Array.isArray(e)||(e=[e]),e.sort().filter(function(t,e,n){if("number"!=typeof t||isNaN(t)||t<0||1<t)throw new Error("threshold must be a number between 0 and 1 inclusively");return t!==n[e-1]})},t.prototype._parseRootMargin=function(t){var e=(t||"0px").split(/\s+/).map(function(t){var e=/^(-?\d*\.?\d+)(px|%)$/.exec(t);if(!e)throw new Error("rootMargin must be specified in pixels or percent");return{value:parseFloat(e[1]),unit:e[2]}});return e[1]=e[1]||e[0],e[2]=e[2]||e[0],e[3]=e[3]||e[1],e},t.prototype._monitorIntersections=function(){this._monitoringIntersections||(this._monitoringIntersections=!0,this.POLL_INTERVAL?this._monitoringInterval=setInterval(this._checkForIntersections,this.POLL_INTERVAL):(n(d,"resize",this._checkForIntersections,!0),n(_,"scroll",this._checkForIntersections,!0),this.USE_MUTATION_OBSERVER&&"MutationObserver"in d&&(this._domObserver=new MutationObserver(this._checkForIntersections),this._domObserver.observe(_,{attributes:!0,childList:!0,characterData:!0,subtree:!0}))))},t.prototype._unmonitorIntersections=function(){this._monitoringIntersections&&(this._monitoringIntersections=!1,clearInterval(this._monitoringInterval),this._monitoringInterval=null,o(d,"resize",this._checkForIntersections,!0),o(_,"scroll",this._checkForIntersections,!0),this._domObserver&&(this._domObserver.disconnect(),this._domObserver=null))},t.prototype._checkForIntersections=function(){var h=this._rootIsInDom(),c=h?this._getRootRect():{top:0,bottom:0,left:0,right:0,width:0,height:0};this._observationTargets.forEach(function(t){var e=t.element,n=m(e),o=this._rootContainsTarget(e),i=t.entry,r=h&&o&&this._computeTargetAndRootIntersection(e,c),s=t.entry=new a({time:d.performance&&performance.now&&performance.now(),target:e,boundingClientRect:n,rootBounds:c,intersectionRect:r});i?h&&o?this._hasCrossedThreshold(i,s)&&this._queuedEntries.push(s):i&&i.isIntersecting&&this._queuedEntries.push(s):this._queuedEntries.push(s)},this),this._queuedEntries.length&&this._callback(this.takeRecords(),this)},t.prototype._computeTargetAndRootIntersection=function(t,e){if("none"!=d.getComputedStyle(t).display){for(var n,o,i,r,s,h,c,a,u=m(t),l=v(t),p=!1;!p;){var f=null,g=1==l.nodeType?d.getComputedStyle(l):{};if("none"==g.display)return;if(l==this.root||l==_?(p=!0,f=e):l!=_.body&&l!=_.documentElement&&"visible"!=g.overflow&&(f=m(l)),f&&(n=f,o=u,void 0,i=Math.max(n.top,o.top),r=Math.min(n.bottom,o.bottom),s=Math.max(n.left,o.left),h=Math.min(n.right,o.right),a=r-i,!(u=0<=(c=h-s)&&0<=a&&{top:i,bottom:r,left:s,right:h,width:c,height:a})))break;l=v(l)}return u}},t.prototype._getRootRect=function(){var t;if(this.root)t=m(this.root);else{var e=_.documentElement,n=_.body;t={top:0,left:0,right:e.clientWidth||n.clientWidth,width:e.clientWidth||n.clientWidth,bottom:e.clientHeight||n.clientHeight,height:e.clientHeight||n.clientHeight}}return this._expandRectByRootMargin(t)},t.prototype._expandRectByRootMargin=function(n){var t=this._rootMarginValues.map(function(t,e){return"px"==t.unit?t.value:t.value*(e%2?n.width:n.height)/100}),e={top:n.top-t[0],right:n.right+t[1],bottom:n.bottom+t[2],left:n.left-t[3]};return e.width=e.right-e.left,e.height=e.bottom-e.top,e},t.prototype._hasCrossedThreshold=function(t,e){var n=t&&t.isIntersecting?t.intersectionRatio||0:-1,o=e.isIntersecting?e.intersectionRatio||0:-1;if(n!==o)for(var i=0;i<this.thresholds.length;i++){var r=this.thresholds[i];if(r==n||r==o||r<n!=r<o)return!0}},t.prototype._rootIsInDom=function(){return!this.root||i(_,this.root)},t.prototype._rootContainsTarget=function(t){return i(this.root||_,t)},t.prototype._registerInstance=function(){e.indexOf(this)<0&&e.push(this)},t.prototype._unregisterInstance=function(){var t=e.indexOf(this);-1!=t&&e.splice(t,1)},d.IntersectionObserver=t,d.IntersectionObserverEntry=a}function a(t){this.time=t.time,this.target=t.target,this.rootBounds=t.rootBounds,this.boundingClientRect=t.boundingClientRect,this.intersectionRect=t.intersectionRect||{top:0,bottom:0,left:0,right:0,width:0,height:0},this.isIntersecting=!!t.intersectionRect;var e=this.boundingClientRect,n=e.width*e.height,o=this.intersectionRect,i=o.width*o.height;this.intersectionRatio=n?i/n:this.isIntersecting?1:0}function t(t,e){var n,o,i,r=e||{};if("function"!=typeof t)throw new Error("callback must be a function");if(r.root&&1!=r.root.nodeType)throw new Error("root must be an Element");this._checkForIntersections=(n=this._checkForIntersections.bind(this),o=this.THROTTLE_TIMEOUT,i=null,function(){i||(i=setTimeout(function(){n(),i=null},o))}),this._callback=t,this._observationTargets=[],this._queuedEntries=[],this._rootMarginValues=this._parseRootMargin(r.rootMargin),this.thresholds=this._initThresholds(r.threshold),this.root=r.root||null,this.rootMargin=this._rootMarginValues.map(function(t){return t.value+t.unit}).join(" ")}function n(t,e,n,o){"function"==typeof t.addEventListener?t.addEventListener(e,n,o||!1):"function"==typeof t.attachEvent&&t.attachEvent("on"+e,n)}function o(t,e,n,o){"function"==typeof t.removeEventListener?t.removeEventListener(e,n,o||!1):"function"==typeof t.detatchEvent&&t.detatchEvent("on"+e,n)}function m(t){var e;try{e=t.getBoundingClientRect()}catch(t){}return e?(e.width&&e.height||(e={top:e.top,right:e.right,bottom:e.bottom,left:e.left,width:e.right-e.left,height:e.bottom-e.top}),e):{top:0,bottom:0,left:0,right:0,width:0,height:0}}function i(t,e){for(var n=e;n;){if(n==t)return!0;n=v(n)}return!1}function v(t){var e=t.parentNode;return e&&11==e.nodeType&&e.host?e.host:e}}(window,document);]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="jquery" javascript_name="jquery-migrate.js" javascript_type="framework" javascript_version="107643" javascript_position="102"><![CDATA[/*!
 * jQuery Migrate - v3.3.0 - 2020-05-05T01:57Z
 * Copyright OpenJS Foundation and other contributors
 */
( function( factory ) {
	"use strict";

	if ( typeof define === "function" && define.amd ) {

		// AMD. Register as an anonymous module.
		define( [ "jquery" ], function( jQuery ) {
			return factory( jQuery, window );
		} );
	} else if ( typeof module === "object" && module.exports ) {

		// Node/CommonJS
		// eslint-disable-next-line no-undef
		module.exports = factory( require( "jquery" ), window );
	} else {

		// Browser globals
		factory( jQuery, window );
	}
} )( function( jQuery, window ) {
"use strict";

jQuery.migrateVersion = "3.3.0";

// Returns 0 if v1 == v2, -1 if v1 < v2, 1 if v1 > v2
function compareVersions( v1, v2 ) {
	var i,
		rVersionParts = /^(\d+)\.(\d+)\.(\d+)/,
		v1p = rVersionParts.exec( v1 ) || [ ],
		v2p = rVersionParts.exec( v2 ) || [ ];

	for ( i = 1; i <= 3; i++ ) {
		if ( +v1p[ i ] > +v2p[ i ] ) {
			return 1;
		}
		if ( +v1p[ i ] < +v2p[ i ] ) {
			return -1;
		}
	}
	return 0;
}

function jQueryVersionSince( version ) {
	return compareVersions( jQuery.fn.jquery, version ) >= 0;
}

( function() {

	// Support: IE9 only
	// IE9 only creates console object when dev tools are first opened
	// IE9 console is a host object, callable but doesn't have .apply()
	if ( !window.console || !window.console.log ) {
		return;
	}

	// Need jQuery 3.0.0+ and no older Migrate loaded
	if ( !jQuery || !jQueryVersionSince( "3.0.0" ) ) {
		window.console.log( "JQMIGRATE: jQuery 3.0.0+ REQUIRED" );
	}
	if ( jQuery.migrateWarnings ) {
		window.console.log( "JQMIGRATE: Migrate plugin loaded multiple times" );
	}

	// Show a message on the console so devs know we're active
	window.console.log( "JQMIGRATE: Migrate is installed" +
		( jQuery.migrateMute ? "" : " with logging active" ) +
		", version " + jQuery.migrateVersion );

} )();

var warnedAbout = {};

// By default each warning is only reported once.
jQuery.migrateDeduplicateWarnings = true;

// List of warnings already given; public read only
jQuery.migrateWarnings = [];

// Set to false to disable traces that appear with warnings
if ( jQuery.migrateTrace === undefined ) {
	jQuery.migrateTrace = true;
}

// Forget any warnings we've already given; public
jQuery.migrateReset = function() {
	warnedAbout = {};
	jQuery.migrateWarnings.length = 0;
};

function migrateWarn( msg ) {
	var console = window.console;
	if ( !jQuery.migrateDeduplicateWarnings || !warnedAbout[ msg ] ) {
		warnedAbout[ msg ] = true;
		jQuery.migrateWarnings.push( msg );
		if ( console && console.warn && !jQuery.migrateMute ) {
			console.warn( "JQMIGRATE: " + msg );
			if ( jQuery.migrateTrace && console.trace ) {
				console.trace();
			}
		}
	}
}

function migrateWarnProp( obj, prop, value, msg ) {
	Object.defineProperty( obj, prop, {
		configurable: true,
		enumerable: true,
		get: function() {
			migrateWarn( msg );
			return value;
		},
		set: function( newValue ) {
			migrateWarn( msg );
			value = newValue;
		}
	} );
}

function migrateWarnFunc( obj, prop, newFunc, msg ) {
	obj[ prop ] = function() {
		migrateWarn( msg );
		return newFunc.apply( this, arguments );
	};
}

if ( window.document.compatMode === "BackCompat" ) {

	// JQuery has never supported or tested Quirks Mode
	migrateWarn( "jQuery is not compatible with Quirks Mode" );
}

var findProp,
	class2type = {},
	oldInit = jQuery.fn.init,
	oldFind = jQuery.find,

	rattrHashTest = /\[(\s*[-\w]+\s*)([~|^$*]?=)\s*([-\w#]*?#[-\w#]*)\s*\]/,
	rattrHashGlob = /\[(\s*[-\w]+\s*)([~|^$*]?=)\s*([-\w#]*?#[-\w#]*)\s*\]/g,

	// Support: Android <=4.0 only
	// Make sure we trim BOM and NBSP
	rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;

jQuery.fn.init = function( arg1 ) {
	var args = Array.prototype.slice.call( arguments );

	if ( typeof arg1 === "string" && arg1 === "#" ) {

		// JQuery( "#" ) is a bogus ID selector, but it returned an empty set before jQuery 3.0
		migrateWarn( "jQuery( '#' ) is not a valid selector" );
		args[ 0 ] = [];
	}

	return oldInit.apply( this, args );
};
jQuery.fn.init.prototype = jQuery.fn;

jQuery.find = function( selector ) {
	var args = Array.prototype.slice.call( arguments );

	// Support: PhantomJS 1.x
	// String#match fails to match when used with a //g RegExp, only on some strings
	if ( typeof selector === "string" && rattrHashTest.test( selector ) ) {

		// The nonstandard and undocumented unquoted-hash was removed in jQuery 1.12.0
		// First see if qS thinks it's a valid selector, if so avoid a false positive
		try {
			window.document.querySelector( selector );
		} catch ( err1 ) {

			// Didn't *look* valid to qSA, warn and try quoting what we think is the value
			selector = selector.replace( rattrHashGlob, function( _, attr, op, value ) {
				return "[" + attr + op + "\"" + value + "\"]";
			} );

			// If the regexp *may* have created an invalid selector, don't update it
			// Note that there may be false alarms if selector uses jQuery extensions
			try {
				window.document.querySelector( selector );
				migrateWarn( "Attribute selector with '#' must be quoted: " + args[ 0 ] );
				args[ 0 ] = selector;
			} catch ( err2 ) {
				migrateWarn( "Attribute selector with '#' was not fixed: " + args[ 0 ] );
			}
		}
	}

	return oldFind.apply( this, args );
};

// Copy properties attached to original jQuery.find method (e.g. .attr, .isXML)
for ( findProp in oldFind ) {
	if ( Object.prototype.hasOwnProperty.call( oldFind, findProp ) ) {
		jQuery.find[ findProp ] = oldFind[ findProp ];
	}
}

// The number of elements contained in the matched element set
migrateWarnFunc( jQuery.fn, "size", function() {
	return this.length;
},
"jQuery.fn.size() is deprecated and removed; use the .length property" );

migrateWarnFunc( jQuery, "parseJSON", function() {
	return JSON.parse.apply( null, arguments );
},
"jQuery.parseJSON is deprecated; use JSON.parse" );

migrateWarnFunc( jQuery, "holdReady", jQuery.holdReady,
	"jQuery.holdReady is deprecated" );

migrateWarnFunc( jQuery, "unique", jQuery.uniqueSort,
	"jQuery.unique is deprecated; use jQuery.uniqueSort" );

// Now jQuery.expr.pseudos is the standard incantation
migrateWarnProp( jQuery.expr, "filters", jQuery.expr.pseudos,
	"jQuery.expr.filters is deprecated; use jQuery.expr.pseudos" );
migrateWarnProp( jQuery.expr, ":", jQuery.expr.pseudos,
	"jQuery.expr[':'] is deprecated; use jQuery.expr.pseudos" );

// Prior to jQuery 3.1.1 there were internal refs so we don't warn there
if ( jQueryVersionSince( "3.1.1" ) ) {
	migrateWarnFunc( jQuery, "trim", function( text ) {
		return text == null ?
			"" :
			( text + "" ).replace( rtrim, "" );
	},
	"jQuery.trim is deprecated; use String.prototype.trim" );
}

// Prior to jQuery 3.2 there were internal refs so we don't warn there
if ( jQueryVersionSince( "3.2.0" ) ) {
	migrateWarnFunc( jQuery, "nodeName", function( elem, name ) {
		return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
	},
	"jQuery.nodeName is deprecated" );
}

if ( jQueryVersionSince( "3.3.0" ) ) {

	migrateWarnFunc( jQuery, "isNumeric", function( obj ) {

			// As of jQuery 3.0, isNumeric is limited to
			// strings and numbers (primitives or objects)
			// that can be coerced to finite numbers (gh-2662)
			var type = typeof obj;
			return ( type === "number" || type === "string" ) &&

				// parseFloat NaNs numeric-cast false positives ("")
				// ...but misinterprets leading-number strings, e.g. hex literals ("0x...")
				// subtraction forces infinities to NaN
				!isNaN( obj - parseFloat( obj ) );
		},
		"jQuery.isNumeric() is deprecated"
	);

	// Populate the class2type map
	jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".
		split( " " ),
	function( _, name ) {
		class2type[ "[object " + name + "]" ] = name.toLowerCase();
	} );

	migrateWarnFunc( jQuery, "type", function( obj ) {
		if ( obj == null ) {
			return obj + "";
		}

		// Support: Android <=2.3 only (functionish RegExp)
		return typeof obj === "object" || typeof obj === "function" ?
			class2type[ Object.prototype.toString.call( obj ) ] || "object" :
			typeof obj;
	},
	"jQuery.type is deprecated" );

	migrateWarnFunc( jQuery, "isFunction",
		function( obj ) {
			return typeof obj === "function";
		},
		"jQuery.isFunction() is deprecated" );

	migrateWarnFunc( jQuery, "isWindow",
		function( obj ) {
			return obj != null && obj === obj.window;
		},
		"jQuery.isWindow() is deprecated"
	);

	migrateWarnFunc( jQuery, "isArray", Array.isArray,
		"jQuery.isArray is deprecated; use Array.isArray"
	);
}

// Support jQuery slim which excludes the ajax module
if ( jQuery.ajax ) {

var oldAjax = jQuery.ajax;

jQuery.ajax = function( ) {
	var jQXHR = oldAjax.apply( this, arguments );

	// Be sure we got a jQXHR (e.g., not sync)
	if ( jQXHR.promise ) {
		migrateWarnFunc( jQXHR, "success", jQXHR.done,
			"jQXHR.success is deprecated and removed" );
		migrateWarnFunc( jQXHR, "error", jQXHR.fail,
			"jQXHR.error is deprecated and removed" );
		migrateWarnFunc( jQXHR, "complete", jQXHR.always,
			"jQXHR.complete is deprecated and removed" );
	}

	return jQXHR;
};

}

var oldRemoveAttr = jQuery.fn.removeAttr,
	oldToggleClass = jQuery.fn.toggleClass,
	rmatchNonSpace = /\S+/g;

jQuery.fn.removeAttr = function( name ) {
	var self = this;

	jQuery.each( name.match( rmatchNonSpace ), function( _i, attr ) {
		if ( jQuery.expr.match.bool.test( attr ) ) {
			migrateWarn( "jQuery.fn.removeAttr no longer sets boolean properties: " + attr );
			self.prop( attr, false );
		}
	} );

	return oldRemoveAttr.apply( this, arguments );
};

jQuery.fn.toggleClass = function( state ) {

	// Only deprecating no-args or single boolean arg
	if ( state !== undefined && typeof state !== "boolean" ) {
		return oldToggleClass.apply( this, arguments );
	}

	migrateWarn( "jQuery.fn.toggleClass( boolean ) is deprecated" );

	// Toggle entire class name of each element
	return this.each( function() {
		var className = this.getAttribute && this.getAttribute( "class" ) || "";

		if ( className ) {
			jQuery.data( this, "__className__", className );
		}

		// If the element has a class name or if we're passed `false`,
		// then remove the whole classname (if there was one, the above saved it).
		// Otherwise bring back whatever was previously saved (if anything),
		// falling back to the empty string if nothing was stored.
		if ( this.setAttribute ) {
			this.setAttribute( "class",
				className || state === false ?
				"" :
				jQuery.data( this, "__className__" ) || ""
			);
		}
	} );
};

function camelCase( string ) {
	return string.replace( /-([a-z])/g, function( _, letter ) {
		return letter.toUpperCase();
	} );
}

var oldFnCss,
	internalSwapCall = false,
	ralphaStart = /^[a-z]/,

	// The regex visualized:
	//
	//                         /----------\
	//                        |            |    /-------\
	//                        |  / Top  \  |   |         |
	//         /--- Border ---+-| Right  |-+---+- Width -+---\
	//        |                 | Bottom |                    |
	//        |                  \ Left /                     |
	//        |                                               |
	//        |                              /----------\     |
	//        |          /-------------\    |            |    |- END
	//        |         |               |   |  / Top  \  |    |
	//        |         |  / Margin  \  |   | | Right  | |    |
	//        |---------+-|           |-+---+-| Bottom |-+----|
	//        |            \ Padding /         \ Left /       |
	// BEGIN -|                                               |
	//        |                /---------\                    |
	//        |               |           |                   |
	//        |               |  / Min \  |    / Width  \     |
	//         \--------------+-|       |-+---|          |---/
	//                           \ Max /       \ Height /
	rautoPx = /^(?:Border(?:Top|Right|Bottom|Left)?(?:Width|)|(?:Margin|Padding)?(?:Top|Right|Bottom|Left)?|(?:Min|Max)?(?:Width|Height))$/;

// If this version of jQuery has .swap(), don't false-alarm on internal uses
if ( jQuery.swap ) {
	jQuery.each( [ "height", "width", "reliableMarginRight" ], function( _, name ) {
		var oldHook = jQuery.cssHooks[ name ] && jQuery.cssHooks[ name ].get;

		if ( oldHook ) {
			jQuery.cssHooks[ name ].get = function() {
				var ret;

				internalSwapCall = true;
				ret = oldHook.apply( this, arguments );
				internalSwapCall = false;
				return ret;
			};
		}
	} );
}

jQuery.swap = function( elem, options, callback, args ) {
	var ret, name,
		old = {};

	if ( !internalSwapCall ) {
		migrateWarn( "jQuery.swap() is undocumented and deprecated" );
	}

	// Remember the old values, and insert the new ones
	for ( name in options ) {
		old[ name ] = elem.style[ name ];
		elem.style[ name ] = options[ name ];
	}

	ret = callback.apply( elem, args || [] );

	// Revert the old values
	for ( name in options ) {
		elem.style[ name ] = old[ name ];
	}

	return ret;
};

if ( jQueryVersionSince( "3.4.0" ) && typeof Proxy !== "undefined" ) {

	jQuery.cssProps = new Proxy( jQuery.cssProps || {}, {
		set: function() {
			migrateWarn( "JQMIGRATE: jQuery.cssProps is deprecated" );
			return Reflect.set.apply( this, arguments );
		}
	} );
}

// Create a dummy jQuery.cssNumber if missing. It won't be used by jQuery but
// it will prevent code adding new keys to it unconditionally from crashing.
if ( !jQuery.cssNumber ) {
	jQuery.cssNumber = {};
}

function isAutoPx( prop ) {

	// The first test is used to ensure that:
	// 1. The prop starts with a lowercase letter (as we uppercase it for the second regex).
	// 2. The prop is not empty.
	return ralphaStart.test( prop ) &&
		rautoPx.test( prop[ 0 ].toUpperCase() + prop.slice( 1 ) );
}

oldFnCss = jQuery.fn.css;

jQuery.fn.css = function( name, value ) {
	var origThis = this;
	if ( typeof name !== "string" ) {
		jQuery.each( name, function( n, v ) {
			jQuery.fn.css.call( origThis, n, v );
		} );
	}
	if ( typeof value === "number" && !isAutoPx( camelCase( name ) ) ) {
		migrateWarn( "Use of number-typed values is deprecated in jQuery.fn.css" );
	}

	return oldFnCss.apply( this, arguments );
};

var oldData = jQuery.data;

jQuery.data = function( elem, name, value ) {
	var curData, sameKeys, key;

	// Name can be an object, and each entry in the object is meant to be set as data
	if ( name && typeof name === "object" && arguments.length === 2 ) {
		curData = jQuery.hasData( elem ) && oldData.call( this, elem );
		sameKeys = {};
		for ( key in name ) {
			if ( key !== camelCase( key ) ) {
				migrateWarn( "jQuery.data() always sets/gets camelCased names: " + key );
				curData[ key ] = name[ key ];
			} else {
				sameKeys[ key ] = name[ key ];
			}
		}

		oldData.call( this, elem, sameKeys );

		return name;
	}

	// If the name is transformed, look for the un-transformed name in the data object
	if ( name && typeof name === "string" && name !== camelCase( name ) ) {
		curData = jQuery.hasData( elem ) && oldData.call( this, elem );
		if ( curData && name in curData ) {
			migrateWarn( "jQuery.data() always sets/gets camelCased names: " + name );
			if ( arguments.length > 2 ) {
				curData[ name ] = value;
			}
			return curData[ name ];
		}
	}

	return oldData.apply( this, arguments );
};

// Support jQuery slim which excludes the effects module
if ( jQuery.fx ) {

var intervalValue, intervalMsg,
	oldTweenRun = jQuery.Tween.prototype.run,
	linearEasing = function( pct ) {
		return pct;
	};

jQuery.Tween.prototype.run = function( ) {
	if ( jQuery.easing[ this.easing ].length > 1 ) {
		migrateWarn(
			"'jQuery.easing." + this.easing.toString() + "' should use only one argument"
		);

		jQuery.easing[ this.easing ] = linearEasing;
	}

	oldTweenRun.apply( this, arguments );
};

intervalValue = jQuery.fx.interval || 13;
intervalMsg = "jQuery.fx.interval is deprecated";

// Support: IE9, Android <=4.4
// Avoid false positives on browsers that lack rAF
// Don't warn if document is hidden, jQuery uses setTimeout (#292)
if ( window.requestAnimationFrame ) {
	Object.defineProperty( jQuery.fx, "interval", {
		configurable: true,
		enumerable: true,
		get: function() {
			if ( !window.document.hidden ) {
				migrateWarn( intervalMsg );
			}
			return intervalValue;
		},
		set: function( newValue ) {
			migrateWarn( intervalMsg );
			intervalValue = newValue;
		}
	} );
}

}

var oldLoad = jQuery.fn.load,
	oldEventAdd = jQuery.event.add,
	originalFix = jQuery.event.fix;

jQuery.event.props = [];
jQuery.event.fixHooks = {};

migrateWarnProp( jQuery.event.props, "concat", jQuery.event.props.concat,
	"jQuery.event.props.concat() is deprecated and removed" );

jQuery.event.fix = function( originalEvent ) {
	var event,
		type = originalEvent.type,
		fixHook = this.fixHooks[ type ],
		props = jQuery.event.props;

	if ( props.length ) {
		migrateWarn( "jQuery.event.props are deprecated and removed: " + props.join() );
		while ( props.length ) {
			jQuery.event.addProp( props.pop() );
		}
	}

	if ( fixHook && !fixHook._migrated_ ) {
		fixHook._migrated_ = true;
		migrateWarn( "jQuery.event.fixHooks are deprecated and removed: " + type );
		if ( ( props = fixHook.props ) && props.length ) {
			while ( props.length ) {
				jQuery.event.addProp( props.pop() );
			}
		}
	}

	event = originalFix.call( this, originalEvent );

	return fixHook && fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
};

jQuery.event.add = function( elem, types ) {

	// This misses the multiple-types case but that seems awfully rare
	if ( elem === window && types === "load" && window.document.readyState === "complete" ) {
		migrateWarn( "jQuery(window).on('load'...) called after load event occurred" );
	}
	return oldEventAdd.apply( this, arguments );
};

jQuery.each( [ "load", "unload", "error" ], function( _, name ) {

	jQuery.fn[ name ] = function() {
		var args = Array.prototype.slice.call( arguments, 0 );

		// If this is an ajax load() the first arg should be the string URL;
		// technically this could also be the "Anything" arg of the event .load()
		// which just goes to show why this dumb signature has been deprecated!
		// jQuery custom builds that exclude the Ajax module justifiably die here.
		if ( name === "load" && typeof args[ 0 ] === "string" ) {
			return oldLoad.apply( this, args );
		}

		migrateWarn( "jQuery.fn." + name + "() is deprecated" );

		args.splice( 0, 0, name );
		if ( arguments.length ) {
			return this.on.apply( this, args );
		}

		// Use .triggerHandler here because:
		// - load and unload events don't need to bubble, only applied to window or image
		// - error event should not bubble to window, although it does pre-1.7
		// See http://bugs.jquery.com/ticket/11820
		this.triggerHandler.apply( this, args );
		return this;
	};

} );

jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
	"change select submit keydown keypress keyup contextmenu" ).split( " " ),
	function( _i, name ) {

	// Handle event binding
	jQuery.fn[ name ] = function( data, fn ) {
		migrateWarn( "jQuery.fn." + name + "() event shorthand is deprecated" );
		return arguments.length > 0 ?
			this.on( name, null, data, fn ) :
			this.trigger( name );
	};
} );

// Trigger "ready" event only once, on document ready
jQuery( function() {
	jQuery( window.document ).triggerHandler( "ready" );
} );

jQuery.event.special.ready = {
	setup: function() {
		if ( this === window.document ) {
			migrateWarn( "'ready' event is deprecated" );
		}
	}
};

jQuery.fn.extend( {

	bind: function( types, data, fn ) {
		migrateWarn( "jQuery.fn.bind() is deprecated" );
		return this.on( types, null, data, fn );
	},
	unbind: function( types, fn ) {
		migrateWarn( "jQuery.fn.unbind() is deprecated" );
		return this.off( types, null, fn );
	},
	delegate: function( selector, types, data, fn ) {
		migrateWarn( "jQuery.fn.delegate() is deprecated" );
		return this.on( types, selector, data, fn );
	},
	undelegate: function( selector, types, fn ) {
		migrateWarn( "jQuery.fn.undelegate() is deprecated" );
		return arguments.length === 1 ?
			this.off( selector, "**" ) :
			this.off( types, selector || "**", fn );
	},
	hover: function( fnOver, fnOut ) {
		migrateWarn( "jQuery.fn.hover() is deprecated" );
		return this.on( "mouseenter", fnOver ).on( "mouseleave", fnOut || fnOver );
	}
} );

var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,
	origHtmlPrefilter = jQuery.htmlPrefilter,
	makeMarkup = function( html ) {
		var doc = window.document.implementation.createHTMLDocument( "" );
		doc.body.innerHTML = html;
		return doc.body && doc.body.innerHTML;
	},
	warnIfChanged = function( html ) {
		var changed = html.replace( rxhtmlTag, "<$1></$2>" );
		if ( changed !== html && makeMarkup( html ) !== makeMarkup( changed ) ) {
			migrateWarn( "HTML tags must be properly nested and closed: " + html );
		}
	};

jQuery.UNSAFE_restoreLegacyHtmlPrefilter = function() {
	jQuery.htmlPrefilter = function( html ) {
		warnIfChanged( html );
		return html.replace( rxhtmlTag, "<$1></$2>" );
	};
};

jQuery.htmlPrefilter = function( html ) {
	warnIfChanged( html );
	return origHtmlPrefilter( html );
};

var oldOffset = jQuery.fn.offset;

jQuery.fn.offset = function() {
	var docElem,
		elem = this[ 0 ],
		bogus = { top: 0, left: 0 };

	if ( !elem || !elem.nodeType ) {
		migrateWarn( "jQuery.fn.offset() requires a valid DOM element" );
		return undefined;
	}

	docElem = ( elem.ownerDocument || window.document ).documentElement;
	if ( !jQuery.contains( docElem, elem ) ) {
		migrateWarn( "jQuery.fn.offset() requires an element connected to a document" );
		return bogus;
	}

	return oldOffset.apply( this, arguments );
};

// Support jQuery slim which excludes the ajax module
// The jQuery.param patch is about respecting `jQuery.ajaxSettings.traditional`
// so it doesn't make sense for the slim build.
if ( jQuery.ajax ) {

var oldParam = jQuery.param;

jQuery.param = function( data, traditional ) {
	var ajaxTraditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;

	if ( traditional === undefined && ajaxTraditional ) {

		migrateWarn( "jQuery.param() no longer uses jQuery.ajaxSettings.traditional" );
		traditional = ajaxTraditional;
	}

	return oldParam.call( this, data, traditional );
};

}

var oldSelf = jQuery.fn.andSelf || jQuery.fn.addBack;

jQuery.fn.andSelf = function() {
	migrateWarn( "jQuery.fn.andSelf() is deprecated and removed, use jQuery.fn.addBack()" );
	return oldSelf.apply( this, arguments );
};

// Support jQuery slim which excludes the deferred module in jQuery 4.0+
if ( jQuery.Deferred ) {

var oldDeferred = jQuery.Deferred,
	tuples = [

		// Action, add listener, callbacks, .then handlers, final state
		[ "resolve", "done", jQuery.Callbacks( "once memory" ),
			jQuery.Callbacks( "once memory" ), "resolved" ],
		[ "reject", "fail", jQuery.Callbacks( "once memory" ),
			jQuery.Callbacks( "once memory" ), "rejected" ],
		[ "notify", "progress", jQuery.Callbacks( "memory" ),
			jQuery.Callbacks( "memory" ) ]
	];

jQuery.Deferred = function( func ) {
	var deferred = oldDeferred(),
		promise = deferred.promise();

	deferred.pipe = promise.pipe = function( /* fnDone, fnFail, fnProgress */ ) {
		var fns = arguments;

		migrateWarn( "deferred.pipe() is deprecated" );

		return jQuery.Deferred( function( newDefer ) {
			jQuery.each( tuples, function( i, tuple ) {
				var fn = typeof fns[ i ] === "function" && fns[ i ];

				// Deferred.done(function() { bind to newDefer or newDefer.resolve })
				// deferred.fail(function() { bind to newDefer or newDefer.reject })
				// deferred.progress(function() { bind to newDefer or newDefer.notify })
				deferred[ tuple[ 1 ] ]( function() {
					var returned = fn && fn.apply( this, arguments );
					if ( returned && typeof returned.promise === "function" ) {
						returned.promise()
							.done( newDefer.resolve )
							.fail( newDefer.reject )
							.progress( newDefer.notify );
					} else {
						newDefer[ tuple[ 0 ] + "With" ](
							this === promise ? newDefer.promise() : this,
							fn ? [ returned ] : arguments
						);
					}
				} );
			} );
			fns = null;
		} ).promise();

	};

	if ( func ) {
		func.call( deferred, deferred );
	}

	return deferred;
};

// Preserve handler of uncaught exceptions in promise chains
jQuery.Deferred.exceptionHook = oldDeferred.exceptionHook;

}

return jQuery;
} );
]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="jquery" javascript_name="jquery.dotdotdot.min.js" javascript_type="framework" javascript_version="107643" javascript_position="104"><![CDATA[/*
 *	jQuery dotdotdot 1.8.3
 *
 *	Plugin website:
 *	dotdotdot.frebsite.nl
 *
 *	Licensed under the MIT license.
 *	http://en.wikipedia.org/wiki/MIT_License
 */
!function(t,e){function n(t,e,n){var r=t.children(),o=!1;t.empty();for(var i=0,d=r.length;d>i;i++){var l=r.eq(i);if(t.append(l),n&&t.append(n),a(t,e)){l.remove(),o=!0;break}n&&n.detach()}return o}function r(e,n,i,d,l){var s=!1,c="a, table, thead, tbody, tfoot, tr, col, colgroup, object, embed, param, ol, ul, dl, blockquote, select, optgroup, option, textarea, script, style",u="script, .dotdotdot-keep";return e.contents().detach().each(function(){var h=this,f=t(h);if("undefined"==typeof h)return!0;if(f.is(u))e.append(f);else{if(s)return!0;e.append(f),!l||f.is(d.after)||f.find(d.after).length||e[e.is(c)?"after":"append"](l),a(i,d)&&(s=3==h.nodeType?o(f,n,i,d,l):r(f,n,i,d,l)),s||l&&l.detach()}}),n.addClass("is-truncated"),s}function o(e,n,r,o,d){var c=e[0];if(!c)return!1;var h=s(c),f=-1!==h.indexOf(" ")?" ":"　",p="letter"==o.wrap?"":f,g=h.split(p),v=-1,w=-1,b=0,m=g.length-1;for(o.fallbackToLetter&&0==b&&0==m&&(p="",g=h.split(p),m=g.length-1);m>=b&&(0!=b||0!=m);){var y=Math.floor((b+m)/2);if(y==w)break;w=y,l(c,g.slice(0,w+1).join(p)+o.ellipsis),r.children().each(function(){t(this).toggle().toggle()}),a(r,o)?(m=w,o.fallbackToLetter&&0==b&&0==m&&(p="",g=g[0].split(p),v=-1,w=-1,b=0,m=g.length-1)):(v=w,b=w)}if(-1==v||1==g.length&&0==g[0].length){var x=e.parent();e.detach();var C=d&&d.closest(x).length?d.length:0;if(x.contents().length>C?c=u(x.contents().eq(-1-C),n):(c=u(x,n,!0),C||x.detach()),c&&(h=i(s(c),o),l(c,h),C&&d)){var T=d.parent();t(c).parent().append(d),t.trim(T.html())||T.remove()}}else h=i(g.slice(0,v+1).join(p),o),l(c,h);return!0}function a(t,e){return t.innerHeight()>e.maxHeight}function i(e,n){for(;t.inArray(e.slice(-1),n.lastCharacter.remove)>-1;)e=e.slice(0,-1);return t.inArray(e.slice(-1),n.lastCharacter.noEllipsis)<0&&(e+=n.ellipsis),e}function d(t){return{width:t.innerWidth(),height:t.innerHeight()}}function l(t,e){t.innerText?t.innerText=e:t.nodeValue?t.nodeValue=e:t.textContent&&(t.textContent=e)}function s(t){return t.innerText?t.innerText:t.nodeValue?t.nodeValue:t.textContent?t.textContent:""}function c(t){do t=t.previousSibling;while(t&&1!==t.nodeType&&3!==t.nodeType);return t}function u(e,n,r){var o,a=e&&e[0];if(a){if(!r){if(3===a.nodeType)return a;if(t.trim(e.text()))return u(e.contents().last(),n)}for(o=c(a);!o;){if(e=e.parent(),e.is(n)||!e.length)return!1;o=c(e[0])}if(o)return u(t(o),n)}return!1}function h(e,n){return e?"string"==typeof e?(e=t(e,n),e.length?e:!1):e.jquery?e:!1:!1}function f(t){for(var e=t.innerHeight(),n=["paddingTop","paddingBottom"],r=0,o=n.length;o>r;r++){var a=parseInt(t.css(n[r]),10);isNaN(a)&&(a=0),e-=a}return e}if(!t.fn.dotdotdot){t.fn.dotdotdot=function(e){if(0==this.length)return t.fn.dotdotdot.debug('No element found for "'+this.selector+'".'),this;if(this.length>1)return this.each(function(){t(this).dotdotdot(e)});var o=this,i=o.contents();o.data("dotdotdot")&&o.trigger("destroy.dot"),o.data("dotdotdot-style",o.attr("style")||""),o.css("word-wrap","break-word"),"nowrap"===o.css("white-space")&&o.css("white-space","normal"),o.bind_events=function(){return o.bind("update.dot",function(e,d){switch(o.removeClass("is-truncated"),e.preventDefault(),e.stopPropagation(),typeof l.height){case"number":l.maxHeight=l.height;break;case"function":l.maxHeight=l.height.call(o[0]);break;default:l.maxHeight=f(o)}l.maxHeight+=l.tolerance,"undefined"!=typeof d&&(("string"==typeof d||"nodeType"in d&&1===d.nodeType)&&(d=t("<div />").append(d).contents()),d instanceof t&&(i=d)),g=o.wrapInner('<div class="dotdotdot" />').children(),g.contents().detach().end().append(i.clone(!0)).find("br").replaceWith("  <br />  ").end().css({height:"auto",width:"auto",border:"none",padding:"0",margin:"0"});var c=!1,u=!1;return s.afterElement&&(c=s.afterElement.clone(!0),c.show(),s.afterElement.detach()),a(g,l)&&(u="children"==l.wrap?n(g,l,c):r(g,o,g,l,c)),g.replaceWith(g.contents()),g=null,t.isFunction(l.callback)&&l.callback.call(o[0],u,i),s.isTruncated=u,u}).bind("isTruncated.dot",function(t,e){return t.preventDefault(),t.stopPropagation(),"function"==typeof e&&e.call(o[0],s.isTruncated),s.isTruncated}).bind("originalContent.dot",function(t,e){return t.preventDefault(),t.stopPropagation(),"function"==typeof e&&e.call(o[0],i),i}).bind("destroy.dot",function(t){t.preventDefault(),t.stopPropagation(),o.unwatch().unbind_events().contents().detach().end().append(i).attr("style",o.data("dotdotdot-style")||"").removeClass("is-truncated").data("dotdotdot",!1)}),o},o.unbind_events=function(){return o.unbind(".dot"),o},o.watch=function(){if(o.unwatch(),"window"==l.watch){var e=t(window),n=e.width(),r=e.height();e.bind("resize.dot"+s.dotId,function(){n==e.width()&&r==e.height()&&l.windowResizeFix||(n=e.width(),r=e.height(),u&&clearInterval(u),u=setTimeout(function(){o.trigger("update.dot")},100))})}else c=d(o),u=setInterval(function(){if(o.is(":visible")){var t=d(o);c.width==t.width&&c.height==t.height||(o.trigger("update.dot"),c=t)}},500);return o},o.unwatch=function(){return t(window).unbind("resize.dot"+s.dotId),u&&clearInterval(u),o};var l=t.extend(!0,{},t.fn.dotdotdot.defaults,e),s={},c={},u=null,g=null;return l.lastCharacter.remove instanceof Array||(l.lastCharacter.remove=t.fn.dotdotdot.defaultArrays.lastCharacter.remove),l.lastCharacter.noEllipsis instanceof Array||(l.lastCharacter.noEllipsis=t.fn.dotdotdot.defaultArrays.lastCharacter.noEllipsis),s.afterElement=h(l.after,o),s.isTruncated=!1,s.dotId=p++,o.data("dotdotdot",!0).bind_events().trigger("update.dot"),l.watch&&o.watch(),o},t.fn.dotdotdot.defaults={ellipsis:"... ",wrap:"word",fallbackToLetter:!0,lastCharacter:{},tolerance:0,callback:null,after:null,height:null,watch:!1,windowResizeFix:!0},t.fn.dotdotdot.defaultArrays={lastCharacter:{remove:[" ","　",",",";",".","!","?"],noEllipsis:[]}},t.fn.dotdotdot.debug=function(t){};var p=1,g=t.fn.html;t.fn.html=function(n){return n!=e&&!t.isFunction(n)&&this.data("dotdotdot")?this.trigger("update",[n]):g.apply(this,arguments)};var v=t.fn.text;t.fn.text=function(n){return n!=e&&!t.isFunction(n)&&this.data("dotdotdot")?(n=t("<div />").text(n).html(),this.trigger("update",[n])):v.apply(this,arguments)}}}(jQuery),jQuery(document).ready(function(t){t(".dot-ellipsis").each(function(){var e=t(this).hasClass("dot-resize-update"),n=t(this).hasClass("dot-timer-update"),r=0,o=t(this).attr("class").split(/\s+/);t.each(o,function(t,e){var n=e.match(/^dot-height-(\d+)$/);null!==n&&(r=Number(n[1]))});var a=new Object;n&&(a.watch=!0),e&&(a.watch="window"),r>0&&(a.height=r),t(this).dotdotdot(a)})}),jQuery(window).on("load",function(){jQuery(".dot-ellipsis.dot-load-update").trigger("update.dot")});
]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="jquery" javascript_name="jquery.history.js" javascript_type="framework" javascript_version="107643" javascript_position="103"><![CDATA[typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n<s;n+=1)u[n]=str(n,a)||"null";return i=u.length===0?"[]":gap?"[\n"+gap+u.join(",\n"+gap)+"\n"+o+"]":"["+u.join(",")+"]",gap=o,i}if(rep&&typeof rep=="object"){s=rep.length;for(n=0;n<s;n+=1)typeof rep[n]=="string"&&(r=rep[n],i=str(r,a),i&&u.push(quote(r)+(gap?": ":":")+i))}else for(r in a)Object.prototype.hasOwnProperty.call(a,r)&&(i=str(r,a),i&&u.push(quote(r)+(gap?": ":":")+i));return i=u.length===0?"{}":gap?"{\n"+gap+u.join(",\n"+gap)+"\n"+o+"}":"{"+u.join(",")+"}",gap=o,i}}typeof Date.prototype.toJSON!="function"&&(Date.prototype.toJSON=function(e){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(e){return this.valueOf()});var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","	":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;typeof JSON.stringify!="function"&&(JSON.stringify=function(e,t,n){var r;gap="",indent="";if(typeof n=="number")for(r=0;r<n;r+=1)indent+=" ";else typeof n=="string"&&(indent=n);rep=t;if(!t||typeof t=="function"||typeof t=="object"&&typeof t.length=="number")return str("",{"":e});throw new Error("JSON.stringify")}),typeof JSON.parse!="function"&&(JSON.parse=function(text,reviver){function walk(e,t){var n,r,i=e[t];if(i&&typeof i=="object")for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(r=walk(i,n),r!==undefined?i[n]=r:delete i[n]);return reviver.call(e,t,i)}var j;text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(e){return"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)}));if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return j=eval("("+text+")"),typeof reviver=="function"?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}(),function(e,t){"use strict";var n=e.History=e.History||{},r=e.jQuery;if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={bind:function(e,t,n){r(e).bind(t,n)},trigger:function(e,t,n){r(e).trigger(t,n)},extractEventData:function(e,n,r){var i=n&&n.originalEvent&&n.originalEvent[e]||r&&r[e]||t;return i},onDomLoad:function(e){r(e)}},typeof n.init!="undefined"&&n.init()}(window),function(e,t){"use strict";var n=e.document,r=e.setTimeout||r,i=e.clearTimeout||i,s=e.setInterval||s,o=e.History=e.History||{};if(typeof o.initHtml4!="undefined")throw new Error("History.js HTML4 Support has already been loaded...");o.initHtml4=function(){if(typeof o.initHtml4.initialized!="undefined")return!1;o.initHtml4.initialized=!0,o.enabled=!0,o.savedHashes=[],o.isLastHash=function(e){var t=o.getHashByIndex(),n;return n=e===t,n},o.isHashEqual=function(e,t){return e=encodeURIComponent(e).replace(/%25/g,"%"),t=encodeURIComponent(t).replace(/%25/g,"%"),e===t},o.saveHash=function(e){return o.isLastHash(e)?!1:(o.savedHashes.push(e),!0)},o.getHashByIndex=function(e){var t=null;return typeof e=="undefined"?t=o.savedHashes[o.savedHashes.length-1]:e<0?t=o.savedHashes[o.savedHashes.length+e]:t=o.savedHashes[e],t},o.discardedHashes={},o.discardedStates={},o.discardState=function(e,t,n){var r=o.getHashByState(e),i;return i={discardedState:e,backState:n,forwardState:t},o.discardedStates[r]=i,!0},o.discardHash=function(e,t,n){var r={discardedHash:e,backState:n,forwardState:t};return o.discardedHashes[e]=r,!0},o.discardedState=function(e){var t=o.getHashByState(e),n;return n=o.discardedStates[t]||!1,n},o.discardedHash=function(e){var t=o.discardedHashes[e]||!1;return t},o.recycleState=function(e){var t=o.getHashByState(e);return o.discardedState(e)&&delete o.discardedStates[t],!0},o.emulated.hashChange&&(o.hashChangeInit=function(){o.checkerFunction=null;var t="",r,i,u,a,f=Boolean(o.getHash());return o.isInternetExplorer()?(r="historyjs-iframe",i=n.createElement("iframe"),i.setAttribute("id",r),i.setAttribute("src","#"),i.style.display="none",n.body.appendChild(i),i.contentWindow.document.open(),i.contentWindow.document.close(),u="",a=!1,o.checkerFunction=function(){if(a)return!1;a=!0;var n=o.getHash(),r=o.getHash(i.contentWindow.document);return n!==t?(t=n,r!==n&&(u=r=n,i.contentWindow.document.open(),i.contentWindow.document.close(),i.contentWindow.document.location.hash=o.escapeHash(n)),o.Adapter.trigger(e,"hashchange")):r!==u&&(u=r,f&&r===""?o.back():o.setHash(r,!1)),a=!1,!0}):o.checkerFunction=function(){var n=o.getHash()||"";return n!==t&&(t=n,o.Adapter.trigger(e,"hashchange")),!0},o.intervalList.push(s(o.checkerFunction,o.options.hashChangeInterval)),!0},o.Adapter.onDomLoad(o.hashChangeInit)),o.emulated.pushState&&(o.onHashChange=function(t){var n=t&&t.newURL||o.getLocationHref(),r=o.getHashByUrl(n),i=null,s=null,u=null,a;return o.isLastHash(r)?(o.busy(!1),!1):(o.doubleCheckComplete(),o.saveHash(r),r&&o.isTraditionalAnchor(r)?(o.Adapter.trigger(e,"anchorchange"),o.busy(!1),!1):(i=o.extractState(o.getFullUrl(r||o.getLocationHref()),!0),o.isLastSavedState(i)?(o.busy(!1),!1):(s=o.getHashByState(i),a=o.discardedState(i),a?(o.getHashByIndex(-2)===o.getHashByState(a.forwardState)?o.back(!1):o.forward(!1),!1):(o.pushState(i.data,i.title,encodeURI(i.url),!1),!0))))},o.Adapter.bind(e,"hashchange",o.onHashChange),o.pushState=function(t,n,r,i){r=encodeURI(r).replace(/%25/g,"%");if(o.getHashByUrl(r))throw new Error("History.js does not support states with fragment-identifiers (hashes/anchors).");if(i!==!1&&o.busy())return o.pushQueue({scope:o,callback:o.pushState,args:arguments,queue:i}),!1;o.busy(!0);var s=o.createStateObject(t,n,r),u=o.getHashByState(s),a=o.getState(!1),f=o.getHashByState(a),l=o.getHash(),c=o.expectedStateId==s.id;return o.storeState(s),o.expectedStateId=s.id,o.recycleState(s),o.setTitle(s),u===f?(o.busy(!1),!1):(o.saveState(s),c||o.Adapter.trigger(e,"statechange"),!o.isHashEqual(u,l)&&!o.isHashEqual(u,o.getShortUrl(o.getLocationHref()))&&o.setHash(u,!1),o.busy(!1),!0)},o.replaceState=function(t,n,r,i){r=encodeURI(r).replace(/%25/g,"%");if(o.getHashByUrl(r))throw new Error("History.js does not support states with fragment-identifiers (hashes/anchors).");if(i!==!1&&o.busy())return o.pushQueue({scope:o,callback:o.replaceState,args:arguments,queue:i}),!1;o.busy(!0);var s=o.createStateObject(t,n,r),u=o.getHashByState(s),a=o.getState(!1),f=o.getHashByState(a),l=o.getStateByIndex(-2);return o.discardState(a,s,l),u===f?(o.storeState(s),o.expectedStateId=s.id,o.recycleState(s),o.setTitle(s),o.saveState(s),o.Adapter.trigger(e,"statechange"),o.busy(!1)):o.pushState(s.data,s.title,s.url,!1),!0}),o.emulated.pushState&&o.getHash()&&!o.emulated.hashChange&&o.Adapter.onDomLoad(function(){o.Adapter.trigger(e,"hashchange")})},typeof o.init!="undefined"&&o.init()}(window),function(e,t){"use strict";var n=e.console||t,r=e.document,i=e.navigator,s=!1,o=e.setTimeout,u=e.clearTimeout,a=e.setInterval,f=e.clearInterval,l=e.JSON,c=e.alert,h=e.History=e.History||{},p=e.history;try{s=e.sessionStorage,s.setItem("TEST","1"),s.removeItem("TEST")}catch(d){s=!1}l.stringify=l.stringify||l.encode,l.parse=l.parse||l.decode;if(typeof h.init!="undefined")throw new Error("History.js Core has already been loaded...");h.init=function(e){return typeof h.Adapter=="undefined"?!1:(typeof h.initCore!="undefined"&&h.initCore(),typeof h.initHtml4!="undefined"&&h.initHtml4(),!0)},h.initCore=function(d){if(typeof h.initCore.initialized!="undefined")return!1;h.initCore.initialized=!0,h.options=h.options||{},h.options.hashChangeInterval=h.options.hashChangeInterval||100,h.options.safariPollInterval=h.options.safariPollInterval||500,h.options.doubleCheckInterval=h.options.doubleCheckInterval||500,h.options.disableSuid=h.options.disableSuid||!1,h.options.storeInterval=h.options.storeInterval||1e3,h.options.busyDelay=h.options.busyDelay||250,h.options.debug=h.options.debug||!1,h.options.initialTitle=h.options.initialTitle||r.title,h.options.html4Mode=h.options.html4Mode||!1,h.options.delayInit=h.options.delayInit||!1,h.intervalList=[],h.clearAllIntervals=function(){var e,t=h.intervalList;if(typeof t!="undefined"&&t!==null){for(e=0;e<t.length;e++)f(t[e]);h.intervalList=null}},h.debug=function(){(h.options.debug||!1)&&h.log.apply(h,arguments)},h.log=function(){var e=typeof n!="undefined"&&typeof n.log!="undefined"&&typeof n.log.apply!="undefined",t=r.getElementById("log"),i,s,o,u,a;e?(u=Array.prototype.slice.call(arguments),i=u.shift(),typeof n.debug!="undefined"?n.debug.apply(n,[i,u]):n.log.apply(n,[i,u])):i="\n"+arguments[0]+"\n";for(s=1,o=arguments.length;s<o;++s){a=arguments[s];if(typeof a=="object"&&typeof l!="undefined")try{a=l.stringify(a)}catch(f){}i+="\n"+a+"\n"}return t?(t.value+=i+"\n-----\n",t.scrollTop=t.scrollHeight-t.clientHeight):e||c(i),!0},h.getInternetExplorerMajorVersion=function(){var e=h.getInternetExplorerMajorVersion.cached=typeof h.getInternetExplorerMajorVersion.cached!="undefined"?h.getInternetExplorerMajorVersion.cached:function(){var e=3,t=r.createElement("div"),n=t.getElementsByTagName("i");while((t.innerHTML="<!--[if gt IE "+ ++e+"]><i></i><![endif]-->")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","&lt;").replace(">","&gt;").replace(" & "," &amp; ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window)]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="jquery" javascript_name="jquery.hoverintent.js" javascript_type="framework" javascript_version="107643" javascript_position="104"><![CDATA[/*!
 * hoverIntent v1.8.1 // 2014.08.11 // jQuery v1.9.1+
 * http://briancherne.github.io/jquery-hoverIntent/
 *
 * You may use hoverIntent under the terms of the MIT license. Basically that
 * means you are free to use hoverIntent as long as this header is left intact.
 * Copyright 2007, 2014 Brian Cherne
 */
!function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):jQuery&&!jQuery.fn.hoverIntent&&a(jQuery)}(function(a){"use strict";var d,e,b={interval:100,sensitivity:6,timeout:0},c=0,f=function(a){d=a.pageX,e=a.pageY},g=function(a,b,c,h){if(Math.sqrt((c.pX-d)*(c.pX-d)+(c.pY-e)*(c.pY-e))<h.sensitivity)return b.off(c.event,f),delete c.timeoutId,c.isActive=!0,a.pageX=d,a.pageY=e,delete c.pX,delete c.pY,h.over.apply(b[0],[a]);c.pX=d,c.pY=e,c.timeoutId=setTimeout(function(){g(a,b,c,h)},h.interval)},h=function(a,b,c,d){return delete b.data("hoverIntent")[c.id],d.apply(b[0],[a])};a.fn.hoverIntent=function(d,e,i){var j=c++,k=a.extend({},b);a.isPlainObject(d)?(k=a.extend(k,d),a.isFunction(k.out)||(k.out=k.over)):k=a.isFunction(e)?a.extend(k,{over:d,out:e,selector:i}):a.extend(k,{over:d,out:d,selector:e});var l=function(b){var c=a.extend({},b),d=a(this),e=d.data("hoverIntent");e||d.data("hoverIntent",e={});var i=e[j];i||(e[j]=i={id:j}),i.timeoutId&&(i.timeoutId=clearTimeout(i.timeoutId));var l=i.event="mousemove.hoverIntent.hoverIntent"+j;if("mouseenter"===b.type){if(i.isActive)return;i.pX=c.pageX,i.pY=c.pageY,d.off(l,f).on(l,f),i.timeoutId=setTimeout(function(){g(c,d,i,k)},k.interval)}else{if(!i.isActive)return;d.off(l,f),i.timeoutId=setTimeout(function(){h(c,d,i,k.out)},k.timeout)}};return this.on({"mouseenter.hoverIntent":l,"mouseleave.hoverIntent":l},k.selector)}});]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="jquery" javascript_name="jquery.imagesloaded.js" javascript_type="framework" javascript_version="107643" javascript_position="104"><![CDATA[/*!
 * imagesLoaded PACKAGED v4.1.1
 * JavaScript is all like "You images are done yet or what?"
 * MIT License
 */

!function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},n=i[t]=i[t]||[];return-1==n.indexOf(e)&&n.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},n=i[t]=i[t]||{};return n[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=i.indexOf(e);return-1!=n&&i.splice(n,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=0,o=i[n];e=e||[];for(var r=this._onceEvents&&this._onceEvents[t];o;){var s=r&&r[o];s&&(this.off(t,o),delete r[o]),o.apply(this,e),n+=s?0:1,o=i[n]}return this}},t}),function(t,e){"use strict";"function"==typeof define&&define.amd?define(["ev-emitter/ev-emitter"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("ev-emitter")):t.imagesLoaded=e(t,t.EvEmitter)}(window,function(t,e){function i(t,e){for(var i in e)t[i]=e[i];return t}function n(t){var e=[];if(Array.isArray(t))e=t;else if("number"==typeof t.length)for(var i=0;i<t.length;i++)e.push(t[i]);else e.push(t);return e}function o(t,e,r){return this instanceof o?("string"==typeof t&&(t=document.querySelectorAll(t)),this.elements=n(t),this.options=i({},this.options),"function"==typeof e?r=e:i(this.options,e),r&&this.on("always",r),this.getImages(),h&&(this.jqDeferred=new h.Deferred),void setTimeout(function(){this.check()}.bind(this))):new o(t,e,r)}function r(t){this.img=t}function s(t,e){this.url=t,this.element=e,this.img=new Image}var h=t.jQuery,a=t.console;o.prototype=Object.create(e.prototype),o.prototype.options={},o.prototype.getImages=function(){this.images=[],this.elements.forEach(this.addElementImages,this)},o.prototype.addElementImages=function(t){"IMG"==t.nodeName&&this.addImage(t),this.options.background===!0&&this.addElementBackgroundImages(t);var e=t.nodeType;if(e&&d[e]){for(var i=t.querySelectorAll("img"),n=0;n<i.length;n++){var o=i[n];this.addImage(o)}if("string"==typeof this.options.background){var r=t.querySelectorAll(this.options.background);for(n=0;n<r.length;n++){var s=r[n];this.addElementBackgroundImages(s)}}}};var d={1:!0,9:!0,11:!0};return o.prototype.addElementBackgroundImages=function(t){var e=getComputedStyle(t);if(e)for(var i=/url\((['"])?(.*?)\1\)/gi,n=i.exec(e.backgroundImage);null!==n;){var o=n&&n[2];o&&this.addBackground(o,t),n=i.exec(e.backgroundImage)}},o.prototype.addImage=function(t){var e=new r(t);this.images.push(e)},o.prototype.addBackground=function(t,e){var i=new s(t,e);this.images.push(i)},o.prototype.check=function(){function t(t,i,n){setTimeout(function(){e.progress(t,i,n)})}var e=this;return this.progressedCount=0,this.hasAnyBroken=!1,this.images.length?void this.images.forEach(function(e){e.once("progress",t),e.check()}):void this.complete()},o.prototype.progress=function(t,e,i){this.progressedCount++,this.hasAnyBroken=this.hasAnyBroken||!t.isLoaded,this.emitEvent("progress",[this,t,e]),this.jqDeferred&&this.jqDeferred.notify&&this.jqDeferred.notify(this,t),this.progressedCount==this.images.length&&this.complete(),this.options.debug&&a&&a.log("progress: "+i,t,e)},o.prototype.complete=function(){var t=this.hasAnyBroken?"fail":"done";if(this.isComplete=!0,this.emitEvent(t,[this]),this.emitEvent("always",[this]),this.jqDeferred){var e=this.hasAnyBroken?"reject":"resolve";this.jqDeferred[e](this)}},r.prototype=Object.create(e.prototype),r.prototype.check=function(){var t=this.getIsImageComplete();return t?void this.confirm(0!==this.img.naturalWidth,"naturalWidth"):(this.proxyImage=new Image,this.proxyImage.addEventListener("load",this),this.proxyImage.addEventListener("error",this),this.img.addEventListener("load",this),this.img.addEventListener("error",this),void(this.proxyImage.src=this.img.src))},r.prototype.getIsImageComplete=function(){return this.img.complete&&void 0!==this.img.naturalWidth},r.prototype.confirm=function(t,e){this.isLoaded=t,this.emitEvent("progress",[this,this.img,e])},r.prototype.handleEvent=function(t){var e="on"+t.type;this[e]&&this[e](t)},r.prototype.onload=function(){this.confirm(!0,"onload"),this.unbindEvents()},r.prototype.onerror=function(){this.confirm(!1,"onerror"),this.unbindEvents()},r.prototype.unbindEvents=function(){this.proxyImage.removeEventListener("load",this),this.proxyImage.removeEventListener("error",this),this.img.removeEventListener("load",this),this.img.removeEventListener("error",this)},s.prototype=Object.create(r.prototype),s.prototype.check=function(){this.img.addEventListener("load",this),this.img.addEventListener("error",this),this.img.src=this.url;var t=this.getIsImageComplete();t&&(this.confirm(0!==this.img.naturalWidth,"naturalWidth"),this.unbindEvents())},s.prototype.unbindEvents=function(){this.img.removeEventListener("load",this),this.img.removeEventListener("error",this)},s.prototype.confirm=function(t,e){this.isLoaded=t,this.emitEvent("progress",[this,this.element,e])},o.makeJQueryPlugin=function(e){e=e||t.jQuery,e&&(h=e,h.fn.imagesLoaded=function(t,e){var i=new o(this,t,e);return i.jqDeferred.promise(h(this))})},o.makeJQueryPlugin(),o});]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="jquery" javascript_name="jquery.js" javascript_type="framework" javascript_version="107643" javascript_position="101"><![CDATA[/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}S.fn=S.prototype={jquery:f,constructor:S,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:function(n){return this.pushStack(S.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(S.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(S.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},S.extend=S.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(S.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||S.isPlainObject(n)?n:{},i=!1,a[t]=S.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},S.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=v.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){b(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(p(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(p(Object(e))?S.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(p(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:y}),"function"==typeof Symbol&&(S.fn[Symbol.iterator]=t[Symbol.iterator]),S.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var d=function(n){var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,v,s,c,y,S="sizzle"+1*new Date,p=n.document,k=0,r=0,m=ue(),x=ue(),A=ue(),N=ue(),D=function(e,t){return e===t&&(l=!0),0},j={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",F=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",B=new RegExp(M+"+","g"),$=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="<a id='"+S+"'></a><select id='"+S+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0<se(t,C,null,[e]).length},se.contains=function(e,t){return(e.ownerDocument||e)!=C&&T(e),y(e,t)},se.attr=function(e,t){(e.ownerDocument||e)!=C&&T(e);var n=b.attrHandle[t.toLowerCase()],r=n&&j.call(b.attrHandle,t.toLowerCase())?n(e,t,!E):void 0;return void 0!==r?r:d.attributes||!E?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},se.escape=function(e){return(e+"").replace(re,ie)},se.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},se.uniqueSort=function(e){var t,n=[],r=0,i=0;if(l=!d.detectDuplicates,u=!d.sortStable&&e.slice(0),e.sort(D),l){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)e.splice(n[r],1)}return u=null,e},o=se.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else while(t=e[r++])n+=o(t);return n},(b=se.selectors={cacheLength:50,createPseudo:le,match:G,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1<t.indexOf(i):"$="===r?i&&t.slice(-i.length)===i:"~="===r?-1<(" "+t.replace(B," ")+" ").indexOf(i):"|="===r&&(t===i||t.slice(0,i.length+1)===i+"-"))}},CHILD:function(h,e,t,g,v){var y="nth"!==h.slice(0,3),m="last"!==h.slice(-4),x="of-type"===e;return 1===g&&0===v?function(e){return!!e.parentNode}:function(e,t,n){var r,i,o,a,s,u,l=y!==m?"nextSibling":"previousSibling",c=e.parentNode,f=x&&e.nodeName.toLowerCase(),p=!n&&!x,d=!1;if(c){if(y){while(l){a=e;while(a=a[l])if(x?a.nodeName.toLowerCase()===f:1===a.nodeType)return!1;u=l="only"===h&&!u&&"nextSibling"}return!0}if(u=[m?c.firstChild:c.lastChild],m&&p){d=(s=(r=(i=(o=(a=c)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1])&&r[2],a=s&&c.childNodes[s];while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if(1===a.nodeType&&++d&&a===e){i[h]=[k,s,d];break}}else if(p&&(d=s=(r=(i=(o=(a=e)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1]),!1===d)while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if((x?a.nodeName.toLowerCase()===f:1===a.nodeType)&&++d&&(p&&((i=(o=a[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]=[k,d]),a===e))break;return(d-=v)===g||d%g==0&&0<=d/g}}},PSEUDO:function(e,o){var t,a=b.pseudos[e]||b.setFilters[e.toLowerCase()]||se.error("unsupported pseudo: "+e);return a[S]?a(o):1<a.length?(t=[e,e,"",o],b.setFilters.hasOwnProperty(e.toLowerCase())?le(function(e,t){var n,r=a(e,o),i=r.length;while(i--)e[n=P(e,r[i])]=!(t[n]=r[i])}):function(e){return a(e,0,t)}):a}},pseudos:{not:le(function(e){var r=[],i=[],s=f(e.replace($,"$1"));return s[S]?le(function(e,t,n,r){var i,o=s(e,null,r,[]),a=e.length;while(a--)(i=o[a])&&(e[a]=!(t[a]=i))}):function(e,t,n){return r[0]=e,s(r,null,n,i),r[0]=null,!i.pop()}}),has:le(function(t){return function(e){return 0<se(t,e).length}}),contains:le(function(t){return t=t.replace(te,ne),function(e){return-1<(e.textContent||o(e)).indexOf(t)}}),lang:le(function(n){return V.test(n||"")||se.error("unsupported lang: "+n),n=n.replace(te,ne).toLowerCase(),function(e){var t;do{if(t=E?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(t=t.toLowerCase())===n||0===t.indexOf(n+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var t=n.location&&n.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===a},focus:function(e){return e===C.activeElement&&(!C.hasFocus||C.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:ge(!1),disabled:ge(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!b.pseudos.empty(e)},header:function(e){return J.test(e.nodeName)},input:function(e){return Q.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:ve(function(){return[0]}),last:ve(function(e,t){return[t-1]}),eq:ve(function(e,t,n){return[n<0?n+t:n]}),even:ve(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:ve(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:ve(function(e,t,n){for(var r=n<0?n+t:t<n?t:n;0<=--r;)e.push(r);return e}),gt:ve(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=b.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})b.pseudos[e]=de(e);for(e in{submit:!0,reset:!0})b.pseudos[e]=he(e);function me(){}function xe(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function be(s,e,t){var u=e.dir,l=e.next,c=l||u,f=t&&"parentNode"===c,p=r++;return e.first?function(e,t,n){while(e=e[u])if(1===e.nodeType||f)return s(e,t,n);return!1}:function(e,t,n){var r,i,o,a=[k,p];if(n){while(e=e[u])if((1===e.nodeType||f)&&s(e,t,n))return!0}else while(e=e[u])if(1===e.nodeType||f)if(i=(o=e[S]||(e[S]={}))[e.uniqueID]||(o[e.uniqueID]={}),l&&l===e.nodeName.toLowerCase())e=e[u]||e;else{if((r=i[c])&&r[0]===k&&r[1]===p)return a[2]=r[2];if((i[c]=a)[2]=s(e,t,n))return!0}return!1}}function we(i){return 1<i.length?function(e,t,n){var r=i.length;while(r--)if(!i[r](e,t,n))return!1;return!0}:i[0]}function Te(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Ce(d,h,g,v,y,e){return v&&!v[S]&&(v=Ce(v)),y&&!y[S]&&(y=Ce(y,e)),le(function(e,t,n,r){var i,o,a,s=[],u=[],l=t.length,c=e||function(e,t,n){for(var r=0,i=t.length;r<i;r++)se(e,t[r],n);return n}(h||"*",n.nodeType?[n]:n,[]),f=!d||!e&&h?c:Te(c,s,d,n,r),p=g?y||(e?d:l||v)?[]:t:f;if(g&&g(f,p,n,r),v){i=Te(p,u),v(i,[],n,r),o=i.length;while(o--)(a=i[o])&&(p[u[o]]=!(f[u[o]]=a))}if(e){if(y||d){if(y){i=[],o=p.length;while(o--)(a=p[o])&&i.push(f[o]=a);y(null,p=[],i,r)}o=p.length;while(o--)(a=p[o])&&-1<(i=y?P(e,a):s[o])&&(e[i]=!(t[i]=a))}}else p=Te(p===t?p.splice(l,p.length):p),y?y(null,t,p,r):H.apply(t,p)})}function Ee(e){for(var i,t,n,r=e.length,o=b.relative[e[0].type],a=o||b.relative[" "],s=o?1:0,u=be(function(e){return e===i},a,!0),l=be(function(e){return-1<P(i,e)},a,!0),c=[function(e,t,n){var r=!o&&(n||t!==w)||((i=t).nodeType?u(e,t,n):l(e,t,n));return i=null,r}];s<r;s++)if(t=b.relative[e[s].type])c=[be(we(c),t)];else{if((t=b.filter[e[s].type].apply(null,e[s].matches))[S]){for(n=++s;n<r;n++)if(b.relative[e[n].type])break;return Ce(1<s&&we(c),1<s&&xe(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace($,"$1"),t,s<n&&Ee(e.slice(s,n)),n<r&&Ee(e=e.slice(n)),n<r&&xe(e))}c.push(t)}return we(c)}return me.prototype=b.filters=b.pseudos,b.setFilters=new me,h=se.tokenize=function(e,t){var n,r,i,o,a,s,u,l=x[e+" "];if(l)return t?0:l.slice(0);a=e,s=[],u=b.preFilter;while(a){for(o in n&&!(r=_.exec(a))||(r&&(a=a.slice(r[0].length)||a),s.push(i=[])),n=!1,(r=z.exec(a))&&(n=r.shift(),i.push({value:n,type:r[0].replace($," ")}),a=a.slice(n.length)),b.filter)!(r=G[o].exec(a))||u[o]&&!(r=u[o](r))||(n=r.shift(),i.push({value:n,type:o,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?se.error(e):x(e,s).slice(0)},f=se.compile=function(e,t){var n,v,y,m,x,r,i=[],o=[],a=A[e+" "];if(!a){t||(t=h(e)),n=t.length;while(n--)(a=Ee(t[n]))[S]?i.push(a):o.push(a);(a=A(e,(v=o,m=0<(y=i).length,x=0<v.length,r=function(e,t,n,r,i){var o,a,s,u=0,l="0",c=e&&[],f=[],p=w,d=e||x&&b.find.TAG("*",i),h=k+=null==p?1:Math.random()||.1,g=d.length;for(i&&(w=t==C||t||i);l!==g&&null!=(o=d[l]);l++){if(x&&o){a=0,t||o.ownerDocument==C||(T(o),n=!E);while(s=v[a++])if(s(o,t||C,n)){r.push(o);break}i&&(k=h)}m&&((o=!s&&o)&&u--,e&&c.push(o))}if(u+=l,m&&l!==u){a=0;while(s=y[a++])s(c,f,t,n);if(e){if(0<u)while(l--)c[l]||f[l]||(f[l]=q.call(r));f=Te(f)}H.apply(r,f),i&&!e&&0<f.length&&1<u+y.length&&se.uniqueSort(r)}return i&&(k=h,w=p),c},m?le(r):r))).selector=e}return a},g=se.select=function(e,t,n,r){var i,o,a,s,u,l="function"==typeof e&&e,c=!r&&h(e=l.selector||e);if(n=n||[],1===c.length){if(2<(o=c[0]=c[0].slice(0)).length&&"ID"===(a=o[0]).type&&9===t.nodeType&&E&&b.relative[o[1].type]){if(!(t=(b.find.ID(a.matches[0].replace(te,ne),t)||[])[0]))return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}i=G.needsContext.test(e)?0:o.length;while(i--){if(a=o[i],b.relative[s=a.type])break;if((u=b.find[s])&&(r=u(a.matches[0].replace(te,ne),ee.test(o[0].type)&&ye(t.parentNode)||t))){if(o.splice(i,1),!(e=r.length&&xe(o)))return H.apply(n,r),n;break}}}return(l||f(e,c))(r,t,!E,n,!t||ee.test(e)&&ye(t.parentNode)||t),n},d.sortStable=S.split("").sort(D).join("")===S,d.detectDuplicates=!!l,T(),d.sortDetached=ce(function(e){return 1&e.compareDocumentPosition(C.createElement("fieldset"))}),ce(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),d.attributes&&ce(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||fe("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ce(function(e){return null==e.getAttribute("disabled")})||fe(R,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),se}(C);S.find=d,S.expr=d.selectors,S.expr[":"]=S.expr.pseudos,S.uniqueSort=S.unique=d.uniqueSort,S.text=d.getText,S.isXMLDoc=d.isXML,S.contains=d.contains,S.escapeSelector=d.escape;var h=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&S(e).is(n))break;r.push(e)}return r},T=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},k=S.expr.match.needsContext;function A(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var N=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1<i.call(n,e)!==r}):S.filter(n,e,r)}S.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?S.find.matchesSelector(r,e)?[r]:[]:S.find.matches(e,S.grep(t,function(e){return 1===e.nodeType}))},S.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(S(e).filter(function(){for(t=0;t<r;t++)if(S.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)S.find(e,i[t],n);return 1<r?S.uniqueSort(n):n},filter:function(e){return this.pushStack(D(this,e||[],!1))},not:function(e){return this.pushStack(D(this,e||[],!0))},is:function(e){return!!D(this,"string"==typeof e&&k.test(e)?S(e):e||[],!1).length}});var j,q=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(S.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&S(e);if(!k.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?-1<a.index(n):1===n.nodeType&&S.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(1<o.length?S.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?i.call(S(e),this[0]):i.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(S.uniqueSort(S.merge(this.get(),S(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),S.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return h(e,"parentNode")},parentsUntil:function(e,t,n){return h(e,"parentNode",n)},next:function(e){return O(e,"nextSibling")},prev:function(e){return O(e,"previousSibling")},nextAll:function(e){return h(e,"nextSibling")},prevAll:function(e){return h(e,"previousSibling")},nextUntil:function(e,t,n){return h(e,"nextSibling",n)},prevUntil:function(e,t,n){return h(e,"previousSibling",n)},siblings:function(e){return T((e.parentNode||{}).firstChild,e)},children:function(e){return T(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(A(e,"template")&&(e=e.content||e),S.merge([],e.childNodes))}},function(r,i){S.fn[r]=function(e,t){var n=S.map(this,i,e);return"Until"!==r.slice(-5)&&(t=e),t&&"string"==typeof t&&(n=S.filter(t,n)),1<this.length&&(H[r]||S.uniqueSort(n),L.test(r)&&n.reverse()),this.pushStack(n)}});var P=/[^\x20\t\r\n\f]+/g;function R(e){return e}function M(e){throw e}function I(e,t,n,r){var i;try{e&&m(i=e.promise)?i.call(e).done(t).fail(n):e&&m(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}S.Callbacks=function(r){var e,n;r="string"==typeof r?(e=r,n={},S.each(e.match(P)||[],function(e,t){n[t]=!0}),n):S.extend({},r);var i,t,o,a,s=[],u=[],l=-1,c=function(){for(a=a||r.once,o=i=!0;u.length;l=-1){t=u.shift();while(++l<s.length)!1===s[l].apply(t[0],t[1])&&r.stopOnFalse&&(l=s.length,t=!1)}r.memory||(t=!1),i=!1,a&&(s=t?[]:"")},f={add:function(){return s&&(t&&!i&&(l=s.length-1,u.push(t)),function n(e){S.each(e,function(e,t){m(t)?r.unique&&f.has(t)||s.push(t):t&&t.length&&"string"!==w(t)&&n(t)})}(arguments),t&&!i&&c()),this},remove:function(){return S.each(arguments,function(e,t){var n;while(-1<(n=S.inArray(t,s,n)))s.splice(n,1),n<=l&&l--}),this},has:function(e){return e?-1<S.inArray(e,s):0<s.length},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=t="",this},disabled:function(){return!s},lock:function(){return a=u=[],t||i||(s=t=""),this},locked:function(){return!!a},fireWith:function(e,t){return a||(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),i||c()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!o}};return f},S.extend({Deferred:function(e){var o=[["notify","progress",S.Callbacks("memory"),S.Callbacks("memory"),2],["resolve","done",S.Callbacks("once memory"),S.Callbacks("once memory"),0,"resolved"],["reject","fail",S.Callbacks("once memory"),S.Callbacks("once memory"),1,"rejected"]],i="pending",a={state:function(){return i},always:function(){return s.done(arguments).fail(arguments),this},"catch":function(e){return a.then(null,e)},pipe:function(){var i=arguments;return S.Deferred(function(r){S.each(o,function(e,t){var n=m(i[t[4]])&&i[t[4]];s[t[1]](function(){var e=n&&n.apply(this,arguments);e&&m(e.promise)?e.promise().progress(r.notify).done(r.resolve).fail(r.reject):r[t[0]+"With"](this,n?[e]:arguments)})}),i=null}).promise()},then:function(t,n,r){var u=0;function l(i,o,a,s){return function(){var n=this,r=arguments,e=function(){var e,t;if(!(i<u)){if((e=a.apply(n,r))===o.promise())throw new TypeError("Thenable self-resolution");t=e&&("object"==typeof e||"function"==typeof e)&&e.then,m(t)?s?t.call(e,l(u,o,R,s),l(u,o,M,s)):(u++,t.call(e,l(u,o,R,s),l(u,o,M,s),l(u,o,R,o.notifyWith))):(a!==R&&(n=void 0,r=[e]),(s||o.resolveWith)(n,r))}},t=s?e:function(){try{e()}catch(e){S.Deferred.exceptionHook&&S.Deferred.exceptionHook(e,t.stackTrace),u<=i+1&&(a!==M&&(n=void 0,r=[e]),o.rejectWith(n,r))}};i?t():(S.Deferred.getStackHook&&(t.stackTrace=S.Deferred.getStackHook()),C.setTimeout(t))}}return S.Deferred(function(e){o[0][3].add(l(0,e,m(r)?r:R,e.notifyWith)),o[1][3].add(l(0,e,m(t)?t:R)),o[2][3].add(l(0,e,m(n)?n:M))}).promise()},promise:function(e){return null!=e?S.extend(e,a):a}},s={};return S.each(o,function(e,t){var n=t[2],r=t[5];a[t[1]]=n.add,r&&n.add(function(){i=r},o[3-e][2].disable,o[3-e][3].disable,o[0][2].lock,o[0][3].lock),n.add(t[3].fire),s[t[0]]=function(){return s[t[0]+"With"](this===s?void 0:this,arguments),this},s[t[0]+"With"]=n.fireWith}),a.promise(s),e&&e.call(s,s),s},when:function(e){var n=arguments.length,t=n,r=Array(t),i=s.call(arguments),o=S.Deferred(),a=function(t){return function(e){r[t]=this,i[t]=1<arguments.length?s.call(arguments):e,--n||o.resolveWith(r,i)}};if(n<=1&&(I(e,o.done(a(t)).resolve,o.reject,!n),"pending"===o.state()||m(i[t]&&i[t].then)))return o.then();while(t--)I(i[t],a(t),o.reject);return o.promise()}});var W=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;S.Deferred.exceptionHook=function(e,t){C.console&&C.console.warn&&e&&W.test(e.name)&&C.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},S.readyException=function(e){C.setTimeout(function(){throw e})};var F=S.Deferred();function B(){E.removeEventListener("DOMContentLoaded",B),C.removeEventListener("load",B),S.ready()}S.fn.ready=function(e){return F.then(e)["catch"](function(e){S.readyException(e)}),this},S.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--S.readyWait:S.isReady)||(S.isReady=!0)!==e&&0<--S.readyWait||F.resolveWith(E,[S])}}),S.ready.then=F.then,"complete"===E.readyState||"loading"!==E.readyState&&!E.documentElement.doScroll?C.setTimeout(S.ready):(E.addEventListener("DOMContentLoaded",B),C.addEventListener("load",B));var $=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===w(n))for(s in i=!0,n)$(e,t,s,n[s],!0,o,a);else if(void 0!==r&&(i=!0,m(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(S(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},_=/^-ms-/,z=/-([a-z])/g;function U(e,t){return t.toUpperCase()}function X(e){return e.replace(_,"ms-").replace(z,U)}var V=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function G(){this.expando=S.expando+G.uid++}G.uid=1,G.prototype={cache:function(e){var t=e[this.expando];return t||(t={},V(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[X(t)]=n;else for(r in t)i[X(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][X(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(X):(t=X(t))in r?[t]:t.match(P)||[]).length;while(n--)delete r[t[n]]}(void 0===t||S.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!S.isEmptyObject(t)}};var Y=new G,Q=new G,J=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,K=/[A-Z]/g;function Z(e,t,n){var r,i;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(K,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n="true"===(i=n)||"false"!==i&&("null"===i?null:i===+i+""?+i:J.test(i)?JSON.parse(i):i)}catch(e){}Q.set(e,t,n)}else n=void 0;return n}S.extend({hasData:function(e){return Q.hasData(e)||Y.hasData(e)},data:function(e,t,n){return Q.access(e,t,n)},removeData:function(e,t){Q.remove(e,t)},_data:function(e,t,n){return Y.access(e,t,n)},_removeData:function(e,t){Y.remove(e,t)}}),S.fn.extend({data:function(n,e){var t,r,i,o=this[0],a=o&&o.attributes;if(void 0===n){if(this.length&&(i=Q.get(o),1===o.nodeType&&!Y.get(o,"hasDataAttrs"))){t=a.length;while(t--)a[t]&&0===(r=a[t].name).indexOf("data-")&&(r=X(r.slice(5)),Z(o,r,i[r]));Y.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof n?this.each(function(){Q.set(this,n)}):$(this,function(e){var t;if(o&&void 0===e)return void 0!==(t=Q.get(o,n))?t:void 0!==(t=Z(o,n))?t:void 0;this.each(function(){Q.set(this,n,e)})},null,e,1<arguments.length,null,!0)},removeData:function(e){return this.each(function(){Q.remove(this,e)})}}),S.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=Y.get(e,t),n&&(!r||Array.isArray(n)?r=Y.access(e,t,S.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=S.queue(e,t),r=n.length,i=n.shift(),o=S._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){S.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Y.get(e,n)||Y.access(e,n,{empty:S.Callbacks("once memory").add(function(){Y.remove(e,[t+"queue",n])})})}}),S.fn.extend({queue:function(t,n){var e=2;return"string"!=typeof t&&(n=t,t="fx",e--),arguments.length<e?S.queue(this[0],t):void 0===n?this:this.each(function(){var e=S.queue(this,t,n);S._queueHooks(this,t),"fx"===t&&"inprogress"!==e[0]&&S.dequeue(this,t)})},dequeue:function(e){return this.each(function(){S.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=S.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=Y.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var ee=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,te=new RegExp("^(?:([+-])=|)("+ee+")([a-z%]*)$","i"),ne=["Top","Right","Bottom","Left"],re=E.documentElement,ie=function(e){return S.contains(e.ownerDocument,e)},oe={composed:!0};re.getRootNode&&(ie=function(e){return S.contains(e.ownerDocument,e)||e.getRootNode(oe)===e.ownerDocument});var ae=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&ie(e)&&"none"===S.css(e,"display")};function se(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return S.css(e,t,"")},u=s(),l=n&&n[3]||(S.cssNumber[t]?"":"px"),c=e.nodeType&&(S.cssNumber[t]||"px"!==l&&+u)&&te.exec(S.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)S.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,S.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var ue={};function le(e,t){for(var n,r,i,o,a,s,u,l=[],c=0,f=e.length;c<f;c++)(r=e[c]).style&&(n=r.style.display,t?("none"===n&&(l[c]=Y.get(r,"display")||null,l[c]||(r.style.display="")),""===r.style.display&&ae(r)&&(l[c]=(u=a=o=void 0,a=(i=r).ownerDocument,s=i.nodeName,(u=ue[s])||(o=a.body.appendChild(a.createElement(s)),u=S.css(o,"display"),o.parentNode.removeChild(o),"none"===u&&(u="block"),ue[s]=u)))):"none"!==n&&(l[c]="none",Y.set(r,"display",n)));for(c=0;c<f;c++)null!=l[c]&&(e[c].style.display=l[c]);return e}S.fn.extend({show:function(){return le(this,!0)},hide:function(){return le(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ae(this)?S(this).show():S(this).hide()})}});var ce,fe,pe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="<textarea>x</textarea>",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="<option></option>",y.option=!!ce.lastChild;var ge={thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n<r;n++)Y.set(e[n],"globalEval",!t||Y.get(t[n],"globalEval"))}ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td,y.option||(ge.optgroup=ge.option=[1,"<select multiple='multiple'>","</select>"]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===w(o))S.merge(p,o.nodeType?[o]:o);else if(me.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+S.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;S.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&-1<S.inArray(o,r))i&&i.push(o);else if(l=ie(o),a=ve(f.appendChild(o),"script"),l&&ye(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}var be=/^key/,we=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Te=/^([^.]*)(?:\.(.+)|)/;function Ce(){return!0}function Ee(){return!1}function Se(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function ke(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)ke(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Ee;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return S().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=S.guid++)),e.each(function(){S.event.add(this,t,i,r,n)})}function Ae(e,i,o){o?(Y.set(e,i,!1),S.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Y.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(S.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Y.set(this,i,r),t=o(this,i),this[i](),r!==(n=Y.get(this,i))||t?Y.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Y.set(this,i,{value:S.event.trigger(S.extend(r[0],S.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Y.get(e,i)&&S.event.add(e,i,Ce)}S.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.get(t);if(V(t)){n.handler&&(n=(o=n).handler,i=o.selector),i&&S.find.matchesSelector(re,i),n.guid||(n.guid=S.guid++),(u=v.events)||(u=v.events=Object.create(null)),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof S&&S.event.triggered!==e.type?S.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(P)||[""]).length;while(l--)d=g=(s=Te.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=S.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=S.event.special[d]||{},c=S.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&S.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),S.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.hasData(e)&&Y.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(P)||[""]).length;while(l--)if(d=g=(s=Te.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=S.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||S.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)S.event.remove(e,d+t[l],n,r,!0);S.isEmptyObject(u)&&Y.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=new Array(arguments.length),u=S.event.fix(e),l=(Y.get(this,"events")||Object.create(null))[u.type]||[],c=S.event.special[u.type]||{};for(s[0]=u,t=1;t<arguments.length;t++)s[t]=arguments[t];if(u.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,u)){a=S.event.handlers.call(this,u,l),t=0;while((i=a[t++])&&!u.isPropagationStopped()){u.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!u.isImmediatePropagationStopped())u.rnamespace&&!1!==o.namespace&&!u.rnamespace.test(o.namespace)||(u.handleObj=o,u.data=o.data,void 0!==(r=((S.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,s))&&!1===(u.result=r)&&(u.preventDefault(),u.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,u),u.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&1<=e.button))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?-1<S(i,this).index(l):S.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(t,e){Object.defineProperty(S.Event.prototype,t,{enumerable:!0,configurable:!0,get:m(e)?function(){if(this.originalEvent)return e(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[t]},set:function(e){Object.defineProperty(this,t,{enumerable:!0,configurable:!0,writable:!0,value:e})}})},fix:function(e){return e[S.expando]?e:new S.Event(e)},special:{load:{noBubble:!0},click:{setup:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&Ae(t,"click",Ce),!1},trigger:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&Ae(t,"click"),!0},_default:function(e){var t=e.target;return pe.test(t.type)&&t.click&&A(t,"input")&&Y.get(t,"click")||A(t,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},S.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},S.Event=function(e,t){if(!(this instanceof S.Event))return new S.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?Ce:Ee,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&S.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[S.expando]=!0},S.Event.prototype={constructor:S.Event,isDefaultPrevented:Ee,isPropagationStopped:Ee,isImmediatePropagationStopped:Ee,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=Ce,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=Ce,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=Ce,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},S.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,code:!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(e){var t=e.button;return null==e.which&&be.test(e.type)?null!=e.charCode?e.charCode:e.keyCode:!e.which&&void 0!==t&&we.test(e.type)?1&t?1:2&t?3:4&t?2:0:e.which}},S.event.addProp),S.each({focus:"focusin",blur:"focusout"},function(e,t){S.event.special[e]={setup:function(){return Ae(this,e,Se),!1},trigger:function(){return Ae(this,e),!0},delegateType:t}}),S.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,i){S.event.special[e]={delegateType:i,bindType:i,handle:function(e){var t,n=e.relatedTarget,r=e.handleObj;return n&&(n===this||S.contains(this,n))||(e.type=r.origType,t=r.handler.apply(this,arguments),e.type=i),t}}}),S.fn.extend({on:function(e,t,n,r){return ke(this,e,t,n,r)},one:function(e,t,n,r){return ke(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,S(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=Ee),this.each(function(){S.event.remove(this,e,n,t)})}});var Ne=/<script|<style|<link/i,De=/checked\s*(?:[^=]|=\s*.checked.)/i,je=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n<r;n++)S.event.add(t,i,s[i][n]);Q.hasData(e)&&(o=Q.access(e),a=S.extend({},o),Q.set(t,a))}}function Pe(n,r,i,o){r=g(r);var e,t,a,s,u,l,c=0,f=n.length,p=f-1,d=r[0],h=m(d);if(h||1<f&&"string"==typeof d&&!y.checkClone&&De.test(d))return n.each(function(e){var t=n.eq(e);h&&(r[0]=d.call(this,e,t.html())),Pe(t,r,i,o)});if(f&&(t=(e=xe(r,n[0].ownerDocument,!1,n,o)).firstChild,1===e.childNodes.length&&(e=t),t||o)){for(s=(a=S.map(ve(e,"script"),Le)).length;c<f;c++)u=e,c!==p&&(u=S.clone(u,!0,!0),s&&S.merge(a,ve(u,"script"))),i.call(n[c],u,c);if(s)for(l=a[a.length-1].ownerDocument,S.map(a,He),c=0;c<s;c++)u=a[c],he.test(u.type||"")&&!Y.access(u,"globalEval")&&S.contains(l,u)&&(u.src&&"module"!==(u.type||"").toLowerCase()?S._evalUrl&&!u.noModule&&S._evalUrl(u.src,{nonce:u.nonce||u.getAttribute("nonce")},l):b(u.textContent.replace(je,""),u,l))}return n}function Re(e,t,n){for(var r,i=t?S.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||S.cleanData(ve(r)),r.parentNode&&(n&&ie(r)&&ye(ve(r,"script")),r.parentNode.removeChild(r));return e}S.extend({htmlPrefilter:function(e){return e},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=ie(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||S.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r<i;r++)s=o[r],u=a[r],void 0,"input"===(l=u.nodeName.toLowerCase())&&pe.test(s.type)?u.checked=s.checked:"input"!==l&&"textarea"!==l||(u.defaultValue=s.defaultValue);if(t)if(n)for(o=o||ve(e),a=a||ve(c),r=0,i=o.length;r<i;r++)Oe(o[r],a[r]);else Oe(e,c);return 0<(a=ve(c,"script")).length&&ye(a,!f&&ve(e,"script")),c},cleanData:function(e){for(var t,n,r,i=S.event.special,o=0;void 0!==(n=e[o]);o++)if(V(n)){if(t=n[Y.expando]){if(t.events)for(r in t.events)i[r]?S.event.remove(n,r):S.removeEvent(n,r,t.handle);n[Y.expando]=void 0}n[Q.expando]&&(n[Q.expando]=void 0)}}}),S.fn.extend({detach:function(e){return Re(this,e,!0)},remove:function(e){return Re(this,e)},text:function(e){return $(this,function(e){return void 0===e?S.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Pe(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||qe(this,e).appendChild(e)})},prepend:function(){return Pe(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=qe(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Pe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Pe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(S.cleanData(ve(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return S.clone(this,e,t)})},html:function(e){return $(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ne.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=S.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(S.cleanData(ve(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var n=[];return Pe(this,arguments,function(e){var t=this.parentNode;S.inArray(this,n)<0&&(S.cleanData(ve(this)),t&&t.replaceChild(e,this))},n)}}),S.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,a){S.fn[e]=function(e){for(var t,n=[],r=S(e),i=r.length-1,o=0;o<=i;o++)t=o===i?this:this.clone(!0),S(r[o])[a](t),u.apply(n,t.get());return this.pushStack(n)}});var Me=new RegExp("^("+ee+")(?!px)[a-z%]+$","i"),Ie=function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=C),t.getComputedStyle(e)},We=function(e,t,n){var r,i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.call(e),t)e.style[i]=o[i];return r},Fe=new RegExp(ne.join("|"),"i");function Be(e,t,n){var r,i,o,a,s=e.style;return(n=n||Ie(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||ie(e)||(a=S.style(e,t)),!y.pixelBoxStyles()&&Me.test(a)&&Fe.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function $e(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}!function(){function e(){if(l){u.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",l.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",re.appendChild(u).appendChild(l);var e=C.getComputedStyle(l);n="1%"!==e.top,s=12===t(e.marginLeft),l.style.right="60%",o=36===t(e.right),r=36===t(e.width),l.style.position="absolute",i=12===t(l.offsetWidth/3),re.removeChild(u),l=null}}function t(e){return Math.round(parseFloat(e))}var n,r,i,o,a,s,u=E.createElement("div"),l=E.createElement("div");l.style&&(l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",y.clearCloneStyle="content-box"===l.style.backgroundClip,S.extend(y,{boxSizingReliable:function(){return e(),r},pixelBoxStyles:function(){return e(),o},pixelPosition:function(){return e(),n},reliableMarginLeft:function(){return e(),s},scrollboxSize:function(){return e(),i},reliableTrDimensions:function(){var e,t,n,r;return null==a&&(e=E.createElement("table"),t=E.createElement("tr"),n=E.createElement("div"),e.style.cssText="position:absolute;left:-11111px",t.style.height="1px",n.style.height="9px",re.appendChild(e).appendChild(t).appendChild(n),r=C.getComputedStyle(t),a=3<parseInt(r.height),re.removeChild(e)),a}}))}();var _e=["Webkit","Moz","ms"],ze=E.createElement("div").style,Ue={};function Xe(e){var t=S.cssProps[e]||Ue[e];return t||(e in ze?e:Ue[e]=function(e){var t=e[0].toUpperCase()+e.slice(1),n=_e.length;while(n--)if((e=_e[n]+t)in ze)return e}(e)||e)}var Ve=/^(none|table(?!-c[ea]).+)/,Ge=/^--/,Ye={position:"absolute",visibility:"hidden",display:"block"},Qe={letterSpacing:"0",fontWeight:"400"};function Je(e,t,n){var r=te.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function Ke(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=S.css(e,n+ne[a],!0,i)),r?("content"===n&&(u-=S.css(e,"padding"+ne[a],!0,i)),"margin"!==n&&(u-=S.css(e,"border"+ne[a]+"Width",!0,i))):(u+=S.css(e,"padding"+ne[a],!0,i),"padding"!==n?u+=S.css(e,"border"+ne[a]+"Width",!0,i):s+=S.css(e,"border"+ne[a]+"Width",!0,i));return!r&&0<=o&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u}function Ze(e,t,n){var r=Ie(e),i=(!y.boxSizingReliable()||n)&&"border-box"===S.css(e,"boxSizing",!1,r),o=i,a=Be(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if(Me.test(a)){if(!n)return a;a="auto"}return(!y.boxSizingReliable()&&i||!y.reliableTrDimensions()&&A(e,"tr")||"auto"===a||!parseFloat(a)&&"inline"===S.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===S.css(e,"boxSizing",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+Ke(e,t,n||(i?"border":"content"),o,r,a)+"px"}function et(e,t,n,r,i){return new et.prototype.init(e,t,n,r,i)}S.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Be(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=X(t),u=Ge.test(t),l=e.style;if(u||(t=Xe(s)),a=S.cssHooks[t]||S.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"===(o=typeof n)&&(i=te.exec(n))&&i[1]&&(n=se(e,t,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(S.cssNumber[s]?"":"px")),y.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=X(t);return Ge.test(t)||(t=Xe(s)),(a=S.cssHooks[t]||S.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Be(e,t,r)),"normal"===i&&t in Qe&&(i=Qe[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),S.each(["height","width"],function(e,u){S.cssHooks[u]={get:function(e,t,n){if(t)return!Ve.test(S.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?Ze(e,u,n):We(e,Ye,function(){return Ze(e,u,n)})},set:function(e,t,n){var r,i=Ie(e),o=!y.scrollboxSize()&&"absolute"===i.position,a=(o||n)&&"border-box"===S.css(e,"boxSizing",!1,i),s=n?Ke(e,u,n,a,i):0;return a&&o&&(s-=Math.ceil(e["offset"+u[0].toUpperCase()+u.slice(1)]-parseFloat(i[u])-Ke(e,u,"border",!1,i)-.5)),s&&(r=te.exec(t))&&"px"!==(r[3]||"px")&&(e.style[u]=t,t=S.css(e,u)),Je(0,t,s)}}}),S.cssHooks.marginLeft=$e(y.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Be(e,"marginLeft"))||e.getBoundingClientRect().left-We(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),S.each({margin:"",padding:"",border:"Width"},function(i,o){S.cssHooks[i+o]={expand:function(e){for(var t=0,n={},r="string"==typeof e?e.split(" "):[e];t<4;t++)n[i+ne[t]+o]=r[t]||r[t-2]||r[0];return n}},"margin"!==i&&(S.cssHooks[i+o].set=Je)}),S.fn.extend({css:function(e,t){return $(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=Ie(e),i=t.length;a<i;a++)o[t[a]]=S.css(e,t[a],!1,r);return o}return void 0!==n?S.style(e,t,n):S.css(e,t)},e,t,1<arguments.length)}}),((S.Tween=et).prototype={constructor:et,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||S.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(S.cssNumber[n]?"":"px")},cur:function(){var e=et.propHooks[this.prop];return e&&e.get?e.get(this):et.propHooks._default.get(this)},run:function(e){var t,n=et.propHooks[this.prop];return this.options.duration?this.pos=t=S.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):et.propHooks._default.set(this),this}}).init.prototype=et.prototype,(et.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=S.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){S.fx.step[e.prop]?S.fx.step[e.prop](e):1!==e.elem.nodeType||!S.cssHooks[e.prop]&&null==e.elem.style[Xe(e.prop)]?e.elem[e.prop]=e.now:S.style(e.elem,e.prop,e.now+e.unit)}}}).scrollTop=et.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},S.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},S.fx=et.prototype.init,S.fx.step={};var tt,nt,rt,it,ot=/^(?:toggle|show|hide)$/,at=/queueHooks$/;function st(){nt&&(!1===E.hidden&&C.requestAnimationFrame?C.requestAnimationFrame(st):C.setTimeout(st,S.fx.interval),S.fx.tick())}function ut(){return C.setTimeout(function(){tt=void 0}),tt=Date.now()}function lt(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=ne[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function ct(e,t,n){for(var r,i=(ft.tweeners[t]||[]).concat(ft.tweeners["*"]),o=0,a=i.length;o<a;o++)if(r=i[o].call(n,t,e))return r}function ft(o,e,t){var n,a,r=0,i=ft.prefilters.length,s=S.Deferred().always(function(){delete u.elem}),u=function(){if(a)return!1;for(var e=tt||ut(),t=Math.max(0,l.startTime+l.duration-e),n=1-(t/l.duration||0),r=0,i=l.tweens.length;r<i;r++)l.tweens[r].run(n);return s.notifyWith(o,[l,n,t]),n<1&&i?t:(i||s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l]),!1)},l=s.promise({elem:o,props:S.extend({},e),opts:S.extend(!0,{specialEasing:{},easing:S.easing._default},t),originalProperties:e,originalOptions:t,startTime:tt||ut(),duration:t.duration,tweens:[],createTween:function(e,t){var n=S.Tween(o,l.opts,e,t,l.opts.specialEasing[e]||l.opts.easing);return l.tweens.push(n),n},stop:function(e){var t=0,n=e?l.tweens.length:0;if(a)return this;for(a=!0;t<n;t++)l.tweens[t].run(1);return e?(s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l,e])):s.rejectWith(o,[l,e]),this}}),c=l.props;for(!function(e,t){var n,r,i,o,a;for(n in e)if(i=t[r=X(n)],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=S.cssHooks[r])&&"expand"in a)for(n in o=a.expand(o),delete e[r],o)n in e||(e[n]=o[n],t[n]=i);else t[r]=i}(c,l.opts.specialEasing);r<i;r++)if(n=ft.prefilters[r].call(l,o,c,l.opts))return m(n.stop)&&(S._queueHooks(l.elem,l.opts.queue).stop=n.stop.bind(n)),n;return S.map(c,ct,l),m(l.opts.start)&&l.opts.start.call(o,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),S.fx.timer(S.extend(u,{elem:o,anim:l,queue:l.opts.queue})),l}S.Animation=S.extend(ft,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return se(n.elem,e,te.exec(t),n),n}]},tweener:function(e,t){m(e)?(t=e,e=["*"]):e=e.match(P);for(var n,r=0,i=e.length;r<i;r++)n=e[r],ft.tweeners[n]=ft.tweeners[n]||[],ft.tweeners[n].unshift(t)},prefilters:[function(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&ae(e),v=Y.get(e,"fxshow");for(r in n.queue||(null==(a=S._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,S.queue(e,"fx").length||a.empty.fire()})})),t)if(i=t[r],ot.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!v||void 0===v[r])continue;g=!0}d[r]=v&&v[r]||S.style(e,r)}if((u=!S.isEmptyObject(t))||!S.isEmptyObject(d))for(r in f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=v&&v.display)&&(l=Y.get(e,"display")),"none"===(c=S.css(e,"display"))&&(l?c=l:(le([e],!0),l=e.style.display||l,c=S.css(e,"display"),le([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===S.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1,d)u||(v?"hidden"in v&&(g=v.hidden):v=Y.access(e,"fxshow",{display:l}),o&&(v.hidden=!g),g&&le([e],!0),p.done(function(){for(r in g||le([e]),Y.remove(e,"fxshow"),d)S.style(e,r,d[r])})),u=ct(g?v[r]:0,r,p),r in v||(v[r]=u.start,g&&(u.end=u.start,u.start=0))}],prefilter:function(e,t){t?ft.prefilters.unshift(e):ft.prefilters.push(e)}}),S.speed=function(e,t,n){var r=e&&"object"==typeof e?S.extend({},e):{complete:n||!n&&t||m(e)&&e,duration:e,easing:n&&t||t&&!m(t)&&t};return S.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in S.fx.speeds?r.duration=S.fx.speeds[r.duration]:r.duration=S.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){m(r.old)&&r.old.call(this),r.queue&&S.dequeue(this,r.queue)},r},S.fn.extend({fadeTo:function(e,t,n,r){return this.filter(ae).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(t,e,n,r){var i=S.isEmptyObject(t),o=S.speed(e,n,r),a=function(){var e=ft(this,S.extend({},t),o);(i||Y.get(this,"finish"))&&e.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(i,e,o){var a=function(e){var t=e.stop;delete e.stop,t(o)};return"string"!=typeof i&&(o=e,e=i,i=void 0),e&&this.queue(i||"fx",[]),this.each(function(){var e=!0,t=null!=i&&i+"queueHooks",n=S.timers,r=Y.get(this);if(t)r[t]&&r[t].stop&&a(r[t]);else for(t in r)r[t]&&r[t].stop&&at.test(t)&&a(r[t]);for(t=n.length;t--;)n[t].elem!==this||null!=i&&n[t].queue!==i||(n[t].anim.stop(o),e=!1,n.splice(t,1));!e&&o||S.dequeue(this,i)})},finish:function(a){return!1!==a&&(a=a||"fx"),this.each(function(){var e,t=Y.get(this),n=t[a+"queue"],r=t[a+"queueHooks"],i=S.timers,o=n?n.length:0;for(t.finish=!0,S.queue(this,a,[]),r&&r.stop&&r.stop.call(this,!0),e=i.length;e--;)i[e].elem===this&&i[e].queue===a&&(i[e].anim.stop(!0),i.splice(e,1));for(e=0;e<o;e++)n[e]&&n[e].finish&&n[e].finish.call(this);delete t.finish})}}),S.each(["toggle","show","hide"],function(e,r){var i=S.fn[r];S.fn[r]=function(e,t,n){return null==e||"boolean"==typeof e?i.apply(this,arguments):this.animate(lt(r,!0),e,t,n)}}),S.each({slideDown:lt("show"),slideUp:lt("hide"),slideToggle:lt("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,r){S.fn[e]=function(e,t,n){return this.animate(r,e,t,n)}}),S.timers=[],S.fx.tick=function(){var e,t=0,n=S.timers;for(tt=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||S.fx.stop(),tt=void 0},S.fx.timer=function(e){S.timers.push(e),S.fx.start()},S.fx.interval=13,S.fx.start=function(){nt||(nt=!0,st())},S.fx.stop=function(){nt=null},S.fx.speeds={slow:600,fast:200,_default:400},S.fn.delay=function(r,e){return r=S.fx&&S.fx.speeds[r]||r,e=e||"fx",this.queue(e,function(e,t){var n=C.setTimeout(e,r);t.stop=function(){C.clearTimeout(n)}})},rt=E.createElement("input"),it=E.createElement("select").appendChild(E.createElement("option")),rt.type="checkbox",y.checkOn=""!==rt.value,y.optSelected=it.selected,(rt=E.createElement("input")).value="t",rt.type="radio",y.radioValue="t"===rt.value;var pt,dt=S.expr.attrHandle;S.fn.extend({attr:function(e,t){return $(this,S.attr,e,t,1<arguments.length)},removeAttr:function(e){return this.each(function(){S.removeAttr(this,e)})}}),S.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?S.prop(e,t,n):(1===o&&S.isXMLDoc(e)||(i=S.attrHooks[t.toLowerCase()]||(S.expr.match.bool.test(t)?pt:void 0)),void 0!==n?null===n?void S.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=S.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!y.radioValue&&"radio"===t&&A(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(P);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),pt={set:function(e,t,n){return!1===t?S.removeAttr(e,n):e.setAttribute(n,n),n}},S.each(S.expr.match.bool.source.match(/\w+/g),function(e,t){var a=dt[t]||S.find.attr;dt[t]=function(e,t,n){var r,i,o=t.toLowerCase();return n||(i=dt[o],dt[o]=r,r=null!=a(e,t,n)?o:null,dt[o]=i),r}});var ht=/^(?:input|select|textarea|button)$/i,gt=/^(?:a|area)$/i;function vt(e){return(e.match(P)||[]).join(" ")}function yt(e){return e.getAttribute&&e.getAttribute("class")||""}function mt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(P)||[]}S.fn.extend({prop:function(e,t){return $(this,S.prop,e,t,1<arguments.length)},removeProp:function(e){return this.each(function(){delete this[S.propFix[e]||e]})}}),S.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&S.isXMLDoc(e)||(t=S.propFix[t]||t,i=S.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=S.find.attr(e,"tabindex");return t?parseInt(t,10):ht.test(e.nodeName)||gt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),y.optSelected||(S.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),S.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){S.propFix[this.toLowerCase()]=this}),S.fn.extend({addClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){S(this).addClass(t.call(this,e,yt(this)))});if((e=mt(t)).length)while(n=this[u++])if(i=yt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=e[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){S(this).removeClass(t.call(this,e,yt(this)))});if(!arguments.length)return this.attr("class","");if((e=mt(t)).length)while(n=this[u++])if(i=yt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=e[a++])while(-1<r.indexOf(" "+o+" "))r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(i,t){var o=typeof i,a="string"===o||Array.isArray(i);return"boolean"==typeof t&&a?t?this.addClass(i):this.removeClass(i):m(i)?this.each(function(e){S(this).toggleClass(i.call(this,e,yt(this),t),t)}):this.each(function(){var e,t,n,r;if(a){t=0,n=S(this),r=mt(i);while(e=r[t++])n.hasClass(e)?n.removeClass(e):n.addClass(e)}else void 0!==i&&"boolean"!==o||((e=yt(this))&&Y.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===i?"":Y.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&-1<(" "+vt(yt(n))+" ").indexOf(t))return!0;return!1}});var xt=/\r/g;S.fn.extend({val:function(n){var r,e,i,t=this[0];return arguments.length?(i=m(n),this.each(function(e){var t;1===this.nodeType&&(null==(t=i?n.call(this,e,S(this).val()):n)?t="":"number"==typeof t?t+="":Array.isArray(t)&&(t=S.map(t,function(e){return null==e?"":e+""})),(r=S.valHooks[this.type]||S.valHooks[this.nodeName.toLowerCase()])&&"set"in r&&void 0!==r.set(this,t,"value")||(this.value=t))})):t?(r=S.valHooks[t.type]||S.valHooks[t.nodeName.toLowerCase()])&&"get"in r&&void 0!==(e=r.get(t,"value"))?e:"string"==typeof(e=t.value)?e.replace(xt,""):null==e?"":e:void 0}}),S.extend({valHooks:{option:{get:function(e){var t=S.find.attr(e,"value");return null!=t?t:vt(S.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!A(n.parentNode,"optgroup"))){if(t=S(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=S.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=-1<S.inArray(S.valHooks.option.get(r),o))&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),S.each(["radio","checkbox"],function(){S.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=-1<S.inArray(S(e).val(),t)}},y.checkOn||(S.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),y.focusin="onfocusin"in C;var bt=/^(?:focusinfocus|focusoutblur)$/,wt=function(e){e.stopPropagation()};S.extend(S.event,{trigger:function(e,t,n,r){var i,o,a,s,u,l,c,f,p=[n||E],d=v.call(e,"type")?e.type:e,h=v.call(e,"namespace")?e.namespace.split("."):[];if(o=f=a=n=n||E,3!==n.nodeType&&8!==n.nodeType&&!bt.test(d+S.event.triggered)&&(-1<d.indexOf(".")&&(d=(h=d.split(".")).shift(),h.sort()),u=d.indexOf(":")<0&&"on"+d,(e=e[S.expando]?e:new S.Event(d,"object"==typeof e&&e)).isTrigger=r?2:3,e.namespace=h.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=null==t?[e]:S.makeArray(t,[e]),c=S.event.special[d]||{},r||!c.trigger||!1!==c.trigger.apply(n,t))){if(!r&&!c.noBubble&&!x(n)){for(s=c.delegateType||d,bt.test(s+d)||(o=o.parentNode);o;o=o.parentNode)p.push(o),a=o;a===(n.ownerDocument||E)&&p.push(a.defaultView||a.parentWindow||C)}i=0;while((o=p[i++])&&!e.isPropagationStopped())f=o,e.type=1<i?s:c.bindType||d,(l=(Y.get(o,"events")||Object.create(null))[e.type]&&Y.get(o,"handle"))&&l.apply(o,t),(l=u&&o[u])&&l.apply&&V(o)&&(e.result=l.apply(o,t),!1===e.result&&e.preventDefault());return e.type=d,r||e.isDefaultPrevented()||c._default&&!1!==c._default.apply(p.pop(),t)||!V(n)||u&&m(n[d])&&!x(n)&&((a=n[u])&&(n[u]=null),S.event.triggered=d,e.isPropagationStopped()&&f.addEventListener(d,wt),n[d](),e.isPropagationStopped()&&f.removeEventListener(d,wt),S.event.triggered=void 0,a&&(n[u]=a)),e.result}},simulate:function(e,t,n){var r=S.extend(new S.Event,n,{type:e,isSimulated:!0});S.event.trigger(r,null,t)}}),S.fn.extend({trigger:function(e,t){return this.each(function(){S.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return S.event.trigger(e,t,n,!0)}}),y.focusin||S.each({focus:"focusin",blur:"focusout"},function(n,r){var i=function(e){S.event.simulate(r,e.target,S.event.fix(e))};S.event.special[r]={setup:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r);t||e.addEventListener(n,i,!0),Y.access(e,r,(t||0)+1)},teardown:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r)-1;t?Y.access(e,r,t):(e.removeEventListener(n,i,!0),Y.remove(e,r))}}});var Tt=C.location,Ct={guid:Date.now()},Et=/\?/;S.parseXML=function(e){var t;if(!e||"string"!=typeof e)return null;try{t=(new C.DOMParser).parseFromString(e,"text/xml")}catch(e){t=void 0}return t&&!t.getElementsByTagName("parsererror").length||S.error("Invalid XML: "+e),t};var St=/\[\]$/,kt=/\r?\n/g,At=/^(?:submit|button|image|reset|file)$/i,Nt=/^(?:input|select|textarea|keygen)/i;function Dt(n,e,r,i){var t;if(Array.isArray(e))S.each(e,function(e,t){r||St.test(n)?i(n,t):Dt(n+"["+("object"==typeof t&&null!=t?e:"")+"]",t,r,i)});else if(r||"object"!==w(e))i(n,e);else for(t in e)Dt(n+"["+t+"]",e[t],r,i)}S.param=function(e,t){var n,r=[],i=function(e,t){var n=m(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!S.isPlainObject(e))S.each(e,function(){i(this.name,this.value)});else for(n in e)Dt(n,e[n],t,i);return r.join("&")},S.fn.extend({serialize:function(){return S.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=S.prop(this,"elements");return e?S.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!S(this).is(":disabled")&&Nt.test(this.nodeName)&&!At.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=S(this).val();return null==n?null:Array.isArray(n)?S.map(n,function(e){return{name:t.name,value:e.replace(kt,"\r\n")}}):{name:t.name,value:n.replace(kt,"\r\n")}}).get()}});var jt=/%20/g,qt=/#.*$/,Lt=/([?&])_=[^&]*/,Ht=/^(.*?):[ \t]*([^\r\n]*)$/gm,Ot=/^(?:GET|HEAD)$/,Pt=/^\/\//,Rt={},Mt={},It="*/".concat("*"),Wt=E.createElement("a");function Ft(o){return function(e,t){"string"!=typeof e&&(t=e,e="*");var n,r=0,i=e.toLowerCase().match(P)||[];if(m(t))while(n=i[r++])"+"===n[0]?(n=n.slice(1)||"*",(o[n]=o[n]||[]).unshift(t)):(o[n]=o[n]||[]).push(t)}}function Bt(t,i,o,a){var s={},u=t===Mt;function l(e){var r;return s[e]=!0,S.each(t[e]||[],function(e,t){var n=t(i,o,a);return"string"!=typeof n||u||s[n]?u?!(r=n):void 0:(i.dataTypes.unshift(n),l(n),!1)}),r}return l(i.dataTypes[0])||!s["*"]&&l("*")}function $t(e,t){var n,r,i=S.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&S.extend(!0,e,r),e}Wt.href=Tt.href,S.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Tt.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Tt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":It,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":S.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?$t($t(e,S.ajaxSettings),t):$t(S.ajaxSettings,e)},ajaxPrefilter:Ft(Rt),ajaxTransport:Ft(Mt),ajax:function(e,t){"object"==typeof e&&(t=e,e=void 0),t=t||{};var c,f,p,n,d,r,h,g,i,o,v=S.ajaxSetup({},t),y=v.context||v,m=v.context&&(y.nodeType||y.jquery)?S(y):S.event,x=S.Deferred(),b=S.Callbacks("once memory"),w=v.statusCode||{},a={},s={},u="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(h){if(!n){n={};while(t=Ht.exec(p))n[t[1].toLowerCase()+" "]=(n[t[1].toLowerCase()+" "]||[]).concat(t[2])}t=n[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return h?p:null},setRequestHeader:function(e,t){return null==h&&(e=s[e.toLowerCase()]=s[e.toLowerCase()]||e,a[e]=t),this},overrideMimeType:function(e){return null==h&&(v.mimeType=e),this},statusCode:function(e){var t;if(e)if(h)T.always(e[T.status]);else for(t in e)w[t]=[w[t],e[t]];return this},abort:function(e){var t=e||u;return c&&c.abort(t),l(0,t),this}};if(x.promise(T),v.url=((e||v.url||Tt.href)+"").replace(Pt,Tt.protocol+"//"),v.type=t.method||t.type||v.method||v.type,v.dataTypes=(v.dataType||"*").toLowerCase().match(P)||[""],null==v.crossDomain){r=E.createElement("a");try{r.href=v.url,r.href=r.href,v.crossDomain=Wt.protocol+"//"+Wt.host!=r.protocol+"//"+r.host}catch(e){v.crossDomain=!0}}if(v.data&&v.processData&&"string"!=typeof v.data&&(v.data=S.param(v.data,v.traditional)),Bt(Rt,v,t,T),h)return T;for(i in(g=S.event&&v.global)&&0==S.active++&&S.event.trigger("ajaxStart"),v.type=v.type.toUpperCase(),v.hasContent=!Ot.test(v.type),f=v.url.replace(qt,""),v.hasContent?v.data&&v.processData&&0===(v.contentType||"").indexOf("application/x-www-form-urlencoded")&&(v.data=v.data.replace(jt,"+")):(o=v.url.slice(f.length),v.data&&(v.processData||"string"==typeof v.data)&&(f+=(Et.test(f)?"&":"?")+v.data,delete v.data),!1===v.cache&&(f=f.replace(Lt,"$1"),o=(Et.test(f)?"&":"?")+"_="+Ct.guid+++o),v.url=f+o),v.ifModified&&(S.lastModified[f]&&T.setRequestHeader("If-Modified-Since",S.lastModified[f]),S.etag[f]&&T.setRequestHeader("If-None-Match",S.etag[f])),(v.data&&v.hasContent&&!1!==v.contentType||t.contentType)&&T.setRequestHeader("Content-Type",v.contentType),T.setRequestHeader("Accept",v.dataTypes[0]&&v.accepts[v.dataTypes[0]]?v.accepts[v.dataTypes[0]]+("*"!==v.dataTypes[0]?", "+It+"; q=0.01":""):v.accepts["*"]),v.headers)T.setRequestHeader(i,v.headers[i]);if(v.beforeSend&&(!1===v.beforeSend.call(y,T,v)||h))return T.abort();if(u="abort",b.add(v.complete),T.done(v.success),T.fail(v.error),c=Bt(Mt,v,t,T)){if(T.readyState=1,g&&m.trigger("ajaxSend",[T,v]),h)return T;v.async&&0<v.timeout&&(d=C.setTimeout(function(){T.abort("timeout")},v.timeout));try{h=!1,c.send(a,l)}catch(e){if(h)throw e;l(-1,e)}}else l(-1,"No Transport");function l(e,t,n,r){var i,o,a,s,u,l=t;h||(h=!0,d&&C.clearTimeout(d),c=void 0,p=r||"",T.readyState=0<e?4:0,i=200<=e&&e<300||304===e,n&&(s=function(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(v,T,n)),!i&&-1<S.inArray("script",v.dataTypes)&&(v.converters["text script"]=function(){}),s=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}(v,s,T,i),i?(v.ifModified&&((u=T.getResponseHeader("Last-Modified"))&&(S.lastModified[f]=u),(u=T.getResponseHeader("etag"))&&(S.etag[f]=u)),204===e||"HEAD"===v.type?l="nocontent":304===e?l="notmodified":(l=s.state,o=s.data,i=!(a=s.error))):(a=l,!e&&l||(l="error",e<0&&(e=0))),T.status=e,T.statusText=(t||l)+"",i?x.resolveWith(y,[o,l,T]):x.rejectWith(y,[T,l,a]),T.statusCode(w),w=void 0,g&&m.trigger(i?"ajaxSuccess":"ajaxError",[T,v,i?o:a]),b.fireWith(y,[T,l]),g&&(m.trigger("ajaxComplete",[T,v]),--S.active||S.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return S.get(e,t,n,"json")},getScript:function(e,t){return S.get(e,void 0,t,"script")}}),S.each(["get","post"],function(e,i){S[i]=function(e,t,n,r){return m(t)&&(r=r||n,n=t,t=void 0),S.ajax(S.extend({url:e,type:i,dataType:r,data:t,success:n},S.isPlainObject(e)&&e))}}),S.ajaxPrefilter(function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")}),S._evalUrl=function(e,t,n){return S.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){S.globalEval(e,t,n)}})},S.fn.extend({wrapAll:function(e){var t;return this[0]&&(m(e)&&(e=e.call(this[0])),t=S(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(n){return m(n)?this.each(function(e){S(this).wrapInner(n.call(this,e))}):this.each(function(){var e=S(this),t=e.contents();t.length?t.wrapAll(n):e.append(n)})},wrap:function(t){var n=m(t);return this.each(function(e){S(this).wrapAll(n?t.call(this,e):t)})},unwrap:function(e){return this.parent(e).not("body").each(function(){S(this).replaceWith(this.childNodes)}),this}}),S.expr.pseudos.hidden=function(e){return!S.expr.pseudos.visible(e)},S.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},S.ajaxSettings.xhr=function(){try{return new C.XMLHttpRequest}catch(e){}};var _t={0:200,1223:204},zt=S.ajaxSettings.xhr();y.cors=!!zt&&"withCredentials"in zt,y.ajax=zt=!!zt,S.ajaxTransport(function(i){var o,a;if(y.cors||zt&&!i.crossDomain)return{send:function(e,t){var n,r=i.xhr();if(r.open(i.type,i.url,i.async,i.username,i.password),i.xhrFields)for(n in i.xhrFields)r[n]=i.xhrFields[n];for(n in i.mimeType&&r.overrideMimeType&&r.overrideMimeType(i.mimeType),i.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest"),e)r.setRequestHeader(n,e[n]);o=function(e){return function(){o&&(o=a=r.onload=r.onerror=r.onabort=r.ontimeout=r.onreadystatechange=null,"abort"===e?r.abort():"error"===e?"number"!=typeof r.status?t(0,"error"):t(r.status,r.statusText):t(_t[r.status]||r.status,r.statusText,"text"!==(r.responseType||"text")||"string"!=typeof r.responseText?{binary:r.response}:{text:r.responseText},r.getAllResponseHeaders()))}},r.onload=o(),a=r.onerror=r.ontimeout=o("error"),void 0!==r.onabort?r.onabort=a:r.onreadystatechange=function(){4===r.readyState&&C.setTimeout(function(){o&&a()})},o=o("abort");try{r.send(i.hasContent&&i.data||null)}catch(e){if(o)throw e}},abort:function(){o&&o()}}}),S.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),S.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return S.globalEval(e),e}}}),S.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),S.ajaxTransport("script",function(n){var r,i;if(n.crossDomain||n.scriptAttrs)return{send:function(e,t){r=S("<script>").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="<form></form><form></form>",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1<s&&(r=vt(e.slice(s)),e=e.slice(0,s)),m(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),0<a.length&&S.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?S("<div>").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0<arguments.length?this.on(n,null,e,t):this.trigger(n)}});var Gt=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;S.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),m(e))return r=s.call(arguments,2),(i=function(){return e.apply(t||this,r.concat(s.call(arguments)))}).guid=e.guid=e.guid||S.guid++,i},S.holdReady=function(e){e?S.readyWait++:S.ready(!0)},S.isArray=Array.isArray,S.parseJSON=JSON.parse,S.nodeName=A,S.isFunction=m,S.isWindow=x,S.camelCase=X,S.type=w,S.now=Date.now,S.isNumeric=function(e){var t=S.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},S.trim=function(e){return null==e?"":(e+"").replace(Gt,"")},"function"==typeof define&&define.amd&&define("jquery",[],function(){return S});var Yt=C.jQuery,Qt=C.$;return S.noConflict=function(e){return C.$===S&&(C.$=Qt),e&&C.jQuery===S&&(C.jQuery=Yt),S},"undefined"==typeof e&&(C.jQuery=C.$=S),S});
]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="jquery" javascript_name="jquery.transform.js" javascript_type="framework" javascript_version="107643" javascript_position="104"><![CDATA[/*
 * transform: A jQuery cssHooks adding cross-browser 2d transform capabilities to $.fn.css() and $.fn.animate()
 *
 * limitations:
 * - requires jQuery 1.4.3+
 * - Should you use the *translate* property, then your elements need to be absolutely positionned in a relatively positionned wrapper **or it will fail in IE678**.
 * - transformOrigin is not accessible
 *
 * latest version and complete README available on Github:
 * https://github.com/louisremi/jquery.transform.js
 *
 * Copyright 2011 @louis_remi
 * Licensed under the MIT license.
 *
 * This saved you an hour of work?
 * Send me music http://www.amazon.co.uk/wishlist/HNTU0468LQON
 *
 */
(function(e,t,n,r,i){function T(t){t=t.split(")");var n=e.trim,i=-1,s=t.length-1,o,u,a,f=h?new Float32Array(6):[],l=h?new Float32Array(6):[],c=h?new Float32Array(6):[1,0,0,1,0,0];f[0]=f[3]=c[0]=c[3]=1;f[1]=f[2]=f[4]=f[5]=0;while(++i<s){o=t[i].split("(");u=n(o[0]);a=o[1];l[0]=l[3]=1;l[1]=l[2]=l[4]=l[5]=0;switch(u){case b+"X":l[4]=parseInt(a,10);break;case b+"Y":l[5]=parseInt(a,10);break;case b:a=a.split(",");l[4]=parseInt(a[0],10);l[5]=parseInt(a[1]||0,10);break;case w:a=M(a);l[0]=r.cos(a);l[1]=r.sin(a);l[2]=-r.sin(a);l[3]=r.cos(a);break;case E+"X":l[0]=+a;break;case E+"Y":l[3]=a;break;case E:a=a.split(",");l[0]=a[0];l[3]=a.length>1?a[1]:a[0];break;case S+"X":l[2]=r.tan(M(a));break;case S+"Y":l[1]=r.tan(M(a));break;case x:a=a.split(",");l[0]=a[0];l[1]=a[1];l[2]=a[2];l[3]=a[3];l[4]=parseInt(a[4],10);l[5]=parseInt(a[5],10);break}c[0]=f[0]*l[0]+f[2]*l[1];c[1]=f[1]*l[0]+f[3]*l[1];c[2]=f[0]*l[2]+f[2]*l[3];c[3]=f[1]*l[2]+f[3]*l[3];c[4]=f[0]*l[4]+f[2]*l[5]+f[4];c[5]=f[1]*l[4]+f[3]*l[5]+f[5];f=[c[0],c[1],c[2],c[3],c[4],c[5]]}return c}function N(e){var t,n,i,s=e[0],o=e[1],u=e[2],a=e[3];if(s*a-o*u){t=r.sqrt(s*s+o*o);s/=t;o/=t;i=s*u+o*a;u-=s*i;a-=o*i;n=r.sqrt(u*u+a*a);u/=n;a/=n;i/=n;if(s*a<o*u){s=-s;o=-o;i=-i;t=-t}}else{t=n=i=0}return[[b,[+e[4],+e[5]]],[w,r.atan2(o,s)],[S+"X",r.atan(i)],[E,[t,n]]]}function C(t,n){var r={start:[],end:[]},i=-1,s,o,u,a;(t=="none"||L(t))&&(t="");(n=="none"||L(n))&&(n="");if(t&&n&&!n.indexOf("matrix")&&_(t).join()==_(n.split(")")[0]).join()){r.origin=t;t="";n=n.slice(n.indexOf(")")+1)}if(!t&&!n){return}if(!t||!n||A(t)==A(n)){t&&(t=t.split(")"))&&(s=t.length);n&&(n=n.split(")"))&&(s=n.length);while(++i<s-1){t[i]&&(o=t[i].split("("));n[i]&&(u=n[i].split("("));a=e.trim((o||u)[0]);O(r.start,k(a,o?o[1]:0));O(r.end,k(a,u?u[1]:0))}}else{r.start=N(T(t));r.end=N(T(n))}return r}function k(e,t){var n=+!e.indexOf(E),r,i=e.replace(/e[XY]/,"e");switch(e){case b+"Y":case E+"Y":t=[n,t?parseFloat(t):n];break;case b+"X":case b:case E+"X":r=1;case E:t=t?(t=t.split(","))&&[parseFloat(t[0]),parseFloat(t.length>1?t[1]:e==E?r||t[0]:n+"")]:[n,n];break;case S+"X":case S+"Y":case w:t=t?M(t):0;break;case x:return N(t?_(t):[1,0,0,1,0,0]);break}return[[i,t]]}function L(e){return m.test(e)}function A(e){return e.replace(/(?:\([^)]*\))|\s/g,"")}function O(e,t,n){while(n=t.shift()){e.push(n)}}function M(e){return~e.indexOf("deg")?parseInt(e,10)*(r.PI*2/360):~e.indexOf("grad")?parseInt(e,10)*(r.PI/200):parseFloat(e)}function _(e){e=/([^,]*),([^,]*),([^,]*),([^,]*),([^,p]*)(?:px)?,([^)p]*)(?:px)?/.exec(e);return[e[1],e[2],e[3],e[4],e[5],e[6]]}var s=n.createElement("div"),o=s.style,u="Transform",a=["O"+u,"ms"+u,"Webkit"+u,"Moz"+u],f=a.length,l,c,h="Float32Array"in t,p,d,v=/Matrix([^)]*)/,m=/^\s*matrix\(\s*1\s*,\s*0\s*,\s*0\s*,\s*1\s*(?:,\s*0(?:px)?\s*){2}\)\s*$/,g="transform",y="transformOrigin",b="translate",w="rotate",E="scale",S="skew",x="matrix";while(f--){if(a[f]in o){e.support[g]=l=a[f];e.support[y]=l+"Origin";continue}}if(!l){e.support.matrixFilter=c=o.filter===""}e.cssNumber[g]=e.cssNumber[y]=true;if(l&&l!=g){e.cssProps[g]=l;e.cssProps[y]=l+"Origin";if(l=="Moz"+u){p={get:function(t,n){return n?e.css(t,l).split("px").join(""):t.style[l]},set:function(e,t){e.style[l]=/matrix\([^)p]*\)/.test(t)?t.replace(/matrix((?:[^,]*,){4})([^,]*),([^)]*)/,x+"$1$2px,$3px"):t}}}else if(/^1\.[0-5](?:\.|$)/.test(e.fn.jquery)){p={get:function(t,n){return n?e.css(t,l.replace(/^ms/,"Ms")):t.style[l]}}}}else if(c){p={get:function(t,n,r){var s=n&&t.currentStyle?t.currentStyle:t.style,o,u;if(s&&v.test(s.filter)){o=RegExp.$1.split(",");o=[o[0].split("=")[1],o[2].split("=")[1],o[1].split("=")[1],o[3].split("=")[1]]}else{o=[1,0,0,1]}if(!e.cssHooks[y]){o[4]=s?parseInt(s.left,10)||0:0;o[5]=s?parseInt(s.top,10)||0:0}else{u=e._data(t,"transformTranslate",i);o[4]=u?u[0]:0;o[5]=u?u[1]:0}return r?o:x+"("+o+")"},set:function(t,n,r){var i=t.style,s,o,u,a;if(!r){i.zoom=1}n=T(n);o=["Matrix("+"M11="+n[0],"M12="+n[2],"M21="+n[1],"M22="+n[3],"SizingMethod='auto expand'"].join();u=(s=t.currentStyle)&&s.filter||i.filter||"";i.filter=v.test(u)?u.replace(v,o):u+" progid:DXImageTransform.Microsoft."+o+")";if(!e.cssHooks[y]){if(a=e.transform.centerOrigin){i[a=="margin"?"marginLeft":"left"]=-(t.offsetWidth/2)+t.clientWidth/2+"px";i[a=="margin"?"marginTop":"top"]=-(t.offsetHeight/2)+t.clientHeight/2+"px"}i.left=n[4]+"px";i.top=n[5]+"px"}else{e.cssHooks[y].set(t,n)}}}}if(p){e.cssHooks[g]=p}d=p&&p.get||e.css;e.fx.step.transform=function(t){var n=t.elem,i=t.start,s=t.end,o=t.pos,u="",a=1e5,f,h,v,m;if(!i||typeof i==="string"){if(!i){i=d(n,l)}if(c){n.style.zoom=1}s=s.split("+=").join(i);e.extend(t,C(i,s));i=t.start;s=t.end}f=i.length;while(f--){h=i[f];v=s[f];m=+false;switch(h[0]){case b:m="px";case E:m||(m="");u=h[0]+"("+r.round((h[1][0]+(v[1][0]-h[1][0])*o)*a)/a+m+","+r.round((h[1][1]+(v[1][1]-h[1][1])*o)*a)/a+m+")"+u;break;case S+"X":case S+"Y":case w:u=h[0]+"("+r.round((h[1]+(v[1]-h[1])*o)*a)/a+"rad)"+u;break}}t.origin&&(u=t.origin+u);p&&p.set?p.set(n,u,+true):n.style[l]=u};e.transform={centerOrigin:"margin"}})(jQuery,window,document,Math)]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="linkify" javascript_name="linkify-jquery.min.js" javascript_type="framework" javascript_version="107643" javascript_position="450"><![CDATA["use strict";!function(e,t,n){var i=function(t,n){function i(e,t,n){var i=n[n.length-1];e.replaceChild(i,t);for(var r=n.length-2;r>=0;r--)e.insertBefore(n[r],i),i=n[r]}function r(e,t,n){for(var i=[],r=e,a=Array.isArray(r),o=0,r=a?r:r[Symbol.iterator]();;){var l;if(a){if(o>=r.length)break;l=r[o++]}else{if(o=r.next(),o.done)break;l=o.value}var f=l;if("nl"===f.type&&t.nl2br)i.push(n.createElement("br"));else if(f.isLink&&t.check(f)){var s=t.resolve(f),c=s.formatted,u=s.formattedHref,d=s.tagName,m=s.className,y=s.target,h=s.events,k=s.attributes,v=n.createElement(d);if(v.setAttribute("href",u),m&&v.setAttribute("class",m),y&&v.setAttribute("target",y),k)for(var g in k)v.setAttribute(g,k[g]);if(h)for(var b in h)v.addEventListener?v.addEventListener(b,h[b]):v.attachEvent&&v.attachEvent("on"+b,h[b]);v.appendChild(n.createTextNode(c)),i.push(v)}else i.push(n.createTextNode(f.toString()))}return i}function a(e,t,n){if(!e||e.nodeType!==d)throw new Error("Cannot linkify "+e+" - Invalid DOM Node type");var o=t.ignoreTags;if("A"===e.tagName||s.contains(o,e.tagName))return e;for(var l=e.firstChild;l;){switch(l.nodeType){case d:a(l,t,n);break;case m:var c=l.nodeValue,y=f(c);if(0===y.length||1===y.length&&y[0]instanceof u)break;var h=r(y,t,n);i(e,l,h),l=h[h.length-1]}l=l.nextSibling}return e}function o(t,n){var i=arguments.length>2&&void 0!==arguments[2]&&arguments[2];try{i=i||document||e&&e.document||global&&global.document}catch(r){}if(!i)throw new Error("Cannot find document implementation. If you are in a non-browser environment like Node.js, pass the document implementation as the third argument to linkifyElement.");return n=new c(n),a(t,n,i)}function l(t){function n(e){return e=o.normalize(e),this.each(function(){o.helper(this,e,i)})}var i=arguments.length>1&&void 0!==arguments[1]&&arguments[1];t.fn=t.fn||{};try{i=i||document||e&&e.document||global&&global.document}catch(r){}if(!i)throw new Error("Cannot find document implementation. If you are in a non-browser environment like Node.js, pass the document implementation as the second argument to linkify/jquery");"function"!=typeof t.fn.linkify&&(t.fn.linkify=n,t(i).ready(function(){t("[data-linkify]").each(function(){var e=t(this),n=e.data(),i=n.linkify,r=n.linkifyNlbr,a={attributes:n.linkifyAttributes,defaultProtocol:n.linkifyDefaultProtocol,events:n.linkifyEvents,format:n.linkifyFormat,formatHref:n.linkifyFormatHref,nl2br:!!r&&0!==r&&"false"!==r,tagName:n.linkifyTagname,target:n.linkifyTarget,className:n.linkifyClassName||n.linkifyLinkclass,validate:n.linkifyValidate,ignoreTags:n.linkifyIgnoreTags},o="this"===i?e:e.find(i);o.linkify(a)})}))}t="default"in t?t["default"]:t;var f=n.tokenize,s=n.options,c=s.Options,u=n.parser.TOKENS.TEXT,d=1,m=3;o.helper=a,o.normalize=function(e){return new c(e)};try{!define&&(e.linkifyElement=o)}catch(y){}return l}(n,t);"function"!=typeof n.fn.linkify&&i(n)}(window,linkify,jQuery);]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="linkify" javascript_name="linkify.min.js" javascript_type="framework" javascript_version="107643" javascript_position="400"><![CDATA[!function(){"use strict";var t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};!function(e){function n(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=Object.create(t.prototype);for(var a in n)o[a]=n[a];return o.constructor=e,e.prototype=o,e}function o(t){t=t||{},this.defaultProtocol=t.defaultProtocol||h.defaultProtocol,this.events=t.events||h.events,this.format=t.format||h.format,this.formatHref=t.formatHref||h.formatHref,this.nl2br=t.nl2br||h.nl2br,this.tagName=t.tagName||h.tagName,this.target=t.target||h.target,this.validate=t.validate||h.validate,this.ignoreTags=[],this.attributes=t.attributes||t.linkAttributes||h.attributes,this.className=t.className||t.linkClass||h.className;for(var e=t.ignoreTags||h.ignoreTags,n=0;n<e.length;n++)this.ignoreTags.push(e[n].toUpperCase())}function a(t,e){for(var n=0;n<t.length;n++)if(t[n]===e)return!0;return!1}function r(t){return t}function i(t,e){return"url"===e?"_blank":null}function s(){return function(t){this.j=[],this.T=t||null}}function c(t,e,n,o){for(var a=0,r=t.length,i=e,s=[],c=void 0;a<r&&(c=i.next(t[a]));)i=c,a++;if(a>=r)return[];for(;a<r-1;)c=new m(o),s.push(c),i.on(t[a],c),i=c,a++;return c=new m(n),s.push(c),i.on(t[r-1],c),s}function l(){return function(t){t&&(this.v=t)}}function u(t){var e=t?{v:t}:{};return n(b,l(),e)}function p(t){return t instanceof v||t instanceof R}var h={defaultProtocol:"http",events:null,format:r,formatHref:r,nl2br:!1,tagName:"a",target:i,validate:!0,ignoreTags:[],attributes:null,className:"linkified"};o.prototype={resolve:function(t){var e=t.toHref(this.defaultProtocol);return{formatted:this.get("format",t.toString(),t),formattedHref:this.get("formatHref",e,t),tagName:this.get("tagName",e,t),className:this.get("className",e,t),target:this.get("target",e,t),events:this.getObject("events",e,t),attributes:this.getObject("attributes",e,t)}},check:function(t){return this.get("validate",t.toString(),t)},get:function(e,n,o){var a=this[e];if(!a)return a;switch("undefined"==typeof a?"undefined":t(a)){case"function":return a(n,o.type);case"object":var r=a[o.type]||h[e];return"function"==typeof r?r(n,o.type):r}return a},getObject:function(t,e,n){var o=this[t];return"function"==typeof o?o(e,n.type):o}};var g=Object.freeze({defaults:h,Options:o,contains:a}),f=s();f.prototype={defaultTransition:!1,on:function(t,e){if(t instanceof Array){for(var n=0;n<t.length;n++)this.j.push([t[n],e]);return this}return this.j.push([t,e]),this},next:function(t){for(var e=0;e<this.j.length;e++){var n=this.j[e],o=n[0],a=n[1];if(this.test(t,o))return a}return this.defaultTransition},accepts:function(){return!!this.T},test:function(t,e){return t===e},emit:function(){return this.T}};var m=n(f,s(),{test:function(t,e){return t===e||e instanceof RegExp&&e.test(t)}}),d=n(f,s(),{jump:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=this.next(new t(""));return n===this.defaultTransition?(n=new this.constructor(e),this.on(t,n)):e&&(n.T=e),n},test:function(t,e){return t instanceof e}}),b=l();b.prototype={toString:function(){return this.v+""}};var v=u(),y=u("@"),k=u(":"),w=u("."),j=u(),x=u(),z=u("\n"),O=u(),S=u("+"),N=u("#"),T=u(),A=u("mailto:"),L=u("?"),E=u("/"),C=u("_"),P=u(),R=u(),q=u(),H=u("{"),B=u("["),U=u("<"),M=u("("),D=u("}"),I=u("]"),K=u(">"),_=u(")"),G=u("&"),Y=Object.freeze({Base:b,DOMAIN:v,AT:y,COLON:k,DOT:w,PUNCTUATION:j,LOCALHOST:x,NL:z,NUM:O,PLUS:S,POUND:N,QUERY:L,PROTOCOL:T,MAILTO:A,SLASH:E,UNDERSCORE:C,SYM:P,TLD:R,WS:q,OPENBRACE:H,OPENBRACKET:B,OPENANGLEBRACKET:U,OPENPAREN:M,CLOSEBRACE:D,CLOSEBRACKET:I,CLOSEANGLEBRACKET:K,CLOSEPAREN:_,AMPERSAND:G}),Q="aaa|aarp|abb|abbott|abogado|ac|academy|accenture|accountant|accountants|aco|active|actor|ad|adac|ads|adult|ae|aeg|aero|af|afl|ag|agency|ai|aig|airforce|airtel|al|alibaba|alipay|allfinanz|alsace|am|amica|amsterdam|an|analytics|android|ao|apartments|app|apple|aq|aquarelle|ar|aramco|archi|army|arpa|arte|as|asia|associates|at|attorney|au|auction|audi|audio|author|auto|autos|avianca|aw|ax|axa|az|azure|ba|baidu|band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bb|bbc|bbva|bcg|bcn|bd|be|beats|beer|bentley|berlin|best|bet|bf|bg|bh|bharti|bi|bible|bid|bike|bing|bingo|bio|biz|bj|black|blackfriday|bloomberg|blue|bm|bms|bmw|bn|bnl|bnpparibas|bo|boats|boehringer|bom|bond|boo|book|boots|bosch|bostik|bot|boutique|br|bradesco|bridgestone|broadway|broker|brother|brussels|bs|bt|budapest|bugatti|build|builders|business|buy|buzz|bv|bw|by|bz|bzh|ca|cab|cafe|cal|call|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|cc|cd|ceb|center|ceo|cern|cf|cfa|cfd|cg|ch|chanel|channel|chase|chat|cheap|chloe|christmas|chrome|church|ci|cipriani|circle|cisco|citic|city|cityeats|ck|cl|claims|cleaning|click|clinic|clinique|clothing|cloud|club|clubmed|cm|cn|co|coach|codes|coffee|college|cologne|com|commbank|community|company|compare|computer|comsec|condos|construction|consulting|contact|contractors|cooking|cool|coop|corsica|country|coupon|coupons|courses|cr|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cu|cuisinella|cv|cw|cx|cy|cymru|cyou|cz|dabur|dad|dance|date|dating|datsun|day|dclk|de|dealer|deals|degree|delivery|dell|deloitte|delta|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount|dj|dk|dm|dnp|do|docs|dog|doha|domains|download|drive|dubai|durban|dvag|dz|earth|eat|ec|edeka|edu|education|ee|eg|email|emerck|energy|engineer|engineering|enterprises|epson|equipment|er|erni|es|esq|estate|et|eu|eurovision|eus|events|everbank|exchange|expert|exposed|express|fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|fast|feedback|ferrero|fi|film|final|finance|financial|firestone|firmdale|fish|fishing|fit|fitness|fj|fk|flickr|flights|florist|flowers|flsmidth|fly|fm|fo|foo|football|ford|forex|forsale|forum|foundation|fox|fr|fresenius|frl|frogans|frontier|fund|furniture|futbol|fyi|ga|gal|gallery|gallup|game|garden|gb|gbiz|gd|gdn|ge|gea|gent|genting|gf|gg|ggee|gh|gi|gift|gifts|gives|giving|gl|glass|gle|global|globo|gm|gmail|gmbh|gmo|gmx|gn|gold|goldpoint|golf|goo|goog|google|gop|got|gov|gp|gq|gr|grainger|graphics|gratis|green|gripe|group|gs|gt|gu|gucci|guge|guide|guitars|guru|gw|gy|hamburg|hangout|haus|hdfcbank|health|healthcare|help|helsinki|here|hermes|hiphop|hitachi|hiv|hk|hm|hn|hockey|holdings|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hr|hsbc|ht|hu|hyundai|ibm|icbc|ice|icu|id|ie|ifm|iinet|il|im|immo|immobilien|in|industries|infiniti|info|ing|ink|institute|insurance|insure|int|international|investments|io|ipiranga|iq|ir|irish|is|iselect|ist|istanbul|it|itau|iwc|jaguar|java|jcb|je|jetzt|jewelry|jlc|jll|jm|jmp|jo|jobs|joburg|jot|joy|jp|jpmorgan|jprs|juegos|kaufen|kddi|ke|kerryhotels|kerrylogistics|kerryproperties|kfh|kg|kh|ki|kia|kim|kinder|kitchen|kiwi|km|kn|koeln|komatsu|kp|kpn|kr|krd|kred|kuokgroup|kw|ky|kyoto|kz|la|lacaixa|lamborghini|lamer|lancaster|land|landrover|lanxess|lasalle|lat|latrobe|law|lawyer|lb|lc|lds|lease|leclerc|legal|lexus|lgbt|li|liaison|lidl|life|lifeinsurance|lifestyle|lighting|like|limited|limo|lincoln|linde|link|live|living|lixil|lk|loan|loans|local|locus|lol|london|lotte|lotto|love|lr|ls|lt|ltd|ltda|lu|lupin|luxe|luxury|lv|ly|ma|madrid|maif|maison|makeup|man|management|mango|market|marketing|markets|marriott|mba|mc|md|me|med|media|meet|melbourne|meme|memorial|men|menu|meo|mg|mh|miami|microsoft|mil|mini|mk|ml|mm|mma|mn|mo|mobi|mobily|moda|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar|mp|mq|mr|ms|mt|mtn|mtpc|mtr|mu|museum|mutuelle|mv|mw|mx|my|mz|na|nadex|nagoya|name|natura|navy|nc|ne|nec|net|netbank|network|neustar|new|news|nexus|nf|ng|ngo|nhk|ni|nico|nikon|ninja|nissan|nl|no|nokia|norton|nowruz|np|nr|nra|nrw|ntt|nu|nyc|nz|obi|office|okinawa|om|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|origins|osaka|otsuka|ovh|pa|page|pamperedchef|panerai|paris|pars|partners|parts|party|passagens|pe|pet|pf|pg|ph|pharmacy|philips|photo|photography|photos|physio|piaget|pics|pictet|pictures|pid|pin|ping|pink|pizza|pk|pl|place|play|playstation|plumbing|plus|pm|pn|pohl|poker|porn|post|pr|praxi|press|pro|prod|productions|prof|promo|properties|property|protection|ps|pt|pub|pw|pwc|py|qa|qpon|quebec|quest|racing|re|read|realtor|realty|recipes|red|redstone|redumbrella|rehab|reise|reisen|reit|ren|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rexroth|rich|ricoh|rio|rip|ro|rocher|rocks|rodeo|room|rs|rsvp|ru|ruhr|run|rw|rwe|ryukyu|sa|saarland|safe|safety|sakura|sale|salon|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|sas|saxo|sb|sbs|sc|sca|scb|schaeffler|schmidt|scholarships|school|schule|schwarz|science|scor|scot|sd|se|seat|security|seek|select|sener|services|seven|sew|sex|sexy|sfr|sg|sh|sharp|shell|shia|shiksha|shoes|show|shriram|si|singles|site|sj|sk|ski|skin|sky|skype|sl|sm|smile|sn|sncf|so|soccer|social|softbank|software|sohu|solar|solutions|song|sony|soy|space|spiegel|spot|spreadbetting|sr|srl|st|stada|star|starhub|statefarm|statoil|stc|stcgroup|stockholm|storage|store|studio|study|style|su|sucks|supplies|supply|support|surf|surgery|suzuki|sv|swatch|swiss|sx|sy|sydney|symantec|systems|sz|tab|taipei|taobao|tatamotors|tatar|tattoo|tax|taxi|tc|tci|td|team|tech|technology|tel|telecity|telefonica|temasek|tennis|tf|tg|th|thd|theater|theatre|tickets|tienda|tiffany|tips|tires|tirol|tj|tk|tl|tm|tmall|tn|to|today|tokyo|tools|top|toray|toshiba|total|tours|town|toyota|toys|tp|tr|trade|trading|training|travel|travelers|travelersinsurance|trust|trv|tt|tube|tui|tunes|tushu|tv|tvs|tw|tz|ua|ubs|ug|uk|unicom|university|uno|uol|us|uy|uz|va|vacations|vana|vc|ve|vegas|ventures|verisign|versicherung|vet|vg|vi|viajes|video|viking|villas|vin|vip|virgin|vision|vista|vistaprint|viva|vlaanderen|vn|vodka|volkswagen|vote|voting|voto|voyage|vu|vuelos|wales|walter|wang|wanggou|watch|watches|weather|weatherchannel|webcam|weber|website|wed|wedding|weir|wf|whoswho|wien|wiki|williamhill|win|windows|wine|wme|wolterskluwer|work|works|world|ws|wtc|wtf|xbox|xerox|xin|xperia|xxx|xyz|yachts|yahoo|yamaxun|yandex|ye|yodobashi|yoga|yokohama|youtube|yt|za|zara|zero|zip|zm|zone|zuerich|zw".split("|"),W="0123456789".split(""),X="0123456789abcdefghijklmnopqrstuvwxyz".split(""),Z=[" ","\f","\r","\t","\x0B"," "," ","᠎"],F=[],J=function(t){return new m(t)},V=J(),$=J(O),tt=J(v),et=J(),nt=J(q);V.on("@",J(y)).on(".",J(w)).on("+",J(S)).on("#",J(N)).on("?",J(L)).on("/",J(E)).on("_",J(C)).on(":",J(k)).on("{",J(H)).on("[",J(B)).on("<",J(U)).on("(",J(M)).on("}",J(D)).on("]",J(I)).on(">",J(K)).on(")",J(_)).on("&",J(G)).on([",",";","!",'"',"'"],J(j)),V.on("\n",J(z)).on(Z,nt),nt.on(Z,nt);for(var ot=0;ot<Q.length;ot++){var at=c(Q[ot],V,R,v);F.push.apply(F,at)}var rt=c("file",V,v,v),it=c("ftp",V,v,v),st=c("http",V,v,v),ct=c("mailto",V,v,v);F.push.apply(F,rt),F.push.apply(F,it),F.push.apply(F,st);var lt=rt.pop(),ut=it.pop(),pt=st.pop(),ht=ct.pop(),gt=J(v),ft=J(T),mt=J(A);ut.on("s",gt).on(":",ft),pt.on("s",gt).on(":",ft),F.push(gt),lt.on(":",ft),gt.on(":",ft),ht.on(":",mt);var dt=c("localhost",V,x,v);F.push.apply(F,dt),V.on(W,$),$.on("-",et).on(W,$).on(X,tt),tt.on("-",et).on(X,tt);for(var bt=0;bt<F.length;bt++)F[bt].on("-",et).on(X,tt);et.on("-",et).on(W,tt).on(X,tt),V.defaultTransition=J(P);var vt=function(t){for(var e=t.replace(/[A-Z]/g,function(t){return t.toLowerCase()}),n=t.length,o=[],a=0;a<n;){for(var r=V,i=null,s=null,c=0,l=null,u=-1;a<n&&(s=r.next(e[a]));)i=null,r=s,r.accepts()?(u=0,l=r):u>=0&&u++,c++,a++;if(!(u<0)){a-=u,c-=u;var p=l.emit();o.push(new p(t.substr(a-c,c)))}}return o},yt=V,kt=Object.freeze({State:m,TOKENS:Y,run:vt,start:yt}),wt=l();wt.prototype={type:"token",isLink:!1,toString:function(){for(var t=[],e=0;e<this.v.length;e++)t.push(this.v[e].toString());return t.join("")},toHref:function(){return this.toString()},toObject:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"http";return{type:this.type,value:this.toString(),href:this.toHref(t)}}};var jt=n(wt,l(),{type:"email",isLink:!0}),xt=n(wt,l(),{type:"email",isLink:!0,toHref:function(){this.v;return"mailto:"+this.toString()}}),zt=n(wt,l(),{type:"text"}),Ot=n(wt,l(),{type:"nl"}),St=n(wt,l(),{type:"url",isLink:!0,toHref:function(){for(var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"http",e=!1,n=!1,o=this.v,a=[],r=0;o[r]instanceof T;)e=!0,a.push(o[r].toString().toLowerCase()),r++;for(;o[r]instanceof E;)n=!0,a.push(o[r].toString()),r++;for(;p(o[r]);)a.push(o[r].toString().toLowerCase()),r++;for(;r<o.length;r++)a.push(o[r].toString());return a=a.join(""),e||n||(a=t+"://"+a),a},hasProtocol:function(){return this.v[0]instanceof T}}),Nt=Object.freeze({Base:wt,MAILTOEMAIL:jt,EMAIL:xt,NL:Ot,TEXT:zt,URL:St}),Tt=function(t){return new d(t)},At=Tt(),Lt=Tt(),Et=Tt(),Ct=Tt(),Pt=Tt(),Rt=Tt(),qt=Tt(),Ht=Tt(St),Bt=Tt(),Ut=Tt(St),Mt=Tt(St),Dt=Tt(),It=Tt(),Kt=Tt(),_t=Tt(),Gt=Tt(),Yt=Tt(St),Qt=Tt(St),Wt=Tt(St),Xt=Tt(St),Zt=Tt(),Ft=Tt(),Jt=Tt(),Vt=Tt(),$t=Tt(),te=Tt(),ee=Tt(xt),ne=Tt(),oe=Tt(xt),ae=Tt(jt),re=Tt(),ie=Tt(),se=Tt(),ce=Tt(),le=Tt(Ot);At.on(z,le).on(T,Lt).on(A,Et).on(E,Ct),Lt.on(E,Ct),Ct.on(E,Pt),At.on(R,Rt).on(v,Rt).on(x,Ht).on(O,Rt),Pt.on(R,Mt).on(v,Mt).on(O,Mt).on(x,Mt),Rt.on(w,qt),$t.on(w,te),qt.on(R,Ht).on(v,Rt).on(O,Rt).on(x,Rt),te.on(R,ee).on(v,$t).on(O,$t).on(x,$t),Ht.on(w,qt),ee.on(w,te),Ht.on(k,Bt).on(E,Mt),Bt.on(O,Ut),Ut.on(E,Mt),ee.on(k,ne),ne.on(O,oe);var ue=[v,y,x,O,S,N,T,E,R,C,P,G],pe=[k,w,L,j,D,I,K,_,H,B,U,M];Mt.on(H,It).on(B,Kt).on(U,_t).on(M,Gt),Dt.on(H,It).on(B,Kt).on(U,_t).on(M,Gt),It.on(D,Mt),Kt.on(I,Mt),_t.on(K,Mt),Gt.on(_,Mt),Yt.on(D,Mt),Qt.on(I,Mt),Wt.on(K,Mt),Xt.on(_,Mt),Zt.on(D,Mt),Ft.on(I,Mt),Jt.on(K,Mt),Vt.on(_,Mt),It.on(ue,Yt),Kt.on(ue,Qt),_t.on(ue,Wt),Gt.on(ue,Xt),It.on(pe,Zt),Kt.on(pe,Ft),_t.on(pe,Jt),Gt.on(pe,Vt),Yt.on(ue,Yt),Qt.on(ue,Qt),Wt.on(ue,Wt),Xt.on(ue,Xt),Yt.on(pe,Yt),Qt.on(pe,Qt),Wt.on(pe,Wt),Xt.on(pe,Xt),Zt.on(ue,Yt),Ft.on(ue,Qt),Jt.on(ue,Wt),Vt.on(ue,Xt),Zt.on(pe,Zt),Ft.on(pe,Ft),Jt.on(pe,Jt),Vt.on(pe,Vt),Mt.on(ue,Mt),Dt.on(ue,Mt),Mt.on(pe,Dt),Dt.on(pe,Dt),Et.on(R,ae).on(v,ae).on(O,ae).on(x,ae),ae.on(ue,ae).on(pe,re),re.on(ue,ae).on(pe,re);var he=[v,O,S,N,L,C,P,G,R];Rt.on(he,ie).on(y,se),Ht.on(he,ie).on(y,se),qt.on(he,ie),ie.on(he,ie).on(y,se).on(w,ce),ce.on(he,ie),se.on(R,$t).on(v,$t).on(x,ee);var ge=function(t){for(var e=t.length,n=0,o=[],a=[];n<e;){for(var r=At,i=null,s=null,c=0,l=null,u=-1;n<e&&!(i=r.next(t[n]));)a.push(t[n++]);for(;n<e&&(s=i||r.next(t[n]));)i=null,r=s,r.accepts()?(u=0,l=r):u>=0&&u++,n++,c++;if(u<0)for(var p=n-c;p<n;p++)a.push(t[p]);else{a.length>0&&(o.push(new zt(a)),a=[]),n-=u,c-=u;var h=l.emit();o.push(new h(t.slice(n-c,n)))}}return a.length>0&&o.push(new zt(a)),o},fe=Object.freeze({State:d,TOKENS:Nt,run:ge,start:At});Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)});var me=function(t){return ge(vt(t))},de=function(t){for(var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=me(t),o=[],a=0;a<n.length;a++){var r=n[a];!r.isLink||e&&r.type!==e||o.push(r.toObject())}return o},be=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=me(t);return 1===n.length&&n[0].isLink&&(!e||n[0].type===e)};e.find=de,e.inherits=n,e.options=g,e.parser=fe,e.scanner=kt,e.test=be,e.tokenize=me}(self.linkify=self.linkify||{})}();]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="mixins" javascript_name="ips.core.files.moderate.js" javascript_type="mixins" javascript_version="107643" javascript_position="1000600"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.files.moderate.js - Mixin to update moderator form endpoint when table sorting changes
 *
 * Author: bfarber
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.mixin( 'file.moderate', 'core.global.core.table', false, function () {

		/**
		 * After we handle state changes, check the URL and adjust the moderation form
		 *
		 * @returns {void}
		 */
		this.after('_updateSort', function () {
			var current = this._getSortValue();

			var formAction = ips.utils.url.removeParams( [ 'sortby', 'sortdirection', 'listResort' ], this.scope.find('[data-role="moderationTools"]').attr('action') );
			this.scope.find('[data-role="moderationTools"]').attr( 'action', formAction + '&listResort=1&sortby=' + current.by + '&sortdirection=' + current.order );
		});
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="mixins" javascript_name="ips.core.groups.counts.js" javascript_type="mixins" javascript_version="107643" javascript_position="1000600"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.groups.counts.js - Mixin to fetch group counts in ACP
 *
 * Author: bfarber
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.mixin('group.counts', 'core.global.core.table', true, function () {

		/**
		 * After init, init
		 *
		 * @returns {void}
		 */
		this.after('setup', function () {
			this.getGroupCounts();
			$(document).on( 'tableRowsUpdated', _.bind( this.getGroupCounts, this ) );
		});

		/**
		 * Get the group count
		 *
		 * @returns {void}
		 */
		this.getGroupCounts = function() {
			this.scope.find('[data-ipsGroupCount].ipsLoading').each( function(){
				var element = $(this);
				var groupId = element.attr('data-ipsGroupId');

				ips.getAjax()( '?app=core&module=members&controller=groups&do=getCount&group=' + groupId )
					.done( function (response) {
						element
							.removeClass( 'ipsLoading' )
							.removeClass( 'ipsLoading_tiny' )
							.html( response );

						// Inform the document
						$( document ).trigger( 'contentChange', [ element ] );
					});
			} );
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="mixins" javascript_name="ips.core.table.js" javascript_type="mixins" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.table.js - ACP mixin for tables 
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.mixin('acpTable', 'core.global.core.table', true, function () {

		this._timer = null;
		this._searchField = null;
		this._curSearchValue = '';
		this._currentValue = '';

		/**
		 * Add acp-specific events
		 *
		 * @returns {void}
		 */
		this.after('initialize', function () {
			this.on( 'focus', '[data-role="tableSearch"]', this.startLiveSearch );
			this.on( 'blur', '[data-role="tableSearch"]', this.endLiveSearch );
			this.on( 'click', '[data-action="tableSort"]', this.changeSorting );
			this.on( 'paginationClicked', this.adminPaginationClicked );
			this.on( 'menuItemSelected', '#elSortMenu', this.sortByMenu );
			this.on( 'menuItemSelected', '#elOrderMenu', this.orderByMenu );
		});

		/**
		 * Set up search
		 *
		 * @returns {void}
		 */
		this.before('setup', function () {			
			this._searchField = this.scope.find('[data-role="tableSearch"]');
			this.scope.find('[data-role="tableSearch"]').removeClass('ipsHide').show();			
		});

		/**
		 * Mixin for _getUrlParams, adding our current search value
		 *
		 * @returns {object}	Extended object including search value
		 */
		this.around('_getUrlParams', function (origFn) {
			return _.extend( origFn(), {
				quicksearch: this._getSearchValue() || ''
			});
		});

		/**
		 * Updates the sorting order classnames
		 *
		 * @param 	{object} 	data 	Sort data
		 * @returns {void}
		 */
		this.after('_updateSort', function (data) {
			var directions = 'ipsTable_sortableAsc ipsTable_sortableDesc';
			
			// Do the cell headers
			this.scope
				.find('[data-role="table"] [data-action="tableSort"]')
					.removeClass('ipsTable_sortableActive')
					.removeAttr('aria-sort')
				.end()
				.find('[data-action="tableSort"][data-key="' + data.by + '"]')
					.addClass('ipsTable_sortableActive')
					.removeClass( directions )
					.addClass( 'ipsTable_sortable' + data.order.charAt(0).toUpperCase() + data.order.slice(1) )
					.attr( 'aria-sort', ( data.order == 'asc' ) ? 'ascending' : 'descending' );

			// Do the menus
			$('#elSortMenu_menu, #elOrderMenu_menu')
				.find('.ipsMenu_item')
					.removeClass('ipsMenu_itemChecked')
				.end()
				.find('[data-ipsMenuValue="' + data.by + '"], [data-ipsMenuValue="' + data.order + '"]')
					.addClass('ipsMenu_itemChecked');
		});	

		/**
		 * Mixin for _handleStateChange, checking for an updated search value
		 *
		 * @returns {object}	Extended object including search value
		 */
		this.after('_handleStateChanges', function (state) {
			if( !_.isUndefined( state.data.quicksearch ) && state.data.quicksearch != this._urlParams.quicksearch ){
				this._updateSearch( state.data.quicksearch );
			}
		});

		/**
		 * Scroll to pagination when clicked
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		this.adminPaginationClicked = function () {
			var wrappingDialog = this.scope.closest('.ipsDialog');

			// Get top postition of table
			var elemPosition = ips.utils.position.getElemPosition( wrappingDialog.length ? wrappingDialog : this.scope );
			$('html, body').animate( { scrollTop: ( elemPosition.absPos.top - 60 ) + 'px' } );
		};

		/**
		 * Handles events from the sort menu (shown only on mobile)
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		this.sortByMenu = function (e, data) {
			data.originalEvent.preventDefault();

			this._updateSort( {
				by: data.selectedItemID
			});
		};

		/**
		 * Handles events from the order menu (shown only on mobile)
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		this.orderByMenu = function (e, data) {
			data.originalEvent.preventDefault();

			this._updateSort( {
				order: data.selectedItemID
			});
		};

		/**
		 * Event handler for choosing new sort column/order
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		this.changeSorting = function (e) {
			e.preventDefault();
			var cell = $( e.currentTarget );
			var order = '';

			// Apply asc or desc classnames to the cell, depending on its current state
			if( cell.hasClass('ipsTable_sortableActive') ){
				order = ( cell.hasClass('ipsTable_sortableDesc') ) ? 'asc' : 'desc';
			} else {
				order = ( cell.hasClass('ipsTable_sortableDesc') ) ? 'desc' : 'asc';
			}

			this._updateSort( {
				by: cell.attr('data-key'),
				order: order
			});
		};

		/**
		 * Focus event handler for live search box
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		this.startLiveSearch = function (e) {
			this._timer = setInterval( _.bind( this._checkSearchValue, this ), 500 );
		};

		/**
		 * Blur event handler for live search box
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		this.endLiveSearch = function (e) {
			clearInterval( this._timer );
		};

		/**
		 * Determines whether the search field value has changed from the last loop run,
		 * and updates the URL if it has
		 *
		 * @returns {void}
		 */
		this._checkSearchValue = function () {
			var val = this._searchField.val();

			if( this._currentValue != val ){
				this.updateURL({
					quicksearch: val,
					page: 1
				});

				this._currentValue = val;
			}
		};
		
		/**
		 * Updates the search field with a provided value
		 *
		 * @param	{string} 	searchValue 		Value to update
		 * @returns {void}
		 */
		this._updateSearch = function (searchValue) {
			this._searchField.val( searchValue );
		};

		/**
		 * Updates element classnames for filtering
		 *
		 * @param	{string} 	newFilter 		Filter ID of new filter to select
		 * @returns {void}
		 */
		this._updateFilter = function (newFilter) {
			this.scope
				.find('[data-role="tableSortBar"] [data-action="tableFilter"] a')
					.removeClass('ipsButtonRow_active')
				.end()
				.find('[data-action="tableFilter"][data-filter="' + newFilter + '"] a')
					.addClass('ipsButtonRow_active');
		};

		/**
		 * Returns the current sort by and sort order value
		 *
		 * @returns {object}	Object containing by and order keys
		 */
		this._getSortValue = function () {
			var sortBy = this.scope.find('[data-role="table"] thead .ipsTable_sortable.ipsTable_sortableActive');			
			var sortOrder = 'desc';
			if( sortBy.hasClass('ipsTable_sortableAsc') ){
				sortOrder = 'asc';
			}

			return { by: sortBy.attr('data-key'), order: sortOrder };
		};

		/**
		 * Returns the current filter value
		 *
		 * @returns {string}
		 */
		this._getFilterValue = function () {
			var sortBar = this.scope.find('[data-role="tableSortBar"]');

			if( !sortBar.length ){
				return '';
			}

			return sortBar.find('.ipsButtonRow_active').closest('[data-filter]').attr('data-filter');
		};

		/**
		 * Gets the current search value, either from the URL or contents of the search box
		 *
		 * @returns {string}
		 */
		this._getSearchValue = function () {
			if( ips.utils.url.getParam('quicksearch') ){
				return ips.utils.url.getParam('quicksearch');
			}

			return this.scope.find('[data-role="tableSearch"]').val();
		};
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="mixins" javascript_name="ips.core.table.js" javascript_type="mixins" javascript_version="107643" javascript_position="1000100"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.table.js - Front-end mixin for tables 
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.mixin('contentListing', 'core.global.core.table', true, function () {

		this._rowSelector = 'li';

		/**
		 * Adds front-end table events
		 *
		 * @returns {void}
		 */
		this.after('initialize', function () {
			//this.on( 'submit', '[data-role="moderationTools"]', this.moderationSubmit );
			this.on( 'menuItemSelected', '[data-role="sortButton"]', this.changeSorting );
			this.on( 'change', '[data-role="moderation"]', this.selectRow );
			this.on( 'click', '[data-action="markAsRead"]', this.markAsRead );
			this.on( 'paginationClicked', this.frontPaginationClicked );
			this.on( 'markTableRead', this.markAllRead );

			$( document ).on( 'markTableRowRead', _.bind( this.markRowRead, this ) );
			$( document ).on( 'markAllRead', _.bind( this.markAllRead, this ) );
			$( document ).on( 'updateTableURL', _.bind( this.updateTableURL, this ) );
			$( document ).on( 'moderationSubmitted', _.bind( this.clearLocalStorage, this ) );
		});

		this.after('setup', function () {
			this._tableID = this.scope.attr('data-tableID');
			this._storeID = 'table-' + this._tableID;

			if( this.scope.attr('data-dummyLoadingSelector') ){
				this._rowSelector = this.scope.attr('data-dummyLoadingSelector');
			}

			this._findCheckedRows();
		});

		/**
		 * Handle state changes (called after we've already verified this controller *should* handle this state change)
		 *
		 * @param 	{object} 	state  History state object
		 * @returns {void}
		 */
		this.before('_handleStateChanges', function (state) {
			ips.utils.analytics.trackPageView( state.url );
		});

		/**
		 * Show the table as loading before the ajax
		 *
		 * @returns {void}
		 */
		this.before('_getResults', function () {
			this._setTableLoading( true );
		});

		/**
		 * Switch off table loading after results are fetched
		 *
		 * @returns {void}
		 */
		this.after('_getResultsAlways', function () {
			this._setTableLoading( false );
		});

		/**
		 * After the table is updated, check for any pageAction widgets and refresh them
		 *
		 * @returns {void}
		 */
		this.after('_updateTable', function () {
			this.scope.find('[data-ipsPageAction]').trigger('refresh.pageAction');
			this.scope.find('[data-role="tableRows"]')
				.css({ opacity: "0.0001" })
				.animate({
					opacity: "1"
				});

			this._findCheckedRows();
		});

		/**
		 * Checks localStorage and checks any rows that we've previously selected in this topic
		 *
		 * @returns {void}
		 */
		this._findCheckedRows = function () {
			if( !this.scope.find('input[type="checkbox"]').length ){
				return;
			}

			// Fetch the checked comments for this feedID
			var dataStore = ips.utils.db.get( 'moderation', this._storeID ) || {};
			var self = this;
			var pageAction = this.scope.find('[data-ipsPageAction]');

			if( _.size( dataStore ) ){
				var sizeOtherPage = 0;

				_.each( dataStore, function (val, key) {
					if( self.scope.find('[data-rowid="' + key + '"]').length ){
						self.scope
							.find('[data-rowid="' + key + '"]')
								.addClass( 'ipsDataItem_selected' )
								.find('input[type="checkbox"][data-role="moderation"]')
									.attr( 'checked', true )
									.trigger('change');
					} else {
						sizeOtherPage++;

						pageAction.trigger('addManualItem.pageAction', {
							id: 'moderate[' + key + ']',
							actions: val
						});
					}
				});

				if( this.scope.find('[data-ipsAutoCheck]') )
				{
					this.scope.find('[data-ipsAutoCheck]').trigger( 'setInitialCount.autoCheck', { count: sizeOtherPage } );
				}
			}
		};

		/**
         * Clear local storage after form is submitted
         *
         * @returns {void}
         */
        this.clearLocalStorage = function () {
            ips.utils.db.remove( 'moderation', this._storeID );
        };

		/**
		 * Prevent the default loading throbber from being shown here
		 *
		 * @returns {void}
		 */
		this._showLoading = function () {
			return _.isUndefined( this.scope.attr('data-dummyLoading') );
		};

		/**
		 * Marks everything in this table as read
		 *
		 * @returns {void}
		 */
		this.markAllRead = function () {
			this.scope
				.find('.ipsDataItem, .ipsDataItem_subList .ipsDataItem_unread')
					.removeClass('ipsDataItem_unread')
					.find('.ipsItemStatus')
						.addClass('ipsItemStatus_read');
		};

		/**
		 * Marks a row in this table read
		 *
		 * @returns {void}
		 */
		this.markRowRead = function (e, data) {

			// Make sure we're working on the right table
			if( _.isUndefined( data.tableID ) || data.tableID != this._tableID ){
				return;
			}

			// Update row
			this.scope
				.find('[data-rowID="' + data.rowID + '"]')
					.removeClass('ipsDataItem_unread')
					.find('.ipsItemStatus')
						.addClass('ipsItemStatus_read');
		};
		
		/**
		 * Update the table URL from an external source
		 *
		 * @returns {void}
		 */
		this.updateTableURL = function (e, data) {

			this.updateURL( data );
		};

		/**
		 * Scroll to pagination when clicked
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		this.frontPaginationClicked = function () {
			var wrappingDialog = this.scope.closest('.ipsDialog');

			// Get top postition of table
			var elemPosition = ips.utils.position.getElemPosition( wrappingDialog.length ? wrappingDialog : this.scope );
			$('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' } );
		};

		/**
		 * Toggles classes when the moderation checkbox is checked
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		this.selectRow = function (e) {
			var row = $( e.currentTarget ).closest('.ipsDataItem');
			var rowID = row.attr('data-rowID');
			var dataStore = ips.utils.db.get( 'moderation', this._storeID ) || {};
			var rowActions = row.find('[data-role="moderation"]').attr('data-actions');

			// Toggle the row styling
			row.toggleClass( 'ipsDataItem_selected', $( e.currentTarget ).is(':checked') );

			// Add it to our dataStore object which will go into localstorage
			if( $( e.currentTarget ).is(':checked') ){
				if( _.isUndefined( dataStore[ rowID ] ) ){
					dataStore[ rowID ] = rowActions;
				}
			} else {
				delete dataStore[ rowID ];
			}

			// Store the updated value, or delete if it's empty  now
			if( _.size( dataStore ) ){
				ips.utils.db.set( 'moderation', this._storeID, dataStore );	
			} else {
				ips.utils.db.remove( 'moderation', this._storeID );
			}
		};

		/**
		 * Mark as read functionality for table rows
		 *
		 * @param	{event} 	e 		Event object
		 * @returns {void}
		 */
		this.markAsRead = function (e) {
			e.preventDefault();

			var self = this;
			var item = $( e.currentTarget );
			var url = item.attr('href');

			var execMark = function () {
				// Update row
				var row = item.closest('.ipsDataItem');
				row.removeClass('ipsDataItem_unread').find('.ipsItemStatus').addClass('ipsItemStatus_read');
				row.find('.ipsDataItem_subList .ipsDataItem_unread').removeClass('ipsDataItem_unread');

				ips.utils.anim.go('fadeOut', $('#ipsTooltip'));
				item.removeAttr('data-ipstooltip').removeAttr('title');

				// Mark as read on server
				ips.getAjax()(url, {
					bypassRedirect: true
				})
					.done(function (response) {
						item.trigger('markedAsRead');
					})
					.fail(function () {
						// Reset styles
						item
							.closest('.ipsDataItem')
							.addClass('ipsDataItem_unread')
							.find('.ipsItemStatus')
							.removeClass('ipsItemStatus_read');

						ips.ui.alert.show({
							type: 'alert',
							icon: 'error',
							message: ips.getString('errorMarkingRead'),
							callbacks: {
								ok: function () {
								}
							}
						});
					});
			};

			if( ips.utils.events.isTouchDevice() ) {
				ips.ui.alert.show( {
					type: 'confirm',
					icon: 'question',
					message: ips.getString('notificationMarkAsRead'),
					callbacks: {
						ok: function () {
							execMark();
						}
					}
				});
			} else {
				execMark();
			}
		};

		/**
		 * Update the content and pagination elements
		 *
		 * @param	{object} 	response 		JSON object containing new HTML pieces
		 * @returns {void}
		 */
		this._setTableLoading = function (loading) {
			var rowElem = this.scope.find('[data-role="tableRows"]');
			var rows = rowElem.find('> ' + this._rowSelector);
			
			if( _.isUndefined( this.scope.attr('data-dummyLoading') ) ){
				this._basicLoading( loading );
				return;
			}

			if( !loading || !rowElem.length || !rows.length ){
				return;
			}

			var template = 'table.row.loading';

			if( this.scope.attr('data-dummyLoadingTemplate') ){
				template = this.scope.attr('data-dummyLoadingTemplate');
			}

			var newRows = [];

			// Build an array of rendered rows that we'll insert in one go
			for( var i = 0; i <= rows.length; i++ ){
				var rnd = parseInt( Math.random() * (10 - 1) + 1 );
				newRows.push( ips.templates.render( template, { extraClass: this.scope.attr('data-dummyLoadingClass') || '', rnd: rnd } ) );
			}

			this.scope.find('[data-role="tableRows"]').html( newRows.join('') );
		};

		/**
		 * Show a loading spinner over the top of the existing rows
		 *
		 * @param	{object} 	response 		JSON object containing new HTML pieces
		 * @returns {void}
		 */
		this._basicLoading = function ( loading ) {
			var rowElem = this.scope.find('[data-role="tableRows"]');

			// Make sure we actually have a tableRows element
			if( !rowElem.length )
			{
				return;
			}

			if( !this._tableOverlay ){
				this._tableOverlay = $('<div/>').addClass('ipsLoading').hide();
				ips.getContainer().append( this._tableOverlay );
			}

			if( loading ){
				// Get dims & position			
				var dims = ips.utils.position.getElemDims( rowElem );
				var position = ips.utils.position.getElemPosition( rowElem );

				this._tableOverlay.show().css({
					left: position.viewportOffset.left + 'px',
					top: position.viewportOffset.top + $( document ).scrollTop() + 'px',
					width: dims.width + 'px',
					height: dims.height + 'px',
					position: 'absolute',
					zIndex: ips.ui.zIndex()
				});

				rowElem.css({
					opacity: "0.5"
				});
			} else {
				rowElem.animate({
					opacity: "1"
				});

				this._tableOverlay.hide();
			}
		};

		/**
		 * Change the sorting
		 *
		 * @param	{event} 	e 		Event object
		 * @param	{object} 	data	Event data object
		 * @returns {void}
		 */
		this.changeSorting = function (e, data) {
			data.originalEvent.preventDefault();
			
			// Don't sort if there's no sort value on this item
			if( _.isUndefined( data.selectedItemID ) ){
				return;
			}

			var current = this._getSortValue();
			var menuItem = data.menuElem.find('[data-ipsMenuValue="' + data.selectedItemID + '"]');
			var sortBy = data.selectedItemID;
			var sortDirection = current.order;

			// Does this option also have a direction?
			if( menuItem.attr('data-sortDirection') ){
				sortDirection = menuItem.attr('data-sortDirection');
			}

			this.updateURL( {
				sortby: sortBy,
				sortdirection: sortDirection,
				page: 1
			});
		};

		/**
		 * Updates element classnames for filtering
		 *
		 * @param	{string} 	newFilter 		Filter ID of new filter to select
		 * @returns {void}
		 */
		this._updateFilter = function (newFilter) {
			// This space left intentionally blank
		};

		/**
		 * Returns the current sort by and sort order value
		 *
		 * @returns {object}	Object containing by and order keys
		 */
		this._getSortValue = function () {
			var by = ips.utils.url.getParam('sortby');
			var order = ips.utils.url.getParam('sortdirection');

			return { by: by || '', order: order || '' };
		};

		/**
		 * Returns the current sort by and sort order value
		 *
		 * @returns {object}	Object containing by and order keys
		 */
		this._getFilterValue = function () {
			var filter = ips.utils.url.getParam('filter');
			return filter || '';
		};
	
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="front" javascript_path="models/core" javascript_name="ips.core.comment.js" javascript_type="model" javascript_version="107643" javascript_position="1000050">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.core.comment.js - Comment model
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.model.register('core.comment', {

		initialize: function () {
			this.on( 'getEditForm.comment', this.getEditForm );
			this.on( 'saveEditComment.comment', this.saveEditComment );
			this.on( 'deleteComment.comment', this.deleteComment );
			this.on( 'newComment.comment', this.newComment );
			this.on( 'approveComment.comment', this.approveComment );
			this.on( 'unrecommendComment.comment', this.unrecommendComment );
		},
				
		/**
		 * Retrieves edit form
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object}	data 	Event data object
		 * @returns 	{void}
		 */
		getEditForm: function (e, data) {
			this.getData( {
				url: data.url,
				dataType: 'html',
				data: {},
				events: 'getEditForm',
				namespace: 'comment'
			}, data);
		},

		/**
		 * Saves edit back to server
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object}	data 	Event data object
		 * @returns 	{void}
		 */
		saveEditComment: function (e, data) {
			var url = data.url;
			
			this.getData( {
				url: data.url,
				dataType: 'html',
				type: 'post',
				data: data.form || {},
				events: 'saveEditComment',
				namespace: 'comment'
			}, data);
		},

		/**
		 * Approves comment
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object}	data 	Event data object
		 * @returns 	{void}
		 */
		approveComment: function (e, data) {
			this.getData( {
				url: data.url,
				dataType: 'html',
				data: data.form || {},
				events: 'approveComment',
				namespace: 'comment'
			}, data);
		},

		/**
		 * Unrecommend this comment
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object}	data 	Event data object
		 * @returns 	{void}
		 */
		unrecommendComment: function (e, data) {
			this.getData( {
				url: data.url,
				dataType: 'json',
				data: data.form || {},
				events: 'unrecommendComment',
				namespace: 'comment'
			}, data);
		},

		/**
		 * Deletes comment
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object}	data 	Event data object
		 * @returns 	{void}
		 */
		deleteComment: function (e, data) {
			this.getData( {
				url: data.url,
				dataType: 'html',
				data: data.form || {},
				events: 'deleteComment',
				namespace: 'comment'
			}, data);
		},

		/**
		 * Adds a new comment
		 *	
		 * @param 		{event} 	e 		Event object
		 * @param 		{object}	data 	Event data object
		 * @returns 	{void}
		 */
		newComment: function (e, data) {
			this.getData( {
				url: data.url,
				dataType: 'json',
				data: data.form || {},
				events: 'newComment',
				namespace: 'comment'
			}, data);
		}
	});
}(jQuery, _));</file>
 <file javascript_app="core" javascript_location="front" javascript_path="models/messages" javascript_name="ips.messages.folder.js" javascript_type="model" javascript_version="107643" javascript_position="1000200"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.messages.folder.js - Message folders model
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.model.register('messages.folder', {

		initialize: function () {
			this.on( 'loadFolder.messages', this.loadFolder );
			this.on( 'addFolder.messages', this.addFolder );
			this.on( 'renameFolder.messages', this.renameFolder );
			this.on( 'markFolder.messages', this.markFolder );
			this.on( 'emptyFolder.messages', this.emptyFolder );
			this.on( 'searchFolder.messages', this.searchFolder );
			/*this.on( 'markFolderRead.messages', this.markFolderRead );
			this.on( 'emptyFolder.messages', this.emptyFolder );*/
			this.on( 'deleteFolder.messages', this.deleteFolder );
			this.on( 'deleteMessages.messages', this.deleteMessages );
		},

		searchFolder: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger',
				dataType: 'json',
				data: data,
				events: 'searchFolder',
				namespace: 'messages'
			}, data);
		},

		loadFolder: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger',
				dataType: 'json',
				data: {
					folder: data.folder,
					sortBy: data.sortBy,
					filter: data.filter,
					overview: 1
				},
				events: 'loadFolder',
				namespace: 'messages'
			}, data);
		},

		addFolder: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger&do=addFolder',
				dataType: 'json',
				data: {
					messenger_add_folder_name: data.name,
					form_submitted: 1
				},
				events: 'addFolder',
				namespace: 'messages'
			}, data);
		},

		renameFolder: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger&do=renameFolder',
				dataType: 'json',
				data: {
					folder: data.folder,
					messenger_add_folder_name: data.name,
					form_submitted: 1
				},
				events: 'renameFolder',
				namespace: 'messages'
			}, data);
		},

		markFolder: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger&do=readFolder',
				dataType: 'html',
				data: {
					folder: data.folder,
					form_submitted: 1
				},
				events: 'markFolder',
				namespace: 'messages'
			}, data);
		},

		emptyFolder: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger&do=emptyFolder',
				dataType: 'json',
				data: {
					folder: data.folder,
					form_submitted: 1
				},
				events: 'emptyFolder',
				namespace: 'messages'
			}, data);
		},

		deleteFolder: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger&do=deleteFolder',
				dataType: 'json',
				data: {
					folder: data.folder,
					form_submitted: 1,
					wasConfirmed: 1
				},
				events: 'deleteFolder',
				namespace: 'messages'
			}, data);
		},

		deleteMessages: function (e, data) {
			this.getData({
				url: 'app=core&module=messaging&controller=messenger&do=leaveConversation',
				dataType: 'json',
				data: {
					id: data.id
				},
				events: 'deleteMessages',
				namespace: 'messages'
			}, data);
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="models/messages" javascript_name="ips.messages.message.js" javascript_type="model" javascript_version="107643" javascript_position="1000200"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.messages.message.js - Messages model for messenger
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.model.register('messages.message', {

		initialize: function () {
			this.on( 'fetchMessage.messages', this.fetchMessage );
			this.on( 'deleteMessage.messages', this.deleteMessage );
			this.on( 'moveMessage.messages', this.moveMessage );
			this.on( 'blockUser.messages', this.blockUser );
			this.on( 'addUser.messages', this.addUser );
		},

		fetchMessage: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger',
				dataType: 'html',
				data: {
					id: data.id,
					page: data.page || 1
				},
				events: 'loadMessage',
				namespace: 'messages'
			}, data );
		},

		deleteMessage: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger&do=leaveConversation',
				dataType: 'json',
				data: {
					id: data.id
				},
				events: 'deleteMessage',
				namespace: 'messages'
			}, data );
		},

		moveMessage: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger&do=move',
				dataType: 'json',
				data: {
					id: data.id,
					to: data.folder
				},
				events: 'moveMessage',
				namespace: 'messages'
			}, data );
		},

		blockUser: function (e, data) {
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger&do=blockParticipant',
				dataType: 'html',
				data: {
					id: data.id,
					member: data.member
				},
				events: 'blockUser',
				namespace: 'messages'
			}, data );
		},

		addUser: function (e, data) {
			var sendData = {
				id: data.id
			};

			if( data.names ){
				_.extend( sendData, {
					member_names: data.names
				});
			}

			if( data.member ){
				_.extend( sendData, {
					member: data.member
				});
			}
			
			if( data.unblock ){
				_.extend( sendData, {
					unblock: true
				});
			}
			
			this.getData( {
				url: 'app=core&module=messaging&controller=messenger&do=addParticipant',
				dataType: 'json',
				data: sendData,
				events: 'addUser',
				namespace: 'messages'
			}, data );
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="mustache" javascript_name="mustache.js" javascript_type="framework" javascript_version="107643" javascript_position="150"><![CDATA[/*!
 * mustache.js - Logic-less {{mustache}} templates with JavaScript
 * http://github.com/janl/mustache.js
 */

/*global define: false Mustache: true*/
(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i<valueLength;++i){chr=value.charAt(i);if(isWhitespace(chr)){spaces.push(tokens.length);indentation+=chr}else{nonSpace=true;lineHasNonSpace=true;indentation+=" "}tokens.push(["text",chr,start,start+1]);start+=1;if(chr==="\n"){stripSpace();indentation="";tagIndex=0;lineHasNonSpace=false}}}if(!scanner.scan(openingTagRe))break;hasTag=true;type=scanner.scan(tagRe)||"name";scanner.scan(whiteRe);if(type==="="){value=scanner.scanUntil(equalsRe);scanner.scan(equalsRe);scanner.scanUntil(closingTagRe)}else if(type==="{"){value=scanner.scanUntil(closingCurlyRe);scanner.scan(curlyRe);scanner.scanUntil(closingTagRe);type="&"}else{value=scanner.scanUntil(closingTagRe)}if(!scanner.scan(closingTagRe))throw new Error("Unclosed tag at "+scanner.pos);if(type==">"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i<numTokens;++i){token=tokens[i];if(token){if(token[0]==="text"&&lastToken&&lastToken[0]==="text"){lastToken[1]+=token[1];lastToken[3]=token[3]}else{squashedTokens.push(token);lastToken=token}}}return squashedTokens}function nestTokens(tokens){var nestedTokens=[];var collector=nestedTokens;var sections=[];var token,section;for(var i=0,numTokens=tokens.length;i<numTokens;++i){token=tokens[i];switch(token[0]){case"#":case"^":collector.push(token);sections.push(token);collector=token[4]=[];break;case"/":section=sections.pop();section[5]=token[2];collector=sections.length>0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index<names.length){if(index===names.length-1)lookupHit=hasProperty(intermediateValue,names[index])||primitiveHasOwnProperty(intermediateValue,names[index]);intermediateValue=intermediateValue[names[index++]]}}else{intermediateValue=context.view[name];lookupHit=hasProperty(context.view,name)}if(lookupHit){value=intermediateValue;break}context=context.parent}cache[name]=value}if(isFunction(value))value=value.call(this.view);return value};function Writer(){this.templateCache={_cache:{},set:function set(key,value){this._cache[key]=value},get:function get(key){return this._cache[key]},clear:function clear(){this._cache={}}}}Writer.prototype.clearCache=function clearCache(){if(typeof this.templateCache!=="undefined"){this.templateCache.clear()}};Writer.prototype.parse=function parse(template,tags){var cache=this.templateCache;var cacheKey=template+":"+(tags||mustache.tags).join(":");var isCacheEnabled=typeof cache!=="undefined";var tokens=isCacheEnabled?cache.get(cacheKey):undefined;if(tokens==undefined){tokens=parseTemplate(template,tags);isCacheEnabled&&cache.set(cacheKey,tokens)}return tokens};Writer.prototype.render=function render(template,view,partials,config){var tags=this.getConfigTags(config);var tokens=this.parse(template,tags);var context=view instanceof Context?view:new Context(view,undefined);return this.renderTokens(tokens,context,partials,template,config)};Writer.prototype.renderTokens=function renderTokens(tokens,context,partials,originalTemplate,config){var buffer="";var token,symbol,value;for(var i=0,numTokens=tokens.length;i<numTokens;++i){value=undefined;token=tokens[i];symbol=token[0];if(symbol==="#")value=this.renderSection(token,context,partials,originalTemplate,config);else if(symbol==="^")value=this.renderInverted(token,context,partials,originalTemplate,config);else if(symbol===">")value=this.renderPartial(token,context,partials,config);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context,config);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate,config){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials,config)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j<valueLength;++j){buffer+=this.renderTokens(token[4],context.push(value[j]),partials,originalTemplate,config)}}else if(typeof value==="object"||typeof value==="string"||typeof value==="number"){buffer+=this.renderTokens(token[4],context.push(value),partials,originalTemplate,config)}else if(isFunction(value)){if(typeof originalTemplate!=="string")throw new Error("Cannot use higher-order sections without the original template");value=value.call(context.view,originalTemplate.slice(token[3],token[5]),subRender);if(value!=null)buffer+=value}else{buffer+=this.renderTokens(token[4],context,partials,originalTemplate,config)}return buffer};Writer.prototype.renderInverted=function renderInverted(token,context,partials,originalTemplate,config){var value=context.lookup(token[1]);if(!value||isArray(value)&&value.length===0)return this.renderTokens(token[4],context,partials,originalTemplate,config)};Writer.prototype.indentPartial=function indentPartial(partial,indentation,lineHasNonSpace){var filteredIndentation=indentation.replace(/[^ \t]/g,"");var partialByNl=partial.split("\n");for(var i=0;i<partialByNl.length;i++){if(partialByNl[i].length&&(i>0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,config){if(!partials)return;var tags=this.getConfigTags(config);var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}var tokens=this.parse(indentedValue,tags);return this.renderTokens(tokens,context,partials,indentedValue,config)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context,config){var escape=this.getConfigEscape(config)||mustache.escape;var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"&&escape===mustache.escape?String(value):escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};Writer.prototype.getConfigTags=function getConfigTags(config){if(isArray(config)){return config}else if(config&&typeof config==="object"){return config.tags}else{return undefined}};Writer.prototype.getConfigEscape=function getConfigEscape(config){if(config&&typeof config==="object"&&!isArray(config)){return config.escape}else{return undefined}};var mustache={name:"mustache.js",version:"4.2.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,config){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,config)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache});]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="prettify" javascript_name="prettify.js" javascript_type="framework" javascript_version="107643" javascript_position="1000500"><![CDATA[!function(){/*

 Copyright (C) 2006 Google Inc.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
*/
"undefined"!==typeof window&&(window.PR_SHOULD_USE_CONTINUATION=!0);
(function(){function T(a){function d(e){var a=e.charCodeAt(0);if(92!==a)return a;var c=e.charAt(1);return(a=w[c])?a:"0"<=c&&"7">=c?parseInt(e.substring(1),8):"u"===c||"x"===c?parseInt(e.substring(2),16):e.charCodeAt(1)}function f(e){if(32>e)return(16>e?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return"\\"===e||"-"===e||"]"===e||"^"===e?"\\"+e:e}function c(e){var c=e.substring(1,e.length-1).match(RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g"));
e=[];var a="^"===c[0],b=["["];a&&b.push("^");for(var a=a?1:0,g=c.length;a<g;++a){var h=c[a];if(/\\[bdsw]/i.test(h))b.push(h);else{var h=d(h),k;a+2<g&&"-"===c[a+1]?(k=d(c[a+2]),a+=2):k=h;e.push([h,k]);65>k||122<h||(65>k||90<h||e.push([Math.max(65,h)|32,Math.min(k,90)|32]),97>k||122<h||e.push([Math.max(97,h)&-33,Math.min(k,122)&-33]))}}e.sort(function(e,a){return e[0]-a[0]||a[1]-e[1]});c=[];g=[];for(a=0;a<e.length;++a)h=e[a],h[0]<=g[1]+1?g[1]=Math.max(g[1],h[1]):c.push(g=h);for(a=0;a<c.length;++a)h=
c[a],b.push(f(h[0])),h[1]>h[0]&&(h[1]+1>h[0]&&b.push("-"),b.push(f(h[1])));b.push("]");return b.join("")}function m(e){for(var a=e.source.match(RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g")),b=a.length,d=[],g=0,h=0;g<b;++g){var k=a[g];"("===k?++h:"\\"===k.charAt(0)&&(k=+k.substring(1))&&(k<=h?d[k]=-1:a[g]=f(k))}for(g=1;g<d.length;++g)-1===d[g]&&(d[g]=++E);for(h=g=0;g<b;++g)k=a[g],
"("===k?(++h,d[h]||(a[g]="(?:")):"\\"===k.charAt(0)&&(k=+k.substring(1))&&k<=h&&(a[g]="\\"+d[k]);for(g=0;g<b;++g)"^"===a[g]&&"^"!==a[g+1]&&(a[g]="");if(e.ignoreCase&&q)for(g=0;g<b;++g)k=a[g],e=k.charAt(0),2<=k.length&&"["===e?a[g]=c(k):"\\"!==e&&(a[g]=k.replace(/[a-zA-Z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return a.join("")}for(var E=0,q=!1,l=!1,n=0,b=a.length;n<b;++n){var p=a[n];if(p.ignoreCase)l=!0;else if(/[a-z]/i.test(p.source.replace(/\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi,
""))){q=!0;l=!1;break}}for(var w={b:8,t:9,n:10,v:11,f:12,r:13},r=[],n=0,b=a.length;n<b;++n){p=a[n];if(p.global||p.multiline)throw Error(""+p);r.push("(?:"+m(p)+")")}return new RegExp(r.join("|"),l?"gi":"g")}function U(a,d){function f(a){var b=a.nodeType;if(1==b){if(!c.test(a.className)){for(b=a.firstChild;b;b=b.nextSibling)f(b);b=a.nodeName.toLowerCase();if("br"===b||"li"===b)m[l]="\n",q[l<<1]=E++,q[l++<<1|1]=a}}else if(3==b||4==b)b=a.nodeValue,b.length&&(b=d?b.replace(/\r\n?/g,"\n"):b.replace(/[ \t\r\n]+/g,
" "),m[l]=b,q[l<<1]=E,E+=b.length,q[l++<<1|1]=a)}var c=/(?:^|\s)nocode(?:\s|$)/,m=[],E=0,q=[],l=0;f(a);return{a:m.join("").replace(/\n$/,""),c:q}}function J(a,d,f,c,m){f&&(a={h:a,l:1,j:null,m:null,a:f,c:null,i:d,g:null},c(a),m.push.apply(m,a.g))}function V(a){for(var d=void 0,f=a.firstChild;f;f=f.nextSibling)var c=f.nodeType,d=1===c?d?a:f:3===c?W.test(f.nodeValue)?a:d:d;return d===a?void 0:d}function G(a,d){function f(a){for(var l=a.i,n=a.h,b=[l,"pln"],p=0,q=a.a.match(m)||[],r={},e=0,t=q.length;e<
t;++e){var z=q[e],v=r[z],g=void 0,h;if("string"===typeof v)h=!1;else{var k=c[z.charAt(0)];if(k)g=z.match(k[1]),v=k[0];else{for(h=0;h<E;++h)if(k=d[h],g=z.match(k[1])){v=k[0];break}g||(v="pln")}!(h=5<=v.length&&"lang-"===v.substring(0,5))||g&&"string"===typeof g[1]||(h=!1,v="src");h||(r[z]=v)}k=p;p+=z.length;if(h){h=g[1];var A=z.indexOf(h),C=A+h.length;g[2]&&(C=z.length-g[2].length,A=C-h.length);v=v.substring(5);J(n,l+k,z.substring(0,A),f,b);J(n,l+k+A,h,K(v,h),b);J(n,l+k+C,z.substring(C),f,b)}else b.push(l+
k,v)}a.g=b}var c={},m;(function(){for(var f=a.concat(d),l=[],n={},b=0,p=f.length;b<p;++b){var w=f[b],r=w[3];if(r)for(var e=r.length;0<=--e;)c[r.charAt(e)]=w;w=w[1];r=""+w;n.hasOwnProperty(r)||(l.push(w),n[r]=null)}l.push(/[\0-\uffff]/);m=T(l)})();var E=d.length;return f}function x(a){var d=[],f=[];a.tripleQuotedStrings?d.push(["str",/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,
null,"'\""]):a.multiLineStrings?d.push(["str",/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"]):d.push(["str",/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"]);a.verbatimStrings&&f.push(["str",/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null]);var c=a.hashComments;c&&(a.cStyleComments?(1<c?d.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"]):d.push(["com",/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\r\n]*)/,
null,"#"]),f.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,null])):d.push(["com",/^#[^\r\n]*/,null,"#"]));a.cStyleComments&&(f.push(["com",/^\/\/[^\r\n]*/,null]),f.push(["com",/^\/\*[\s\S]*?(?:\*\/|$)/,null]));if(c=a.regexLiterals){var m=(c=1<c?"":"\n\r")?".":"[\\S\\s]";f.push(["lang-regex",RegExp("^(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[+\\-]=|->|\\/=?|::?|<<?=?|>>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+
("/(?=[^/*"+c+"])(?:[^/\\x5B\\x5C"+c+"]|\\x5C"+m+"|\\x5B(?:[^\\x5C\\x5D"+c+"]|\\x5C"+m+")*(?:\\x5D|$))+/")+")")])}(c=a.types)&&f.push(["typ",c]);c=(""+a.keywords).replace(/^ | $/g,"");c.length&&f.push(["kwd",new RegExp("^(?:"+c.replace(/[\s,]+/g,"|")+")\\b"),null]);d.push(["pln",/^\s+/,null," \r\n\t\u00a0"]);c="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(c+="(?!s*/)");f.push(["lit",/^@[a-z_$][a-z_$@0-9]*/i,null],["typ",/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],["pln",/^[a-z_$][a-z_$@0-9]*/i,
null],["lit",/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],["pln",/^\\[\s\S]?/,null],["pun",new RegExp(c),null]);return G(d,f)}function L(a,d,f){function c(a){var b=a.nodeType;if(1==b&&!t.test(a.className))if("br"===a.nodeName.toLowerCase())m(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)c(a);else if((3==b||4==b)&&f){var e=a.nodeValue,d=e.match(q);d&&(b=e.substring(0,d.index),a.nodeValue=b,(e=e.substring(d.index+
d[0].length))&&a.parentNode.insertBefore(l.createTextNode(e),a.nextSibling),m(a),b||a.parentNode.removeChild(a))}}function m(a){function c(a,b){var e=b?a.cloneNode(!1):a,k=a.parentNode;if(k){var k=c(k,1),d=a.nextSibling;k.appendChild(e);for(var f=d;f;f=d)d=f.nextSibling,k.appendChild(f)}return e}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;a=c(a.nextSibling,0);for(var e;(e=a.parentNode)&&1===e.nodeType;)a=e;b.push(a)}for(var t=/(?:^|\s)nocode(?:\s|$)/,q=/\r\n?|\n/,l=a.ownerDocument,n=l.createElement("li");a.firstChild;)n.appendChild(a.firstChild);
for(var b=[n],p=0;p<b.length;++p)c(b[p]);d===(d|0)&&b[0].setAttribute("value",d);var w=l.createElement("ol");w.className="linenums";d=Math.max(0,d-1|0)||0;for(var p=0,r=b.length;p<r;++p)n=b[p],n.className="L"+(p+d)%10,n.firstChild||n.appendChild(l.createTextNode("\u00a0")),w.appendChild(n);a.appendChild(w)}function t(a,d){for(var f=d.length;0<=--f;){var c=d[f];I.hasOwnProperty(c)?D.console&&console.warn("cannot override language handler %s",c):I[c]=a}}function K(a,d){a&&I.hasOwnProperty(a)||(a=/^\s*</.test(d)?
"default-markup":"default-code");return I[a]}function M(a){var d=a.j;try{var f=U(a.h,a.l),c=f.a;a.a=c;a.c=f.c;a.i=0;K(d,c)(a);var m=/\bMSIE\s(\d+)/.exec(navigator.userAgent),m=m&&8>=+m[1],d=/\n/g,t=a.a,q=t.length,f=0,l=a.c,n=l.length,c=0,b=a.g,p=b.length,w=0;b[p]=q;var r,e;for(e=r=0;e<p;)b[e]!==b[e+2]?(b[r++]=b[e++],b[r++]=b[e++]):e+=2;p=r;for(e=r=0;e<p;){for(var x=b[e],z=b[e+1],v=e+2;v+2<=p&&b[v+1]===z;)v+=2;b[r++]=x;b[r++]=z;e=v}b.length=r;var g=a.h;a="";g&&(a=g.style.display,g.style.display="none");
try{for(;c<n;){var h=l[c+2]||q,k=b[w+2]||q,v=Math.min(h,k),A=l[c+1],C;if(1!==A.nodeType&&(C=t.substring(f,v))){m&&(C=C.replace(d,"\r"));A.nodeValue=C;var N=A.ownerDocument,u=N.createElement("span");u.className=b[w+1];var B=A.parentNode;B.replaceChild(u,A);u.appendChild(A);f<h&&(l[c+1]=A=N.createTextNode(t.substring(v,h)),B.insertBefore(A,u.nextSibling))}f=v;f>=h&&(c+=2);f>=k&&(w+=2)}}finally{g&&(g.style.display=a)}}catch(y){D.console&&console.log(y&&y.stack||y)}}var D="undefined"!==typeof window?
window:{},B=["break,continue,do,else,for,if,return,while"],F=[[B,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,restrict,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],H=[F,"alignas,alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,noexcept,noreturn,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],
O=[F,"abstract,assert,boolean,byte,extends,finally,final,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],P=[F,"abstract,add,alias,as,ascending,async,await,base,bool,by,byte,checked,decimal,delegate,descending,dynamic,event,finally,fixed,foreach,from,get,global,group,implicit,in,interface,internal,into,is,join,let,lock,null,object,out,override,orderby,params,partial,readonly,ref,remove,sbyte,sealed,select,set,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,value,var,virtual,where,yield"],
F=[F,"abstract,async,await,constructor,debugger,enum,eval,export,function,get,import,implements,instanceof,interface,let,null,of,set,undefined,var,with,yield,Infinity,NaN"],Q=[B,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],R=[B,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],
B=[B,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],S=/^(DIR|FILE|array|vector|(de|priority_)?queue|(forward_)?list|stack|(const_)?(reverse_)?iterator|(unordered_)?(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,W=/\S/,X=x({keywords:[H,P,O,F,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",Q,R,B],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),
I={};t(X,["default-code"]);t(G([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),"default-markup htm html mxml xhtml xml xsl".split(" "));t(G([["pln",/^[\s]+/,
null," \t\r\n"],["atv",/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],["pun",/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);
t(G([],[["atv",/^[\s\S]+/]]),["uq.val"]);t(x({keywords:H,hashComments:!0,cStyleComments:!0,types:S}),"c cc cpp cxx cyc m".split(" "));t(x({keywords:"null,true,false"}),["json"]);t(x({keywords:P,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:S}),["cs"]);t(x({keywords:O,cStyleComments:!0}),["java"]);t(x({keywords:B,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);t(x({keywords:Q,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);t(x({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",
hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);t(x({keywords:R,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);t(x({keywords:F,cStyleComments:!0,regexLiterals:!0}),["javascript","js","ts","typescript"]);t(x({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,
regexLiterals:!0}),["coffee"]);t(G([],[["str",/^[\s\S]+/]]),["regex"]);var Y=D.PR={createSimpleLexer:G,registerLangHandler:t,sourceDecorator:x,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:D.prettyPrintOne=function(a,d,f){f=f||!1;d=d||null;var c=document.createElement("div");c.innerHTML="<pre>"+a+"</pre>";
c=c.firstChild;f&&L(c,f,!0);M({j:d,m:f,h:c,l:1,a:null,i:null,c:null,g:null});return c.innerHTML},prettyPrint:D.prettyPrint=function(a,d){function f(){for(var c=D.PR_SHOULD_USE_CONTINUATION?b.now()+250:Infinity;p<x.length&&b.now()<c;p++){for(var d=x[p],l=g,n=d;n=n.previousSibling;){var m=n.nodeType,u=(7===m||8===m)&&n.nodeValue;if(u?!/^\??prettify\b/.test(u):3!==m||/\S/.test(n.nodeValue))break;if(u){l={};u.replace(/\b(\w+)=([\w:.%+-]+)/g,function(a,b,c){l[b]=c});break}}n=d.className;if((l!==g||r.test(n))&&
!e.test(n)){m=!1;for(u=d.parentNode;u;u=u.parentNode)if(v.test(u.tagName)&&u.className&&r.test(u.className)){m=!0;break}if(!m){d.className+=" prettyprinted";m=l.lang;if(!m){var m=n.match(w),q;!m&&(q=V(d))&&z.test(q.tagName)&&(m=q.className.match(w));m&&(m=m[1])}if(B.test(d.tagName))u=1;else var u=d.currentStyle,y=t.defaultView,u=(u=u?u.whiteSpace:y&&y.getComputedStyle?y.getComputedStyle(d,null).getPropertyValue("white-space"):0)&&"pre"===u.substring(0,3);y=l.linenums;(y="true"===y||+y)||(y=(y=n.match(/\blinenums\b(?::(\d+))?/))?
y[1]&&y[1].length?+y[1]:!0:!1);y&&L(d,y,u);M({j:m,h:d,m:y,l:u,a:null,i:null,c:null,g:null})}}}p<x.length?D.setTimeout(f,250):"function"===typeof a&&a()}for(var c=d||document.body,t=c.ownerDocument||document,c=[c.getElementsByTagName("pre"),c.getElementsByTagName("code"),c.getElementsByTagName("xmp")],x=[],q=0;q<c.length;++q)for(var l=0,n=c[q].length;l<n;++l)x.push(c[q][l]);var c=null,b=Date;b.now||(b={now:function(){return+new Date}});var p=0,w=/\blang(?:uage)?-([\w.]+)(?!\S)/,r=/\bprettyprint\b/,
e=/\bprettyprinted\b/,B=/pre|xmp/i,z=/^code$/i,v=/^(?:pre|code|xmp)$/i,g={};f()}},H=D.define;"function"===typeof H&&H.amd&&H("google-code-prettify",[],function(){return Y})})();}()
PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[ \n\r\t\v\f\0]+/,null," \n\r\t\v\f\x00"],["str",/^"(?:[^"\\]|(?:\\.)|(?:\\\((?:[^"\\)]|\\.)*\)))*"/,null,'"']],[["lit",/^(?:(?:0x[\da-fA-F][\da-fA-F_]*\.[\da-fA-F][\da-fA-F_]*[pP]?)|(?:\d[\d_]*\.\d[\d_]*[eE]?))[+-]?\d[\d_]*/,null],["lit",/^-?(?:(?:0(?:(?:b[01][01_]*)|(?:o[0-7][0-7_]*)|(?:x[\da-fA-F][\da-fA-F_]*)))|(?:\d[\d_]*))/,null],["lit",/^(?:true|false|nil)\b/,null],["kwd",/^\b(?:__COLUMN__|__FILE__|__FUNCTION__|__LINE__|#available|#colorLiteral|#column|#else|#elseif|#endif|#file|#fileLiteral|#function|#if|#imageLiteral|#line|#selector|#sourceLocation|arch|arm|arm64|associatedtype|associativity|as|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|dynamicType|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|import|indirect|infix|init|inout|internal|i386|if|in|iOS|iOSApplicationExtension|is|lazy|left|let|mutating|none|nonmutating|open|operator|optional|OSX|OSXApplicationExtension|override|postfix|precedence|prefix|private|protocol|Protocol|public|required|rethrows|return|right|safe|Self|self|set|static|struct|subscript|super|switch|throw|try|Type|typealias|unowned|unsafe|var|weak|watchOS|while|willSet|x86_64)\b/,
null],["com",/^\/\/.*?[\n\r]/,null],["com",/^\/\*[\s\S]*?(?:\*\/|$)/,null],["pun",/^<<=|<=|<<|>>=|>=|>>|===|==|\.\.\.|&&=|\.\.<|!==|!=|&=|~=|~|\(|\)|\[|\]|{|}|@|#|;|\.|,|:|\|\|=|\?\?|\|\||&&|&\*|&\+|&-|&=|\+=|-=|\/=|\*=|\^=|%=|\|=|->|`|==|\+\+|--|\/|\+|!|\*|%|<|>|&|\||\^|\?|=|-|_/,null],["typ",/^\b(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null]]),["swift"]);
PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[["str",/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],["str",/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']+)\)/i],["kwd",/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],
["com",/^(?:\x3c!--|--\x3e)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#(?:[0-9a-f]{3}){1,2}\b/i],["pln",/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],["pun",/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^\)\"\']+/]]),["css-str"]);
PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["com",/^%[^\r\n]*/,null,"%"]],[["kwd",/^\\[a-zA-Z@]+/],["kwd",/^\\./],["typ",/^[$&]/],["lit",/[+-]?(?:\.\d+|\d+(?:\.\d*)?)(cm|em|ex|in|pc|pt|bp|mm)/i],["pun",/^[{}()\[\]=]+/]]),["latex","tex"]);
PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["str",/^(?:\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)|\'(?:[^\'\\]|\\[\s\S])*(?:\'|$))/,null,"\"'"]],[["com",/^--(?:\[(=*)\[[\s\S]*?(?:\]\1\]|$)|[^\r\n]*)/],["str",/^\[(=*)\[[\s\S]*?(?:\]\1\]|$)/],["kwd",/^(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,null],["lit",/^[+-]?(?:0x[\da-f]+|(?:(?:\.\d+|\d+(?:\.\d*)?)(?:e[+\-]?\d+)?))/i],
["pln",/^[a-z_]\w*/i],["pun",/^[^\w\t\n\r \xA0][^\w\t\n\r \xA0\"\'\-\+=]*/]]),["lua"]);
]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="templates" javascript_name="ips.core.templates.js" javascript_type="framework" javascript_version="107643" javascript_position="1000150"><![CDATA[/* MENUS */
ips.templates.set('core.appMenu.reorder', " \
	<span data-role='reorder' style='display: none'><i class='fa fa-bars'></i></span>\
");

/* CONTROL STRIP TEMPLATES */
ips.templates.set('core.controlStrip.menu', " \
	<ul class='ipsMenu ipsMenu_auto' role='menu' id='{{id}}_menu' style='display: none'>\
		{{{content}}}\
	</ul>\
");

ips.templates.set('core.controlStrip.menuItem', " \
	<li class='ipsMenu_item ipsControlStrip_menuItem' role='menuitem' id='{{id}}'>\
		{{{item}}}\
	</li>\
");

ips.templates.set('core.controlStrip.dropdown', " \
	<li class='ipsControlStrip_button ipsControlStrip_dropdown' data-dropdown id='{{id}}'>\
		<a href='#'>\
			<i class='ipsControlStrip_icon fa fa-angle-down'></i>\
		</a>\
	</li>\
");

/* TOGGLE TEMPLATES */
ips.templates.set('core.forms.toggle', " \
	<span class='ipsToggle {{className}}' id='{{id}}' tabindex='0' role='switch' aria-checked='{{status}}'>\
		<span data-role='status'></span>\
	</span>\
");

/* TREES */
ips.templates.set('core.trees.childWrapper', " \
	{{{content}}}\
");

ips.templates.set('core.trees.loadingRow', " \
	<ol class='ipsTree'><li class='ipsTree_loadingRow ipsLoading_tiny'>{{#lang}}loading{{/lang}}</li></ol>\
");

ips.templates.set('core.trees.loadingPane', " \
	<div class='ipsLoading' style='height: 150px'>&nbsp;</div>\
");

ips.templates.set('core.trees.noRows', " \
	<div class='ipsType_center ipsPad ipsType_light'>{{#lang}}no_results{{/lang}}</div>\
");

/* LIVE SEARCH */
ips.templates.set('core.livesearch.noResults', " \
	<li class='ipsType_center ipsPad ipsType_light ipsType_normal' data-role='result'>\
		<br><br>\
	</li>\
");

/* LANGUAGES */
ips.templates.set('languages.translateString', " \
	<div class='cTranslateTable_field'>\
		<textarea>{{value}}</textarea>\
		<a href='#' data-action='saveWords' tabindex='-1' class='ipsButton ipsButton_positive ipsButton_verySmall ipsButton_narrow'><i class='fa fa-check'></i> {{#lang}}languageSave{{/lang}}</a>\
	</div>\
");

/* Guide search result */
ips.templates.set('support.guideSearch', " \
	<li>\
		<a href=\"{{link}}\" target='_blank' rel='noopener'>{{title}}</a>\
	</li>\
");
ips.templates.set('support.guideSearch.noResults', " \
	<li class='ipsType_light'>\
		{{#lang}}no_results{{/lang}}\
	</li>\
");

ips.templates.set('support.ticket.supportSummary', " \
	<div class='ipsType_normal ipsPadding_top'>\
		{{#lang}}health_ticket_beforeproceeding{{/lang}}\
	</div>\
");]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.dashboard.js" javascript_type="template" javascript_version="107643" javascript_position="1000550"><![CDATA[/* DASHBOARD TEMPLATES */
ips.templates.set('dashboard.widget', " \
	<li id='elWidget_{{key}}' data-widgetKey='{{key}}' data-widgetName='{{name}}' data-widgetBy='{{by}}' style='display: none'>\
		<div class='ipsBox acpWidget_item'>\
			<h2 class='ipsBox_titleBar ipsType_reset'>\
				<ul class='ipsList_reset ipsList_inline acpWidget_tools'>\
					<li>\
						<a href='#' class='acpWidget_reorder ipsJS_show ipsCursor_drag' data-ipsTooltip title='Reorder widget'><i class='fa fa-bars'></i></a>\
					</li>\
					<li>\
						<a href='#' class='acpWidget_close' data-ipsTooltip title='Close widget'><i class='fa fa-times'></i></a>\
					</li>\
				</ul>\
				{{name}} {{#by}}<span class='ipsType_light ipsType_medium ipsType_unbold'>By {{by}}</span>{{/by}}\
			</h2>\
			<div class='ipsPad' data-role='widgetContent'>\
				{{content}}\
			</div>\
		</div>\
	</li>\
");

ips.templates.set('dashboard.menuItem', " \
	<li class='ipsMenu_item' data-ipsMenuValue='{{key}}' data-widgetName='{{name}}' data-widgetBy='{{by}}'>\
		<a href='#'>{{name}}</a>\
	</li>\
");]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.members.js" javascript_type="template" javascript_version="107643" javascript_position="1000550"><![CDATA[
ips.templates.set( 'moderatorPermissions.checkUncheckAll', "\
	<li class='ipsFieldRow ipsPad_half ipsClearfix'>\
		<div class='ipsFieldRow_title'>\
		</div>\
		<div class='ipsFieldRow_content'>\
			<ul class='ipsList_inline'>\
				<li><a href='#' data-role='checkAll'>{{#lang}}check_all{{/lang}}</a></li>\
				<li><a href='#' data-role='uncheckAll'>{{#lang}}uncheck_all{{/lang}}</a></li>\
			</ul>\
		</div>\
	</li>\
");]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.system.js" javascript_type="template" javascript_version="107643" javascript_position="1000550"><![CDATA[ips.templates.set( 'menuManager.temporaryDropdown', "\
	<li class='ipsMenu_item {{#selected}}cMenuManager_active{{/selected}}' data-itemID='temp' data-role='menuItem'>\
		<span>\
			<ul class='ipsList_inline ipsPos_right cMenuManager_tools'>\
				<li>\
					<a href='#' data-action='removeItem' data-ipsTooltip title='{{#lang}}menuManagerRemoveItem{{/lang}}' class='ipsType_blendLinks'>\
						<i class='fa fa-times'></i></i>\
					</a>\
				</li>\
			</ul>\
			{{#lang}}menuManagerNewItem{{/lang}}\
		</span>\
	</li>\
");

ips.templates.set( 'menuManager.temporaryMenuItem', "\
	<li id='menu_{{id}}' data-role='menuNode'>\
		<div class='cMenuManager_leaf {{#selected}}cMenuManager_active{{/selected}}' data-itemID='temp' data-role='menuItem'>\
			<ul class='ipsList_inline ipsPos_right cMenuManager_tools'>\
				<li>\
					<a href='#' data-action='removeItem' data-ipsTooltip title='{{#lang}}menuManagerRemoveItem{{/lang}}' class='ipsType_blendLinks'>\
						<i class='fa fa-times'></i></i>\
					</a>\
				</li>\
			</ul>\
			<h3 class='cMenuManager_leafTitle'>{{#lang}}menuManagerNewItem{{/lang}}</h3>\
		</div>\
	</li>\
");


ips.templates.set( 'menuManager.emptyList', "\
	<li class='cMenuManager_emptyList ipsType_light ipsType_center'>{{#lang}}menuManagerEmptyList{{/lang}}</li>\
");]]></file>
 <file javascript_app="core" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.templates.js" javascript_type="template" javascript_version="107643" javascript_position="1000550"><![CDATA[/* TEMPLATE EDITOR TEMPLATES */
/* TEMPLATECEPTION */
ips.templates.set('templates.editor.newTab', " \
	<li data-fileid='{{fileid}}'>\
		<a href='#' class='ipsTabs_item' id='{{id}}'>{{title}} <span data-action='closeTab'><i class='fa fa-times'></i></span></a>\
	</li>\
");

ips.templates.set('templates.editor.tabPanel', " \
	<div data-fileid='{{fileid}}' id='ipsTabs_elTemplateEditor_tabbar_tab_{{fileid}}_panel' class='ipsTabs_panel' style='display: none' data-app='{{app}}' data-location='{{location}}' data-group='{{group}}' data-name='{{name}}' data-type='{{type}}' data-itemID='{{id}}' data-inherited-value='{{inherited}}'>\
		{{{content}}}\
	</div>\
");

ips.templates.set('templates.editor.tabContent', " \
	<input data-role='variables' type='hidden' name='variables_{{fileid}}' value=\"{{{variables}}}\">\
	<textarea data-fileid='{{fileid}}' id='editor_{{fileid}}'>{{{content}}}</textarea>\
");

ips.templates.set('templates.editor.unsaved', " \
	<i class='fa fa-circle'></i>\
");

ips.templates.set('templates.editor.saved', " \
	<i class='fa fa-times'></i>\
");

ips.templates.set('templates.editor.diffHeaders', " \
	<div class='cTemplateMergeHeaders ipsAreaBackground_light'>\
		<div class='cTemplateMergeHeader'>\
			<div class='ipsPad_half'><strong>{{#lang}}theme_diff_original_header{{/lang}}</strong> <span class='ipsType_small ipsType_light'>{{#lang}}theme_diff_original_desc{{/lang}}</span></div>\
		</div>\
		<div class='cTemplateMergeHeader ipsPos_right'>\
			<div class='ipsPad_half'><strong>{{#lang}}theme_diff_custom_header{{/lang}}</strong> <span class='ipsType_small ipsType_light'>{{#lang}}theme_diff_custom_desc{{/lang}}</span></div>\
		</div>\
	</div>\
");

ips.templates.set('templates.editor.diffHeadersParent', " \
	<div class='cTemplateMergeHeaders ipsAreaBackground_light'>\
		<div class='cTemplateMergeHeader'>\
			<div class='ipsPad_half'><strong>{{#lang}}theme_diff_parent_header{{/lang}}</strong> <span class='ipsType_small ipsType_light'>{{#lang}}theme_diff_original_desc{{/lang}}</span></div>\
		</div>\
		<div class='cTemplateMergeHeader ipsPos_right'>\
			<div class='ipsPad_half'><strong>{{#lang}}theme_diff_custom_header{{/lang}}</strong> <span class='ipsType_small ipsType_light'>{{#lang}}theme_diff_custom_desc{{/lang}}</span></div>\
		</div>\
	</div>\
");
]]></file>
 <file javascript_app="global" javascript_location="framework" javascript_path="templates" javascript_name="ips.core.templates.js" javascript_type="framework" javascript_version="107643" javascript_position="50"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 */
/* GENERAL TEMPLATES */
ips.templates.set('core.general.loading', " \
	&nbsp;<span class='ipsType_light'><i class='icon-spinner2 ipsLoading_tinyIcon'></i>&nbsp;&nbsp;&nbsp;</span> {{text}}</span>\
");

ips.templates.set('core.general.ajax', " \
	<div id='elAjaxLoading'><i class='ipsLoading ipsLoading_tiny ipsLoading_dark ipsMargin_right:half'></i> &nbsp;&nbsp;{{#lang}}loading{{/lang}}...</div>\
");

ips.templates.set('core.general.flashMsg', " \
	<div id='elFlashMessage'><div class='ipsFlex ipsFlex-ai:center ipsGap:3 ipsGap_row:0'><div data-role='flashMessage' class='ipsFlex-flex:11'></div><div class='ipsFlex-flex:00'><a href='#' data-action='dismissFlashMessage'>&times;</a></div></div></div>\
");

ips.templates.set('core.hovercard.loading', " \
	<i class='icon-spinner2 ipsLoading_tinyIcon'></i>\
");

/* POST CONTENT */
ips.templates.set('core.posts.spoiler', " \
	<span class='ipsStyle_spoilerFancy_text'><span class='ipsButton ipsButton_verySmall ipsButton_primary ipsButton_narrow'><i class='fa fa-chevron-right'></i></span> {{#lang}}spoilerClickToReveal{{/lang}}</span>\
");

ips.templates.set('core.posts.spoilerOpen', " \
	<span class='ipsStyle_spoilerFancy_text'><span class='ipsButton ipsButton_verySmall ipsButton_primary ipsButton_narrow'><i class='fa fa-chevron-down'></i></span> {{#lang}}spoilerClickToHide{{/lang}}</span>\
");

ips.templates.set('core.posts.multiQuoteOff', " \
	<i class='fa fa-plus'></i>\
");

ips.templates.set('core.posts.multiQuoteOn', " \
	<i class='fa fa-check'></i>\
");

ips.templates.set('core.posts.multiQuoter', " \
	<div id='ipsMultiQuoter' data-commentsContainer='{{commentFeedId}}'>\
		<button class='ipsButton ipsButton_veryLight ipsButton_small' data-role='multiQuote_{{commentFeedId}}'><i class='fa fa-comments'></i> &nbsp;&nbsp;{{{count}}}</button> &nbsp;&nbsp;<a href='#' data-action='clearQuoted_{{commentFeedId}}'><i class='fa fa-times'></i></a>\
	</div>\
");

ips.templates.set('core.menus.menuItem', " \
	<li class='ipsMenu_item {{#checked}}ipsMenu_itemChecked{{/checked}}' data-ipsMenuValue='{{value}}'>\
		<a href='{{link}}'>{{title}}</a>\
	</li>\
");

ips.templates.set('core.menus.menuSep', " \
	<li class='ipsMenu_sep'><hr></li>\
");

ips.templates.set('core.posts.quotedSpoiler', " \
	<p><em>{{#lang}}quotedSpoiler{{/lang}}</em></p>\
");

/* NOTIFICATION TEMPLATE */
ips.templates.set('core.postNotify.single', " \
	<span data-role='newPostNotification' class='ipsType_medium'>\
		<img src='{{photo}}' alt='' class='ipsUserPhoto ipsUserPhoto_tiny ipsPos_middle'> &nbsp;&nbsp;&nbsp;{{{text}}}\
		&nbsp;&nbsp;&nbsp;<a href='#' data-action='loadNewPosts'>{{#lang}}showReply{{/lang}}</a>\
	</span>\
");

ips.templates.set('core.postNotify.multiple', " \
	<span data-role='newPostNotification' class='ipsType_medium'>\
		{{text}}\
		&nbsp;&nbsp;&nbsp;<a href='#' data-action='loadNewPosts'>{{#lang}}showReplies{{/lang}}</a>\
	</span>\
");

ips.templates.set('core.postNotify.multipleSpillOver', " \
	<span data-role='newPostNotification' class='ipsType_medium'>\
		{{text}}\
		{{#canLoadNew}}\
			&nbsp;&nbsp;&nbsp;<a href='#' data-action='loadNewPosts'>{{showFirstX}}</a>\
			&nbsp;&nbsp;&nbsp;<span class='ipsType_light'>{{#lang}}showRepliesOr{{/lang}}</span>\
		{{/canLoadNew}}\
		&nbsp;&nbsp;&nbsp;<a href='{{spillOverUrl}}'>{{#lang}}goToNewestPage{{/lang}}</a>\
	</span>\
");

ips.templates.set('core.notification.flashSingle', " \
	<a href='{{url}}' data-role='newNotification'>\
		<div class='ipsFlex ipsFlex-ai:center ipsGap:3 ipsGap_row:0 ipsType_medium ipsType_blendLinks'>\
			{{#icon}}<div class='ipsFlex-flex:00'><img src='{{icon}}' alt='' class='ipsUserPhoto ipsUserPhoto_tiny'></div>{{/icon}}\
			<div class='ipsFlex-flex:11 ipsType_left'>\
				{{text}}\
				<p class='ipsType_reset ipsType_light ipsTruncate ipsTruncate_line'>{{{body}}}</p>\
			</div>\
		</div>\
	</a>\
");

ips.templates.set('core.notification.flashMultiple', " \
	<div class='ipsFlex ipsFlex-ai:center ipsGap:3 ipsGap_row:0 ipsType_medium ipsType_blendLinks' data-role='newNotification'>\
		<span class='ipsFlex-flex:00 ipsType_veryLarge'><i class='fa fa-bell'></i></span>\
		<div class='ipsFlex-flex:11 ipsType_left'>\
			{{text}}\
			<p class='ipsType_reset ipsType_light ipsTruncate ipsTruncate_line'>{{{body}}}</p>\
		</div>\
	</div>\
");

/* ALERTS */
ips.templates.set('core.alert.box', " \
<div class='ipsAlert' style='display: none' role='alertdialog' aria-describedby='{{id}}_message'>\
	{{{icon}}}\
	<div class='ipsAlert_msg ipsType_break' id='{{id}}_message'>\
		<strong>{{{text}}}</strong>\
		{{{subtext}}}\
	</div>\
	<ul class='ipsToolList ipsToolList_horizontal ipsPos_center ipsAlert_buttonRow ipsClear ipsClearfix'>\
		{{{buttons}}}\
	</ul>\
</div>\
");

ips.templates.set('core.alert.subText', " \
<div class='ipsType_light ipsType_normal'>{{text}}</div>\
");

ips.templates.set('core.alert.subTextHtml', " \
<div class='ipsType_light ipsType_normal'>{{{text}}}</div>\
");

ips.templates.set('core.alert.icon', " \
<i class='{{icon}} ipsAlert_icon'></i>\
");

ips.templates.set('core.alert.button', " \
<li><button data-action='{{action}}' class='ipsButton ipsButton_fullWidth {{extra}}' role='button'>{{title}}</button></li>\
");

ips.templates.set('core.alert.prompt', " \
<br><br>\
<input type='text' value='{{value}}' class='ipsField_fullWidth' data-role='promptValue'>\
<br><br>\
");

/* LIGHTBOX TEMPLATES */
ips.templates.set('core.lightbox.meta', "{{title}}");

ips.templates.set('core.lightbox.toolsMenu', " \
<a href='{{url}}&amp;direction=right' class='ipsButton ipsButton_link ipsButton_small' title='Rotate Right' data-ipsTooltip data-action='rotateImage'>\
    <i class='fa fa-fw fa-rotate-right'></i>\
</a>\
<a href='{{url}}&amp;direction=left' class='ipsButton ipsButton_link ipsButton_small' title='Rotate Left' data-ipsTooltip data-action='rotateImage'>\
    <i class='fa fa-fw fa-rotate-left'></i>\
</a>\
");

/* DIALOG TEMPLATES */
ips.templates.set('core.dialog.main', " \
<div class='{{class}} {{#fixed}}{{class}}_fixed{{/fixed}} {{#size}}{{class}}_{{size}}{{/size}} {{extraClass}}' style='display: none' id='{{id}}' role='dialog' aria-label='{{title}}'>\
	<div>\
		{{#title}}\
			<h3 class='{{class}}_title'>{{title}}</h3>\
			<hr class='ipsHr'>\
		{{/title}}\
		{{#close}}\
			<a href='#' class='{{class}}_close' data-action='dialogClose'>&times;</a>\
		{{/close}}\
		<div class='{{class}}_content'>\
			{{content}}\
		</div>\
		<div class='{{class}}_loading {{class}}_large ipsLoading ipsLoading_noAnim' style='display: none'></div>\
	</div>\
</div>\
")

/* TOOLTIP TEMPLATE */
ips.templates.set('core.tooltip', " \
	<div id='{{id}}' class='ipsTooltip' role='tooltip'>{{content}}</div>\
");

/* SEARCH TEMPLATES */
ips.templates.set('core.search.loadingPanel', " \
	<div id='{{id}}' class='ipsLoading' style='min-height: 100px'>\
		&nbsp;\
	</div>\
");

/* EDITOR TEMPLATES */
ips.templates.set('core.editor.panelWrapper', " \
	<div id='{{id}}' class='ipsRTE_panel ipsPad'>\
		{{content}}\
	</div>\
");

ips.templates.set('core.editor.giphy', " \
<div class='ipsMenu ipsMenu_wide' id='{{id}}_menu' style='display: none' data-editorID='{{editor}}' data-controller='core.global.editor.giphy'>\
	<div class='ipsMenu_headerBar'>\
		<div class='ipsGiphy_attribution'><img src='{{attribution_image}}'></div>\
		<h4 class='ipsType_sectionHead'>\
			{{#lang}}giphy{{/lang}}\
		</h4>\
	</div>\
	<div class='ipsMenu_innerContent ipsGiphy_content' data-role='giphyResults'>\
		<div data-role='giphyLoading'>\
			\
		</div>\
		<div class='ipsGiphy_moar' data-role='giphyMore' data-offset='0'>\
			<div data-role='giphyMoreLoading' class='ipsType_light ipsHide ipsSpacer_bottom'>{{#lang}}giphyMore_loading{{/lang}}</div>\
		</div>\
	</div>\
	<div class='ipsMenu_footerBar'>\
		<input type='text' data-role='giphySearch' class='ipsField_fullWidth' placeholder='{{#lang}}giphyFind{{/lang}}'>\
	</div>\
</div>\
");

ips.templates.set('core.editor.giphyThumb', " \
	<div class='ipsGiphy_thumb'><img src=\"{{thumb}}\" class=\"ipsGiphyImage\" data-url=\"{{url}}\" alt='{{title}}' title='{{title}}'></div>\
");

ips.templates.set('core.editor.giphyRow', " \
	<div class='ipsGiphy_row'>{{{gifs}}}</div>\
");

ips.templates.set('core.editor.pixabayThumb', " \
	<div class='ipsPixabay_thumb'><img src=\"{{thumb}}\" class=\"ipsPixabayImage\" data-url=\"{{url}}\" data-id=\"{{imgid}}\"></div>\
");

ips.templates.set('core.editor.pixabayRow', " \
	<div class='ipsPixabay_row'>{{{images}}}</div>\
");

ips.templates.set('core.editor.emoticons', " \
<div class='ipsMenu ipsMenu_wide' id='{{id}}_menu' style='display: none' data-editorID='{{editor}}' data-controller='core.global.editor.emoticons'>\
	<div class='ipsMenu_headerBar'>\
		<p class='ipsType_reset ipsPos_right'>\
			<a href='#' class='ipsType_blendLinks ipsHide' data-role='skinToneMenu' data-ipsMenu data-ipsMenu-appendTo='#{{id}}_menu' id='{{id}}_tones'>{{#lang}}emoji_skin_tone{{/lang}} <i class='fa fa-caret-down'></i></a>\
			&nbsp;&nbsp;&nbsp;\
			<a href='#' class='ipsType_blendLinks ipsHide' data-role='categoryTrigger' data-ipsMenu data-ipsMenu-appendTo='#{{id}}_menu' id='{{id}}_more'>{{#lang}}emoticonCategories{{/lang}} <i class='fa fa-caret-down'></i></a>\
		</p>\
		<h4 class='ipsType_sectionHead'>{{#lang}}emoji{{/lang}}</h4>\
		<ul class='ipsMenu ipsMenu_veryNarrow ipsCursor_pointer' id='{{id}}_tones_menu' role='menu' style='display: none'>\
			<li class='ipsMenu_title'>{{#lang}}emoji_skin_tone{{/lang}}</li>\
			<li class='ipsMenu_item' role='menuitem' data-ipsMenuValue='none'><a>{{#lang}}emoji_skin_tone_default{{/lang}}</a></li>\
			<li class='ipsMenu_sep'><hr></li>\
			<li class='ipsMenu_item' role='menuitem' data-ipsMenuValue='light'><a>\uD83C\uDFFB {{#lang}}emoji_skin_tone_light{{/lang}}</a></li>\
			<li class='ipsMenu_item' role='menuitem' data-ipsMenuValue='medium-light'><a>\uD83C\uDFFC {{#lang}}emoji_skin_tone_medium_light{{/lang}}</a></li>\
			<li class='ipsMenu_item' role='menuitem' data-ipsMenuValue='medium'><a>\uD83C\uDFFD {{#lang}}emoji_skin_tone_medium{{/lang}}</a></li>\
			<li class='ipsMenu_item' role='menuitem' data-ipsMenuValue='medium-dark'><a>\uD83C\uDFFE {{#lang}}emoji_skin_tone_medium_dark{{/lang}}</a></li>\
			<li class='ipsMenu_item' role='menuitem' data-ipsMenuValue='dark'><a>\uD83C\uDFFF {{#lang}}emoji_skin_tone_dark{{/lang}}</a></li>\
		</ul>\
		<ul data-role='categoryMenu' class='ipsMenu ipsMenu_auto ipsCursor_pointer' id='{{id}}_more_menu' role='menu' style='display: none'>\
		</ul>\
	</div>\
	<div class='ipsMenu_innerContent'>\
		<div class='ipsEmoticons_content'>\
			<div class='ipsEmpty ipsType_center ipsEmoticons_contentLoading' data-role='emojiLoading'>\
				{{#lang}}loading{{/lang}}...\
			</div>\
		</div>\
	</div>\
	<div class='ipsMenu_footerBar'>\
		<input type='text' data-role='emoticonSearch' class='ipsField_fullWidth' placeholder='{{#lang}}emoticonFind{{/lang}}'>\
	</div>\
</div>\
");		

ips.templates.set('core.editor.emoticonSection', " \
	<div data-panel='{{id}}'>{{{content}}}</div>\
");

ips.templates.set('core.editor.emoticonMenu', " \
	<li class='ipsMenu_item' role='menuitem' data-ipsMenuValue='{{categoryID}}'><a><span class='ipsMenu_itemCount'>{{count}}</span>{{title}}</a></li>\
");

ips.templates.set('core.editor.emoticonCategory', " \
	<div class='ipsAreaBackground_light ipsPad_half'><strong>{{title}}</strong></div>\
	<div class='ipsEmoticons_category' data-categoryid='{{categoryID}}'>{{{emoticons}}}</div>\
");
ips.templates.set('core.editor.emoticonSearch', " \
	<div class='ipsEmoticons_category'>{{{emoticons}}}</div>\
");

ips.templates.set('core.editor.emoticonRow', " \
	<div class='ipsEmoticons_row ipsEmoji'>{{{emoticons}}}</div>\
");

ips.templates.set('core.editor.emoticonItem', " \
	<div class='ipsEmoticons_item' data-emoticon='{{tag}}' data-src='{{src}}' data-srcset='{{srcset}}' data-height='{{height}}' data-width='{{width}}' title='{{tag}}'>{{{img}}}</div>\
");

ips.templates.set('core.editor.emoji', " \
	<div class='ipsEmoticons_item' title='{{name}}' data-emoji='{{code}}'>{{{display}}}</div>\
");

ips.templates.set('core.editor.emojiNotNative', " \
	<div class='ipsEmoticons_item' title='{{name}}' data-emoji='{{code}}'>{{{img}}}</div>\
");


ips.templates.set('core.editor.emoticonBlank', " \
	<div class='ipsEmoticons_item'>&nbsp;</div>\
");

ips.templates.set('core.editor.emoticonNoResults', " \
	<div class='ipsPad ipsType_center ipsType_light'>{{#lang}}no_results{{/lang}}</div>\
");

ips.templates.set('core.editor.emojiResult', " \
	<li class='ipsMenu_item ipsCursor_pointer' title='{{name}}' data-emoji='{{code}}'>\
		<a><span class='ipsEmoji_result'>{{{emoji}}}</span> <span data-role='shortCode'>{{short_code}}</span></a>\
	</li>\
");

ips.templates.set('core.editor.quote', "<blockquote class='ipsQuote' data-ipsQuote data-gramm='false'><div class='ipsQuote_citation'>{{citation}}</div><div class='ipsQuote_contents ipsClearfix' data-gramm='false'>{{{contents}}}</div></blockquote>");
ips.templates.set('core.editor.legacyQuoteUpcast', "<div class='ipsQuote_citation'>{{citation}}</div><div class='ipsQuote_contents ipsClearfix' data-gramm='false'>{{{contents}}}</div>");

ips.templates.set('core.editor.citation', " \
	<div class='ipsQuote_citation ipsQuote_open'>\
		<a href='#' data-action='toggleQuote'>&nbsp;</a>\
		{{#contenturl}}\
			<a class='ipsPos_right' href='{{contenturl}}'><i class='fa fa-share'></i></a>\
		{{/contenturl}}\
		{{{citation}}}\
	</div>\
");

ips.templates.set('core.editor.citationLink', " \
	<a href='{{baseURL}}?app=core&module=members&controller=profile&id={{userid}}' data-ipsHover data-ipshover-target='{{baseURL}}?app=core&module=members&controller=profile&id={{userid}}&do=hovercard'>{{username}}</a>\
");

ips.templates.set('core.editor.spoiler', "<div class='ipsSpoiler' data-ipsSpoiler><div class='ipsSpoiler_header'><span>{{#lang}}editorSpoiler{{/lang}}</span></div><div class='ipsSpoiler_contents ipsClearfix'></div></div>");
ips.templates.set('core.editor.legacySpoilerUpcast', "<div class='ipsSpoiler_header'><span>{{#lang}}editorSpoiler{{/lang}}</span></div><div class='ipsSpoiler_contents ipsClearfix' data-gramm='false'>{{{contents}}}</div>");
ips.templates.set('core.editor.spoilerHeader', " \
	<div class='ipsSpoiler_header ipsSpoiler_closed'>\
		<a href='#' data-action='toggleSpoiler'>&nbsp;</a>\
		<span>{{#lang}}spoilerClickToReveal{{/lang}}</span>\
	</div>\
");

ips.templates.set('core.editor.initLoading', " \
	<div class='ipsLoading ipsLoading_tiny'>&nbsp;</div>\
");

ips.templates.set('core.editor.previewLoading', " \
	<div data-role='previewLoading' class='ipsLoading' style='min-height: 100px'>\
		&nbsp;\
	</div>\
");

ips.templates.set('core.editor.stockReplies', " \
<div class='ipsMenu ipsMenu_wide' id='{{id}}_menu' style='display: none' data-editorID='{{editor}}' data-controller='core.global.editor.stockReplies'>\
	<div class='ipsMenu_headerBar'>\
		<h4 class='ipsType_sectionHead'>\
			{{#lang}}editorStoredReplies{{/lang}}\
		</h4>\
	</div>\
	<div class='ipsStockReplies_content ipsMenu_innerContent'>\
		<div data-role='stockRepliesLoading' class='ipsLoading'>\
			\
		</div>\
	</div>\
</div>\
");

ips.templates.set('core.editor.editorStockRepliesWrap', " \
<div class='ipsStockReplies_menu'>{{{content}}}</div>\
");

ips.templates.set('core.editor.editorStockRepliesRow', " \
<div class='ipsStockReplies_row ipsPad_half' data-templatesId='{{{id}}}'>{{{title}}}</div>\
");

/* ATTACHMENT TEMPLATES */
ips.templates.set('core.attachments.metaInfo', " \
	<span class='ipsFlex-inline ipsFlex-ai:center ipsFlex-jc:center'><span>{{size}}</span><span>&nbsp;&middot;&nbsp;</span><span>{{downloads}}</span></span> \
");

ips.templates.set('core.attachments.attachmentPreview', " \
	<span class='ipsAttachLink_title'>{{title}}</span><span class='ipsAttachLink_metaInfo'>{{#lang}}attachmentPending{{/lang}}</span> \
");

ips.templates.set('core.attachments.fileItemWrapper', " \
	<div class='ipsUploader__container ipsUploader__container--files'>{{{content}}}</div>\
");

ips.templates.set('core.attachments.fileItem', " \
	<div class='ipsUploader__row ipsUploader__row--file ipsAttach ipsContained {{#done}}ipsAttach_done{{/done}}' id='{{id}}' data-role='file' data-fileid='{{id}}' data-filesize='{{sizeRaw}}' data-filekey='{{securityKey}}'>\
		<div class='ipsUploader__rowPreview ipsType_center' data-role='preview' {{#insertable}}data-action='insertFile'{{/insertable}}>\
			{{#thumb}}\
				{{{thumb}}}\
			{{/thumb}}\
			<div class='ipsUploader__rowPreview__generic ipsFlex ipsFlex-ai:center ipsFlex-jc:center' {{#thumb}}style='display: none'{{/thumb}}>\
				<i class='fa fa-{{extIcon}} ipsType_large'></i>\
			</div>\
		</div>\
		<div class='ipsUploader_rowMeta ipsFlex ipsFlex-flex:11 ipsFlex-fd:column ipsFlex-jc:center ipsFlex-ai:start' {{#insertable}}data-action='insertFile'{{/insertable}}>\
			<h2 class='ipsUploader_rowTitle ipsMargin:none ipsType_reset ipsAttach_title ipsTruncate ipsTruncate_line' data-role='title'>{{title}}</h2>\
			<p class='ipsDataItem_meta ipsType_medium ipsType_light'>\
				{{size}} {{#statusText}}&middot; <span class='ipsType_light' data-role='status'>{{statusText}}</span>{{/statusText}}\
			</p>\
			{{#status}}<span class='ipsAttachment_progress'><span data-role='progressbar'></span></span>{{/status}}\
			<div data-role='insert' class='ipsUploader__rowInsert' {{#insertable}}style='display: none'{{/insertable}}>\
				<a href='#' data-ipsTooltip title='{{#lang}}insertIntoPost{{/lang}}'>\
					{{#lang}}insert{{/lang}}\
				</a>\
			</div>\
		</div>\
		{{#supportsDelete}}\
			<div data-role='deleteFileWrapper' {{#newUpload}}style='display: none'{{/newUpload}}>\
				<input type='hidden' name='{{field_name}}_keep[{{id}}]' value='1'>\
				<a href='#' data-role='deleteFile' class='ipsUploader__rowDelete' data-ipsTooltip title='{{#lang}}attachRemove{{/lang}}'>\
					&times;\
				</a>\
			</div>\
		{{/supportsDelete}}\
		{{^supportsDelete}}\
			<div data-role='deleteFileWrapper' style='display: none'>\
				<input type='hidden' name='{{field_name}}_keep[{{id}}]' value='1'>\
				<a href='#' class='ipsUploader__rowDelete' data-role='deleteFile' data-ipsTooltip title='{{#lang}}attachRemove{{/lang}}'>&times;</a>\
			</div>\
		{{/supportsDelete}}\
	</div>\
");

ips.templates.set('core.attachments.imageItem', " \
	<div class='ipsUploader__row ipsUploader__row--image ipsAttach ipsContained {{#done}}ipsAttach_done{{/done}}' id='{{id}}' data-role='file' data-fileid='{{id}}' data-fullsizeurl='{{imagesrc}}' data-thumbnailurl='{{thumbnail}}' data-fileType='image'>\
		<div class='ipsUploader__rowPreview ipsType_center' data-role='preview' {{#insertable}}data-action='insertFile'{{/insertable}}>\
			{{#thumb}}\
				{{{thumb}}}\
			{{/thumb}}\
			<div class='ipsUploader__rowPreview__generic ipsFlex ipsFlex-ai:center ipsFlex-jc:center' {{#thumb}}style='display: none'{{/thumb}}>\
				<i class='fa fa-{{extIcon}} ipsType_large'></i>\
			</div>\
		</div>\
		<div class='ipsUploader_rowMeta ipsFlex ipsFlex-flex:11 ipsFlex-fd:column ipsFlex-jc:center ipsFlex-ai:start' {{#insertable}}data-action='insertFile'{{/insertable}}>\
			<h2 class='ipsUploader_rowTitle ipsMargin:none ipsType_reset ipsAttach_title ipsTruncate ipsTruncate_line' data-role='title'>{{title}}</h2>\
			<p class='ipsDataItem_meta ipsType_medium ipsType_light'>\
				{{size}} {{#statusText}}&middot; <span class='ipsType_light' data-role='status'>{{statusText}}</span>{{/statusText}}\
			</p>\
			{{#status}}<span class='ipsAttachment_progress'><span data-role='progressbar'></span></span>{{/status}}\
			<div data-role='insert' class='ipsUploader__rowInsert' {{#insertable}}style='display: none'{{/insertable}}>\
				<a href='#' data-ipsTooltip title='{{#lang}}insertIntoPost{{/lang}}'>\
					{{#lang}}insert{{/lang}}\
				</a>\
			</div>\
		</div>\
		{{#supportsDelete}}\
			<div data-role='deleteFileWrapper' {{#newUpload}}style='display: none'{{/newUpload}}>\
				<input type='hidden' name='{{field_name}}_keep[{{id}}]' value='1'>\
				<a href='#' data-role='deleteFile' class='ipsUploader__rowDelete' data-ipsTooltip title='{{#lang}}attachRemove{{/lang}}'>\
					&times;\
				</a>\
			</div>\
		{{/supportsDelete}}\
		{{^supportsDelete}}\
			<div data-role='deleteFileWrapper' style='display: none'>\
				<input type='hidden' name='{{field_name}}_keep[{{id}}]' value='1'>\
				<a href='#' class='ipsUploader__rowDelete' data-role='deleteFile' data-ipsTooltip title='{{#lang}}attachRemove{{/lang}}'>&times;</a>\
			</div>\
		{{/supportsDelete}}\
	</div>\
");

ips.templates.set('core.attachments.videoItem', " \
	<div class='ipsUploader__row ipsUploader__row--image ipsAttach ipsContained {{#done}}ipsAttach_done{{/done}}' id='{{id}}' data-role='file' data-fileid='{{id}}' data-fullsizeurl='{{imagesrc}}' data-thumbnailurl='{{thumbnail}}' data-fileType='video' data-mimeType='{{mime}}'>\
		<div class='ipsUploader__rowPreview ipsType_center' data-role='preview' {{#insertable}}data-action='insertFile'{{/insertable}}>\
			{{#thumb}}\
				<video>\
					<source src='{{{thumb}}}' type='{{mime}}'>\
				</video>\
			{{/thumb}}\
			<div class='ipsUploader__rowPreview__generic ipsFlex ipsFlex-ai:center ipsFlex-jc:center' {{#thumb}}style='display: none'{{/thumb}}>\
				<i class='fa fa-{{extIcon}} ipsType_large'></i>\
			</div>\
		</div>\
		<div class='ipsUploader_rowMeta ipsFlex ipsFlex-flex:11 ipsFlex-fd:column ipsFlex-jc:center ipsFlex-ai:start' {{#insertable}}data-action='insertFile'{{/insertable}}>\
			<h2 class='ipsUploader_rowTitle ipsMargin:none ipsType_reset ipsAttach_title ipsTruncate ipsTruncate_line' data-role='title'>{{title}}</h2>\
			<p class='ipsDataItem_meta ipsType_medium ipsType_light'>\
				{{size}} {{#statusText}}&middot; <span class='ipsType_light' data-role='status'>{{statusText}}</span>{{/statusText}}\
			</p>\
			{{#status}}<span class='ipsAttachment_progress'><span data-role='progressbar'></span></span>{{/status}}\
			<div data-role='insert' class='ipsUploader__rowInsert' {{#insertable}}style='display: none'{{/insertable}}>\
				<a href='#' data-ipsTooltip title='{{#lang}}insertIntoPost{{/lang}}'>\
					{{#lang}}insert{{/lang}}\
				</a>\
			</div>\
		</div>\
		{{#supportsDelete}}\
			<div data-role='deleteFileWrapper' {{#newUpload}}style='display: none'{{/newUpload}}>\
				<input type='hidden' name='{{field_name}}_keep[{{id}}]' value='1'>\
				<a href='#' data-role='deleteFile' class='ipsUploader__rowDelete' data-ipsTooltip title='{{#lang}}attachRemove{{/lang}}'>\
					&times;\
				</a>\
			</div>\
		{{/supportsDelete}}\
		{{^supportsDelete}}\
			<div data-role='deleteFileWrapper' style='display: none'>\
				<input type='hidden' name='{{field_name}}_keep[{{id}}]' value='1'>\
				<a href='#' class='ipsUploader__rowDelete' data-role='deleteFile' data-ipsTooltip title='{{#lang}}attachRemove{{/lang}}'>&times;</a>\
			</div>\
		{{/supportsDelete}}\
	</div>\
");

ips.templates.set('core.attachments.audioItem', " \
	<div class='ipsUploader__row ipsUploader__row--image ipsAttach ipsContained {{#done}}ipsAttach_done{{/done}}' id='{{id}}' data-role='file' data-fileid='{{id}}' data-fullsizeurl='{{imagesrc}}' data-thumbnailurl='{{thumbnail}}' data-fileType='audio' data-mimeType='{{mime}}'>\
		<div class='ipsUploader__rowPreview ipsType_center' data-role='preview' {{#insertable}}data-action='insertFile'{{/insertable}}>\
			{{#thumb}}\
				<audio src='{{{thumb}}}' type='{{mime}}'>\
				</audio>\
			{{/thumb}}\
			<div class='ipsUploader__rowPreview__generic ipsFlex ipsFlex-ai:center ipsFlex-jc:center' {{#thumb}}style='display: none'{{/thumb}}>\
				<i class='fa fa-{{extIcon}} ipsType_large'></i>\
			</div>\
		</div>\
		<div class='ipsUploader_rowMeta ipsFlex ipsFlex-flex:11 ipsFlex-fd:column ipsFlex-jc:center ipsFlex-ai:start' {{#insertable}}data-action='insertFile'{{/insertable}}>\
			<h2 class='ipsUploader_rowTitle ipsMargin:none ipsType_reset ipsAttach_title ipsTruncate ipsTruncate_line' data-role='title'>{{title}}</h2>\
			<p class='ipsDataItem_meta ipsType_medium ipsType_light'>\
				{{size}} {{#statusText}}&middot; <span class='ipsType_light' data-role='status'>{{statusText}}</span>{{/statusText}}\
			</p>\
			{{#status}}<span class='ipsAttachment_progress'><span data-role='progressbar'></span></span>{{/status}}\
			<div data-role='insert' class='ipsUploader__rowInsert' {{#insertable}}style='display: none'{{/insertable}}>\
				<a href='#' data-ipsTooltip title='{{#lang}}insertIntoPost{{/lang}}'>\
					{{#lang}}insert{{/lang}}\
				</a>\
			</div>\
		</div>\
		{{#supportsDelete}}\
			<div data-role='deleteFileWrapper' {{#newUpload}}style='display: none'{{/newUpload}}>\
				<input type='hidden' name='{{field_name}}_keep[{{id}}]' value='1'>\
				<a href='#' data-role='deleteFile' class='ipsUploader__rowDelete' data-ipsTooltip title='{{#lang}}attachRemove{{/lang}}'>\
					&times;\
				</a>\
			</div>\
		{{/supportsDelete}}\
		{{^supportsDelete}}\
			<div data-role='deleteFileWrapper' style='display: none'>\
				<input type='hidden' name='{{field_name}}_keep[{{id}}]' value='1'>\
				<a href='#' class='ipsUploader__rowDelete' data-role='deleteFile' data-ipsTooltip title='{{#lang}}attachRemove{{/lang}}'>&times;</a>\
			</div>\
		{{/supportsDelete}}\
	</div>\
");

ips.templates.set('core.attachments.imageItemWrapper', " \
	<div class='ipsGrid ipsGrid_collapsePhone' data-ipsGrid data-ipsGrid-minItemSize='150' data-ipsGrid-maxItemSize='250'>{{{content}}}</div>\
");

/* FORM TEMPLATES */
ips.templates.set('core.autocomplete.field', " \
	<div class='ipsField_autocomplete' id='{{id}}_wrapper' role='combobox' aria-autocomplete='list' aria-owns='{{id}}_results'>\
		<span class='ipsField_autocomplete_loading' style='display: none' id='{{id}}_loading'></span>\
		<ul class='ipsList_inline' role='listbox'><li id='{{id}}_inputItem' role='option'>{{content}}</li></ul>\
	</div>\
");

ips.templates.set('core.autocomplete.addToken', " \
	<a href='#' data-action='addToken'><i class='fa fa-plus'></i> {{text}}</a> \
");

ips.templates.set('core.autocomplete.resultWrapper', " \
	<div class='ipsAutocompleteMenu' id='{{id}}_results' aria-expanded='false' style='display: none'>\
		<ul class='ipsAutocompleteMenu_itemWrapper ipsList_reset' role='listbox' aria-expanded='false' data-role='items'></ul>\
	</div>\
");

ips.templates.set('core.autocomplete.searchTypeAhead', " \
	<div class='ipsPad_half ipsAreaBackground' data-role='autocompleteSearch'>\
		<div class='ipsClearfix'>\
			<input type='search' name='autocompleteSearch' placeholder='{{#lang}}autocomplete_search_placeholder{{/lang}}'>\
		</div>\
	</div>\
");

ips.templates.set('core.autocomplete.resultItem', " \
	<li class='ipsAutocompleteMenu_item' data-value='{{value}}' role='option'>\
		<div class='ipsClearfix'>\
			{{html}}\
		</div>\
	</li>\
");

ips.templates.set('core.autocomplete.tagsResultItem', " \
	<li class='ipsAutocompleteMenu_item' data-value='{{value}}' role='option'>\
		<div class='ipsClearfix'>\
			{{html}}\
			{{#recommended}}\
				<span class='ipsPos_right ipsType_success'>{{#lang}}tag_recommended{{/lang}}\
			{{/recommended}}\
		</div>\
	</li>\
");

ips.templates.set('core.autocomplete.token', " \
	<li class='cToken' data-value='{{value}}' role='option'>\
		{{{title}}} <span class='cToken_close' data-action='delete'>&times;</span>\
	</li>\
");

ips.templates.set('core.autocomplete.memberItem', " \
	<li class='ipsAutocompleteMenu_item ipsClearfix' data-value=\"{{value}}\" role='option'>\
		<div class='ipsPhotoPanel ipsPhotoPanel_tiny'>\
			<span class='ipsUserPhoto ipsUserPhoto_tiny'><img src='{{{photo}}}' loading='lazy'></span>\
			<div>\
				<strong>{{{name}}}</strong><br>\
				<span class='ipsType_light'>{{{extra}}}</span>\
			</div>\
		</div>\
	</li>\
");

ips.templates.set('core.autocomplete.optional', " \
	<a href='#' data-action='showAutocomplete' class='ipsButton ipsButton_light ipsButton_verySmall'>{{langString}}...</a>\
");

ips.templates.set('core.forms.toggle', " \
	<span class='ipsToggle {{className}}' id='{{id}}' tabindex='0' role='switch' aria-checked='{{status}}'>\
		<span data-role='status'></span>\
	</span>\
");

ips.templates.set('core.forms.validationWrapper', " \
	<ul id='{{id}}' class='ipsList_reset ipsType_small ipsForm_errorList'>{{content}}</ul>\
");

ips.templates.set('core.forms.validationItem', " \
	<li class='ipsType_warning'>{{message}}</li>\
");

ips.templates.set('core.forms.advicePopup', "\
<div class='ipsHovercard' data-role='advicePopup' id='elPasswordAdvice_{{id}}'>\
	<div class='ipsPad'>\
		<h2 class='ipsType_sectionHead'>{{#lang}}password_advice_title{{/lang}}</h2>\
		<p class='ipsSpacer_top ipsSpacer_half ipsType_reset ipsType_medium'>\
			{{#min}}\
				{{min}} \
			{{/min}}\
			{{{text}}}\
		</p>\
	</div>\
	<span class='ipsHovercard_stem'></span>\
</div>\
");

ips.templates.set('core.forms.validateOk', "\
<span>\
	<i class='fa fa-check-circle ipsType_success'></i>\
</span>\
");

ips.templates.set('core.forms.validateFail', "\
<span data-ipsTooltip data-ipsTooltip-label='{{message}}'>\
	<i class='fa fa-times-circle ipsType_warning'></i>\
</span>\
");

ips.templates.set('core.forms.validateFailText', "\
<p class='ipsType_reset ipsSpacer_top ipsSpacer_half ipsType_warning'>\
	<i class='fa fa-times-circle'></i> {{message}}\
</p>\
");

/* TRUNCATE TEMPLATE */
ips.templates.set('core.truncate.expand', " \
	<a class='ipsTruncate_more' data-action='expandTruncate'><span>{{text}} &nbsp;<i class='fa fa-caret-down'></i></span></a>\
");

/* NODE SELECT */
ips.templates.set('core.selectTree.token', " \
<li><span class='ipsSelectTree_token cToken' data-nodeID='{{id}}'>{{title}}</span></li>\
");

/* ACCESSIBILITY KEYBOARD NAV TEMPLATES */
ips.templates.set('core.accessibility.border', " \
<div id='ipsAccessibility_border'></div>\
");

ips.templates.set('core.accessibility.arrow', " \
<div id='ipsAccessibility_arrow'></div>\
");

/* INFINITE SCROLL */
ips.templates.set('core.infScroll.loading', " \
	<li class='ipsPad ipsType_center' data-role='infScroll_loading'>\
		{{#lang}}loading{{/lang}}...\
	</li>\
");

ips.templates.set('core.infScroll.pageBreak', " \
	<li class='ipsPad_half ipsAreaBackground' data-role='infScroll_break' data-infScrollPage='{{page}}'>\
		{{#lang}}page{{/lang}} {{page}}\
	</li>\
");

ips.templates.set('core.pageAction.actionMenuItem', " \
	<li data-role='actionMenu' data-action='{{action}}' id='{{id}}_{{action}}' data-ipsMenu data-ipsMenu-above='force' data-ipsMenu-appendTo='#{{id}}_bar' data-ipsMenu-activeClass='ipsPageAction_active' data-ipsTooltip title='{{title}}' class='ipsHide'>\
		{{#icon}}\
			<i class='fa fa-{{icon}} ipsPageAction_icon'></i> <i class='fa fa-caret-up'></i>\
		{{/icon}}\
		{{^icon}}\
			<span class='ipsPageAction_text'>{{title}} <i class='fa fa-caret-up'></i></span>\
		{{/icon}}\
		<ul id='{{id}}_{{action}}_menu' class='ipsMenu ipsMenu_auto' style='display: none'>\
			{{{menucontent}}}\
		</ul>\
	</li>\
");

ips.templates.set('core.pageAction.actionItem', " \
	<li data-role='actionButton' data-action='{{action}}' id='{{id}}_{{action}}' data-ipsTooltip title='{{title}}'>\
		{{#icon}}\
			<i class='fa fa-{{icon}} ipsPageAction_icon' data-ipsTooltip='{{title}}'></i></i>\
		{{/icon}}\
		{{^icon}}\
			<span class='ipsPageAction_text'>{{title}}</span>\
		{{/icon}}\
	</li>\
");

/* PAGE ACTIONS */
ips.templates.set('core.pageAction.wrapper', " \
	<div class='ipsPageAction' data-role='actionBar' id='{{id}}_bar'>\
		<ul class='ipsList_inline ipsList_reset' data-role='actionItems'>\
			<li>{{{selectedLang}}}</li>\
			{{{content}}}\
		</ul>\
	</div>\
");

/* CAROUSEL */
ips.templates.set('core.carousel.bulletWrapper', "\
	<ul class='ipsCarousel_bullets'>{{content}}</ul>\
");

ips.templates.set('core.carousel.bulletItem', "\
	<li><i class='fa fa-circle'></i></li>\
");

/* RATINGS */
ips.templates.set('core.rating.wrapper', "\
	<div class='ipsClearfix ipsRating'>\
		<ul class='{{className}}' data-role='ratingList'>\
			{{{content}}}\
		</ul>\
	</div>\
	<span data-role='ratingStatus' class='ipsType_light ipsType_medium'>{{status}}</span>\
")

ips.templates.set('core.rating.star', "\
	<li class='{{className}}' data-ratingValue='{{value}}'><a href='#'><i class='fa fa-star'></i></a></li>\
");

ips.templates.set('core.rating.halfStar', "\
	<li class='ipsRating_half' data-ratingValue='{{value}}'><i class='fa fa-star-half'></i><i class='fa fa-star-half fa-flip-horizontal'></i></li>\
");

ips.templates.set('core.rating.loading', "\
	<i class='icon-spinner2 ipsLoading_tinyIcon'></span>\
");

/* SIDEBAR MANAGER */
ips.templates.set('core.sidebar.managerWrapper', " \
	<div id='elSidebarManager' data-role='manager' class='ipsToolbox ipsScrollbar ipsHide'>\
		<div class='ipsPad'>\
			<h3 class='ipsToolbox_title ipsType_reset'>{{#lang}}sidebarManager{{/lang}}</h3>\
			<p class='ipsType_light'>{{#lang}}sidebarManagerDesc{{/lang}}</p>\
			<p class='ipsType_light'>{{#lang}}sidebarManagerDesc2{{/lang}}</p>\
			<div data-role='availableBlocks' class='ipsLoading ipsLoading_dark'></div>\
		</div>\
		<div id='elSidebarManager_submit' class='ipsPad'>\
			<button class='ipsButton ipsButton_important ipsButton_medium ipsButton_fullWidth' data-action='closeSidebar'>{{#lang}}finishEditing{{/lang}}</button>\
		</div>\
	</div>\
");

ips.templates.set('core.sidebar.blockManage', " \
	<div class='cSidebarBlock_managing ipsType_center'>\
		<h4>{{title}}</h4>\
		<a href='#' data-action='removeBlock' data-ipsTooltip title='{{#lang}}removeBlock{{/lang}}'><i class='fa fa-times'></i></a>\
		<button	data-ipsMenu data-ipsMenu-closeOnClick='false' id='{{id}}_edit' data-action='manageBlock' class='ipsButton ipsButton_primary'>\
			<i class='fa fa-pencil'></i> &nbsp;{{#lang}}editBlock{{/lang}}\
		</button>\
		<div class='ipsMenu ipsMenu_wide ipsHide' id='{{id}}_edit_menu'>\
		</div>\
	</div>\
");

ips.templates.set('core.sidebar.blockManageNoConfig', " \
	<div class='cSidebarBlock_managing ipsType_center'>\
		<h4>{{title}}</h4>\
		<a href='#' data-action='removeBlock' data-ipsTooltip title='{{#lang}}removeBlock{{/lang}}'><i class='fa fa-times'></i></a>\
	</div>\
");

ips.templates.set('core.sidebar.blockIsEmpty', " \
	<div class='ipsWidgetBlank ipsPad'>\
		{{text}}\
	</div>\
");

/* FOLLOW BUTTON LOADING */
ips.templates.set('core.follow.loading', " \
<div class='ipsLoading ipsLoading_tiny'></div>\
");

/* STATUS TEMPLATES */
ips.templates.set('core.statuses.loadingComments', " \
	<i class='icon-spinner2 ipsLoading_tinyIcon'></i> &nbsp;<span class='ipsType_light'> &nbsp;{{#lang}}loadingComments{{/lang}}</span>\
");


/* STACKS */
ips.templates.set('core.forms.stack', " \
	<li class='ipsField_stackItem' data-role='stackItem'>\
		<span class='ipsField_stackDrag ipsDrag' data-action='stackDrag'>\
			<i class='fa fa-bars ipsDrag_dragHandle'></i>\
		</span>\
		<a href='#' class='ipsField_stackDelete ipsCursor_pointer' data-action='stackDelete'>\
			&times;\
		</a>\
		<div data-ipsStack-wrapper>\
			{{{field}}}\
		</div>\
	</li>\
");

/* POLLS */
ips.templates.set('core.pollEditor.question', " \
	<div class='ipsAreaBackground_light ipsBox ipsBox_transparent' data-role='question' data-questionID='{{questionID}}'>\
		<div class='ipsAreaBackground ipsPad'>\
			<input type='text' data-role='questionTitle' name='{{pollName}}[questions][{{questionID}}][title]' placeholder='{{#lang}}questionPlaceholder{{/lang}}' class='ipsField_fullWidth' value='{{question}}'>\
		</div>\
		<div>\
			<ul class='ipsDataList cPollChoices' data-role='choices'>\
				<li class='ipsDataItem ipsResponsive_hidePhone'>\
					<p class='ipsDataItem_generic ipsDataItem_size1'>&nbsp;</p>\
					<p class='ipsDataItem_main'><strong>{{#lang}}choicesTitle{{/lang}}</strong></p>\
					{{#showCounts}}\
						<p class='ipsDataItem_generic ipsDataItem_size4'><strong>{{#lang}}votesTitle{{/lang}}</strong></p>\
					{{/showCounts}}\
					<p class='ipsDataItem_generic ipsDataItem_size1'>&nbsp;</p>\
				</li>\
				{{{choices}}}\
			</ul>\
			<br>\
			<div class='ipsDataList'>\
				<p class='ipsDataItem_generic ipsDataItem_size1'>&nbsp;</p>\
				<ul class='ipsDataItem_main ipsList_inline ipsPadding_right:half'>\
					{{#removeQuestion}}<li class='ipsPos_right'><a href='#' data-action='removeQuestion' class='ipsButton ipsButton_verySmall ipsButton_link ipsButton_link--negative'>{{#lang}}removeQuestion{{/lang}}</a></li>{{/removeQuestion}}\
					<li><a href='#' data-action='addChoice' class='ipsButton ipsButton_verySmall ipsButton_link'>{{#lang}}addChoice{{/lang}}</a></li>\
					<li>\
						<span class='ipsCustomInput'>\
							<input type='checkbox' id='elPoll_{{pollName}}_{{questionID}}multi' name='{{pollName}}[questions][{{questionID}}][multichoice]' {{#multiChoice}}checked{{/multiChoice}}>\
							<span></span>\
						</span> <label for='elPoll_{{pollName}}_{{questionID}}multi'>{{#lang}}multipleChoiceQuestion{{/lang}}</label></li>\
					</li>\
				</ul>\
			</div>\
		</div>\
	</div>\
");

ips.templates.set('core.pollEditor.choice', " \
	<li class='ipsDataItem' data-choiceID='{{choiceID}}'>\
		<div class='ipsDataItem_generic ipsDataItem_size1 cPollChoiceNumber ipsType_right ipsType_normal'>\
			<strong data-role='choiceNumber'>{{choiceID}}</strong>\
		</div>\
		<div class='ipsDataItem_main'>\
			<input type='text' name='{{pollName}}[questions][{{questionID}}][answers][{{choiceID}}][value]' value='{{choiceTitle}}' class='ipsField_fullWidth'>\
		</div>\
		<div class='ipsDataItem_generic ipsDataItem_size1'>\
			<a href='#' data-action='removeChoice' class='ipsButton ipsButton_verySmall ipsButton_link ipsButton_link--negative ipsButton_narrow'><i class='fa fa-times'></i></a>\
		</div>\
	</li>\
");

/* COVER PHOTOS */
ips.templates.set('core.coverPhoto.controls', " \
	<ul class='ipsList_reset ipsFlex ipsFlex-ai:center ipsGap:1' data-role='coverPhotoControls'>\
		<li><a href='#' class='ipsButton ipsButton_overlaid ipsButton_small' data-action='cancelPosition'><i class='fa fa-times'></i> {{#lang}}cancel{{/lang}}</a></li>\
		<li><a href='#' class='ipsButton ipsButton_veryLight ipsButton_small' data-action='savePosition'><i class='fa fa-check'></i> {{#lang}}save_position{{/lang}}</a></li>\
	</ul>\
");

/* PATCHWORK */
ips.templates.set('core.patchwork.imageList', " \
	{{#showThumb}}\
		<li class='cGalleryPatchwork_item' style='width: {{dims.width}}px; height: {{dims.height}}px; margin: {{dims.margin}}px {{dims.marginRight}}px {{dims.margin}}px {{dims.marginLeft}}px'>\
	{{/showThumb}}\
	{{^showThumb}}\
		<li class='cGalleryPatchwork_item ipsNoThumb ipsNoThumb_video' style='width: {{dims.width}}px; height: {{dims.height}}px; margin: {{dims.margin}}px {{dims.marginRight}}px {{dims.margin}}px {{dims.marginLeft}}px'>\
	{{/showThumb}}\
		<a href='{{image.url}}'>\
			{{#showThumb}}<img src='{{image.src}}' alt='{{image.title}}' class='cGalleryPatchwork_image'>{{/showThumb}}\
			<div class='ipsPhotoPanel ipsPhotoPanel_mini'>\
				<img src='{{image.author.photo}}' class='ipsUserPhoto ipsUserPhoto_mini'>\
				<div>\
					<span class='ipsType_normal ipsTruncate ipsTruncate_line'>{{image.caption}}</span>\
					<span class='ipsType_small ipsTruncate ipsTruncate_line'>{{#lang}}by{{/lang}} {{image.author.name}}</span>\
				</div>\
			</div>\
			<ul class='ipsList_inline cGalleryPatchwork_stats'>\
				{{#image.unread}}\
					<li class='ipsPos_left'>\
						<span class='ipsItemStatus ipsItemStatus_small' data-ipsTooltip title='{{image.unread}}'><i class='fa fa-circle'></i></span>\
					</li>\
				{{/image.unread}}\
				{{#image.hasState}}\
					<li class='ipsPos_left'>\
						{{#image.state.hidden}}\
							<span class='ipsBadge ipsBadge_icon ipsBadge_small ipsBadge_warning' data-ipsTooltip title='{{#lang}}hidden{{/lang}}'><i class='fa fa-eye-slash'></i></span>\
						{{/image.state.hidden}}\
						{{#image.state.pending}}\
							<span class='ipsBadge ipsBadge_icon ipsBadge_small ipsBadge_warning' data-ipsTooltip title='{{#lang}}pending{{/lang}}'><i class='fa fa-warning'></i></span>\
						{{/image.state.pending}}\
						{{#image.state.pinned}}\
							<span class='ipsBadge ipsBadge_icon ipsBadge_small ipsBadge_positive' data-ipsTooltip title='{{#lang}}pinned{{/lang}}'><i class='fa fa-thumb-tack'></i></span>\
						{{/image.state.pinned}}\
						{{#image.state.featured}}\
							<span class='ipsBadge ipsBadge_icon ipsBadge_small ipsBadge_positive' data-ipsTooltip title='{{#lang}}featured{{/lang}}'><i class='fa fa-star'></i></span>\
						{{/image.state.featured}}\
					</li>\
				{{/image.hasState}}\
				{{#image.allowComments}}\
					<li class='ipsPos_right' data-commentCount='{{image.comments}}'><i class='fa fa-comment'></i> {{image.comments}}</li>\
				{{/image.allowComments}}\
			</ul>\
		</a>\
		{{#image.modActions}}\
			<input type='checkbox' data-role='moderation' name='moderate[{{image.id}}]' data-actions='{{image.modActions}}' data-state='{{image.modStates}}'>\
		{{/image.modActions}}\
	</li>\
");

/* Editor preference */
ips.templates.set('core.editor.preferences', " \
	<div id='editorPreferencesPanel' class='ipsPad'>\
		<div class='ipsMessage ipsMessage_info'> \
			{{#lang}}papt_warning{{/lang}} \
		</div> \
		<br> \
		<ul class='ipsForm ipsForm_vertical'> \
			<li class='ipsFieldRow ipsClearfix'> \
				<div class='ipsFieldRow_content'> \
					<input type='checkbox' {{#checked}}checked{{/checked}} name='papt' id='papt'> \
					<label for='papt'>{{#lang}}papt_label{{/lang}}</label> \
				</div> \
			</li> \
		</ul> \
		<div class='ipsPadding_top:half ipsType_center'> \
			<button role='button' class='ipsButton ipsButton_medium ipsButton_primary' id='papt_submit'>{{#lang}}save_preference{{/lang}}</button> \
		</div> \
	</div> \
");

/* Pagination */
ips.templates.set('core.pagination', " \
	<ul class='ipsPagination' data-ipsPagination data-ipsPagination-pages='{{pages}}'>\
		<li class='ipsPagination_prev'>\
			<a href='#' data-page='prev'><i class='fa fa-caret-left'></i> {{#lang}}prev_page{{/lang}}</a>\
		</li>\
		<li class='ipsPagination_next'>\
			<a href='#' data-page='next'>{{#lang}}next_page{{/lang}} <i class='fa fa-caret-right'></i></a>\
		</li>\
	</ul>\
");

/* selective quoting */
ips.templates.set('core.selection.quote', " \
	<div class='ipsTooltip ipsTooltip_{{direction}} ipsComment_inlineQuoteTooltip' data-role='inlineQuoteTooltip'>\
	    <a href='#' data-action='quoteSelection' class='ipsButton ipsButton_veryVerySmall ipsButton_veryLight'>\
			{{#lang}}quote_selected_text{{/lang}}\
	    </a>\
    </div>\
");

/* Content item selector */
ips.templates.set('core.contentItem.resultItem', " \
	<li class='ipsAutocompleteMenu_item' data-id='{{{id}}}' role='option' role='listitem'>\
		<div class='ipsClearfix'>\
			{{{html}}}\
		</div>\
	</li>\
");

ips.templates.set('core.contentItem.field', " \
	<div class='ipsField_autocomplete' id='{{id}}_wrapper' role='combobox' aria-autocomplete='list' aria-owns='{{id}}_results'>\
		<span class='ipsField_autocomplete_loading' style='display: none' id='{{id}}_loading'></span>\
		<ul class='ipsList_inline'><li id='{{id}}_inputItem'>{{content}}</li></ul>\
	</div>\
");

ips.templates.set('core.contentItem.resultWrapper', " \
	<div class='ipsAutocompleteMenu' id='{{id}}_results' aria-expanded='false' style='display: none'>\
		<ul class='ipsAutocompleteMenu_itemWrapper ipsList_reset' data-role='items'></ul>\
	</div>\
");

ips.templates.set('core.contentItem.item', " \
	<li data-id='{{id}}'>\
		<span class='cContentItem_delete' data-action='delete'>&times;</span> {{{html}}} \
	</li>\
");

/* PROMOTES */
ips.templates.set('promote.imageUpload', " \
	<div class='ipsGrid_span4 cPromote_attachImage' id='{{id}}' data-role='file' data-fileid='{{id}}' data-fullsizeurl='{{imagesrc}}' data-thumbnailurl='{{thumbnail}}' data-fileType='image'>\
		<div class='ipsThumb ipsThumb_bg' data-role='preview' {{#thumbnail}}style='background-image: url( \"{{thumbnail_for_css}}\" )'{{/thumbnail}}>\
			{{#thumbnail}}<img src='{{thumbnail}}' class='ipsImage'>{{/thumbnail}}\
		</div>\
		<ul class='ipsList_inline ipsImageAttach_controls'>\
			<li class='ipsPos_right' {{#newUpload}}style='display: none'{{/newUpload}} data-role='deleteFileWrapper'>\
				<input type='hidden' name='{{field_name}}_keep[{{id}}]' value='1'>\
				<a href='#' data-role='deleteFile' class='ipsButton ipsButton_verySmall ipsButton_light' data-ipsTooltip title='{{#lang}}attachRemove{{/lang}}'><i class='fa fa-trash-o'></i></a>\
			</li>\
		</ul>\
		<span class='ipsAttachment_progress'><span data-role='progressbar'></span></span>\
	</div>\
");

/* TABLE ROW LOADING */
ips.templates.set('table.row.loading', " \
	<li class='ipsDataItem ipsDataItem_loading'>\
		<div>\
			<span></span>\
			<span style='margin-right: {{rnd}}%'></span>\
		</div>\
	</li>\
");

/* LICENSE RENEWAL */
ips.templates.set('licenseRenewal.wrapper', " \
	<div class='acpLicenseRenewal' data-role='licenseRenewal'>\
		<div class='acpLicenseRenewal_wrap'>\
			<div class='acpLicenseRenewal_inner'>\
				<div class='acpLicenseRenewal_content'>\
					<h1 class='acpLicenseRenewal_mainTitle' data-role='mainTitle'>{{#lang}}licenseRenewalTitle{{/lang}}</h1>\
					<p class='ipsType_normal'>{{#lang}}licenseRenewalText{{/lang}}</p>\
					<span class='ipsCustomInput'><input type='checkbox' checked='checked' name='hideRenewalNotice' id='hideRenewalNotice'><span></span></span> <label for='hideRenewalNotice'>{{#lang}}licenseRenewalCheckbox{{/lang}}</label>\
				</div>\
				<ul class='ipsPad ipsToolList ipsToolList_horizontal ipsPos_center ipsList_reset ipsClearfix ipsAreaBackground'>\
					<li>\
						<a href='#' class='ipsButton ipsButton_medium ipsButton_veryLight ipsButton_fullWidth' data-action='closeLicenseRenewal'>{{#lang}}licenseRenewalNo{{/lang}}</a>\
					</li>\
					<li>\
						<a href='#' class='ipsButton ipsButton_medium ipsButton_primary ipsButton_fullWidth' data-role='survey' data-action='closeLicenseRenewal' target='_blank'>{{#lang}}licenseRenewalYes{{/lang}}</a>\
					</li>\
				</ul>\
			</div>\
		</div>\
	</div>\
");

/* Browser Notifications Notices */
ips.templates.set( 'core.browserNotification.prompt' , "\
	<div class='cNotifcationPrompt'>\
		<div class='ipsPadding'>\
			<div class='ipsPhotoPanel ipsPhotoPanel_mini'>\
				<span class='cNotifcationPrompt_icon ipsPos_left'></span>\
				<div>\
					<a href='#' class='cNotifcationPrompt_dismiss' data-role=\"dismissNotification\">×</a>\
					<h3 class='cNotifcationPrompt_title ipsType_large ipsType_sectionHead'>{{#lang}}notificationsCallout{{/lang}}</h3>\
					<p class='cNotifcationPrompt_text ipsType_reset ipsType_medium ipsSpacer_top ipsSpacer_half'>\
						{{#lang}}notificationsDefaultBlurb{{/lang}}\
					</p>\
					<div class='ipsFlex ipsFlex-ai:center ipsGap:2 ipsMargin_top'>\
						<button data-action='browserNotificationPrompt' class='ipsButton ipsButton_small ipsButton_veryLight ipsButton_fullWidth'>{{#lang}}notificationsAllow{{/lang}}</button>\
					</div>\
					<p class='ipsType_small ipsSpacer_both ipsSpacer_half ipsHide' data-role='promptMessage'>\
						{{#lang}}notificationsAllowPrompt{{/lang}}\
					</p>\
				</div>\
			</div>\
		</div>\
	</div>\
");

ips.templates.set( 'core.browserNotification.missingSubscription' , "\
	<div class='cNotifcationPrompt'>\
		<div class='ipsPadding'>\
			<div class='ipsPhotoPanel ipsPhotoPanel_mini'>\
				<span class='cNotifcationPrompt_icon ipsPos_left'></span>\
				<div>\
					<a href='#' class='cNotifcationPrompt_dismiss' data-role=\"dismissNotification\">×</a>\
					<h3 class='cNotifcationPrompt_title ipsType_large ipsType_sectionHead'>{{#lang}}notificationsCalloutPush{{/lang}}</h3>\
					<p class='cNotifcationPrompt_text ipsType_reset ipsType_medium ipsMargin_vertical:half'>\
						{{#lang}}notificationsDefaultBlurb{{/lang}}\
					</p>\
					<p class='cNotifcationPrompt_text ipsType_reset ipsType_small ipsMargin_vertical:half'>\
						{{#lang}}notificationsUpgradeBlurb{{/lang}}\
					</p>\
					<div class='ipsFlex ipsFlex-ai:center ipsGap:4 ipsGap_row:0 ipsMargin_top ipsType_blendLinks'>\
						<button data-action='browserNotificationPrompt' class='ipsButton ipsButton_small ipsButton_veryLight ipsButton_fullWidth'>{{#lang}}notificationsAllow{{/lang}}</button>\
						<a href='#' data-action='rejectPush' class='ipsFlex-flex:00'>{{#lang}}notificationsNoThanks{{/lang}}</a>\
					</div>\
					<p class='ipsType_small ipsSpacer_both ipsSpacer_half ipsHide' data-role='promptMessage'>\
						{{#lang}}notificationsAllowPrompt{{/lang}}\
					</p>\
				</div>\
			</div>\
		</div>\
	</div>\
");

ips.templates.set( 'core.notifications.pending' , "\
	<span class='ipsType_light'>{{#lang}}notificationsEnabling{{/lang}}</span>\
");
ips.templates.set( 'core.notifications.success' , "\
	<span class='ipsType_success'><i class='fa fa-fw fa-check'></i> {{#lang}}notificationsEnabled{{/lang}}</span>\
");
ips.templates.set( 'core.notifications.fail' , "\
	<span class='ipsType_negative'><i class='fa fa-fw fa-times'></i> {{#lang}}notificationsFailed{{/lang}}</span>\
");
ips.templates.set( 'core.notifications.notSupported' , "\
	<span class='ipsType_light'><i class='fa fa-fw fa-times'></i> {{#lang}}notificationsNotSupported{{/lang}}</span>\
");
ips.templates.set( 'core.notifications.checking' , "\
	<span class='ipsType_light'>{{#lang}}notificationsChecking{{/lang}}</span>\
");

/* Warning form pre-set action list */
ips.templates.set('system.warningpenalty.nomodify', "\
	<ul class='ipsList_bullets' id='elWarningPenalties'>\
		{{#penalties}}\
		<li>{{.}}</li>\
		{{/penalties}}\
	</ul>\
");

ips.templates.set('core.edittags.default', "\
	<div class='ipsPad'>\
		<span><i class='icon-spinner2 ipsLoading_tinyIcon'></i>  &nbsp;{{#lang}}loading{{/lang}}</span>\
	</div>\
");

ips.templates.set('core.onlineUser.linked', "\
<li>\
	<a href='{{memberUrl}}' data-ipsHover data-ipsHover-target='{{memberHovercardUrl}}'>{{{formattedName}}}</a>\
</li>");]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="templates" javascript_name="ips.templates.clubs.js" javascript_type="template" javascript_version="107643" javascript_position="1000700"><![CDATA[ips.templates.set('club.request.approve', "\
	<span class='cClubRequestCover_icon ipsAreaBackground_positive'>\
		<i class='fa fa-check'></i>\
	</span>\
	<br>\
	<span class='ipsBadge ipsBadge_large ipsBadge_positive'>{{#lang}}clubRequestApproved{{/lang}}</span>\
");

ips.templates.set('club.request.decline', "\
	<span class='cClubRequestCover_icon ipsAreaBackground_negative'>\
		<i class='fa fa-times'></i>\
	</span>\
	<br>\
	<span class='ipsBadge ipsBadge_large ipsBadge_negative'>{{#lang}}clubRequestDenied{{/lang}}</span>\
");

ips.templates.set('club.menu.dragHandle', "\
	<span data-role='clubMenuDrag' style='display: none'><i class='fa fa-bars'></i> &nbsp;</span>\
");]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="templates" javascript_name="ips.templates.messages.js" javascript_type="template" javascript_version="107643" javascript_position="1000700"><![CDATA[/* VIEW TEMPLATES */
ips.templates.set('messages.view.placeholder', " \
<div class='ipsType_center ipsType_large cMessageView_inactive ipsEmpty'>\
	<i class='fa fa-envelope'></i><br>\
	{{#lang}}no_message_selected{{/lang}}\
</div>\
");

ips.templates.set('messages.main.folderMenu', "\
<li class='ipsMenu_item' data-ipsMenuValue='{{key}}'><a href='#'><span class='ipsMenu_itemCount'>{{count}}</span> <span data-role='folderName'>{{name}}</span></a></li>\
");]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="templates" javascript_name="ips.templates.streams.js" javascript_type="template" javascript_version="107643" javascript_position="1000700"><![CDATA[ips.templates.set('core.streams.teaser', "\
	<li data-action='insertNewItems' class='ipsStreamItem_loadMore ipsBox ipsPadding:half' style='display: none'>\
		<button class='ipsButton ipsButton_light ipsButton_fullWidth ipsButton_medium'>{{{words}}}</button>\
	</li>\
");

ips.templates.set('core.streams.unreadBar', "\
	<li data-role='unreadBar' class='ipsStreamItem_bar'><hr class='ipsHr'></li>\
");

ips.templates.set('core.streams.noMore', "\
	<li class='ipsType_center ipsType_light ipsType_medium ipsPad' data-role=\"loadMoreContainer\">\
		{{#lang}}noMoreActivity{{/lang}}\
	</li>\
");

ips.templates.set('core.streams.loadMore', "\
	<a href='#' class='ipsButton ipsButton_veryLight ipsButton_small' data-action='loadMore'>{{#lang}}loadNewActivity{{/lang}}</a>\
");]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="templates" javascript_name="ips.templates.system.js" javascript_type="template" javascript_version="107643" javascript_position="1000700"><![CDATA[ips.templates.set('follow.frequency', "\
	{{#hasNotifications}}\
		<i class='fa fa-bell'></i>\
	{{/hasNotifications}}\
	{{^hasNotifications}}\
		<i class='fa fa-bell-slash-o'></i>\
	{{/hasNotifications}}\
	{{text}}\
");]]></file>
 <file javascript_app="core" javascript_location="front" javascript_path="templates" javascript_name="ips.templates.vse.js" javascript_type="template" javascript_version="107643" javascript_position="1000700"><![CDATA[/* CLASS LIST TEMPLATES */
ips.templates.set('vse.classes.title', " \
<li class='ipsToolbox_sectionTitle ipsType_reset' data-role='{{role}}'>{{title}}</li>\
");

ips.templates.set('vse.classes.item', " \
<li data-styleID='{{styleid}}' data-themeKey='{{themekey}}'>\
	{{#swatch.back}}\
		<input class='vseClass_swatch vseClass_swatch--back' value='{{swatch.back.color}}' data-key='{{swatch.back.key}}'>\
	{{/swatch.back}}\
	{{^swatch.back}}\
	 	<span class='vseClass_swatch vseClass_swatch--back vseClass_swatch--noStyle'>&times;</span>\
	{{/swatch.back}}\
	{{#swatch.fore}}\
		<input class='vseClass_swatch vseClass_swatch--fore' value='{{swatch.fore.color}}' data-key='{{swatch.fore.key}}'>\
	{{/swatch.fore}}\
	{{^swatch.fore}}\
	 	<span class='vseClass_swatch vseClass_swatch--fore vseClass_swatch--noStyle'>&times;</span>\
	{{/swatch.fore}}\
	{{title}}\
</li>\
");

ips.templates.set('vse.panels.header', " \
	<h2 class='ipsType_sectionHead'>{{title}}</h2>\
	{{#desc}}\
		<p class='ipsType_reset ipsType_light ipsType_small'>\
			{{desc}}\
		</p>\
	{{/desc}}\
	<br>\
");

ips.templates.set('vse.panels.wrapper', " \
	<div class='vseStyleSection' data-role='{{type}}Panel'>\
		{{{content}}}\
	</div>\
");

ips.templates.set('vse.panels.background', " \
	<h3>{{#lang}}vseBackground{{/lang}}</h3>\
	<div data-role='backgroundControls' class='ipsGrid'>\
		<div class='ipsGrid_span3'>\
			<div data-role='backgroundPreview' class='vseBackground_preview'>&nbsp;</div>\
		</div>\
		<div class='ipsGrid_span9'>\
			<input type='text' class='ipsField_fullWidth color vseBackground_color' data-role='backgroundColor' value='{{backgroundColor}}'>\
			<br>\
			<div class='ipsGrid'>\
				<!--<div class='ipsGrid_span6'>\
					<button data-ipsTooltip title='{{#lang}}vseBackground_image{{/lang}}' class='ipsButton ipsButton_primary ipsButton_verySmall ipsButton_fullWidth ipsType_center ipsType_large'><i class='fa fa-picture-o'></i></button>\
				</div>-->\
				<div class='ipsGrid_span6'>\
					<button data-ipsTooltip title='{{#lang}}vseBackground_gradient{{/lang}}' data-action='launchGradientEditor' class='ipsButton ipsButton_primary ipsButton_verySmall ipsButton_fullWidth ipsType_center ipsType_large'><i class='fa fa-barcode'></i></button>\
				</div>\
			</div>\
		</div>\
	</div>\
");

ips.templates.set('vse.panels.font', " \
	<h3>{{#lang}}vseFont_color{{/lang}}</h3>\
	<input type='text' class='ipsField_fullWidth color' data-role='fontColor' value='{{fontColor}}'>\
");

ips.templates.set('vse.gradient.editor', " \
	<div data-role='gradientPreview' class='vseBackground_gradient'></div>\
	<div class='ipsGrid'>\
		<button data-action='gradientAngle' data-angle='90' class='ipsButton ipsButton_primary ipsButton_verySmall ipsGrid_span3'>\
				<i class='fa fa-arrow-down'></i>\
		</button>\
		<button data-action='gradientAngle' data-angle='0' class='ipsButton ipsButton_primary ipsButton_verySmall ipsGrid_span3'>\
			<i class='fa fa-arrow-left'></i>\
		</button>\
		<button data-action='gradientAngle' data-angle='45' class='ipsButton ipsButton_primary ipsButton_verySmall ipsGrid_span3'>\
			<i class='fa fa-arrow-up'></i>\
		</button>\
		<button data-action='gradientAngle' data-angle='120' class='ipsButton ipsButton_primary ipsButton_verySmall ipsGrid_span3'>\
			<i class='fa fa-arrow-right'></i>\
		</button>\
	</div>\
	<hr class='ipsHr'>\
	<ul class='ipsList_reset' data-role='gradientStops'>\
		<li class='ipsGrid'>\
			<p class='ipsType_reset ipsGrid_span1'>&nbsp;</p>\
			<p class='ipsType_reset ipsType_light ipsType_small ipsGrid_span5'>{{#lang}}vseGradient_color{{/lang}}</p>\
			<p class='ipsType_reset ipsType_light ipsType_small ipsGrid_span6'>{{#lang}}vseGradient_position{{/lang}}</p>\
		</li>\
		<li class='ipsGrid'>\
			<p class='ipsType_reset ipsGrid_span1'>&nbsp;</p>\
			<p class='ipsType_reset ipsGrid_span11'><a href='#' class='ipsType_medium' data-action='gradientAddStop'>{{#lang}}vseAddStop{{/lang}}</a></p>\
		</li>\
	</ul>\
	<hr class='ipsHr'>\
	<div class='ipsGrid'>\
		{{{buttons}}}\
	</div>\
");

ips.templates.set('vse.gradient.twoButtons', "\
	<button data-action='saveGradient' class='ipsGrid_span8 ipsButton ipsButton_normal ipsButton_verySmall ipsButton_fullWidth'>{{#lang}}vseGradient_save{{/lang}}</button>\
	<button data-action='cancelGradient' class='ipsGrid_span4 ipsButton ipsButton_normal ipsButton_verySmall ipsButton_fullWidth'>{{#lang}}vseCancel{{/lang}}</button>\
");

ips.templates.set('vse.gradient.threeButtons', "\
	<button data-action='saveGradient' class='ipsGrid_span4 ipsButton ipsButton_normal ipsButton_verySmall ipsButton_fullWidth'>{{#lang}}vseSave{{/lang}}</button>\
	<button data-action='cancelGradient' class='ipsGrid_span4 ipsButton ipsButton_normal ipsButton_verySmall ipsButton_fullWidth'>{{#lang}}vseCancel{{/lang}}</button>\
	<button data-action='removeGradient' class='ipsGrid_span4 ipsButton ipsButton_important ipsButton_verySmall ipsButton_fullWidth'>{{#lang}}vseDelete{{/lang}}</button>\
");

ips.templates.set('vse.gradient.stop', " \
	<li class='ipsGrid'>\
		<span class='ipsGrid_span1 ipsType_light ipsType_center'><i class='fa fa-bars'></i></span>\
		<input type='text' class='ipsGrid_span5' value='{{color}}' maxlength='6' pattern='^([0-9a-zA-Z]{6})$'>\
		<input type='range' class='ipsGrid_span5' min='0' max='100' value='{{location}}'>\
		<p class='ipsType_reset ipsType_center ipsGrid_span1'><a href='#' data-action='gradientRemoveStop'><i class='fa fa-times'></i></a></p>\
	</li>\
");

ips.templates.set('vse.colorizer.panel', " \
	<p class='ipsType_light ipsPad'>\
		{{#lang}}vseColorizer_desc{{/lang}}\
	</p>\
	<div class='ipsPad'>\
		<div class='ipsGrid'>\
			<div class='ipsGrid_span5 ipsType_center'>\
				<input type='text' class='vseColorizer_swatch color' data-role='primaryColor' value='{{primaryColor}}'>\
				<span class='ipsType_light'>{{#lang}}vseColorizer_primary{{/lang}}</span>\
			</div>\
			<div class='ipsGrid_span2'></div>\
			<div class='ipsGrid_span5 ipsType_center'>\
				<input type='text' class='vseColorizer_swatch color' data-role='secondaryColor' value='{{secondaryColor}}'>\
				<span class='ipsType_light'>{{#lang}}vseColorizer_secondary{{/lang}}</span>\
			</div>\
		</div>\
		<br>\
		<div class='ipsGrid_span4 ipsType_center'>\
			<input type='text' class='vseColorizer_swatch color' data-role='textColor' value='{{textColor}}'>\
			<span class='ipsType_light'>{{#lang}}vseColorizer_text{{/lang}}</span>\
		</div>\
		<br><br>\
		<button class='ipsButton ipsButton_veryLight ipsButton_small ipsButton_fullWidth' data-action='invertColors'>{{#lang}}vseColorizer_invert{{/lang}}</button>\
		<br>\
		<button class='ipsButton ipsButton_veryLight ipsButton_small ipsButton_fullWidth' data-action='revertColorizer' disabled>{{#lang}}vseColorizer_revert{{/lang}}</button>\
	</div>\
");]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="ui" javascript_name="ips.ui.controlStrip.js" javascript_type="ui" javascript_version="107643" javascript_position="1000200"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.controlStrip.js - Handles functionality for control strips (button rows) in the AdminCP
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.controlStrip', function(){

		var respond = function (elem, options) {
			if( !$( elem ).data('_controlStrip') ){
				$( elem ).data('_controlStrip', controlStripObj( elem ) );
			}
		};

		ips.ui.registerWidget( 'controlStrip', ips.ui.controlStrip );

		return {
			respond: respond
		};
	});

	/**
	 * Control strip instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @returns {void}
	 */
	var controlStripObj = function (elem) {

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			var buttons = elem.find('.ipsControlStrip_button:not(.ipsJS_hide)');

			if( buttons.length > 3 ){
				_buildMenu( buttons );
			}

			// Set up the events we'll handle here
			elem
				.on( 'click', '[data-replace], [data-remove], [data-bubble]', _remoteAction );
		},

		_remoteAction = function (e) {
			e.preventDefault();
			var link = $( e.currentTarget );
			var url = link.attr('href');

			ips.getAjax()( url, {
				dataType: 'json',
				showLoading: true
			})
				.done( function (response) {
					if( link.is('[data-replace]') ){
						_replaceButton( link, response );
					} else if( link.is('[data-remove]') ){
						_removeButton( link, response );
					} else if( link.is('[data-bubble]') ){
						_bubbleAction( link, response );
					}
				})
				.fail( function () {
					window.location = url;
				});
		},

		/**
		 * Simply triggers an event that bubbles up, which can be caught by page controllers/widgets.
		 *
		 * @param	{element}	link 		Link element that was clicked
		 * @param	{object} 	response 	Response object from the ajax request
		 * @returns {void}
		 */
		_bubbleAction = function (link, response) {
			link.trigger( 'buttonAction', response );
		},

		/**
		 * Event handler for a button that replaces itself with a different button when clicked
		 *
		 * @param	{element}	link 		Link element that was clicked
		 * @param	{object} 	response 	Response object from the ajax request
		 * @returns {void}
		 */
		_replaceButton = function (link, response) {
			ips.ui.flashMsg.show( response );

			var item = link.closest('.ipsControlStrip_button, .ipsControlStrip_menuItem');
			var newItem = $('#' + link.attr('data-replacewith') );

			if( item.hasClass('ipsControlStrip_button') ){
				item.hide();
				newItem.show();
			} else {
				if( !newItem.hasClass('ipsControlStrip_menuItem') ){
					var newHTML = _getMenuItemFromButton( newItem );
					item.hide().after( newHTML );
				} else {
					item.hide();
					newItem.show();
				}
			}
		},

		/**
		 * Event handler for a button that removes itself after being clicked
		 *
		 * @param	{element} 	link 		Link element that was clicked
		 * @param	{object} 	response	Response object from the ajax request
		 * @returns {void}
		 */
		_removeButton = function (link, response) {
			ips.ui.flashMsg.show( response );

			var dropdown = $( elem ).find('[data-dropdown]');

			// Do we have any others to remove too?
			if( link.attr('data-alsoremove') ){
				var also = ips.utils.getIDsFromList( link.attr('data-alsoremove') );
			}

			// Get a jquery object containing the buttons or menu items for each item to remove
			var toRemove = $( link ).add( also || '' ).closest('.ipsControlStrip_button, .ipsControlStrip_menuItem');
			// .. and then remove them.
			toRemove.remove();

			// See if we need to remove the menu & dropdown
			if( dropdown.length ){
				var menu = $( dropdown.attr('id') + '_menu' );

				if( !menu.find('.ipsControlStrip_menuItem').length ){
					menu.remove();
					dropdown.remove();
				}
			}
		},

		/**
		 * Builds a dropdown menu by slicing off excess menu items, and manipulating the links to 
		 * turn them into menu items. Then an ipsMenu widget is created to control the menu.
		 *
		 * @param	{array} 	buttons 	jQuery array of buttons in the control strip
		 * @returns {void}
		 */
		_buildMenu = function (buttons) {
			var buttonsToMove = buttons.slice(2);
			var menu = ips.templates.render('core.controlStrip.menu', {
				id: elem.identify().attr('id') + '_more',
				content: _moveButtonsToMenu( buttonsToMove )
			});

			$( elem ).after( menu );

			// Remove buttons
			buttonsToMove.remove();

			// Add a menu dropdown to the strip and set up the menu
			elem
				.css({ position: 'relative' })
				.find('.ipsControlStrip_button')
					.last()
					.after( ips.templates.render('core.controlStrip.dropdown', {
						id: elem.identify().attr('id') + '_more'
					}));
			
			elem
				.parent()
					.wrapInner( $('<div/>').css( { position: 'relative' } ) ) // wrapInner so that the menu is positioned properly in firefox
					.find('[data-dropdown]')
						.attr('aria-haspopup', 'true')
						.ipsMenu( {	
							appendTo: '#' + elem.parent().identify().attr('id') 
						});

			$( document ).trigger( 'contentChange', [ elem ] );
		},

		/**
		 * Creates dropdown menu items for each of the buttons in the provided array
		 *
		 * @param	{array} 	buttons 	jQuery array of buttons to be turned into menu items
		 * @returns {string} 	Menu contents (all items concatenated into a string)
		 */
		_moveButtonsToMenu = function (buttons) {
			var menuContent = '';

			for (var i = 0; i < buttons.length; i++){
				menuContent += _getMenuItemFromButton( buttons[i] );
			}

			return menuContent;
		},

		/**
		 * Builds an individual menu item from a provided button
		 *
		 * @param	{element} 	button 	Button element to build from
		 * @returns {string} 	Menu item HTML
		 */
		_getMenuItemFromButton = function (button) {
			var buttonLink = $( button ).find('> a');
			//buttonLink.find('.ipsControlStrip_icon').after( '&nbsp;&nbsp;' + buttonLink.attr('title') );
			$( button ).find('[data-ipsTooltip]').removeAttr('data-ipsTooltip');

			return ips.templates.render('core.controlStrip.menuItem', {
				id: $( button ).attr('id') || '',
				item: $( button ).html()
			});
		};

		init();

		return {
		
		};
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="ui" javascript_name="ips.ui.customtags.js" javascript_type="ui" javascript_version="107643" javascript_position="1000200"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.customtags.js - Controller for inserting custom tags into textareas - custom tags are defined by data-textareacustomtag attributes on elements.
 *
 * Author: Rikki Tissier & Brandon Farber
 */
;( function($, _, undefined){
	"use strict";

	ips.controller.register('textarea.customtags', {

		initialize: function () {
			this.on( 'click', '[data-textareacustomtag]', this.insertTag );
		},

		/**
		 * Event handler for inserting custom tags defined on the page
		 *
		 * @param 		{event} 	e 		Event object
		 * @returns 	{void}
		 */
		insertTag: function (e) {
			console.log( 'Inserting custom tag: ' + $( e.currentTarget ).attr('data-textareacustomtag') );

			$( '#' + this.scope.data('textareaid') ).focus();
			$( '#' + this.scope.data('textareaid') ).insertText( $( e.currentTarget ).attr('data-textareacustomtag'),
				$( '#' + this.scope.data('textareaid') ).getSelection().start,
				"collapseToEnd" );
		}
	});
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="ui" javascript_name="ips.ui.matrix.js" javascript_type="ui" javascript_version="107643" javascript_position="1000200"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.matrix.js - Matrix widget for the AdminCP permissions systems
 *
 * Author: Mark Wade & Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.matrix', function(){

		var defaults = {
			manageable: true,
			sortable: false,
			squashFields: false
		};

		var respond = function (elem, options) {
			var matrix = $( elem ).data('_matrix');
			if( !matrix ){
				$( elem ).data('_matrix', matrixObj(elem, _.defaults( options, defaults ) ) );
			} else {
				matrix.checkRows();
			}
		},
		refresh = function (elem) {
			try {
				var obj = $( elem ).data('_matrix');
				obj.checkRows();
			} catch (err) {
				Debug.log("Couldn't refresh matrix " + $( elem ).identify().attr('id') );
			}
		};

		ips.ui.registerWidget( 'matrix', ips.ui.matrix, [ 'manageable', 'sortable', 'squashFields' ] );

		return {
			respond: respond,
			refresh: refresh
		};
	});

	/**
	 * Matrix instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var matrixObj = function (elem, options) {

		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {
			_setUpEvent();

			if( options.manageable ){
				_setUpManageable();
			}
			
			if( options.sortable ){
				ips.loader.get( ['core/interface/jquery/jquery-ui.js'] ).then( function () {
					elem.find('tbody').sortable( {
						handle: '.ipsTree_drag'
					});
				});
			}

			_checkRows();
		},

		/**
		 * Sets up the various events the matrix needs
		 *
		 * @returns {void}
		 */
		_setUpEvent = function () {
			elem.on( 'click', 'td, th', _clickCell );
			elem.on( 'click', 'td input, th input', _clickInputInCell );
			elem.on( 'click', '.matrixAdd', _addRow );
			elem.on( 'click', '.matrixDelete', _deleteRow );
			elem.on( 'click', '[data-action="checkRow"]', _checkRow );
			elem.on( 'click', '[data-action="unCheckRow"]', _unCheckRow );
			elem.on( 'change', '[data-action="checkAll"]', _checkAll ); 
			elem.on( 'change', 'td input[type="checkbox"]', _checkboxChanged );
			elem.closest('form').on( 'submit', _submitForm );
			$( document ).on( 'tabShown', function () {
				_checkRows();
			});
		},

		/**
		 * Called when any cell checkbox is checked. Checks all checkboxes in the column to see if all are checked.
		 * If all are checked, checks the column header. Otherwise, unchecks column header.
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_checkboxChanged = function (e){
			// Which column are we in?
			var col = $( e.currentTarget ).closest('[data-col]').attr('data-col');
			var colHead = elem.find('[data-checkallheader="' + col + '"]');

			if( _.isUndefined( col ) || !elem.find('[data-checkallheader="' + col + '"]').length ){
				return;
			}

			// Get all checkboxes with the same column key
			var similar = elem.find('[data-col="' + col + '"] input[type="checkbox"]');

			colHead.prop('checked', similar.filter(':checked').length == similar.length );
		},

		/**
		 * Event handler for clicking in a cell
		 * Check a checkbox if it exists in a cell, otherwise focus the input
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_clickCell = function (e) {
										
			// find input
			if( !$( e.target ).is('td') && !$( e.target ).is('th') ){
				return;
			}

			var input = $( e.currentTarget ).find('input:not([type="hidden"]),select,textarea');

			if( input.attr('type') == 'checkbox' ){
				input.click();
			} else {
				input.focus();
			}
		},
		
		/**
		 * Checks all checkboxes in the row
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_checkRow = function (e) {
			e.preventDefault();

			$( e.target )
				.closest('tr')
				.find('input[type="checkbox"]:not(:disabled)')
					.prop( 'checked', true )
					.trigger('change');
		},
		
		/**
		 * Unchecks all checkboxes in the row
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_unCheckRow = function (e) {
			e.preventDefault();

			$( e.target )
				.closest('tr')
				.find('input[type="checkbox"]:not(:disabled)')
					.prop( 'checked', false )
					.trigger('change');
		},

		/**
		 * Checks all checkboxes that match the column
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_checkAll = function (e) {
			var regex = '^.*\\[' + $(this).attr('data-checkallheader') + '_checkbox\\]$';

			$(this).closest( 'table.ipsMatrix' ).find( 'input[type="checkbox"]:not(:disabled)' ).filter( function () {
				return $(this).attr('name').match( regex );
			} ).prop( 'checked', $(this).is(':checked') );
		},

		/**
		 * Event handler for deleting a row of the matrix
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_deleteRow = function (e) {
			e.preventDefault();
			var row = $( this ).closest('tr');

			// Change the value of the hidden input
			row.closest('form').find('input[data-matrixrowid="' + row.attr('data-matrixrowid') + '"]').val( 0 );

			// Fade it out them remove
			ips.utils.anim.go( 'fadeOut', row )
				.done( function () {
					row.remove();
					_checkRows();
				});
		},

		/**
		 * Event handler for the Add Row button
		 * Adds a new row by cloning the blank row
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_addRow = function (e) {
			var table = elem.find( '.ipsTable[data-matrixID="' + $( this ).attr('data-matrixID') + '"]' );
			var blankRow = table.find('tbody tr:not( .ipsMatrix_empty ):last-child');

			// Clone the blank row and insert the copy to form our new row
			var newRow = blankRow.clone();
			newRow.insertBefore( blankRow );
			
			// Rename the form fields inside the new row
			var index = newRow.index();
			newRow.find('input,textarea,select,option').each( function () {
				var input = $( this );

				if( input.attr( 'name' ) ){
					input.attr( 'name', input.attr( 'name' ).replace( /_new_\[x\]/g, '_new_[' + index + ']' ) ).show();
				}

				if( input.attr( 'id' ) ){
					input.attr( 'id', input.attr( 'id' ).replace( /_new__x_/g, '_new__' + index + '_' ) );
				}

				if( input.attr( 'data-toggles' ) ){
					input.attr( 'data-toggles', input.attr( 'data-toggles' ).replace( /_new__x_/g, '_new__' + index + '_' ) );
				}

				if( input.attr( 'data-toggle-id' ) ){
					input.attr( 'data-toggle-id', input.attr( 'data-toggle-id' ).replace( /_new__x_/g, '_new__' + index + '_' ) );
				}

				// Allow color fields to reinit
				if( input.attr( 'data-ipsFormData' ) ){
					input.removeAttr( 'data-ipsFormData' );
				}
			});

			// Remove dummy yes/no toggle
			newRow.find('#check__new__x__yesno__wrapper').remove();

			// Animate
			ips.utils.anim.go( 'fadeIn', newRow )
				.done( function () {
					// Hide the empty row if necessary
					_checkRows();
				});
				
			// Let the document know
			$( document ).trigger( 'contentChange', [ newRow ] );

			// Scroll to it
			$('html, body').animate( { scrollTop: String(newRow.offset().top) } );

			newRow.find('input,textarea,select').first().focus();
			
			return false;
		},

		/**
		 * Shows the 'empty' row if there's no real rows
		 *
		 * @returns {void}
		 */
		_checkRows = function () {
			if( elem.find('[data-matrixrowid]:visible').length > 0 ){
				elem.find('.ipsMatrix_empty').addClass('ipsHide');
			} else {
				elem.find('.ipsMatrix_empty').removeClass('ipsHide');
			}
		},

		/**
		 * Event handler for clicking an input within a cell
		 * Simply stops propagation
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_clickInputInCell = function (e) {
			e.stopPropagation();
		},

		/**
		 * Hooks into the submit event for the form, to wipe out the name on the blank row inputs
		 *
		 * @param 	{event} 	e 	Event object
		 * @returns {void}
		 */
		_submitForm = function (e) {
			// Remove names from the inputs in the blank row
			elem.find('[data-matrixrowid]:hidden')
				.find('input, select, textarea')
				.attr( 'name', '' )
				.prop( 'disabled', true );

			// Are we squashing fields?
			if( !options.squashFields ){
				return;
			}

			// Get all values from the matrix
			var formElements = elem.find('[data-matrixid] *').filter(':input:enabled:not([data-role="noMatrixSquash"])');
			var output = ips.utils.form.serializeAsObject( formElements );
			var matrixID = elem.find('[data-matrixid]').attr('data-matrixid');
			var newInput = $('<input />').attr('type', 'hidden').attr('name', matrixID + '_squashed');

			// JSON encode the data
			Debug.log("Before encoding, matrix data is:");
			Debug.log( output );			
			output = JSON.stringify( output );

			// Add a new hidden form field
			elem.prepend( newInput.val( output ) );

			// Disable all of the elements we squashed so that they don't get sent
			formElements.prop('disabled', true);
		},

		/**
		 * Initializes the blank row by removing the required attribute, and hiding it
		 *
		 * @returns {void}
		 */
		_setUpManageable = function () {
			elem.find('tr:last-child').find('input, select[required], textarea').removeAttr('required');
			elem.find('tbody tr:not( .ipsMatrix_empty ):last-child').hide();
		};

		init();

		return {
			init: init,
			checkRows: _checkRows
		};
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="admin" javascript_path="ui" javascript_name="ips.ui.statusToggle.js" javascript_type="ui" javascript_version="107643" javascript_position="1000200">/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.statusToggle.js - Toggles things between enabled/disabled, online/offline etc.
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	&quot;use strict&quot;;

	ips.createModule('ips.ui.statusToggle', function(){

		/**
		 * Respond method for statusToggles
		 * Finds the active item, and determines the next state. Fires an ajax request to set the state,
		 * and updates the badge shown as needed.
		 *
		 * @param		{element} 	elem 		The element the widget is registered on
		 * @param		{object} 	options		Options object for this widget instance
		 * @param 		{event} 	e 			Event object
		 * @returns 	{void}
		 */
		var respond = function (elem, options, e) {
			e.preventDefault();

			elem = $( elem );
			
			if( elem.attr('data-loading') ){
				return;
			}

			// What's selected now?
			var currentBadge = elem.find('[data-state]:visible');
			var currentState = currentBadge.attr('data-state');
			var url = currentBadge.attr('href');
			
			// Don't do anything if the button opens a dialog
			if ( currentBadge.attr('data-ipsdialog' ) ) {
				return;
			}

			var nextState;

			if( options.intermediate ){
				nextState = ( currentState == 'enabled' ) ? 'intermediate' : ( currentState == 'disabled' ) ? 'enabled' : 'disabled';
			} else {
				nextState = ( currentState == 'enabled' ) ? 'disabled' : 'enabled';
			}

			var nextBadge = elem.find('[data-state=&quot;' + nextState + '&quot;]');

			if( !nextBadge.length ){
				Debug.warn( &quot;No badge found for &quot; + nextState + &quot; state&quot;);
				return;
			}

			elem.attr( 'data-loading', true );
			currentBadge.css({ opacity: &quot;0.5&quot; });

			// Send ajax request to make the change
			ips.getAjax()( url, {
				showLoading: true // show our global loading indicator
			})
				.done( function (response) {
					currentBadge.hide().css({ opacity: &quot;1&quot; });
					nextBadge.show();

					elem.removeAttr('data-loading');

					// Trigger an event to let the page know
					elem.trigger( 'stateChanged', {
						status: nextState
					});
				})
				.fail( function (jqXHR, textStatus, errorThrown) {
					window.location = url;
				});

		};

		ips.ui.registerWidget( 'statusToggle', ips.ui.statusToggle, [
			'intermediate'
		], { lazyLoad: true, lazyEvent: 'click' } );

		return {
			respond: respond
		};
	});
}(jQuery, _));</file>
 <file javascript_app="global" javascript_location="admin" javascript_path="ui" javascript_name="ips.ui.tree.js" javascript_type="ui" javascript_version="107643" javascript_position="1000200"><![CDATA[/**
 * Invision Community
 * (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
 *
 * ips.ui.tree.js - Tree widget
 *
 * Author: Rikki Tissier
 */
;( function($, _, undefined){
	"use strict";

	ips.createModule('ips.ui.tree', function(){

		var defaults = {
			openClass: 'ipsTree_open',
			closedClass: 'ipsTree_closed',
			searchable: false,
			sortable: true
		};

		var respond = function (elem, options) {
			if( !$( elem ).data('_tree') ){
				$( elem ).data('_tree', treeObj(elem, _.defaults( options, defaults ) ) );
			}
		};

		ips.ui.registerWidget( 'tree', ips.ui.tree, [
			'openClass', 'closedClass', 'searchable', 'results', 'url', 'sortable', 'lockParents', 'protectRoots'
		]);

		return {
			respond: respond
		};
	});

	/**
	 * Tree instance
	 *
	 * @param	{element} 	elem 		The element this widget is being created on
	 * @param	{object} 	options 	The options passed into this instance
	 * @returns {void}
	 */
	var treeObj = function (elem, options, e) {

		var _timer = null;
		var _searchAjax = null;
		var _currentParentOver = null;


		/**
		 * Sets up this instance
		 *
		 * @returns 	{void}
		 */
		var init = function () {

			if( !options.url ){
				Debug.error( "No URL provided for tree widget on " + elem.identify().attr('id') );
			}

			// Add a class to this widget so we can show/hide appropriate elements
			elem.addClass('ipsTree_js');
			
			// Set up sortables
			if( options.sortable ){
				_makeSortable();
			}
			/*$( elem ).find('.ipsTree_node').each( function () {
				_makeSortable( $( this ) );
			});*/


			// Set up events
			elem.on( 'click', '.ipsTree_parent:not( .ipsTree_noToggle )', _toggleRow ); 

			if( options.searchable && $( options.searchable ).length ){
				$( options.searchable )
					.on( 'keydown', _searchKeyPress )
					.on( 'search', _doSearch );
			}
		},

		/**
		 * Event handler for searching the tree
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		_searchKeyPress = function (e) {
			clearTimeout( _timer );
			_timer = setTimeout( _doSearch, 500 );
		},

		/**
		 * Executes a search
		 *
		 * @returns 	{void}
		 */
		_doSearch = function () {

			// Abort existing ajax if possible
			if( _searchAjax && _searchAjax.abort ){
				_searchAjax.abort();
			}

			var value = $( options.searchable ).val().trim();
			var searchPane = elem.find('[data-role="treeResults"]');
			var listPane = elem.find('[data-role="treeListing"]');

			if( !_.isEmpty( value ) ){
				// Show results pane
				listPane.hide();
				searchPane.show();

				// Set loading
				searchPane.html( ips.templates.render('core.trees.loadingPane') );

				// Do the search
				_searchAjax = ips.getAjax()( options.url + '&do=search', {
					data: {
						input: value
					}
				})
					.done( function (response) {
						// Show rows if there were results
						if( _.isEmpty( response.trim() ) ){
							searchPane.html( ips.templates.render('core.trees.noRows') );
						} else {
							searchPane.html( response );
							$( document ).trigger( 'contentChange', [ searchPane ] );							
						}
					});

			} else {
				listPane.show();
				searchPane.hide().html('');
			}
		},

		/**
		 * Event handler for clicking on a row
		 *
		 * @param 		{event} 	e 	Event object
		 * @returns 	{void}
		 */
		_toggleRow = function (e) {
			var target = $( e.target );
			var row = $( e.currentTarget );
			
			if( target.closest('.ipsTree_controls').length || target.closest('[data-ipsStatusToggle]').length ){
				return;
			}

			if( row.hasClass( options.openClass ) ){
				_closeRow( row );
			} else {
				_openRow( row );
			}
		},

		/**
		 * Closes an open row
		 *
		 * @param 		{element} 	row 	The row to close
		 * @returns 	{void}
		 */
		_closeRow = function (row) {
			row.removeClass( options.openClass ).addClass( options.closedClass );

			var realRow = row.closest('[data-role="node"]');
			var rowID = realRow.find('[data-nodeid]').first().attr('data-nodeid');

			if( realRow.find('> ol').length ){
				ips.utils.anim.go( 'fadeOut fast', realRow.find('> ol') );
			}
		},

		/**
		 * Opens a closed row
		 *
		 * @param 		{element} 	row 	The row to open
		 * @returns 	{void}
		 */
		_openRow = function (row) {
			row.removeClass( options.closedClass ).addClass( options.openClass );
			var realRow = row.closest('[data-role="node"]');
			
			var rowID = realRow.find('[data-nodeid]').first().attr('data-nodeid');
			realRow.attr('data-nodeid', rowID);

			if( _.isUndefined( rowID ) ){
				Debug.warn( 'No rowID for row ' + realRow.identify().attr('id') );
				return;
			}

			// Do we have results loaded or loading? Show them immediately if so
			if( realRow.data('_childrenLoaded') || realRow.data('_childrenLoading') ){
				ips.utils.anim.go('fadeInDown fast', realRow.find('> ol') );
				return;
			} 

			// Not loaded or loading, so we need to do that here
			// First build the loading box
			var loading = ips.templates.render('core.trees.loadingRow');
			var content = ips.templates.render('core.trees.childWrapper', {
				content: loading
			});

			// Set to loading, append loading content
			realRow
				.data('_childrenLoading', true)
				.append( content );

			// Fetch real content
			ips.getAjax()( options.url + '&root=' + rowID )
				.done( function (response) {

					realRow.find('> ol').remove();
					realRow.find('> .ipsTree_row').after( response );

					realRow
						.data('_childrenLoaded', true)
						.removeData('_childrenLoading');

					// Now animate
					ips.utils.anim.go( 'fadeInDown', realRow.find('> ol') );

					// Let document know
					$( document ).trigger( 'contentChange', [ realRow ] );

					// Are we sorting?
					if( options.sortable ){
						elem.find('.ipsTree_rows > .ipsTree').nestedSortable('refresh');
						elem.find('.ipsTree_rows > .ipsTree').nestedSortable('refreshPositions');
						//_makeSortable( realRow.find('.ipsTree_node') );
					}
				})
				.fail( function () {
					window.location = options.url + '&root=' + rowID;
				});
		},

		_checkParentStatus = function () {
			Debug.log( 'check parent status' );

			// Find each tree row and loop
			elem.find('.ipsTree_row:not( .ipsTree_root )').each( function () {
				var row = $( this );

				// Ignore if the row is closed since we don't know what's inside it
				if( row.hasClass('ipsTree_parent') && !row.hasClass('ipsTree_open') ){
					return;
				}

				var subList = row.siblings('ol');
				var currentlyParent = row.is('ipsTree_parent');
				var hasChildren = subList.find('> li').length > 0;

				Debug.log( 'sublist: ');
				Debug.log( subList );

				row.toggleClass('ipsTree_parent', hasChildren );

				if( hasChildren && !currentlyParent ){
					row.addClass('ipsTree_open');
				}

				// sortable removes the <ol> if it's now empty, so we need to add it back here
				// so that the user can carry on sorting properly
				if( row.hasClass('ipsTree_acceptsChildren') && !subList.length ){
					var newRow = $('<ol/>').addClass('ipsTree ipsTree_node');
					row.after( newRow );
					//newRow.find('li').remove();
				}
			});
		},

		/**
		 * Makes the tree sortable
		 *
		 * @returns 	{void}
		 */
		_makeSortable = function () {
			var sortableOptions = {
				placeholder: 'sortable-placeholder',
				handle: '.ipsTree_dragHandle',
				items: '[data-role="node"]',
				excludeRoot: true,
				update: function (event, ui) {
					var url = options.url + '&do=reorder';
					var rootID = elem.find('.ipsTree_root').attr('data-nodeid');
					var data = '';

					// We need to run this after a short delay to let sortable clean itself up first
					setTimeout( function () {
						_checkParentStatus();	
					}, 200);					

					let failed = false;
					if( rootID ){
						url += '&root=' + rootID;

						// If we have a root item (that isn't technically part of the tree) we can't
						// use the standard serialize method or all items have the value null. Instead
						// we have to build a manual param string and replace null with the parent id.
						var dataArray = $( this ).nestedSortable( 'toArray', { key: 'ajax_order'} );
						var outputArray = [];

						for( var i = 0; i < dataArray.length; i++ ) {
							let id = dataArray[i].item_id !== undefined ? dataArray[i].item_id : dataArray[i].id;
							if ( [undefined, null, NaN].includes(id) ) {
								failed = true;
								break;
							}
							outputArray.push( 'ajax_order[' + id + ']=' + ( ( dataArray[i].parent_id == null ) ? rootID : dataArray[i].parent_id ) );
						}

						data = outputArray.join('&');
					} else {
						data = $( this ).nestedSortable( 'serialize', { key: 'ajax_order' } );	
					}

					if (failed) {
						window.location.reload();
					}

					data = data + '&csrfKey=' + ips.getSetting('csrfKey');
					ips.getAjax()( url, {
						data: data,
						method: 'POST'
					})
						.fail( function () {
							window.location = url + "&" + data;
						});
				},
				toleranceElement: '> div',
				listType: 'ol',
				isTree: true,
				// Called by nestedSortable to determine whether an item can be dragged into
				// the current location. We check for the ipsTree_acceptsChildren class which
				// indicates it can be a parent item.
				isAllowed: function (placeholder, placeholderParent, currentItem) {
					// Hide tooltip
					$('#ipsTooltip').hide();

					var parent = null;

					// Find nearest list
					if( _.isUndefined( placeholderParent ) || placeholderParent === null ){
						parent = elem.find('> .ipsTree_root');
					} else {
						parent = placeholderParent.closest('[data-role="node"]').find('> .ipsTree_row');	
					}			
					
					if( parent.hasClass('ipsTree_acceptsChildren') || ( !parent.length && !currentItem.find('> .ipsTree_row').hasClass('ipsTree_noRoot') ) ) {
						
						console.log( currentItem );
						
						placeholder.removeAttr('data-error');
						return true;
					} else {
						console.log('no');
						placeholder.attr('data-error', ips.getString('cannotDragInto') );
						return false;
					}
				},
				// This method is triggered by nestedSortable, and we piggy pack on it to call our _openRow
				// method to load closed nodes. _openRow calls the refresh() method of nestedSortable to enable
				// the item currently being dragged to be dropped in the newly-opened list. Phew.
				expand: function (event, ui) {
					var row = $( this ).find('.mjs-nestedSortable-hovering > .ipsTree_parent[data-nodeid]').first();

					if( !row.hasClass('ipsTree_open') ){
						_openRow( row );	
					}					
				},
				// Triggered when the dom position of the item changes.
				// We highlight the parent of the new position so it's clearer to users where the item is going
				change: function (event, ui) {
					// Remove the class from everywhere first
					$( this ).find('.ipsTree_draggingInto').removeClass('ipsTree_draggingInto');

					// Find the nearest list
					ui.placeholder.closest('[data-role="node"]').find('> .ipsTree_row').addClass('ipsTree_draggingInto');
				},
				// Triggered when dragging starts
				// Highlight the current parent
				start: function (event, ui) {
					// Find the nearest list
					ui.placeholder.closest('[data-role="node"]').find('> .ipsTree_row').addClass('ipsTree_draggingInto');
				},
				// Triggered when dragging stops
				// Remove all parent highlights
				stop: function (event, ui) {
					$( this ).find('.ipsTree_draggingInto').removeClass('ipsTree_draggingInto');
				}
			};

			// Locks the parents, allowing any items to be reordered but not moved out of their current list
			if( options.lockParents ){
				sortableOptions['disableParentChange'] = true;
			}

			// Protects the root items, preventing them from being turned into subitems, or subitems to be turned into roots
			if( options.protectRoots ){
				sortableOptions['protectRoot'] = true;
			}

			// Create the sortable
			elem.find('.ipsTree_rows > .ipsTree').nestedSortable( sortableOptions );
		};

		init();

		return {
			init: init
		};
	};
}(jQuery, _));]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="underscore" javascript_name="underscore.js" javascript_type="framework" javascript_version="107643" javascript_position="50"><![CDATA[!function(n,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define("underscore",r):(n="undefined"!=typeof globalThis?globalThis:n||self,function(){var t=n._,e=n._=r();e.noConflict=function(){return n._=t,e}}())}(this,(function(){
    //     Underscore.js 1.13.1
    //     https://underscorejs.org
    //     (c) 2009-2021 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors
    //     Underscore may be freely distributed under the MIT license.
    var n="1.13.1",r="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||Function("return this")()||{},t=Array.prototype,e=Object.prototype,u="undefined"!=typeof Symbol?Symbol.prototype:null,o=t.push,i=t.slice,a=e.toString,f=e.hasOwnProperty,c="undefined"!=typeof ArrayBuffer,l="undefined"!=typeof DataView,s=Array.isArray,p=Object.keys,v=Object.create,h=c&&ArrayBuffer.isView,y=isNaN,d=isFinite,g=!{toString:null}.propertyIsEnumerable("toString"),b=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"],m=Math.pow(2,53)-1;function j(n,r){return r=null==r?n.length-1:+r,function(){for(var t=Math.max(arguments.length-r,0),e=Array(t),u=0;u<t;u++)e[u]=arguments[u+r];switch(r){case 0:return n.call(this,e);case 1:return n.call(this,arguments[0],e);case 2:return n.call(this,arguments[0],arguments[1],e)}var o=Array(r+1);for(u=0;u<r;u++)o[u]=arguments[u];return o[r]=e,n.apply(this,o)}}function _(n){var r=typeof n;return"function"===r||"object"===r&&!!n}function w(n){return void 0===n}function A(n){return!0===n||!1===n||"[object Boolean]"===a.call(n)}function x(n){var r="[object "+n+"]";return function(n){return a.call(n)===r}}var S=x("String"),O=x("Number"),M=x("Date"),E=x("RegExp"),B=x("Error"),N=x("Symbol"),I=x("ArrayBuffer"),T=x("Function"),k=r.document&&r.document.childNodes;"function"!=typeof/./&&"object"!=typeof Int8Array&&"function"!=typeof k&&(T=function(n){return"function"==typeof n||!1});var D=T,R=x("Object"),F=l&&R(new DataView(new ArrayBuffer(8))),V="undefined"!=typeof Map&&R(new Map),P=x("DataView");var q=F?function(n){return null!=n&&D(n.getInt8)&&I(n.buffer)}:P,U=s||x("Array");function W(n,r){return null!=n&&f.call(n,r)}var z=x("Arguments");!function(){z(arguments)||(z=function(n){return W(n,"callee")})}();var L=z;function $(n){return O(n)&&y(n)}function C(n){return function(){return n}}function K(n){return function(r){var t=n(r);return"number"==typeof t&&t>=0&&t<=m}}function J(n){return function(r){return null==r?void 0:r[n]}}var G=J("byteLength"),H=K(G),Q=/\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/;var X=c?function(n){return h?h(n)&&!q(n):H(n)&&Q.test(a.call(n))}:C(!1),Y=J("length");function Z(n,r){r=function(n){for(var r={},t=n.length,e=0;e<t;++e)r[n[e]]=!0;return{contains:function(n){return r[n]},push:function(t){return r[t]=!0,n.push(t)}}}(r);var t=b.length,u=n.constructor,o=D(u)&&u.prototype||e,i="constructor";for(W(n,i)&&!r.contains(i)&&r.push(i);t--;)(i=b[t])in n&&n[i]!==o[i]&&!r.contains(i)&&r.push(i)}function nn(n){if(!_(n))return[];if(p)return p(n);var r=[];for(var t in n)W(n,t)&&r.push(t);return g&&Z(n,r),r}function rn(n,r){var t=nn(r),e=t.length;if(null==n)return!e;for(var u=Object(n),o=0;o<e;o++){var i=t[o];if(r[i]!==u[i]||!(i in u))return!1}return!0}function tn(n){return n instanceof tn?n:this instanceof tn?void(this._wrapped=n):new tn(n)}function en(n){return new Uint8Array(n.buffer||n,n.byteOffset||0,G(n))}tn.VERSION=n,tn.prototype.value=function(){return this._wrapped},tn.prototype.valueOf=tn.prototype.toJSON=tn.prototype.value,tn.prototype.toString=function(){return String(this._wrapped)};var un="[object DataView]";function on(n,r,t,e){if(n===r)return 0!==n||1/n==1/r;if(null==n||null==r)return!1;if(n!=n)return r!=r;var o=typeof n;return("function"===o||"object"===o||"object"==typeof r)&&function n(r,t,e,o){r instanceof tn&&(r=r._wrapped);t instanceof tn&&(t=t._wrapped);var i=a.call(r);if(i!==a.call(t))return!1;if(F&&"[object Object]"==i&&q(r)){if(!q(t))return!1;i=un}switch(i){case"[object RegExp]":case"[object String]":return""+r==""+t;case"[object Number]":return+r!=+r?+t!=+t:0==+r?1/+r==1/t:+r==+t;case"[object Date]":case"[object Boolean]":return+r==+t;case"[object Symbol]":return u.valueOf.call(r)===u.valueOf.call(t);case"[object ArrayBuffer]":case un:return n(en(r),en(t),e,o)}var f="[object Array]"===i;if(!f&&X(r)){if(G(r)!==G(t))return!1;if(r.buffer===t.buffer&&r.byteOffset===t.byteOffset)return!0;f=!0}if(!f){if("object"!=typeof r||"object"!=typeof t)return!1;var c=r.constructor,l=t.constructor;if(c!==l&&!(D(c)&&c instanceof c&&D(l)&&l instanceof l)&&"constructor"in r&&"constructor"in t)return!1}o=o||[];var s=(e=e||[]).length;for(;s--;)if(e[s]===r)return o[s]===t;if(e.push(r),o.push(t),f){if((s=r.length)!==t.length)return!1;for(;s--;)if(!on(r[s],t[s],e,o))return!1}else{var p,v=nn(r);if(s=v.length,nn(t).length!==s)return!1;for(;s--;)if(p=v[s],!W(t,p)||!on(r[p],t[p],e,o))return!1}return e.pop(),o.pop(),!0}(n,r,t,e)}function an(n){if(!_(n))return[];var r=[];for(var t in n)r.push(t);return g&&Z(n,r),r}function fn(n){var r=Y(n);return function(t){if(null==t)return!1;var e=an(t);if(Y(e))return!1;for(var u=0;u<r;u++)if(!D(t[n[u]]))return!1;return n!==hn||!D(t[cn])}}var cn="forEach",ln="has",sn=["clear","delete"],pn=["get",ln,"set"],vn=sn.concat(cn,pn),hn=sn.concat(pn),yn=["add"].concat(sn,cn,ln),dn=V?fn(vn):x("Map"),gn=V?fn(hn):x("WeakMap"),bn=V?fn(yn):x("Set"),mn=x("WeakSet");function jn(n){for(var r=nn(n),t=r.length,e=Array(t),u=0;u<t;u++)e[u]=n[r[u]];return e}function _n(n){for(var r={},t=nn(n),e=0,u=t.length;e<u;e++)r[n[t[e]]]=t[e];return r}function wn(n){var r=[];for(var t in n)D(n[t])&&r.push(t);return r.sort()}function An(n,r){return function(t){var e=arguments.length;if(r&&(t=Object(t)),e<2||null==t)return t;for(var u=1;u<e;u++)for(var o=arguments[u],i=n(o),a=i.length,f=0;f<a;f++){var c=i[f];r&&void 0!==t[c]||(t[c]=o[c])}return t}}var xn=An(an),Sn=An(nn),On=An(an,!0);function Mn(n){if(!_(n))return{};if(v)return v(n);var r=function(){};r.prototype=n;var t=new r;return r.prototype=null,t}function En(n){return _(n)?U(n)?n.slice():xn({},n):n}function Bn(n){return U(n)?n:[n]}function Nn(n){return tn.toPath(n)}function In(n,r){for(var t=r.length,e=0;e<t;e++){if(null==n)return;n=n[r[e]]}return t?n:void 0}function Tn(n,r,t){var e=In(n,Nn(r));return w(e)?t:e}function kn(n){return n}function Dn(n){return n=Sn({},n),function(r){return rn(r,n)}}function Rn(n){return n=Nn(n),function(r){return In(r,n)}}function Fn(n,r,t){if(void 0===r)return n;switch(null==t?3:t){case 1:return function(t){return n.call(r,t)};case 3:return function(t,e,u){return n.call(r,t,e,u)};case 4:return function(t,e,u,o){return n.call(r,t,e,u,o)}}return function(){return n.apply(r,arguments)}}function Vn(n,r,t){return null==n?kn:D(n)?Fn(n,r,t):_(n)&&!U(n)?Dn(n):Rn(n)}function Pn(n,r){return Vn(n,r,1/0)}function qn(n,r,t){return tn.iteratee!==Pn?tn.iteratee(n,r):Vn(n,r,t)}function Un(){}function Wn(n,r){return null==r&&(r=n,n=0),n+Math.floor(Math.random()*(r-n+1))}tn.toPath=Bn,tn.iteratee=Pn;var zn=Date.now||function(){return(new Date).getTime()};function Ln(n){var r=function(r){return n[r]},t="(?:"+nn(n).join("|")+")",e=RegExp(t),u=RegExp(t,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,r):n}}var $n={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},Cn=Ln($n),Kn=Ln(_n($n)),Jn=tn.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g},Gn=/(.)^/,Hn={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},Qn=/\\|'|\r|\n|\u2028|\u2029/g;function Xn(n){return"\\"+Hn[n]}var Yn=/^\s*(\w|\$)+\s*$/;var Zn=0;function nr(n,r,t,e,u){if(!(e instanceof r))return n.apply(t,u);var o=Mn(n.prototype),i=n.apply(o,u);return _(i)?i:o}var rr=j((function(n,r){var t=rr.placeholder,e=function(){for(var u=0,o=r.length,i=Array(o),a=0;a<o;a++)i[a]=r[a]===t?arguments[u++]:r[a];for(;u<arguments.length;)i.push(arguments[u++]);return nr(n,e,this,this,i)};return e}));rr.placeholder=tn;var tr=j((function(n,r,t){if(!D(n))throw new TypeError("Bind must be called on a function");var e=j((function(u){return nr(n,e,r,this,t.concat(u))}));return e})),er=K(Y);function ur(n,r,t,e){if(e=e||[],r||0===r){if(r<=0)return e.concat(n)}else r=1/0;for(var u=e.length,o=0,i=Y(n);o<i;o++){var a=n[o];if(er(a)&&(U(a)||L(a)))if(r>1)ur(a,r-1,t,e),u=e.length;else for(var f=0,c=a.length;f<c;)e[u++]=a[f++];else t||(e[u++]=a)}return e}var or=j((function(n,r){var t=(r=ur(r,!1,!1)).length;if(t<1)throw new Error("bindAll must be passed function names");for(;t--;){var e=r[t];n[e]=tr(n[e],n)}return n}));var ir=j((function(n,r,t){return setTimeout((function(){return n.apply(null,t)}),r)})),ar=rr(ir,tn,1);function fr(n){return function(){return!n.apply(this,arguments)}}function cr(n,r){var t;return function(){return--n>0&&(t=r.apply(this,arguments)),n<=1&&(r=null),t}}var lr=rr(cr,2);function sr(n,r,t){r=qn(r,t);for(var e,u=nn(n),o=0,i=u.length;o<i;o++)if(r(n[e=u[o]],e,n))return e}function pr(n){return function(r,t,e){t=qn(t,e);for(var u=Y(r),o=n>0?0:u-1;o>=0&&o<u;o+=n)if(t(r[o],o,r))return o;return-1}}var vr=pr(1),hr=pr(-1);function yr(n,r,t,e){for(var u=(t=qn(t,e,1))(r),o=0,i=Y(n);o<i;){var a=Math.floor((o+i)/2);t(n[a])<u?o=a+1:i=a}return o}function dr(n,r,t){return function(e,u,o){var a=0,f=Y(e);if("number"==typeof o)n>0?a=o>=0?o:Math.max(o+f,a):f=o>=0?Math.min(o+1,f):o+f+1;else if(t&&o&&f)return e[o=t(e,u)]===u?o:-1;if(u!=u)return(o=r(i.call(e,a,f),$))>=0?o+a:-1;for(o=n>0?a:f-1;o>=0&&o<f;o+=n)if(e[o]===u)return o;return-1}}var gr=dr(1,vr,yr),br=dr(-1,hr);function mr(n,r,t){var e=(er(n)?vr:sr)(n,r,t);if(void 0!==e&&-1!==e)return n[e]}function jr(n,r,t){var e,u;if(r=Fn(r,t),er(n))for(e=0,u=n.length;e<u;e++)r(n[e],e,n);else{var o=nn(n);for(e=0,u=o.length;e<u;e++)r(n[o[e]],o[e],n)}return n}function _r(n,r,t){r=qn(r,t);for(var e=!er(n)&&nn(n),u=(e||n).length,o=Array(u),i=0;i<u;i++){var a=e?e[i]:i;o[i]=r(n[a],a,n)}return o}function wr(n){var r=function(r,t,e,u){var o=!er(r)&&nn(r),i=(o||r).length,a=n>0?0:i-1;for(u||(e=r[o?o[a]:a],a+=n);a>=0&&a<i;a+=n){var f=o?o[a]:a;e=t(e,r[f],f,r)}return e};return function(n,t,e,u){var o=arguments.length>=3;return r(n,Fn(t,u,4),e,o)}}var Ar=wr(1),xr=wr(-1);function Sr(n,r,t){var e=[];return r=qn(r,t),jr(n,(function(n,t,u){r(n,t,u)&&e.push(n)})),e}function Or(n,r,t){r=qn(r,t);for(var e=!er(n)&&nn(n),u=(e||n).length,o=0;o<u;o++){var i=e?e[o]:o;if(!r(n[i],i,n))return!1}return!0}function Mr(n,r,t){r=qn(r,t);for(var e=!er(n)&&nn(n),u=(e||n).length,o=0;o<u;o++){var i=e?e[o]:o;if(r(n[i],i,n))return!0}return!1}function Er(n,r,t,e){return er(n)||(n=jn(n)),("number"!=typeof t||e)&&(t=0),gr(n,r,t)>=0}var Br=j((function(n,r,t){var e,u;return D(r)?u=r:(r=Nn(r),e=r.slice(0,-1),r=r[r.length-1]),_r(n,(function(n){var o=u;if(!o){if(e&&e.length&&(n=In(n,e)),null==n)return;o=n[r]}return null==o?o:o.apply(n,t)}))}));function Nr(n,r){return _r(n,Rn(r))}function Ir(n,r,t){var e,u,o=-1/0,i=-1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var a=0,f=(n=er(n)?n:jn(n)).length;a<f;a++)null!=(e=n[a])&&e>o&&(o=e);else r=qn(r,t),jr(n,(function(n,t,e){((u=r(n,t,e))>i||u===-1/0&&o===-1/0)&&(o=n,i=u)}));return o}function Tr(n,r,t){if(null==r||t)return er(n)||(n=jn(n)),n[Wn(n.length-1)];var e=er(n)?En(n):jn(n),u=Y(e);r=Math.max(Math.min(r,u),0);for(var o=u-1,i=0;i<r;i++){var a=Wn(i,o),f=e[i];e[i]=e[a],e[a]=f}return e.slice(0,r)}function kr(n,r){return function(t,e,u){var o=r?[[],[]]:{};return e=qn(e,u),jr(t,(function(r,u){var i=e(r,u,t);n(o,r,i)})),o}}var Dr=kr((function(n,r,t){W(n,t)?n[t].push(r):n[t]=[r]})),Rr=kr((function(n,r,t){n[t]=r})),Fr=kr((function(n,r,t){W(n,t)?n[t]++:n[t]=1})),Vr=kr((function(n,r,t){n[t?0:1].push(r)}),!0),Pr=/[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;function qr(n,r,t){return r in t}var Ur=j((function(n,r){var t={},e=r[0];if(null==n)return t;D(e)?(r.length>1&&(e=Fn(e,r[1])),r=an(n)):(e=qr,r=ur(r,!1,!1),n=Object(n));for(var u=0,o=r.length;u<o;u++){var i=r[u],a=n[i];e(a,i,n)&&(t[i]=a)}return t})),Wr=j((function(n,r){var t,e=r[0];return D(e)?(e=fr(e),r.length>1&&(t=r[1])):(r=_r(ur(r,!1,!1),String),e=function(n,t){return!Er(r,t)}),Ur(n,e,t)}));function zr(n,r,t){return i.call(n,0,Math.max(0,n.length-(null==r||t?1:r)))}function Lr(n,r,t){return null==n||n.length<1?null==r||t?void 0:[]:null==r||t?n[0]:zr(n,n.length-r)}function $r(n,r,t){return i.call(n,null==r||t?1:r)}var Cr=j((function(n,r){return r=ur(r,!0,!0),Sr(n,(function(n){return!Er(r,n)}))})),Kr=j((function(n,r){return Cr(n,r)}));function Jr(n,r,t,e){A(r)||(e=t,t=r,r=!1),null!=t&&(t=qn(t,e));for(var u=[],o=[],i=0,a=Y(n);i<a;i++){var f=n[i],c=t?t(f,i,n):f;r&&!t?(i&&o===c||u.push(f),o=c):t?Er(o,c)||(o.push(c),u.push(f)):Er(u,f)||u.push(f)}return u}var Gr=j((function(n){return Jr(ur(n,!0,!0))}));function Hr(n){for(var r=n&&Ir(n,Y).length||0,t=Array(r),e=0;e<r;e++)t[e]=Nr(n,e);return t}var Qr=j(Hr);function Xr(n,r){return n._chain?tn(r).chain():r}function Yr(n){return jr(wn(n),(function(r){var t=tn[r]=n[r];tn.prototype[r]=function(){var n=[this._wrapped];return o.apply(n,arguments),Xr(this,t.apply(tn,n))}})),tn}jr(["pop","push","reverse","shift","sort","splice","unshift"],(function(n){var r=t[n];tn.prototype[n]=function(){var t=this._wrapped;return null!=t&&(r.apply(t,arguments),"shift"!==n&&"splice"!==n||0!==t.length||delete t[0]),Xr(this,t)}})),jr(["concat","join","slice"],(function(n){var r=t[n];tn.prototype[n]=function(){var n=this._wrapped;return null!=n&&(n=r.apply(n,arguments)),Xr(this,n)}}));var Zr=Yr({__proto__:null,VERSION:n,restArguments:j,isObject:_,isNull:function(n){return null===n},isUndefined:w,isBoolean:A,isElement:function(n){return!(!n||1!==n.nodeType)},isString:S,isNumber:O,isDate:M,isRegExp:E,isError:B,isSymbol:N,isArrayBuffer:I,isDataView:q,isArray:U,isFunction:D,isArguments:L,isFinite:function(n){return!N(n)&&d(n)&&!isNaN(parseFloat(n))},isNaN:$,isTypedArray:X,isEmpty:function(n){if(null==n)return!0;var r=Y(n);return"number"==typeof r&&(U(n)||S(n)||L(n))?0===r:0===Y(nn(n))},isMatch:rn,isEqual:function(n,r){return on(n,r)},isMap:dn,isWeakMap:gn,isSet:bn,isWeakSet:mn,keys:nn,allKeys:an,values:jn,pairs:function(n){for(var r=nn(n),t=r.length,e=Array(t),u=0;u<t;u++)e[u]=[r[u],n[r[u]]];return e},invert:_n,functions:wn,methods:wn,extend:xn,extendOwn:Sn,assign:Sn,defaults:On,create:function(n,r){var t=Mn(n);return r&&Sn(t,r),t},clone:En,tap:function(n,r){return r(n),n},get:Tn,has:function(n,r){for(var t=(r=Nn(r)).length,e=0;e<t;e++){var u=r[e];if(!W(n,u))return!1;n=n[u]}return!!t},mapObject:function(n,r,t){r=qn(r,t);for(var e=nn(n),u=e.length,o={},i=0;i<u;i++){var a=e[i];o[a]=r(n[a],a,n)}return o},identity:kn,constant:C,noop:Un,toPath:Bn,property:Rn,propertyOf:function(n){return null==n?Un:function(r){return Tn(n,r)}},matcher:Dn,matches:Dn,times:function(n,r,t){var e=Array(Math.max(0,n));r=Fn(r,t,1);for(var u=0;u<n;u++)e[u]=r(u);return e},random:Wn,now:zn,escape:Cn,unescape:Kn,templateSettings:Jn,template:function(n,r,t){!r&&t&&(r=t),r=On({},r,tn.templateSettings);var e=RegExp([(r.escape||Gn).source,(r.interpolate||Gn).source,(r.evaluate||Gn).source].join("|")+"|$","g"),u=0,o="__p+='";n.replace(e,(function(r,t,e,i,a){return o+=n.slice(u,a).replace(Qn,Xn),u=a+r.length,t?o+="'+\n((__t=("+t+"))==null?'':_.escape(__t))+\n'":e?o+="'+\n((__t=("+e+"))==null?'':__t)+\n'":i&&(o+="';\n"+i+"\n__p+='"),r})),o+="';\n";var i,a=r.variable;if(a){if(!Yn.test(a))throw new Error("variable is not a bare identifier: "+a)}else o="with(obj||{}){\n"+o+"}\n",a="obj";o="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+o+"return __p;\n";try{i=new Function(a,"_",o)}catch(n){throw n.source=o,n}var f=function(n){return i.call(this,n,tn)};return f.source="function("+a+"){\n"+o+"}",f},result:function(n,r,t){var e=(r=Nn(r)).length;if(!e)return D(t)?t.call(n):t;for(var u=0;u<e;u++){var o=null==n?void 0:n[r[u]];void 0===o&&(o=t,u=e),n=D(o)?o.call(n):o}return n},uniqueId:function(n){var r=++Zn+"";return n?n+r:r},chain:function(n){var r=tn(n);return r._chain=!0,r},iteratee:Pn,partial:rr,bind:tr,bindAll:or,memoize:function(n,r){var t=function(e){var u=t.cache,o=""+(r?r.apply(this,arguments):e);return W(u,o)||(u[o]=n.apply(this,arguments)),u[o]};return t.cache={},t},delay:ir,defer:ar,throttle:function(n,r,t){var e,u,o,i,a=0;t||(t={});var f=function(){a=!1===t.leading?0:zn(),e=null,i=n.apply(u,o),e||(u=o=null)},c=function(){var c=zn();a||!1!==t.leading||(a=c);var l=r-(c-a);return u=this,o=arguments,l<=0||l>r?(e&&(clearTimeout(e),e=null),a=c,i=n.apply(u,o),e||(u=o=null)):e||!1===t.trailing||(e=setTimeout(f,l)),i};return c.cancel=function(){clearTimeout(e),a=0,e=u=o=null},c},debounce:function(n,r,t){var e,u,o,i,a,f=function(){var c=zn()-u;r>c?e=setTimeout(f,r-c):(e=null,t||(i=n.apply(a,o)),e||(o=a=null))},c=j((function(c){return a=this,o=c,u=zn(),e||(e=setTimeout(f,r),t&&(i=n.apply(a,o))),i}));return c.cancel=function(){clearTimeout(e),e=o=a=null},c},wrap:function(n,r){return rr(r,n)},negate:fr,compose:function(){var n=arguments,r=n.length-1;return function(){for(var t=r,e=n[r].apply(this,arguments);t--;)e=n[t].call(this,e);return e}},after:function(n,r){return function(){if(--n<1)return r.apply(this,arguments)}},before:cr,once:lr,findKey:sr,findIndex:vr,findLastIndex:hr,sortedIndex:yr,indexOf:gr,lastIndexOf:br,find:mr,detect:mr,findWhere:function(n,r){return mr(n,Dn(r))},each:jr,forEach:jr,map:_r,collect:_r,reduce:Ar,foldl:Ar,inject:Ar,reduceRight:xr,foldr:xr,filter:Sr,select:Sr,reject:function(n,r,t){return Sr(n,fr(qn(r)),t)},every:Or,all:Or,some:Mr,any:Mr,contains:Er,includes:Er,include:Er,invoke:Br,pluck:Nr,where:function(n,r){return Sr(n,Dn(r))},max:Ir,min:function(n,r,t){var e,u,o=1/0,i=1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var a=0,f=(n=er(n)?n:jn(n)).length;a<f;a++)null!=(e=n[a])&&e<o&&(o=e);else r=qn(r,t),jr(n,(function(n,t,e){((u=r(n,t,e))<i||u===1/0&&o===1/0)&&(o=n,i=u)}));return o},shuffle:function(n){return Tr(n,1/0)},sample:Tr,sortBy:function(n,r,t){var e=0;return r=qn(r,t),Nr(_r(n,(function(n,t,u){return{value:n,index:e++,criteria:r(n,t,u)}})).sort((function(n,r){var t=n.criteria,e=r.criteria;if(t!==e){if(t>e||void 0===t)return 1;if(t<e||void 0===e)return-1}return n.index-r.index})),"value")},groupBy:Dr,indexBy:Rr,countBy:Fr,partition:Vr,toArray:function(n){return n?U(n)?i.call(n):S(n)?n.match(Pr):er(n)?_r(n,kn):jn(n):[]},size:function(n){return null==n?0:er(n)?n.length:nn(n).length},pick:Ur,omit:Wr,first:Lr,head:Lr,take:Lr,initial:zr,last:function(n,r,t){return null==n||n.length<1?null==r||t?void 0:[]:null==r||t?n[n.length-1]:$r(n,Math.max(0,n.length-r))},rest:$r,tail:$r,drop:$r,compact:function(n){return Sr(n,Boolean)},flatten:function(n,r){return ur(n,r,!1)},without:Kr,uniq:Jr,unique:Jr,union:Gr,intersection:function(n){for(var r=[],t=arguments.length,e=0,u=Y(n);e<u;e++){var o=n[e];if(!Er(r,o)){var i;for(i=1;i<t&&Er(arguments[i],o);i++);i===t&&r.push(o)}}return r},difference:Cr,unzip:Hr,transpose:Hr,zip:Qr,object:function(n,r){for(var t={},e=0,u=Y(n);e<u;e++)r?t[n[e]]=r[e]:t[n[e][0]]=n[e][1];return t},range:function(n,r,t){null==r&&(r=n||0,n=0),t||(t=r<n?-1:1);for(var e=Math.max(Math.ceil((r-n)/t),0),u=Array(e),o=0;o<e;o++,n+=t)u[o]=n;return u},chunk:function(n,r){if(null==r||r<1)return[];for(var t=[],e=0,u=n.length;e<u;)t.push(i.call(n,e,e+=r));return t},mixin:Yr,default:tn});return Zr._=Zr,Zr}));]]></file>
 <file javascript_app="global" javascript_location="library" javascript_path="xregexp" javascript_name="xregexp-all.js" javascript_type="framework" javascript_version="107643" javascript_position="1000550"><![CDATA[(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.XRegExp = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/*!
 * XRegExp.build 3.2.0
 * <xregexp.com>
 * Steven Levithan (c) 2012-2017 MIT License
 * Inspired by Lea Verou's RegExp.create <lea.verou.me>
 */

module.exports = function(XRegExp) {
    'use strict';

    var REGEX_DATA = 'xregexp';
    var subParts = /(\()(?!\?)|\\([1-9]\d*)|\\[\s\S]|\[(?:[^\\\]]|\\[\s\S])*\]/g;
    var parts = XRegExp.union([/\({{([\w$]+)}}\)|{{([\w$]+)}}/, subParts], 'g', {
        conjunction: 'or'
    });

    /**
     * Strips a leading `^` and trailing unescaped `$`, if both are present.
     *
     * @private
     * @param {String} pattern Pattern to process.
     * @returns {String} Pattern with edge anchors removed.
     */
    function deanchor(pattern) {
        // Allow any number of empty noncapturing groups before/after anchors, because regexes
        // built/generated by XRegExp sometimes include them
        var leadingAnchor = /^(?:\(\?:\))*\^/;
        var trailingAnchor = /\$(?:\(\?:\))*$/;

        if (
            leadingAnchor.test(pattern) &&
            trailingAnchor.test(pattern) &&
            // Ensure that the trailing `$` isn't escaped
            trailingAnchor.test(pattern.replace(/\\[\s\S]/g, ''))
        ) {
            return pattern.replace(leadingAnchor, '').replace(trailingAnchor, '');
        }

        return pattern;
    }

    /**
     * Converts the provided value to an XRegExp. Native RegExp flags are not preserved.
     *
     * @private
     * @param {String|RegExp} value Value to convert.
     * @param {Boolean} [addFlagX] Whether to apply the `x` flag in cases when `value` is not
     *   already a regex generated by XRegExp
     * @returns {RegExp} XRegExp object with XRegExp syntax applied.
     */
    function asXRegExp(value, addFlagX) {
        var flags = addFlagX ? 'x' : '';
        return XRegExp.isRegExp(value) ?
            (value[REGEX_DATA] && value[REGEX_DATA].captureNames ?
                // Don't recompile, to preserve capture names
                value :
                // Recompile as XRegExp
                XRegExp(value.source, flags)
            ) :
            // Compile string as XRegExp
            XRegExp(value, flags);
    }

    /**
     * Builds regexes using named subpatterns, for readability and pattern reuse. Backreferences in
     * the outer pattern and provided subpatterns are automatically renumbered to work correctly.
     * Native flags used by provided subpatterns are ignored in favor of the `flags` argument.
     *
     * @memberOf XRegExp
     * @param {String} pattern XRegExp pattern using `{{name}}` for embedded subpatterns. Allows
     *   `({{name}})` as shorthand for `(?<name>{{name}})`. Patterns cannot be embedded within
     *   character classes.
     * @param {Object} subs Lookup object for named subpatterns. Values can be strings or regexes. A
     *   leading `^` and trailing unescaped `$` are stripped from subpatterns, if both are present.
     * @param {String} [flags] Any combination of XRegExp flags.
     * @returns {RegExp} Regex with interpolated subpatterns.
     * @example
     *
     * var time = XRegExp.build('(?x)^ {{hours}} ({{minutes}}) $', {
     *   hours: XRegExp.build('{{h12}} : | {{h24}}', {
     *     h12: /1[0-2]|0?[1-9]/,
     *     h24: /2[0-3]|[01][0-9]/
     *   }, 'x'),
     *   minutes: /^[0-5][0-9]$/
     * });
     * time.test('10:59'); // -> true
     * XRegExp.exec('10:59', time).minutes; // -> '59'
     */
    XRegExp.build = function(pattern, subs, flags) {
        flags = flags || '';
        // Used with `asXRegExp` calls for `pattern` and subpatterns in `subs`, to work around how
        // some browsers convert `RegExp('\n')` to a regex that contains the literal characters `\`
        // and `n`. See more details at <https://github.com/slevithan/xregexp/pull/163>.
        var addFlagX = flags.indexOf('x') > -1;
        var inlineFlags = /^\(\?([\w$]+)\)/.exec(pattern);
        // Add flags within a leading mode modifier to the overall pattern's flags
        if (inlineFlags) {
            flags = XRegExp._clipDuplicates(flags + inlineFlags[1]);
        }

        var data = {};
        for (var p in subs) {
            if (subs.hasOwnProperty(p)) {
                // Passing to XRegExp enables extended syntax and ensures independent validity,
                // lest an unescaped `(`, `)`, `[`, or trailing `\` breaks the `(?:)` wrapper. For
                // subpatterns provided as native regexes, it dies on octals and adds the property
                // used to hold extended regex instance data, for simplicity.
                var sub = asXRegExp(subs[p], addFlagX);
                data[p] = {
                    // Deanchoring allows embedding independently useful anchored regexes. If you
                    // really need to keep your anchors, double them (i.e., `^^...$$`).
                    pattern: deanchor(sub.source),
                    names: sub[REGEX_DATA].captureNames || []
                };
            }
        }

        // Passing to XRegExp dies on octals and ensures the outer pattern is independently valid;
        // helps keep this simple. Named captures will be put back.
        var patternAsRegex = asXRegExp(pattern, addFlagX);

        // 'Caps' is short for 'captures'
        var numCaps = 0;
        var numPriorCaps;
        var numOuterCaps = 0;
        var outerCapsMap = [0];
        var outerCapNames = patternAsRegex[REGEX_DATA].captureNames || [];
        var output = patternAsRegex.source.replace(parts, function($0, $1, $2, $3, $4) {
            var subName = $1 || $2;
            var capName;
            var intro;
            var localCapIndex;
            // Named subpattern
            if (subName) {
                if (!data.hasOwnProperty(subName)) {
                    throw new ReferenceError('Undefined property ' + $0);
                }
                // Named subpattern was wrapped in a capturing group
                if ($1) {
                    capName = outerCapNames[numOuterCaps];
                    outerCapsMap[++numOuterCaps] = ++numCaps;
                    // If it's a named group, preserve the name. Otherwise, use the subpattern name
                    // as the capture name
                    intro = '(?<' + (capName || subName) + '>';
                } else {
                    intro = '(?:';
                }
                numPriorCaps = numCaps;
                return intro + data[subName].pattern.replace(subParts, function(match, paren, backref) {
                    // Capturing group
                    if (paren) {
                        capName = data[subName].names[numCaps - numPriorCaps];
                        ++numCaps;
                        // If the current capture has a name, preserve the name
                        if (capName) {
                            return '(?<' + capName + '>';
                        }
                    // Backreference
                    } else if (backref) {
                        localCapIndex = +backref - 1;
                        // Rewrite the backreference
                        return data[subName].names[localCapIndex] ?
                            // Need to preserve the backreference name in case using flag `n`
                            '\\k<' + data[subName].names[localCapIndex] + '>' :
                            '\\' + (+backref + numPriorCaps);
                    }
                    return match;
                }) + ')';
            }
            // Capturing group
            if ($3) {
                capName = outerCapNames[numOuterCaps];
                outerCapsMap[++numOuterCaps] = ++numCaps;
                // If the current capture has a name, preserve the name
                if (capName) {
                    return '(?<' + capName + '>';
                }
            // Backreference
            } else if ($4) {
                localCapIndex = +$4 - 1;
                // Rewrite the backreference
                return outerCapNames[localCapIndex] ?
                    // Need to preserve the backreference name in case using flag `n`
                    '\\k<' + outerCapNames[localCapIndex] + '>' :
                    '\\' + outerCapsMap[+$4];
            }
            return $0;
        });

        return XRegExp(output, flags);
    };

};

},{}],2:[function(require,module,exports){
/*!
 * XRegExp.matchRecursive 3.2.0
 * <xregexp.com>
 * Steven Levithan (c) 2009-2017 MIT License
 */

module.exports = function(XRegExp) {
    'use strict';

    /**
     * Returns a match detail object composed of the provided values.
     *
     * @private
     */
    function row(name, value, start, end) {
        return {
            name: name,
            value: value,
            start: start,
            end: end
        };
    }

    /**
     * Returns an array of match strings between outermost left and right delimiters, or an array of
     * objects with detailed match parts and position data. An error is thrown if delimiters are
     * unbalanced within the data.
     *
     * @memberOf XRegExp
     * @param {String} str String to search.
     * @param {String} left Left delimiter as an XRegExp pattern.
     * @param {String} right Right delimiter as an XRegExp pattern.
     * @param {String} [flags] Any native or XRegExp flags, used for the left and right delimiters.
     * @param {Object} [options] Lets you specify `valueNames` and `escapeChar` options.
     * @returns {Array} Array of matches, or an empty array.
     * @example
     *
     * // Basic usage
     * var str = '(t((e))s)t()(ing)';
     * XRegExp.matchRecursive(str, '\\(', '\\)', 'g');
     * // -> ['t((e))s', '', 'ing']
     *
     * // Extended information mode with valueNames
     * str = 'Here is <div> <div>an</div></div> example';
     * XRegExp.matchRecursive(str, '<div\\s*>', '</div>', 'gi', {
     *   valueNames: ['between', 'left', 'match', 'right']
     * });
     * // -> [
     * // {name: 'between', value: 'Here is ',       start: 0,  end: 8},
     * // {name: 'left',    value: '<div>',          start: 8,  end: 13},
     * // {name: 'match',   value: ' <div>an</div>', start: 13, end: 27},
     * // {name: 'right',   value: '</div>',         start: 27, end: 33},
     * // {name: 'between', value: ' example',       start: 33, end: 41}
     * // ]
     *
     * // Omitting unneeded parts with null valueNames, and using escapeChar
     * str = '...{1}.\\{{function(x,y){return {y:x}}}';
     * XRegExp.matchRecursive(str, '{', '}', 'g', {
     *   valueNames: ['literal', null, 'value', null],
     *   escapeChar: '\\'
     * });
     * // -> [
     * // {name: 'literal', value: '...',  start: 0, end: 3},
     * // {name: 'value',   value: '1',    start: 4, end: 5},
     * // {name: 'literal', value: '.\\{', start: 6, end: 9},
     * // {name: 'value',   value: 'function(x,y){return {y:x}}', start: 10, end: 37}
     * // ]
     *
     * // Sticky mode via flag y
     * str = '<1><<<2>>><3>4<5>';
     * XRegExp.matchRecursive(str, '<', '>', 'gy');
     * // -> ['1', '<<2>>', '3']
     */
    XRegExp.matchRecursive = function(str, left, right, flags, options) {
        flags = flags || '';
        options = options || {};
        var global = flags.indexOf('g') > -1;
        var sticky = flags.indexOf('y') > -1;
        // Flag `y` is controlled internally
        var basicFlags = flags.replace(/y/g, '');
        var escapeChar = options.escapeChar;
        var vN = options.valueNames;
        var output = [];
        var openTokens = 0;
        var delimStart = 0;
        var delimEnd = 0;
        var lastOuterEnd = 0;
        var outerStart;
        var innerStart;
        var leftMatch;
        var rightMatch;
        var esc;
        left = XRegExp(left, basicFlags);
        right = XRegExp(right, basicFlags);

        if (escapeChar) {
            if (escapeChar.length > 1) {
                throw new Error('Cannot use more than one escape character');
            }
            escapeChar = XRegExp.escape(escapeChar);
            // Example of concatenated `esc` regex:
            // `escapeChar`: '%'
            // `left`: '<'
            // `right`: '>'
            // Regex is: /(?:%[\S\s]|(?:(?!<|>)[^%])+)+/
            esc = new RegExp(
                '(?:' + escapeChar + '[\\S\\s]|(?:(?!' +
                    // Using `XRegExp.union` safely rewrites backreferences in `left` and `right`.
                    // Intentionally not passing `basicFlags` to `XRegExp.union` since any syntax
                    // transformation resulting from those flags was already applied to `left` and
                    // `right` when they were passed through the XRegExp constructor above.
                    XRegExp.union([left, right], '', {conjunction: 'or'}).source +
                    ')[^' + escapeChar + '])+)+',
                // Flags `gy` not needed here
                flags.replace(/[^imu]+/g, '')
            );
        }

        while (true) {
            // If using an escape character, advance to the delimiter's next starting position,
            // skipping any escaped characters in between
            if (escapeChar) {
                delimEnd += (XRegExp.exec(str, esc, delimEnd, 'sticky') || [''])[0].length;
            }
            leftMatch = XRegExp.exec(str, left, delimEnd);
            rightMatch = XRegExp.exec(str, right, delimEnd);
            // Keep the leftmost match only
            if (leftMatch && rightMatch) {
                if (leftMatch.index <= rightMatch.index) {
                    rightMatch = null;
                } else {
                    leftMatch = null;
                }
            }
            // Paths (LM: leftMatch, RM: rightMatch, OT: openTokens):
            // LM | RM | OT | Result
            // 1  | 0  | 1  | loop
            // 1  | 0  | 0  | loop
            // 0  | 1  | 1  | loop
            // 0  | 1  | 0  | throw
            // 0  | 0  | 1  | throw
            // 0  | 0  | 0  | break
            // The paths above don't include the sticky mode special case. The loop ends after the
            // first completed match if not `global`.
            if (leftMatch || rightMatch) {
                delimStart = (leftMatch || rightMatch).index;
                delimEnd = delimStart + (leftMatch || rightMatch)[0].length;
            } else if (!openTokens) {
                break;
            }
            if (sticky && !openTokens && delimStart > lastOuterEnd) {
                break;
            }
            if (leftMatch) {
                if (!openTokens) {
                    outerStart = delimStart;
                    innerStart = delimEnd;
                }
                ++openTokens;
            } else if (rightMatch && openTokens) {
                if (!--openTokens) {
                    if (vN) {
                        if (vN[0] && outerStart > lastOuterEnd) {
                            output.push(row(vN[0], str.slice(lastOuterEnd, outerStart), lastOuterEnd, outerStart));
                        }
                        if (vN[1]) {
                            output.push(row(vN[1], str.slice(outerStart, innerStart), outerStart, innerStart));
                        }
                        if (vN[2]) {
                            output.push(row(vN[2], str.slice(innerStart, delimStart), innerStart, delimStart));
                        }
                        if (vN[3]) {
                            output.push(row(vN[3], str.slice(delimStart, delimEnd), delimStart, delimEnd));
                        }
                    } else {
                        output.push(str.slice(innerStart, delimStart));
                    }
                    lastOuterEnd = delimEnd;
                    if (!global) {
                        break;
                    }
                }
            } else {
                throw new Error('Unbalanced delimiter found in string');
            }
            // If the delimiter matched an empty string, avoid an infinite loop
            if (delimStart === delimEnd) {
                ++delimEnd;
            }
        }

        if (global && !sticky && vN && vN[0] && str.length > lastOuterEnd) {
            output.push(row(vN[0], str.slice(lastOuterEnd), lastOuterEnd, str.length));
        }

        return output;
    };

};

},{}],3:[function(require,module,exports){
/*!
 * XRegExp Unicode Base 3.2.0
 * <xregexp.com>
 * Steven Levithan (c) 2008-2017 MIT License
 */

module.exports = function(XRegExp) {
    'use strict';

    /**
     * Adds base support for Unicode matching:
     * - Adds syntax `\p{..}` for matching Unicode tokens. Tokens can be inverted using `\P{..}` or
     *   `\p{^..}`. Token names ignore case, spaces, hyphens, and underscores. You can omit the
     *   braces for token names that are a single letter (e.g. `\pL` or `PL`).
     * - Adds flag A (astral), which enables 21-bit Unicode support.
     * - Adds the `XRegExp.addUnicodeData` method used by other addons to provide character data.
     *
     * Unicode Base relies on externally provided Unicode character data. Official addons are
     * available to provide data for Unicode categories, scripts, blocks, and properties.
     *
     * @requires XRegExp
     */

    // ==--------------------------==
    // Private stuff
    // ==--------------------------==

    // Storage for Unicode data
    var unicode = {};

    // Reuse utils
    var dec = XRegExp._dec;
    var hex = XRegExp._hex;
    var pad4 = XRegExp._pad4;

    // Generates a token lookup name: lowercase, with hyphens, spaces, and underscores removed
    function normalize(name) {
        return name.replace(/[- _]+/g, '').toLowerCase();
    }

    // Gets the decimal code of a literal code unit, \xHH, \uHHHH, or a backslash-escaped literal
    function charCode(chr) {
        var esc = /^\\[xu](.+)/.exec(chr);
        return esc ?
            dec(esc[1]) :
            chr.charCodeAt(chr.charAt(0) === '\\' ? 1 : 0);
    }

    // Inverts a list of ordered BMP characters and ranges
    function invertBmp(range) {
        var output = '';
        var lastEnd = -1;

        XRegExp.forEach(
            range,
            /(\\x..|\\u....|\\?[\s\S])(?:-(\\x..|\\u....|\\?[\s\S]))?/,
            function(m) {
                var start = charCode(m[1]);
                if (start > (lastEnd + 1)) {
                    output += '\\u' + pad4(hex(lastEnd + 1));
                    if (start > (lastEnd + 2)) {
                        output += '-\\u' + pad4(hex(start - 1));
                    }
                }
                lastEnd = charCode(m[2] || m[1]);
            }
        );

        if (lastEnd < 0xFFFF) {
            output += '\\u' + pad4(hex(lastEnd + 1));
            if (lastEnd < 0xFFFE) {
                output += '-\\uFFFF';
            }
        }

        return output;
    }

    // Generates an inverted BMP range on first use
    function cacheInvertedBmp(slug) {
        var prop = 'b!';
        return (
            unicode[slug][prop] ||
            (unicode[slug][prop] = invertBmp(unicode[slug].bmp))
        );
    }

    // Combines and optionally negates BMP and astral data
    function buildAstral(slug, isNegated) {
        var item = unicode[slug];
        var combined = '';

        if (item.bmp && !item.isBmpLast) {
            combined = '[' + item.bmp + ']' + (item.astral ? '|' : '');
        }
        if (item.astral) {
            combined += item.astral;
        }
        if (item.isBmpLast && item.bmp) {
            combined += (item.astral ? '|' : '') + '[' + item.bmp + ']';
        }

        // Astral Unicode tokens always match a code point, never a code unit
        return isNegated ?
            '(?:(?!' + combined + ')(?:[\uD800-\uDBFF][\uDC00-\uDFFF]|[\0-\uFFFF]))' :
            '(?:' + combined + ')';
    }

    // Builds a complete astral pattern on first use
    function cacheAstral(slug, isNegated) {
        var prop = isNegated ? 'a!' : 'a=';
        return (
            unicode[slug][prop] ||
            (unicode[slug][prop] = buildAstral(slug, isNegated))
        );
    }

    // ==--------------------------==
    // Core functionality
    // ==--------------------------==

    /*
     * Add astral mode (flag A) and Unicode token syntax: `\p{..}`, `\P{..}`, `\p{^..}`, `\pC`.
     */
    XRegExp.addToken(
        // Use `*` instead of `+` to avoid capturing `^` as the token name in `\p{^}`
        /\\([pP])(?:{(\^?)([^}]*)}|([A-Za-z]))/,
        function(match, scope, flags) {
            var ERR_DOUBLE_NEG = 'Invalid double negation ';
            var ERR_UNKNOWN_NAME = 'Unknown Unicode token ';
            var ERR_UNKNOWN_REF = 'Unicode token missing data ';
            var ERR_ASTRAL_ONLY = 'Astral mode required for Unicode token ';
            var ERR_ASTRAL_IN_CLASS = 'Astral mode does not support Unicode tokens within character classes';
            // Negated via \P{..} or \p{^..}
            var isNegated = match[1] === 'P' || !!match[2];
            // Switch from BMP (0-FFFF) to astral (0-10FFFF) mode via flag A
            var isAstralMode = flags.indexOf('A') > -1;
            // Token lookup name. Check `[4]` first to avoid passing `undefined` via `\p{}`
            var slug = normalize(match[4] || match[3]);
            // Token data object
            var item = unicode[slug];

            if (match[1] === 'P' && match[2]) {
                throw new SyntaxError(ERR_DOUBLE_NEG + match[0]);
            }
            if (!unicode.hasOwnProperty(slug)) {
                throw new SyntaxError(ERR_UNKNOWN_NAME + match[0]);
            }

            // Switch to the negated form of the referenced Unicode token
            if (item.inverseOf) {
                slug = normalize(item.inverseOf);
                if (!unicode.hasOwnProperty(slug)) {
                    throw new ReferenceError(ERR_UNKNOWN_REF + match[0] + ' -> ' + item.inverseOf);
                }
                item = unicode[slug];
                isNegated = !isNegated;
            }

            if (!(item.bmp || isAstralMode)) {
                throw new SyntaxError(ERR_ASTRAL_ONLY + match[0]);
            }
            if (isAstralMode) {
                if (scope === 'class') {
                    throw new SyntaxError(ERR_ASTRAL_IN_CLASS);
                }

                return cacheAstral(slug, isNegated);
            }

            return scope === 'class' ?
                (isNegated ? cacheInvertedBmp(slug) : item.bmp) :
                (isNegated ? '[^' : '[') + item.bmp + ']';
        },
        {
            scope: 'all',
            optionalFlags: 'A',
            leadChar: '\\'
        }
    );

    /**
     * Adds to the list of Unicode tokens that XRegExp regexes can match via `\p` or `\P`.
     *
     * @memberOf XRegExp
     * @param {Array} data Objects with named character ranges. Each object may have properties
     *   `name`, `alias`, `isBmpLast`, `inverseOf`, `bmp`, and `astral`. All but `name` are
     *   optional, although one of `bmp` or `astral` is required (unless `inverseOf` is set). If
     *   `astral` is absent, the `bmp` data is used for BMP and astral modes. If `bmp` is absent,
     *   the name errors in BMP mode but works in astral mode. If both `bmp` and `astral` are
     *   provided, the `bmp` data only is used in BMP mode, and the combination of `bmp` and
     *   `astral` data is used in astral mode. `isBmpLast` is needed when a token matches orphan
     *   high surrogates *and* uses surrogate pairs to match astral code points. The `bmp` and
     *   `astral` data should be a combination of literal characters and `\xHH` or `\uHHHH` escape
     *   sequences, with hyphens to create ranges. Any regex metacharacters in the data should be
     *   escaped, apart from range-creating hyphens. The `astral` data can additionally use
     *   character classes and alternation, and should use surrogate pairs to represent astral code
     *   points. `inverseOf` can be used to avoid duplicating character data if a Unicode token is
     *   defined as the exact inverse of another token.
     * @example
     *
     * // Basic use
     * XRegExp.addUnicodeData([{
     *   name: 'XDigit',
     *   alias: 'Hexadecimal',
     *   bmp: '0-9A-Fa-f'
     * }]);
     * XRegExp('\\p{XDigit}:\\p{Hexadecimal}+').test('0:3D'); // -> true
     */
    XRegExp.addUnicodeData = function(data) {
        var ERR_NO_NAME = 'Unicode token requires name';
        var ERR_NO_DATA = 'Unicode token has no character data ';
        var item;

        for (var i = 0; i < data.length; ++i) {
            item = data[i];
            if (!item.name) {
                throw new Error(ERR_NO_NAME);
            }
            if (!(item.inverseOf || item.bmp || item.astral)) {
                throw new Error(ERR_NO_DATA + item.name);
            }
            unicode[normalize(item.name)] = item;
            if (item.alias) {
                unicode[normalize(item.alias)] = item;
            }
        }

        // Reset the pattern cache used by the `XRegExp` constructor, since the same pattern and
        // flags might now produce different results
        XRegExp.cache.flush('patterns');
    };

    /**
     * @ignore
     *
     * Return a reference to the internal Unicode definition structure for the given Unicode
     * Property if the given name is a legal Unicode Property for use in XRegExp `\p` or `\P` regex
     * constructs.
     *
     * @memberOf XRegExp
     * @param {String} name Name by which the Unicode Property may be recognized (case-insensitive),
     *   e.g. `'N'` or `'Number'`. The given name is matched against all registered Unicode
     *   Properties and Property Aliases.
     * @returns {Object} Reference to definition structure when the name matches a Unicode Property.
     *
     * @note
     * For more info on Unicode Properties, see also http://unicode.org/reports/tr18/#Categories.
     *
     * @note
     * This method is *not* part of the officially documented API and may change or be removed in
     * the future. It is meant for userland code that wishes to reuse the (large) internal Unicode
     * structures set up by XRegExp.
     */
    XRegExp._getUnicodeProperty = function(name) {
        var slug = normalize(name);
        return unicode[slug];
    };

};

},{}],4:[function(require,module,exports){
/*!
 * XRegExp Unicode Blocks 3.2.0
 * <xregexp.com>
 * Steven Levithan (c) 2010-2017 MIT License
 * Unicode data by Mathias Bynens <mathiasbynens.be>
 */

module.exports = function(XRegExp) {
    'use strict';

    /**
     * Adds support for all Unicode blocks. Block names use the prefix 'In'. E.g.,
     * `\p{InBasicLatin}`. Token names are case insensitive, and any spaces, hyphens, and
     * underscores are ignored.
     *
     * Uses Unicode 9.0.0.
     *
     * @requires XRegExp, Unicode Base
     */

    if (!XRegExp.addUnicodeData) {
        throw new ReferenceError('Unicode Base must be loaded before Unicode Blocks');
    }

    XRegExp.addUnicodeData([
        {
            name: 'InAdlam',
            astral: '\uD83A[\uDD00-\uDD5F]'
        },
        {
            name: 'InAegean_Numbers',
            astral: '\uD800[\uDD00-\uDD3F]'
        },
        {
            name: 'InAhom',
            astral: '\uD805[\uDF00-\uDF3F]'
        },
        {
            name: 'InAlchemical_Symbols',
            astral: '\uD83D[\uDF00-\uDF7F]'
        },
        {
            name: 'InAlphabetic_Presentation_Forms',
            bmp: '\uFB00-\uFB4F'
        },
        {
            name: 'InAnatolian_Hieroglyphs',
            astral: '\uD811[\uDC00-\uDE7F]'
        },
        {
            name: 'InAncient_Greek_Musical_Notation',
            astral: '\uD834[\uDE00-\uDE4F]'
        },
        {
            name: 'InAncient_Greek_Numbers',
            astral: '\uD800[\uDD40-\uDD8F]'
        },
        {
            name: 'InAncient_Symbols',
            astral: '\uD800[\uDD90-\uDDCF]'
        },
        {
            name: 'InArabic',
            bmp: '\u0600-\u06FF'
        },
        {
            name: 'InArabic_Extended_A',
            bmp: '\u08A0-\u08FF'
        },
        {
            name: 'InArabic_Mathematical_Alphabetic_Symbols',
            astral: '\uD83B[\uDE00-\uDEFF]'
        },
        {
            name: 'InArabic_Presentation_Forms_A',
            bmp: '\uFB50-\uFDFF'
        },
        {
            name: 'InArabic_Presentation_Forms_B',
            bmp: '\uFE70-\uFEFF'
        },
        {
            name: 'InArabic_Supplement',
            bmp: '\u0750-\u077F'
        },
        {
            name: 'InArmenian',
            bmp: '\u0530-\u058F'
        },
        {
            name: 'InArrows',
            bmp: '\u2190-\u21FF'
        },
        {
            name: 'InAvestan',
            astral: '\uD802[\uDF00-\uDF3F]'
        },
        {
            name: 'InBalinese',
            bmp: '\u1B00-\u1B7F'
        },
        {
            name: 'InBamum',
            bmp: '\uA6A0-\uA6FF'
        },
        {
            name: 'InBamum_Supplement',
            astral: '\uD81A[\uDC00-\uDE3F]'
        },
        {
            name: 'InBasic_Latin',
            bmp: '\0-\x7F'
        },
        {
            name: 'InBassa_Vah',
            astral: '\uD81A[\uDED0-\uDEFF]'
        },
        {
            name: 'InBatak',
            bmp: '\u1BC0-\u1BFF'
        },
        {
            name: 'InBengali',
            bmp: '\u0980-\u09FF'
        },
        {
            name: 'InBhaiksuki',
            astral: '\uD807[\uDC00-\uDC6F]'
        },
        {
            name: 'InBlock_Elements',
            bmp: '\u2580-\u259F'
        },
        {
            name: 'InBopomofo',
            bmp: '\u3100-\u312F'
        },
        {
            name: 'InBopomofo_Extended',
            bmp: '\u31A0-\u31BF'
        },
        {
            name: 'InBox_Drawing',
            bmp: '\u2500-\u257F'
        },
        {
            name: 'InBrahmi',
            astral: '\uD804[\uDC00-\uDC7F]'
        },
        {
            name: 'InBraille_Patterns',
            bmp: '\u2800-\u28FF'
        },
        {
            name: 'InBuginese',
            bmp: '\u1A00-\u1A1F'
        },
        {
            name: 'InBuhid',
            bmp: '\u1740-\u175F'
        },
        {
            name: 'InByzantine_Musical_Symbols',
            astral: '\uD834[\uDC00-\uDCFF]'
        },
        {
            name: 'InCJK_Compatibility',
            bmp: '\u3300-\u33FF'
        },
        {
            name: 'InCJK_Compatibility_Forms',
            bmp: '\uFE30-\uFE4F'
        },
        {
            name: 'InCJK_Compatibility_Ideographs',
            bmp: '\uF900-\uFAFF'
        },
        {
            name: 'InCJK_Compatibility_Ideographs_Supplement',
            astral: '\uD87E[\uDC00-\uDE1F]'
        },
        {
            name: 'InCJK_Radicals_Supplement',
            bmp: '\u2E80-\u2EFF'
        },
        {
            name: 'InCJK_Strokes',
            bmp: '\u31C0-\u31EF'
        },
        {
            name: 'InCJK_Symbols_and_Punctuation',
            bmp: '\u3000-\u303F'
        },
        {
            name: 'InCJK_Unified_Ideographs',
            bmp: '\u4E00-\u9FFF'
        },
        {
            name: 'InCJK_Unified_Ideographs_Extension_A',
            bmp: '\u3400-\u4DBF'
        },
        {
            name: 'InCJK_Unified_Ideographs_Extension_B',
            astral: '[\uD840-\uD868][\uDC00-\uDFFF]|\uD869[\uDC00-\uDEDF]'
        },
        {
            name: 'InCJK_Unified_Ideographs_Extension_C',
            astral: '\uD869[\uDF00-\uDFFF]|[\uD86A-\uD86C][\uDC00-\uDFFF]|\uD86D[\uDC00-\uDF3F]'
        },
        {
            name: 'InCJK_Unified_Ideographs_Extension_D',
            astral: '\uD86D[\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1F]'
        },
        {
            name: 'InCJK_Unified_Ideographs_Extension_E',
            astral: '\uD86E[\uDC20-\uDFFF]|[\uD86F-\uD872][\uDC00-\uDFFF]|\uD873[\uDC00-\uDEAF]'
        },
        {
            name: 'InCarian',
            astral: '\uD800[\uDEA0-\uDEDF]'
        },
        {
            name: 'InCaucasian_Albanian',
            astral: '\uD801[\uDD30-\uDD6F]'
        },
        {
            name: 'InChakma',
            astral: '\uD804[\uDD00-\uDD4F]'
        },
        {
            name: 'InCham',
            bmp: '\uAA00-\uAA5F'
        },
        {
            name: 'InCherokee',
            bmp: '\u13A0-\u13FF'
        },
        {
            name: 'InCherokee_Supplement',
            bmp: '\uAB70-\uABBF'
        },
        {
            name: 'InCombining_Diacritical_Marks',
            bmp: '\u0300-\u036F'
        },
        {
            name: 'InCombining_Diacritical_Marks_Extended',
            bmp: '\u1AB0-\u1AFF'
        },
        {
            name: 'InCombining_Diacritical_Marks_Supplement',
            bmp: '\u1DC0-\u1DFF'
        },
        {
            name: 'InCombining_Diacritical_Marks_for_Symbols',
            bmp: '\u20D0-\u20FF'
        },
        {
            name: 'InCombining_Half_Marks',
            bmp: '\uFE20-\uFE2F'
        },
        {
            name: 'InCommon_Indic_Number_Forms',
            bmp: '\uA830-\uA83F'
        },
        {
            name: 'InControl_Pictures',
            bmp: '\u2400-\u243F'
        },
        {
            name: 'InCoptic',
            bmp: '\u2C80-\u2CFF'
        },
        {
            name: 'InCoptic_Epact_Numbers',
            astral: '\uD800[\uDEE0-\uDEFF]'
        },
        {
            name: 'InCounting_Rod_Numerals',
            astral: '\uD834[\uDF60-\uDF7F]'
        },
        {
            name: 'InCuneiform',
            astral: '\uD808[\uDC00-\uDFFF]'
        },
        {
            name: 'InCuneiform_Numbers_and_Punctuation',
            astral: '\uD809[\uDC00-\uDC7F]'
        },
        {
            name: 'InCurrency_Symbols',
            bmp: '\u20A0-\u20CF'
        },
        {
            name: 'InCypriot_Syllabary',
            astral: '\uD802[\uDC00-\uDC3F]'
        },
        {
            name: 'InCyrillic',
            bmp: '\u0400-\u04FF'
        },
        {
            name: 'InCyrillic_Extended_A',
            bmp: '\u2DE0-\u2DFF'
        },
        {
            name: 'InCyrillic_Extended_B',
            bmp: '\uA640-\uA69F'
        },
        {
            name: 'InCyrillic_Extended_C',
            bmp: '\u1C80-\u1C8F'
        },
        {
            name: 'InCyrillic_Supplement',
            bmp: '\u0500-\u052F'
        },
        {
            name: 'InDeseret',
            astral: '\uD801[\uDC00-\uDC4F]'
        },
        {
            name: 'InDevanagari',
            bmp: '\u0900-\u097F'
        },
        {
            name: 'InDevanagari_Extended',
            bmp: '\uA8E0-\uA8FF'
        },
        {
            name: 'InDingbats',
            bmp: '\u2700-\u27BF'
        },
        {
            name: 'InDomino_Tiles',
            astral: '\uD83C[\uDC30-\uDC9F]'
        },
        {
            name: 'InDuployan',
            astral: '\uD82F[\uDC00-\uDC9F]'
        },
        {
            name: 'InEarly_Dynastic_Cuneiform',
            astral: '\uD809[\uDC80-\uDD4F]'
        },
        {
            name: 'InEgyptian_Hieroglyphs',
            astral: '\uD80C[\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F]'
        },
        {
            name: 'InElbasan',
            astral: '\uD801[\uDD00-\uDD2F]'
        },
        {
            name: 'InEmoticons',
            astral: '\uD83D[\uDE00-\uDE4F]'
        },
        {
            name: 'InEnclosed_Alphanumeric_Supplement',
            astral: '\uD83C[\uDD00-\uDDFF]'
        },
        {
            name: 'InEnclosed_Alphanumerics',
            bmp: '\u2460-\u24FF'
        },
        {
            name: 'InEnclosed_CJK_Letters_and_Months',
            bmp: '\u3200-\u32FF'
        },
        {
            name: 'InEnclosed_Ideographic_Supplement',
            astral: '\uD83C[\uDE00-\uDEFF]'
        },
        {
            name: 'InEthiopic',
            bmp: '\u1200-\u137F'
        },
        {
            name: 'InEthiopic_Extended',
            bmp: '\u2D80-\u2DDF'
        },
        {
            name: 'InEthiopic_Extended_A',
            bmp: '\uAB00-\uAB2F'
        },
        {
            name: 'InEthiopic_Supplement',
            bmp: '\u1380-\u139F'
        },
        {
            name: 'InGeneral_Punctuation',
            bmp: '\u2000-\u206F'
        },
        {
            name: 'InGeometric_Shapes',
            bmp: '\u25A0-\u25FF'
        },
        {
            name: 'InGeometric_Shapes_Extended',
            astral: '\uD83D[\uDF80-\uDFFF]'
        },
        {
            name: 'InGeorgian',
            bmp: '\u10A0-\u10FF'
        },
        {
            name: 'InGeorgian_Supplement',
            bmp: '\u2D00-\u2D2F'
        },
        {
            name: 'InGlagolitic',
            bmp: '\u2C00-\u2C5F'
        },
        {
            name: 'InGlagolitic_Supplement',
            astral: '\uD838[\uDC00-\uDC2F]'
        },
        {
            name: 'InGothic',
            astral: '\uD800[\uDF30-\uDF4F]'
        },
        {
            name: 'InGrantha',
            astral: '\uD804[\uDF00-\uDF7F]'
        },
        {
            name: 'InGreek_Extended',
            bmp: '\u1F00-\u1FFF'
        },
        {
            name: 'InGreek_and_Coptic',
            bmp: '\u0370-\u03FF'
        },
        {
            name: 'InGujarati',
            bmp: '\u0A80-\u0AFF'
        },
        {
            name: 'InGurmukhi',
            bmp: '\u0A00-\u0A7F'
        },
        {
            name: 'InHalfwidth_and_Fullwidth_Forms',
            bmp: '\uFF00-\uFFEF'
        },
        {
            name: 'InHangul_Compatibility_Jamo',
            bmp: '\u3130-\u318F'
        },
        {
            name: 'InHangul_Jamo',
            bmp: '\u1100-\u11FF'
        },
        {
            name: 'InHangul_Jamo_Extended_A',
            bmp: '\uA960-\uA97F'
        },
        {
            name: 'InHangul_Jamo_Extended_B',
            bmp: '\uD7B0-\uD7FF'
        },
        {
            name: 'InHangul_Syllables',
            bmp: '\uAC00-\uD7AF'
        },
        {
            name: 'InHanunoo',
            bmp: '\u1720-\u173F'
        },
        {
            name: 'InHatran',
            astral: '\uD802[\uDCE0-\uDCFF]'
        },
        {
            name: 'InHebrew',
            bmp: '\u0590-\u05FF'
        },
        {
            name: 'InHigh_Private_Use_Surrogates',
            bmp: '\uDB80-\uDBFF'
        },
        {
            name: 'InHigh_Surrogates',
            bmp: '\uD800-\uDB7F'
        },
        {
            name: 'InHiragana',
            bmp: '\u3040-\u309F'
        },
        {
            name: 'InIPA_Extensions',
            bmp: '\u0250-\u02AF'
        },
        {
            name: 'InIdeographic_Description_Characters',
            bmp: '\u2FF0-\u2FFF'
        },
        {
            name: 'InIdeographic_Symbols_and_Punctuation',
            astral: '\uD81B[\uDFE0-\uDFFF]'
        },
        {
            name: 'InImperial_Aramaic',
            astral: '\uD802[\uDC40-\uDC5F]'
        },
        {
            name: 'InInscriptional_Pahlavi',
            astral: '\uD802[\uDF60-\uDF7F]'
        },
        {
            name: 'InInscriptional_Parthian',
            astral: '\uD802[\uDF40-\uDF5F]'
        },
        {
            name: 'InJavanese',
            bmp: '\uA980-\uA9DF'
        },
        {
            name: 'InKaithi',
            astral: '\uD804[\uDC80-\uDCCF]'
        },
        {
            name: 'InKana_Supplement',
            astral: '\uD82C[\uDC00-\uDCFF]'
        },
        {
            name: 'InKanbun',
            bmp: '\u3190-\u319F'
        },
        {
            name: 'InKangxi_Radicals',
            bmp: '\u2F00-\u2FDF'
        },
        {
            name: 'InKannada',
            bmp: '\u0C80-\u0CFF'
        },
        {
            name: 'InKatakana',
            bmp: '\u30A0-\u30FF'
        },
        {
            name: 'InKatakana_Phonetic_Extensions',
            bmp: '\u31F0-\u31FF'
        },
        {
            name: 'InKayah_Li',
            bmp: '\uA900-\uA92F'
        },
        {
            name: 'InKharoshthi',
            astral: '\uD802[\uDE00-\uDE5F]'
        },
        {
            name: 'InKhmer',
            bmp: '\u1780-\u17FF'
        },
        {
            name: 'InKhmer_Symbols',
            bmp: '\u19E0-\u19FF'
        },
        {
            name: 'InKhojki',
            astral: '\uD804[\uDE00-\uDE4F]'
        },
        {
            name: 'InKhudawadi',
            astral: '\uD804[\uDEB0-\uDEFF]'
        },
        {
            name: 'InLao',
            bmp: '\u0E80-\u0EFF'
        },
        {
            name: 'InLatin_Extended_Additional',
            bmp: '\u1E00-\u1EFF'
        },
        {
            name: 'InLatin_Extended_A',
            bmp: '\u0100-\u017F'
        },
        {
            name: 'InLatin_Extended_B',
            bmp: '\u0180-\u024F'
        },
        {
            name: 'InLatin_Extended_C',
            bmp: '\u2C60-\u2C7F'
        },
        {
            name: 'InLatin_Extended_D',
            bmp: '\uA720-\uA7FF'
        },
        {
            name: 'InLatin_Extended_E',
            bmp: '\uAB30-\uAB6F'
        },
        {
            name: 'InLatin_1_Supplement',
            bmp: '\x80-\xFF'
        },
        {
            name: 'InLepcha',
            bmp: '\u1C00-\u1C4F'
        },
        {
            name: 'InLetterlike_Symbols',
            bmp: '\u2100-\u214F'
        },
        {
            name: 'InLimbu',
            bmp: '\u1900-\u194F'
        },
        {
            name: 'InLinear_A',
            astral: '\uD801[\uDE00-\uDF7F]'
        },
        {
            name: 'InLinear_B_Ideograms',
            astral: '\uD800[\uDC80-\uDCFF]'
        },
        {
            name: 'InLinear_B_Syllabary',
            astral: '\uD800[\uDC00-\uDC7F]'
        },
        {
            name: 'InLisu',
            bmp: '\uA4D0-\uA4FF'
        },
        {
            name: 'InLow_Surrogates',
            bmp: '\uDC00-\uDFFF'
        },
        {
            name: 'InLycian',
            astral: '\uD800[\uDE80-\uDE9F]'
        },
        {
            name: 'InLydian',
            astral: '\uD802[\uDD20-\uDD3F]'
        },
        {
            name: 'InMahajani',
            astral: '\uD804[\uDD50-\uDD7F]'
        },
        {
            name: 'InMahjong_Tiles',
            astral: '\uD83C[\uDC00-\uDC2F]'
        },
        {
            name: 'InMalayalam',
            bmp: '\u0D00-\u0D7F'
        },
        {
            name: 'InMandaic',
            bmp: '\u0840-\u085F'
        },
        {
            name: 'InManichaean',
            astral: '\uD802[\uDEC0-\uDEFF]'
        },
        {
            name: 'InMarchen',
            astral: '\uD807[\uDC70-\uDCBF]'
        },
        {
            name: 'InMathematical_Alphanumeric_Symbols',
            astral: '\uD835[\uDC00-\uDFFF]'
        },
        {
            name: 'InMathematical_Operators',
            bmp: '\u2200-\u22FF'
        },
        {
            name: 'InMeetei_Mayek',
            bmp: '\uABC0-\uABFF'
        },
        {
            name: 'InMeetei_Mayek_Extensions',
            bmp: '\uAAE0-\uAAFF'
        },
        {
            name: 'InMende_Kikakui',
            astral: '\uD83A[\uDC00-\uDCDF]'
        },
        {
            name: 'InMeroitic_Cursive',
            astral: '\uD802[\uDDA0-\uDDFF]'
        },
        {
            name: 'InMeroitic_Hieroglyphs',
            astral: '\uD802[\uDD80-\uDD9F]'
        },
        {
            name: 'InMiao',
            astral: '\uD81B[\uDF00-\uDF9F]'
        },
        {
            name: 'InMiscellaneous_Mathematical_Symbols_A',
            bmp: '\u27C0-\u27EF'
        },
        {
            name: 'InMiscellaneous_Mathematical_Symbols_B',
            bmp: '\u2980-\u29FF'
        },
        {
            name: 'InMiscellaneous_Symbols',
            bmp: '\u2600-\u26FF'
        },
        {
            name: 'InMiscellaneous_Symbols_and_Arrows',
            bmp: '\u2B00-\u2BFF'
        },
        {
            name: 'InMiscellaneous_Symbols_and_Pictographs',
            astral: '\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDDFF]'
        },
        {
            name: 'InMiscellaneous_Technical',
            bmp: '\u2300-\u23FF'
        },
        {
            name: 'InModi',
            astral: '\uD805[\uDE00-\uDE5F]'
        },
        {
            name: 'InModifier_Tone_Letters',
            bmp: '\uA700-\uA71F'
        },
        {
            name: 'InMongolian',
            bmp: '\u1800-\u18AF'
        },
        {
            name: 'InMongolian_Supplement',
            astral: '\uD805[\uDE60-\uDE7F]'
        },
        {
            name: 'InMro',
            astral: '\uD81A[\uDE40-\uDE6F]'
        },
        {
            name: 'InMultani',
            astral: '\uD804[\uDE80-\uDEAF]'
        },
        {
            name: 'InMusical_Symbols',
            astral: '\uD834[\uDD00-\uDDFF]'
        },
        {
            name: 'InMyanmar',
            bmp: '\u1000-\u109F'
        },
        {
            name: 'InMyanmar_Extended_A',
            bmp: '\uAA60-\uAA7F'
        },
        {
            name: 'InMyanmar_Extended_B',
            bmp: '\uA9E0-\uA9FF'
        },
        {
            name: 'InNKo',
            bmp: '\u07C0-\u07FF'
        },
        {
            name: 'InNabataean',
            astral: '\uD802[\uDC80-\uDCAF]'
        },
        {
            name: 'InNew_Tai_Lue',
            bmp: '\u1980-\u19DF'
        },
        {
            name: 'InNewa',
            astral: '\uD805[\uDC00-\uDC7F]'
        },
        {
            name: 'InNumber_Forms',
            bmp: '\u2150-\u218F'
        },
        {
            name: 'InOgham',
            bmp: '\u1680-\u169F'
        },
        {
            name: 'InOl_Chiki',
            bmp: '\u1C50-\u1C7F'
        },
        {
            name: 'InOld_Hungarian',
            astral: '\uD803[\uDC80-\uDCFF]'
        },
        {
            name: 'InOld_Italic',
            astral: '\uD800[\uDF00-\uDF2F]'
        },
        {
            name: 'InOld_North_Arabian',
            astral: '\uD802[\uDE80-\uDE9F]'
        },
        {
            name: 'InOld_Permic',
            astral: '\uD800[\uDF50-\uDF7F]'
        },
        {
            name: 'InOld_Persian',
            astral: '\uD800[\uDFA0-\uDFDF]'
        },
        {
            name: 'InOld_South_Arabian',
            astral: '\uD802[\uDE60-\uDE7F]'
        },
        {
            name: 'InOld_Turkic',
            astral: '\uD803[\uDC00-\uDC4F]'
        },
        {
            name: 'InOptical_Character_Recognition',
            bmp: '\u2440-\u245F'
        },
        {
            name: 'InOriya',
            bmp: '\u0B00-\u0B7F'
        },
        {
            name: 'InOrnamental_Dingbats',
            astral: '\uD83D[\uDE50-\uDE7F]'
        },
        {
            name: 'InOsage',
            astral: '\uD801[\uDCB0-\uDCFF]'
        },
        {
            name: 'InOsmanya',
            astral: '\uD801[\uDC80-\uDCAF]'
        },
        {
            name: 'InPahawh_Hmong',
            astral: '\uD81A[\uDF00-\uDF8F]'
        },
        {
            name: 'InPalmyrene',
            astral: '\uD802[\uDC60-\uDC7F]'
        },
        {
            name: 'InPau_Cin_Hau',
            astral: '\uD806[\uDEC0-\uDEFF]'
        },
        {
            name: 'InPhags_pa',
            bmp: '\uA840-\uA87F'
        },
        {
            name: 'InPhaistos_Disc',
            astral: '\uD800[\uDDD0-\uDDFF]'
        },
        {
            name: 'InPhoenician',
            astral: '\uD802[\uDD00-\uDD1F]'
        },
        {
            name: 'InPhonetic_Extensions',
            bmp: '\u1D00-\u1D7F'
        },
        {
            name: 'InPhonetic_Extensions_Supplement',
            bmp: '\u1D80-\u1DBF'
        },
        {
            name: 'InPlaying_Cards',
            astral: '\uD83C[\uDCA0-\uDCFF]'
        },
        {
            name: 'InPrivate_Use_Area',
            bmp: '\uE000-\uF8FF'
        },
        {
            name: 'InPsalter_Pahlavi',
            astral: '\uD802[\uDF80-\uDFAF]'
        },
        {
            name: 'InRejang',
            bmp: '\uA930-\uA95F'
        },
        {
            name: 'InRumi_Numeral_Symbols',
            astral: '\uD803[\uDE60-\uDE7F]'
        },
        {
            name: 'InRunic',
            bmp: '\u16A0-\u16FF'
        },
        {
            name: 'InSamaritan',
            bmp: '\u0800-\u083F'
        },
        {
            name: 'InSaurashtra',
            bmp: '\uA880-\uA8DF'
        },
        {
            name: 'InSharada',
            astral: '\uD804[\uDD80-\uDDDF]'
        },
        {
            name: 'InShavian',
            astral: '\uD801[\uDC50-\uDC7F]'
        },
        {
            name: 'InShorthand_Format_Controls',
            astral: '\uD82F[\uDCA0-\uDCAF]'
        },
        {
            name: 'InSiddham',
            astral: '\uD805[\uDD80-\uDDFF]'
        },
        {
            name: 'InSinhala',
            bmp: '\u0D80-\u0DFF'
        },
        {
            name: 'InSinhala_Archaic_Numbers',
            astral: '\uD804[\uDDE0-\uDDFF]'
        },
        {
            name: 'InSmall_Form_Variants',
            bmp: '\uFE50-\uFE6F'
        },
        {
            name: 'InSora_Sompeng',
            astral: '\uD804[\uDCD0-\uDCFF]'
        },
        {
            name: 'InSpacing_Modifier_Letters',
            bmp: '\u02B0-\u02FF'
        },
        {
            name: 'InSpecials',
            bmp: '\uFFF0-\uFFFF'
        },
        {
            name: 'InSundanese',
            bmp: '\u1B80-\u1BBF'
        },
        {
            name: 'InSundanese_Supplement',
            bmp: '\u1CC0-\u1CCF'
        },
        {
            name: 'InSuperscripts_and_Subscripts',
            bmp: '\u2070-\u209F'
        },
        {
            name: 'InSupplemental_Arrows_A',
            bmp: '\u27F0-\u27FF'
        },
        {
            name: 'InSupplemental_Arrows_B',
            bmp: '\u2900-\u297F'
        },
        {
            name: 'InSupplemental_Arrows_C',
            astral: '\uD83E[\uDC00-\uDCFF]'
        },
        {
            name: 'InSupplemental_Mathematical_Operators',
            bmp: '\u2A00-\u2AFF'
        },
        {
            name: 'InSupplemental_Punctuation',
            bmp: '\u2E00-\u2E7F'
        },
        {
            name: 'InSupplemental_Symbols_and_Pictographs',
            astral: '\uD83E[\uDD00-\uDDFF]'
        },
        {
            name: 'InSupplementary_Private_Use_Area_A',
            astral: '[\uDB80-\uDBBF][\uDC00-\uDFFF]'
        },
        {
            name: 'InSupplementary_Private_Use_Area_B',
            astral: '[\uDBC0-\uDBFF][\uDC00-\uDFFF]'
        },
        {
            name: 'InSutton_SignWriting',
            astral: '\uD836[\uDC00-\uDEAF]'
        },
        {
            name: 'InSyloti_Nagri',
            bmp: '\uA800-\uA82F'
        },
        {
            name: 'InSyriac',
            bmp: '\u0700-\u074F'
        },
        {
            name: 'InTagalog',
            bmp: '\u1700-\u171F'
        },
        {
            name: 'InTagbanwa',
            bmp: '\u1760-\u177F'
        },
        {
            name: 'InTags',
            astral: '\uDB40[\uDC00-\uDC7F]'
        },
        {
            name: 'InTai_Le',
            bmp: '\u1950-\u197F'
        },
        {
            name: 'InTai_Tham',
            bmp: '\u1A20-\u1AAF'
        },
        {
            name: 'InTai_Viet',
            bmp: '\uAA80-\uAADF'
        },
        {
            name: 'InTai_Xuan_Jing_Symbols',
            astral: '\uD834[\uDF00-\uDF5F]'
        },
        {
            name: 'InTakri',
            astral: '\uD805[\uDE80-\uDECF]'
        },
        {
            name: 'InTamil',
            bmp: '\u0B80-\u0BFF'
        },
        {
            name: 'InTangut',
            astral: '[\uD81C-\uD821][\uDC00-\uDFFF]'
        },
        {
            name: 'InTangut_Components',
            astral: '\uD822[\uDC00-\uDEFF]'
        },
        {
            name: 'InTelugu',
            bmp: '\u0C00-\u0C7F'
        },
        {
            name: 'InThaana',
            bmp: '\u0780-\u07BF'
        },
        {
            name: 'InThai',
            bmp: '\u0E00-\u0E7F'
        },
        {
            name: 'InTibetan',
            bmp: '\u0F00-\u0FFF'
        },
        {
            name: 'InTifinagh',
            bmp: '\u2D30-\u2D7F'
        },
        {
            name: 'InTirhuta',
            astral: '\uD805[\uDC80-\uDCDF]'
        },
        {
            name: 'InTransport_and_Map_Symbols',
            astral: '\uD83D[\uDE80-\uDEFF]'
        },
        {
            name: 'InUgaritic',
            astral: '\uD800[\uDF80-\uDF9F]'
        },
        {
            name: 'InUnified_Canadian_Aboriginal_Syllabics',
            bmp: '\u1400-\u167F'
        },
        {
            name: 'InUnified_Canadian_Aboriginal_Syllabics_Extended',
            bmp: '\u18B0-\u18FF'
        },
        {
            name: 'InVai',
            bmp: '\uA500-\uA63F'
        },
        {
            name: 'InVariation_Selectors',
            bmp: '\uFE00-\uFE0F'
        },
        {
            name: 'InVariation_Selectors_Supplement',
            astral: '\uDB40[\uDD00-\uDDEF]'
        },
        {
            name: 'InVedic_Extensions',
            bmp: '\u1CD0-\u1CFF'
        },
        {
            name: 'InVertical_Forms',
            bmp: '\uFE10-\uFE1F'
        },
        {
            name: 'InWarang_Citi',
            astral: '\uD806[\uDCA0-\uDCFF]'
        },
        {
            name: 'InYi_Radicals',
            bmp: '\uA490-\uA4CF'
        },
        {
            name: 'InYi_Syllables',
            bmp: '\uA000-\uA48F'
        },
        {
            name: 'InYijing_Hexagram_Symbols',
            bmp: '\u4DC0-\u4DFF'
        }
    ]);

};

},{}],5:[function(require,module,exports){
/*!
 * XRegExp Unicode Categories 3.2.0
 * <xregexp.com>
 * Steven Levithan (c) 2010-2017 MIT License
 * Unicode data by Mathias Bynens <mathiasbynens.be>
 */

module.exports = function(XRegExp) {
    'use strict';

    /**
     * Adds support for Unicode's general categories. E.g., `\p{Lu}` or `\p{Uppercase Letter}`. See
     * category descriptions in UAX #44 <http://unicode.org/reports/tr44/#GC_Values_Table>. Token
     * names are case insensitive, and any spaces, hyphens, and underscores are ignored.
     *
     * Uses Unicode 9.0.0.
     *
     * @requires XRegExp, Unicode Base
     */

    if (!XRegExp.addUnicodeData) {
        throw new ReferenceError('Unicode Base must be loaded before Unicode Categories');
    }

    XRegExp.addUnicodeData([
        {
            name: 'C',
            alias: 'Other',
            isBmpLast: true,
            bmp: '\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u0380-\u0383\u038B\u038D\u03A2\u0530\u0557\u0558\u0560\u0588\u058B\u058C\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08B5\u08BE-\u08D3\u08E2\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0AF8\u0AFA-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0BFF\u0C04\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D00\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D50-\u0D53\u0D64\u0D65\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F6\u13F7\u13FE\u13FF\u169D-\u169F\u16F9-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180E\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE\u1AAF\u1ABF-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C89-\u1CBF\u1CC8-\u1CCF\u1CF7\u1CFA-\u1CFF\u1DF6-\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u202A-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BF-\u20CF\u20F1-\u20FF\u218C-\u218F\u23FF\u2427-\u243F\u244B-\u245F\u2B74\u2B75\u2B96\u2B97\u2BBA-\u2BBC\u2BC9\u2BD2-\u2BEB\u2BF0-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E45-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FD6-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA6F8-\uA6FF\uA7AF\uA7B8-\uA7F6\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C6-\uA8CD\uA8DA-\uA8DF\uA8FE\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB66-\uAB6F\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF',
            astral: '\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDCFF\uDD03-\uDD06\uDD34-\uDD36\uDD8F\uDD9C-\uDD9F\uDDA1-\uDDCF\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEFC-\uDEFF\uDF24-\uDF2F\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDFC4-\uDFC7\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDD6E\uDD70-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56\uDC9F-\uDCA6\uDCB0-\uDCDF\uDCF3\uDCF6-\uDCFA\uDD1C-\uDD1E\uDD3A-\uDD3E\uDD40-\uDD7F\uDDB8-\uDDBB\uDDD0\uDDD1\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE34-\uDE37\uDE3B-\uDE3E\uDE48-\uDE4F\uDE59-\uDE5F\uDEA0-\uDEBF\uDEE7-\uDEEA\uDEF7-\uDEFF\uDF36-\uDF38\uDF56\uDF57\uDF73-\uDF77\uDF92-\uDF98\uDF9D-\uDFA8\uDFB0-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCF9\uDD00-\uDE5F\uDE7F-\uDFFF]|\uD804[\uDC4E-\uDC51\uDC70-\uDC7E\uDCBD\uDCC2-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD44-\uDD4F\uDD77-\uDD7F\uDDCE\uDDCF\uDDE0\uDDF5-\uDDFF\uDE12\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEAA-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF3B\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC5A\uDC5C\uDC5E-\uDC7F\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDDE-\uDDFF\uDE45-\uDE4F\uDE5A-\uDE5F\uDE6D-\uDE7F\uDEB8-\uDEBF\uDECA-\uDEFF\uDF1A-\uDF1C\uDF2C-\uDF2F\uDF40-\uDFFF]|\uD806[\uDC00-\uDC9F\uDCF3-\uDCFE\uDD00-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC46-\uDC4F\uDC6D-\uDC6F\uDC90\uDC91\uDCA8\uDCB7-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F\uDC75-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD823-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83F\uD874-\uD87D\uD87F-\uDB3F\uDB41-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDE6D\uDE70-\uDECF\uDEEE\uDEEF\uDEF6-\uDEFF\uDF46-\uDF4F\uDF5A\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDEFF\uDF45-\uDF4F\uDF7F-\uDF8E\uDFA0-\uDFDF\uDFE1-\uDFFF]|\uD821[\uDFED-\uDFFF]|\uD822[\uDEF3-\uDFFF]|\uD82C[\uDC02-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A\uDC9B\uDCA0-\uDFFF]|\uD834[\uDCF6-\uDCFF\uDD27\uDD28\uDD73-\uDD7A\uDDE9-\uDDFF\uDE46-\uDEFF\uDF57-\uDF5F\uDF72-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDFCC\uDFCD]|\uD836[\uDE8C-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDFFF]|\uD83A[\uDCC5\uDCC6\uDCD7-\uDCFF\uDD4B-\uDD4F\uDD5A-\uDD5D\uDD60-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDEEF\uDEF2-\uDFFF]|\uD83C[\uDC2C-\uDC2F\uDC94-\uDC9F\uDCAF\uDCB0\uDCC0\uDCD0\uDCF6-\uDCFF\uDD0D-\uDD0F\uDD2F\uDD6C-\uDD6F\uDDAD-\uDDE5\uDE03-\uDE0F\uDE3C-\uDE3F\uDE49-\uDE4F\uDE52-\uDEFF]|\uD83D[\uDED3-\uDEDF\uDEED-\uDEEF\uDEF7-\uDEFF\uDF74-\uDF7F\uDFD5-\uDFFF]|\uD83E[\uDC0C-\uDC0F\uDC48-\uDC4F\uDC5A-\uDC5F\uDC88-\uDC8F\uDCAE-\uDD0F\uDD1F\uDD28-\uDD2F\uDD31\uDD32\uDD3F\uDD4C-\uDD4F\uDD5F-\uDD7F\uDD92-\uDDBF\uDDC1-\uDFFF]|\uD869[\uDED7-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uDB40[\uDC00-\uDCFF\uDDF0-\uDFFF]'
        },
        {
            name: 'Cc',
            alias: 'Control',
            bmp: '\0-\x1F\x7F-\x9F'
        },
        {
            name: 'Cf',
            alias: 'Format',
            bmp: '\xAD\u0600-\u0605\u061C\u06DD\u070F\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB',
            astral: '\uD804\uDCBD|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]'
        },
        {
            name: 'Cn',
            alias: 'Unassigned',
            bmp: '\u0378\u0379\u0380-\u0383\u038B\u038D\u03A2\u0530\u0557\u0558\u0560\u0588\u058B\u058C\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u05FF\u061D\u070E\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08B5\u08BE-\u08D3\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0AF8\u0AFA-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0BFF\u0C04\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D00\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D50-\u0D53\u0D64\u0D65\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F6\u13F7\u13FE\u13FF\u169D-\u169F\u16F9-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE\u1AAF\u1ABF-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C89-\u1CBF\u1CC8-\u1CCF\u1CF7\u1CFA-\u1CFF\u1DF6-\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u2065\u2072\u2073\u208F\u209D-\u209F\u20BF-\u20CF\u20F1-\u20FF\u218C-\u218F\u23FF\u2427-\u243F\u244B-\u245F\u2B74\u2B75\u2B96\u2B97\u2BBA-\u2BBC\u2BC9\u2BD2-\u2BEB\u2BF0-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E45-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FD6-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA6F8-\uA6FF\uA7AF\uA7B8-\uA7F6\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C6-\uA8CD\uA8DA-\uA8DF\uA8FE\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB66-\uAB6F\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD\uFEFE\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFF8\uFFFE\uFFFF',
            astral: '\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDCFF\uDD03-\uDD06\uDD34-\uDD36\uDD8F\uDD9C-\uDD9F\uDDA1-\uDDCF\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEFC-\uDEFF\uDF24-\uDF2F\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDFC4-\uDFC7\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDD6E\uDD70-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56\uDC9F-\uDCA6\uDCB0-\uDCDF\uDCF3\uDCF6-\uDCFA\uDD1C-\uDD1E\uDD3A-\uDD3E\uDD40-\uDD7F\uDDB8-\uDDBB\uDDD0\uDDD1\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE34-\uDE37\uDE3B-\uDE3E\uDE48-\uDE4F\uDE59-\uDE5F\uDEA0-\uDEBF\uDEE7-\uDEEA\uDEF7-\uDEFF\uDF36-\uDF38\uDF56\uDF57\uDF73-\uDF77\uDF92-\uDF98\uDF9D-\uDFA8\uDFB0-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCF9\uDD00-\uDE5F\uDE7F-\uDFFF]|\uD804[\uDC4E-\uDC51\uDC70-\uDC7E\uDCC2-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD44-\uDD4F\uDD77-\uDD7F\uDDCE\uDDCF\uDDE0\uDDF5-\uDDFF\uDE12\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEAA-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF3B\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC5A\uDC5C\uDC5E-\uDC7F\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDDE-\uDDFF\uDE45-\uDE4F\uDE5A-\uDE5F\uDE6D-\uDE7F\uDEB8-\uDEBF\uDECA-\uDEFF\uDF1A-\uDF1C\uDF2C-\uDF2F\uDF40-\uDFFF]|\uD806[\uDC00-\uDC9F\uDCF3-\uDCFE\uDD00-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC46-\uDC4F\uDC6D-\uDC6F\uDC90\uDC91\uDCA8\uDCB7-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F\uDC75-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD823-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83F\uD874-\uD87D\uD87F-\uDB3F\uDB41-\uDB7F][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDE6D\uDE70-\uDECF\uDEEE\uDEEF\uDEF6-\uDEFF\uDF46-\uDF4F\uDF5A\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDEFF\uDF45-\uDF4F\uDF7F-\uDF8E\uDFA0-\uDFDF\uDFE1-\uDFFF]|\uD821[\uDFED-\uDFFF]|\uD822[\uDEF3-\uDFFF]|\uD82C[\uDC02-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A\uDC9B\uDCA4-\uDFFF]|\uD834[\uDCF6-\uDCFF\uDD27\uDD28\uDDE9-\uDDFF\uDE46-\uDEFF\uDF57-\uDF5F\uDF72-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDFCC\uDFCD]|\uD836[\uDE8C-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDFFF]|\uD83A[\uDCC5\uDCC6\uDCD7-\uDCFF\uDD4B-\uDD4F\uDD5A-\uDD5D\uDD60-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDEEF\uDEF2-\uDFFF]|\uD83C[\uDC2C-\uDC2F\uDC94-\uDC9F\uDCAF\uDCB0\uDCC0\uDCD0\uDCF6-\uDCFF\uDD0D-\uDD0F\uDD2F\uDD6C-\uDD6F\uDDAD-\uDDE5\uDE03-\uDE0F\uDE3C-\uDE3F\uDE49-\uDE4F\uDE52-\uDEFF]|\uD83D[\uDED3-\uDEDF\uDEED-\uDEEF\uDEF7-\uDEFF\uDF74-\uDF7F\uDFD5-\uDFFF]|\uD83E[\uDC0C-\uDC0F\uDC48-\uDC4F\uDC5A-\uDC5F\uDC88-\uDC8F\uDCAE-\uDD0F\uDD1F\uDD28-\uDD2F\uDD31\uDD32\uDD3F\uDD4C-\uDD4F\uDD5F-\uDD7F\uDD92-\uDDBF\uDDC1-\uDFFF]|\uD869[\uDED7-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uDB40[\uDC00\uDC02-\uDC1F\uDC80-\uDCFF\uDDF0-\uDFFF]|[\uDBBF\uDBFF][\uDFFE\uDFFF]'
        },
        {
            name: 'Co',
            alias: 'Private_Use',
            bmp: '\uE000-\uF8FF',
            astral: '[\uDB80-\uDBBE\uDBC0-\uDBFE][\uDC00-\uDFFF]|[\uDBBF\uDBFF][\uDC00-\uDFFD]'
        },
        {
            name: 'Cs',
            alias: 'Surrogate',
            bmp: '\uD800-\uDFFF'
        },
        {
            name: 'L',
            alias: 'Letter',
            bmp: 'A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC',
            astral: '\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]'
        },
        {
            name: 'Ll',
            alias: 'Lowercase_Letter',
            bmp: 'a-z\xB5\xDF-\xF6\xF8-\xFF\u0101\u0103\u0105\u0107\u0109\u010B\u010D\u010F\u0111\u0113\u0115\u0117\u0119\u011B\u011D\u011F\u0121\u0123\u0125\u0127\u0129\u012B\u012D\u012F\u0131\u0133\u0135\u0137\u0138\u013A\u013C\u013E\u0140\u0142\u0144\u0146\u0148\u0149\u014B\u014D\u014F\u0151\u0153\u0155\u0157\u0159\u015B\u015D\u015F\u0161\u0163\u0165\u0167\u0169\u016B\u016D\u016F\u0171\u0173\u0175\u0177\u017A\u017C\u017E-\u0180\u0183\u0185\u0188\u018C\u018D\u0192\u0195\u0199-\u019B\u019E\u01A1\u01A3\u01A5\u01A8\u01AA\u01AB\u01AD\u01B0\u01B4\u01B6\u01B9\u01BA\u01BD-\u01BF\u01C6\u01C9\u01CC\u01CE\u01D0\u01D2\u01D4\u01D6\u01D8\u01DA\u01DC\u01DD\u01DF\u01E1\u01E3\u01E5\u01E7\u01E9\u01EB\u01ED\u01EF\u01F0\u01F3\u01F5\u01F9\u01FB\u01FD\u01FF\u0201\u0203\u0205\u0207\u0209\u020B\u020D\u020F\u0211\u0213\u0215\u0217\u0219\u021B\u021D\u021F\u0221\u0223\u0225\u0227\u0229\u022B\u022D\u022F\u0231\u0233-\u0239\u023C\u023F\u0240\u0242\u0247\u0249\u024B\u024D\u024F-\u0293\u0295-\u02AF\u0371\u0373\u0377\u037B-\u037D\u0390\u03AC-\u03CE\u03D0\u03D1\u03D5-\u03D7\u03D9\u03DB\u03DD\u03DF\u03E1\u03E3\u03E5\u03E7\u03E9\u03EB\u03ED\u03EF-\u03F3\u03F5\u03F8\u03FB\u03FC\u0430-\u045F\u0461\u0463\u0465\u0467\u0469\u046B\u046D\u046F\u0471\u0473\u0475\u0477\u0479\u047B\u047D\u047F\u0481\u048B\u048D\u048F\u0491\u0493\u0495\u0497\u0499\u049B\u049D\u049F\u04A1\u04A3\u04A5\u04A7\u04A9\u04AB\u04AD\u04AF\u04B1\u04B3\u04B5\u04B7\u04B9\u04BB\u04BD\u04BF\u04C2\u04C4\u04C6\u04C8\u04CA\u04CC\u04CE\u04CF\u04D1\u04D3\u04D5\u04D7\u04D9\u04DB\u04DD\u04DF\u04E1\u04E3\u04E5\u04E7\u04E9\u04EB\u04ED\u04EF\u04F1\u04F3\u04F5\u04F7\u04F9\u04FB\u04FD\u04FF\u0501\u0503\u0505\u0507\u0509\u050B\u050D\u050F\u0511\u0513\u0515\u0517\u0519\u051B\u051D\u051F\u0521\u0523\u0525\u0527\u0529\u052B\u052D\u052F\u0561-\u0587\u13F8-\u13FD\u1C80-\u1C88\u1D00-\u1D2B\u1D6B-\u1D77\u1D79-\u1D9A\u1E01\u1E03\u1E05\u1E07\u1E09\u1E0B\u1E0D\u1E0F\u1E11\u1E13\u1E15\u1E17\u1E19\u1E1B\u1E1D\u1E1F\u1E21\u1E23\u1E25\u1E27\u1E29\u1E2B\u1E2D\u1E2F\u1E31\u1E33\u1E35\u1E37\u1E39\u1E3B\u1E3D\u1E3F\u1E41\u1E43\u1E45\u1E47\u1E49\u1E4B\u1E4D\u1E4F\u1E51\u1E53\u1E55\u1E57\u1E59\u1E5B\u1E5D\u1E5F\u1E61\u1E63\u1E65\u1E67\u1E69\u1E6B\u1E6D\u1E6F\u1E71\u1E73\u1E75\u1E77\u1E79\u1E7B\u1E7D\u1E7F\u1E81\u1E83\u1E85\u1E87\u1E89\u1E8B\u1E8D\u1E8F\u1E91\u1E93\u1E95-\u1E9D\u1E9F\u1EA1\u1EA3\u1EA5\u1EA7\u1EA9\u1EAB\u1EAD\u1EAF\u1EB1\u1EB3\u1EB5\u1EB7\u1EB9\u1EBB\u1EBD\u1EBF\u1EC1\u1EC3\u1EC5\u1EC7\u1EC9\u1ECB\u1ECD\u1ECF\u1ED1\u1ED3\u1ED5\u1ED7\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1EE5\u1EE7\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u1EF3\u1EF5\u1EF7\u1EF9\u1EFB\u1EFD\u1EFF-\u1F07\u1F10-\u1F15\u1F20-\u1F27\u1F30-\u1F37\u1F40-\u1F45\u1F50-\u1F57\u1F60-\u1F67\u1F70-\u1F7D\u1F80-\u1F87\u1F90-\u1F97\u1FA0-\u1FA7\u1FB0-\u1FB4\u1FB6\u1FB7\u1FBE\u1FC2-\u1FC4\u1FC6\u1FC7\u1FD0-\u1FD3\u1FD6\u1FD7\u1FE0-\u1FE7\u1FF2-\u1FF4\u1FF6\u1FF7\u210A\u210E\u210F\u2113\u212F\u2134\u2139\u213C\u213D\u2146-\u2149\u214E\u2184\u2C30-\u2C5E\u2C61\u2C65\u2C66\u2C68\u2C6A\u2C6C\u2C71\u2C73\u2C74\u2C76-\u2C7B\u2C81\u2C83\u2C85\u2C87\u2C89\u2C8B\u2C8D\u2C8F\u2C91\u2C93\u2C95\u2C97\u2C99\u2C9B\u2C9D\u2C9F\u2CA1\u2CA3\u2CA5\u2CA7\u2CA9\u2CAB\u2CAD\u2CAF\u2CB1\u2CB3\u2CB5\u2CB7\u2CB9\u2CBB\u2CBD\u2CBF\u2CC1\u2CC3\u2CC5\u2CC7\u2CC9\u2CCB\u2CCD\u2CCF\u2CD1\u2CD3\u2CD5\u2CD7\u2CD9\u2CDB\u2CDD\u2CDF\u2CE1\u2CE3\u2CE4\u2CEC\u2CEE\u2CF3\u2D00-\u2D25\u2D27\u2D2D\uA641\uA643\uA645\uA647\uA649\uA64B\uA64D\uA64F\uA651\uA653\uA655\uA657\uA659\uA65B\uA65D\uA65F\uA661\uA663\uA665\uA667\uA669\uA66B\uA66D\uA681\uA683\uA685\uA687\uA689\uA68B\uA68D\uA68F\uA691\uA693\uA695\uA697\uA699\uA69B\uA723\uA725\uA727\uA729\uA72B\uA72D\uA72F-\uA731\uA733\uA735\uA737\uA739\uA73B\uA73D\uA73F\uA741\uA743\uA745\uA747\uA749\uA74B\uA74D\uA74F\uA751\uA753\uA755\uA757\uA759\uA75B\uA75D\uA75F\uA761\uA763\uA765\uA767\uA769\uA76B\uA76D\uA76F\uA771-\uA778\uA77A\uA77C\uA77F\uA781\uA783\uA785\uA787\uA78C\uA78E\uA791\uA793-\uA795\uA797\uA799\uA79B\uA79D\uA79F\uA7A1\uA7A3\uA7A5\uA7A7\uA7A9\uA7B5\uA7B7\uA7FA\uAB30-\uAB5A\uAB60-\uAB65\uAB70-\uABBF\uFB00-\uFB06\uFB13-\uFB17\uFF41-\uFF5A',
            astral: '\uD801[\uDC28-\uDC4F\uDCD8-\uDCFB]|\uD803[\uDCC0-\uDCF2]|\uD806[\uDCC0-\uDCDF]|\uD835[\uDC1A-\uDC33\uDC4E-\uDC54\uDC56-\uDC67\uDC82-\uDC9B\uDCB6-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDCCF\uDCEA-\uDD03\uDD1E-\uDD37\uDD52-\uDD6B\uDD86-\uDD9F\uDDBA-\uDDD3\uDDEE-\uDE07\uDE22-\uDE3B\uDE56-\uDE6F\uDE8A-\uDEA5\uDEC2-\uDEDA\uDEDC-\uDEE1\uDEFC-\uDF14\uDF16-\uDF1B\uDF36-\uDF4E\uDF50-\uDF55\uDF70-\uDF88\uDF8A-\uDF8F\uDFAA-\uDFC2\uDFC4-\uDFC9\uDFCB]|\uD83A[\uDD22-\uDD43]'
        },
        {
            name: 'Lm',
            alias: 'Modifier_Letter',
            bmp: '\u02B0-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0374\u037A\u0559\u0640\u06E5\u06E6\u07F4\u07F5\u07FA\u081A\u0824\u0828\u0971\u0E46\u0EC6\u10FC\u17D7\u1843\u1AA7\u1C78-\u1C7D\u1D2C-\u1D6A\u1D78\u1D9B-\u1DBF\u2071\u207F\u2090-\u209C\u2C7C\u2C7D\u2D6F\u2E2F\u3005\u3031-\u3035\u303B\u309D\u309E\u30FC-\u30FE\uA015\uA4F8-\uA4FD\uA60C\uA67F\uA69C\uA69D\uA717-\uA71F\uA770\uA788\uA7F8\uA7F9\uA9CF\uA9E6\uAA70\uAADD\uAAF3\uAAF4\uAB5C-\uAB5F\uFF70\uFF9E\uFF9F',
            astral: '\uD81A[\uDF40-\uDF43]|\uD81B[\uDF93-\uDF9F\uDFE0]'
        },
        {
            name: 'Lo',
            alias: 'Other_Letter',
            bmp: '\xAA\xBA\u01BB\u01C0-\u01C3\u0294\u05D0-\u05EA\u05F0-\u05F2\u0620-\u063F\u0641-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u0800-\u0815\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0972-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E45\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10D0-\u10FA\u10FD-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17DC\u1820-\u1842\u1844-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C77\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u2135-\u2138\u2D30-\u2D67\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3006\u303C\u3041-\u3096\u309F\u30A1-\u30FA\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA014\uA016-\uA48C\uA4D0-\uA4F7\uA500-\uA60B\uA610-\uA61F\uA62A\uA62B\uA66E\uA6A0-\uA6E5\uA78F\uA7F7\uA7FB-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9E0-\uA9E4\uA9E7-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA6F\uAA71-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB\uAADC\uAAE0-\uAAEA\uAAF2\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF66-\uFF6F\uFF71-\uFF9D\uFFA0-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC',
            astral: '\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC50-\uDC9D\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD83A[\uDC00-\uDCC4]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]'
        },
        {
            name: 'Lt',
            alias: 'Titlecase_Letter',
            bmp: '\u01C5\u01C8\u01CB\u01F2\u1F88-\u1F8F\u1F98-\u1F9F\u1FA8-\u1FAF\u1FBC\u1FCC\u1FFC'
        },
        {
            name: 'Lu',
            alias: 'Uppercase_Letter',
            bmp: 'A-Z\xC0-\xD6\xD8-\xDE\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178\u0179\u017B\u017D\u0181\u0182\u0184\u0186\u0187\u0189-\u018B\u018E-\u0191\u0193\u0194\u0196-\u0198\u019C\u019D\u019F\u01A0\u01A2\u01A4\u01A6\u01A7\u01A9\u01AC\u01AE\u01AF\u01B1-\u01B3\u01B5\u01B7\u01B8\u01BC\u01C4\u01C7\u01CA\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F1\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A\u023B\u023D\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E\u038F\u0391-\u03A1\u03A3-\u03AB\u03CF\u03D2-\u03D4\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F4\u03F7\u03F9\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u10A0-\u10C5\u10C7\u10CD\u13A0-\u13F5\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1FB8-\u1FBB\u1FC8-\u1FCB\u1FD8-\u1FDB\u1FE8-\u1FEC\u1FF8-\u1FFB\u2102\u2107\u210B-\u210D\u2110-\u2112\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u2130-\u2133\u213E\u213F\u2145\u2183\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AE\uA7B0-\uA7B4\uA7B6\uFF21-\uFF3A',
            astral: '\uD801[\uDC00-\uDC27\uDCB0-\uDCD3]|\uD803[\uDC80-\uDCB2]|\uD806[\uDCA0-\uDCBF]|\uD835[\uDC00-\uDC19\uDC34-\uDC4D\uDC68-\uDC81\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB5\uDCD0-\uDCE9\uDD04\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD38\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD6C-\uDD85\uDDA0-\uDDB9\uDDD4-\uDDED\uDE08-\uDE21\uDE3C-\uDE55\uDE70-\uDE89\uDEA8-\uDEC0\uDEE2-\uDEFA\uDF1C-\uDF34\uDF56-\uDF6E\uDF90-\uDFA8\uDFCA]|\uD83A[\uDD00-\uDD21]'
        },
        {
            name: 'M',
            alias: 'Mark',
            bmp: '\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D4-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F',
            astral: '\uD800[\uDDFD\uDEE0\uDF76-\uDF7A]|\uD802[\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD804[\uDC00-\uDC02\uDC38-\uDC46\uDC7F-\uDC82\uDCB0-\uDCBA\uDD00-\uDD02\uDD27-\uDD34\uDD73\uDD80-\uDD82\uDDB3-\uDDC0\uDDCA-\uDDCC\uDE2C-\uDE37\uDE3E\uDEDF-\uDEEA\uDF00-\uDF03\uDF3C\uDF3E-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF57\uDF62\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC35-\uDC46\uDCB0-\uDCC3\uDDAF-\uDDB5\uDDB8-\uDDC0\uDDDC\uDDDD\uDE30-\uDE40\uDEAB-\uDEB7\uDF1D-\uDF2B]|\uD807[\uDC2F-\uDC36\uDC38-\uDC3F\uDC92-\uDCA7\uDCA9-\uDCB6]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDF51-\uDF7E\uDF8F-\uDF92]|\uD82F[\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDCD0-\uDCD6\uDD44-\uDD4A]|\uDB40[\uDD00-\uDDEF]'
        },
        {
            name: 'Mc',
            alias: 'Spacing_Mark',
            bmp: '\u0903\u093B\u093E-\u0940\u0949-\u094C\u094E\u094F\u0982\u0983\u09BE-\u09C0\u09C7\u09C8\u09CB\u09CC\u09D7\u0A03\u0A3E-\u0A40\u0A83\u0ABE-\u0AC0\u0AC9\u0ACB\u0ACC\u0B02\u0B03\u0B3E\u0B40\u0B47\u0B48\u0B4B\u0B4C\u0B57\u0BBE\u0BBF\u0BC1\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCC\u0BD7\u0C01-\u0C03\u0C41-\u0C44\u0C82\u0C83\u0CBE\u0CC0-\u0CC4\u0CC7\u0CC8\u0CCA\u0CCB\u0CD5\u0CD6\u0D02\u0D03\u0D3E-\u0D40\u0D46-\u0D48\u0D4A-\u0D4C\u0D57\u0D82\u0D83\u0DCF-\u0DD1\u0DD8-\u0DDF\u0DF2\u0DF3\u0F3E\u0F3F\u0F7F\u102B\u102C\u1031\u1038\u103B\u103C\u1056\u1057\u1062-\u1064\u1067-\u106D\u1083\u1084\u1087-\u108C\u108F\u109A-\u109C\u17B6\u17BE-\u17C5\u17C7\u17C8\u1923-\u1926\u1929-\u192B\u1930\u1931\u1933-\u1938\u1A19\u1A1A\u1A55\u1A57\u1A61\u1A63\u1A64\u1A6D-\u1A72\u1B04\u1B35\u1B3B\u1B3D-\u1B41\u1B43\u1B44\u1B82\u1BA1\u1BA6\u1BA7\u1BAA\u1BE7\u1BEA-\u1BEC\u1BEE\u1BF2\u1BF3\u1C24-\u1C2B\u1C34\u1C35\u1CE1\u1CF2\u1CF3\u302E\u302F\uA823\uA824\uA827\uA880\uA881\uA8B4-\uA8C3\uA952\uA953\uA983\uA9B4\uA9B5\uA9BA\uA9BB\uA9BD-\uA9C0\uAA2F\uAA30\uAA33\uAA34\uAA4D\uAA7B\uAA7D\uAAEB\uAAEE\uAAEF\uAAF5\uABE3\uABE4\uABE6\uABE7\uABE9\uABEA\uABEC',
            astral: '\uD804[\uDC00\uDC02\uDC82\uDCB0-\uDCB2\uDCB7\uDCB8\uDD2C\uDD82\uDDB3-\uDDB5\uDDBF\uDDC0\uDE2C-\uDE2E\uDE32\uDE33\uDE35\uDEE0-\uDEE2\uDF02\uDF03\uDF3E\uDF3F\uDF41-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF57\uDF62\uDF63]|\uD805[\uDC35-\uDC37\uDC40\uDC41\uDC45\uDCB0-\uDCB2\uDCB9\uDCBB-\uDCBE\uDCC1\uDDAF-\uDDB1\uDDB8-\uDDBB\uDDBE\uDE30-\uDE32\uDE3B\uDE3C\uDE3E\uDEAC\uDEAE\uDEAF\uDEB6\uDF20\uDF21\uDF26]|\uD807[\uDC2F\uDC3E\uDCA9\uDCB1\uDCB4]|\uD81B[\uDF51-\uDF7E]|\uD834[\uDD65\uDD66\uDD6D-\uDD72]'
        },
        {
            name: 'Me',
            alias: 'Enclosing_Mark',
            bmp: '\u0488\u0489\u1ABE\u20DD-\u20E0\u20E2-\u20E4\uA670-\uA672'
        },
        {
            name: 'Mn',
            alias: 'Nonspacing_Mark',
            bmp: '\u0300-\u036F\u0483-\u0487\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D4-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2\u09E3\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B62\u0B63\u0B82\u0BC0\u0BCD\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CBF\u0CC6\u0CCC\u0CCD\u0CE2\u0CE3\u0D01\u0D41-\u0D44\u0D4D\u0D62\u0D63\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFB-\u1DFF\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u3099\u309A\uA66F\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA8C4\uA8C5\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F',
            astral: '\uD800[\uDDFD\uDEE0\uDF76-\uDF7A]|\uD802[\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD804[\uDC01\uDC38-\uDC46\uDC7F-\uDC81\uDCB3-\uDCB6\uDCB9\uDCBA\uDD00-\uDD02\uDD27-\uDD2B\uDD2D-\uDD34\uDD73\uDD80\uDD81\uDDB6-\uDDBE\uDDCA-\uDDCC\uDE2F-\uDE31\uDE34\uDE36\uDE37\uDE3E\uDEDF\uDEE3-\uDEEA\uDF00\uDF01\uDF3C\uDF40\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC38-\uDC3F\uDC42-\uDC44\uDC46\uDCB3-\uDCB8\uDCBA\uDCBF\uDCC0\uDCC2\uDCC3\uDDB2-\uDDB5\uDDBC\uDDBD\uDDBF\uDDC0\uDDDC\uDDDD\uDE33-\uDE3A\uDE3D\uDE3F\uDE40\uDEAB\uDEAD\uDEB0-\uDEB5\uDEB7\uDF1D-\uDF1F\uDF22-\uDF25\uDF27-\uDF2B]|\uD807[\uDC30-\uDC36\uDC38-\uDC3D\uDC3F\uDC92-\uDCA7\uDCAA-\uDCB0\uDCB2\uDCB3\uDCB5\uDCB6]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDF8F-\uDF92]|\uD82F[\uDC9D\uDC9E]|\uD834[\uDD67-\uDD69\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDCD0-\uDCD6\uDD44-\uDD4A]|\uDB40[\uDD00-\uDDEF]'
        },
        {
            name: 'N',
            alias: 'Number',
            bmp: '0-9\xB2\xB3\xB9\xBC-\xBE\u0660-\u0669\u06F0-\u06F9\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u09F4-\u09F9\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0B72-\u0B77\u0BE6-\u0BF2\u0C66-\u0C6F\u0C78-\u0C7E\u0CE6-\u0CEF\u0D58-\u0D5E\u0D66-\u0D78\u0DE6-\u0DEF\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F33\u1040-\u1049\u1090-\u1099\u1369-\u137C\u16EE-\u16F0\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1946-\u194F\u19D0-\u19DA\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49\u1C50-\u1C59\u2070\u2074-\u2079\u2080-\u2089\u2150-\u2182\u2185-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2CFD\u3007\u3021-\u3029\u3038-\u303A\u3192-\u3195\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\uA620-\uA629\uA6E6-\uA6EF\uA830-\uA835\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uA9F0-\uA9F9\uAA50-\uAA59\uABF0-\uABF9\uFF10-\uFF19',
            astral: '\uD800[\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDEE1-\uDEFB\uDF20-\uDF23\uDF41\uDF4A\uDFD1-\uDFD5]|\uD801[\uDCA0-\uDCA9]|\uD802[\uDC58-\uDC5F\uDC79-\uDC7F\uDCA7-\uDCAF\uDCFB-\uDCFF\uDD16-\uDD1B\uDDBC\uDDBD\uDDC0-\uDDCF\uDDD2-\uDDFF\uDE40-\uDE47\uDE7D\uDE7E\uDE9D-\uDE9F\uDEEB-\uDEEF\uDF58-\uDF5F\uDF78-\uDF7F\uDFA9-\uDFAF]|\uD803[\uDCFA-\uDCFF\uDE60-\uDE7E]|\uD804[\uDC52-\uDC6F\uDCF0-\uDCF9\uDD36-\uDD3F\uDDD0-\uDDD9\uDDE1-\uDDF4\uDEF0-\uDEF9]|\uD805[\uDC50-\uDC59\uDCD0-\uDCD9\uDE50-\uDE59\uDEC0-\uDEC9\uDF30-\uDF3B]|\uD806[\uDCE0-\uDCF2]|\uD807[\uDC50-\uDC6C]|\uD809[\uDC00-\uDC6E]|\uD81A[\uDE60-\uDE69\uDF50-\uDF59\uDF5B-\uDF61]|\uD834[\uDF60-\uDF71]|\uD835[\uDFCE-\uDFFF]|\uD83A[\uDCC7-\uDCCF\uDD50-\uDD59]|\uD83C[\uDD00-\uDD0C]'
        },
        {
            name: 'Nd',
            alias: 'Decimal_Number',
            bmp: '0-9\u0660-\u0669\u06F0-\u06F9\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F\u0CE6-\u0CEF\u0D66-\u0D6F\u0DE6-\u0DEF\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29\u1040-\u1049\u1090-\u1099\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49\u1C50-\u1C59\uA620-\uA629\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uA9F0-\uA9F9\uAA50-\uAA59\uABF0-\uABF9\uFF10-\uFF19',
            astral: '\uD801[\uDCA0-\uDCA9]|\uD804[\uDC66-\uDC6F\uDCF0-\uDCF9\uDD36-\uDD3F\uDDD0-\uDDD9\uDEF0-\uDEF9]|\uD805[\uDC50-\uDC59\uDCD0-\uDCD9\uDE50-\uDE59\uDEC0-\uDEC9\uDF30-\uDF39]|\uD806[\uDCE0-\uDCE9]|\uD807[\uDC50-\uDC59]|\uD81A[\uDE60-\uDE69\uDF50-\uDF59]|\uD835[\uDFCE-\uDFFF]|\uD83A[\uDD50-\uDD59]'
        },
        {
            name: 'Nl',
            alias: 'Letter_Number',
            bmp: '\u16EE-\u16F0\u2160-\u2182\u2185-\u2188\u3007\u3021-\u3029\u3038-\u303A\uA6E6-\uA6EF',
            astral: '\uD800[\uDD40-\uDD74\uDF41\uDF4A\uDFD1-\uDFD5]|\uD809[\uDC00-\uDC6E]'
        },
        {
            name: 'No',
            alias: 'Other_Number',
            bmp: '\xB2\xB3\xB9\xBC-\xBE\u09F4-\u09F9\u0B72-\u0B77\u0BF0-\u0BF2\u0C78-\u0C7E\u0D58-\u0D5E\u0D70-\u0D78\u0F2A-\u0F33\u1369-\u137C\u17F0-\u17F9\u19DA\u2070\u2074-\u2079\u2080-\u2089\u2150-\u215F\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2CFD\u3192-\u3195\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\uA830-\uA835',
            astral: '\uD800[\uDD07-\uDD33\uDD75-\uDD78\uDD8A\uDD8B\uDEE1-\uDEFB\uDF20-\uDF23]|\uD802[\uDC58-\uDC5F\uDC79-\uDC7F\uDCA7-\uDCAF\uDCFB-\uDCFF\uDD16-\uDD1B\uDDBC\uDDBD\uDDC0-\uDDCF\uDDD2-\uDDFF\uDE40-\uDE47\uDE7D\uDE7E\uDE9D-\uDE9F\uDEEB-\uDEEF\uDF58-\uDF5F\uDF78-\uDF7F\uDFA9-\uDFAF]|\uD803[\uDCFA-\uDCFF\uDE60-\uDE7E]|\uD804[\uDC52-\uDC65\uDDE1-\uDDF4]|\uD805[\uDF3A\uDF3B]|\uD806[\uDCEA-\uDCF2]|\uD807[\uDC5A-\uDC6C]|\uD81A[\uDF5B-\uDF61]|\uD834[\uDF60-\uDF71]|\uD83A[\uDCC7-\uDCCF]|\uD83C[\uDD00-\uDD0C]'
        },
        {
            name: 'P',
            alias: 'Punctuation',
            bmp: '\x21-\x23\x25-\\x2A\x2C-\x2F\x3A\x3B\\x3F\x40\\x5B-\\x5D\x5F\\x7B\x7D\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E44\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65',
            astral: '\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDF3C-\uDF3E]|\uD807[\uDC41-\uDC45\uDC70\uDC71]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]'
        },
        {
            name: 'Pc',
            alias: 'Connector_Punctuation',
            bmp: '\x5F\u203F\u2040\u2054\uFE33\uFE34\uFE4D-\uFE4F\uFF3F'
        },
        {
            name: 'Pd',
            alias: 'Dash_Punctuation',
            bmp: '\\x2D\u058A\u05BE\u1400\u1806\u2010-\u2015\u2E17\u2E1A\u2E3A\u2E3B\u2E40\u301C\u3030\u30A0\uFE31\uFE32\uFE58\uFE63\uFF0D'
        },
        {
            name: 'Pe',
            alias: 'Close_Punctuation',
            bmp: '\\x29\\x5D\x7D\u0F3B\u0F3D\u169C\u2046\u207E\u208E\u2309\u230B\u232A\u2769\u276B\u276D\u276F\u2771\u2773\u2775\u27C6\u27E7\u27E9\u27EB\u27ED\u27EF\u2984\u2986\u2988\u298A\u298C\u298E\u2990\u2992\u2994\u2996\u2998\u29D9\u29DB\u29FD\u2E23\u2E25\u2E27\u2E29\u3009\u300B\u300D\u300F\u3011\u3015\u3017\u3019\u301B\u301E\u301F\uFD3E\uFE18\uFE36\uFE38\uFE3A\uFE3C\uFE3E\uFE40\uFE42\uFE44\uFE48\uFE5A\uFE5C\uFE5E\uFF09\uFF3D\uFF5D\uFF60\uFF63'
        },
        {
            name: 'Pf',
            alias: 'Final_Punctuation',
            bmp: '\xBB\u2019\u201D\u203A\u2E03\u2E05\u2E0A\u2E0D\u2E1D\u2E21'
        },
        {
            name: 'Pi',
            alias: 'Initial_Punctuation',
            bmp: '\xAB\u2018\u201B\u201C\u201F\u2039\u2E02\u2E04\u2E09\u2E0C\u2E1C\u2E20'
        },
        {
            name: 'Po',
            alias: 'Other_Punctuation',
            bmp: '\x21-\x23\x25-\x27\\x2A\x2C\\x2E\x2F\x3A\x3B\\x3F\x40\\x5C\xA1\xA7\xB6\xB7\xBF\u037E\u0387\u055A-\u055F\u0589\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u166D\u166E\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u1805\u1807-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2016\u2017\u2020-\u2027\u2030-\u2038\u203B-\u203E\u2041-\u2043\u2047-\u2051\u2053\u2055-\u205E\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00\u2E01\u2E06-\u2E08\u2E0B\u2E0E-\u2E16\u2E18\u2E19\u2E1B\u2E1E\u2E1F\u2E2A-\u2E2E\u2E30-\u2E39\u2E3C-\u2E3F\u2E41\u2E43\u2E44\u3001-\u3003\u303D\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFE10-\uFE16\uFE19\uFE30\uFE45\uFE46\uFE49-\uFE4C\uFE50-\uFE52\uFE54-\uFE57\uFE5F-\uFE61\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF07\uFF0A\uFF0C\uFF0E\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3C\uFF61\uFF64\uFF65',
            astral: '\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDF3C-\uDF3E]|\uD807[\uDC41-\uDC45\uDC70\uDC71]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]'
        },
        {
            name: 'Ps',
            alias: 'Open_Punctuation',
            bmp: '\\x28\\x5B\\x7B\u0F3A\u0F3C\u169B\u201A\u201E\u2045\u207D\u208D\u2308\u230A\u2329\u2768\u276A\u276C\u276E\u2770\u2772\u2774\u27C5\u27E6\u27E8\u27EA\u27EC\u27EE\u2983\u2985\u2987\u2989\u298B\u298D\u298F\u2991\u2993\u2995\u2997\u29D8\u29DA\u29FC\u2E22\u2E24\u2E26\u2E28\u2E42\u3008\u300A\u300C\u300E\u3010\u3014\u3016\u3018\u301A\u301D\uFD3F\uFE17\uFE35\uFE37\uFE39\uFE3B\uFE3D\uFE3F\uFE41\uFE43\uFE47\uFE59\uFE5B\uFE5D\uFF08\uFF3B\uFF5B\uFF5F\uFF62'
        },
        {
            name: 'S',
            alias: 'Symbol',
            bmp: '\\x24\\x2B\x3C-\x3E\\x5E\x60\\x7C\x7E\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D4F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20BE\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u23FE\u2400-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B98-\u2BB9\u2BBD-\u2BC8\u2BCA-\u2BD1\u2BEC-\u2BEF\u2CE5-\u2CEA\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u32FE\u3300-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uFB29\uFBB2-\uFBC1\uFDFC\uFDFD\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD',
            astral: '\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C-\uDD8E\uDD90-\uDD9B\uDDA0\uDDD0-\uDDFC]|\uD802[\uDC77\uDC78\uDEC8]|\uD805\uDF3F|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD82F\uDC9C|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDE8\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD83B[\uDEF0\uDEF1]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD10-\uDD2E\uDD30-\uDD6B\uDD70-\uDDAC\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDF00-\uDFFF]|\uD83D[\uDC00-\uDED2\uDEE0-\uDEEC\uDEF0-\uDEF6\uDF00-\uDF73\uDF80-\uDFD4]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDD10-\uDD1E\uDD20-\uDD27\uDD30\uDD33-\uDD3E\uDD40-\uDD4B\uDD50-\uDD5E\uDD80-\uDD91\uDDC0]'
        },
        {
            name: 'Sc',
            alias: 'Currency_Symbol',
            bmp: '\\x24\xA2-\xA5\u058F\u060B\u09F2\u09F3\u09FB\u0AF1\u0BF9\u0E3F\u17DB\u20A0-\u20BE\uA838\uFDFC\uFE69\uFF04\uFFE0\uFFE1\uFFE5\uFFE6'
        },
        {
            name: 'Sk',
            alias: 'Modifier_Symbol',
            bmp: '\\x5E\x60\xA8\xAF\xB4\xB8\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u309B\u309C\uA700-\uA716\uA720\uA721\uA789\uA78A\uAB5B\uFBB2-\uFBC1\uFF3E\uFF40\uFFE3',
            astral: '\uD83C[\uDFFB-\uDFFF]'
        },
        {
            name: 'Sm',
            alias: 'Math_Symbol',
            bmp: '\\x2B\x3C-\x3E\\x7C\x7E\xAC\xB1\xD7\xF7\u03F6\u0606-\u0608\u2044\u2052\u207A-\u207C\u208A-\u208C\u2118\u2140-\u2144\u214B\u2190-\u2194\u219A\u219B\u21A0\u21A3\u21A6\u21AE\u21CE\u21CF\u21D2\u21D4\u21F4-\u22FF\u2320\u2321\u237C\u239B-\u23B3\u23DC-\u23E1\u25B7\u25C1\u25F8-\u25FF\u266F\u27C0-\u27C4\u27C7-\u27E5\u27F0-\u27FF\u2900-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2AFF\u2B30-\u2B44\u2B47-\u2B4C\uFB29\uFE62\uFE64-\uFE66\uFF0B\uFF1C-\uFF1E\uFF5C\uFF5E\uFFE2\uFFE9-\uFFEC',
            astral: '\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD83B[\uDEF0\uDEF1]'
        },
        {
            name: 'So',
            alias: 'Other_Symbol',
            bmp: '\xA6\xA9\xAE\xB0\u0482\u058D\u058E\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u09FA\u0B70\u0BF3-\u0BF8\u0BFA\u0C7F\u0D4F\u0D79\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116\u2117\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u214A\u214C\u214D\u214F\u218A\u218B\u2195-\u2199\u219C-\u219F\u21A1\u21A2\u21A4\u21A5\u21A7-\u21AD\u21AF-\u21CD\u21D0\u21D1\u21D3\u21D5-\u21F3\u2300-\u2307\u230C-\u231F\u2322-\u2328\u232B-\u237B\u237D-\u239A\u23B4-\u23DB\u23E2-\u23FE\u2400-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u25B6\u25B8-\u25C0\u25C2-\u25F7\u2600-\u266E\u2670-\u2767\u2794-\u27BF\u2800-\u28FF\u2B00-\u2B2F\u2B45\u2B46\u2B4D-\u2B73\u2B76-\u2B95\u2B98-\u2BB9\u2BBD-\u2BC8\u2BCA-\u2BD1\u2BEC-\u2BEF\u2CE5-\u2CEA\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u32FE\u3300-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA828-\uA82B\uA836\uA837\uA839\uAA77-\uAA79\uFDFD\uFFE4\uFFE8\uFFED\uFFEE\uFFFC\uFFFD',
            astral: '\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C-\uDD8E\uDD90-\uDD9B\uDDA0\uDDD0-\uDDFC]|\uD802[\uDC77\uDC78\uDEC8]|\uD805\uDF3F|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD82F\uDC9C|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDE8\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD10-\uDD2E\uDD30-\uDD6B\uDD70-\uDDAC\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDF00-\uDFFA]|\uD83D[\uDC00-\uDED2\uDEE0-\uDEEC\uDEF0-\uDEF6\uDF00-\uDF73\uDF80-\uDFD4]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDD10-\uDD1E\uDD20-\uDD27\uDD30\uDD33-\uDD3E\uDD40-\uDD4B\uDD50-\uDD5E\uDD80-\uDD91\uDDC0]'
        },
        {
            name: 'Z',
            alias: 'Separator',
            bmp: '\x20\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000'
        },
        {
            name: 'Zl',
            alias: 'Line_Separator',
            bmp: '\u2028'
        },
        {
            name: 'Zp',
            alias: 'Paragraph_Separator',
            bmp: '\u2029'
        },
        {
            name: 'Zs',
            alias: 'Space_Separator',
            bmp: '\x20\xA0\u1680\u2000-\u200A\u202F\u205F\u3000'
        }
    ]);

};

},{}],6:[function(require,module,exports){
/*!
 * XRegExp Unicode Properties 3.2.0
 * <xregexp.com>
 * Steven Levithan (c) 2012-2017 MIT License
 * Unicode data by Mathias Bynens <mathiasbynens.be>
 */

module.exports = function(XRegExp) {
    'use strict';

    /**
     * Adds properties to meet the UTS #18 Level 1 RL1.2 requirements for Unicode regex support. See
     * <http://unicode.org/reports/tr18/#RL1.2>. Following are definitions of these properties from
     * UAX #44 <http://unicode.org/reports/tr44/>:
     *
     * - Alphabetic
     *   Characters with the Alphabetic property. Generated from: Lowercase + Uppercase + Lt + Lm +
     *   Lo + Nl + Other_Alphabetic.
     *
     * - Default_Ignorable_Code_Point
     *   For programmatic determination of default ignorable code points. New characters that should
     *   be ignored in rendering (unless explicitly supported) will be assigned in these ranges,
     *   permitting programs to correctly handle the default rendering of such characters when not
     *   otherwise supported.
     *
     * - Lowercase
     *   Characters with the Lowercase property. Generated from: Ll + Other_Lowercase.
     *
     * - Noncharacter_Code_Point
     *   Code points permanently reserved for internal use.
     *
     * - Uppercase
     *   Characters with the Uppercase property. Generated from: Lu + Other_Uppercase.
     *
     * - White_Space
     *   Spaces, separator characters and other control characters which should be treated by
     *   programming languages as "white space" for the purpose of parsing elements.
     *
     * The properties ASCII, Any, and Assigned are also included but are not defined in UAX #44. UTS
     * #18 RL1.2 additionally requires support for Unicode scripts and general categories. These are
     * included in XRegExp's Unicode Categories and Unicode Scripts addons.
     *
     * Token names are case insensitive, and any spaces, hyphens, and underscores are ignored.
     *
     * Uses Unicode 9.0.0.
     *
     * @requires XRegExp, Unicode Base
     */

    if (!XRegExp.addUnicodeData) {
        throw new ReferenceError('Unicode Base must be loaded before Unicode Properties');
    }

    var unicodeData = [
        {
            name: 'ASCII',
            bmp: '\0-\x7F'
        },
        {
            name: 'Alphabetic',
            bmp: 'A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0345\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05B0-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0657\u0659-\u065F\u066E-\u06D3\u06D5-\u06DC\u06E1-\u06E8\u06ED-\u06EF\u06FA-\u06FC\u06FF\u0710-\u073F\u074D-\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0817\u081A-\u082C\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08DF\u08E3-\u08E9\u08F0-\u093B\u093D-\u094C\u094E-\u0950\u0955-\u0963\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD-\u09C4\u09C7\u09C8\u09CB\u09CC\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09F0\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3E-\u0A42\u0A47\u0A48\u0A4B\u0A4C\u0A51\u0A59-\u0A5C\u0A5E\u0A70-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD-\u0AC5\u0AC7-\u0AC9\u0ACB\u0ACC\u0AD0\u0AE0-\u0AE3\u0AF9\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D-\u0B44\u0B47\u0B48\u0B4B\u0B4C\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCC\u0BD0\u0BD7\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4C\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCC\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4C\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E46\u0E4D\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0ECD\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F71-\u0F81\u0F88-\u0F97\u0F99-\u0FBC\u1000-\u1036\u1038\u103B-\u103F\u1050-\u1062\u1065-\u1068\u106E-\u1086\u108E\u109C\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1713\u1720-\u1733\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17B3\u17B6-\u17C8\u17D7\u17DC\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u1938\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A1B\u1A20-\u1A5E\u1A61-\u1A74\u1AA7\u1B00-\u1B33\u1B35-\u1B43\u1B45-\u1B4B\u1B80-\u1BA9\u1BAC-\u1BAF\u1BBA-\u1BE5\u1BE7-\u1BF1\u1C00-\u1C35\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1D00-\u1DBF\u1DE7-\u1DF4\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u24B6-\u24E9\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA674-\uA67B\uA67F-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA827\uA840-\uA873\uA880-\uA8C3\uA8C5\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA92A\uA930-\uA952\uA960-\uA97C\uA980-\uA9B2\uA9B4-\uA9BF\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA60-\uAA76\uAA7A\uAA7E-\uAABE\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF5\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC',
            astral: '\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC45\uDC82-\uDCB8\uDCD0-\uDCE8\uDD00-\uDD32\uDD50-\uDD72\uDD76\uDD80-\uDDBF\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE34\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEE8\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D-\uDF44\uDF47\uDF48\uDF4B\uDF4C\uDF50\uDF57\uDF5D-\uDF63]|\uD805[\uDC00-\uDC41\uDC43-\uDC45\uDC47-\uDC4A\uDC80-\uDCC1\uDCC4\uDCC5\uDCC7\uDD80-\uDDB5\uDDB8-\uDDBE\uDDD8-\uDDDD\uDE00-\uDE3E\uDE40\uDE44\uDE80-\uDEB5\uDF00-\uDF19\uDF1D-\uDF2A]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC3E\uDC40\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF36\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF93-\uDF9F\uDFE0]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9E]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43\uDD47]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD30-\uDD49\uDD50-\uDD69\uDD70-\uDD89]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]'
        },
        {
            name: 'Any',
            isBmpLast: true,
            bmp: '\0-\uFFFF',
            astral: '[\uD800-\uDBFF][\uDC00-\uDFFF]'
        },
        {
            name: 'Default_Ignorable_Code_Point',
            bmp: '\xAD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180E\u200B-\u200F\u202A-\u202E\u2060-\u206F\u3164\uFE00-\uFE0F\uFEFF\uFFA0\uFFF0-\uFFF8',
            astral: '\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|[\uDB40-\uDB43][\uDC00-\uDFFF]'
        },
        {
            name: 'Lowercase',
            bmp: 'a-z\xAA\xB5\xBA\xDF-\xF6\xF8-\xFF\u0101\u0103\u0105\u0107\u0109\u010B\u010D\u010F\u0111\u0113\u0115\u0117\u0119\u011B\u011D\u011F\u0121\u0123\u0125\u0127\u0129\u012B\u012D\u012F\u0131\u0133\u0135\u0137\u0138\u013A\u013C\u013E\u0140\u0142\u0144\u0146\u0148\u0149\u014B\u014D\u014F\u0151\u0153\u0155\u0157\u0159\u015B\u015D\u015F\u0161\u0163\u0165\u0167\u0169\u016B\u016D\u016F\u0171\u0173\u0175\u0177\u017A\u017C\u017E-\u0180\u0183\u0185\u0188\u018C\u018D\u0192\u0195\u0199-\u019B\u019E\u01A1\u01A3\u01A5\u01A8\u01AA\u01AB\u01AD\u01B0\u01B4\u01B6\u01B9\u01BA\u01BD-\u01BF\u01C6\u01C9\u01CC\u01CE\u01D0\u01D2\u01D4\u01D6\u01D8\u01DA\u01DC\u01DD\u01DF\u01E1\u01E3\u01E5\u01E7\u01E9\u01EB\u01ED\u01EF\u01F0\u01F3\u01F5\u01F9\u01FB\u01FD\u01FF\u0201\u0203\u0205\u0207\u0209\u020B\u020D\u020F\u0211\u0213\u0215\u0217\u0219\u021B\u021D\u021F\u0221\u0223\u0225\u0227\u0229\u022B\u022D\u022F\u0231\u0233-\u0239\u023C\u023F\u0240\u0242\u0247\u0249\u024B\u024D\u024F-\u0293\u0295-\u02B8\u02C0\u02C1\u02E0-\u02E4\u0345\u0371\u0373\u0377\u037A-\u037D\u0390\u03AC-\u03CE\u03D0\u03D1\u03D5-\u03D7\u03D9\u03DB\u03DD\u03DF\u03E1\u03E3\u03E5\u03E7\u03E9\u03EB\u03ED\u03EF-\u03F3\u03F5\u03F8\u03FB\u03FC\u0430-\u045F\u0461\u0463\u0465\u0467\u0469\u046B\u046D\u046F\u0471\u0473\u0475\u0477\u0479\u047B\u047D\u047F\u0481\u048B\u048D\u048F\u0491\u0493\u0495\u0497\u0499\u049B\u049D\u049F\u04A1\u04A3\u04A5\u04A7\u04A9\u04AB\u04AD\u04AF\u04B1\u04B3\u04B5\u04B7\u04B9\u04BB\u04BD\u04BF\u04C2\u04C4\u04C6\u04C8\u04CA\u04CC\u04CE\u04CF\u04D1\u04D3\u04D5\u04D7\u04D9\u04DB\u04DD\u04DF\u04E1\u04E3\u04E5\u04E7\u04E9\u04EB\u04ED\u04EF\u04F1\u04F3\u04F5\u04F7\u04F9\u04FB\u04FD\u04FF\u0501\u0503\u0505\u0507\u0509\u050B\u050D\u050F\u0511\u0513\u0515\u0517\u0519\u051B\u051D\u051F\u0521\u0523\u0525\u0527\u0529\u052B\u052D\u052F\u0561-\u0587\u13F8-\u13FD\u1C80-\u1C88\u1D00-\u1DBF\u1E01\u1E03\u1E05\u1E07\u1E09\u1E0B\u1E0D\u1E0F\u1E11\u1E13\u1E15\u1E17\u1E19\u1E1B\u1E1D\u1E1F\u1E21\u1E23\u1E25\u1E27\u1E29\u1E2B\u1E2D\u1E2F\u1E31\u1E33\u1E35\u1E37\u1E39\u1E3B\u1E3D\u1E3F\u1E41\u1E43\u1E45\u1E47\u1E49\u1E4B\u1E4D\u1E4F\u1E51\u1E53\u1E55\u1E57\u1E59\u1E5B\u1E5D\u1E5F\u1E61\u1E63\u1E65\u1E67\u1E69\u1E6B\u1E6D\u1E6F\u1E71\u1E73\u1E75\u1E77\u1E79\u1E7B\u1E7D\u1E7F\u1E81\u1E83\u1E85\u1E87\u1E89\u1E8B\u1E8D\u1E8F\u1E91\u1E93\u1E95-\u1E9D\u1E9F\u1EA1\u1EA3\u1EA5\u1EA7\u1EA9\u1EAB\u1EAD\u1EAF\u1EB1\u1EB3\u1EB5\u1EB7\u1EB9\u1EBB\u1EBD\u1EBF\u1EC1\u1EC3\u1EC5\u1EC7\u1EC9\u1ECB\u1ECD\u1ECF\u1ED1\u1ED3\u1ED5\u1ED7\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1EE5\u1EE7\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u1EF3\u1EF5\u1EF7\u1EF9\u1EFB\u1EFD\u1EFF-\u1F07\u1F10-\u1F15\u1F20-\u1F27\u1F30-\u1F37\u1F40-\u1F45\u1F50-\u1F57\u1F60-\u1F67\u1F70-\u1F7D\u1F80-\u1F87\u1F90-\u1F97\u1FA0-\u1FA7\u1FB0-\u1FB4\u1FB6\u1FB7\u1FBE\u1FC2-\u1FC4\u1FC6\u1FC7\u1FD0-\u1FD3\u1FD6\u1FD7\u1FE0-\u1FE7\u1FF2-\u1FF4\u1FF6\u1FF7\u2071\u207F\u2090-\u209C\u210A\u210E\u210F\u2113\u212F\u2134\u2139\u213C\u213D\u2146-\u2149\u214E\u2170-\u217F\u2184\u24D0-\u24E9\u2C30-\u2C5E\u2C61\u2C65\u2C66\u2C68\u2C6A\u2C6C\u2C71\u2C73\u2C74\u2C76-\u2C7D\u2C81\u2C83\u2C85\u2C87\u2C89\u2C8B\u2C8D\u2C8F\u2C91\u2C93\u2C95\u2C97\u2C99\u2C9B\u2C9D\u2C9F\u2CA1\u2CA3\u2CA5\u2CA7\u2CA9\u2CAB\u2CAD\u2CAF\u2CB1\u2CB3\u2CB5\u2CB7\u2CB9\u2CBB\u2CBD\u2CBF\u2CC1\u2CC3\u2CC5\u2CC7\u2CC9\u2CCB\u2CCD\u2CCF\u2CD1\u2CD3\u2CD5\u2CD7\u2CD9\u2CDB\u2CDD\u2CDF\u2CE1\u2CE3\u2CE4\u2CEC\u2CEE\u2CF3\u2D00-\u2D25\u2D27\u2D2D\uA641\uA643\uA645\uA647\uA649\uA64B\uA64D\uA64F\uA651\uA653\uA655\uA657\uA659\uA65B\uA65D\uA65F\uA661\uA663\uA665\uA667\uA669\uA66B\uA66D\uA681\uA683\uA685\uA687\uA689\uA68B\uA68D\uA68F\uA691\uA693\uA695\uA697\uA699\uA69B-\uA69D\uA723\uA725\uA727\uA729\uA72B\uA72D\uA72F-\uA731\uA733\uA735\uA737\uA739\uA73B\uA73D\uA73F\uA741\uA743\uA745\uA747\uA749\uA74B\uA74D\uA74F\uA751\uA753\uA755\uA757\uA759\uA75B\uA75D\uA75F\uA761\uA763\uA765\uA767\uA769\uA76B\uA76D\uA76F-\uA778\uA77A\uA77C\uA77F\uA781\uA783\uA785\uA787\uA78C\uA78E\uA791\uA793-\uA795\uA797\uA799\uA79B\uA79D\uA79F\uA7A1\uA7A3\uA7A5\uA7A7\uA7A9\uA7B5\uA7B7\uA7F8-\uA7FA\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABBF\uFB00-\uFB06\uFB13-\uFB17\uFF41-\uFF5A',
            astral: '\uD801[\uDC28-\uDC4F\uDCD8-\uDCFB]|\uD803[\uDCC0-\uDCF2]|\uD806[\uDCC0-\uDCDF]|\uD835[\uDC1A-\uDC33\uDC4E-\uDC54\uDC56-\uDC67\uDC82-\uDC9B\uDCB6-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDCCF\uDCEA-\uDD03\uDD1E-\uDD37\uDD52-\uDD6B\uDD86-\uDD9F\uDDBA-\uDDD3\uDDEE-\uDE07\uDE22-\uDE3B\uDE56-\uDE6F\uDE8A-\uDEA5\uDEC2-\uDEDA\uDEDC-\uDEE1\uDEFC-\uDF14\uDF16-\uDF1B\uDF36-\uDF4E\uDF50-\uDF55\uDF70-\uDF88\uDF8A-\uDF8F\uDFAA-\uDFC2\uDFC4-\uDFC9\uDFCB]|\uD83A[\uDD22-\uDD43]'
        },
        {
            name: 'Noncharacter_Code_Point',
            bmp: '\uFDD0-\uFDEF\uFFFE\uFFFF',
            astral: '[\uD83F\uD87F\uD8BF\uD8FF\uD93F\uD97F\uD9BF\uD9FF\uDA3F\uDA7F\uDABF\uDAFF\uDB3F\uDB7F\uDBBF\uDBFF][\uDFFE\uDFFF]'
        },
        {
            name: 'Uppercase',
            bmp: 'A-Z\xC0-\xD6\xD8-\xDE\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178\u0179\u017B\u017D\u0181\u0182\u0184\u0186\u0187\u0189-\u018B\u018E-\u0191\u0193\u0194\u0196-\u0198\u019C\u019D\u019F\u01A0\u01A2\u01A4\u01A6\u01A7\u01A9\u01AC\u01AE\u01AF\u01B1-\u01B3\u01B5\u01B7\u01B8\u01BC\u01C4\u01C7\u01CA\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F1\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A\u023B\u023D\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E\u038F\u0391-\u03A1\u03A3-\u03AB\u03CF\u03D2-\u03D4\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F4\u03F7\u03F9\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u10A0-\u10C5\u10C7\u10CD\u13A0-\u13F5\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1FB8-\u1FBB\u1FC8-\u1FCB\u1FD8-\u1FDB\u1FE8-\u1FEC\u1FF8-\u1FFB\u2102\u2107\u210B-\u210D\u2110-\u2112\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u2130-\u2133\u213E\u213F\u2145\u2160-\u216F\u2183\u24B6-\u24CF\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AE\uA7B0-\uA7B4\uA7B6\uFF21-\uFF3A',
            astral: '\uD801[\uDC00-\uDC27\uDCB0-\uDCD3]|\uD803[\uDC80-\uDCB2]|\uD806[\uDCA0-\uDCBF]|\uD835[\uDC00-\uDC19\uDC34-\uDC4D\uDC68-\uDC81\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB5\uDCD0-\uDCE9\uDD04\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD38\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD6C-\uDD85\uDDA0-\uDDB9\uDDD4-\uDDED\uDE08-\uDE21\uDE3C-\uDE55\uDE70-\uDE89\uDEA8-\uDEC0\uDEE2-\uDEFA\uDF1C-\uDF34\uDF56-\uDF6E\uDF90-\uDFA8\uDFCA]|\uD83A[\uDD00-\uDD21]|\uD83C[\uDD30-\uDD49\uDD50-\uDD69\uDD70-\uDD89]'
        },
        {
            name: 'White_Space',
            bmp: '\x09-\x0D\x20\x85\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000'
        }
    ];

    // Add non-generated data
    unicodeData.push({
        name: 'Assigned',
        // Since this is defined as the inverse of Unicode category Cn (Unassigned), the Unicode
        // Categories addon is required to use this property
        inverseOf: 'Cn'
    });

    XRegExp.addUnicodeData(unicodeData);

};

},{}],7:[function(require,module,exports){
/*!
 * XRegExp Unicode Scripts 3.2.0
 * <xregexp.com>
 * Steven Levithan (c) 2010-2017 MIT License
 * Unicode data by Mathias Bynens <mathiasbynens.be>
 */

module.exports = function(XRegExp) {
    'use strict';

    /**
     * Adds support for all Unicode scripts. E.g., `\p{Latin}`. Token names are case insensitive,
     * and any spaces, hyphens, and underscores are ignored.
     *
     * Uses Unicode 9.0.0.
     *
     * @requires XRegExp, Unicode Base
     */

    if (!XRegExp.addUnicodeData) {
        throw new ReferenceError('Unicode Base must be loaded before Unicode Scripts');
    }

    XRegExp.addUnicodeData([
        {
            name: 'Adlam',
            astral: '\uD83A[\uDD00-\uDD4A\uDD50-\uDD59\uDD5E\uDD5F]'
        },
        {
            name: 'Ahom',
            astral: '\uD805[\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF3F]'
        },
        {
            name: 'Anatolian_Hieroglyphs',
            astral: '\uD811[\uDC00-\uDE46]'
        },
        {
            name: 'Arabic',
            bmp: '\u0600-\u0604\u0606-\u060B\u060D-\u061A\u061E\u0620-\u063F\u0641-\u064A\u0656-\u066F\u0671-\u06DC\u06DE-\u06FF\u0750-\u077F\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u08FF\uFB50-\uFBC1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFD\uFE70-\uFE74\uFE76-\uFEFC',
            astral: '\uD803[\uDE60-\uDE7E]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB\uDEF0\uDEF1]'
        },
        {
            name: 'Armenian',
            bmp: '\u0531-\u0556\u0559-\u055F\u0561-\u0587\u058A\u058D-\u058F\uFB13-\uFB17'
        },
        {
            name: 'Avestan',
            astral: '\uD802[\uDF00-\uDF35\uDF39-\uDF3F]'
        },
        {
            name: 'Balinese',
            bmp: '\u1B00-\u1B4B\u1B50-\u1B7C'
        },
        {
            name: 'Bamum',
            bmp: '\uA6A0-\uA6F7',
            astral: '\uD81A[\uDC00-\uDE38]'
        },
        {
            name: 'Bassa_Vah',
            astral: '\uD81A[\uDED0-\uDEED\uDEF0-\uDEF5]'
        },
        {
            name: 'Batak',
            bmp: '\u1BC0-\u1BF3\u1BFC-\u1BFF'
        },
        {
            name: 'Bengali',
            bmp: '\u0980-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09FB'
        },
        {
            name: 'Bhaiksuki',
            astral: '\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC45\uDC50-\uDC6C]'
        },
        {
            name: 'Bopomofo',
            bmp: '\u02EA\u02EB\u3105-\u312D\u31A0-\u31BA'
        },
        {
            name: 'Brahmi',
            astral: '\uD804[\uDC00-\uDC4D\uDC52-\uDC6F\uDC7F]'
        },
        {
            name: 'Braille',
            bmp: '\u2800-\u28FF'
        },
        {
            name: 'Buginese',
            bmp: '\u1A00-\u1A1B\u1A1E\u1A1F'
        },
        {
            name: 'Buhid',
            bmp: '\u1740-\u1753'
        },
        {
            name: 'Canadian_Aboriginal',
            bmp: '\u1400-\u167F\u18B0-\u18F5'
        },
        {
            name: 'Carian',
            astral: '\uD800[\uDEA0-\uDED0]'
        },
        {
            name: 'Caucasian_Albanian',
            astral: '\uD801[\uDD30-\uDD63\uDD6F]'
        },
        {
            name: 'Chakma',
            astral: '\uD804[\uDD00-\uDD34\uDD36-\uDD43]'
        },
        {
            name: 'Cham',
            bmp: '\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA5C-\uAA5F'
        },
        {
            name: 'Cherokee',
            bmp: '\u13A0-\u13F5\u13F8-\u13FD\uAB70-\uABBF'
        },
        {
            name: 'Common',
            bmp: '\0-\x40\\x5B-\x60\\x7B-\xA9\xAB-\xB9\xBB-\xBF\xD7\xF7\u02B9-\u02DF\u02E5-\u02E9\u02EC-\u02FF\u0374\u037E\u0385\u0387\u0589\u0605\u060C\u061B\u061C\u061F\u0640\u06DD\u08E2\u0964\u0965\u0E3F\u0FD5-\u0FD8\u10FB\u16EB-\u16ED\u1735\u1736\u1802\u1803\u1805\u1CD3\u1CE1\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u2000-\u200B\u200E-\u2064\u2066-\u2070\u2074-\u207E\u2080-\u208E\u20A0-\u20BE\u2100-\u2125\u2127-\u2129\u212C-\u2131\u2133-\u214D\u214F-\u215F\u2189-\u218B\u2190-\u23FE\u2400-\u2426\u2440-\u244A\u2460-\u27FF\u2900-\u2B73\u2B76-\u2B95\u2B98-\u2BB9\u2BBD-\u2BC8\u2BCA-\u2BD1\u2BEC-\u2BEF\u2E00-\u2E44\u2FF0-\u2FFB\u3000-\u3004\u3006\u3008-\u3020\u3030-\u3037\u303C-\u303F\u309B\u309C\u30A0\u30FB\u30FC\u3190-\u319F\u31C0-\u31E3\u3220-\u325F\u327F-\u32CF\u3358-\u33FF\u4DC0-\u4DFF\uA700-\uA721\uA788-\uA78A\uA830-\uA839\uA92E\uA9CF\uAB5B\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFEFF\uFF01-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFF70\uFF9E\uFF9F\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFF9-\uFFFD',
            astral: '\uD800[\uDD00-\uDD02\uDD07-\uDD33\uDD37-\uDD3F\uDD90-\uDD9B\uDDD0-\uDDFC\uDEE1-\uDEFB]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD66\uDD6A-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDE8\uDF00-\uDF56\uDF60-\uDF71]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDFCB\uDFCE-\uDFFF]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD00-\uDD0C\uDD10-\uDD2E\uDD30-\uDD6B\uDD70-\uDDAC\uDDE6-\uDDFF\uDE01\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDF00-\uDFFF]|\uD83D[\uDC00-\uDED2\uDEE0-\uDEEC\uDEF0-\uDEF6\uDF00-\uDF73\uDF80-\uDFD4]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDD10-\uDD1E\uDD20-\uDD27\uDD30\uDD33-\uDD3E\uDD40-\uDD4B\uDD50-\uDD5E\uDD80-\uDD91\uDDC0]|\uDB40[\uDC01\uDC20-\uDC7F]'
        },
        {
            name: 'Coptic',
            bmp: '\u03E2-\u03EF\u2C80-\u2CF3\u2CF9-\u2CFF'
        },
        {
            name: 'Cuneiform',
            astral: '\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC70-\uDC74\uDC80-\uDD43]'
        },
        {
            name: 'Cypriot',
            astral: '\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F]'
        },
        {
            name: 'Cyrillic',
            bmp: '\u0400-\u0484\u0487-\u052F\u1C80-\u1C88\u1D2B\u1D78\u2DE0-\u2DFF\uA640-\uA69F\uFE2E\uFE2F'
        },
        {
            name: 'Deseret',
            astral: '\uD801[\uDC00-\uDC4F]'
        },
        {
            name: 'Devanagari',
            bmp: '\u0900-\u0950\u0953-\u0963\u0966-\u097F\uA8E0-\uA8FD'
        },
        {
            name: 'Duployan',
            astral: '\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9C-\uDC9F]'
        },
        {
            name: 'Egyptian_Hieroglyphs',
            astral: '\uD80C[\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]'
        },
        {
            name: 'Elbasan',
            astral: '\uD801[\uDD00-\uDD27]'
        },
        {
            name: 'Ethiopic',
            bmp: '\u1200-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u137C\u1380-\u1399\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E'
        },
        {
            name: 'Georgian',
            bmp: '\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u10FF\u2D00-\u2D25\u2D27\u2D2D'
        },
        {
            name: 'Glagolitic',
            bmp: '\u2C00-\u2C2E\u2C30-\u2C5E',
            astral: '\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]'
        },
        {
            name: 'Gothic',
            astral: '\uD800[\uDF30-\uDF4A]'
        },
        {
            name: 'Grantha',
            astral: '\uD804[\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]'
        },
        {
            name: 'Greek',
            bmp: '\u0370-\u0373\u0375-\u0377\u037A-\u037D\u037F\u0384\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03E1\u03F0-\u03FF\u1D26-\u1D2A\u1D5D-\u1D61\u1D66-\u1D6A\u1DBF\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FC4\u1FC6-\u1FD3\u1FD6-\u1FDB\u1FDD-\u1FEF\u1FF2-\u1FF4\u1FF6-\u1FFE\u2126\uAB65',
            astral: '\uD800[\uDD40-\uDD8E\uDDA0]|\uD834[\uDE00-\uDE45]'
        },
        {
            name: 'Gujarati',
            bmp: '\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AF1\u0AF9'
        },
        {
            name: 'Gurmukhi',
            bmp: '\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75'
        },
        {
            name: 'Han',
            bmp: '\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u3005\u3007\u3021-\u3029\u3038-\u303B\u3400-\u4DB5\u4E00-\u9FD5\uF900-\uFA6D\uFA70-\uFAD9',
            astral: '[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]'
        },
        {
            name: 'Hangul',
            bmp: '\u1100-\u11FF\u302E\u302F\u3131-\u318E\u3200-\u321E\u3260-\u327E\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uFFA0-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC'
        },
        {
            name: 'Hanunoo',
            bmp: '\u1720-\u1734'
        },
        {
            name: 'Hatran',
            astral: '\uD802[\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDCFF]'
        },
        {
            name: 'Hebrew',
            bmp: '\u0591-\u05C7\u05D0-\u05EA\u05F0-\u05F4\uFB1D-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFB4F'
        },
        {
            name: 'Hiragana',
            bmp: '\u3041-\u3096\u309D-\u309F',
            astral: '\uD82C\uDC01|\uD83C\uDE00'
        },
        {
            name: 'Imperial_Aramaic',
            astral: '\uD802[\uDC40-\uDC55\uDC57-\uDC5F]'
        },
        {
            name: 'Inherited',
            bmp: '\u0300-\u036F\u0485\u0486\u064B-\u0655\u0670\u0951\u0952\u1AB0-\u1ABE\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFB-\u1DFF\u200C\u200D\u20D0-\u20F0\u302A-\u302D\u3099\u309A\uFE00-\uFE0F\uFE20-\uFE2D',
            astral: '\uD800[\uDDFD\uDEE0]|\uD834[\uDD67-\uDD69\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD]|\uDB40[\uDD00-\uDDEF]'
        },
        {
            name: 'Inscriptional_Pahlavi',
            astral: '\uD802[\uDF60-\uDF72\uDF78-\uDF7F]'
        },
        {
            name: 'Inscriptional_Parthian',
            astral: '\uD802[\uDF40-\uDF55\uDF58-\uDF5F]'
        },
        {
            name: 'Javanese',
            bmp: '\uA980-\uA9CD\uA9D0-\uA9D9\uA9DE\uA9DF'
        },
        {
            name: 'Kaithi',
            astral: '\uD804[\uDC80-\uDCC1]'
        },
        {
            name: 'Kannada',
            bmp: '\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2'
        },
        {
            name: 'Katakana',
            bmp: '\u30A1-\u30FA\u30FD-\u30FF\u31F0-\u31FF\u32D0-\u32FE\u3300-\u3357\uFF66-\uFF6F\uFF71-\uFF9D',
            astral: '\uD82C\uDC00'
        },
        {
            name: 'Kayah_Li',
            bmp: '\uA900-\uA92D\uA92F'
        },
        {
            name: 'Kharoshthi',
            astral: '\uD802[\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F-\uDE47\uDE50-\uDE58]'
        },
        {
            name: 'Khmer',
            bmp: '\u1780-\u17DD\u17E0-\u17E9\u17F0-\u17F9\u19E0-\u19FF'
        },
        {
            name: 'Khojki',
            astral: '\uD804[\uDE00-\uDE11\uDE13-\uDE3E]'
        },
        {
            name: 'Khudawadi',
            astral: '\uD804[\uDEB0-\uDEEA\uDEF0-\uDEF9]'
        },
        {
            name: 'Lao',
            bmp: '\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF'
        },
        {
            name: 'Latin',
            bmp: 'A-Za-z\xAA\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02B8\u02E0-\u02E4\u1D00-\u1D25\u1D2C-\u1D5C\u1D62-\u1D65\u1D6B-\u1D77\u1D79-\u1DBE\u1E00-\u1EFF\u2071\u207F\u2090-\u209C\u212A\u212B\u2132\u214E\u2160-\u2188\u2C60-\u2C7F\uA722-\uA787\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA7FF\uAB30-\uAB5A\uAB5C-\uAB64\uFB00-\uFB06\uFF21-\uFF3A\uFF41-\uFF5A'
        },
        {
            name: 'Lepcha',
            bmp: '\u1C00-\u1C37\u1C3B-\u1C49\u1C4D-\u1C4F'
        },
        {
            name: 'Limbu',
            bmp: '\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1940\u1944-\u194F'
        },
        {
            name: 'Linear_A',
            astral: '\uD801[\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]'
        },
        {
            name: 'Linear_B',
            astral: '\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA]'
        },
        {
            name: 'Lisu',
            bmp: '\uA4D0-\uA4FF'
        },
        {
            name: 'Lycian',
            astral: '\uD800[\uDE80-\uDE9C]'
        },
        {
            name: 'Lydian',
            astral: '\uD802[\uDD20-\uDD39\uDD3F]'
        },
        {
            name: 'Mahajani',
            astral: '\uD804[\uDD50-\uDD76]'
        },
        {
            name: 'Malayalam',
            bmp: '\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4F\u0D54-\u0D63\u0D66-\u0D7F'
        },
        {
            name: 'Mandaic',
            bmp: '\u0840-\u085B\u085E'
        },
        {
            name: 'Manichaean',
            astral: '\uD802[\uDEC0-\uDEE6\uDEEB-\uDEF6]'
        },
        {
            name: 'Marchen',
            astral: '\uD807[\uDC70-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6]'
        },
        {
            name: 'Meetei_Mayek',
            bmp: '\uAAE0-\uAAF6\uABC0-\uABED\uABF0-\uABF9'
        },
        {
            name: 'Mende_Kikakui',
            astral: '\uD83A[\uDC00-\uDCC4\uDCC7-\uDCD6]'
        },
        {
            name: 'Meroitic_Cursive',
            astral: '\uD802[\uDDA0-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDDFF]'
        },
        {
            name: 'Meroitic_Hieroglyphs',
            astral: '\uD802[\uDD80-\uDD9F]'
        },
        {
            name: 'Miao',
            astral: '\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F]'
        },
        {
            name: 'Modi',
            astral: '\uD805[\uDE00-\uDE44\uDE50-\uDE59]'
        },
        {
            name: 'Mongolian',
            bmp: '\u1800\u1801\u1804\u1806-\u180E\u1810-\u1819\u1820-\u1877\u1880-\u18AA',
            astral: '\uD805[\uDE60-\uDE6C]'
        },
        {
            name: 'Mro',
            astral: '\uD81A[\uDE40-\uDE5E\uDE60-\uDE69\uDE6E\uDE6F]'
        },
        {
            name: 'Multani',
            astral: '\uD804[\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA9]'
        },
        {
            name: 'Myanmar',
            bmp: '\u1000-\u109F\uA9E0-\uA9FE\uAA60-\uAA7F'
        },
        {
            name: 'Nabataean',
            astral: '\uD802[\uDC80-\uDC9E\uDCA7-\uDCAF]'
        },
        {
            name: 'New_Tai_Lue',
            bmp: '\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u19DE\u19DF'
        },
        {
            name: 'Newa',
            astral: '\uD805[\uDC00-\uDC59\uDC5B\uDC5D]'
        },
        {
            name: 'Nko',
            bmp: '\u07C0-\u07FA'
        },
        {
            name: 'Ogham',
            bmp: '\u1680-\u169C'
        },
        {
            name: 'Ol_Chiki',
            bmp: '\u1C50-\u1C7F'
        },
        {
            name: 'Old_Hungarian',
            astral: '\uD803[\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDCFF]'
        },
        {
            name: 'Old_Italic',
            astral: '\uD800[\uDF00-\uDF23]'
        },
        {
            name: 'Old_North_Arabian',
            astral: '\uD802[\uDE80-\uDE9F]'
        },
        {
            name: 'Old_Permic',
            astral: '\uD800[\uDF50-\uDF7A]'
        },
        {
            name: 'Old_Persian',
            astral: '\uD800[\uDFA0-\uDFC3\uDFC8-\uDFD5]'
        },
        {
            name: 'Old_South_Arabian',
            astral: '\uD802[\uDE60-\uDE7F]'
        },
        {
            name: 'Old_Turkic',
            astral: '\uD803[\uDC00-\uDC48]'
        },
        {
            name: 'Oriya',
            bmp: '\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B77'
        },
        {
            name: 'Osage',
            astral: '\uD801[\uDCB0-\uDCD3\uDCD8-\uDCFB]'
        },
        {
            name: 'Osmanya',
            astral: '\uD801[\uDC80-\uDC9D\uDCA0-\uDCA9]'
        },
        {
            name: 'Pahawh_Hmong',
            astral: '\uD81A[\uDF00-\uDF45\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]'
        },
        {
            name: 'Palmyrene',
            astral: '\uD802[\uDC60-\uDC7F]'
        },
        {
            name: 'Pau_Cin_Hau',
            astral: '\uD806[\uDEC0-\uDEF8]'
        },
        {
            name: 'Phags_Pa',
            bmp: '\uA840-\uA877'
        },
        {
            name: 'Phoenician',
            astral: '\uD802[\uDD00-\uDD1B\uDD1F]'
        },
        {
            name: 'Psalter_Pahlavi',
            astral: '\uD802[\uDF80-\uDF91\uDF99-\uDF9C\uDFA9-\uDFAF]'
        },
        {
            name: 'Rejang',
            bmp: '\uA930-\uA953\uA95F'
        },
        {
            name: 'Runic',
            bmp: '\u16A0-\u16EA\u16EE-\u16F8'
        },
        {
            name: 'Samaritan',
            bmp: '\u0800-\u082D\u0830-\u083E'
        },
        {
            name: 'Saurashtra',
            bmp: '\uA880-\uA8C5\uA8CE-\uA8D9'
        },
        {
            name: 'Sharada',
            astral: '\uD804[\uDD80-\uDDCD\uDDD0-\uDDDF]'
        },
        {
            name: 'Shavian',
            astral: '\uD801[\uDC50-\uDC7F]'
        },
        {
            name: 'Siddham',
            astral: '\uD805[\uDD80-\uDDB5\uDDB8-\uDDDD]'
        },
        {
            name: 'SignWriting',
            astral: '\uD836[\uDC00-\uDE8B\uDE9B-\uDE9F\uDEA1-\uDEAF]'
        },
        {
            name: 'Sinhala',
            bmp: '\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2-\u0DF4',
            astral: '\uD804[\uDDE1-\uDDF4]'
        },
        {
            name: 'Sora_Sompeng',
            astral: '\uD804[\uDCD0-\uDCE8\uDCF0-\uDCF9]'
        },
        {
            name: 'Sundanese',
            bmp: '\u1B80-\u1BBF\u1CC0-\u1CC7'
        },
        {
            name: 'Syloti_Nagri',
            bmp: '\uA800-\uA82B'
        },
        {
            name: 'Syriac',
            bmp: '\u0700-\u070D\u070F-\u074A\u074D-\u074F'
        },
        {
            name: 'Tagalog',
            bmp: '\u1700-\u170C\u170E-\u1714'
        },
        {
            name: 'Tagbanwa',
            bmp: '\u1760-\u176C\u176E-\u1770\u1772\u1773'
        },
        {
            name: 'Tai_Le',
            bmp: '\u1950-\u196D\u1970-\u1974'
        },
        {
            name: 'Tai_Tham',
            bmp: '\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA0-\u1AAD'
        },
        {
            name: 'Tai_Viet',
            bmp: '\uAA80-\uAAC2\uAADB-\uAADF'
        },
        {
            name: 'Takri',
            astral: '\uD805[\uDE80-\uDEB7\uDEC0-\uDEC9]'
        },
        {
            name: 'Tamil',
            bmp: '\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BFA'
        },
        {
            name: 'Tangut',
            astral: '\uD81B\uDFE0|[\uD81C-\uD820][\uDC00-\uDFFF]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]'
        },
        {
            name: 'Telugu',
            bmp: '\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C78-\u0C7F'
        },
        {
            name: 'Thaana',
            bmp: '\u0780-\u07B1'
        },
        {
            name: 'Thai',
            bmp: '\u0E01-\u0E3A\u0E40-\u0E5B'
        },
        {
            name: 'Tibetan',
            bmp: '\u0F00-\u0F47\u0F49-\u0F6C\u0F71-\u0F97\u0F99-\u0FBC\u0FBE-\u0FCC\u0FCE-\u0FD4\u0FD9\u0FDA'
        },
        {
            name: 'Tifinagh',
            bmp: '\u2D30-\u2D67\u2D6F\u2D70\u2D7F'
        },
        {
            name: 'Tirhuta',
            astral: '\uD805[\uDC80-\uDCC7\uDCD0-\uDCD9]'
        },
        {
            name: 'Ugaritic',
            astral: '\uD800[\uDF80-\uDF9D\uDF9F]'
        },
        {
            name: 'Vai',
            bmp: '\uA500-\uA62B'
        },
        {
            name: 'Warang_Citi',
            astral: '\uD806[\uDCA0-\uDCF2\uDCFF]'
        },
        {
            name: 'Yi',
            bmp: '\uA000-\uA48C\uA490-\uA4C6'
        }
    ]);

};

},{}],8:[function(require,module,exports){
var XRegExp = require('./xregexp');

require('./addons/build')(XRegExp);
require('./addons/matchrecursive')(XRegExp);
require('./addons/unicode-base')(XRegExp);
require('./addons/unicode-blocks')(XRegExp);
require('./addons/unicode-categories')(XRegExp);
require('./addons/unicode-properties')(XRegExp);
require('./addons/unicode-scripts')(XRegExp);

module.exports = XRegExp;

},{"./addons/build":1,"./addons/matchrecursive":2,"./addons/unicode-base":3,"./addons/unicode-blocks":4,"./addons/unicode-categories":5,"./addons/unicode-properties":6,"./addons/unicode-scripts":7,"./xregexp":9}],9:[function(require,module,exports){
/*!
 * XRegExp 3.2.0
 * <xregexp.com>
 * Steven Levithan (c) 2007-2017 MIT License
 */

'use strict';

/**
 * XRegExp provides augmented, extensible regular expressions. You get additional regex syntax and
 * flags, beyond what browsers support natively. XRegExp is also a regex utility belt with tools to
 * make your client-side grepping simpler and more powerful, while freeing you from related
 * cross-browser inconsistencies.
 */

// ==--------------------------==
// Private stuff
// ==--------------------------==

// Property name used for extended regex instance data
var REGEX_DATA = 'xregexp';
// Optional features that can be installed and uninstalled
var features = {
    astral: false,
    natives: false
};
// Native methods to use and restore ('native' is an ES3 reserved keyword)
var nativ = {
    exec: RegExp.prototype.exec,
    test: RegExp.prototype.test,
    match: String.prototype.match,
    replace: String.prototype.replace,
    split: String.prototype.split
};
// Storage for fixed/extended native methods
var fixed = {};
// Storage for regexes cached by `XRegExp.cache`
var regexCache = {};
// Storage for pattern details cached by the `XRegExp` constructor
var patternCache = {};
// Storage for regex syntax tokens added internally or by `XRegExp.addToken`
var tokens = [];
// Token scopes
var defaultScope = 'default';
var classScope = 'class';
// Regexes that match native regex syntax, including octals
var nativeTokens = {
    // Any native multicharacter token in default scope, or any single character
    'default': /\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9]\d*|x[\dA-Fa-f]{2}|u(?:[\dA-Fa-f]{4}|{[\dA-Fa-f]+})|c[A-Za-z]|[\s\S])|\(\?(?:[:=!]|<[=!])|[?*+]\?|{\d+(?:,\d*)?}\??|[\s\S]/,
    // Any native multicharacter token in character class scope, or any single character
    'class': /\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[\dA-Fa-f]{2}|u(?:[\dA-Fa-f]{4}|{[\dA-Fa-f]+})|c[A-Za-z]|[\s\S])|[\s\S]/
};
// Any backreference or dollar-prefixed character in replacement strings
var replacementToken = /\$(?:{([\w$]+)}|(\d\d?|[\s\S]))/g;
// Check for correct `exec` handling of nonparticipating capturing groups
var correctExecNpcg = nativ.exec.call(/()??/, '')[1] === undefined;
// Check for ES6 `flags` prop support
var hasFlagsProp = /x/.flags !== undefined;
// Shortcut to `Object.prototype.toString`
var toString = {}.toString;

function hasNativeFlag(flag) {
    // Can't check based on the presence of properties/getters since browsers might support such
    // properties even when they don't support the corresponding flag in regex construction (tested
    // in Chrome 48, where `'unicode' in /x/` is true but trying to construct a regex with flag `u`
    // throws an error)
    var isSupported = true;
    try {
        // Can't use regex literals for testing even in a `try` because regex literals with
        // unsupported flags cause a compilation error in IE
        new RegExp('', flag);
    } catch (exception) {
        isSupported = false;
    }
    return isSupported;
}
// Check for ES6 `u` flag support
var hasNativeU = hasNativeFlag('u');
// Check for ES6 `y` flag support
var hasNativeY = hasNativeFlag('y');
// Tracker for known flags, including addon flags
var registeredFlags = {
    g: true,
    i: true,
    m: true,
    u: hasNativeU,
    y: hasNativeY
};

/**
 * Attaches extended data and `XRegExp.prototype` properties to a regex object.
 *
 * @private
 * @param {RegExp} regex Regex to augment.
 * @param {Array} captureNames Array with capture names, or `null`.
 * @param {String} xSource XRegExp pattern used to generate `regex`, or `null` if N/A.
 * @param {String} xFlags XRegExp flags used to generate `regex`, or `null` if N/A.
 * @param {Boolean} [isInternalOnly=false] Whether the regex will be used only for internal
 *   operations, and never exposed to users. For internal-only regexes, we can improve perf by
 *   skipping some operations like attaching `XRegExp.prototype` properties.
 * @returns {RegExp} Augmented regex.
 */
function augment(regex, captureNames, xSource, xFlags, isInternalOnly) {
    var p;

    regex[REGEX_DATA] = {
        captureNames: captureNames
    };

    if (isInternalOnly) {
        return regex;
    }

    // Can't auto-inherit these since the XRegExp constructor returns a nonprimitive value
    if (regex.__proto__) {
        regex.__proto__ = XRegExp.prototype;
    } else {
        for (p in XRegExp.prototype) {
            // An `XRegExp.prototype.hasOwnProperty(p)` check wouldn't be worth it here, since this
            // is performance sensitive, and enumerable `Object.prototype` or `RegExp.prototype`
            // extensions exist on `regex.prototype` anyway
            regex[p] = XRegExp.prototype[p];
        }
    }

    regex[REGEX_DATA].source = xSource;
    // Emulate the ES6 `flags` prop by ensuring flags are in alphabetical order
    regex[REGEX_DATA].flags = xFlags ? xFlags.split('').sort().join('') : xFlags;

    return regex;
}

/**
 * Removes any duplicate characters from the provided string.
 *
 * @private
 * @param {String} str String to remove duplicate characters from.
 * @returns {String} String with any duplicate characters removed.
 */
function clipDuplicates(str) {
    return nativ.replace.call(str, /([\s\S])(?=[\s\S]*\1)/g, '');
}

/**
 * Copies a regex object while preserving extended data and augmenting with `XRegExp.prototype`
 * properties. The copy has a fresh `lastIndex` property (set to zero). Allows adding and removing
 * flags g and y while copying the regex.
 *
 * @private
 * @param {RegExp} regex Regex to copy.
 * @param {Object} [options] Options object with optional properties:
 *   - `addG` {Boolean} Add flag g while copying the regex.
 *   - `addY` {Boolean} Add flag y while copying the regex.
 *   - `removeG` {Boolean} Remove flag g while copying the regex.
 *   - `removeY` {Boolean} Remove flag y while copying the regex.
 *   - `isInternalOnly` {Boolean} Whether the copied regex will be used only for internal
 *     operations, and never exposed to users. For internal-only regexes, we can improve perf by
 *     skipping some operations like attaching `XRegExp.prototype` properties.
 *   - `source` {String} Overrides `<regex>.source`, for special cases.
 * @returns {RegExp} Copy of the provided regex, possibly with modified flags.
 */
function copyRegex(regex, options) {
    if (!XRegExp.isRegExp(regex)) {
        throw new TypeError('Type RegExp expected');
    }

    var xData = regex[REGEX_DATA] || {};
    var flags = getNativeFlags(regex);
    var flagsToAdd = '';
    var flagsToRemove = '';
    var xregexpSource = null;
    var xregexpFlags = null;

    options = options || {};

    if (options.removeG) {flagsToRemove += 'g';}
    if (options.removeY) {flagsToRemove += 'y';}
    if (flagsToRemove) {
        flags = nativ.replace.call(flags, new RegExp('[' + flagsToRemove + ']+', 'g'), '');
    }

    if (options.addG) {flagsToAdd += 'g';}
    if (options.addY) {flagsToAdd += 'y';}
    if (flagsToAdd) {
        flags = clipDuplicates(flags + flagsToAdd);
    }

    if (!options.isInternalOnly) {
        if (xData.source !== undefined) {
            xregexpSource = xData.source;
        }
        // null or undefined; don't want to add to `flags` if the previous value was null, since
        // that indicates we're not tracking original precompilation flags
        if (xData.flags != null) {
            // Flags are only added for non-internal regexes by `XRegExp.globalize`. Flags are never
            // removed for non-internal regexes, so don't need to handle it
            xregexpFlags = flagsToAdd ? clipDuplicates(xData.flags + flagsToAdd) : xData.flags;
        }
    }

    // Augment with `XRegExp.prototype` properties, but use the native `RegExp` constructor to avoid
    // searching for special tokens. That would be wrong for regexes constructed by `RegExp`, and
    // unnecessary for regexes constructed by `XRegExp` because the regex has already undergone the
    // translation to native regex syntax
    regex = augment(
        new RegExp(options.source || regex.source, flags),
        hasNamedCapture(regex) ? xData.captureNames.slice(0) : null,
        xregexpSource,
        xregexpFlags,
        options.isInternalOnly
    );

    return regex;
}

/**
 * Converts hexadecimal to decimal.
 *
 * @private
 * @param {String} hex
 * @returns {Number}
 */
function dec(hex) {
    return parseInt(hex, 16);
}

/**
 * Returns a pattern that can be used in a native RegExp in place of an ignorable token such as an
 * inline comment or whitespace with flag x. This is used directly as a token handler function
 * passed to `XRegExp.addToken`.
 *
 * @private
 * @param {String} match Match arg of `XRegExp.addToken` handler
 * @param {String} scope Scope arg of `XRegExp.addToken` handler
 * @param {String} flags Flags arg of `XRegExp.addToken` handler
 * @returns {String} Either '' or '(?:)', depending on which is needed in the context of the match.
 */
function getContextualTokenSeparator(match, scope, flags) {
    if (
        // No need to separate tokens if at the beginning or end of a group
        match.input.charAt(match.index - 1) === '(' ||
        match.input.charAt(match.index + match[0].length) === ')' ||
        // Avoid separating tokens when the following token is a quantifier
        isPatternNext(match.input, match.index + match[0].length, flags, '[?*+]|{\\d+(?:,\\d*)?}')
    ) {
        return '';
    }
    // Keep tokens separated. This avoids e.g. inadvertedly changing `\1 1` or `\1(?#)1` to `\11`.
    // This also ensures all tokens remain as discrete atoms, e.g. it avoids converting the syntax
    // error `(? :` into `(?:`.
    return '(?:)';
}

/**
 * Returns native `RegExp` flags used by a regex object.
 *
 * @private
 * @param {RegExp} regex Regex to check.
 * @returns {String} Native flags in use.
 */
function getNativeFlags(regex) {
    return hasFlagsProp ?
        regex.flags :
        // Explicitly using `RegExp.prototype.toString` (rather than e.g. `String` or concatenation
        // with an empty string) allows this to continue working predictably when
        // `XRegExp.proptotype.toString` is overridden
        nativ.exec.call(/\/([a-z]*)$/i, RegExp.prototype.toString.call(regex))[1];
}

/**
 * Determines whether a regex has extended instance data used to track capture names.
 *
 * @private
 * @param {RegExp} regex Regex to check.
 * @returns {Boolean} Whether the regex uses named capture.
 */
function hasNamedCapture(regex) {
    return !!(regex[REGEX_DATA] && regex[REGEX_DATA].captureNames);
}

/**
 * Converts decimal to hexadecimal.
 *
 * @private
 * @param {Number|String} dec
 * @returns {String}
 */
function hex(dec) {
    return parseInt(dec, 10).toString(16);
}

/**
 * Returns the first index at which a given value can be found in an array.
 *
 * @private
 * @param {Array} array Array to search.
 * @param {*} value Value to locate in the array.
 * @returns {Number} Zero-based index at which the item is found, or -1.
 */
function indexOf(array, value) {
    var len = array.length;
    var i;

    for (i = 0; i < len; ++i) {
        if (array[i] === value) {
            return i;
        }
    }

    return -1;
}

/**
 * Checks whether the next nonignorable token after the specified position matches the
 * `needlePattern`
 *
 * @private
 * @param {String} pattern Pattern to search within.
 * @param {Number} pos Index in `pattern` to search at.
 * @param {String} flags Flags used by the pattern.
 * @param {String} needlePattern Pattern to match the next token against.
 * @returns {Boolean} Whether the next nonignorable token matches `needlePattern`
 */
function isPatternNext(pattern, pos, flags, needlePattern) {
    var inlineCommentPattern = '\\(\\?#[^)]*\\)';
    var lineCommentPattern = '#[^#\\n]*';
    var patternsToIgnore = flags.indexOf('x') > -1 ?
        // Ignore any leading whitespace, line comments, and inline comments
        ['\\s', lineCommentPattern, inlineCommentPattern] :
        // Ignore any leading inline comments
        [inlineCommentPattern];
    return nativ.test.call(
        new RegExp('^(?:' + patternsToIgnore.join('|') + ')*(?:' + needlePattern + ')'),
        pattern.slice(pos)
    );
}

/**
 * Determines whether a value is of the specified type, by resolving its internal [[Class]].
 *
 * @private
 * @param {*} value Object to check.
 * @param {String} type Type to check for, in TitleCase.
 * @returns {Boolean} Whether the object matches the type.
 */
function isType(value, type) {
    return toString.call(value) === '[object ' + type + ']';
}

/**
 * Adds leading zeros if shorter than four characters. Used for fixed-length hexadecimal values.
 *
 * @private
 * @param {String} str
 * @returns {String}
 */
function pad4(str) {
    while (str.length < 4) {
        str = '0' + str;
    }
    return str;
}

/**
 * Checks for flag-related errors, and strips/applies flags in a leading mode modifier. Offloads
 * the flag preparation logic from the `XRegExp` constructor.
 *
 * @private
 * @param {String} pattern Regex pattern, possibly with a leading mode modifier.
 * @param {String} flags Any combination of flags.
 * @returns {Object} Object with properties `pattern` and `flags`.
 */
function prepareFlags(pattern, flags) {
    var i;

    // Recent browsers throw on duplicate flags, so copy this behavior for nonnative flags
    if (clipDuplicates(flags) !== flags) {
        throw new SyntaxError('Invalid duplicate regex flag ' + flags);
    }

    // Strip and apply a leading mode modifier with any combination of flags except g or y
    pattern = nativ.replace.call(pattern, /^\(\?([\w$]+)\)/, function($0, $1) {
        if (nativ.test.call(/[gy]/, $1)) {
            throw new SyntaxError('Cannot use flag g or y in mode modifier ' + $0);
        }
        // Allow duplicate flags within the mode modifier
        flags = clipDuplicates(flags + $1);
        return '';
    });

    // Throw on unknown native or nonnative flags
    for (i = 0; i < flags.length; ++i) {
        if (!registeredFlags[flags.charAt(i)]) {
            throw new SyntaxError('Unknown regex flag ' + flags.charAt(i));
        }
    }

    return {
        pattern: pattern,
        flags: flags
    };
}

/**
 * Prepares an options object from the given value.
 *
 * @private
 * @param {String|Object} value Value to convert to an options object.
 * @returns {Object} Options object.
 */
function prepareOptions(value) {
    var options = {};

    if (isType(value, 'String')) {
        XRegExp.forEach(value, /[^\s,]+/, function(match) {
            options[match] = true;
        });

        return options;
    }

    return value;
}

/**
 * Registers a flag so it doesn't throw an 'unknown flag' error.
 *
 * @private
 * @param {String} flag Single-character flag to register.
 */
function registerFlag(flag) {
    if (!/^[\w$]$/.test(flag)) {
        throw new Error('Flag must be a single character A-Za-z0-9_$');
    }

    registeredFlags[flag] = true;
}

/**
 * Runs built-in and custom regex syntax tokens in reverse insertion order at the specified
 * position, until a match is found.
 *
 * @private
 * @param {String} pattern Original pattern from which an XRegExp object is being built.
 * @param {String} flags Flags being used to construct the regex.
 * @param {Number} pos Position to search for tokens within `pattern`.
 * @param {Number} scope Regex scope to apply: 'default' or 'class'.
 * @param {Object} context Context object to use for token handler functions.
 * @returns {Object} Object with properties `matchLength`, `output`, and `reparse`; or `null`.
 */
function runTokens(pattern, flags, pos, scope, context) {
    var i = tokens.length;
    var leadChar = pattern.charAt(pos);
    var result = null;
    var match;
    var t;

    // Run in reverse insertion order
    while (i--) {
        t = tokens[i];
        if (
            (t.leadChar && t.leadChar !== leadChar) ||
            (t.scope !== scope && t.scope !== 'all') ||
            (t.flag && flags.indexOf(t.flag) === -1)
        ) {
            continue;
        }

        match = XRegExp.exec(pattern, t.regex, pos, 'sticky');
        if (match) {
            result = {
                matchLength: match[0].length,
                output: t.handler.call(context, match, scope, flags),
                reparse: t.reparse
            };
            // Finished with token tests
            break;
        }
    }

    return result;
}

/**
 * Enables or disables implicit astral mode opt-in. When enabled, flag A is automatically added to
 * all new regexes created by XRegExp. This causes an error to be thrown when creating regexes if
 * the Unicode Base addon is not available, since flag A is registered by that addon.
 *
 * @private
 * @param {Boolean} on `true` to enable; `false` to disable.
 */
function setAstral(on) {
    features.astral = on;
}

/**
 * Enables or disables native method overrides.
 *
 * @private
 * @param {Boolean} on `true` to enable; `false` to disable.
 */
function setNatives(on) {
    RegExp.prototype.exec = (on ? fixed : nativ).exec;
    RegExp.prototype.test = (on ? fixed : nativ).test;
    String.prototype.match = (on ? fixed : nativ).match;
    String.prototype.replace = (on ? fixed : nativ).replace;
    String.prototype.split = (on ? fixed : nativ).split;

    features.natives = on;
}

/**
 * Returns the object, or throws an error if it is `null` or `undefined`. This is used to follow
 * the ES5 abstract operation `ToObject`.
 *
 * @private
 * @param {*} value Object to check and return.
 * @returns {*} The provided object.
 */
function toObject(value) {
    // null or undefined
    if (value == null) {
        throw new TypeError('Cannot convert null or undefined to object');
    }

    return value;
}

// ==--------------------------==
// Constructor
// ==--------------------------==

/**
 * Creates an extended regular expression object for matching text with a pattern. Differs from a
 * native regular expression in that additional syntax and flags are supported. The returned object
 * is in fact a native `RegExp` and works with all native methods.
 *
 * @class XRegExp
 * @constructor
 * @param {String|RegExp} pattern Regex pattern string, or an existing regex object to copy.
 * @param {String} [flags] Any combination of flags.
 *   Native flags:
 *     - `g` - global
 *     - `i` - ignore case
 *     - `m` - multiline anchors
 *     - `u` - unicode (ES6)
 *     - `y` - sticky (Firefox 3+, ES6)
 *   Additional XRegExp flags:
 *     - `n` - explicit capture
 *     - `s` - dot matches all (aka singleline)
 *     - `x` - free-spacing and line comments (aka extended)
 *     - `A` - astral (requires the Unicode Base addon)
 *   Flags cannot be provided when constructing one `RegExp` from another.
 * @returns {RegExp} Extended regular expression object.
 * @example
 *
 * // With named capture and flag x
 * XRegExp('(?<year>  [0-9]{4} ) -?  # year  \n\
 *          (?<month> [0-9]{2} ) -?  # month \n\
 *          (?<day>   [0-9]{2} )     # day   ', 'x');
 *
 * // Providing a regex object copies it. Native regexes are recompiled using native (not XRegExp)
 * // syntax. Copies maintain extended data, are augmented with `XRegExp.prototype` properties, and
 * // have fresh `lastIndex` properties (set to zero).
 * XRegExp(/regex/);
 */
function XRegExp(pattern, flags) {
    if (XRegExp.isRegExp(pattern)) {
        if (flags !== undefined) {
            throw new TypeError('Cannot supply flags when copying a RegExp');
        }
        return copyRegex(pattern);
    }

    // Copy the argument behavior of `RegExp`
    pattern = pattern === undefined ? '' : String(pattern);
    flags = flags === undefined ? '' : String(flags);

    if (XRegExp.isInstalled('astral') && flags.indexOf('A') === -1) {
        // This causes an error to be thrown if the Unicode Base addon is not available
        flags += 'A';
    }

    if (!patternCache[pattern]) {
        patternCache[pattern] = {};
    }

    if (!patternCache[pattern][flags]) {
        var context = {
            hasNamedCapture: false,
            captureNames: []
        };
        var scope = defaultScope;
        var output = '';
        var pos = 0;
        var result;

        // Check for flag-related errors, and strip/apply flags in a leading mode modifier
        var applied = prepareFlags(pattern, flags);
        var appliedPattern = applied.pattern;
        var appliedFlags = applied.flags;

        // Use XRegExp's tokens to translate the pattern to a native regex pattern.
        // `appliedPattern.length` may change on each iteration if tokens use `reparse`
        while (pos < appliedPattern.length) {
            do {
                // Check for custom tokens at the current position
                result = runTokens(appliedPattern, appliedFlags, pos, scope, context);
                // If the matched token used the `reparse` option, splice its output into the
                // pattern before running tokens again at the same position
                if (result && result.reparse) {
                    appliedPattern = appliedPattern.slice(0, pos) +
                        result.output +
                        appliedPattern.slice(pos + result.matchLength);
                }
            } while (result && result.reparse);

            if (result) {
                output += result.output;
                pos += (result.matchLength || 1);
            } else {
                // Get the native token at the current position
                var token = XRegExp.exec(appliedPattern, nativeTokens[scope], pos, 'sticky')[0];
                output += token;
                pos += token.length;
                if (token === '[' && scope === defaultScope) {
                    scope = classScope;
                } else if (token === ']' && scope === classScope) {
                    scope = defaultScope;
                }
            }
        }

        patternCache[pattern][flags] = {
            // Use basic cleanup to collapse repeated empty groups like `(?:)(?:)` to `(?:)`. Empty
            // groups are sometimes inserted during regex transpilation in order to keep tokens
            // separated. However, more than one empty group in a row is never needed.
            pattern: nativ.replace.call(output, /(?:\(\?:\))+/g, '(?:)'),
            // Strip all but native flags
            flags: nativ.replace.call(appliedFlags, /[^gimuy]+/g, ''),
            // `context.captureNames` has an item for each capturing group, even if unnamed
            captures: context.hasNamedCapture ? context.captureNames : null
        };
    }

    var generated = patternCache[pattern][flags];
    return augment(
        new RegExp(generated.pattern, generated.flags),
        generated.captures,
        pattern,
        flags
    );
}

// Add `RegExp.prototype` to the prototype chain
XRegExp.prototype = new RegExp();

// ==--------------------------==
// Public properties
// ==--------------------------==

/**
 * The XRegExp version number as a string containing three dot-separated parts. For example,
 * '2.0.0-beta-3'.
 *
 * @static
 * @memberOf XRegExp
 * @type String
 */
XRegExp.version = '3.2.0';

// ==--------------------------==
// Public methods
// ==--------------------------==

// Intentionally undocumented; used in tests and addons
XRegExp._clipDuplicates = clipDuplicates;
XRegExp._hasNativeFlag = hasNativeFlag;
XRegExp._dec = dec;
XRegExp._hex = hex;
XRegExp._pad4 = pad4;

/**
 * Extends XRegExp syntax and allows custom flags. This is used internally and can be used to
 * create XRegExp addons. If more than one token can match the same string, the last added wins.
 *
 * @memberOf XRegExp
 * @param {RegExp} regex Regex object that matches the new token.
 * @param {Function} handler Function that returns a new pattern string (using native regex syntax)
 *   to replace the matched token within all future XRegExp regexes. Has access to persistent
 *   properties of the regex being built, through `this`. Invoked with three arguments:
 *   - The match array, with named backreference properties.
 *   - The regex scope where the match was found: 'default' or 'class'.
 *   - The flags used by the regex, including any flags in a leading mode modifier.
 *   The handler function becomes part of the XRegExp construction process, so be careful not to
 *   construct XRegExps within the function or you will trigger infinite recursion.
 * @param {Object} [options] Options object with optional properties:
 *   - `scope` {String} Scope where the token applies: 'default', 'class', or 'all'.
 *   - `flag` {String} Single-character flag that triggers the token. This also registers the
 *     flag, which prevents XRegExp from throwing an 'unknown flag' error when the flag is used.
 *   - `optionalFlags` {String} Any custom flags checked for within the token `handler` that are
 *     not required to trigger the token. This registers the flags, to prevent XRegExp from
 *     throwing an 'unknown flag' error when any of the flags are used.
 *   - `reparse` {Boolean} Whether the `handler` function's output should not be treated as
 *     final, and instead be reparseable by other tokens (including the current token). Allows
 *     token chaining or deferring.
 *   - `leadChar` {String} Single character that occurs at the beginning of any successful match
 *     of the token (not always applicable). This doesn't change the behavior of the token unless
 *     you provide an erroneous value. However, providing it can increase the token's performance
 *     since the token can be skipped at any positions where this character doesn't appear.
 * @example
 *
 * // Basic usage: Add \a for the ALERT control code
 * XRegExp.addToken(
 *   /\\a/,
 *   function() {return '\\x07';},
 *   {scope: 'all'}
 * );
 * XRegExp('\\a[\\a-\\n]+').test('\x07\n\x07'); // -> true
 *
 * // Add the U (ungreedy) flag from PCRE and RE2, which reverses greedy and lazy quantifiers.
 * // Since `scope` is not specified, it uses 'default' (i.e., transformations apply outside of
 * // character classes only)
 * XRegExp.addToken(
 *   /([?*+]|{\d+(?:,\d*)?})(\??)/,
 *   function(match) {return match[1] + (match[2] ? '' : '?');},
 *   {flag: 'U'}
 * );
 * XRegExp('a+', 'U').exec('aaa')[0]; // -> 'a'
 * XRegExp('a+?', 'U').exec('aaa')[0]; // -> 'aaa'
 */
XRegExp.addToken = function(regex, handler, options) {
    options = options || {};
    var optionalFlags = options.optionalFlags;
    var i;

    if (options.flag) {
        registerFlag(options.flag);
    }

    if (optionalFlags) {
        optionalFlags = nativ.split.call(optionalFlags, '');
        for (i = 0; i < optionalFlags.length; ++i) {
            registerFlag(optionalFlags[i]);
        }
    }

    // Add to the private list of syntax tokens
    tokens.push({
        regex: copyRegex(regex, {
            addG: true,
            addY: hasNativeY,
            isInternalOnly: true
        }),
        handler: handler,
        scope: options.scope || defaultScope,
        flag: options.flag,
        reparse: options.reparse,
        leadChar: options.leadChar
    });

    // Reset the pattern cache used by the `XRegExp` constructor, since the same pattern and flags
    // might now produce different results
    XRegExp.cache.flush('patterns');
};

/**
 * Caches and returns the result of calling `XRegExp(pattern, flags)`. On any subsequent call with
 * the same pattern and flag combination, the cached copy of the regex is returned.
 *
 * @memberOf XRegExp
 * @param {String} pattern Regex pattern string.
 * @param {String} [flags] Any combination of XRegExp flags.
 * @returns {RegExp} Cached XRegExp object.
 * @example
 *
 * while (match = XRegExp.cache('.', 'gs').exec(str)) {
 *   // The regex is compiled once only
 * }
 */
XRegExp.cache = function(pattern, flags) {
    if (!regexCache[pattern]) {
        regexCache[pattern] = {};
    }
    return regexCache[pattern][flags] || (
        regexCache[pattern][flags] = XRegExp(pattern, flags)
    );
};

// Intentionally undocumented; used in tests
XRegExp.cache.flush = function(cacheName) {
    if (cacheName === 'patterns') {
        // Flush the pattern cache used by the `XRegExp` constructor
        patternCache = {};
    } else {
        // Flush the regex cache populated by `XRegExp.cache`
        regexCache = {};
    }
};

/**
 * Escapes any regular expression metacharacters, for use when matching literal strings. The result
 * can safely be used at any point within a regex that uses any flags.
 *
 * @memberOf XRegExp
 * @param {String} str String to escape.
 * @returns {String} String with regex metacharacters escaped.
 * @example
 *
 * XRegExp.escape('Escaped? <.>');
 * // -> 'Escaped\?\ <\.>'
 */
XRegExp.escape = function(str) {
    return nativ.replace.call(toObject(str), /[-\[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
};

/**
 * Executes a regex search in a specified string. Returns a match array or `null`. If the provided
 * regex uses named capture, named backreference properties are included on the match array.
 * Optional `pos` and `sticky` arguments specify the search start position, and whether the match
 * must start at the specified position only. The `lastIndex` property of the provided regex is not
 * used, but is updated for compatibility. Also fixes browser bugs compared to the native
 * `RegExp.prototype.exec` and can be used reliably cross-browser.
 *
 * @memberOf XRegExp
 * @param {String} str String to search.
 * @param {RegExp} regex Regex to search with.
 * @param {Number} [pos=0] Zero-based index at which to start the search.
 * @param {Boolean|String} [sticky=false] Whether the match must start at the specified position
 *   only. The string `'sticky'` is accepted as an alternative to `true`.
 * @returns {Array} Match array with named backreference properties, or `null`.
 * @example
 *
 * // Basic use, with named backreference
 * var match = XRegExp.exec('U+2620', XRegExp('U\\+(?<hex>[0-9A-F]{4})'));
 * match.hex; // -> '2620'
 *
 * // With pos and sticky, in a loop
 * var pos = 2, result = [], match;
 * while (match = XRegExp.exec('<1><2><3><4>5<6>', /<(\d)>/, pos, 'sticky')) {
 *   result.push(match[1]);
 *   pos = match.index + match[0].length;
 * }
 * // result -> ['2', '3', '4']
 */
XRegExp.exec = function(str, regex, pos, sticky) {
    var cacheKey = 'g';
    var addY = false;
    var fakeY = false;
    var match;
    var r2;

    addY = hasNativeY && !!(sticky || (regex.sticky && sticky !== false));
    if (addY) {
        cacheKey += 'y';
    } else if (sticky) {
        // Simulate sticky matching by appending an empty capture to the original regex. The
        // resulting regex will succeed no matter what at the current index (set with `lastIndex`),
        // and will not search the rest of the subject string. We'll know that the original regex
        // has failed if that last capture is `''` rather than `undefined` (i.e., if that last
        // capture participated in the match).
        fakeY = true;
        cacheKey += 'FakeY';
    }

    regex[REGEX_DATA] = regex[REGEX_DATA] || {};

    // Shares cached copies with `XRegExp.match`/`replace`
    r2 = regex[REGEX_DATA][cacheKey] || (
        regex[REGEX_DATA][cacheKey] = copyRegex(regex, {
            addG: true,
            addY: addY,
            source: fakeY ? regex.source + '|()' : undefined,
            removeY: sticky === false,
            isInternalOnly: true
        })
    );

    pos = pos || 0;
    r2.lastIndex = pos;

    // Fixed `exec` required for `lastIndex` fix, named backreferences, etc.
    match = fixed.exec.call(r2, str);

    // Get rid of the capture added by the pseudo-sticky matcher if needed. An empty string means
    // the original regexp failed (see above).
    if (fakeY && match && match.pop() === '') {
        match = null;
    }

    if (regex.global) {
        regex.lastIndex = match ? r2.lastIndex : 0;
    }

    return match;
};

/**
 * Executes a provided function once per regex match. Searches always start at the beginning of the
 * string and continue until the end, regardless of the state of the regex's `global` property and
 * initial `lastIndex`.
 *
 * @memberOf XRegExp
 * @param {String} str String to search.
 * @param {RegExp} regex Regex to search with.
 * @param {Function} callback Function to execute for each match. Invoked with four arguments:
 *   - The match array, with named backreference properties.
 *   - The zero-based match index.
 *   - The string being traversed.
 *   - The regex object being used to traverse the string.
 * @example
 *
 * // Extracts every other digit from a string
 * var evens = [];
 * XRegExp.forEach('1a2345', /\d/, function(match, i) {
 *   if (i % 2) evens.push(+match[0]);
 * });
 * // evens -> [2, 4]
 */
XRegExp.forEach = function(str, regex, callback) {
    var pos = 0;
    var i = -1;
    var match;

    while ((match = XRegExp.exec(str, regex, pos))) {
        // Because `regex` is provided to `callback`, the function could use the deprecated/
        // nonstandard `RegExp.prototype.compile` to mutate the regex. However, since `XRegExp.exec`
        // doesn't use `lastIndex` to set the search position, this can't lead to an infinite loop,
        // at least. Actually, because of the way `XRegExp.exec` caches globalized versions of
        // regexes, mutating the regex will not have any effect on the iteration or matched strings,
        // which is a nice side effect that brings extra safety.
        callback(match, ++i, str, regex);

        pos = match.index + (match[0].length || 1);
    }
};

/**
 * Copies a regex object and adds flag `g`. The copy maintains extended data, is augmented with
 * `XRegExp.prototype` properties, and has a fresh `lastIndex` property (set to zero). Native
 * regexes are not recompiled using XRegExp syntax.
 *
 * @memberOf XRegExp
 * @param {RegExp} regex Regex to globalize.
 * @returns {RegExp} Copy of the provided regex with flag `g` added.
 * @example
 *
 * var globalCopy = XRegExp.globalize(/regex/);
 * globalCopy.global; // -> true
 */
XRegExp.globalize = function(regex) {
    return copyRegex(regex, {addG: true});
};

/**
 * Installs optional features according to the specified options. Can be undone using
 * `XRegExp.uninstall`.
 *
 * @memberOf XRegExp
 * @param {Object|String} options Options object or string.
 * @example
 *
 * // With an options object
 * XRegExp.install({
 *   // Enables support for astral code points in Unicode addons (implicitly sets flag A)
 *   astral: true,
 *
 *   // DEPRECATED: Overrides native regex methods with fixed/extended versions
 *   natives: true
 * });
 *
 * // With an options string
 * XRegExp.install('astral natives');
 */
XRegExp.install = function(options) {
    options = prepareOptions(options);

    if (!features.astral && options.astral) {
        setAstral(true);
    }

    if (!features.natives && options.natives) {
        setNatives(true);
    }
};

/**
 * Checks whether an individual optional feature is installed.
 *
 * @memberOf XRegExp
 * @param {String} feature Name of the feature to check. One of:
 *   - `astral`
 *   - `natives`
 * @returns {Boolean} Whether the feature is installed.
 * @example
 *
 * XRegExp.isInstalled('astral');
 */
XRegExp.isInstalled = function(feature) {
    return !!(features[feature]);
};

/**
 * Returns `true` if an object is a regex; `false` if it isn't. This works correctly for regexes
 * created in another frame, when `instanceof` and `constructor` checks would fail.
 *
 * @memberOf XRegExp
 * @param {*} value Object to check.
 * @returns {Boolean} Whether the object is a `RegExp` object.
 * @example
 *
 * XRegExp.isRegExp('string'); // -> false
 * XRegExp.isRegExp(/regex/i); // -> true
 * XRegExp.isRegExp(RegExp('^', 'm')); // -> true
 * XRegExp.isRegExp(XRegExp('(?s).')); // -> true
 */
XRegExp.isRegExp = function(value) {
    return toString.call(value) === '[object RegExp]';
    //return isType(value, 'RegExp');
};

/**
 * Returns the first matched string, or in global mode, an array containing all matched strings.
 * This is essentially a more convenient re-implementation of `String.prototype.match` that gives
 * the result types you actually want (string instead of `exec`-style array in match-first mode,
 * and an empty array instead of `null` when no matches are found in match-all mode). It also lets
 * you override flag g and ignore `lastIndex`, and fixes browser bugs.
 *
 * @memberOf XRegExp
 * @param {String} str String to search.
 * @param {RegExp} regex Regex to search with.
 * @param {String} [scope='one'] Use 'one' to return the first match as a string. Use 'all' to
 *   return an array of all matched strings. If not explicitly specified and `regex` uses flag g,
 *   `scope` is 'all'.
 * @returns {String|Array} In match-first mode: First match as a string, or `null`. In match-all
 *   mode: Array of all matched strings, or an empty array.
 * @example
 *
 * // Match first
 * XRegExp.match('abc', /\w/); // -> 'a'
 * XRegExp.match('abc', /\w/g, 'one'); // -> 'a'
 * XRegExp.match('abc', /x/g, 'one'); // -> null
 *
 * // Match all
 * XRegExp.match('abc', /\w/g); // -> ['a', 'b', 'c']
 * XRegExp.match('abc', /\w/, 'all'); // -> ['a', 'b', 'c']
 * XRegExp.match('abc', /x/, 'all'); // -> []
 */
XRegExp.match = function(str, regex, scope) {
    var global = (regex.global && scope !== 'one') || scope === 'all';
    var cacheKey = ((global ? 'g' : '') + (regex.sticky ? 'y' : '')) || 'noGY';
    var result;
    var r2;

    regex[REGEX_DATA] = regex[REGEX_DATA] || {};

    // Shares cached copies with `XRegExp.exec`/`replace`
    r2 = regex[REGEX_DATA][cacheKey] || (
        regex[REGEX_DATA][cacheKey] = copyRegex(regex, {
            addG: !!global,
            removeG: scope === 'one',
            isInternalOnly: true
        })
    );

    result = nativ.match.call(toObject(str), r2);

    if (regex.global) {
        regex.lastIndex = (
            (scope === 'one' && result) ?
                // Can't use `r2.lastIndex` since `r2` is nonglobal in this case
                (result.index + result[0].length) : 0
        );
    }

    return global ? (result || []) : (result && result[0]);
};

/**
 * Retrieves the matches from searching a string using a chain of regexes that successively search
 * within previous matches. The provided `chain` array can contain regexes and or objects with
 * `regex` and `backref` properties. When a backreference is specified, the named or numbered
 * backreference is passed forward to the next regex or returned.
 *
 * @memberOf XRegExp
 * @param {String} str String to search.
 * @param {Array} chain Regexes that each search for matches within preceding results.
 * @returns {Array} Matches by the last regex in the chain, or an empty array.
 * @example
 *
 * // Basic usage; matches numbers within <b> tags
 * XRegExp.matchChain('1 <b>2</b> 3 <b>4 a 56</b>', [
 *   XRegExp('(?is)<b>.*?</b>'),
 *   /\d+/
 * ]);
 * // -> ['2', '4', '56']
 *
 * // Passing forward and returning specific backreferences
 * html = '<a href="http://xregexp.com/api/">XRegExp</a>\
 *         <a href="http://www.google.com/">Google</a>';
 * XRegExp.matchChain(html, [
 *   {regex: /<a href="([^"]+)">/i, backref: 1},
 *   {regex: XRegExp('(?i)^https?://(?<domain>[^/?#]+)'), backref: 'domain'}
 * ]);
 * // -> ['xregexp.com', 'www.google.com']
 */
XRegExp.matchChain = function(str, chain) {
    return (function recurseChain(values, level) {
        var item = chain[level].regex ? chain[level] : {regex: chain[level]};
        var matches = [];

        function addMatch(match) {
            if (item.backref) {
                // Safari 4.0.5 (but not 5.0.5+) inappropriately uses sparse arrays to hold the
                // `undefined`s for backreferences to nonparticipating capturing groups. In such
                // cases, a `hasOwnProperty` or `in` check on its own would inappropriately throw
                // the exception, so also check if the backreference is a number that is within the
                // bounds of the array.
                if (!(match.hasOwnProperty(item.backref) || +item.backref < match.length)) {
                    throw new ReferenceError('Backreference to undefined group: ' + item.backref);
                }

                matches.push(match[item.backref] || '');
            } else {
                matches.push(match[0]);
            }
        }

        for (var i = 0; i < values.length; ++i) {
            XRegExp.forEach(values[i], item.regex, addMatch);
        }

        return ((level === chain.length - 1) || !matches.length) ?
            matches :
            recurseChain(matches, level + 1);
    }([str], 0));
};

/**
 * Returns a new string with one or all matches of a pattern replaced. The pattern can be a string
 * or regex, and the replacement can be a string or a function to be called for each match. To
 * perform a global search and replace, use the optional `scope` argument or include flag g if using
 * a regex. Replacement strings can use `${n}` for named and numbered backreferences. Replacement
 * functions can use named backreferences via `arguments[0].name`. Also fixes browser bugs compared
 * to the native `String.prototype.replace` and can be used reliably cross-browser.
 *
 * @memberOf XRegExp
 * @param {String} str String to search.
 * @param {RegExp|String} search Search pattern to be replaced.
 * @param {String|Function} replacement Replacement string or a function invoked to create it.
 *   Replacement strings can include special replacement syntax:
 *     - $$ - Inserts a literal $ character.
 *     - $&, $0 - Inserts the matched substring.
 *     - $` - Inserts the string that precedes the matched substring (left context).
 *     - $' - Inserts the string that follows the matched substring (right context).
 *     - $n, $nn - Where n/nn are digits referencing an existent capturing group, inserts
 *       backreference n/nn.
 *     - ${n} - Where n is a name or any number of digits that reference an existent capturing
 *       group, inserts backreference n.
 *   Replacement functions are invoked with three or more arguments:
 *     - The matched substring (corresponds to $& above). Named backreferences are accessible as
 *       properties of this first argument.
 *     - 0..n arguments, one for each backreference (corresponding to $1, $2, etc. above).
 *     - The zero-based index of the match within the total search string.
 *     - The total string being searched.
 * @param {String} [scope='one'] Use 'one' to replace the first match only, or 'all'. If not
 *   explicitly specified and using a regex with flag g, `scope` is 'all'.
 * @returns {String} New string with one or all matches replaced.
 * @example
 *
 * // Regex search, using named backreferences in replacement string
 * var name = XRegExp('(?<first>\\w+) (?<last>\\w+)');
 * XRegExp.replace('John Smith', name, '${last}, ${first}');
 * // -> 'Smith, John'
 *
 * // Regex search, using named backreferences in replacement function
 * XRegExp.replace('John Smith', name, function(match) {
 *   return match.last + ', ' + match.first;
 * });
 * // -> 'Smith, John'
 *
 * // String search, with replace-all
 * XRegExp.replace('RegExp builds RegExps', 'RegExp', 'XRegExp', 'all');
 * // -> 'XRegExp builds XRegExps'
 */
XRegExp.replace = function(str, search, replacement, scope) {
    var isRegex = XRegExp.isRegExp(search);
    var global = (search.global && scope !== 'one') || scope === 'all';
    var cacheKey = ((global ? 'g' : '') + (search.sticky ? 'y' : '')) || 'noGY';
    var s2 = search;
    var result;

    if (isRegex) {
        search[REGEX_DATA] = search[REGEX_DATA] || {};

        // Shares cached copies with `XRegExp.exec`/`match`. Since a copy is used, `search`'s
        // `lastIndex` isn't updated *during* replacement iterations
        s2 = search[REGEX_DATA][cacheKey] || (
            search[REGEX_DATA][cacheKey] = copyRegex(search, {
                addG: !!global,
                removeG: scope === 'one',
                isInternalOnly: true
            })
        );
    } else if (global) {
        s2 = new RegExp(XRegExp.escape(String(search)), 'g');
    }

    // Fixed `replace` required for named backreferences, etc.
    result = fixed.replace.call(toObject(str), s2, replacement);

    if (isRegex && search.global) {
        // Fixes IE, Safari bug (last tested IE 9, Safari 5.1)
        search.lastIndex = 0;
    }

    return result;
};

/**
 * Performs batch processing of string replacements. Used like `XRegExp.replace`, but accepts an
 * array of replacement details. Later replacements operate on the output of earlier replacements.
 * Replacement details are accepted as an array with a regex or string to search for, the
 * replacement string or function, and an optional scope of 'one' or 'all'. Uses the XRegExp
 * replacement text syntax, which supports named backreference properties via `${name}`.
 *
 * @memberOf XRegExp
 * @param {String} str String to search.
 * @param {Array} replacements Array of replacement detail arrays.
 * @returns {String} New string with all replacements.
 * @example
 *
 * str = XRegExp.replaceEach(str, [
 *   [XRegExp('(?<name>a)'), 'z${name}'],
 *   [/b/gi, 'y'],
 *   [/c/g, 'x', 'one'], // scope 'one' overrides /g
 *   [/d/, 'w', 'all'],  // scope 'all' overrides lack of /g
 *   ['e', 'v', 'all'],  // scope 'all' allows replace-all for strings
 *   [/f/g, function($0) {
 *     return $0.toUpperCase();
 *   }]
 * ]);
 */
XRegExp.replaceEach = function(str, replacements) {
    var i;
    var r;

    for (i = 0; i < replacements.length; ++i) {
        r = replacements[i];
        str = XRegExp.replace(str, r[0], r[1], r[2]);
    }

    return str;
};

/**
 * Splits a string into an array of strings using a regex or string separator. Matches of the
 * separator are not included in the result array. However, if `separator` is a regex that contains
 * capturing groups, backreferences are spliced into the result each time `separator` is matched.
 * Fixes browser bugs compared to the native `String.prototype.split` and can be used reliably
 * cross-browser.
 *
 * @memberOf XRegExp
 * @param {String} str String to split.
 * @param {RegExp|String} separator Regex or string to use for separating the string.
 * @param {Number} [limit] Maximum number of items to include in the result array.
 * @returns {Array} Array of substrings.
 * @example
 *
 * // Basic use
 * XRegExp.split('a b c', ' ');
 * // -> ['a', 'b', 'c']
 *
 * // With limit
 * XRegExp.split('a b c', ' ', 2);
 * // -> ['a', 'b']
 *
 * // Backreferences in result array
 * XRegExp.split('..word1..', /([a-z]+)(\d+)/i);
 * // -> ['..', 'word', '1', '..']
 */
XRegExp.split = function(str, separator, limit) {
    return fixed.split.call(toObject(str), separator, limit);
};

/**
 * Executes a regex search in a specified string. Returns `true` or `false`. Optional `pos` and
 * `sticky` arguments specify the search start position, and whether the match must start at the
 * specified position only. The `lastIndex` property of the provided regex is not used, but is
 * updated for compatibility. Also fixes browser bugs compared to the native
 * `RegExp.prototype.test` and can be used reliably cross-browser.
 *
 * @memberOf XRegExp
 * @param {String} str String to search.
 * @param {RegExp} regex Regex to search with.
 * @param {Number} [pos=0] Zero-based index at which to start the search.
 * @param {Boolean|String} [sticky=false] Whether the match must start at the specified position
 *   only. The string `'sticky'` is accepted as an alternative to `true`.
 * @returns {Boolean} Whether the regex matched the provided value.
 * @example
 *
 * // Basic use
 * XRegExp.test('abc', /c/); // -> true
 *
 * // With pos and sticky
 * XRegExp.test('abc', /c/, 0, 'sticky'); // -> false
 * XRegExp.test('abc', /c/, 2, 'sticky'); // -> true
 */
XRegExp.test = function(str, regex, pos, sticky) {
    // Do this the easy way :-)
    return !!XRegExp.exec(str, regex, pos, sticky);
};

/**
 * Uninstalls optional features according to the specified options. All optional features start out
 * uninstalled, so this is used to undo the actions of `XRegExp.install`.
 *
 * @memberOf XRegExp
 * @param {Object|String} options Options object or string.
 * @example
 *
 * // With an options object
 * XRegExp.uninstall({
 *   // Disables support for astral code points in Unicode addons
 *   astral: true,
 *
 *   // DEPRECATED: Restores native regex methods
 *   natives: true
 * });
 *
 * // With an options string
 * XRegExp.uninstall('astral natives');
 */
XRegExp.uninstall = function(options) {
    options = prepareOptions(options);

    if (features.astral && options.astral) {
        setAstral(false);
    }

    if (features.natives && options.natives) {
        setNatives(false);
    }
};

/**
 * Returns an XRegExp object that is the union of the given patterns. Patterns can be provided as
 * regex objects or strings. Metacharacters are escaped in patterns provided as strings.
 * Backreferences in provided regex objects are automatically renumbered to work correctly within
 * the larger combined pattern. Native flags used by provided regexes are ignored in favor of the
 * `flags` argument.
 *
 * @memberOf XRegExp
 * @param {Array} patterns Regexes and strings to combine.
 * @param {String} [flags] Any combination of XRegExp flags.
 * @param {Object} [options] Options object with optional properties:
 *   - `conjunction` {String} Type of conjunction to use: 'or' (default) or 'none'.
 * @returns {RegExp} Union of the provided regexes and strings.
 * @example
 *
 * XRegExp.union(['a+b*c', /(dogs)\1/, /(cats)\1/], 'i');
 * // -> /a\+b\*c|(dogs)\1|(cats)\2/i
 *
 * XRegExp.union([/man/, /bear/, /pig/], 'i', {conjunction: 'none'});
 * // -> /manbearpig/i
 */
XRegExp.union = function(patterns, flags, options) {
    options = options || {};
    var conjunction = options.conjunction || 'or';
    var numCaptures = 0;
    var numPriorCaptures;
    var captureNames;

    function rewrite(match, paren, backref) {
        var name = captureNames[numCaptures - numPriorCaptures];

        // Capturing group
        if (paren) {
            ++numCaptures;
            // If the current capture has a name, preserve the name
            if (name) {
                return '(?<' + name + '>';
            }
        // Backreference
        } else if (backref) {
            // Rewrite the backreference
            return '\\' + (+backref + numPriorCaptures);
        }

        return match;
    }

    if (!(isType(patterns, 'Array') && patterns.length)) {
        throw new TypeError('Must provide a nonempty array of patterns to merge');
    }

    var parts = /(\()(?!\?)|\\([1-9]\d*)|\\[\s\S]|\[(?:[^\\\]]|\\[\s\S])*\]/g;
    var output = [];
    var pattern;
    for (var i = 0; i < patterns.length; ++i) {
        pattern = patterns[i];

        if (XRegExp.isRegExp(pattern)) {
            numPriorCaptures = numCaptures;
            captureNames = (pattern[REGEX_DATA] && pattern[REGEX_DATA].captureNames) || [];

            // Rewrite backreferences. Passing to XRegExp dies on octals and ensures patterns are
            // independently valid; helps keep this simple. Named captures are put back
            output.push(nativ.replace.call(XRegExp(pattern.source).source, parts, rewrite));
        } else {
            output.push(XRegExp.escape(pattern));
        }
    }

    var separator = conjunction === 'none' ? '' : '|';
    return XRegExp(output.join(separator), flags);
};

// ==--------------------------==
// Fixed/extended native methods
// ==--------------------------==

/**
 * Adds named capture support (with backreferences returned as `result.name`), and fixes browser
 * bugs in the native `RegExp.prototype.exec`. Calling `XRegExp.install('natives')` uses this to
 * override the native method. Use via `XRegExp.exec` without overriding natives.
 *
 * @memberOf RegExp
 * @param {String} str String to search.
 * @returns {Array} Match array with named backreference properties, or `null`.
 */
fixed.exec = function(str) {
    var origLastIndex = this.lastIndex;
    var match = nativ.exec.apply(this, arguments);
    var name;
    var r2;
    var i;

    if (match) {
        // Fix browsers whose `exec` methods don't return `undefined` for nonparticipating capturing
        // groups. This fixes IE 5.5-8, but not IE 9's quirks mode or emulation of older IEs. IE 9
        // in standards mode follows the spec.
        if (!correctExecNpcg && match.length > 1 && indexOf(match, '') > -1) {
            r2 = copyRegex(this, {
                removeG: true,
                isInternalOnly: true
            });
            // Using `str.slice(match.index)` rather than `match[0]` in case lookahead allowed
            // matching due to characters outside the match
            nativ.replace.call(String(str).slice(match.index), r2, function() {
                var len = arguments.length;
                var i;
                // Skip index 0 and the last 2
                for (i = 1; i < len - 2; ++i) {
                    if (arguments[i] === undefined) {
                        match[i] = undefined;
                    }
                }
            });
        }

        // Attach named capture properties
        if (this[REGEX_DATA] && this[REGEX_DATA].captureNames) {
            // Skip index 0
            for (i = 1; i < match.length; ++i) {
                name = this[REGEX_DATA].captureNames[i - 1];
                if (name) {
                    match[name] = match[i];
                }
            }
        }

        // Fix browsers that increment `lastIndex` after zero-length matches
        if (this.global && !match[0].length && (this.lastIndex > match.index)) {
            this.lastIndex = match.index;
        }
    }

    if (!this.global) {
        // Fixes IE, Opera bug (last tested IE 9, Opera 11.6)
        this.lastIndex = origLastIndex;
    }

    return match;
};

/**
 * Fixes browser bugs in the native `RegExp.prototype.test`. Calling `XRegExp.install('natives')`
 * uses this to override the native method.
 *
 * @memberOf RegExp
 * @param {String} str String to search.
 * @returns {Boolean} Whether the regex matched the provided value.
 */
fixed.test = function(str) {
    // Do this the easy way :-)
    return !!fixed.exec.call(this, str);
};

/**
 * Adds named capture support (with backreferences returned as `result.name`), and fixes browser
 * bugs in the native `String.prototype.match`. Calling `XRegExp.install('natives')` uses this to
 * override the native method.
 *
 * @memberOf String
 * @param {RegExp|*} regex Regex to search with. If not a regex object, it is passed to `RegExp`.
 * @returns {Array} If `regex` uses flag g, an array of match strings or `null`. Without flag g,
 *   the result of calling `regex.exec(this)`.
 */
fixed.match = function(regex) {
    var result;

    if (!XRegExp.isRegExp(regex)) {
        // Use the native `RegExp` rather than `XRegExp`
        regex = new RegExp(regex);
    } else if (regex.global) {
        result = nativ.match.apply(this, arguments);
        // Fixes IE bug
        regex.lastIndex = 0;

        return result;
    }

    return fixed.exec.call(regex, toObject(this));
};

/**
 * Adds support for `${n}` tokens for named and numbered backreferences in replacement text, and
 * provides named backreferences to replacement functions as `arguments[0].name`. Also fixes browser
 * bugs in replacement text syntax when performing a replacement using a nonregex search value, and
 * the value of a replacement regex's `lastIndex` property during replacement iterations and upon
 * completion. Calling `XRegExp.install('natives')` uses this to override the native method. Note
 * that this doesn't support SpiderMonkey's proprietary third (`flags`) argument. Use via
 * `XRegExp.replace` without overriding natives.
 *
 * @memberOf String
 * @param {RegExp|String} search Search pattern to be replaced.
 * @param {String|Function} replacement Replacement string or a function invoked to create it.
 * @returns {String} New string with one or all matches replaced.
 */
fixed.replace = function(search, replacement) {
    var isRegex = XRegExp.isRegExp(search);
    var origLastIndex;
    var captureNames;
    var result;

    if (isRegex) {
        if (search[REGEX_DATA]) {
            captureNames = search[REGEX_DATA].captureNames;
        }
        // Only needed if `search` is nonglobal
        origLastIndex = search.lastIndex;
    } else {
        search += ''; // Type-convert
    }

    // Don't use `typeof`; some older browsers return 'function' for regex objects
    if (isType(replacement, 'Function')) {
        // Stringifying `this` fixes a bug in IE < 9 where the last argument in replacement
        // functions isn't type-converted to a string
        result = nativ.replace.call(String(this), search, function() {
            var args = arguments;
            var i;
            if (captureNames) {
                // Change the `arguments[0]` string primitive to a `String` object that can store
                // properties. This really does need to use `String` as a constructor
                args[0] = new String(args[0]);
                // Store named backreferences on the first argument
                for (i = 0; i < captureNames.length; ++i) {
                    if (captureNames[i]) {
                        args[0][captureNames[i]] = args[i + 1];
                    }
                }
            }
            // Update `lastIndex` before calling `replacement`. Fixes IE, Chrome, Firefox, Safari
            // bug (last tested IE 9, Chrome 17, Firefox 11, Safari 5.1)
            if (isRegex && search.global) {
                search.lastIndex = args[args.length - 2] + args[0].length;
            }
            // ES6 specs the context for replacement functions as `undefined`
            return replacement.apply(undefined, args);
        });
    } else {
        // Ensure that the last value of `args` will be a string when given nonstring `this`,
        // while still throwing on null or undefined context
        result = nativ.replace.call(this == null ? this : String(this), search, function() {
            // Keep this function's `arguments` available through closure
            var args = arguments;
            return nativ.replace.call(String(replacement), replacementToken, function($0, $1, $2) {
                var n;
                // Named or numbered backreference with curly braces
                if ($1) {
                    // XRegExp behavior for `${n}`:
                    // 1. Backreference to numbered capture, if `n` is an integer. Use `0` for the
                    //    entire match. Any number of leading zeros may be used.
                    // 2. Backreference to named capture `n`, if it exists and is not an integer
                    //    overridden by numbered capture. In practice, this does not overlap with
                    //    numbered capture since XRegExp does not allow named capture to use a bare
                    //    integer as the name.
                    // 3. If the name or number does not refer to an existing capturing group, it's
                    //    an error.
                    n = +$1; // Type-convert; drop leading zeros
                    if (n <= args.length - 3) {
                        return args[n] || '';
                    }
                    // Groups with the same name is an error, else would need `lastIndexOf`
                    n = captureNames ? indexOf(captureNames, $1) : -1;
                    if (n < 0) {
                        throw new SyntaxError('Backreference to undefined group ' + $0);
                    }
                    return args[n + 1] || '';
                }
                // Else, special variable or numbered backreference without curly braces
                if ($2 === '$') { // $$
                    return '$';
                }
                if ($2 === '&' || +$2 === 0) { // $&, $0 (not followed by 1-9), $00
                    return args[0];
                }
                if ($2 === '`') { // $` (left context)
                    return args[args.length - 1].slice(0, args[args.length - 2]);
                }
                if ($2 === "'") { // $' (right context)
                    return args[args.length - 1].slice(args[args.length - 2] + args[0].length);
                }
                // Else, numbered backreference without curly braces
                $2 = +$2; // Type-convert; drop leading zero
                // XRegExp behavior for `$n` and `$nn`:
                // - Backrefs end after 1 or 2 digits. Use `${..}` for more digits.
                // - `$1` is an error if no capturing groups.
                // - `$10` is an error if less than 10 capturing groups. Use `${1}0` instead.
                // - `$01` is `$1` if at least one capturing group, else it's an error.
                // - `$0` (not followed by 1-9) and `$00` are the entire match.
                // Native behavior, for comparison:
                // - Backrefs end after 1 or 2 digits. Cannot reference capturing group 100+.
                // - `$1` is a literal `$1` if no capturing groups.
                // - `$10` is `$1` followed by a literal `0` if less than 10 capturing groups.
                // - `$01` is `$1` if at least one capturing group, else it's a literal `$01`.
                // - `$0` is a literal `$0`.
                if (!isNaN($2)) {
                    if ($2 > args.length - 3) {
                        throw new SyntaxError('Backreference to undefined group ' + $0);
                    }
                    return args[$2] || '';
                }
                // `$` followed by an unsupported char is an error, unlike native JS
                throw new SyntaxError('Invalid token ' + $0);
            });
        });
    }

    if (isRegex) {
        if (search.global) {
            // Fixes IE, Safari bug (last tested IE 9, Safari 5.1)
            search.lastIndex = 0;
        } else {
            // Fixes IE, Opera bug (last tested IE 9, Opera 11.6)
            search.lastIndex = origLastIndex;
        }
    }

    return result;
};

/**
 * Fixes browser bugs in the native `String.prototype.split`. Calling `XRegExp.install('natives')`
 * uses this to override the native method. Use via `XRegExp.split` without overriding natives.
 *
 * @memberOf String
 * @param {RegExp|String} separator Regex or string to use for separating the string.
 * @param {Number} [limit] Maximum number of items to include in the result array.
 * @returns {Array} Array of substrings.
 */
fixed.split = function(separator, limit) {
    if (!XRegExp.isRegExp(separator)) {
        // Browsers handle nonregex split correctly, so use the faster native method
        return nativ.split.apply(this, arguments);
    }

    var str = String(this);
    var output = [];
    var origLastIndex = separator.lastIndex;
    var lastLastIndex = 0;
    var lastLength;

    // Values for `limit`, per the spec:
    // If undefined: pow(2,32) - 1
    // If 0, Infinity, or NaN: 0
    // If positive number: limit = floor(limit); if (limit >= pow(2,32)) limit -= pow(2,32);
    // If negative number: pow(2,32) - floor(abs(limit))
    // If other: Type-convert, then use the above rules
    // This line fails in very strange ways for some values of `limit` in Opera 10.5-10.63, unless
    // Opera Dragonfly is open (go figure). It works in at least Opera 9.5-10.1 and 11+
    limit = (limit === undefined ? -1 : limit) >>> 0;

    XRegExp.forEach(str, separator, function(match) {
        // This condition is not the same as `if (match[0].length)`
        if ((match.index + match[0].length) > lastLastIndex) {
            output.push(str.slice(lastLastIndex, match.index));
            if (match.length > 1 && match.index < str.length) {
                Array.prototype.push.apply(output, match.slice(1));
            }
            lastLength = match[0].length;
            lastLastIndex = match.index + lastLength;
        }
    });

    if (lastLastIndex === str.length) {
        if (!nativ.test.call(separator, '') || lastLength) {
            output.push('');
        }
    } else {
        output.push(str.slice(lastLastIndex));
    }

    separator.lastIndex = origLastIndex;
    return output.length > limit ? output.slice(0, limit) : output;
};

// ==--------------------------==
// Built-in syntax/flag tokens
// ==--------------------------==

/*
 * Letter escapes that natively match literal characters: `\a`, `\A`, etc. These should be
 * SyntaxErrors but are allowed in web reality. XRegExp makes them errors for cross-browser
 * consistency and to reserve their syntax, but lets them be superseded by addons.
 */
XRegExp.addToken(
    /\\([ABCE-RTUVXYZaeg-mopqyz]|c(?![A-Za-z])|u(?![\dA-Fa-f]{4}|{[\dA-Fa-f]+})|x(?![\dA-Fa-f]{2}))/,
    function(match, scope) {
        // \B is allowed in default scope only
        if (match[1] === 'B' && scope === defaultScope) {
            return match[0];
        }
        throw new SyntaxError('Invalid escape ' + match[0]);
    },
    {
        scope: 'all',
        leadChar: '\\'
    }
);

/*
 * Unicode code point escape with curly braces: `\u{N..}`. `N..` is any one or more digit
 * hexadecimal number from 0-10FFFF, and can include leading zeros. Requires the native ES6 `u` flag
 * to support code points greater than U+FFFF. Avoids converting code points above U+FFFF to
 * surrogate pairs (which could be done without flag `u`), since that could lead to broken behavior
 * if you follow a `\u{N..}` token that references a code point above U+FFFF with a quantifier, or
 * if you use the same in a character class.
 */
XRegExp.addToken(
    /\\u{([\dA-Fa-f]+)}/,
    function(match, scope, flags) {
        var code = dec(match[1]);
        if (code > 0x10FFFF) {
            throw new SyntaxError('Invalid Unicode code point ' + match[0]);
        }
        if (code <= 0xFFFF) {
            // Converting to \uNNNN avoids needing to escape the literal character and keep it
            // separate from preceding tokens
            return '\\u' + pad4(hex(code));
        }
        // If `code` is between 0xFFFF and 0x10FFFF, require and defer to native handling
        if (hasNativeU && flags.indexOf('u') > -1) {
            return match[0];
        }
        throw new SyntaxError('Cannot use Unicode code point above \\u{FFFF} without flag u');
    },
    {
        scope: 'all',
        leadChar: '\\'
    }
);

/*
 * Empty character class: `[]` or `[^]`. This fixes a critical cross-browser syntax inconsistency.
 * Unless this is standardized (per the ES spec), regex syntax can't be accurately parsed because
 * character class endings can't be determined.
 */
XRegExp.addToken(
    /\[(\^?)\]/,
    function(match) {
        // For cross-browser compatibility with ES3, convert [] to \b\B and [^] to [\s\S].
        // (?!) should work like \b\B, but is unreliable in some versions of Firefox
        return match[1] ? '[\\s\\S]' : '\\b\\B';
    },
    {leadChar: '['}
);

/*
 * Comment pattern: `(?# )`. Inline comments are an alternative to the line comments allowed in
 * free-spacing mode (flag x).
 */
XRegExp.addToken(
    /\(\?#[^)]*\)/,
    getContextualTokenSeparator,
    {leadChar: '('}
);

/*
 * Whitespace and line comments, in free-spacing mode (aka extended mode, flag x) only.
 */
XRegExp.addToken(
    /\s+|#[^\n]*\n?/,
    getContextualTokenSeparator,
    {flag: 'x'}
);

/*
 * Dot, in dotall mode (aka singleline mode, flag s) only.
 */
XRegExp.addToken(
    /\./,
    function() {
        return '[\\s\\S]';
    },
    {
        flag: 's',
        leadChar: '.'
    }
);

/*
 * Named backreference: `\k<name>`. Backreference names can use the characters A-Z, a-z, 0-9, _,
 * and $ only. Also allows numbered backreferences as `\k<n>`.
 */
XRegExp.addToken(
    /\\k<([\w$]+)>/,
    function(match) {
        // Groups with the same name is an error, else would need `lastIndexOf`
        var index = isNaN(match[1]) ? (indexOf(this.captureNames, match[1]) + 1) : +match[1];
        var endIndex = match.index + match[0].length;
        if (!index || index > this.captureNames.length) {
            throw new SyntaxError('Backreference to undefined group ' + match[0]);
        }
        // Keep backreferences separate from subsequent literal numbers. This avoids e.g.
        // inadvertedly changing `(?<n>)\k<n>1` to `()\11`.
        return '\\' + index + (
            endIndex === match.input.length || isNaN(match.input.charAt(endIndex)) ?
                '' : '(?:)'
        );
    },
    {leadChar: '\\'}
);

/*
 * Numbered backreference or octal, plus any following digits: `\0`, `\11`, etc. Octals except `\0`
 * not followed by 0-9 and backreferences to unopened capture groups throw an error. Other matches
 * are returned unaltered. IE < 9 doesn't support backreferences above `\99` in regex syntax.
 */
XRegExp.addToken(
    /\\(\d+)/,
    function(match, scope) {
        if (
            !(
                scope === defaultScope &&
                /^[1-9]/.test(match[1]) &&
                +match[1] <= this.captureNames.length
            ) &&
            match[1] !== '0'
        ) {
            throw new SyntaxError('Cannot use octal escape or backreference to undefined group ' +
                match[0]);
        }
        return match[0];
    },
    {
        scope: 'all',
        leadChar: '\\'
    }
);

/*
 * Named capturing group; match the opening delimiter only: `(?<name>`. Capture names can use the
 * characters A-Z, a-z, 0-9, _, and $ only. Names can't be integers. Supports Python-style
 * `(?P<name>` as an alternate syntax to avoid issues in some older versions of Opera which natively
 * supported the Python-style syntax. Otherwise, XRegExp might treat numbered backreferences to
 * Python-style named capture as octals.
 */
XRegExp.addToken(
    /\(\?P?<([\w$]+)>/,
    function(match) {
        // Disallow bare integers as names because named backreferences are added to match arrays
        // and therefore numeric properties may lead to incorrect lookups
        if (!isNaN(match[1])) {
            throw new SyntaxError('Cannot use integer as capture name ' + match[0]);
        }
        if (match[1] === 'length' || match[1] === '__proto__') {
            throw new SyntaxError('Cannot use reserved word as capture name ' + match[0]);
        }
        if (indexOf(this.captureNames, match[1]) > -1) {
            throw new SyntaxError('Cannot use same name for multiple groups ' + match[0]);
        }
        this.captureNames.push(match[1]);
        this.hasNamedCapture = true;
        return '(';
    },
    {leadChar: '('}
);

/*
 * Capturing group; match the opening parenthesis only. Required for support of named capturing
 * groups. Also adds explicit capture mode (flag n).
 */
XRegExp.addToken(
    /\((?!\?)/,
    function(match, scope, flags) {
        if (flags.indexOf('n') > -1) {
            return '(?:';
        }
        this.captureNames.push(null);
        return '(';
    },
    {
        optionalFlags: 'n',
        leadChar: '('
    }
);

module.exports = XRegExp;

},{}]},{},[8])(8)
});]]></file>
 <order app="global" path="/dev/js//framework/">templates
common/ips.loader.js
common/ui
common/utils
common
controllers</order>
 <order app="global" path="/dev/js//library/">underscore
jquery
mustache
IntersectionObserver
Debug.js
app.js</order>
 <order app="global" path="/dev/js//library//jquery">jquery.js
jquery-migrate.js
jquery.history.js
jquery.transform.js</order>
 <order app="global" path="/dev/js//library//linkify">linkify.min.js
linkify-jquery.min.js</order>
</javascript>
