<?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="103021" 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 || {};
( function($, _, undefined){
"use strict";
ips = ( function () {
var _settings,
_strings = {},
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 .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 () {
return ( getSetting('mock_ajax') ) ? getSetting('mock_ajax') : $.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 () {
// Make sure all ajax requests have our secure key & adsess
var data = {
csrfKey: ips.getSetting('csrfKey')
};
if( ips.getSetting('adsess') ){
_.extend( data, {
adsess: ips.getSetting('adsess')
});
}
$.ajaxSetup({
data: data,
cache: true
});
// Add global loading indicator ability
var count = 0;
$( document )
.ajaxSend( function (event, request, settings) {
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
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 + "]" : "";
}
},
/**
* 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 ) ){
$( 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 = $.trim( output.replace( /\[.+\]/, value == null ? fallback : value ) );
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
});
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,
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, _));]]></file>
<file javascript_app="global" javascript_location="library" javascript_path="" javascript_name="Debug.js" javascript_type="framework" javascript_version="103021" 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 ) && 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="103021" 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 {
return ipsJavascriptMap[ bits[0] ][ bits[1] + '_' + bits[2] ];
} 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] = $.trim( pieces[i] );
// 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( $.trim( val ) );
});
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="103021" 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('antiCache')
}
})
.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="103021" 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="103021" 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( $.trim( $( this ).find('option[value="' + $( this ).val() + '"]').text() ) );
} else {
value.push( $.trim( $( this ).val() ) );
}
}
});
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();
var i;
var streetNumber;
var addressLines = [];
for( i in place.address_components ) {
switch ( place.address_components[i].types[0] ) {
case 'street_number':
streetNumber = place.address_components[i].long_name;
break;
case 'street_address':
case 'route':
case 'sublocality':
case 'neighborhood':
case 'premise':
case 'subpremise':
addressLines.push( place.address_components[i].long_name );
break;
case 'administrative_area_level_1':
case 'administrative_area_level_2':
elem.find('[data-role="regionSelect"]').val( place.address_components[i].long_name );
elem.find('[data-role="regionText"]').focus().val( place.address_components[i].long_name );
break;
case 'locality':
case 'postal_town':
elem.find('[data-role="city"]').focus().val( place.address_components[i].long_name );
break;
case 'postal_code':
elem.find('[data-role="postalCode"]').focus().val( place.address_components[i].long_name );
break;
}
}
for( var i = 0; i < $( elem ).find('[data-role="addressLine"]').length; i++ ){
$( elem ).find('[data-role="addressLine"]').val( '' );
}
setTimeout( function () {
var existingAddressLines = $( elem ).find('[data-role="addressLine"]').length;
if( streetNumber && addressLines[0] ){
// Some locales don't use "{streetNumber} {addressLine}" so see if the first part of formatted_address looks like it matches, and if so, use that...
var splitFormatted = place.formatted_address.split(',');
if ( splitFormatted[0].indexOf( addressLines[0] ) != -1 ) {
addressLines[0] = splitFormatted[0];
}
// Otherwise, fallback to "{streetNumber} {addressLine}"
else {
addressLines[0] = streetNumber + ' ' + addressLines[0];
}
}
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]);
}
}
}, 50 );
});
}
// 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="103021" 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){
"use strict";
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="ok"]').click();
} else {
alert.find('[data-action="cancel"], [data-action="no"]').click();
}
break;
case ips.ui.key.ENTER:
alert.find('[data-action="ok"], [data-action="yes"]').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="promptValue"]').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
});
}
// 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( $.trim( ips.templates.render( 'core.alert.box', parts ) ) );
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="' + options.focus + '"]')
.focus();
} else {
if( options.type == 'prompt' ){
alert
.find('[data-role="promptValue"]')
.val( options.value || '' )
.focus();
} else {
alert
.find('[data-action="ok"], [data-action="yes"]')
.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="103021" 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) {
/**
* 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.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();
},
/**
* 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>');
}
if( count.length ){
elem.find('[data-role="autoCheckCount"]').text( count.length ).show();
} else if( elem.find('[data-role="autoCheckCount"]').is(':visible') ) {
ips.utils.anim.go( 'fadeOut', elem.find('[data-role="autoCheckCount"]') );
}
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="103021" 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
};
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( 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( '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("'", "'").replace("'", "'");
},
/**
* 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 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');
}
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();
}
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();
if( options.multiValues ){
if( value.indexOf( options.itemSep.chr ) === -1 || !options.commaTrigger ){
// Multi items, but only one entered so far
return $.trim( value );
} else {
// Get the last-entered item
var pieces = value.split( options.itemSep.chr );
return $.trim( pieces[ pieces.length - 1 ] );
}
} 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 = $.trim( textField.val() );
// 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 = $.trim( _.escape( value ) );
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 + '=' + encodeURI( 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="103021" 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="103021" 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 currentLeftPos = 0;
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 leftPos = parseInt( ui.itemList.css('left') );
if( !_.isNaN( leftPos ) ){
currentLeftPos = leftPos;
}
_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 );
// Add touch controls
var mc = new Hammer( elem[0] );
mc.on( 'panleft', function( e ) {
if( !animating ){
_navNext();
}
} );
mc.on( 'panright', function( e ) {
if( !animating ){
_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 );
// Check for visible images
_loadImagesInVisibleItems();
},
/**
* 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 );
},
/**
* Get the visible items and run loadItemImage on them.
*
* @returns {void}
*/
_loadImagesInVisibleItems = function (from) {
if( !from ){
var items = elem.find( options.item );
} else {
var items = from.nextAll( options.item );
}
var _loadItemImage = function (item) {
if( item.attr('data-lazyLoad') ){
var src = item.attr('data-lazyLoad');
item.removeAttr('data-lazyLoad');
Debug.log("Loaded " + src);
if( item.find('[data-srcBg]').length ){
item.find('[data-srcBg]').css({
backgroundImage: "url(" + src.replace( /\(/g, '\\(' ).replace( /\)/g, '\\)' ) + ")"
});
}
if( item.find('[data-src]').length ){
item.find('[data-src]').attr('src', src);
}
}
};
var wrapperWidth = elem.outerWidth();
var listWidth = _getCurrentWidth();
var stayOnScreen = currentLeftPos + wrapperWidth;
_.each( items, function (item) {
var item = $( item );
var leftEdge = $( item ).position().left; // left edge
var width = $( item ).outerWidth();
var margin = ( rtlMode ) ? parseInt( $( item ).css('marginLeft') ) : parseInt( $( item ).css('marginRight') );
if( rtlMode ){
var rightEdge = ui.itemList.outerWidth() - ( leftEdge + width ) - margin; // right edge
var currentRightPos = ( parseInt( ui.itemList.css('right') ) ) * -1;
var showing = ( ( currentRightPos * 1 ) + wrapperWidth ) * 1;
if( rightEdge >= currentRightPos && rightEdge < showing ){
_loadItemImage( item );
}
if( rightEdge > showing ){
return false;
}
} else {
if( leftEdge >= currentLeftPos && leftEdge < stayOnScreen ){
_loadItemImage( item );
}
if( leftEdge > stayOnScreen ){
return false;
}
}
});
},
/**
* 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 stayOnScreen = currentLeftPos + wrapperWidth;
var forceNext = false;
var deferred = $.Deferred();
var nextFirst = _.find( items, function (item, idx) {
var edge = $( item ).position().left; // left edge
var width = $( item ).outerWidth();
var margin = ( rtlMode ) ? parseInt( $( item ).css('marginLeft') ) : parseInt( $( item ).css('marginRight') );
if( rtlMode ){
edge = ui.itemList.outerWidth() - ( edge + width ) - margin; // right edge
}
// If we set forceNext = true on the last loop, return the item now.
if( forceNext ){
return true;
}
// If this is off to the left of the screen, just cancel
if( edge < currentLeftPos ){
return false;
}
var otherEdge = edge + width;
// 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( rtlMode ){
if( stayOnScreen <= ( otherEdge + margin ) ){
forceNext = true;
return false;
}
} else if( stayOnScreen >= otherEdge && stayOnScreen <= ( otherEdge + margin ) ){
forceNext = true;
return false;
}
// If the left of this item is on screen and the right isn't, this is our next item to show.
if( rtlMode ){
if( edge > stayOnScreen && otherEdge < stayOnScreen ){
return true;
}
} else if( edge < stayOnScreen && otherEdge > stayOnScreen ){
return true;
}
return false;
});
var nextFirst = $( nextFirst );
if( !nextFirst.length ){
//Debug.error("nextFirst didn't exist");
deferred.resolve();
return deferred.promise();
}
var nextFirstPos = nextFirst.position().left;
var nextFirstMargin = ( rtlMode ) ? parseInt( $( nextFirst ).css('marginLeft') ) : parseInt( $( nextFirst ).css('marginRight') );
// For RTL we need the right-hand edge
if( rtlMode ){
nextFirstPos = ui.itemList.outerWidth() - ( nextFirstPos + nextFirst.outerWidth() ) - nextFirstMargin;
}
// 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;
}
currentLeftPos = 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;
_loadImagesInVisibleItems( nextFirst );
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 = currentLeftPos + 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 idealLeft = ( currentLeftPos * -1 ) + wrapperWidth;
// If we need to go back to the bottom, we can shortcut
if( idealLeft >= 0 || backToBeginning ){
currentLeftPos = 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();
idealLeft = ( idealLeft * -1 ) + wrapperWidth;
// Now we can find the first item that's fully on screen
var prevFirst = _.find( items, function (item) {
var edge = $( item ).position().left;
var width = $( item ).outerWidth();
var otherEdge = edge + width;
var margin = ( rtlMode ) ? parseInt( $( item ).css('marginLeft') ) : parseInt( $( item ).css('marginRight') );
if( rtlMode ){
edge = ui.itemList.outerWidth() - ( otherEdge ); // right edge
}
// If we set forcePrev = true on the last loop, return the item now.
if( forcePrev ){
return true;
}
if( edge > idealLeft ){
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 >= otherEdge && stayOnScreen <= ( otherEdge + margin ) ){
forcePrev = true;
return false;
}
if( otherEdge > idealLeft && edge <= idealLeft ){
return true;
}
return false;
});
// 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
prevFirst = $( prevFirst );
Debug.log( prevFirst );
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 ) {
currentLeftPos = prevFirst.position().left + prevFirst.outerWidth() - wrapperWidth;
if( rtlMode ){
var prevFirstMargin = ( rtlMode ) ? parseInt( $( prevFirst ).css('marginLeft') ) : parseInt( $( prevFirst ).css('marginRight') );
currentLeftPos = ui.itemList.outerWidth() - ( prevFirst.position().left + prevFirst.outerWidth() ) - prevFirstMargin - wrapperWidth; // right edge
}
} else {
currentLeftPos = prevFirst.outerWidth() + wrapperWidth;
}
animating = true;
ui.itemList.animate(
( rtlMode ) ? { right: ( currentLeftPos * -1 ) + 'px' } : { left: ( currentLeftPos * -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();
},
/**
* 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( Math.floor(list.right) <= Math.floor(container.right) ){
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( Math.floor(list.left) >= Math.floor(container.left) ){
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'
});
});
}
};
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="103021" 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){
"use strict";
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.google.com/jsapi'] ).then( function () {
google.load( 'visualization', '1.0', {'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 ) ){
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="103021" 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 = $.trim( textField.val() );
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 + '=' + encodeURI( 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.dialog.js" javascript_type="ui" javascript_version="103021" 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 ) );
},
/**
* 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
};
});
/**
* 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="103021" 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 () {
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();
},
_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="103021" 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
};
/**
* 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;
if( ips.getSetting('useCompiledFiles') !== true ){
ips.loader.get( ['core/dev/ckeditor/ckeditor.js'] ).then( bootEditor );
} else {
ips.loader.get( ['core/interface/ckeditor/ckeditor/ckeditor.js'] ).then( bootEditor );
}
/**
* Wrapper function that ensures we don't try and boot ckeditor until the library is ready
*
* @returns {void}
*/
function bootEditor () {
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();
} else {
CKEDITOR.on( 'loaded', function () {
ckLoaded();
});
}
};
/**
* The function that actually initializes the editor on our widget element
*
* @returns {void}
*/
function ckLoaded () {
if( !$( elem ).data('_editor') ){
var editor = editorObj( elem, _.defaults( options, defaults ) );
$( elem ).data('_editor', editor );
editor.init();
}
};
},
/**
* 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;
};
ips.ui.registerWidget('editor', ips.ui.editor,
[ 'allbuttons', 'postKey', 'toolbars', 'extraPlugins', 'autoGrow', 'contentsCss', 'minimized', 'autoSaveKey', 'skin', 'name', 'pasteBehaviour', 'autoEmbed', 'controller', 'defaultIfNoAutoSave' ]
);
return {
respond: respond,
getObj: getObj,
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) {
// 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: 'ipsautolink,ipsautosave,ipsctrlenter,ipscode,ipscontextmenu,ipsemoticon,ipsimage,ipslink,ipsmentions,ipspage,ipspaste,ipspreview,ipsquote,ipsspoiler,ipsautogrow,ipssource,removeformat',
// 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) {
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;
}
// 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(){
elem.trigger( 'editorWidgetInitialized', { id: name } );
if( _.isFunction( callback ) ){
callback();
}
});
// 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 );
};
/**
* Destructs this object
*
* @returns void
*/
var destruct = function () {
try {
// 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();
Debug.log("Destroyed editor instance");
} catch (err) {
Debug.error("Editor destruct error:");
Debug.error( err );
}
};
/**
* 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 ) {
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();
}
};
/**
* 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' );
}
};
/**
* 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 () {
instance.setData('<p></p>');
_closePreview();
elem.find('[data-ipsUploader]').trigger('resetUploader');
};
/**
* 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');
$.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();
}
});
var images = document.find('img,video');
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') ){
toRemove.push( image );
}
});
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,
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="103021" 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){
"use strict";
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="filterBar"]');
filterContent = elem.find('[data-role="filterContent"]');
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="filterBarSwitch"]', _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="filterBar"], [data-role="filterContent"]' )
.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="103021" 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( _.escape( $('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
.css({ zIndex: ips.ui.zIndex() })
.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="103021" 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"]',
// Phone number international dialling code select
'diallingCode': '[data-control="diallingCode"]'
};
/**
* 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( $( 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 );
});
_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;
}
elem.addClass('color');
elem.attr('data-ipsFormData', 1);
ips.loader.get( ['core/interface/jscolor/jscolor.js'] ).then( function () {
// We need to set the image dir for jscolor manually because when our JS is compiled,
// jscolor's auto-detect function won't get it right
jscolor.dir = ips.getSetting('baseURL') + 'applications/core/interface/jscolor/';
new jscolor.color( elem.get(0), { required: false } );
if( elem.is(':focus') )
{
elem.blur();
}
});
},
/**
* 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 );
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 );
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[type="checkbox"]').prop( 'checked', false );
elem.find('[data-role="checkboxsetUnlimited"]').slideDown();
elem.find('[data-role="checkboxsetGranular"]').slideUp();
elem.find('[data-role="checkboxsetUnlimitedToggle"]').prop( 'checked', false ).change();
});
},
/**
* 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( 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 );
});
},
/**
* 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();
thisInput.attr( 'data-previousvalue', val );
thisInput.val(''); // Empty the value
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="103021" 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');
formElement.on( 'submit', function( e ){
formElement.find('input[type="submit"],button[type="submit"]').prop( 'disabled', true );
});
};
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="103021" 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') ){
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="103021" 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="103021" 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: 'body',
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 == 'body' ){
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) {
return elem.attr('data-ipsInfScroll-url') + '&' + 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="103021" 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(/^[0-9]+$/g) ){
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( $.trim( thisOption ) === '' ){
thisOption = true;
}
options[ optionKeys[i] ] = thisOption;
}
}
}
return options;
},
/**
* Returns the next zIndex value
*
* @returns {number}
*/
zIndex = function () {
ziCounter += ziIncrement;
return 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.lightbox.js" javascript_type="ui" javascript_version="103021" 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 );
// 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");
}
});
// Hide all images, and append this new one
pieces.imagePanel
.find('.' + options.className + '_image ')
.hide()
.end()
.append(
thisImage
);
}
// 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 );
// 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();
}
}
},
/**
* 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 ];
},
/**
* 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(),
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("×"),
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.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-commentURL')
};
},
/**
* 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="103021" 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,
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 );
if( ips.getSetting('mapProvider') == 'google' ){
_google( elem, options );
} else if( ips.getSetting('mapProvider') == 'mapbox' ){
_mapbox( elem, options );
}
};
/**
* 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.1.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.1.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), 'mapbox.streets').setView([45, 0], options.zoom);
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
}).addTo(map);
cluster.addLayer( marker );
// Build info popup for this marker
if( options.contentUrl ){
marker.bindPopup( ips.getString('loading') );
marker.on('click', handlePopup );
}
}
});
});
};
/**
* 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,
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 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 );
});
});
}
}
});
};
/**
* Provides a callback for Google Maps API
*
* @returns {void}
*/
var googleCallback = function(){
$( window ).trigger( 'googleApiLoaded' );
};
ips.ui.registerWidget('map', ips.ui.map, [ 'zoom', '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="103021" 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 right = ( itemSize.outerWidth - 5 );
// If the submenu won't fit to the left of the item, and will fit to the right, then we'll position it there
if( ( itemPosition.viewportOffset.right + itemSize.outerWidth + subMenuSize.outerWidth - 5 ) > $( window ).width() ){
if( ( itemPosition.viewportOffset.right + 5 - subMenuSize.outerWidth ) >= 0 ){
right = ( ( subMenuSize.outerWidth * -1 ) + 5 );
}
}
subMenu
.css({
left: right + 'px',
top: ( menuItem.position()['top'] - 5 ) + 'px'
})
.show();
} else {
var left = ( itemSize.outerWidth - 5 );
// If the submenu won't fit to the right of the item, and will fit to the left, then we'll position it there
if( ( itemPosition.viewportOffset.left + itemSize.outerWidth + subMenuSize.outerWidth - 5 ) > $( window ).width() ){
if( ( itemPosition.viewportOffset.left + 5 - subMenuSize.outerWidth ) >= 0 ){
left = ( ( subMenuSize.outerWidth * -1 ) + 5 );
}
}
subMenu
.css({
left: left + 'px',
top: ( menuItem.position()['top'] - 5 ) + '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 });
},
/**
* 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;
menu
.attr( 'data-originalWidth', menuDims.width )
.css({
left: '10px',
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 ).parent( '.' + 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 = $.trim( appends[i] );
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="103021" 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.getParam('page');
if( !_.isUndefined( page ) ){
var pageNumber = $('<input/>').attr( 'type', 'hidden' ).attr( 'name', 'page' ).attr( 'value', ips.utils.url.getParam('page') );
tools.find('[type="submit"]').before( pageNumber );
tools.closest('form').attr( 'action', tools.closest('form').attr( 'action' ) + '&page=' +ips.utils.url.getParam('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="103021" 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'
};
/**
* 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']
);
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),
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),
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="103021" 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' ]
);
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 () {
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');
// Do _ajax
_ajax( ips.getSetting('baseURL') + '?app=core&module=system&controller=ajax&do=passwordStrength', {
dataType: 'json',
data: {
input: value
},
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="103021" 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="103021" 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
});
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%'
});
},
/**
* 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 '
imageData.container = imageData.container.replace( /'/ig, "'" );
}
return ips.templates.render( options.itemTemplate, {
showThumb: showThumb,
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="103021" 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;
/**
* 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 () {
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,
width: elemSize.outerHeight + 'px',
height: elemSize.outerHeight + 'px',
left: elemPosition.absPos.left + elemSize.outerWidth + 20 + 'px',
top: elemPosition.absPos.top + 'px',
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="103021" 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');
/* 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 );
},
/**
* 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="103021" 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="103021" 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 );
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();
}
} 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"]') );
} else {
listItem.append( $('<div/>').attr('data-role', 'childWrapper').html( ips.templates.render('core.general.loading', { text: ips.getString('loading') } ) ) );
// 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');
}
});
});
}
}
},
/**
* 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();
},
/**
* 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
}) );
// 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 () {
// Get position of select box
var positionInfo = {
trigger: elem,
target: results,
targetContainer: elem
};
var selectWidth = elem.outerWidth();
var resultsPosition = ips.utils.position.positionElem( positionInfo );
results.css({
left: -parseFloat( results.css('borderLeftWidth') ) - parseFloat( results.css('marginLeft') ),
position: 'absolute',
zIndex: ips.ui.zIndex(),
minWidth: selectWidth + 'px'
});
},
/**
* 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;
};
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="103021" 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="103021" 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) {
/* 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="103021" 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;
/**
* Sets up this instance
*
* @returns {void}
*/
var init = function () {
if ( !elem.attr('data-initiated') ) {
stack = elem.find('[data-role="stack"]');
// 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;
}
var field = stack.find('[data-ipsStack-wrapper]')
.first()
.html()
.replace( /(name=['"][a-zA-Z0-9\-_]+?)\[([^\]]+?)?\]/g, '$1[' + _getItemCount() + ']' )
.replace( /data-ipsFormData=['"](.+?)['"]/ig, '' )
.replace( /id=['"](.+?)['"]/g, 'id="$1_' + _getItemCount() + '"' )
.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 + '_' + _getItemCount() );
} 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();
}, 100);
});
if( options.maxItems && _getItemCount() < options.maxItems ){
elem.find('[data-action="stackAdd"]').show();
}
},
/**
* 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="103021" 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 $.trim( item )
});
}
// 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: $( elem ).width(),
height: $( elem ).outerHeight()
})
.show();
}
status = 'fixed';
$( elem ).trigger( 'stickyStatusChange.sticky', {
status: 'fixed'
});
},
/**
* Returns the element to 'normal' mode
*
* @returns {void}
*/
_makeNormal = function () {
$( elem )
.css( {
position: originalStyles.position,
width: originalStyles.width
})
.removeClass( options.stickyClass )
.removeClass( options.stickyClass + '_top' )
// Reset the value we set earlier
if( options.stickTo == 'bottom' ){
$( elem )
.css( { bottom: originalStyles.bottom } )
.removeClass( options.stickyClass + '_bottom' );
} else {
$( elem )
.css( { top: 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="103021" 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="103021" 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){
"use strict";
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'),
lang = ( status ) ? ips.getString('toggleOn') : ips.getString('toggleOff'),
className = ( status ) ? 'ipsToggle_on' : 'ipsToggle_off';
// Build toggle wrapper
$( elem )
.after(
ips.templates.render( options.template, {
id: checkID + '_wrapper',
status: lang,
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')
.find('[data-role="status"]')
.text( ips.getString('toggleOff') );
elem.get(0).checked = false;
} else {
wrapper
.removeClass('ipsToggle_off')
.addClass('ipsToggle_on')
.find('[data-role="status"]')
.text( ips.getString('toggleOn') );
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="103021" 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 {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 );
}
// Remove the title if any
if( elem.attr('title') ){
elem
.attr( '_title', elem.attr('title') )
.removeAttr('title');
}
// 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');
}
_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 );
// Option takes priority
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' ],
{ 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="103021" 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) {
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'
});
// Hook up event
expander.on('click', function (e) {
// We need to recalculate the height here, because the content height may have
// changeed since iniialization (e.g. for spoilers).
elem.css({ height: 'auto' });
var newOriginalSize = elem.outerHeight();
elem.css({ height: size.pixels + 'px' });
ips.utils.anim.go( 'fadeOutDown fast', expander )
.done( function () {
expander.remove();
});
if( newOriginalSize > size.pixels ){
$( elem ).animate( { height: newOriginalSize + 'px' }, function () {
$( this ).css( {
'position': originalPos,
'height': 'auto'
} );
$( elem ).trigger( 'truncateExpanded' );
});
}
});
},
/**
* 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 parseInt( 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="103021" 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";
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: ''
};
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') );
}
}
},
/**
* 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;
};
ips.ui.registerWidget('uploader', ips.ui.uploader, [
'multiple', 'allowedFileTypes', 'maxFileSize', 'maxTotalSize', 'maxChunkSize', 'action', 'name', 'button', 'key',
'maxFiles', 'dropTarget', 'listContainer', 'autoStart', 'insertable', 'template', 'existingFiles', 'postkey'
] );
return {
respond: respond,
getObj: getObj
};
});
/**
* 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,
listContainer,
uploadCount = 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;
}
// 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'
});
}
});
},
/**
* 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 ){
listContainer.append( ips.templates.render( options.template, data ) );
}
},
/**
* 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
};
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');
}
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( null, data.file ) );
injectIds[pluploadFile.id] = data.data;
pluploadObj.addFile( pluploadFile );
} )
.on( 'resetUploader', function ( data ){
_resetUploader( e, data );
} )
},
/**
* Resets this uploader instance, clearing it of files
*
* @returns {void}
*/
_resetUploader = function (data) {
// Update upload count
uploadCount = 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...');
_preloadNotificationSound();
pluploadObj.start();
},
/**
* Preload a notification sound that we may use to indicate success
*
* @returns {void}
*/
_preloadNotificationSound = function () {
ips.loader.get( ['core/interface/howler/howler.core.min.js'] ).then( function () {
sound = new Howl({
src: ips.getSetting('baseURL') + 'applications/core/interface/sounds/success.mp3',
autoplay: false
});
});
},
/**
* 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 );
_updateStatus( fileElem, file.status );
_removeStatusClasses( fileElem );
_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) );
},
/**
* 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 ){
ips.utils.anim.go( 'fadeOut fast', fileElem.find('.ipsAttachment_progress') );
}
},
/**
* 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 );
},
/**
* 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--;
_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 );
},
/**
* 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 && up.total.size > ( 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 badType = 0;
var allowedFileTypes = ( options.allowedFileTypes !== null ) ? $.parseJSON( options.allowedFileTypes ).join(',').toLowerCase().split(',') : '';
_.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;
}
// 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;
// Figure out if this is an video or image based on extension
if( [ 'jpg', 'jpeg', 'jpe', 'gif', 'png' ].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;
}
var data = {
uploaderID: uploaderID,
id: file.id,
title: file.name,
mime: file.type,
size: 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
};
// 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( 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.supported ){
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++;
var fileElem = _updateFileElement( file );
fileElem.addClass('ipsAttach_done');
if( options.insertable ){
ips.utils.anim.go('fadeIn', fileElem.find('[data-role="insert"]') );
}
ips.utils.anim.go('fadeIn', fileElem.find('[data-role="deleteFileWrapper"]') );
// 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'],
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 ){
errorCounts[ ( err.code == -600 ) ? 'size' : 'ext' ]++;
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
} );
}
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: errorMessage,
callbacks: {}
});
}
};
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="103021" 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="103021" 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();
ips.getAjax()( target.attr('action'), {
data: target.serialize(),
type: 'post'
}).done( function (response) {
// 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 responseWizard = $( '<div>' + response + '</div>' ).find('[data-ipsWizard]').html();
if ( !responseWizard ) {
responseWizard = response;
}
ips.controller.cleanContentsOf( elem );
elem.html( responseWizard );
$( document ).trigger( 'contentChange', [ elem ] );
elem.trigger( 'wizardStepChanged' );
})
.fail(function(response, textStatus, errorThrown){
target.attr( 'data-bypassAjax', true );
target.submit();
})
} else {
e.preventDefault();
ips.getAjax()( target.attr('href') ).done( function (response) {
// 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 responseWizard = $( '<div>' + response + '</div>' ).find('[data-ipsWizard]').html();
if ( !responseWizard ) {
responseWizard = response;
}
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( !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="103021" javascript_position="1000400"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.utils.analytics.js - Analytics helper methods
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.createModule('ips.utils.analytics', function () {
/**
* Initialize analytics module
*
* @returns {void}
*/
var init = function () {
},
trackPageView = function (url) {
try {
if( ips.getSetting('analyticsProvider') == 'ga' ){
if( !_.isUndefined( window.ga ) ){
var urlObj = ips.utils.url.getURIObject( url || document.location );
Debug.log("Manual page view tracked with Google Analytics: " + urlObj.relative);
ga('send', 'pageview', urlObj.relative);
}
} else if( ips.getSetting('analyticsProvider') == 'piwik' ){
if( !_isUndefined( window._paq ) ){
Debug.log("Manual page view tracked with Piwik");
_paq.push(['trackPageView']);
}
} else if( ips.getSetting('analyticsProvider') == 'custom' && _.isFunction( ips.getSetting('paginateCode') ) ){
ips.getSetting('paginateCode').call(url);
}
} catch (err) {
Debug.log("Error tracking page view: " + 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="103021" 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;
}
}
},
/**
* 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="103021" 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) {
return parseInt( hex,16 );
},
/**
* 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="103021" 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 ){
expires = "; expires=Wed, 1 Jan 2020 00:00:00 GMT";
} 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[ $.trim( pair[0] ) ] = $.trim( unescape( pair[1] ) );
}
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="103021" 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="103021" 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('adsess') ) || ( _.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="103021" 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}', '<span data-content-marker="true"></span>' ).replace( '{content}', '<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}', '<span data-content-marker="true"></span>' ).replace( '{content}', '<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="103021" 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,
_allEmoji = null,
_testingCanvasContext = null,
_ajax = null,
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 ) {
callback(this._emoji);
return;
}
var storage = ips.utils.db.get( 'emoji', ips.getSetting('baseURL') + '-' + ips.getSetting('emoji_cache') );
if ( storage ) {
this._emoji = storage;
callback( storage );
} else {
ips.utils.db.removeByType('emoji');
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._allemoji = emoji;
var canUseTones = null;
for ( var category in emoji ) {
this._emoji[category] = [];
for ( var i = 0; i < emoji[category].length; i++ ) {
if ( this.canRender( emoji[category][i].code ) ) {
if( canUseTones !== null )
{
emoji[category][i].skinTone = canUseTones;
}
else if ( emoji[category][i].skinTone ) {
if( !this.canRender( this.tonedCode( emoji[category][i].code, 'light' ) ) )
{
canUseTones = false;
}
else
{
canUseTones = true;
}
emoji[category][i].skinTone = canUseTones;
}
// Add translated name too
emoji[category][i].shortNames.push( ips.getString('emoji-' + emoji[category][i].name ) );
this._emoji[category].push( emoji[category][i] );
}
}
}
ips.utils.db.set( 'emoji', ips.getSetting('baseURL') + '-' + ips.getSetting('emoji_cache'), this._emoji );
callback(this._emoji);
}.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 this.emojiImage( code );
}
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 ) {
if ( codeToUse.substr( 0, 7 ) == 'custom-' ) {
var store = this._allemoji || this._emoji;
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( store[ parts[1] ] ) )
{
return null;
}
for ( var i = 0; i < store[ parts[1] ].length; i++ ) {
if( store[ parts[1] ][i].code == codeToUse ) {
var imgTag = '<img src="' + store[ parts[1] ][i].image + '" title="' + store[ parts[1] ][i].name + '" alt="' + store[ parts[1] ][i].name + '"';
if( store[ parts[1] ][i].image2x )
{
imgTag += ' srcset="' + store[ parts[1] ][i].image2x + ' 2x"';
}
if( parseInt( store[ parts[1] ][i].width ) && parseInt( store[ parts[1] ][i].height ) )
{
imgTag += ' width="' + store[ parts[1] ][i].width + '" height="' + store[ parts[1] ][i].height + '"';
}
imgTag += ' data-emoticon="true">';
return imgTag;
}
}
return null;
} else {
var url;
var image = codeToUse.toLowerCase();
if ( ips.getSetting('emoji_style') == 'emojione' ) {
image = image.replace( /\-200d/g, '' );
image = image.replace( /\-fe0f/g, '' );
url = "https://cdn.jsdelivr.net/emojione/assets/3.1/png/64/" + image + ".png";
} else {
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://twemoji.maxcdn.com/2/72x72/" + image + ".png";
}
var character = this.emojiFromHex( codeToUse );
return '<img src="' + url + '" alt="' + character + '" 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 );
}
},
/**
* 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 ) );
}
},
/**
* 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="103021" 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="103021" 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.js" javascript_type="framework" javascript_version="103021" 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
} );
}
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, "\\$&" );
};
return {
argsToArray: argsToArray,
uppercaseFirst: uppercaseFirst,
getIDsFromList: getIDsFromList,
getCitation: getCitation,
escapeRegexp: escapeRegexp
};
});
}(jQuery,_));]]></file>
<file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.notification.js" javascript_type="framework" javascript_version="103021" 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 supported = ( "Notification" in window );
var sound = null;
// Preload our notification sound
if( !ips.getSetting('disableNotificationSounds') ){
sound = new Howl({
src: ips.getSetting('baseURL') + 'applications/core/interface/sounds/notification.mp3',
autoplay: false
});
}
/**
* Determines whether the user has granted permission for notifications
*
* @returns {boolean} Whether notifications have permission
*/
var hasPermission = function () {
if( !supported || Notification.permission == 'denied' || Notification.permission == 'default' ){
return false;
}
return true;
},
/**
* 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 && Notification.permission == 'default' ){
return true;
}
return false;
},
/**
* Returns the granted permission level
*
* @returns {mixed}
*/
permissionLevel = function () {
if( !supported ){
return null;
}
return Notification.permission;
},
/**
* 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');
}
});
}
},
/**
* Creates a new notification and returns a notification object
*
* @param {object} options Configuration object
* @returns {function}
*/
create = function (options) {
return new notification( options );
},
/**
* Plays the notification sound
*
* @returns {function}
*/
playSound = function () {
try {
if( !ips.getSetting('disableNotificationSounds') ){
sound.play();
}
} catch (err) { }
};
/**
* 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( /'/g, "'" ).replace( /<[^>]*>?/g, '' ) );
this._options.title = _.unescape( this._options.title.replace( /'/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 );
};
};
return {
supported: supported,
hasPermission: hasPermission,
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="103021" 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: 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 + targetDims.outerWidth - offset.left ) > theWindow.width() ){
position.horizontal = 'right';
}
} else if ( position.horizontal == 'right' ) {
if( ( viewportOffset.right - 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="103021" 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 ).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="103021" 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 '';
};
/**
* 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
};
});
}(jQuery, _));]]></file>
<file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.time.js" javascript_type="framework" javascript_version="103021" 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 ( 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 ) );
} else {
// 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.getMonth() + '_short' );
},
"A": function(d) { // A full textual representation of the day
return ips.getString( 'day_' + d.getMonth() );
},
"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="103021" 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 ];
},
/**
* 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 );
var qsValues = _.pick( uriObject.queryKey, function( value, key ) {
return ( jQuery.inArray( key, name ) == -1 );
} );
var returnUrl = uriObject.protocol + '://' + uriObject.host + ( ( uriObject.port !== '' ) ? ':' + uriObject.port : '' ) + uriObject.path;
if( _.keys( qsValues ).length )
{
var qsParam = '?';
_.each( qsValues, 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) {
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 {
getParam: getParam,
removeParam: removeParam,
removeParams: removeParams,
getURIObject: getURIObject,
getOrigin: getOrigin
};
});
}(jQuery, _));]]></file>
<file javascript_app="global" javascript_location="framework" javascript_path="common/utils" javascript_name="ips.utils.validate.js" javascript_type="framework" javascript_version="103021" 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/attachments" javascript_name="ips.attachments.list.js" javascript_type="controller" javascript_version="103021" javascript_position="1000700"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.attachments.list.js - My-Attachments controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.attachments.list', {
initialize: function () {
this.on( 'click', '[data-action="deleteAttachment"]', this.deleteAttachment );
},
/**
* Removes the attachment row
*
* @param e
*/
deleteAttachment: function (e) {
e.preventDefault();
var elem = $( e.currentTarget );
// Confirm it
ips.ui.alert.show( {
type: 'confirm',
icon: 'question',
message: ips.getString('delete'),
subText: ips.getString('delete_confirm'),
callbacks: {
ok: function () {
ips.getAjax()( elem.attr('href') + '&wasConfirmed=1' )
.done( function (response) {
var row = elem.closest('div.ipsDataItem');
row.remove();
ips.utils.anim.go( 'fadeOut', row );
});
}
}
});
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/clubs" javascript_name="ips.clubs.requests.js" javascript_type="controller" javascript_version="103021" javascript_position="1000150"><![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,
left: elemPosition.absPos.left,
width: dims.outerWidth,
height: dims.outerHeight
});
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.acpSearchKeywords.js" javascript_type="controller" javascript_version="103021" javascript_position="1000200">/**
* 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){
"use strict";
ips.controller.register('core.admin.core.acpSearchKeywords', {
initialize: function () {
this.on( 'click', '[data-action="save"]', 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( "[data-role='lang_key']" ).val(),
restriction: scope.find( "[data-role='restriction']" ).val(),
keywords: []
};
scope.find( "[data-role='keywords']" ).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="103021" javascript_position="1000200"><![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 () {
// 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') + '&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]');
}
// Trigger ajax request to actually delete
ips.getAjax()( elem.attr('href'), {
showLoading: true,
bypassRedirect: true,
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') + '&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="global" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.dynamicChart.js" javascript_type="controller" javascript_version="103021" javascript_position="1000200"><![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( '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');
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') );
});
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 );
});
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
});
// 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();
});
});
},
/**
* 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');
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
})
.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 );
},
/**
* 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
pieces.push( "type[" + this.identifier + "]=" + this.storedValues.type );
// Filters
_.each( this.storedValues.filters, function (value, idx ) {
pieces.push( "filters[" + self.identifier + "][" + idx + "]=" + value );
});
// Dates
if( this.storedValues.dates.start || this.storedValues.dates.start ){
pieces.push( "start[" + this.identifier + "]=" + this.storedValues.dates.start );
pieces.push( "end[" + this.identifier + "]=" + this.storedValues.dates.end );
}
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="103021" javascript_position="1000200"><![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="103021" javascript_position="1000200"><![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="103021" javascript_position="1000200"><![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="core" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.licenseRenewal.js" javascript_type="controller" javascript_version="103021" javascript_position="1000200">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.core.licenseRenewal.js - License Renewal message
*
* Author: Stuart Silvester
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.core.licenseRenewal', {
initialize: function () {
this.on( 'click', '', this.renewalPrompt );
this.on(document, 'click', '[data-action="closeLicenseRenewal"]', this.close);
},
renewalPrompt: function(e) {
// Don't show modal if 'renew now' is clicked
if( $(e.target).is('.elRenewLink') )
{
return;
}
this._modal = ips.ui.getModal();
var container = ips.templates.render('licenseRenewal.wrapper');
$('body').append( container );
this._container = $('body').find('[data-role="licenseRenewal"]').css({ opacity: 0.001, transform: "scale(0.8)" });
// Set the survey URL
$('body').find( '[data-action="survey"]').attr( 'href', $('#elLicenseRenewal').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: 1,
transform: "scale(1)"
}, 'fast');
}, 500);
ips.utils.anim.go('fadeIn', this._modal);
},
/**
* Close the popup
*
* @returns {void}
*/
close: function (e) {
if( e ){
e.preventDefault();
}
if( this._container.find( 'input[type=checkbox][name=hideRenewalNotice]' ).is(':checked') )
{
$('#elLicenseRenewal').slideUp();
var date = new Date();
date.setTime( date.getTime() + ( 16 * 86400000 ) );
ips.utils.cookie.set( 'licenseRenewDismiss', true, date.toUTCString() );
}
this._container.animate({
transform: "scale(0.7)",
opacity: 0
}, 'fast');
ips.utils.anim.go('fadeOut', this._modal);
}
});
}(jQuery, _));</file>
<file javascript_app="global" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.liveSearch.js" javascript_type="controller" javascript_version="103021" javascript_position="1000200"><![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 = $.trim( this._textField.val() );
if( currentValue == this._lastValue || $.trim( this._textField.val().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 ipsPad_top').html(' ');
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 = $.trim( this._lastValue );
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: encodeURI( value )
}
}).done( function (response) {
self._results[key] = response;
$( tab )
.find('[data-role="resultCount"]')
.removeClass('ipsLoading ipsPad_top 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="103021" javascript_position="1000200">/**
* 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.core.mobileNav', {
initialize: function () {
this.on( 'click', '[data-action="mobileSearch"]', 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="103021" javascript_position="1000200"><![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="103021" javascript_position="1000200"><![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;
});*/
if( $( this.scope ).next().hasClass('ipsSelectTree') )
{
var url = $(this.scope).attr('data-baseLink') + '&value=' + $(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 url = $(this.scope).attr('data-baseLink') + '&value=' + vals;
}
var dialogRef = ips.ui.dialog.create({
title: $(this.scope).attr('_title'),
url: url,
forceReload: true,
fixed: false
});
dialogRef.show();
}
});
}(jQuery, _));]]></file>
<file javascript_app="global" javascript_location="admin" javascript_path="controllers/core" javascript_name="ips.core.pageActions.js" javascript_type="controller" javascript_version="103021" javascript_position="1000200"><![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="admin" javascript_path="controllers/core" javascript_name="ips.core.viglink.js" javascript_type="controller" javascript_version="103021" javascript_position="1000200">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.core.viglink.js - Controller to launch VigLink "convert account" window
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.core.viglink', {
initialize: function () {
this.on( 'submit', this._submitForm );
},
/**
* Event handler for when form is submitted
*
* @param {event} e Event object
* @returns {void}
*/
_submitForm: function (e) {
if( $(e.currentTarget).find('input[name="viglink_account_type"]:checked').val() == 'create' ) {
window.open( $(e.currentTarget).attr('data-viglinkUrl'), 'viglink', 'height=980,width=780' );
}
}
});
}(jQuery, _));
</file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.announcementBanner.js" javascript_type="controller" javascript_version="103021" javascript_position="1000200">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.core.announcementBanner.js - Announcement Banners
*
* Author: Stuart Silvester
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.announcementBanner', {
/**
* Initialise event handlers
*
* @returns {void}
*/
initialize: function () {
this.setup();
this.on( 'click', '[data-role="dismissAnnouncement"]', this.dismissAnnouncement );
this.on( document, 'announcements.reflow', this.reflow );
this.on( window, 'resize', this.reflow );
},
/**
* Set up CSS for announcements
*
* @returns {void}
*/
setup: function()
{
$('.cAnnouncements').addClass( 'cAnnouncementsFloat' ).css( 'zIndex', ips.ui.zIndex() );
this.reflow();
},
/**
* 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();
$(document).trigger('announcements.reflow' );
},
progress: function() {
$(document).trigger('announcements.reflow' );
}
});
},
/**
* Adjust the body margin
*
* @param {event} e Event object
* @returns {void}
*/
reflow: function( e )
{
var height = $('.cAnnouncements').outerHeight();
$( 'body' ).css( 'margin-top', height + 'px' );
}
});
}(jQuery, _));</file>
<file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.app.js" javascript_type="controller" javascript_version="103021" javascript_position="1000100"><![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._linkRemoteImages );
this.setup();
},
/**
* Setup method for the front app
*
* @returns {void}
*/
setup: function () {
// 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
if( typeof jstz !== 'undefined' ) {
ips.utils.cookie.set( 'ipsTimezone', jstz.determine().name() );
}
// 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
if( ips.getSetting('links_external') ) {
this.scope.find('a[rel*="external"]').each( function( index, elem ){
elem.target = "_blank";
elem.rel = elem.rel + " noopener";
})
}
// Find any embeds on the page and upgrade them
this._upgradeOldEmbeds( this.scope );
// Set up prettyprint
prettyPrint();
// Set up notifications
if( ips.getSetting('memberID') && ips.utils.notification.supported && ips.utils.notification.needsPermission() ){
ips.utils.notification.requestPermission();
}
// 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();
}
// Add links to image proxy images
this._linkRemoteImages();
},
/**
* 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) {
// Apply embed controllers on old content
var oldEmbeds = element.find('[data-embedcontent]:not([data-controller])');
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';
}
}
});
},
/**
* Prepare image proxy images for lightboxing
*
* @returns {void}
*/
_linkRemoteImages: function () {
this.scope
.find('img[data-imageproxy-source]')
.replaceWith( function(){
var image = $(this);
/* If this image is already linked, we don't want to overwrite it */
if( image.closest('a').length )
{
return image;
}
var target = ips.getSetting('links_external') ? "target='_blank' " : '';
var rel = ips.getSetting('links_external') ? "rel='noopener' " : '';
return image.wrap( "<a href='" + image.data( 'imageproxy-source' ) + "' " + target + rel + "></a>");
} );
}
});
}(jQuery, _));
]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.articlePages.js" javascript_type="controller" javascript_version="103021" javascript_position="1000200"><![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="103021" javascript_position="1000100"><![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';
iframe.scrolling = "no";
iframe.seamless = "seamless";
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 ) || parseInt( this._lastDims.width ) !== parseInt( data.width ) ){
this.scope.css({
height: parseInt( data.height ) + this._border.vertical + 'px',
maxWidth: parseInt( data.width ) + this._border.horizontal + 'px'
});
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="103021" javascript_position="1000100"><![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( 'menuOpened', '[data-role="shareComment"]', this.shareCommentMenu );
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() );
if( $.trim( selectedText ) == '' ){
this._hideQuoteTooltip();
return;
}
// 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.
if( !selectedText.startsWith('<') ){
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;
}
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}
*/
shareCommentMenu: function (e, data) {
if( data.menu ){
data.menu.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();
}
}
});
},
/**
* 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 = $.trim(CKEDITOR.instances['comment_value'].editable().getData().replace(/ /g, ''));
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-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('initializeImages');
this.scope.find('[data-action="expandTruncate"], [data-role="commentControls"]').show();
// Open external links in a new window
if( ips.getSetting('links_external') ) {
this.scope.find('a[rel*="external"]').each( function( index, elem ){
elem.target = "_blank";
})
}
$( 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="103021" javascript_position="1000100"><![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',
_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.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._boundMQ );
$( document ).on( 'click', '[data-action="clearQuoted"]', 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 );
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;
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._boundMQ );
$( document ).off( 'click', '[data-action="clearQuoted"]', 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.getParam( 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();
// Open external links in a new window
if( ips.getSetting('links_external') ) {
this.scope.find('a[rel*="external"]').each( function( index, elem ){
elem.target = "_blank";
})
}
// If the 'lastpage' flag is set, see if we're still on the last page and if not, remove the flag.
// This is needed in case the user hits back in their browser
if( this.scope.attr( 'data-lastPage' ) )
{
var currentPageNo = this._urlParams[ this._pageParam ];
var lastPageNo = this.scope.find('li.ipsPagination_last > a').first().attr('data-page');
if( currentPageNo != lastPageNo )
{
this.scope.removeAttr( 'data-lastPage' );
}
}
// 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;
}
// If we're now on the last page, start polling again
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 );
}
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 ) ){
_.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 {
pageAction.trigger('addManualItem.pageAction', {
id: 'multimod[' + key + ']',
actions: val
});
}
});
}
},
/**
* 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) {
var editor = ips.ui.editor.getObj( this.scope.find('[data-role="replyArea"] [data-ipsEditor]') );
if( 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();
}
}
},
/**
* 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._playNotification();
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 ipsPad_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.supported ){
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();
},
/**
* Play our notification sound
*
* @param {event} e Event object
* @returns {void}
*/
_playNotification: function () {
ips.utils.notification.playSound();
},
/**
* 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.getParam( this._pageParam );
if( !page ){
page = 1;
}
ips.getAjax()( form.attr('action'), {
data: form.serialize() + '¤tPage=' + 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();
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 {
self.trigger( 'addToCommentFeed', {
content: response.content,
totalItems: response.total,
feedID: self._commentFeedID,
scrollToItem: true
});
if ( response.message ) {
ips.ui.flashMsg.show( response.message );
}
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 }) );
// Do we need to syntax highlight anything?
if( comment.find('pre.prettyprint').length ){
comment.find('pre.prettyprint').each( function () {
$( this ).html( window.PR.prettyPrintOne( _.escape( $( this ).text() ) ) );
});
}
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 );
};
// 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);
}
// Open external links in a new window
if( ips.getSetting('links_external') ) {
this.scope.find('a[rel*="external"]').each( function( index, elem ){
elem.target = "_blank";
})
}
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 editor = ips.ui.editor.getObj( this.scope.find('[data-role="replyArea"] [data-ipsEditor]') );
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 );
});
if( 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 ] )
})
}));
ips.utils.anim.go( 'zoomIn fast', $('#ipsMultiQuoter') );
} else {
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="103021" javascript_position="1000100">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.core.commentWrapper.js
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
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) {
this._updateCount( $(e.target).closest('[data-commentsType]').attr('data-commentsType'), data.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.followButton.js" javascript_type="controller" javascript_version="103021" javascript_position="1000200"><![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="103021" javascript_position="1000200">/**
* 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.followForm', {
initialize: function () {
this.on( 'submit', this.submitForm );
this.on( 'click', '[data-action="unfollow"]', 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="follow_type"]:checked').val(),
anonymous: !self.scope.find('[name="follow_public_checkbox"]').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="103021" javascript_position="1000200">/**
* 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){
"use strict";
ips.controller.register('core.front.core.guestTerms', {
initialize: function () {
this.on( 'click', '[data-action="dismissTerms"]', 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: 0
}, '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="103021" javascript_position="1000100"><![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="103021" javascript_position="1000100"><![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 ){
ips.utils.notification.playSound();
// 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 = $.trim( document.title.replace( /^\(\d+\)/, '' ) );
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() ] && ips.utils.notification.supported ){
// When the window is INACTIVE (and we support HTML5 notifications)
// Show a browser notification & play a sound
this._showBrowserPopup( notifyData, type );
} else {
// When the window is ACTIVE
// Show a flash message
this._showFlashMessage( notifyData, type );
}
},
/**
* Shows a HTML5 browser popup notification
*
* @param {object} notifyData Object containing notification data
* @param {string} type Type of notification (notification or message)
* @returns {void}
*/
_showBrowserPopup: function (notifyData, type) {
notifyData = _.extend( notifyData, {
timeout: 15
});
// Try and hide any existing popup for this type (even though it'll be on a timeout)
if( this._browserNotifications[ type ] ){
try {
this._browserNotifications[ type ].hide();
} catch (err) {}
}
// Create the new one
this._browserNotifications[ type ] = ips.utils.notification.create( notifyData );
this._browserNotifications[ type ].show();
},
/**
* 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 ipsPad_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="103021" javascript_position="1000100"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.core.lightboxedImages.js - Sets up lightbox on user-posted content
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.lightboxedImages', {
_random: null,
initialize: function () {
this.on( 'initializeImages', this.initializeImages );
this.setup();
},
/**
* Setup method
*
* @returns {void}
*/
setup: function () {
this._random = 'g' + ( Math.round( Math.random() * 100000 ) );
this._initializeImages();
},
/**
* Event handler for main event
*
* @returns {void}
*/
initializeImages: function () {
this._initializeImages();
},
/**
* Initializes images by checking if their full size is larger than shown, and wrapping them
* with the lightbox ui widget if so.
*
* @returns {void}
*/
_initializeImages: function () {
var self = this;
var images = this.scope.find('img');
if( !images.length ){
return;
}
this.scope.imagesLoaded( function (imagesLoaded) {
if( !imagesLoaded.images.length ){
return;
}
_.each( imagesLoaded.images, function (image, i) {
var image = image.img;
if( !_.isUndefined( $( image ).attr('data-emoticon') ) || $( image ).hasClass('ipsEmoji') || image.width >= image.naturalWidth && !$( image ).hasClass('ipsImage_thumbnailed') ) {
return;
}
image = $( image );
image.addClass('ipsImage_thumbnailed');
// If the image is already inside an a, then just add the lightbox params; otherwise, wrap in new <a>
if( image.closest('a').length && image.closest('a').hasClass('ipsAttachLink') && image.closest('a').hasClass('ipsAttachLink_image') ){
if ( [ 'gif', 'jpeg', 'jpe', 'jpg', 'png' ].indexOf( image.closest('a').attr('href').substr( image.closest('a').attr('href').lastIndexOf('.') + 1 ).toLowerCase() ) != -1 ) { // Only if the link is to an image
image.closest('a')
.attr( 'data-fullURL',image.closest('a').attr('src') )
.attr( 'data-ipsLightbox', '' )
.attr( 'data-ipsLightbox-group', self._random );
}
} else {
if( !image.closest('a').length ){
image.wrap( $( "<a href='" + image.attr('src') + "' title='" + ips.getString('enlargeImage') + "' data-ipsLightbox data-ipsLightbox-group='" + self._random + "'></a>" ) );
}
}
});
});
}
});
}(jQuery, _));]]></file>
<file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.markRead.js" javascript_type="controller" javascript_version="103021" javascript_position="1000100">/**
* 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){
"use strict";
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="103021" javascript_position="1000100">/**
* 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){
"use strict";
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="103021" javascript_position="1000100"><![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="103021" javascript_position="1000200"><![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( inputNode.val() );
})
.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="103021" javascript_position="1000100"><![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;
}
Debug.log('available space: ' + availableSpace );
// 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"]').prepend( 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');
}
newDropdown.append( newMenuItem.append( $( this ).contents() ).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');
}
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="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.pagination.js" javascript_type="controller" javascript_version="103021" javascript_position="1000100">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.core.pagination.js - Pagination controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
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.anim.go('fadeIn', self.scope);
// Open external links in a new window
if( ips.getSetting('links_external') ) {
this.scope.find('a[rel*="external"]').each( function( index, elem ){
elem.target = "_blank";
})
}
})
.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="103021" javascript_position="1000200"><![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="103021" javascript_position="1000200"><![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');
this._showCounts = this.scope.attr('data-showCounts') === 'false' ? false : true;
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 ) ) {
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, data.choices[ i ].count ) );
}
} else if ( _.isObject( data.choices ) ) {
for( var i in data.choices ){
choices.push( this._getChoiceHTML( i, qid, data.choices[ i ].title, data.choices[ i ].count ) );
}
}
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,
showCounts: this._showCounts,
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, count) {
return ips.templates.render('core.pollEditor.choice', {
choiceID: cid,
questionID: qid,
pollName: this._name,
choiceTitle: name,
showCounts: this._showCounts,
hideCounts: !this._showCounts,
count: count
});
},
/**
* 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="103021" javascript_position="1000200"><![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="103021" javascript_position="1000100"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.core.quickSearch.js - Controller for search in header
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.quickSearch', {
_moreOptions: null,
initialize: function () {
this.on( 'focus', '#elSearchField', this.openSearch );
this.on( 'click', '[data-action="showMoreSearchContexts"]', this.showMoreSearchContexts );
this.setup();
},
/**
* Setup method
*
* @returns {void}
*/
setup: function () {
},
/**
* Event handler for when the search box loses focus
*
* @param {event} e Event object
* @returns {void}
*/
closeSearch: function (e, data) {
// This function returns the trigger element that was clicked on, if any
var clickedOnTrigger = function () {
if( $( e.target ).is('#elSearchExpanded') ){
Debug.log( e.target );
return $( e.target );
} else if ( $( e.target ).parent('#elSearchExpanded') ){
return $( e.target ).parent('#elSearchExpanded').get(0);
}
}();
if( clickedOnTrigger || $( e.target ).attr('href') == '#' || $( e.target ).is('label') || $( e.target ).attr('id') == 'elSearchField' || $.contains( $('#elSearchExpanded'), $( e.target ) ) ){
return true;
}
$( document ).off('click.elSearchExpanded');
ips.utils.anim.go('fadeOut fast', $('#elSearchExpanded'));
$('#elSearchWrapper').removeClass('cSearchExpanded');
},
/**
* Event handler for when the search box is clicked into
*
* @param {event} e Event object
* @returns {void}
*/
openSearch: function (e, data) {
if ( $('#elSearchExpanded').is(':visible') ) {
return;
}
ips.utils.anim.go('fadeIn fast', $('#elSearchExpanded'));
$('#elSearchWrapper').addClass('cSearchExpanded');
$( document ).on('click.elSearchExpanded', this.closeSearch );
if( this._moreOptions === null ){
var url = ips.getSetting('baseURL') + 'index.php?app=core&module=search&controller=search&do=globalFilterOptions&exclude=' + $('[data-action="showMoreSearchContexts"]').attr('data-exclude');
ips.getAjax()( url )
.done(function( response ){
this._moreOptions = response;
}.bind(this));
}
},
/**
* Event handler for the "More Options" lik being clicked
*
* @param {event} e Event object
* @returns {void}
*/
showMoreSearchContexts: function (e, data) {
e.stopPropagation();
e.preventDefault();
this.scope.find('[data-role="showMoreSearchContexts"]').replaceWith( this._moreOptions );
}
});
}(jQuery, _));]]></file>
<file javascript_app="global" javascript_location="front" javascript_path="controllers/core" javascript_name="ips.core.rating.js" javascript_type="controller" javascript_version="103021" javascript_position="1000100">/**
* 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){
"use strict";
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="103021" javascript_position="1000100"><![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();
}
this._ajaxObj = ips.getAjax()( url )
.done( function (response) {
self._updateReaction( response );
} )
.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="103021" javascript_position="1000100"><![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="103021" javascript_position="1000100"><![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="103021" javascript_position="1000200">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.core.reviewForm.js - Review form controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.reviewForm', {
initialize: function () {
this.on( 'click', '[data-action="writeReview"]', this.toggleReview );
},
toggleReview: function (e) {
e.preventDefault();
this.scope.find('[data-role="reviewIntro"]').hide();
this.scope.find('[data-role="reviewForm"]').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="103021" javascript_position="1000100"><![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="103021" javascript_position="1000100"><![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="103021" javascript_position="1000200"><![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="103021" javascript_position="1000200"><![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();
},
/**
* 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="103021" javascript_position="1000200"><![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,
_formLoaded: false,
_menuID: '',
_tagEditID: '',
initialize: function () {
this.on( document, 'menuOpened', this.menuOpened );
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 '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;
}
if( this._formLoaded ){
this._setLoading( true );
}
this._formLoaded = true;
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="103021" javascript_position="1000100"><![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="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.2fa.js" javascript_type="controller" javascript_version="103021" 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){
"use strict";
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="text"]: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="text"]: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="mfa_method"]').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="103021" 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){
"use strict";
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="onetouchCode"]').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="103021" 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. Calls this._positionImage when the cover photo is loaded
*
* @returns {void}
*/
setup: function () {
this._image = this.scope.find('.ipsCoverPhoto_photo');
this._image.css({ opacity: 0.0001 });
this._offset = this.scope.attr('data-coverOffset') || 0;
this._containerHeight = this.scope.outerHeight();
if( this._image.length ){
this._image.imagesLoaded( _.bind( this._positionImage, this ) );
}
// 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');
},
/**
* Event handler for the window resizing
*
* @returns {void}
*/
resizeWindow: function () {
if ( this._expandedCover )
{
this.toggleCoverPhoto();
}
this._positionImage();
},
/**
* 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 );
this._image
.css({
position: 'absolute',
left: 0,
top: ( adjustedOffset * -1 ) + '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', opacity: 0.01 }).animate({ opacity: 1 });
this._hideTooltip();
// Reset the URL so refreshing the page does not re-trigger repositioning
History.pushState( "", document.title, 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="103021" 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){
"use strict";
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="profilePhoto"]');
this._coords = {
topLeftX: this.scope.find('[data-role="topLeftX"]'),
topLeftY: this.scope.find('[data-role="topLeftY"]'),
bottomRightX: this.scope.find('[data-role="bottomRightX"]'),
bottomRightY: this.scope.find('[data-role="bottomRightY"]'),
};
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="cropper"]').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="103021" 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 () {
$(this.scope).text( ips.utils.time.formatTime( new Date( $(this.scope).attr('data-time') ), { format: $(this.scope).attr('data-format') } ) );
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.embeddedvideo.js" javascript_type="controller" javascript_version="103021" 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){
"use strict";
ips.controller.register('core.global.core.embeddedvideo', {
initialize: function () {
var video = $(this.scope).get(0);
var canPlay = false;
if ( !ips.getSetting('adsess') ) { // In the ACP assume we can't play videos because the munging will break external ones. @todo remove this once we fix that
$(this.scope).find('source').each(function(){
if ( video.canPlayType( $(this).attr('type') ) ) {
console.log(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="103021" 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){
"use strict";
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="103021" 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"]').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="103021" 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.license.js" javascript_type="controller" javascript_version="103021" 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){
"use strict";
ips.controller.register('core.global.core.license', {
initialize: function () {
this.on( 'click', '[data-role="closeMessage"]', this.hideMessage );
},
hideMessage: function () {
var date = new Date();
date.setTime( date.getTime() + ( 14 * 86400000 ) );
ips.utils.cookie.set( 'licenseDismiss', true, date.toUTCString() );
this.scope.slideUp();
}
});
}(jQuery, _));</file>
<file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.login.js" javascript_type="controller" javascript_version="103021" javascript_position="1000050"><![CDATA[/**
* IPS Social Suite 4
* (c) 2017 Invision Power Services - http://www.invisionpower.com
*
* ips.core.login.js - Login form controller
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.global.core.login', {
initialize: function () {
this.on( 'click', 'button[type="submit"]', this.buttonClick );
},
buttonClick: function (e, data) {
if ( $(e.currentTarget).attr('name') ) {
$(e.currentTarget).closest('form').append(
$("<input type='hidden'>").attr({
name: $(e.currentTarget).attr('name'),
value: $(e.currentTarget).attr('value')
})
);
}
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="global" javascript_path="controllers/core" javascript_name="ips.core.multipleRedirect.js" javascript_type="controller" javascript_version="103021" 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') );
});
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-role="message"]').html( response[1] );
if ( response[2] ) {
elem.find('[data-role="progressBarContainer"]').removeClass('ipsHide');
elem.find('[data-role="loadingIcon"]').addClass('ipsHide');
elem.find('[data-role="progressBar"]').css({ width: ( response[2] + '%' ) });
} else {
elem.find('[data-role="progressBarContainer"]').addClass('ipsHide');
elem.find('[data-role="loadingIcon"]').removeClass('ipsHide');
}
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.optionalAutocomplete.js" javascript_type="controller" javascript_version="103021" 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') );
// 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();
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="103021" 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="103021" 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,
_doneInitialState: false,
_initialURL: '',
_ajax: null,
initialize: function () {
this.on( 'paginationClicked paginationJump', this.paginationClicked );
this.on( 'refreshResults', this._getResults );
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._currentPage = ips.utils.url.getParam( 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 );
},
/**
* 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;
}*/
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 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();
}
},
/**
* Refresh table contents
*
* @returns {void}
*/
buttonAction: function () {
this._getResults();
},
/**
* Update the current URL
*
* @param {object} newParams New values to use in the search
* @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, bypassState: true } ),
document.title,
document.location
);
this._doneInitialState = true;
}*/
_.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 );
}
//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.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 () {
var self = this;
var urlBits = this._getURL();
try {
if( this._ajax && _.isFunction( this._ajax.abort ) ){
this._ajax.abort();
this._ajax = null;
}
} catch(err) {
}
if( _.isUndefined( this.scope.attr('data-resort') ) )
{
return;
}
this._ajax = ips.getAjax()( ( this._baseURL + ( ( urlBits ) ? this._getURL() + '&' : '' ) + this.scope.attr('data-resort') + '=1' ).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
pagination.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 ] );
},
/**
* 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="103021" 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){
"use strict";
ips.controller.register('core.global.core.updateBanner', {
initialize: function () {
this.on( 'click', '[data-role="closeMessage"]', 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="103021" 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="103021" javascript_position="1000250"><![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 = elem.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="103021" javascript_position="1000250"><![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');
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="103021" javascript_position="1000250"><![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="103021" 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) {
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 ) {
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="103021" javascript_position="1000300">/**
* 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){
"use strict";
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="notesInfo"]').hide();
this.scope.find('[data-role="notesLoading"]').removeClass('ipsHide');
ips.getAjax()( url, { type: 'post', data: $('#admin_notes').serialize() } )
.done( function (response) {
self.scope.find('[data-role="notesInfo"]').html( response );
})
.fail( function () {
})
.always( function () {
self.scope.find('[data-role="notesInfo"]').show();
self.scope.find('[data-role="notesLoading"]').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="103021" javascript_position="1000300"><![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', '.ipsMessage_close', this.closeWarning );
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 warning button
*
* @param {event} e Event object
* @returns {void}
*/
closeWarning: function (e) {
var self = this;
var warning = $( e.currentTarget ).closest('.ipsMessage');
// Get widget info
var key = warning.attr('data-warningKey');
warning.animate({ height: 0 });
ips.utils.anim.go( 'zoomOut fast', warning );
var _ids = [];
if( ! _.isUndefined( ips.utils.cookie.get('acpWarnings_mute') ) ) {
_ids = ips.utils.cookie.get('acpWarnings_mute').split(',');
}
_ids.push( key );
/* Set date for a week */
var date = new Date();
date.setTime( date.getTime() + ( 7 * 86400000 ) );
ips.utils.cookie.set('acpWarnings_mute', _ids.join(','), date.toUTCString());
},
/**
* 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.newFeatures.js" javascript_type="controller" javascript_version="103021" javascript_position="1000300"><![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.newFeatures', {
_modal: null,
_container: null,
_cards: null,
_dots: null,
_next: null,
_prev: null,
_data: [],
_active: null,
_popupActive: false,
initialize: function () {
this.on(document, 'click', '[data-action="nextFeature"]', this.next);
this.on(document, 'click', '[data-action="prevFeature"]', this.prev);
this.on(document, 'click', '[data-role="dotFeature"]', this.dot);
this.on(document, 'click', '[data-action="closeNewFeature"]', this.close);
this.on(document, 'keydown', this.keydown);
this.setup();
},
/**
* Setup method
*
* @returns {void}
*/
setup: function () {
if( !this.scope.attr('data-newFeatures') ){
// No new features; controller shouldn't be called in that situation, but just in case.
return;
}
try {
var data = $.parseJSON( this.scope.attr('data-newFeatures') );
this._data = data.slice(0,9); // Take 10 items
} catch (err) {
Debug.error( err );
}
this._buildPopup();
this._showPopup();
},
/**
* Keydown handler
*
* @returns {void}
*/
keydown: function (e) {
if( !this._popupActive ){
return;
}
switch( event.which ){
case 27: // Esc
this.close();
break;
case 39: // ->
this.next();
break;
case 37: // <-
this.prev();
break;
}
},
/**
* Close the popup
*
* @returns {void}
*/
close: function (e) {
if( e ){
e.preventDefault();
}
this._popupActive = false;
this._container.animate({
transform: "scale(0.7)",
opacity: 0
}, 'fast');
ips.utils.anim.go('fadeOut', this._modal);
},
/**
* Handles animating from one card to another in a given direction
*
* @returns {void}
*/
_animate: function (from, to, dir) {
// Set up position of next, and show it (invisible though)
to.css({
transform: dir == 'forward' ? "translateX(100px)" : "translateX(-100px)",
opacity: 0
}).show();
// Is there another next card available? If not, we need to hide the next button now
this._next.toggle( !!to.next().length );
this._prev.toggle( !!to.prev().length );
// Move current card out
from.stop(true, true).animate({
transform: dir == 'forward' ? "translateX(-100px)" : "translateX(100px)",
opacity: 0
}, function () {
from.hide()
});
// Move new card in
to.stop(true, true).animate({
transform: "translateX(0)",
opacity: 1
});
this._active = this._cards.index( to );
// Highlight dot
this._dots.removeClass('acpNewFeature_active');
this._dots.eq( this._active ).addClass('acpNewFeature_active');
},
/**
* Event handler for clicking on one of the dots
*
* @returns {void}
*/
dot: function (e) {
if( e ){
e.preventDefault();
}
var dot = $( e.currentTarget ).parent();
var idx = this._dots.index( dot );
Debug.log( 'idx: ' + idx );
var current = this._cards.eq( this._active );
var next = this._cards.eq( idx );
var dir = 'forward';
if( idx < this._active ){
dir = 'backward';
}
if( idx == this._active ){
return;
}
this._animate( current, next, dir );
},
/**
* Event handler for clicking Next
*
* @returns {void}
*/
next: function (e) {
if( e ){
e.preventDefault();
}
var current = this._cards.eq( this._active );
var next = current.next();
if( !next.length ){
Debug.log('no next');
return;
}
this._animate( current, next, 'forward' );
},
/**
* Event handler for clicking Prev
*
* @returns {void}
*/
prev: function (e) {
if( e ){
e.preventDefault();
}
var current = this._cards.eq( this._active );
var prev = current.prev();
if( !prev.length ){
Debug.log('no prev');
return;
}
this._animate( current, prev, 'backward' );
},
/**
* Builds the popup
*
* @returns {void}
*/
_buildPopup: function () {
var cards = [];
var dots = [];
this._modal = ips.ui.getModal();
_.each( this._data, function (item) {
cards.push( ips.templates.render('newFeatures.card', {
title: item.title,
image: item.image,
description: item.description,
moreInfo: item.more_info || false
}));
});
for( var i = 0; i < cards.length; i++ ){
dots.push( ips.templates.render('newFeatures.dot', {
i: i
}));
}
var container = ips.templates.render('newFeatures.wrapper', {
cards: cards.join(''),
dots: dots.join('')
});
$('body').append( container );
this._container = $('body').find('[data-role="newFeatures"]').css({ opacity: 0.001, transform: "scale(0.8)" });
this._cards = this._container.find('[data-role="card"]');
this._dots = this._container.find('[data-role="dots"] [data-role="dot"]');
this._next = this._container.find('[data-action="nextFeature"]').hide();
this._prev = this._container.find('[data-action="prevFeature"]').hide();
$( document ).trigger('contentChange', [this._container]);
this._modal.on( 'click', this._closeModal.bind( this ) );
},
/**
* Close the modal upon clicking
*
* @returns {void}
*/
_closeModal: function (e) {
e.preventDefault();
this.close();
},
/**
* Shows the popup
*
* @returns {void}
*/
_showPopup: function () {
var self = this;
// Hide all except the first item
this._cards.not(':first').hide();
this._active = 0;
// Mark the first dot
this._dots.first().addClass('acpNewFeature_active');
if( this._data.length > 1 ){
this._next.show();
}
this._modal.css( { zIndex: ips.ui.zIndex() } );
ips.utils.anim.go('fadeIn', this._modal);
// Style the title so we can animate it in momentarily
var title = self._container.find('[data-role="mainTitle"]');
title.css({
transform: "scale(1.2)",
opacity: 0
});
this._popupActive = true;
// Animate the card in
setTimeout( function () {
self._container.css( { zIndex: ips.ui.zIndex() } );
self._container.animate({
opacity: 1,
transform: "scale(1)"
}, 'fast');
}, 500);
// Now animate the title in
setTimeout( function () {
title.animate({
opacity: 1,
transform: "scale(1)"
}, 'fast');
}, 800);
}
});
}(jQuery, _));
]]></file>
<file javascript_app="core" javascript_location="admin" javascript_path="controllers/dashboard" javascript_name="ips.dashboard.validation.js" javascript_type="controller" javascript_version="103021" javascript_position="1000300">/**
* 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){
"use strict";
ips.controller.register('core.admin.dashboard.validation', {
initialize: function () {
this.on( 'click', '[data-action="approve"], [data-action="ban"]', 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('.ipsDataItem');
var name = row.find('[data-role="userName"]').text();
var toggles = button.closest('[data-role="validateToggles"]');
button
.text( type == 'approve' ? ips.getString('widgetApproving') : ips.getString('widgetBanning') )
.addClass('ipsButton_disabled');
ips.getAjax()( url )
.done( function () {
// Highlight row
row.addClass( type == 'approve' ? 'ipsDataItem_success' : 'ipsDataItem_error' );
row.attr('data-status', 'done');
button.text( type == 'approve' ? ips.getString('widgetApproved') : ips.getString('widgetBanned') );
setTimeout( function () {
ips.utils.anim.go( 'fadeOut', toggles );
}, 750);
// Show flash msg
ips.ui.flashMsg.show( ips.getString( type == 'approve' ? 'userApproved' : 'userBanned', {
name: name
}));
self._checkForFinished();
});
},
/**
* Checks whether all users shown in the widget have been approved/banned
* If so, triggers an event whch will reload the content to show more users
*
* @param {event} e Event object
* @returns {void}
*/
_checkForFinished: function (e) {
var items = this.scope.find('[data-role="userAwaitingValidation"]');
var doneItems = this.scope.find('[data-status="done"]');
var self = this;
if( items.length === doneItems.length ){
setTimeout( function () {
self.scope.trigger('refreshWidget');
}, 750);
}
}
});
}(jQuery, _));</file>
<file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.code.js" javascript_type="controller" javascript_version="103021" 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',
},
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
} );
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="103021" 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){
"use strict";
ips.controller.register('core.global.editor.codePreview', {
_origin: '',
initialize: function () {
this.on( 'click', '[data-action="preview"]', 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="103021" 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"]');
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' );
}
}
});
}(jQuery, _));]]></file>
<file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.emoticons.js" javascript_type="controller" javascript_version="103021" 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( '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){
setTimeout(function(){
this._buildEmoji( emoji, ips.utils.cookie.get('emojiSkinTone') );
}.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){
this._buildEmoji( emoji, ips.utils.cookie.get('emojiSkinTone') );
}.bind(this));
},
/* !Main panel */
/**
* Show emoji
*
* @param {array} emoji Array of emoji data
* @param {string} tone Skin tone
* @param {RegExp|null} search RegEx for searching
* @returns {void}
*/
_buildEmoji: function(emoji, tone, search) {
/* 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] ) ) {
newRecentlyUsed.push( recentlyUsed[i] );
emojiForThisRow += ips.templates.render('core.editor.emoji', {
display: ips.utils.emoji.preview( recentlyUsed[i] ),
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 category in emoji ) {
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_people','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"]').html( menuContent.join('') );
}
/* Display */
this.scope.find('.ipsMenu_innerContent').html( 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){
this._buildEmoji( emoji, data.selectedItemID, this._lastVal ? new RegExp( this._lastVal.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" ), 'i' ) : null );
}.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('[data-role="emoticonSearch"]').focus();
}.bind(this),100);
}
},
/**
* 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){
this._buildEmoji( emoji, ips.utils.cookie.get('emojiSkinTone') );
}.bind(this));
},
/**
* Finds emoticons matching the value
*
* @param {string} value Search value
* @returns {void}
*/
_doSearch: function (value) {
ips.utils.emoji.getEmoji(function(emoji){
this._buildEmoji( emoji, ips.utils.cookie.get('emojiSkinTone'), new RegExp( value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" ), 'i' ) );
}.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').animate({
scrollTop: this.scope.find('.ipsMenu_innerContent').scrollTop() + this.scope.find('[data-categoryid="' + data.selectedItemID + '"]').position().top - 85
}, 250);
}
});
}(jQuery, _));]]></file>
<file javascript_app="global" javascript_location="framework" javascript_path="controllers/editor" javascript_name="ips.editor.image.js" javascript_type="controller" javascript_version="103021" 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.ceil( 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.ceil( 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 ) {
var editor = $( 'textarea[name="' + $( this.scope ).data('editorid') + '"]' ).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="103021" 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 provides 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 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 {
url = ( element.attr('data-filelink') ) ? element.attr('data-filelink') : '';
}
extension = element.closest('[data-extension]').attr('data-extension');
return {
fileID: fileID,
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 ).html( item.title );
element.append( fallbackLink );
} else {
element = $('<a>').addClass('ipsAttachLink').html( item.title );
if( item.link ){
element.attr( 'href', item.link );
} else {
element.attr( 'href', ips.getSetting('baseURL') + 'applications/core/interface/file/attachment.php?id=' + item.fileID );
}
}
if ( item.extension )
{
element.attr( 'data-extension', item.extension );
}
return $('<div/>').append( element ).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="103021" 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();
if ( !url ) {
$(this.scope).find('.ipsFieldRow').addClass('ipsFieldRow_error');
return;
} else {
$(this.scope).find('.ipsFieldRow').removeClass('ipsFieldRow_error');
}
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.replace(/'/g, '%27').replace(/"/g, '%22').replace(/</g, '%3C').replace(/>/g, '%3E') + "'>" + selectedElement[0].outerHTML + "</a>" );
editor.insertElement( element );
}
else
{
selectedElement.parent().attr( 'href', url.replace(/'/g, '%27').replace(/"/g, '%22').replace(/</g, '%3C').replace(/>/g, '%3E') ).removeAttr('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').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.replace(/'/g, '%27').replace(/"/g, '%22').replace(/</g, '%3C').replace(/>/g, '%3E'),
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').addClass('ipsFieldRow_error');
}
})
.fail(function(){
scope.find('[data-role="linkURL"]').removeClass('ipsField_loading');
scope.find('[data-action="linkButton"]').prop('disabled', false);
scope.find('.ipsFieldRow').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,' ' );
if ( !title ) {
title = decodeURI( url );
}
title = title.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
var element = CKEDITOR.dom.element.createFromHtml( "<a>" + title + "</a>" );
}
// Something (i.e. an img tag) is selected
else
{
element = selectedElement;
}
element.setAttribute( 'href', url.replace(/'/g, '%27').replace(/"/g, '%22').replace(/</g, '%3C').replace(/>/g, '%3E') );
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="103021" 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){
"use strict";
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="myMediaContent"]').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="103021" 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="103021" 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) {
self.scope.find('[data-role="previewContainer"]').html( self._processResponse( response ) );
});
},
/**
* 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.uploader.js" javascript_type="controller" javascript_version="103021" 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"]').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 {
container = this.scope.find('[data-role="files"]');
template += 'fileItem';
}
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"]').hide();
count++;
}
});
if( count == 3 ){
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="103021" javascript_position="1000450">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.files.form.js - ACP Files Form Stuffs
*
* Author: MTM
*/
;( function($, _, undefined){
"use strict";
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="103021" javascript_position="1000450"><![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="103021" javascript_position="1000300"><![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="103021" javascript_position="1000300">/**
* 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){
"use strict";
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="member"]');
ips.ui.autocomplete.getObj( field ).removeAll();
form.find( "[type='checkbox']" ).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="member"]');
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="member"]').val()
}
} )
.done( function( response ) {
var i;
for ( i in response ) {
form.find( "[name='ignore_" + i + "_checkbox']" ).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/members" javascript_name="ips.members.form.js" javascript_type="controller" javascript_version="103021" javascript_position="1000500"><![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 ( 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="103021" javascript_position="1000500">/**
* 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){
"use strict";
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="historyTitle"]').text( data.menuElem.find('[data-ipsMenuValue="' + data.selectedItemID + '"]').text() );
$(this.scope).find('[data-role="historyDisplay"]').addClass('ipsLoading ipsLoading_dark').html('');
ips.getAjax()( data.menuElem.find('[data-ipsMenuValue="' + data.selectedItemID + '"] a').attr('href') ).done(function(response){
$(this.scope).find('[data-role="historyDisplay"]').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="103021" javascript_position="1000500">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.core.members.lazyLoadingProfileBlock.js
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
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.moderatorPermissions.js" javascript_type="controller" javascript_version="103021" javascript_position="1000500"><![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="103021" javascript_position="1000500">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.members.restrictions.js - Controller for restrictions screen
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.members.restrictions', {
initialize: function () {
this.on( 'click', '.acpRestrictions_subHeader h3', this.toggleSubHeader );
this.on( 'change', '.acpRestrictions_header input[type="checkbox"]', this.toggleHeader );
this.on( 'change', '.acpAppRestrictions_header input[type="checkbox"]', this.toggleAppHeader );
this.on( 'click', '[data-action="checkAll"]', this.checkAll );
this.on( 'click', '[data-action="checkNone"]', this.checkNone );
this.on( 'click', '[data-action="expandAll"], [data-action="collapseAll"]', 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="checkbox"]')
.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="checkbox"]')
.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="checkbox"]')
.each( function () {
var thisCheck = $( this );
var checked = thisCheck.closest('.acpRestrictions_panel').siblings('.acpRestrictions_header').find('input[type="checkbox"]').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="checkbox"]')
.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="checkbox"]: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="103021" javascript_position="1000050">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.messages.folderDialog.js - Folder naming dialog controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
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="folderName"]');
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="103021" javascript_position="1000050"><![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) {
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(' ')
.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"]') );
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="103021" javascript_position="1000050"><![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') ] ) );
}
});
},
/**
* 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="103021" javascript_position="1000050"><![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(' ')
.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.approveQueue.js" javascript_type="controller" javascript_version="103021" 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){
"use strict";
ips.controller.register('core.front.modcp.approveQueue', {
initialize: function () {
this.on( 'click', '[data-action="approvalQueueNext"]', this.doAction );
},
/**
* 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="103021" 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){
"use strict";
ips.controller.register('core.front.modcp.report', {
initialize: function () {
this.on( document, 'submitDialog', '[data-role="warnUserDialog"]', 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="' + data.selectedItemID + '"] 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="authorPanel"]')
.css( 'height', this.scope.find('[data-role="authorPanel"]').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="authorPanel"]')
.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="103021" 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){
"use strict";
ips.controller.register('core.front.modcp.reportList', {
initialize: function () {
this.on( 'menuItemSelected', '[data-action="changeStatus"]', 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="103021" 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){
"use strict";
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="' + data.selectedItemID + '"]');
var icon = item.find('[data-role="ipsMenu_selectedIcon"]').attr('class');
var status = item.find('[data-role="ipsMenu_selectedText"]').text();
this.scope.find('[data-role="reportIcon"]').get(0).className = icon;
this.scope.find('[data-role="reportStatus"]').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="103021" 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,
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
}
},
/**
* 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;
}
if ( response.remove.unlimited ) {
if( self.expirationEdited == false || response.remove_override == 0 )
{
if( scope.find('[name="warn_remove_unlimited"]').prop( 'checked' ) == false )
{
scope.find('[name="warn_remove_unlimited"]').click();
}
}
} else if( self.expirationEdited == false || response.remove_override == 0 ) {
scope.find('[name="warn_remove_unlimited"]').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 );
}
scope.find('[name="warn_remove_unlimited"]').prop( 'disabled', response.remove_override == 0 );
var editor = ips.ui.editor.getObj( $( 'textarea[name="warn_member_note"]' ).closest('[data-ipsEditor]') );
if( response.notes ){
editor.unminimize( function(){
if( !editor.checkDirty() ){
editor.reset();
editor.insertHtml( response.notes );
editor.resetDirty();
}
});
} else {
if( editor && !editor.minimized && !editor.checkDirty() ){
editor.reset();
editor.resetDirty();
}
}
} );
},
/**
* 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' ];
for( var i = 0; i < 3; i++ ) {
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', !response.override );
scope.find( '[name="warn_' + types[i] + '_time"]' ).val( response.actions[ types[i] ].time ).prop( 'disabled', !response.override );
scope.find( '[name="warn_' + types[i] + '_unlimited"]' ).prop( 'checked', response.actions[ types[i] ].unlimited ).prop( 'disabled', !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="103021" 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="103021" javascript_position="1000550">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.profile.body.js - Profile body controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
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="showRecentWarnings"]', this.showRecentWarnings );
this.setup();
},
/**
* Non-event-based setup
*
* @returns {void}
*/
setup: function () {
},
showRecentWarnings: function (e) {
e.preventDefault();
this.scope.find('[data-action="showRecentWarnings"]').hide();
ips.utils.anim.go( 'fadeIn fast', this.scope.find('[data-role="recentWarnings"]') );
}
});
}(jQuery, _));</file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/profile" javascript_name="ips.profile.followers.js" javascript_type="controller" javascript_version="103021" javascript_position="1000550"><![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="103021" javascript_position="1000550"><![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 );
// 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"]');
},
/**
* 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':
this._showRepLog( 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 log
*
* @param {string} url URL to load the content
* @returns {void}
*/
_showRepLog: 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;
this._changeContent( true, url );
this._showProfileButton();
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;
this._changeContent( false, url );
this._showContentButton();
$('#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;
// Get height and set it, so that it doesn't jolt the page
this.contentArea.css({
height: 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'
});
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="103021" javascript_position="1000550">/**
* 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){
"use strict";
ips.controller.register('core.front.profile.toggleBlock', {
initialize: function () {
this.on( 'click', '[data-action="disable"]', this.toggleBlock );
this.on( 'click', '[data-action="enable"]', 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: 0.6
});
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: 1
});
});
}
});
}(jQuery, _));</file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/promote" javascript_name="ips.system.promote.js" javascript_type="controller" javascript_version="103021" 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_facebook_shareable"]', this.facebookOptions );
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.facebookOptions();
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();
},
/**
* Dynamically update the list of facebook share-to
*
* @param {event} e Event object
* @returns {void}
*/
facebookOptions: function () {
var names = new Array;
var _self = this;
/* If we only have 1 shareable item, then do away with the menu, etc */
if ( this.scope.find('[name*="promote_facebook_shareable"]').length == 1 ) {
$('#elFacebookOptions').hide();
this.scope.find('[name*="promote_facebook_shareable"]').each( function(i,v) {
$(v).prop('checked', true );
} );
}
this.scope.find('[name*="promote_facebook_shareable"]:checked').each( function(i,v) {
names.push( _self.scope.find( 'span[data-label=' + $(v).attr('id') + ']' ).html() );
} );
if ( names.length ) {
$('#elFacebookNoPropertiesError').fadeOut();
this.scope.find('[data-role="promoteFacebookOptions"]').html( names.join( ', ' ) );
}
else {
$('#elFacebookNoPropertiesError').fadeIn();
this.scope.find('[data-role="promoteFacebookOptions"]').html( ips.getString('promotes_no_facebook_selected') );
}
},
/**
* 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="103021" 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){
"use strict";
ips.controller.register('core.front.system.promoteList', {
initialize: function () {
this.on( 'click', '[data-action="delete"]', 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="103021" javascript_position="1000450"><![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( '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
});
},
/**
* 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(e)
{
var type = this.scope.find('input[name="type"]:checked').val();
if ( ! type ) {
$('#elSearch_filter_by_number').hide();
} else {
$('#elSearch_filter_by_number').show();
}
},
/**
* 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) {
e.preventDefault();
$( e.currentTarget ).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');
this.scope.find('[data-role="' + opens + '"]').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();
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 = $.trim( this.scope.find('#elMainSearchInput').val() );
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="103021" javascript_position="1000450"><![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.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 );
History.pushState( {
controller: 'core.front.search.main',
url: url,
filterData: this._formData
}, this._getBrowserTitle(), url );
},
/**
* 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'], function (val) {
if( !_.isUndefined( data[ val ] ) && data[ val ] !== '' ){
params.push( val + '=' + 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 ){
params.push( val + '=' + data[ val ] );
}
});
if( !_.isUndefined( data['tags'] ) ){
params.push( 'tags=' + data['tags'].replace(/\n/g, ',') );
}
// Are we searching nodes?
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( $.trim( data['q'] ) !== '' && $.trim( data['tags'] ) !== '' ){
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
_.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] ] );
}
}
});
// Custom date param
_.each( [ ['startDateCustom[start]', 'start_after'], ['startDateCustom[end]', 'start_before'],
['updatedDateCustom[start]', 'updated_after'], ['updatedDateCustom[end]', 'updated_before'] ], function (val) {
if( !_.isUndefined( data[ val[0] ] ) ){
// 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="103021" javascript_position="1000450"><![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 = $.trim( result.text() );
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 = $.trim( text.substring( startPoint, startPoint + 300 ) );
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("<mark class="ipsMatch" + ( index + 1 ) + "">", 'ig'), "<mark class='ipsMatch" + ( index + 1 ) + "'>" ).replace( new RegExp("</mark>", 'ig'), "</mark>" ) );
} );
});
});
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="admin" javascript_path="controllers/settings" javascript_name="ips.settings.posting.js" javascript_type="controller" javascript_version="103021" javascript_position="1000150">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.settings.posting.js
*
* Author: Stuart Silvester
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.settings.posting', {
defaultStatus: false,
initialize: function () {
this.defaultStatus = $( '#check_remote_image_proxy' ).is(':checked');
this.on( 'change', '#check_remote_image_proxy', this.promptRebuildPreference );
},
promptRebuildPreference: function (e) {
/* Do not prompt if the setting is toggled to the default */
if( this.defaultStatus == $( '#check_remote_image_proxy' ).is(':checked') )
{
/* Disable any rebuild process */
$('input[name=rebuildImageProxy]').val( 0 );
return;
}
/* Show Rebuild Prompt */
ips.ui.alert.show({
type: 'confirm',
message: ips.getString('imageProxyRebuild'),
subText: $( '#check_remote_image_proxy' ).is(':checked') ? ips.getString('imageProxyRebuildBlurbEnable') : ips.getString('imageProxyRebuildBlurbDisable'),
icon: 'question',
buttons: {
ok: ips.getString('imageProxyRebuildYes'),
cancel: ips.getString('imageProxyRebuildNo')
},
callbacks: {
ok: function(){
$('input[name=rebuildImageProxy]').val( 1 );
},
cancel: function(){
$('input[name=rebuildImageProxy]').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="103021" javascript_position="1000550">/**
* 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.stats.filtering', {
initialize: function () {
this.on( 'click', '[data-role="toggleGroupFilter"]', 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="checkbox"]').prop('checked', true);
$('#elGroupFilter').slideUp();
}
else
{
$('#elGroupFilter').slideDown();
}
}
});
}(jQuery, _));</file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/statuses" javascript_name="ips.statuses.status.js" javascript_type="controller" javascript_version="103021" javascript_position="1000650"><![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: 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¤tPage=' + 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="103021" javascript_position="1000650"><![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 () {
//form.submit();
})
.always( function () {
submit
.prop( 'disabled', false )
.text( currentText );
});
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/streams" javascript_name="ips.streams.form.js" javascript_type="controller" javascript_version="103021" javascript_position="1000400"><![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,
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
};
this._formData = ips.getSetting('stream_config');
/* Are we looking for unread fings and dat? */
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'], function (val) {
var lang = 'streamFilter_' + val + '_' + values[ val ];
_overview( val ).html( ips.getString( lang ) );
});
// Tags
if( !_.isUndefined( values['stream_tags'] ) && $.trim( values['stream_tags'] ) !== '' ){
var tags = _.compact( values['stream_tags'].split(/\n/) );
if( tags.length <= 2 ){
var tagLang = ips.getString('streamFilter_stream_tags_tags', { 'tags': _.escape( tags.join("\n") ) });
} 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' ){
var dateLang = ips.pluralize( ips.getString('streamFilter_stream_date_type_relative'), 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( $.trim( option.text() ) );
}
}
_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( $.trim( elem.text() ) );
}
}
_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', 'stream_unread_links'], 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' && this.reloaded == false ) ) {
_.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').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="103021" javascript_position="1000400"><![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( document, 'breakpointChange', this.breakpointChange );
// 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_link')
.filter('[data-view="' + type + '"]')
.removeClass('ipsButton_link')
.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(/'/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' );
if( !_.size( data ) ){
return returnedValues;
}
// Reset any values that have been altered
if ( ! _.isUndefined( defaultValues['changed'] ) ) {
_.each( defaultValues['changed'], function( val, key ) {
defaultValues[ key ] = val;
} );
}
// Simple values
_.each( ['stream_sort', 'stream_include_comments', 'stream_read', 'stream_unread_links', 'stream_default_view', 'stream_club_select', 'stream_club_filter'], function (val) {
if( defaultValues[ val ] != 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(',');
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( ! _.isEmpty( $.trim( data['stream_tags'] ) ) ){
var newTags = _.compact( ( data['stream_tags'] || '' ).split(',') );
var oldTags = _.compact( ( defaultValues['stream_tags'] || '' ).split(',') );
var tagIntersection = _.intersection( newTags, defaultValues['stream_tags'] );
if( newTags.length !== oldTags.length || tagIntersection.length !== newTags.length ){
returnedValues['stream_tags'] = newTags.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
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/streams" javascript_name="ips.streams.results.js" javascript_type="controller" javascript_version="103021" javascript_position="1000400"><![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');
this._updateURLs();
},
/**
* 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 );
}
this._updateURLs();
},
/**
* 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.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 {
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._updateURLs();
this._animateNewItems();
},
/**
* Adjust URLs if this is an unread stream
*
* @returns {void}
*/
_updateURLs: function() {
if( this._config['stream_unread_links'] == 1 ) {
this.scope.find('div.ipsStreamItem_unread').each( function() {
var star = $(this).find('a[data-linkType="star"]');
var link = $(this).find('a[data-linkType="link"]');
// Replace with getLast link
link.attr('href', star.attr('href'));
} );
}
},
/**
* 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="103021" javascript_position="1000400">/**
* 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 );
},
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 );
}
}
});
}
}
});
}(jQuery, _));</file>
<file javascript_app="core" javascript_location="admin" javascript_path="controllers/support" javascript_name="ips.support.diagnostics.js" javascript_type="controller" javascript_version="103021" javascript_position="1000400">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.support.diagnostics.js
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.support.diagnostics', {
initialize: function () {
this.on( 'click', '[data-action="contactSupport"]', this.stillNeedHelpButton );
this.on( 'click', '[data-action="disableThirdParty"]', this.disableThirdPartyButton );
this.on( 'click', '[data-action="enableThirdParty"]', this.enableThirdPartyButton );
this.on( 'click', '[data-action="enableThirdPartyPart"]', this.enableThirdPartyPartButton );
},
stillNeedHelpButton: function (e) {
e.preventDefault();
if ( $(this.scope).attr('data-fails') ) {
ips.ui.alert.show({
type: 'confirm',
message: ips.getString('supportToolFailsHead'),
subText: ips.getString('supportToolFailsInfo'),
icon: 'warn',
buttons: {
ok: ips.getString('supportContinueAnyway'),
cancel: ips.getString('cancel')
},
callbacks: {
ok: _.bind( $(this.scope).attr('data-thirdParty') == '0' ? this._continueToSupportForm : this._launchThirdPartyAlert, this )
}
});
return;
}
if ( $(this.scope).attr('data-thirdParty') ) {
this._launchThirdPartyAlert();
}
},
_launchThirdPartyAlert: function() {
ips.ui.alert.show({
type: 'confirm',
message: ips.getString('supportTool3rdPartyHead'),
subText: ips.getString('supportTool3rdPartyInfo'),
icon: 'warn',
buttons: {
ok: ips.getString('supportContinueAnyway'),
cancel: ips.getString('cancel')
},
callbacks: {
ok: _.bind( this._continueToSupportForm, this )
}
});
},
_continueToSupportForm: function() {
$(this.scope).find('[data-action="contactSupport"]').attr( 'data-action', 'wizardLink' ).click();
},
disableThirdPartyButton: function (e) {
e.preventDefault();
$(this.scope).attr('data-thirdParty', '0');
$(e.target).prop('disabled', true).addClass('ipsButton_disabled').addClass('ipsButton_disabled').text( ips.getString('supportDisablingCustomizations') );
var self = this;
ips.getAjax()( $(e.target).attr('href') )
.done( function(response) {
$(self.scope).find('[data-role="thirdPartyInfo"]').html( response );
$( document ).trigger( 'contentChange', [ $(self.scope) ] );
});
},
enableThirdPartyButton: function (e) {
e.preventDefault();
$(e.target).prop('disabled', true).addClass('ipsButton_disabled').addClass('ipsButton_disabled').text( ips.getString('supportEnablingCustomizations') );
var self = this;
ips.getAjax()( $(e.target).attr('href') )
.done( function(response) {
$(e.target).remove();
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) ] );
});
},
enableThirdPartyPartButton: function (e) {
e.preventDefault();
$(e.target).prop('disabled', true).addClass('ipsButton_disabled').addClass('ipsButton_disabled').text( ips.getString('supportEnablingCustomizations') );
var self = this;
ips.getAjax()( $(e.target).attr('href') )
.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();
$(e.target).remove();
$( document ).trigger( 'contentChange', [ $(self.scope) ] );
});
}
});
}(jQuery, _));</file>
<file javascript_app="core" javascript_location="admin" javascript_path="controllers/support" javascript_name="ips.support.md5.js" javascript_type="controller" javascript_version="103021" javascript_position="1000400">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.support.md5.js
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.support.md5', {
initialize: function () {
this.on( 'click', '[data-action="downloadDelta"]', this.downloadDelta );
},
downloadDelta: function (e) {
e.preventDefault();
$(this.scope).find('[data-role="initialScreen"]').hide();
$(this.scope).find('[data-role="downloadForm"]').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="103021" javascript_position="1000350"><![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: 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="103021" javascript_position="1000350"><![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.codeHook.js" javascript_type="controller" javascript_version="103021" javascript_position="1000350">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.system.codeHook.js - Handles editing code hooks
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
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="103021" javascript_position="1000350"><![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="103021" javascript_position="1000350">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.system.login.js - ACP login screen controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.system.login', {
initialize: function () {
this.on( 'tabChanged', this.tabChanged );
this.on( 'click', '[data-action="upgradeWarningContinue"]', 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("#elTabContent .ipsTabs_panel:visible")
.first()
.find('input[type="text"]')
.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="upgradeWarning"]').hide();
$(this.scope).find('[data-role="loginForms"]').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="103021" javascript_position="1000350"><![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,
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"]');
// Set up nested sortable for the root items
this.scope.find('.cMenuManager_root > ol').nestedSortable({
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 )
});
// 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()( '?app=core&module=applications&controller=menu&do=reorder&reorderDropdown=1&' + result.join('&') )
.done(function(){
self._updatePreview();
});
}
});
},
_update: function () {
var self = this;
this._changeOccurred();
ips.getAjax()( '?app=core&module=applications&controller=menu&do=reorder&' + this.scope.find('.cMenuManager_root > ol').nestedSortable( 'serialize', { key: 'menu_order' } ) )
.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()( '?app=core&module=applications&controller=menu&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();
// Slide columns
self.scope.find('[data-manager="dropdown"], [data-manager="main"]').show().animate({
left: "-50%"
}, 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: $("#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 ){
// Back to the root
this.scope.find('.cMenuManager_column').addClass('cMenuManager_readyToSlide');
this.scope.find('[data-manager="dropdown"], [data-manager="main"]').show().animate({
left: "0"
}, 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();
this._previewWindow.css({
left: width + 'px'
});
}
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="admin" javascript_path="controllers/system" javascript_name="ips.system.themeHook.js" javascript_type="controller" javascript_version="103021" javascript_position="1000350">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.system.codeHook.js - Handles editing theme hooks
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.system.themeHook', {
initialize: function () {
var scope = this.scope;
scope.find( '[data-action="showTemplate"]' ).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="showTemplate"]' ) ).hide();
scope.find( 'input[name="plugin_theme_hook_selector"]' ).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="103021" javascript_position="1000350"><![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 = $.trim( target.text() );
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="103021" javascript_position="1000350">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.system.codeHook.js - Handles editing code hooks
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
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="front" javascript_path="controllers/system" javascript_name="ips.system.manageFollowed.js" javascript_type="controller" javascript_version="103021" javascript_position="1000500">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.system.manageFollowed.js - Followed content controll
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
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="followDate"], [data-role="followFrequency"]').html('');
this.scope.find('[data-role="followAnonymous"]').addClass('ipsHide');
this.scope.find('[data-role="followButton"]').addClass('ipsButton_disabled');
this.scope.addClass('ipsFaded');
return;
}
// Update anonymous badge
this.scope.find('[data-role="followAnonymous"]').toggleClass( 'ipsHide', !data.anonymous );
// Update notification type
if( data.notificationType ){
this.scope.find('[data-role="followFrequency"]').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="103021" javascript_position="1000500">/**
* 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( '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();
}
},
/**
* 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;
})
.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 () {
// Duplicate the metaTemplate block and append it to the list
var copy = this.scope.find('[data-role="metaTemplate"]').clone().removeAttr('data-role').hide();
$('#elMetaTagEditor_tags').append( copy );
ips.utils.anim.go( 'fadeIn', copy );
$(document).trigger( 'contentChange', [ copy ] );
},
/**
* 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="103021" javascript_position="1000500">/**
* 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="promptMe"]', this.promptMe );
this.on( document, 'permissionGranted.notifications', this.permissionChanged );
this.on( document, 'permissionDenied.notifications', this.permissionChanged );
this.setup();
},
/**
* Setup method
*
* @returns {void}
*/
setup: function () {
if( ips.utils.notification.supported ){
this._showNotificationChoice();
}
},
/**
* Event handler for when the notification permission changes
*
* @returns {void}
*/
permissionChanged: function () {
this._showNotificationChoice();
},
/**
* Event handler for clicking the 'enabled notifications' button.
* Shows an info message, and then triggers a browser prompt 2 secs later
*
* @param {event} e Event object
* @returns {void}
*/
promptMe: function (e) {
e.preventDefault();
if( ips.utils.notification.hasPermission() ){
// No need to do anything
return;
}
// Shoe the message then wait a couple of secs before prompting
this.scope.find('[data-role="promptMessage"]').slideDown();
setTimeout( function () {
ips.utils.notification.requestPermission();
}, 2000);
},
/**
* Displays an info panel, with the message depending on whether notifications are enabled in the browser
*
* @returns {void}
*/
_showNotificationChoice: function () {
var type = ips.utils.notification.permissionLevel();
this.scope
.find('[data-role="browserNotifyInfo"]')
.show()
.html( ips.templates.render( 'notification.' + type ) );
}
});
}(jQuery, _));</file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/system" javascript_name="ips.system.register.js" javascript_type="controller" javascript_version="103021" javascript_position="1000500"><![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: { 'username': ips.getAjax(), 'email': ips.getAjax() },
popup: null,
passwordBlurred: true,
dirty: false,
initialize: function () {
this.on( 'keyup', '#elInput_username', this.changeUsername );
this.on( 'keyup', '#elInput_email_address', this.changeEmail );
this.on( 'blur', '#elInput_username', this.changeUsername );
this.on( 'blur', '#elInput_email_address', this.changeEmail );
this.on( 'keyup', '#elInput_password_confirm', this.confirmPassword );
this.on( 'blur', '#elInput_password_confirm', this.confirmPassword );
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.emailField = this.scope.find('#elInput_email_address');
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.emailField.after( $('<span/>').attr( 'data-role', 'validationCheck' ) );
this.confirmPasswordField.after( $('<span/>').attr( 'data-role', 'validationCheck' ) );
},
/**
* 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, 'username', this.usernameField ), 700 );
} else {
this._clearResult( this.usernameField );
}
},
/**
* Event handler for a change on the email field
* Waits 700ms, then calls this._doCheck
*
* @param {event} e Event object
* @returns {void}
*/
changeEmail: function (e) {
if( this.timers['email'] ){
clearTimeout( this.timers['email'] );
}
if( ( this.emailField.val().length > 5 && ips.utils.validate.isEmail(this.emailField.val()) ) || e.type != "keyup" ){
this.timers['email'] = setTimeout( _.bind( this._doCheck, this, 'email', this.emailField ), 700 );
} else {
this._clearResult( this.emailField );
}
},
/**
* Event handler for a change on the password field
* Waits 700ms, then calls this._doCheck
*
* @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('');
},
/**
* 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 (type, field) {
var value = field.val();
var resultElem = field.next('[data-role="validationCheck"]');
var self = this;
if( this.ajax[ type ] && this.ajax[ type ].abort ){
this.ajax[ type ].abort();
}
// Set loading
field.addClass('ipsField_loading');
// Do ajax
this.ajax[ type ]( ips.getSetting('baseURL') + '?app=core&module=system&controller=ajax&do=' + type + 'Exists', {
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="103021" javascript_position="1000100">/**
* 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){
"use strict";
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="103021" javascript_position="1000100">/**
* 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){
"use strict";
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="editor"]').length ) {
_.each( $('[data-role="editor"]'), 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' );
} );
}
},
/**
* "Use this version" 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="103021" javascript_position="1000100"><![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,
_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();
}
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 = $.trim( active.tabPanel.find('[data-role="variables"]').val() );
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
*
* @returns {void}
*/
toggleDiff: function () {
var self = this;
var active = this._getActiveTab();
var panel = active.tabPanel;
var key = panel.attr('data-fileid');
// 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') );
}
// Toggle on
else {
// 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', {
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 = $( ips.templates.render( 'templates.editor.diffHeaders' ) );
panel.prepend( headers );
// Set size
var height = self._getTabContentHeight() - headers.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
active.tabPanel.find('[data-role="variables"]').val( $.trim( response.template_data ) );
$('#elTemplateEditor_variables_dialog, #elTemplateEditor_attributes').find('[data-role="variables"]').val( $.trim( response.template_data ) );
// 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 ) {
$('[data-ipsmenuvalue="diff"]').addClass('ipsMenu_itemChecked');
} else {
$('[data-ipsmenuvalue="diff"]').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();
// Find all codemirror instances and resize those
this._tabContent.find('.CodeMirror').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 = this.scope.find('#elTemplateEditor_info').outerHeight();
this._panelHeight = windowHeight - tabContentTop - infoHeight - 31;
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="103021" javascript_position="1000100"><![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 );
// 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 = this.scope.find('#elTemplateEditor_info').height();
var newButtonHeight = this.scope.find('#elTemplateEditor_newButton').outerHeight();
var browserHeight = $( window ).height();
var fileListNewTop = browserHeight - fileListTop - infoHeight - newButtonHeight - 30;
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="103021" javascript_position="1000100"><![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.variablesDialog.js" javascript_type="controller" javascript_version="103021" javascript_position="1000100">/**
* 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){
"use strict";
ips.controller.register('core.admin.templates.variablesDialog', {
initialize: function () {
this.on( 'click', 'input[type="submit"]', 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) {
this.trigger( 'variablesUpdated.templates', {
fileID: this.scope.find('[name="_variables_fileid"]').val(),
type: this.scope.find('[name="_variables_type"]').val(),
value: this.scope.find('[data-role="variables"]').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="103021" 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( 'click', '[data-action="revertColorizer"]', this.revertChanges );
this.setup();
},
setup: function () {
var colors = {};
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
this.scope.find('input[type="text"].color').each( function () {
$( this ).attr( 'data-original', $( this ).val() );
new jscolor.color( this, {
slider: false
});
});
},
/**
* 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) {
var self = this;
var type = $( e.currentTarget ).attr('data-role');
var color = $( e.currentTarget ).val().replace('#', '');
var h = $( e.currentTarget ).get(0).color.hsv[0] * 60;
var s = $( e.currentTarget ).get(0).color.hsv[1] * 100;
if( _.isUndefined( colorizer[ type ] ) ){
Debug.error("Can't find data for " + type);
return;
}
// We need the HSL for the color chosen
/*var r = ips.utils.color.hexToRGB( color.slice(0, 2) ) / 255;
var g = ips.utils.color.hexToRGB( color.slice(2, 4) ) / 255;
var b = ips.utils.color.hexToRGB( color.slice(4, 6) ) / 255;*/
//var hsl = ips.utils.color.RGBtoHSL( r, g, b );
// Get the classes for which we're updating the color
var toUpdate = colorizer[ type ];
// Loop through every section and check if it's in our type object
_.each( ipsVSEData.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
if( !_.isUndefined( toUpdate[ styleKey ] ) ){
for( var i = 0; i < toUpdate[ styleKey ].length; i++ ){
if( self[ '_update' + toUpdate[ styleKey ][ i ] ] ){
self[ '_update' + toUpdate[ styleKey ][ i ] ]( styleData, styleKey, h, s );
}
}
}
});
});
// 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.color.fromString( $( this ).attr('data-original') );
});
}
}
});
},
/**
* 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="103021" 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 );
// Messages coming from panels
this.on( 'styleUpdated', this.styleUpdated );
this.on( 'revertChanges', this.revertChanges );
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();
},
/**
* Merge resume data with vseData
*
* @return {object}
*/
getVSEData: function()
{
if ( _.isObject( this._vseData ) )
{
return this._vseData;
}
this._vseData = ipsVSEData;
if ( ipsResumeVse.data )
{
if ( ! _.isObject( ipsResumeVse.data ) )
{
ipsResumeVse.data = JSON.parse( ipsResumeVse.data );
if ( ! _.isObject( ipsResumeVse.data ) )
{
return this._vseData;
}
}
this._vseData = _.extend( ipsVSEData, ipsResumeVse.data );
}
return this._vseData;
},
/**
* 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 );
}
},
/**
* Sends the provided command to the frame window
*
* @param {event} e Event object
* @returns {void}
*/
sendCommand: function (command, data) {
this._mainWindow.postMessage( _.extend( data || {}, { command: command } ), this._url );
},
/**
* 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;
},
/**
* Responds to events bubbled by panels/widgets indicating a style has been updated
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
styleUpdated: function (e, data) {
var styles = this._generateLiveStyleObject( data.selector );
this._updateSwatch( data.selector );
this.sendCommand('updateStyle', {
selector: data.selector,
styles: styles
});
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;
this._data = $.extend( true, {}, this._originalData );
_.each( this._data.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
self.trigger( 'styleUpdated', {
selector: styleData.selector
});
});
});
this._unsaved = false;
},
/**
* Responds to a command from the frame indicating that it is ready
*
* @returns {void}
*/
commandWindowReady: function () {
this._frameReady = true;
this.sendCommand('selectorData', {
selectors: this._getSelectors()
});
},
/**
* Responds to a command from the frame, sending a complete stylesheet object in response
*
* @returns {void}
*/
commandGetStylesheet: function () {
this.sendCommand('createStylesheet', {
classes: this._generateLiveStyleObject(),
custom: this.scope.find('#vseCustomCSS_editor').val()
});
},
/**
* Responds to command from the frame indicating an element has been selected by the xray
* Method receives the primary match and other related matches
*
* @param {event} e Event object
* @returns {void}
*/
commandSelectorsMatched: function (data) {
var self = this;
// Loop through our list, hiding any that don't match
_.each( this._data.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
if( styleData.selector != data.primary && _.indexOf( data.other, styleData.selector ) === -1 ){
self.scope.find('li[data-styleid="' + sectionKey + '_' + styleKey + '"]').hide();
}
})
});
// Also hide section title
this.scope.find('#vseClassList .ipsToolbox_sectionTitle').hide();
// Add new section title
this.scope.find('#vseClassList > ul').prepend( ips.templates.render('vse.classes.title', {
title: ips.getString('vseMatchedStyles'),
role: "xrayResultsTitle"
}));
},
/**
* Event handler for clicking a class in the list
*
* @param {event} e Event object
* @returns {void}
*/
selectClass: function (e) {
var styleID = $( e.currentTarget ).attr('data-styleID');
if( !styleID ){
return;
}
var styleData = this._getStyleData( styleID );
// Do we need to build panels?
if( !$('#vseClassEditor').find('[data-styleid="' + styleID + '"]').length ){
this._buildPanel( styleID, styleData );
}
// Update title
$('#vseClassEditor').find('[data-role="classTitle"]').text( styleData.title );
// Hide all panels but show the one we're working on
$('#vseClassEditor')
.find('[data-styleid]')
.hide()
.end()
.find('[data-styleid="' + styleID + '"]')
.show();
// Add class to the wrapper which will start the animation
$('#vseClassWrap').addClass('vseShow_editor');
},
/**
* Event handler for clicking 'back' in the class editor
*
* @param {event} e Event object
* @returns {void}
*/
editorBack: function (e) {
$('#vseClassWrap').removeClass('vseShow_editor');
},
/**
* 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();
},
/**
* 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') );
},
/**
* 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');
});
},
/**
* 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()
});
},
/**
* Generates an object of style properties that can be passed into ips.utils.css to build a style block
*
* @param {string} selector If provided, will only returns the styles for the given selector. Otherwise, all selectors are built.
* @returns {object}
*/
_generateLiveStyleObject: function (selector) {
var self = this;
var returnedObject = {};
_.each( this._data.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
if( selector && styleData.selector == selector ){
returnedObject = self._getStyleObjectForSelector( styleData, styleKey, sectionKey );
} else if( _.isUndefined( selector ) ) {
if( _.isUndefined( returnedObject[ styleData.selector ] ) ){
returnedObject[ styleData.selector ] = {};
}
returnedObject[ styleData.selector ] = self._getStyleObjectForSelector( styleData, styleKey, sectionKey );
}
});
});
return returnedObject;
},
/**
* Takes style data from our main data object and returns an object containing CSS rules, ready to be used by ips.utils.css
* Intended to be executed by this._generateLiveStyleObject, not directly
*
* @param {object} styleData The data for the selector this iteration is building
* @param {string} styleKey The key in our master data object that identifies this style
* @param {string} sectionKey The key in our master data object that identifies the section this style belongs to
* @returns {object}
*/
_getStyleObjectForSelector: function (styleData, styleKey, sectionKey) {
var self = this;
var returnedObject = {};
var orig = this._originalData;
// Background
if( !_.isUndefined( styleData.background ) ){
var tmpNew = styleData.background;
var tmpOrig = orig.sections[ sectionKey ][ styleKey ];
var doGradient = false;
// Background COLOR
if( !_.isUndefined( tmpNew.color ) ){
if( _.isUndefined( tmpOrig.background ) || _.isUndefined( tmpOrig.background.color ) ||
tmpOrig.background.color != tmpNew.color ) {
returnedObject[ ( !_.isUndefined( tmpNew.gradient ) ) ? 'background-color' : 'background' ] = '#' + tmpNew.color.replace('#', '');
}
}
// Background GRADIENT
if( !_.isUndefined( tmpNew.gradient ) ){
if( _.isUndefined( tmpOrig.background ) || _.isUndefined( tmpOrig.background.gradient ) ) {
doGradient = true;
} else if ( tmpOrig.background.gradient.angle != tmpNew.gradient.angle ) {
doGradient = true;
} else {
// Compare each stop
for( var i = 0; i < tmpNew.gradient.stops.length; i++ ){
if( _.isUndefined( tmpOrig.background.gradient.stops[ i ] ) ){
doGradient = true;
break;
}
if( tmpNew.gradient.stops[ i ][ 0 ] != tmpOrig.background.gradient.stops[ i ][ 0 ] ){
doGradient = true;
break;
}
if( tmpNew.gradient.stops[ i ][ 1 ] != tmpOrig.background.gradient.stops[ i ][ 1 ] ){
doGradient = true;
break;
}
}
}
if( doGradient ){
returnedObject['background-image'] = ips.utils.css.generateGradient( tmpNew.gradient.angle, tmpNew.gradient.stops, false );
}
} else {
// No gradient here, but if our original data does, we need to remove it
if( !_.isUndefined( tmpOrig.background.gradient ) ){
returnedObject['background-image'] = 'none';
}
}
}
// Font
if( !_.isUndefined( styleData.font ) ){
var tmpNew = styleData.font;
var tmpOrig = orig.sections[ sectionKey ][ styleKey ];
// Background COLOR
if( !_.isUndefined( tmpNew.color ) ){
if( _.isUndefined( tmpOrig.font ) || _.isUndefined( tmpOrig.font.color ) ||
tmpOrig.font.color != tmpNew.color ) {
returnedObject['color'] = '#' + tmpNew.color.replace('#', '');
}
}
}
return returnedObject;
},
/**
* 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,
hasFont: !_.isUndefined( item.font ),
styleid: key + '_' + itemKey,
swatch: self._buildSwatch( item, true )
});
});
}
});
this.scope.find('#vseClassList > ul').html( output );
},
/**
* 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 {mixed} Object or string
*/
_buildSwatch: function (data, toString) {
var defaultStyle = {
'display': 'block',
'background-color': 'transparent',
'background-image': 'none',
'border': 'none'
};
var main = _.clone( defaultStyle );
var sub = _.clone( defaultStyle );
if( !_.isUndefined( data.background ) ){
if( !_.isUndefined( data.background.color ) ){
_.extend( main, { 'background-color': '#' + data.background.color } );
}
if( !_.isUndefined( data.background.gradient ) ){
_.extend( main, { 'background-image': ips.utils.css.generateGradient( data.background.gradient.angle, data.background.gradient.stops ) } );
}
} else {
_.extend( main, { background: 'transparent' } );
}
if( !_.isUndefined( data.font ) && !_.isUndefined( data.font.color ) ){
_.extend( sub, { background: '#' + data.font.color } );
}
if( toString ){
var finalObj = {};
_.each( { main: main, sub: sub }, function (obj, key) {
var output = [];
var pairs = _.pairs( obj );
_.each( pairs, function (value, idx ){
if( _.isArray( value[ 1 ] ) ){
for( var i = 0; i < value[ 1 ].length; i++ ){
output.push( value[ 0 ] + ': ' + value[ 1 ][ i ] );
}
} else {
output.push( value.join(': ') );
}
});
finalObj[ key ] = output.join('; ');
});
return finalObj;
} else {
return { main: main, sub: sub };
}
},
/**
* Updates an existing swatch based on its selector
*
* @param {string} selector Selector in which to find the swatch
* @returns {void}
*/
_updateSwatch: function (selector) {
var selectorData = this._findDataBySelector( selector );
var newStyles = this._buildSwatch( selectorData.styleData, true );
// Find the swatch
var row = this.scope.find('[data-styleid="' + selectorData.sectionKey + '_' + selectorData.styleKey + '"]');
var mainSwatch = row.find('a > .vseClass_swatch');
var subSwatch = row.find('a > .vseClass_swatch > .vseClass_swatch');
// Update css
// Use the style attribute here because _buildSwatch is returning a string like background: ...; color: ...;, so .css() won't work
mainSwatch.attr( 'style', newStyles.main );
if( subSwatch.length && newStyles.sub ){
subSwatch.attr( 'style', newStyles.sub );
}
},
/**
* Returns data from our main data object based on the selector provided. Returns the style data, style key, section data and section key.
*
* @param {string} selector Selector to return data for
* @returns {object|null}
*/
_findDataBySelector: function (selector) {
var data = null;
_.each( this._data.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
if( styleData.selector == selector ){
data = {
styleData: styleData,
styleKey: styleKey,
sectionData: sectionData,
sectionKey: sectionKey
};
}
});
});
return data;
},
/**
* Returns style data for the given style ID
*
* @param {string} styleID Style ID to fetch
* @returns {mixed} Object or false
*/
_getStyleData: function (styleID) {
var pieces = styleID.split('_');
if( !_.isUndefined( this.getVSEData().sections[ pieces[0] ][ pieces[ 1 ] ] ) ){
return this.getVSEData().sections[ pieces[0] ][ pieces[ 1 ] ];
}
return false;
},
/**
* Returns an array of selectors the VSE can work with
*
* @returns {array}
*/
_getSelectors: function () {
var selectors = [];
_.each( this._data.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
selectors.push( styleData.selector );
});
});
return selectors;
},
/**
* Builds an empty panel
*
* @param {string} styleID The styleID of the panel being created
* @param {object} styleData Data for this style from our main data object
* @returns {void}
*/
_buildPanel: function (styleID, styleData) {
var output = [];
var innerContainer = $('<div/>').attr('data-role', 'widgets');
var container = $('<div/>')
.attr( 'id', styleID + '_panel' )
.attr( 'data-styleid', styleID )
.attr('data-controller', 'core.front.vse.panel')
.append( innerContainer );
if( !styleData ){
return;
}
$('#vseClassEditor')
.find('[data-role="panels"]')
.append( container )
.end()
.find( '#' + styleID + '_panel' )
.data( 'styleData', styleData );
$( document ).trigger( 'contentChange', [ container ] );
},
/**
* 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;
var setStyle = function (selector) {
if( _.isUndefined( stylesObj[ selector ] ) ){
stylesObj[ selector ] = {};
}
return stylesObj[ selector ];
};
var updateSetting = function (key, value) {
settingsObj[ key ] = value;
};
// Loop through every section then every style, and build
_.each( this._data.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
if( !_.isUndefined( styleData.background ) ){
var tmpNew = styleData.background;
var tmpOrig = orig.sections[ sectionKey ][ styleKey ].background || false;
// Background COLOR
if( !_.isUndefined( tmpNew.color ) ){
if( ( !tmpOrig || _.isUndefined( tmpOrig.color ) ) || tmpOrig.color != tmpNew.color ) {
var settingKeyColor = self._settingKey( styleData, 'background', 'color' );
// Do we have a setting for this one?
if( settingKeyColor ){
updateSetting( settingKeyColor, '#' + tmpNew.color.replace('#', '') );
} else {
setStyle( styleData.selector )[ ( !_.isUndefined( tmpNew.gradient ) ) ? 'background-color' : 'background' ] = '#' + tmpNew.color.replace('#', '');
}
}
}
// Background GRADIENT
if( !_.isUndefined( tmpNew.gradient ) ){
// First stage - figure out if our gradients have the same angle, stop locations and stop length
// If so, and if settings exist, we can just update settings. Otherwise we need to build a new gradient block
var settingKeyFrom = self._settingKey( styleData, 'background', 'from' );
var settingKeyTo = self._settingKey( styleData, 'background', 'to' );
var gradientDone = false;
if( settingKeyFrom && settingKeyTo && !_.isUndefined( tmpOrig.gradient ) ){
var stopsMatch = self._doStopLocationsMatch( tmpOrig.gradient.stops, tmpNew.gradient.stops );
if( stopsMatch && tmpOrig.gradient.angle == tmpNew.gradient.angle ){
updateSetting( settingKeyFrom, tmpNew.gradient.stops[0][0] );
updateSetting( settingKeyTo, tmpNew.gradient.stops[1][0] );
gradientDone = true;
}
}
if( !gradientDone ){
setStyle( styleData.selector )['background-image'] = ips.utils.css.generateGradient( tmpNew.gradient.angle, tmpNew.gradient.stops, false );
}
} else {
// No gradient here, but if our original data does, we need to remove it
if( !_.isUndefined( tmpOrig.gradient ) ){
setStyle( styleData.selector )['background-image'] = 'none';
}
}
}
// Font
if( !_.isUndefined( styleData.font ) ){
var tmpNew = styleData.font;
var tmpOrig = orig.sections[ sectionKey ][ styleKey ].font || false;
var settingKeyFont = self._settingKey( styleData, 'color' );
// Background COLOR
if( !_.isUndefined( tmpNew.color ) ){
if( !tmpOrig || _.isUndefined( tmpOrig.color ) || tmpOrig.color != tmpNew.color ) {
if( settingKeyFont ){
updateSetting( settingKeyFont, '#' + tmpNew.color.replace('#', '') );
} else {
setStyle( styleData.selector )['color'] = '#' + tmpNew.color.replace('#', '');
}
}
}
}
});
});
if( _.size( stylesObj ) ){
_.each( stylesObj, function (value, selector) {
styleBlock += ips.utils.css.buildStyleBlock( selector, value );
});
}
// Get other form fields
this.scope.find('#ipsTabs_vseSection_vseSettingsTab_panel :input').each( function () {
settingsObj[ $( this ).attr('name') ] = $( this ).val();
});
return {
customcss: $('#vseCustomCSS_editor').val(),
values: JSON.stringify( this._data ),
css: styleBlock,
settings: settingsObj
};
},
/**
* Finds and returns a setting key in the provided data object
*
* @param {object} data Data object to look in. 'settings' should be a child value.
* @param {string} levelOne Key of the first level inside settings
* @param {string} levelTwo Key of the second level inside settings. Optional.
* @returns {string|void}
*/
_settingKey: function (data, levelOne, levelTwo) {
if( !data.settings || !levelOne ){
return;
}
if( levelTwo ){
if( !_.isUndefined( data.settings[ levelOne ][ levelTwo ] ) ){
return data.settings[ levelOne ][ levelTwo ];
}
}
return data.settings[ levelOne ];
return;
},
/**
* Compares two gradient stop arrays, returning false if their lengths don't match or any locations differ
*
* @param {array} originalStops Original array of stops
* @param {string} newStops New array of stops
* @returns {boolean}
*/
_doStopLocationsMatch: function (originalStops, newStops) {
if( originalStops.length != newStops.length ){
return false;
}
// originalStops[ i ][ 0 ] != newStops[ i ][ 0 ] ||
for( var i = 0; i < originalStops.length; i++ ){
if( originalStops[ i ][ 1 ] != newStops[ i ][ 1 ] ){
return false;
}
}
return true;
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/vse" javascript_name="ips.vse.panel.js" javascript_type="controller" javascript_version="103021" javascript_position="1000600"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.vse.panel.js - VSE panel controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.vse.panel', {
initialize: function () {
this.on( 'widgetStyleUpdated', this.widgetStyleUpdated );
this.setup();
},
/**
* Setup method; initiates building our panels
*
* @returns {void}
*/
setup: function () {
this.data = this.scope.data('styleData');
var types = ['background', 'font'];
for( var i = 0; i < types.length; i++ ){
if( this.data[ types[ i ] ] ){
this._build( types[ i ], this.data[ types[ i ] ] );
}
}
$( document ).trigger( 'contentChange', [ this.scope ] );
},
/**
* Handles a styleUpdated event coming from a widget. Simply adds our selector to the data object
* then sends it off again.
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
widgetStyleUpdated: function (e, data) {
e.stopPropagation();
this.trigger( 'styleUpdated', {
selector: this.data.selector
});
},
/**
* Builds 'widgets' (e.g. background changer) inside this panel
*
* @param {string} type Type of widget
* @param {data} data Style data for this widget type
* @returns {void}
*/
_build: function (type, data) {
this.scope.append(
$('<div/>')
.attr('id', this.scope.attr('id') + '_' + type )
.attr( 'data-controller', 'core.front.vse.panel' + ips.utils.uppercaseFirst( type ) )
.data( 'styleData', data )
);
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/vse" javascript_name="ips.vse.panelBackground.js" javascript_type="controller" javascript_version="103021" javascript_position="1000600"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.vse.panelBackground.js - VSE background panel controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.vse.panelBackground', {
_data: {},
_gradientStops: null,
_gradientEditor: null,
_currentAngle: 0,
initialize: function () {
this.setup();
this.on( 'click', '[data-action="launchGradientEditor"]', this.launchGradientEditor );
this.on( 'click', '[data-action="gradientAngle"]', this.changeGradientAngle );
this.on( 'click', '[data-action="gradientAddStop"]', this.addGradientStop );
this.on( 'click', '[data-action="gradientRemoveStop"]', this.removeGradientStop );
this.on( 'click', '[data-action="saveGradient"]', this.saveGradient );
this.on( 'click', '[data-action="cancelGradient"]', this.cancelGradient );
this.on( 'click', '[data-action="removeGradient"]', this.removeGradient );
this.on( 'change', '[data-role="backgroundColor"]', this.changeBackground );
this.on( 'change', '[data-role="gradientEditor"] input', this.refreshGradientPreview );
},
/**
* Setup: builds the background widget HTML
*
* @returns {void}
*/
setup: function () {
this._data = this.scope.data('styleData');
// Build itself
this.scope.append( ips.templates.render('vse.panels.wrapper', {
content: ips.templates.render('vse.panels.background', {
backgroundColor: this._data.color || '417ba3'
}),
type: 'background'
}));
this._updateBackgroundPreview();
// Add jscolor to boxes
this.scope.find('input[type="text"].color').each( function () {
new jscolor.color( this );
});
},
/**
* Event handler for the background color field changing
*
* @param {event} e Event object
* @returns {void}
*/
changeBackground: function (e) {
this._data.color = $( e.currentTarget ).val();
this._updateBackgroundPreview();
this.trigger('widgetStyleUpdated');
},
/**
* Shows the gradient editor view
*
* @param {event} e Event object
* @returns {void}
*/
launchGradientEditor: function (e) {
e.preventDefault();
if( !this.scope.find('[data-role="gradientEditor"]').length ){
this._buildGradientEditor();
}
this.scope.find('[data-role="backgroundControls"]').hide();
ips.utils.anim.go( 'fadeIn', this.scope.find('[data-role="gradientEditor"]') );
},
/**
* Saves an edited gradient
*
* @param {event} e Event object
* @returns {void}
*/
saveGradient: function (e) {
e.preventDefault();
// Get gradient data
var stops = this._getGradientStops();
var angle = this._currentAngle;
// Update gradient data
// Because our data object is referenced, updating here updates it in all controllers automatically
// so we don't need to send this data upwards manually
this._data.gradient = {
angle: angle,
stops: stops
};
this._resetGradientEditor();
this._updateBackgroundPreview();
// Let the panel know a widget style has changed
this.trigger('widgetStyleUpdated');
},
/**
* Cancels an edited gradient
*
* @param {event} e Event object
* @returns {void}
*/
cancelGradient: function (e) {
e.preventDefault();
this._resetGradientEditor();
},
/**
* Removes an existing gradient
*
* @returns {void}
*/
removeGradient: function (e) {
e.preventDefault();
delete this._data.gradient;
this._resetGradientEditor();
this._updateBackgroundPreview();
// Let the panel know a widget style has changed
this.trigger('widgetStyleUpdated');
},
/**
* Refreshes the gradient preview box (called when color, location etc. changes)
*
* @returns {void}
*/
refreshGradientPreview: function () {
var gradient = ips.utils.css.generateGradient( this._currentAngle, this._getGradientStops(), false );
var previewer = this._gradientEditor.find('[data-role="gradientPreview"]');
_.each( gradient, function (val, idx) {
previewer.css( { background: val } );
});
// Enable the cancel/save buttons
this._gradientEditor.find('[data-action="cancelGradient"], [data-action="saveGradient"]').prop( 'disabled', false );
},
/**
* Event handler for changing the angle of the gradient by clicking one of the angle buttons
*
* @param {event} e Event object
* @returns {void}
*/
changeGradientAngle: function (e) {
this._gradientEditor.find('[data-action="gradientAngle"]').removeClass('ipsButton_normal').addClass('ipsButton_primary');
$( e.currentTarget ).removeClass('ipsButton_primary').addClass('ipsButton_normal');
this._currentAngle = $( e.currentTarget ).attr('data-angle');
this.refreshGradientPreview();
},
/**
* Adds a new stop to the gradient
*
* @param {event} e Event object
* @returns {void}
*/
addGradientStop: function (e) {
e.preventDefault();
this._gradientStops
.find('li:last-child')
.before( ips.templates.render('vse.gradient.stop', {
color: '444444',
location: 100
}) );
this._gradientStops
.find('li:last-child')
.prev('li')
.find('input[type="text"]')
.each( function () {
new jscolor.color( this );
});
},
/**
* Called when the order changes. We need to adjust the positions so that they are in numerical order
*
* @param {event} e Event object
* @returns {void}
*/
changeStopOrdering: function (e, ui) {
var stops = this._gradientStops.find('li:not( :first-child ):not( :last-child ) input[type="range"]');
var self = this;
// If we just have two, switch them
if( stops.length == 2 ){
var firstInput = stops.first();
var secondInput = stops.last();
var firstPos = firstInput.val();
var secondPos = secondInput.val();
firstInput.val( secondPos );
secondInput.val( firstPos );
} else {
var gaps = Math.floor( 100 / ( stops.length - 1 ) );
var pos = 0;
stops.each( function (index) {
var value = $( this ).val();
// get prev and next values
var prev = parseInt( $( stops[ index - 1 ] ).val() || 0 );
var next = parseInt( $( stops[ index + 1 ] ).val() || 100 );
// Try and reposition the stops so that they're in order
if( index == 0 && value >= next ){
// If this is the first stop, and the value is bigger than the next stop, halve it
$( this ).val( Math.floor( next / 2 ) );
} else if( index == ( stops.length - 1 ) && value <= prev ){
// If this is the last stop, and the value is smaller than the prev stop, add one (up to 100)
$( this ).val( _.min( [ 100, prev + 1 ] ) );
} else if( ( index > 0 && index < ( stops.length - 1 ) ) && ( value <= prev || value >= next ) ){
// If the value is less than the prev or more than the next, then adjust it
if( next > prev ){
$( this ).val( prev + ( ( next - prev ) / 2 ) );
} else {
$( this ).val( prev + 1 );
}
}
});
}
this.refreshGradientPreview();
},
/**
* Removes a gradient stop from the list
*
* @param {event} e Event object
* @returns {void}
*/
removeGradientStop: function (e) {
// Find the row
$( e.currentTarget ).closest('li').remove();
this.refreshGradientPreview();
},
/**
* Resets the gradient editor, removing it from the dom and resetting controller properties
*
* @returns {void}
*/
_resetGradientEditor: function () {
// Hide the editor
this._gradientEditor.hide().remove();
ips.utils.anim.go( 'fadeIn', this.scope.find('[data-role="backgroundControls"]') );
// Reset data
this._gradientEditor = null;
this._currentAngle = 0;
this._currentStops = null;
},
/**
* Updates the background preview chip using current data
*
* @returns {void}
*/
_updateBackgroundPreview: function () {
var chip = this.scope.find('[data-role="backgroundPreview"]');
// Reset the properties first
chip.css({
backgroundColor: 'transparent',
backgroundImage: 'none'
});
// Add color
if( this._data.color ){
chip.css({
backgroundColor: '#' + this._data.color
});
}
// Gradient
if( this._data.gradient ){
var gradient = ips.utils.css.generateGradient( this._data.gradient.angle, this._data.gradient.stops, false );
_.each( gradient, function (val) {
chip.css({
backgroundImage: val
});
});
}
},
/**
* Builds the gradient editor HTML, either in a default state or based on the data we have for an existing gradient
*
* @returns {void}
*/
_buildGradientEditor: function () {
var self = this;
var buttons = ( _.isUndefined( this._data.gradient ) ) ? ips.templates.render('vse.gradient.twoButtons') : ips.templates.render('vse.gradient.threeButtons');
this.scope.find('[data-role="backgroundControls"]').after(
$('<div/>').attr('data-role', 'gradientEditor').html( ips.templates.render('vse.gradient.editor', {
buttons: buttons
}) )
);
this._gradientEditor = this.scope.find('[data-role="gradientEditor"]');
this._gradientStops = this._gradientEditor.find('[data-role="gradientStops"]');
// Do we have an existing gradient to show?
if( !_.isUndefined( this._data.gradient ) ){
_.each( this._data.gradient.stops, function (value) {
self._gradientStops
.find('li:last-child')
.before( ips.templates.render('vse.gradient.stop', {
color: value[0],
location: parseInt( value[1] )
}));
});
} else {
// No existing gradient - add two default rows
this._gradientStops
.find('li:last-child')
.before( ips.templates.render('vse.gradient.stop', { color: '6592c4', location: 0 } ))
.before( ips.templates.render('vse.gradient.stop', { color: '2b517b', location: 100 } ));
}
this._currentAngle = ( _.isUndefined( this._data.gradient ) ) ? 90 : this._data.gradient.angle || 90;
this.refreshGradientPreview();
// Highlight the correct angle button
this._gradientEditor
.find('[data-action="gradientAngle"][data-angle="' + this._currentAngle + '"]')
.addClass('ipsButton_normal')
.removeClass('ipsButton_primary');
// Set up jscolor on inputs
this._gradientStops.find('input[type="text"]').each( function () {
new jscolor.color( this );
});
// Disable the save & cancel buttons for now
this._gradientEditor.find('[data-action="cancelGradient"], [data-action="saveGradient"]').prop( 'disabled', true );
// Allow stops to be sortable
ips.loader.get( ['core/interface/jquery/jquery-ui.js'] ).then( function () {
self._gradientStops.sortable({
handle: '.fa-bars',
axis: 'y',
update: _.bind( self.changeStopOrdering, self )
})
});
},
/**
* Returns an array of the current stops the gradient editor has
*
* @returns {array} Format: [ [ color, location ], [ color, location ] ]
*/
_getGradientStops: function () {
var self = this;
var output = [];
this._gradientStops.find('> li:not( :first-child ):not( :last-child )').each( function () {
output.push( [ $( this ).find('input[type="text"]').val(), $( this ).find('input[type="range"]').val() ] );
});
return output;
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/vse" javascript_name="ips.vse.panelFont.js" javascript_type="controller" javascript_version="103021" javascript_position="1000600">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.vse.panelFont.js - VSE font panel controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.vse.panelFont', {
_data: {},
initialize: function () {
this.on( 'change', '[data-role="fontColor"]', this.changeFontColor );
this.setup();
},
/**
* Setup: builds the font panel
*
* @returns {void}
*/
setup: function () {
this._data = this.scope.data('styleData');
this.scope.append( ips.templates.render('vse.panels.wrapper', {
content: ips.templates.render('vse.panels.font', {
fontColor: this._data.color
}),
type: 'font'
}) );
// Add jscolor to boxes
this.scope.find('input[type="text"].color').each( function () {
new jscolor.color( this );
});
},
/**
* Event handler for the font color box changing value
*
* @param {event} e Event object
* @returns {void}
*/
changeFontColor: function (e) {
this._data.color = $( e.currentTarget ).val();
this.trigger('widgetStyleUpdated');
}
});
}(jQuery, _));</file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/vse" javascript_name="ips.vse.window.js" javascript_type="controller" javascript_version="103021" 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,
_selectors: [],
_events: {},
_exclude: null,
_xrayElem: null,
_url: '',
_pointerEvents: false,
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');
// 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 style has been updated
*
* @param {object} data Data object
* @returns {void}
*/
updateStyle: function (data) {
ips.utils.css.replaceStyle( 'elInjectedStyles', data.selector, data.styles );
},
/**
* 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;
},
/**
* 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) {
var update = '';
_.each( data.classes, function (val, key) {
if( ( _.isObject( val ) && !_.size( val ) ) || !val ){
return;
}
update += key + "{\n";
_.each( val, function (styleVal, styleKey) {
if( _.isObject( styleVal ) ){
_.each( styleVal, function (thisVal) {
update += styleKey + ': ' + thisVal + ";\n";
});
} else {
update += styleKey + ': ' + styleVal + ";\n";
}
});
update += "}\n\n";
});
this._stylesheet.html( update );
this._custom.html( data.custom || '' );
},
/**
* Starts the xray
*
* @returns {void}
*/
xrayStart: function () {
this._createXRayElem();
this._pointerEvents = this._supportsPointerEvents();
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
* If browser supports pointerEvents we highlight the hovered element, otherwise do nothing
*
* @param {event} e Event object
* @returns {void}
*/
_doOver: function (e) {
if( this._pointerEvents ){
this._doXray(e);
} else {
this._doFalse(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) {
// Get all ancestors
var ancestors = elem.parents().addBack( elem );
var matched = [];
var primaryMatch = null;
var self = this;
var selectorsLength = this._selectors.length;
_.each( ancestors.get().reverse(), function (value, idx) {
for( var i = 0; i < selectorsLength; i++ ){
if( $( value ).is( self._selectors[ i ] ) ){
if( idx == 0 ){
primaryMatch = self._selectors[ i ];
} else {
matched.push( self._selectors[ i ] );
}
}
}
});
this.sendCommand( 'selectorsMatched', {
primary: primaryMatch,
other: matched
});
},
/**
* Determines whether the browser supports css pointer-events style
*
* @param {event} e Event object
* @returns {void}
*/
_supportsPointerEvents: function () {
// From https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js
var element = document.createElement('x');
element.style.cssText = 'pointer-events:auto';
return element.style.pointerEvents === 'auto';
}
});
}(jQuery, _));]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="controllers/widgets" javascript_name="ips.widgets.area.js" javascript_type="controller" javascript_version="103021" javascript_position="1000750"><![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');
// 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', {
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="103021" javascript_position="1000750"><![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 );
$( 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();
});
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 );
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 );
});
},
/**
* 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,
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);
}
},
/**
* 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="103021" javascript_position="1000750"><![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( 'click', '[data-role="availableBlocks"] [data-action="toggleSection"]', this.toggleSection );
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');
}
},
/**
* Toggles a block list in the manager panel
*
* @param {event} e The event
* @returns {void}
*/
toggleSection: function (e) {
var target = $( e.currentTarget );
if( target.hasClass('cSidebarManager_open') ){
target
.removeClass('cSidebarManager_open')
.addClass('cSidebarManager_closed')
.find('+ ul, + ul + p')
.hide();
} else {
target
.removeClass('cSidebarManager_closed')
.addClass('cSidebarManager_open');
if( target.find('+ ul > li:not( [data-hidden] )').length ) {
ips.utils.anim.go( 'fadeIn', target.find('+ ul') );
} else {
target.find('+ ul + p').show();
}
}
},
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;
if( $('html').attr("dir") == "rtl"){
$('body').css({
marginRight: '+=300px'
});
}
else{
$('body').css({
marginLeft: '+=300px'
});
}
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 () {
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;
if ($('html').attr("dir") == "rtl"){
$('body').css({
marginRight: '-=300px'
}, 'fast');
} else {
$('body').css({
marginLeft: '-=300px'
}, 'fast');
}
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="103021" javascript_position="1000750"><![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]').not('.ipsHide').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="103021" 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="jquery" javascript_name="jquery.dotdotdot.min.js" javascript_type="framework" javascript_version="103021" javascript_position="103"><![CDATA[/*
* jQuery dotdotdot 1.8.3
*
* Copyright (c) Fred Heusschen
* www.frebsite.nl
*
* 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="103021" javascript_position="102"><![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("<","<").replace(">",">").replace(" & "," & ")}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="103021" javascript_position="103"><![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="103021" javascript_position="103"><![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="103021" javascript_position="101"><![CDATA[/*! jQuery v2.2.4 | (c) jQuery Foundation | jquery.org/license */
!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="2.2.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isPlainObject:function(a){var b;if("object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype||{},"isPrototypeOf"))return!1;for(b in a);return void 0===b||k.call(a,b)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=d.createElement("script"),b.text=a,d.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:h.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(d=e.call(arguments,2),f=function(){return a.apply(b||this,d.concat(e.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=la(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=ma(b);function pa(){}pa.prototype=d.filters=d.pseudos,d.setFilters=new pa,g=fa.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=R.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=S.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(Q," ")}),h=h.slice(c.length));for(g in d.filter)!(e=W[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fa.error(a):z(a,i).slice(0)};function qa(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return h.call(b,a)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&f.parentNode&&(this.length=1,this[0]=f),this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?void 0!==c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?h.call(n(a),this[0]):h.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||n.uniqueSort(e),D.test(a)&&e.reverse()),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){n.each(b,function(b,c){n.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==n.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return n.each(arguments,function(a,b){var c;while((c=n.inArray(b,f,c))>-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.removeEventListener("DOMContentLoaded",J),a.removeEventListener("load",J),n.ready()}n.ready.promise=function(b){return I||(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(n.ready):(d.addEventListener("DOMContentLoaded",J),a.addEventListener("load",J))),I.promise(b)},n.ready.promise();var K=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)K(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},L=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function M(){this.expando=n.expando+M.uid++}M.uid=1,M.prototype={register:function(a,b){var c=b||{};return a.nodeType?a[this.expando]=c:Object.defineProperty(a,this.expando,{value:c,writable:!0,configurable:!0}),a[this.expando]},cache:function(a){if(!L(a))return{};var b=a[this.expando];return b||(b={},L(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[b]=c;else for(d in b)e[d]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=a[this.expando];if(void 0!==f){if(void 0===b)this.register(a);else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in f?d=[b,e]:(d=e,d=d in f?[d]:d.match(G)||[])),c=d.length;while(c--)delete f[d[c]]}(void 0===b||n.isEmptyObject(f))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!n.isEmptyObject(b)}};var N=new M,O=new M,P=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Q=/[A-Z]/g;function R(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Q,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:P.test(c)?n.parseJSON(c):c;
}catch(e){}O.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return O.hasData(a)||N.hasData(a)},data:function(a,b,c){return O.access(a,b,c)},removeData:function(a,b){O.remove(a,b)},_data:function(a,b,c){return N.access(a,b,c)},_removeData:function(a,b){N.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=O.get(f),1===f.nodeType&&!N.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),R(f,d,e[d])));N.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){O.set(this,a)}):K(this,function(b){var c,d;if(f&&void 0===b){if(c=O.get(f,a)||O.get(f,a.replace(Q,"-$&").toLowerCase()),void 0!==c)return c;if(d=n.camelCase(a),c=O.get(f,d),void 0!==c)return c;if(c=R(f,d,void 0),void 0!==c)return c}else d=n.camelCase(a),this.each(function(){var c=O.get(this,d);O.set(this,d,b),a.indexOf("-")>-1&&void 0!==c&&O.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){O.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=N.get(a,b),c&&(!d||n.isArray(c)?d=N.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return N.get(a,c)||N.access(a,c,{empty:n.Callbacks("once memory").add(function(){N.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=N.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var S=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),U=["Top","Right","Bottom","Left"],V=function(a,b){return a=b||a,"none"===n.css(a,"display")||!n.contains(a.ownerDocument,a)};function W(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return n.css(a,b,"")},i=h(),j=c&&c[3]||(n.cssNumber[b]?"":"px"),k=(n.cssNumber[b]||"px"!==j&&+i)&&T.exec(n.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,n.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var X=/^(?:checkbox|radio)$/i,Y=/<([\w:-]+)/,Z=/^$|\/(?:java|ecma)script/i,$={option:[1,"<select multiple='multiple'>","</select>"],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,"",""]};$.optgroup=$.option,$.tbody=$.tfoot=$.colgroup=$.caption=$.thead,$.th=$.td;function _(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function aa(a,b){for(var c=0,d=a.length;d>c;c++)N.set(a[c],"globalEval",!b||N.get(b[c],"globalEval"))}var ba=/<|&#?\w+;/;function ca(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],o=0,p=a.length;p>o;o++)if(f=a[o],f||0===f)if("object"===n.type(f))n.merge(m,f.nodeType?[f]:f);else if(ba.test(f)){g=g||l.appendChild(b.createElement("div")),h=(Y.exec(f)||["",""])[1].toLowerCase(),i=$[h]||$._default,g.innerHTML=i[1]+n.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;n.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",o=0;while(f=m[o++])if(d&&n.inArray(f,d)>-1)e&&e.push(f);else if(j=n.contains(f.ownerDocument,f),g=_(l.appendChild(f),"script"),j&&aa(g),c){k=0;while(f=g[k++])Z.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},props:"altKey bubbles cancelable ctrlKey currentTarget detail eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,e,f,g=b.button;return null==a.pageX&&null!=b.clientX&&(c=a.target.ownerDocument||d,e=c.documentElement,f=c.body,a.pageX=b.clientX+(e&&e.scrollLeft||f&&f.scrollLeft||0)-(e&&e.clientLeft||f&&f.clientLeft||0),a.pageY=b.clientY+(e&&e.scrollTop||f&&f.scrollTop||0)-(e&&e.clientTop||f&&f.clientTop||0)),a.which||void 0===g||(a.which=1&g?1:2&g?3:4&g?2:0),a}},fix:function(a){if(a[n.expando])return a;var b,c,e,f=a.type,g=a,h=this.fixHooks[f];h||(this.fixHooks[f]=h=ea.test(f)?this.mouseHooks:da.test(f)?this.keyHooks:{}),e=h.props?this.props.concat(h.props):this.props,a=new n.Event(g),b=e.length;while(b--)c=e[b],a[c]=g[c];return a.target||(a.target=d),3===a.target.nodeType&&(a.target=a.target.parentNode),h.filter?h.filter(a,g):a},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==ia()&&this.focus?(this.focus(),!1):void 0},delegateType:"focusin"},blur:{trigger:function(){return this===ia()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&n.nodeName(this,"input")?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},n.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ga:ha):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={constructor:n.Event,isDefaultPrevented:ha,isPropagationStopped:ha,isImmediatePropagationStopped:ha,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ga,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ga,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ga,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||n.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),n.fn.extend({on:function(a,b,c,d){return ja(this,a,b,c,d)},one:function(a,b,c,d){return ja(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=ha),this.each(function(){n.event.remove(this,a,c,b)})}});var ka=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,la=/<script|<style|<link/i,ma=/checked\s*(?:[^=]|=\s*.checked.)/i,na=/^true\/(.*)/,oa=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=wa[0].contentDocument,b.write(),b.close(),c=ya(a,b),wa.detach()),xa[a]=c),c}var Aa=/^margin/,Ba=new RegExp("^("+S+")(?!px)[a-z%]+$","i"),Ca=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)},Da=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e},Ea=d.documentElement;!function(){var b,c,e,f,g=d.createElement("div"),h=d.createElement("div");if(h.style){h.style.backgroundClip="content-box",h.cloneNode(!0).style.backgroundClip="",l.clearCloneStyle="content-box"===h.style.backgroundClip,g.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",g.appendChild(h);function i(){h.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",h.innerHTML="",Ea.appendChild(g);var d=a.getComputedStyle(h);b="1%"!==d.top,f="2px"===d.marginLeft,c="4px"===d.width,h.style.marginRight="50%",e="4px"===d.marginRight,Ea.removeChild(g)}n.extend(l,{pixelPosition:function(){return i(),b},boxSizingReliable:function(){return null==c&&i(),c},pixelMarginRight:function(){return null==c&&i(),e},reliableMarginLeft:function(){return null==c&&i(),f},reliableMarginRight:function(){var b,c=h.appendChild(d.createElement("div"));return c.style.cssText=h.style.cssText="-webkit-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",c.style.marginRight=c.style.width="0",h.style.width="1px",Ea.appendChild(g),b=!parseFloat(a.getComputedStyle(c).marginRight),Ea.removeChild(g),h.removeChild(c),b}})}}();function Fa(a,b,c){var d,e,f,g,h=a.style;return c=c||Ca(a),g=c?c.getPropertyValue(b)||c[b]:void 0,""!==g&&void 0!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),c&&!l.pixelMarginRight()&&Ba.test(g)&&Aa.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f),void 0!==g?g+"":g}function Ga(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Ha=/^(none|table(?!-c[ea]).+)/,Ia={position:"absolute",visibility:"hidden",display:"block"},Ja={letterSpacing:"0",fontWeight:"400"},Ka=["Webkit","O","Moz","ms"],La=d.createElement("div").style;function Ma(a){if(a in La)return a;var b=a[0].toUpperCase()+a.slice(1),c=Ka.length;while(c--)if(a=Ka[c]+b,a in La)return a}function Na(a,b,c){var d=T.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Oa(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=n.css(a,c+U[f],!0,e)),d?("content"===c&&(g-=n.css(a,"padding"+U[f],!0,e)),"margin"!==c&&(g-=n.css(a,"border"+U[f]+"Width",!0,e))):(g+=n.css(a,"padding"+U[f],!0,e),"padding"!==c&&(g+=n.css(a,"border"+U[f]+"Width",!0,e)));return g}function Pa(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ca(a),g="border-box"===n.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Fa(a,b,f),(0>e||null==e)&&(e=a.style[b]),Ba.test(e))return e;d=g&&(l.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Oa(a,b,c||(g?"border":"content"),d,f)+"px"}function Qa(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=N.get(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&V(d)&&(f[g]=N.access(d,"olddisplay",za(d.nodeName)))):(e=V(d),"none"===c&&e||N.set(d,"olddisplay",e?c:n.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Fa(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;return b=n.cssProps[h]||(n.cssProps[h]=Ma(h)||h),g=n.cssHooks[b]||n.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=T.exec(c))&&e[1]&&(c=W(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(n.cssNumber[h]?"":"px")),l.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=Ma(h)||h),g=n.cssHooks[b]||n.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Fa(a,b,d)),"normal"===e&&b in Ja&&(e=Ja[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),n.each(["height","width"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?Ha.test(n.css(a,"display"))&&0===a.offsetWidth?Da(a,Ia,function(){return Pa(a,b,d)}):Pa(a,b,d):void 0},set:function(a,c,d){var e,f=d&&Ca(a),g=d&&Oa(a,b,d,"border-box"===n.css(a,"boxSizing",!1,f),f);return g&&(e=T.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=n.css(a,b)),Na(a,c,g)}}}),n.cssHooks.marginLeft=Ga(l.reliableMarginLeft,function(a,b){return b?(parseFloat(Fa(a,"marginLeft"))||a.getBoundingClientRect().left-Da(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px":void 0}),n.cssHooks.marginRight=Ga(l.reliableMarginRight,function(a,b){return b?Da(a,{display:"inline-block"},Fa,[a,"marginRight"]):void 0}),n.each({margin:"",padding:"",border:"Width"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+U[d]+b]=f[d]||f[d-2]||f[0];return e}},Aa.test(a)||(n.cssHooks[a+b].set=Na)}),n.fn.extend({css:function(a,b){return K(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=Ca(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return Qa(this,!0)},hide:function(){return Qa(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){V(this)?n(this).show():n(this).hide()})}});function Ra(a,b,c,d,e){return new Ra.prototype.init(a,b,c,d,e)}n.Tween=Ra,Ra.prototype={constructor:Ra,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||n.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?"":"px")},cur:function(){var a=Ra.propHooks[this.prop];return a&&a.get?a.get(this):Ra.propHooks._default.get(this)},run:function(a){var b,c=Ra.propHooks[this.prop];return this.options.duration?this.pos=b=n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ra.propHooks._default.set(this),this}},Ra.prototype.init.prototype=Ra.prototype,Ra.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=n.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[n.cssProps[a.prop]]&&!n.cssHooks[a.prop]?a.elem[a.prop]=a.now:n.style(a.elem,a.prop,a.now+a.unit)}}},Ra.propHooks.scrollTop=Ra.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},n.fx=Ra.prototype.init,n.fx.step={};var Sa,Ta,Ua=/^(?:toggle|show|hide)$/,Va=/queueHooks$/;function Wa(){return a.setTimeout(function(){Sa=void 0}),Sa=n.now()}function Xa(a,b){var c,d=0,e={height:a};for(b=b?1:0;4>d;d+=2-b)c=U[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function Ya(a,b,c){for(var d,e=(_a.tweeners[b]||[]).concat(_a.tweeners["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function Za(a,b,c){var d,e,f,g,h,i,j,k,l=this,m={},o=a.style,p=a.nodeType&&V(a),q=N.get(a,"fxshow");c.queue||(h=n._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,l.always(function(){l.always(function(){h.unqueued--,n.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=n.css(a,"display"),k="none"===j?N.get(a,"olddisplay")||za(a.nodeName):j,"inline"===k&&"none"===n.css(a,"float")&&(o.display="inline-block")),c.overflow&&(o.overflow="hidden",l.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],Ua.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}m[d]=q&&q[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(m))"inline"===("none"===j?za(a.nodeName):j)&&(o.display=j);else{q?"hidden"in q&&(p=q.hidden):q=N.access(a,"fxshow",{}),f&&(q.hidden=!p),p?n(a).show():l.done(function(){n(a).hide()}),l.done(function(){var b;N.remove(a,"fxshow");for(b in m)n.style(a,b,m[b])});for(d in m)g=Ya(p?q[d]:0,d,l),d in q||(q[d]=g.start,p&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function $a(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function _a(a,b,c){var d,e,f=0,g=_a.prefilters.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Sa||Wa(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{},easing:n.easing._default},c),originalProperties:b,originalOptions:c,startTime:Sa||Wa(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for($a(k,j.opts.specialEasing);g>f;f++)if(d=_a.prefilters[f].call(j,a,k,j.opts))return n.isFunction(d.stop)&&(n._queueHooks(j.elem,j.opts.queue).stop=n.proxy(d.stop,d)),d;return n.map(k,Ya,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(_a,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return W(c.elem,a,T.exec(b),c),c}]},tweener:function(a,b){n.isFunction(a)?(b=a,a=["*"]):a=a.match(G);for(var c,d=0,e=a.length;e>d;d++)c=a[d],_a.tweeners[c]=_a.tweeners[c]||[],_a.tweeners[c].unshift(b)},prefilters:[Za],prefilter:function(a,b){b?_a.prefilters.unshift(a):_a.prefilters.push(a)}}),n.speed=function(a,b,c){var d=a&&"object"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,null!=d.queue&&d.queue!==!0||(d.queue="fx"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(V).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=_a(this,n.extend({},a),f);(e||N.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=n.timers,g=N.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&Va.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=N.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each(["toggle","show","hide"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(Xa(b,!0),a,d,e)}}),n.each({slideDown:Xa("show"),slideUp:Xa("hide"),slideToggle:Xa("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=0,c=n.timers;for(Sa=n.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||n.fx.stop(),Sa=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){Ta||(Ta=a.setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){a.clearInterval(Ta),Ta=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(b,c){return b=n.fx?n.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",l.checkOn=""!==a.value,l.optSelected=c.selected,b.disabled=!0,l.optDisabled=!c.disabled,a=d.createElement("input"),a.value="t",a.type="radio",l.radioValue="t"===a.value}();var ab,bb=n.expr.attrHandle;n.fn.extend({attr:function(a,b){return K(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),e=n.attrHooks[b]||(n.expr.match.bool.test(b)?ab:void 0)),void 0!==c?null===c?void n.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=n.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!l.radioValue&&"radio"===b&&n.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(G);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)&&(a[d]=!1),a.removeAttribute(c)}}),ab={set:function(a,b,c){return b===!1?n.removeAttr(a,c):a.setAttribute(c,c),c}},n.each(n.expr.match.bool.source.match(/\w+/g),function(a,b){var c=bb[b]||n.find.attr;bb[b]=function(a,b,d){var e,f;return d||(f=bb[b],bb[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,bb[b]=f),e}});var cb=/^(?:input|select|textarea|button)$/i,db=/^(?:a|area)$/i;n.fn.extend({prop:function(a,b){return K(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[n.propFix[a]||a]})}}),n.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&n.isXMLDoc(a)||(b=n.propFix[b]||b,e=n.propHooks[b]),
void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=n.find.attr(a,"tabindex");return b?parseInt(b,10):cb.test(a.nodeName)||db.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),l.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),n.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){n.propFix[this.toLowerCase()]=this});var eb=/[\t\r\n\f]/g;function fb(a){return a.getAttribute&&a.getAttribute("class")||""}n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,fb(this)))});if("string"==typeof a&&a){b=a.match(G)||[];while(c=this[i++])if(e=fb(c),d=1===c.nodeType&&(" "+e+" ").replace(eb," ")){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=n.trim(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,fb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(G)||[];while(c=this[i++])if(e=fb(c),d=1===c.nodeType&&(" "+e+" ").replace(eb," ")){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=n.trim(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):n.isFunction(a)?this.each(function(c){n(this).toggleClass(a.call(this,c,fb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=n(this),f=a.match(G)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=fb(this),b&&N.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":N.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+fb(c)+" ").replace(eb," ").indexOf(b)>-1)return!0;return!1}});var gb=/\r/g,hb=/[\x20\t\r\n\f]+/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e="":"number"==typeof e?e+="":n.isArray(e)&&(e=n.map(e,function(a){return null==a?"":a+""})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(gb,""):null==c?"":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,"value");return null!=b?b:n.trim(n.text(a)).replace(hb," ")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],(c.selected||i===e)&&(l.optDisabled?!c.disabled:null===c.getAttribute("disabled"))&&(!c.parentNode.disabled||!n.nodeName(c.parentNode,"optgroup"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=n.inArray(n.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),n.each(["radio","checkbox"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>-1:void 0}},l.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var ib=/^(?:focusinfocus|focusoutblur)$/;n.extend(n.event,{trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!ib.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),l=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},f||!o.trigger||o.trigger.apply(e,c)!==!1)){if(!f&&!o.noBubble&&!n.isWindow(e)){for(j=o.delegateType||q,ib.test(j+q)||(h=h.parentNode);h;h=h.parentNode)p.push(h),i=h;i===(e.ownerDocument||d)&&p.push(i.defaultView||i.parentWindow||a)}g=0;while((h=p[g++])&&!b.isPropagationStopped())b.type=g>1?j:o.bindType||q,m=(N.get(h,"events")||{})[b.type]&&N.get(h,"handle"),m&&m.apply(h,c),m=l&&h[l],m&&m.apply&&L(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=q,f||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!L(e)||l&&n.isFunction(e[q])&&!n.isWindow(e)&&(i=e[l],i&&(e[l]=null),n.event.triggered=q,e[q](),n.event.triggered=void 0,i&&(e[l]=i)),b.result}},simulate:function(a,b,c){var d=n.extend(new n.Event,c,{type:a,isSimulated:!0});n.event.trigger(d,null,b)}}),n.fn.extend({trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}}),n.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),l.focusin="onfocusin"in a,l.focusin||n.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a))};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=N.access(d,b);e||d.addEventListener(a,c,!0),N.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=N.access(d,b)-1;e?N.access(d,b,e):(d.removeEventListener(a,c,!0),N.remove(d,b))}}});var jb=a.location,kb=n.now(),lb=/\?/;n.parseJSON=function(a){return JSON.parse(a+"")},n.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||n.error("Invalid XML: "+b),c};var mb=/#.*$/,nb=/([?&])_=[^&]*/,ob=/^(.*?):[ \t]*([^\r\n]*)$/gm,pb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,qb=/^(?:GET|HEAD)$/,rb=/^\/\//,sb={},tb={},ub="*/".concat("*"),vb=d.createElement("a");vb.href=jb.href;function wb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(G)||[];if(n.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function xb(a,b,c,d){var e={},f=a===tb;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function yb(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&n.extend(!0,a,d),a}function zb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Ab(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:jb.href,type:"GET",isLocal:pb.test(jb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":ub,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":n.parseJSON,"text xml":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?yb(yb(a,n.ajaxSettings),b):yb(n.ajaxSettings,a)},ajaxPrefilter:wb(sb),ajaxTransport:wb(tb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m=n.ajaxSetup({},c),o=m.context||m,p=m.context&&(o.nodeType||o.jquery)?n(o):n.event,q=n.Deferred(),r=n.Callbacks("once memory"),s=m.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,getResponseHeader:function(a){var b;if(2===v){if(!h){h={};while(b=ob.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===v?g:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return v||(a=u[c]=u[c]||a,t[a]=b),this},overrideMimeType:function(a){return v||(m.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>v)for(b in a)s[b]=[s[b],a[b]];else x.always(a[x.status]);return this},abort:function(a){var b=a||w;return e&&e.abort(b),z(0,b),this}};if(q.promise(x).complete=r.add,x.success=x.done,x.error=x.fail,m.url=((b||m.url||jb.href)+"").replace(mb,"").replace(rb,jb.protocol+"//"),m.type=c.method||c.type||m.method||m.type,m.dataTypes=n.trim(m.dataType||"*").toLowerCase().match(G)||[""],null==m.crossDomain){j=d.createElement("a");try{j.href=m.url,j.href=j.href,m.crossDomain=vb.protocol+"//"+vb.host!=j.protocol+"//"+j.host}catch(y){m.crossDomain=!0}}if(m.data&&m.processData&&"string"!=typeof m.data&&(m.data=n.param(m.data,m.traditional)),xb(sb,m,c,x),2===v)return x;k=n.event&&m.global,k&&0===n.active++&&n.event.trigger("ajaxStart"),m.type=m.type.toUpperCase(),m.hasContent=!qb.test(m.type),f=m.url,m.hasContent||(m.data&&(f=m.url+=(lb.test(f)?"&":"?")+m.data,delete m.data),m.cache===!1&&(m.url=nb.test(f)?f.replace(nb,"$1_="+kb++):f+(lb.test(f)?"&":"?")+"_="+kb++)),m.ifModified&&(n.lastModified[f]&&x.setRequestHeader("If-Modified-Since",n.lastModified[f]),n.etag[f]&&x.setRequestHeader("If-None-Match",n.etag[f])),(m.data&&m.hasContent&&m.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",m.contentType),x.setRequestHeader("Accept",m.dataTypes[0]&&m.accepts[m.dataTypes[0]]?m.accepts[m.dataTypes[0]]+("*"!==m.dataTypes[0]?", "+ub+"; q=0.01":""):m.accepts["*"]);for(l in m.headers)x.setRequestHeader(l,m.headers[l]);if(m.beforeSend&&(m.beforeSend.call(o,x,m)===!1||2===v))return x.abort();w="abort";for(l in{success:1,error:1,complete:1})x[l](m[l]);if(e=xb(tb,m,c,x)){if(x.readyState=1,k&&p.trigger("ajaxSend",[x,m]),2===v)return x;m.async&&m.timeout>0&&(i=a.setTimeout(function(){x.abort("timeout")},m.timeout));try{v=1,e.send(t,z)}catch(y){if(!(2>v))throw y;z(-1,y)}}else z(-1,"No Transport");function z(b,c,d,h){var j,l,t,u,w,y=c;2!==v&&(v=2,i&&a.clearTimeout(i),e=void 0,g=h||"",x.readyState=b>0?4:0,j=b>=200&&300>b||304===b,d&&(u=zb(m,x,d)),u=Ab(m,u,x,j),j?(m.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(n.lastModified[f]=w),w=x.getResponseHeader("etag"),w&&(n.etag[f]=w)),204===b||"HEAD"===m.type?y="nocontent":304===b?y="notmodified":(y=u.state,l=u.data,t=u.error,j=!t)):(t=y,!b&&y||(y="error",0>b&&(b=0))),x.status=b,x.statusText=(c||y)+"",j?q.resolveWith(o,[l,y,x]):q.rejectWith(o,[x,y,t]),x.statusCode(s),s=void 0,k&&p.trigger(j?"ajaxSuccess":"ajaxError",[x,m,j?l:t]),r.fireWith(o,[x,y]),k&&(p.trigger("ajaxComplete",[x,m]),--n.active||n.event.trigger("ajaxStop")))}return x},getJSON:function(a,b,c){return n.get(a,b,c,"json")},getScript:function(a,b){return n.get(a,void 0,b,"script")}}),n.each(["get","post"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax(n.extend({url:a,type:b,dataType:e,data:c,success:d},n.isPlainObject(a)&&a))}}),n._evalUrl=function(a){return n.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},n.fn.extend({wrapAll:function(a){var b;return n.isFunction(a)?this.each(function(b){n(this).wrapAll(a.call(this,b))}):(this[0]&&(b=n(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this)},wrapInner:function(a){return n.isFunction(a)?this.each(function(b){n(this).wrapInner(a.call(this,b))}):this.each(function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,"body")||n(this).replaceWith(this.childNodes)}).end()}}),n.expr.filters.hidden=function(a){return!n.expr.filters.visible(a)},n.expr.filters.visible=function(a){return a.offsetWidth>0||a.offsetHeight>0||a.getClientRects().length>0};var Bb=/%20/g,Cb=/\[\]$/,Db=/\r?\n/g,Eb=/^(?:submit|button|image|reset|file)$/i,Fb=/^(?:input|select|textarea|keygen)/i;function Gb(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||Cb.test(a)?d(a,e):Gb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==n.type(b))d(a,b);else for(e in b)Gb(a+"["+e+"]",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)Gb(c,a[c],b,e);return d.join("&").replace(Bb,"+")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,"elements");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(":disabled")&&Fb.test(this.nodeName)&&!Eb.test(a)&&(this.checked||!X.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(Db,"\r\n")}}):{name:b.name,value:c.replace(Db,"\r\n")}}).get()}}),n.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Hb={0:200,1223:204},Ib=n.ajaxSettings.xhr();l.cors=!!Ib&&"withCredentials"in Ib,l.ajax=Ib=!!Ib,n.ajaxTransport(function(b){var c,d;return l.cors||Ib&&!b.crossDomain?{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Hb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}:void 0}),n.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),n.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=n("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Jb=[],Kb=/(=)\?(?=&|$)|\?\?/;n.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Jb.pop()||n.expando+"_"+kb++;return this[a]=!0,a}}),n.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Kb.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&Kb.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Kb,"$1"+e):b.jsonp!==!1&&(b.url+=(lb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||n.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?n(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Jb.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),n.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||d;var e=x.exec(a),f=!c&&[];return e?[b.createElement(e[1])]:(e=ca([a],b,f),f&&f.length&&n(f).remove(),n.merge([],e.childNodes))};var Lb=n.fn.load;n.fn.load=function(a,b,c){if("string"!=typeof a&&Lb)return Lb.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=n.trim(a.slice(h)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&n.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?n("<div>").append(n.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},n.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};function Mb(a){return n.isWindow(a)?a:9===a.nodeType&&a.defaultView}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,n.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d=this[0],e={top:0,left:0},f=d&&d.ownerDocument;if(f)return b=f.documentElement,n.contains(b,d)?(e=d.getBoundingClientRect(),c=Mb(f),{top:e.top+c.pageYOffset-b.clientTop,left:e.left+c.pageXOffset-b.clientLeft}):e},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===n.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(d=a.offset()),d.top+=n.css(a[0],"borderTopWidth",!0),d.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-d.top-n.css(c,"marginTop",!0),left:b.left-d.left-n.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===n.css(a,"position"))a=a.offsetParent;return a||Ea})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;n.fn[a]=function(d){return K(this,function(a,d,e){var f=Mb(a);return void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=Ga(l.pixelPosition,function(a,c){return c?(c=Fa(a,b),Ba.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return K(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)},size:function(){return this.length}}),n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var Nb=a.jQuery,Ob=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=Ob),b&&a.jQuery===n&&(a.jQuery=Nb),n},b||(a.jQuery=a.$=n),n});]]></file>
<file javascript_app="global" javascript_location="library" javascript_path="jquery" javascript_name="jquery.transform.js" javascript_type="framework" javascript_version="103021" javascript_position="103"><![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="jstz" javascript_name="jstz.js" javascript_type="framework" javascript_version="103021" javascript_position="200"><![CDATA[/* jstz.min.js Version: 1.0.6 Build date: 2015-11-04 */
!function(e){var a=function(){"use strict";var e="s",s={DAY:864e5,HOUR:36e5,MINUTE:6e4,SECOND:1e3,BASELINE_YEAR:2014,MAX_SCORE:864e6,AMBIGUITIES:{"America/Denver":["America/Mazatlan"],"Europe/London":["Africa/Casablanca"],"America/Chicago":["America/Mexico_City"],"America/Asuncion":["America/Campo_Grande","America/Santiago"],"America/Montevideo":["America/Sao_Paulo","America/Santiago"],"Asia/Beirut":["Asia/Amman","Asia/Jerusalem","Europe/Helsinki","Asia/Damascus","Africa/Cairo","Asia/Gaza","Europe/Minsk"],"Pacific/Auckland":["Pacific/Fiji"],"America/Los_Angeles":["America/Santa_Isabel"],"America/New_York":["America/Havana"],"America/Halifax":["America/Goose_Bay"],"America/Godthab":["America/Miquelon"],"Asia/Dubai":["Asia/Yerevan"],"Asia/Jakarta":["Asia/Krasnoyarsk"],"Asia/Shanghai":["Asia/Irkutsk","Australia/Perth"],"Australia/Sydney":["Australia/Lord_Howe"],"Asia/Tokyo":["Asia/Yakutsk"],"Asia/Dhaka":["Asia/Omsk"],"Asia/Baku":["Asia/Yerevan"],"Australia/Brisbane":["Asia/Vladivostok"],"Pacific/Noumea":["Asia/Vladivostok"],"Pacific/Majuro":["Asia/Kamchatka","Pacific/Fiji"],"Pacific/Tongatapu":["Pacific/Apia"],"Asia/Baghdad":["Europe/Minsk","Europe/Moscow"],"Asia/Karachi":["Asia/Yekaterinburg"],"Africa/Johannesburg":["Asia/Gaza","Africa/Cairo"]}},i=function(e){var a=-e.getTimezoneOffset();return null!==a?a:0},r=function(){var a=i(new Date(s.BASELINE_YEAR,0,2)),r=i(new Date(s.BASELINE_YEAR,5,2)),n=a-r;return 0>n?a+",1":n>0?r+",1,"+e:a+",0"},n=function(){var e,a;if("undefined"!=typeof Intl&&"undefined"!=typeof Intl.DateTimeFormat&&(e=Intl.DateTimeFormat(),"undefined"!=typeof e&&"undefined"!=typeof e.resolvedOptions))return a=e.resolvedOptions().timeZone,a&&(a.indexOf("/")>-1||"UTC"===a)?a:void 0},o=function(e){for(var a=new Date(e,0,1,0,0,1,0).getTime(),s=new Date(e,12,31,23,59,59).getTime(),i=a,r=new Date(i).getTimezoneOffset(),n=null,o=null;s-864e5>i;){var t=new Date(i),A=t.getTimezoneOffset();A!==r&&(r>A&&(n=t),A>r&&(o=t),r=A),i+=864e5}return n&&o?{s:u(n).getTime(),e:u(o).getTime()}:!1},u=function l(e,a,i){"undefined"==typeof a&&(a=s.DAY,i=s.HOUR);for(var r=new Date(e.getTime()-a).getTime(),n=e.getTime()+a,o=new Date(r).getTimezoneOffset(),u=r,t=null;n-i>u;){var A=new Date(u),c=A.getTimezoneOffset();if(c!==o){t=A;break}u+=i}return a===s.DAY?l(t,s.HOUR,s.MINUTE):a===s.HOUR?l(t,s.MINUTE,s.SECOND):t},t=function(e,a,s,i){if("N/A"!==s)return s;if("Asia/Beirut"===a){if("Africa/Cairo"===i.name&&13983768e5===e[6].s&&14116788e5===e[6].e)return 0;if("Asia/Jerusalem"===i.name&&13959648e5===e[6].s&&14118588e5===e[6].e)return 0}else if("America/Santiago"===a){if("America/Asuncion"===i.name&&14124816e5===e[6].s&&1397358e6===e[6].e)return 0;if("America/Campo_Grande"===i.name&&14136912e5===e[6].s&&13925196e5===e[6].e)return 0}else if("America/Montevideo"===a){if("America/Sao_Paulo"===i.name&&14136876e5===e[6].s&&1392516e6===e[6].e)return 0}else if("Pacific/Auckland"===a&&"Pacific/Fiji"===i.name&&14142456e5===e[6].s&&13961016e5===e[6].e)return 0;return s},A=function(e,i){for(var r=function(a){for(var r=0,n=0;n<e.length;n++)if(a.rules[n]&&e[n]){if(!(e[n].s>=a.rules[n].s&&e[n].e<=a.rules[n].e)){r="N/A";break}if(r=0,r+=Math.abs(e[n].s-a.rules[n].s),r+=Math.abs(a.rules[n].e-e[n].e),r>s.MAX_SCORE){r="N/A";break}}return r=t(e,i,r,a)},n={},o=a.olson.dst_rules.zones,u=o.length,A=s.AMBIGUITIES[i],c=0;u>c;c++){var m=o[c],l=r(o[c]);"N/A"!==l&&(n[m.name]=l)}for(var f in n)if(n.hasOwnProperty(f))for(var d=0;d<A.length;d++)if(A[d]===f)return f;return i},c=function(e){var s=function(){for(var e=[],s=0;s<a.olson.dst_rules.years.length;s++){var i=o(a.olson.dst_rules.years[s]);e.push(i)}return e},i=function(e){for(var a=0;a<e.length;a++)if(e[a]!==!1)return!0;return!1},r=s(),n=i(r);return n?A(r,e):e},m=function(){var e=n();return e||(e=a.olson.timezones[r()],"undefined"!=typeof s.AMBIGUITIES[e]&&(e=c(e))),{name:function(){return e}}};return{determine:m}}();a.olson=a.olson||{},a.olson.timezones={"-720,0":"Etc/GMT+12","-660,0":"Pacific/Pago_Pago","-660,1,s":"Pacific/Apia","-600,1":"America/Adak","-600,0":"Pacific/Honolulu","-570,0":"Pacific/Marquesas","-540,0":"Pacific/Gambier","-540,1":"America/Anchorage","-480,1":"America/Los_Angeles","-480,0":"Pacific/Pitcairn","-420,0":"America/Phoenix","-420,1":"America/Denver","-360,0":"America/Guatemala","-360,1":"America/Chicago","-360,1,s":"Pacific/Easter","-300,0":"America/Bogota","-300,1":"America/New_York","-270,0":"America/Caracas","-240,1":"America/Halifax","-240,0":"America/Santo_Domingo","-240,1,s":"America/Asuncion","-210,1":"America/St_Johns","-180,1":"America/Godthab","-180,0":"America/Argentina/Buenos_Aires","-180,1,s":"America/Montevideo","-120,0":"America/Noronha","-120,1":"America/Noronha","-60,1":"Atlantic/Azores","-60,0":"Atlantic/Cape_Verde","0,0":"UTC","0,1":"Europe/London","60,1":"Europe/Berlin","60,0":"Africa/Lagos","60,1,s":"Africa/Windhoek","120,1":"Asia/Beirut","120,0":"Africa/Johannesburg","180,0":"Asia/Baghdad","180,1":"Europe/Moscow","210,1":"Asia/Tehran","240,0":"Asia/Dubai","240,1":"Asia/Baku","270,0":"Asia/Kabul","300,1":"Asia/Yekaterinburg","300,0":"Asia/Karachi","330,0":"Asia/Kolkata","345,0":"Asia/Kathmandu","360,0":"Asia/Dhaka","360,1":"Asia/Omsk","390,0":"Asia/Rangoon","420,1":"Asia/Krasnoyarsk","420,0":"Asia/Jakarta","480,0":"Asia/Shanghai","480,1":"Asia/Irkutsk","525,0":"Australia/Eucla","525,1,s":"Australia/Eucla","540,1":"Asia/Yakutsk","540,0":"Asia/Tokyo","570,0":"Australia/Darwin","570,1,s":"Australia/Adelaide","600,0":"Australia/Brisbane","600,1":"Asia/Vladivostok","600,1,s":"Australia/Sydney","630,1,s":"Australia/Lord_Howe","660,1":"Asia/Kamchatka","660,0":"Pacific/Noumea","690,0":"Pacific/Norfolk","720,1,s":"Pacific/Auckland","720,0":"Pacific/Majuro","765,1,s":"Pacific/Chatham","780,0":"Pacific/Tongatapu","780,1,s":"Pacific/Apia","840,0":"Pacific/Kiritimati"},a.olson.dst_rules={years:[2008,2009,2010,2011,2012,2013,2014],zones:[{name:"Africa/Cairo",rules:[{e:12199572e5,s:12090744e5},{e:1250802e6,s:1240524e6},{e:12858804e5,s:12840696e5},!1,!1,!1,{e:14116788e5,s:1406844e6}]},{name:"Africa/Casablanca",rules:[{e:12202236e5,s:12122784e5},{e:12508092e5,s:12438144e5},{e:1281222e6,s:12727584e5},{e:13120668e5,s:13017888e5},{e:13489704e5,s:1345428e6},{e:13828392e5,s:13761e8},{e:14142888e5,s:14069448e5}]},{name:"America/Asuncion",rules:[{e:12050316e5,s:12243888e5},{e:12364812e5,s:12558384e5},{e:12709548e5,s:12860784e5},{e:13024044e5,s:1317528e6},{e:1333854e6,s:13495824e5},{e:1364094e6,s:1381032e6},{e:13955436e5,s:14124816e5}]},{name:"America/Campo_Grande",rules:[{e:12032172e5,s:12243888e5},{e:12346668e5,s:12558384e5},{e:12667212e5,s:1287288e6},{e:12981708e5,s:13187376e5},{e:13302252e5,s:1350792e6},{e:136107e7,s:13822416e5},{e:13925196e5,s:14136912e5}]},{name:"America/Goose_Bay",rules:[{e:122559486e4,s:120503526e4},{e:125704446e4,s:123648486e4},{e:128909886e4,s:126853926e4},{e:13205556e5,s:129998886e4},{e:13520052e5,s:13314456e5},{e:13834548e5,s:13628952e5},{e:14149044e5,s:13943448e5}]},{name:"America/Havana",rules:[{e:12249972e5,s:12056436e5},{e:12564468e5,s:12364884e5},{e:12885012e5,s:12685428e5},{e:13211604e5,s:13005972e5},{e:13520052e5,s:13332564e5},{e:13834548e5,s:13628916e5},{e:14149044e5,s:13943412e5}]},{name:"America/Mazatlan",rules:[{e:1225008e6,s:12074724e5},{e:12564576e5,s:1238922e6},{e:1288512e6,s:12703716e5},{e:13199616e5,s:13018212e5},{e:13514112e5,s:13332708e5},{e:13828608e5,s:13653252e5},{e:14143104e5,s:13967748e5}]},{name:"America/Mexico_City",rules:[{e:12250044e5,s:12074688e5},{e:1256454e6,s:12389184e5},{e:12885084e5,s:1270368e6},{e:1319958e6,s:13018176e5},{e:13514076e5,s:13332672e5},{e:13828572e5,s:13653216e5},{e:14143068e5,s:13967712e5}]},{name:"America/Miquelon",rules:[{e:12255984e5,s:12050388e5},{e:1257048e6,s:12364884e5},{e:12891024e5,s:12685428e5},{e:1320552e6,s:12999924e5},{e:13520016e5,s:1331442e6},{e:13834512e5,s:13628916e5},{e:14149008e5,s:13943412e5}]},{name:"America/Santa_Isabel",rules:[{e:12250116e5,s:1207476e6},{e:12564612e5,s:12389256e5},{e:12885156e5,s:12703752e5},{e:13199652e5,s:13018248e5},{e:13514148e5,s:13332744e5},{e:13828644e5,s:13653288e5},{e:1414314e6,s:13967784e5}]},{name:"America/Santiago",rules:[{e:1206846e6,s:1223784e6},{e:1237086e6,s:12552336e5},{e:127035e7,s:12866832e5},{e:13048236e5,s:13138992e5},{e:13356684e5,s:13465584e5},{e:1367118e6,s:13786128e5},{e:13985676e5,s:14100624e5}]},{name:"America/Sao_Paulo",rules:[{e:12032136e5,s:12243852e5},{e:12346632e5,s:12558348e5},{e:12667176e5,s:12872844e5},{e:12981672e5,s:1318734e6},{e:13302216e5,s:13507884e5},{e:13610664e5,s:1382238e6},{e:1392516e6,s:14136876e5}]},{name:"Asia/Amman",rules:[{e:1225404e6,s:12066552e5},{e:12568536e5,s:12381048e5},{e:12883032e5,s:12695544e5},{e:13197528e5,s:13016088e5},!1,!1,{e:14147064e5,s:13959576e5}]},{name:"Asia/Damascus",rules:[{e:12254868e5,s:120726e7},{e:125685e7,s:12381048e5},{e:12882996e5,s:12701592e5},{e:13197492e5,s:13016088e5},{e:13511988e5,s:13330584e5},{e:13826484e5,s:1364508e6},{e:14147028e5,s:13959576e5}]},{name:"Asia/Dubai",rules:[!1,!1,!1,!1,!1,!1,!1]},{name:"Asia/Gaza",rules:[{e:12199572e5,s:12066552e5},{e:12520152e5,s:12381048e5},{e:1281474e6,s:126964086e4},{e:1312146e6,s:130160886e4},{e:13481784e5,s:13330584e5},{e:13802292e5,s:1364508e6},{e:1414098e6,s:13959576e5}]},{name:"Asia/Irkutsk",rules:[{e:12249576e5,s:12068136e5},{e:12564072e5,s:12382632e5},{e:12884616e5,s:12697128e5},!1,!1,!1,!1]},{name:"Asia/Jerusalem",rules:[{e:12231612e5,s:12066624e5},{e:1254006e6,s:1238112e6},{e:1284246e6,s:12695616e5},{e:131751e7,s:1301616e6},{e:13483548e5,s:13330656e5},{e:13828284e5,s:13645152e5},{e:1414278e6,s:13959648e5}]},{name:"Asia/Kamchatka",rules:[{e:12249432e5,s:12067992e5},{e:12563928e5,s:12382488e5},{e:12884508e5,s:12696984e5},!1,!1,!1,!1]},{name:"Asia/Krasnoyarsk",rules:[{e:12249612e5,s:12068172e5},{e:12564108e5,s:12382668e5},{e:12884652e5,s:12697164e5},!1,!1,!1,!1]},{name:"Asia/Omsk",rules:[{e:12249648e5,s:12068208e5},{e:12564144e5,s:12382704e5},{e:12884688e5,s:126972e7},!1,!1,!1,!1]},{name:"Asia/Vladivostok",rules:[{e:12249504e5,s:12068064e5},{e:12564e8,s:1238256e6},{e:12884544e5,s:12697056e5},!1,!1,!1,!1]},{name:"Asia/Yakutsk",rules:[{e:1224954e6,s:120681e7},{e:12564036e5,s:12382596e5},{e:1288458e6,s:12697092e5},!1,!1,!1,!1]},{name:"Asia/Yekaterinburg",rules:[{e:12249684e5,s:12068244e5},{e:1256418e6,s:1238274e6},{e:12884724e5,s:12697236e5},!1,!1,!1,!1]},{name:"Asia/Yerevan",rules:[{e:1224972e6,s:1206828e6},{e:12564216e5,s:12382776e5},{e:1288476e6,s:12697272e5},{e:13199256e5,s:13011768e5},!1,!1,!1]},{name:"Australia/Lord_Howe",rules:[{e:12074076e5,s:12231342e5},{e:12388572e5,s:12545838e5},{e:12703068e5,s:12860334e5},{e:13017564e5,s:1317483e6},{e:1333206e6,s:13495374e5},{e:13652604e5,s:1380987e6},{e:139671e7,s:14124366e5}]},{name:"Australia/Perth",rules:[{e:12068136e5,s:12249576e5},!1,!1,!1,!1,!1,!1]},{name:"Europe/Helsinki",rules:[{e:12249828e5,s:12068388e5},{e:12564324e5,s:12382884e5},{e:12884868e5,s:1269738e6},{e:13199364e5,s:13011876e5},{e:1351386e6,s:13326372e5},{e:13828356e5,s:13646916e5},{e:14142852e5,s:13961412e5}]},{name:"Europe/Minsk",rules:[{e:12249792e5,s:12068352e5},{e:12564288e5,s:12382848e5},{e:12884832e5,s:12697344e5},!1,!1,!1,!1]},{name:"Europe/Moscow",rules:[{e:12249756e5,s:12068316e5},{e:12564252e5,s:12382812e5},{e:12884796e5,s:12697308e5},!1,!1,!1,!1]},{name:"Pacific/Apia",rules:[!1,!1,!1,{e:13017528e5,s:13168728e5},{e:13332024e5,s:13489272e5},{e:13652568e5,s:13803768e5},{e:13967064e5,s:14118264e5}]},{name:"Pacific/Fiji",rules:[!1,!1,{e:12696984e5,s:12878424e5},{e:13271544e5,s:1319292e6},{e:1358604e6,s:13507416e5},{e:139005e7,s:1382796e6},{e:14215032e5,s:14148504e5}]},{name:"Europe/London",rules:[{e:12249828e5,s:12068388e5},{e:12564324e5,s:12382884e5},{e:12884868e5,s:1269738e6},{e:13199364e5,s:13011876e5},{e:1351386e6,s:13326372e5},{e:13828356e5,s:13646916e5},{e:14142852e5,s:13961412e5}]}]},"undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports=a:"undefined"!=typeof define&&null!==define&&null!=define.amd?define([],function(){return a}):"undefined"==typeof e?window.jstz=a:e.jstz=a}();]]></file>
<file javascript_app="global" javascript_location="library" javascript_path="linkify" javascript_name="linkify-jquery.min.js" javascript_type="framework" javascript_version="103021" 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="103021" 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.groups.counts.js" javascript_type="mixins" javascript_version="103021" javascript_position="1000050"><![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="103021" 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 () {
// Get top postition of table
var elemPosition = ips.utils.position.getElemPosition( 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="103021" javascript_position="1000050"><![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
});
});
/**
* 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 ) ){
_.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 {
pageAction.trigger('addManualItem.pageAction', {
id: 'moderate[' + key + ']',
actions: val
});
}
});
}
};
/**
* 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 () {
// Get top postition of table
var elemPosition = ips.utils.position.getElemPosition( 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="103021" javascript_position="1000100">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.core.comment.js - Comment model
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
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="103021" javascript_position="1000050"><![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="103021" javascript_position="1000050"><![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="103021" javascript_position="150"><![CDATA[/*!
* mustache.js - Logic-less {{mustache}} templates with JavaScript
* http://github.com/janl/mustache.js
*/
/*global define: false Mustache: true*/
(function defineMustache(global,factory){if(typeof exports==="object"&&exports&&typeof exports.nodeName!=="string"){factory(exports)}else if(typeof define==="function"&&define.amd){define(["exports"],factory)}else{global.Mustache={};factory(global.Mustache)}})(this,function mustacheFactory(mustache){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}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={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};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 sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;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)}else{nonSpace=true}tokens.push(["text",chr,start,start+1]);start+=1;if(chr==="\n")stripSpace()}}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);token=[type,value,start,scanner.pos];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)}}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,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){value=context.view;names=name.split(".");index=0;while(value!=null&&index<names.length){if(index===names.length-1)lookupHit=hasProperty(value,names[index]);value=value[names[index++]]}}else{value=context.view[name];lookupHit=hasProperty(context.view,name)}if(lookupHit)break;context=context.parent}cache[name]=value}if(isFunction(value))value=value.call(this.view);return value};function Writer(){this.cache={}}Writer.prototype.clearCache=function clearCache(){this.cache={}};Writer.prototype.parse=function parse(template,tags){var cache=this.cache;var tokens=cache[template];if(tokens==null)tokens=cache[template]=parseTemplate(template,tags);return tokens};Writer.prototype.render=function render(template,view,partials){var tokens=this.parse(template);var context=view instanceof Context?view:new Context(view);return this.renderTokens(tokens,context,partials,template)};Writer.prototype.renderTokens=function renderTokens(tokens,context,partials,originalTemplate){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);else if(symbol==="^")value=this.renderInverted(token,context,partials,originalTemplate);else if(symbol===">")value=this.renderPartial(token,context,partials,originalTemplate);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials)}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)}}else if(typeof value==="object"||typeof value==="string"||typeof value==="number"){buffer+=this.renderTokens(token[4],context.push(value),partials,originalTemplate)}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)}return buffer};Writer.prototype.renderInverted=function renderInverted(token,context,partials,originalTemplate){var value=context.lookup(token[1]);if(!value||isArray(value)&&value.length===0)return this.renderTokens(token[4],context,partials,originalTemplate)};Writer.prototype.renderPartial=function renderPartial(token,context,partials){if(!partials)return;var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null)return this.renderTokens(this.parse(value),context,partials,value)};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){var value=context.lookup(token[1]);if(value!=null)return mustache.escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};mustache.name="mustache.js";mustache.version="2.3.0";mustache.tags=["{{","}}"];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){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)};mustache.to_html=function to_html(template,view,partials,send){var result=mustache.render(template,view,partials);if(isFunction(send)){send(result)}else{return result}};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="103021" javascript_position="1000550"><![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="103021" 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'>\
<span data-role='status'>{{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'> </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>\
");]]></file>
<file javascript_app="core" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.core.js" javascript_type="template" javascript_version="103021" javascript_position="1000100"><![CDATA[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-action='survey' target='_blank'>{{#lang}}licenseRenewalYes{{/lang}}</a>\
</li>\
</ul>\
</div>\
</div>\
</div>\
");]]></file>
<file javascript_app="core" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.dashboard.js" javascript_type="template" javascript_version="103021" javascript_position="1000100"><![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>\
");
ips.templates.set('newFeatures.card', " \
<div class='acpNewFeature_card' data-role='card'>\
<img src='{{image}}' class='acpNewFeature_image'>\
<div class='acpNewFeature_info'>\
<h2 class='acpNewFeature_title'>{{title}}</h2>\
<p class='acpNewFeature_desc'>{{description}}</p>\
{{#moreInfo}}<a href='{{moreInfo}}' class='acpNewFeature_button ipsButton ipsButton_verySmall ipsButton_primary'>{{#lang}}findOutMore{{/lang}}</a>{{/moreInfo}}\
</div>\
</div>\
");
ips.templates.set('newFeatures.dot', " \
<li class='acpNewFeature_dot' data-role='dot'><a href='#' data-dotIdx='{{i}}' data-role='dotFeature'></a></li>\
");
ips.templates.set('newFeatures.wrapper', " \
<div class='acpNewFeature' data-role='newFeatures'>\
<div class='acpNewFeature_wrap'>\
<a href='#' class='acpNewFeature_close' data-action='closeNewFeature' data-ipsTooltip title='{{#lang}}close{{/lang}}'>×</a>\
<a href='#' class='acpNewFeature_arrow acpNewFeature_next' data-action='nextFeature'><i class='fa fa-angle-right'></i></a>\
<a href='#' class='acpNewFeature_arrow acpNewFeature_prev' data-action='prevFeature'><i class='fa fa-angle-left'></i></a>\
<div class='acpNewFeature_inner'>\
<h1 class='acpNewFeature_mainTitle' data-role='mainTitle'>{{#lang}}whatsNew{{/lang}}</h1>\
<div class='acpNewFeature_cardWrap'>\
{{{cards}}}\
</div>\
<ul class='acpNewFeature_dots ipsList_reset' data-role='dots'>\
{{{dots}}}\
</ul>\
</div>\
</div>\
</div>\
");]]></file>
<file javascript_app="core" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.members.js" javascript_type="template" javascript_version="103021" javascript_position="1000100"><![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="103021" javascript_position="1000100"><![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="103021" javascript_position="1000100"><![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>\
");
]]></file>
<file javascript_app="global" javascript_location="framework" javascript_path="templates" javascript_name="ips.core.templates.js" javascript_type="framework" javascript_version="103021" javascript_position="50"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*/
/* GENERAL TEMPLATES */
ips.templates.set('core.general.loading', " \
<span class='ipsType_light'><i class='icon-spinner2 ipsLoading_tinyIcon'></i> </span> {{text}}</span>\
");
ips.templates.set('core.general.ajax', " \
<div id='elAjaxLoading'><i class='icon-spinner2 ipsLoading_tinyIcon'></i> {{#lang}}loading{{/lang}}...</div>\
");
ips.templates.set('core.general.flashMsg', " \
<div id='elFlashMessage'><div data-role='flashMessage'></div><a href='#' data-action='dismissFlashMessage'>×</a></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'>\
<button class='ipsButton ipsButton_light ipsButton_small' data-role='multiQuote'><i class='fa fa-comments'></i> {{{count}}}</button> <a href='#' data-action='clearQuoted'><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'> {{{text}}}\
<a href='#' data-action='loadNewPosts'>{{#lang}}showReply{{/lang}}</a>\
</span>\
");
ips.templates.set('core.postNotify.multiple', " \
<span data-role='newPostNotification' class='ipsType_medium'>\
{{text}}\
<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}}\
<a href='#' data-action='loadNewPosts'>{{showFirstX}}</a>\
<span class='ipsType_light'>{{#lang}}showRepliesOr{{/lang}}</span>\
{{/canLoadNew}}\
<a href='{{spillOverUrl}}'>{{#lang}}goToNewestPage{{/lang}}</a>\
</span>\
");
ips.templates.set('core.notification.flashSingle', " \
<a href='{{url}}' data-role='newNotification'>\
<div class='ipsPhotoPanel ipsPhotoPanel_tiny ipsType_medium ipsType_blendLinks'>\
<img src='{{icon}}' alt='' class='ipsUserPhoto ipsUserPhoto_tiny ipsPos_middle'>\
<div class='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='ipsPhotoPanel ipsPhotoPanel_tiny ipsType_medium ipsType_blendLinks' data-role='newNotification'>\
<span class='ipsPos_left ipsType_veryLarge ipsPos_left'><i class='fa fa-bell'></i></span>\
<div class='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'>\
{{{text}}}\
{{{subtext}}}\
</div>\
<ul class='ipsToolList ipsToolList_horizontal ipsType_right 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.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}}");
/* 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'>×</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 ipsType_noBreak' role='tooltip'>{{content}}</div>\
");
/* SEARCH TEMPLATES */
ips.templates.set('core.search.loadingPanel', " \
<div id='{{id}}' class='ipsLoading' style='min-height: 100px'>\
\
</div>\
");
/* EDITOR TEMPLATES */
ips.templates.set('core.editor.panelWrapper', " \
<div id='{{id}}' class='ipsRTE_panel ipsPad'>\
{{content}}\
</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>\
\
<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 ipsEmoticons_content'>\
<div class='ipsEmpty ipsType_center ipsEmoticons_contentLoading' data-role='emojiLoading'>\
{{#lang}}loading{{/lang}}...\
</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'> </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><div class='ipsQuote_citation'>{{citation}}</div><div class='ipsQuote_contents ipsClearfix'>{{{contents}}}</div></blockquote>");
ips.templates.set('core.editor.legacyQuoteUpcast', "<div class='ipsQuote_citation'>{{citation}}</div><div class='ipsQuote_contents ipsClearfix'>{{{contents}}}</div>");
ips.templates.set('core.editor.citation', " \
<div class='ipsQuote_citation ipsQuote_open'>\
<a href='#' data-action='toggleQuote'> </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'></div></div>");
ips.templates.set('core.editor.legacySpoilerUpcast', "<div class='ipsSpoiler_header'><span>{{#lang}}editorSpoiler{{/lang}}</span></div><div class='ipsSpoiler_contents'>{{{contents}}}</div>");
ips.templates.set('core.editor.spoilerHeader', " \
<div class='ipsSpoiler_header ipsSpoiler_closed'>\
<a href='#' data-action='toggleSpoiler'> </a>\
<span>{{#lang}}spoilerClickToReveal{{/lang}}</span>\
</div>\
");
ips.templates.set('core.editor.previewLoading', " \
<div data-role='previewLoading' class='ipsLoading' style='min-height: 100px'>\
\
</div>\
");
/* ATTACHMENT TEMPLATES */
ips.templates.set('core.attachments.fileItemWrapper', " \
<ul class='ipsDataList'>{{{content}}}</ul>\
");
ips.templates.set('core.attachments.fileItem', " \
<li class='ipsDataItem ipsAttach ipsContained {{#done}}ipsAttach_done{{/done}}' id='{{id}}' data-role='file' data-fileid='{{id}}'>\
<div class='ipsDataItem_generic ipsDataItem_size2 ipsResponsive_hidePhone ipsResponsive_block ipsType_center' data-role='preview' data-action='insertFile' {{#thumb}}style='background-image: url( {{thumbnail}} )'{{/thumb}}>\
{{#thumb}}\
{{{thumb}}}\
{{/thumb}}\
<i class='fa fa-file ipsType_large' {{#thumb}}style='display: none'{{/thumb}}></i>\
</div>\
<div class='ipsDataItem_main' data-action='insertFile'>\
<h2 class='ipsDataItem_title ipsType_medium ipsType_reset ipsAttach_title ipsTruncate ipsTruncate_line' data-role='title'>{{title}}</h2>\
<p class='ipsDataItem_meta ipsType_light'>\
{{size}}\
</p>\
</div>\
<div class='ipsDataItem_generic ipsDataItem_size5'>\
{{#status}}<span class='ipsAttachment_progress'><span data-role='progressbar'></span></span>{{/status}}\
{{#statusText}}<span class='ipsType_light' data-role='status'>{{statusText}}</span>{{/statusText}}\
</div>\
<div class='ipsDataItem_generic ipsDataItem_size8 ipsType_right'>\
<ul class='ipsList_inline'>\
<li data-role='insert' {{#insertable}}style='display: none'{{/insertable}}>\
<a href='#' data-action='insertFile' class='ipsAttach_selection' data-ipsTooltip title='{{#lang}}insertIntoPost{{/lang}}'>\
<i class='fa fa-plus'></i>\
</a>\
</li>\
<li data-role='deleteFileWrapper' {{#newUpload}}style='display: none'{{/newUpload}}>\
<input type='hidden' name='{{field_name}}_keep[{{id}}]' value='1'>\
<a href='#' data-role='deleteFile' class='ipsType_warning' data-ipsTooltip title='{{#lang}}attachRemove{{/lang}}'>\
<i class='fa fa-trash-o'></i> {{#lang}}delete{{/lang}}\
</a>\
</li>\
</ul>\
</div>\
</li>\
");
ips.templates.set('core.attachments.imageItem', " \
<div class='ipsGrid_span3 ipsAttach ipsImageAttach ipsPad_half ipsAreaBackground_light {{#done}}ipsAttach_done{{/done}}' id='{{id}}' data-role='file' data-fileid='{{id}}' data-fullsizeurl='{{imagesrc}}' data-thumbnailurl='{{thumbnail}}' data-fileType='image'>\
<ul class='ipsList_inline ipsImageAttach_controls'>\
<li data-role='insert' {{#insertable}}style='display: none'{{/insertable}}><a href='#' data-action='insertFile' class='ipsAttach_selection' data-ipsTooltip title='{{#lang}}insertIntoPost{{/lang}}'><i class='fa fa-plus'></i></a></li>\
</li>\
<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>\
<div class='ipsImageAttach_thumb ipsType_center' data-role='preview' data-grid-ratio='65' data-action='insertFile' {{#thumb}}style='background-image: url( {{thumbnail}} )'{{/thumb}}>\
{{#status}}\
<span class='ipsImageAttach_status ipsType_light' data-role='status'>{{{status}}}</span>\
<span class='ipsAttachment_progress'><span data-role='progressbar'></span></span>\
{{/status}}\
{{#thumb}}\
{{{thumb}}}\
{{/thumb}}\
</div>\
<h2 class='ipsType_reset ipsAttach_title ipsType_medium ipsTruncate ipsTruncate_line' data-role='title'>{{title}}</h2>\
<p class='ipsType_light'>{{size}} {{#statusText}}· <span data-role='status'>{{statusText}}</span>{{/statusText}}</p>\
</div>\
");
ips.templates.set('core.attachments.videoItem', " \
<div class='ipsGrid_span3 ipsAttach ipsImageAttach ipsPad_half ipsAreaBackground_light {{#done}}ipsAttach_done{{/done}}' id='{{id}}' data-role='file' data-fileid='{{id}}' data-fullsizeurl='{{imagesrc}}' data-thumbnailurl='{{thumbnail}}' data-fileType='video' data-mimeType='{{mime}}'>\
<ul class='ipsList_inline ipsImageAttach_controls'>\
<li data-role='insert' {{#insertable}}style='display: none'{{/insertable}}><a href='#' data-action='insertFile' class='ipsAttach_selection' data-ipsTooltip title='{{#lang}}insertIntoPost{{/lang}}'><i class='fa fa-plus'></i></a></li>\
</li>\
<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>\
<div class='ipsImageAttach_thumb ipsType_center' data-role='preview' data-grid-ratio='65' data-action='insertFile'>\
{{#status}}\
<span class='ipsImageAttach_status ipsType_light' data-role='status'>{{{status}}}</span>\
<span class='ipsAttachment_progress'><span data-role='progressbar'></span></span>\
{{/status}}\
{{#thumb}}\
<video>\
<source src='{{{thumb}}}' type='{{mime}}'>\
</video>\
{{/thumb}}\
</div>\
<h2 class='ipsType_reset ipsAttach_title ipsType_medium ipsTruncate ipsTruncate_line' data-role='title'>{{title}}</h2>\
<p class='ipsType_light'>{{size}} {{#statusText}}· <span data-role='status'>{{statusText}}</span>{{/statusText}}</p>\
</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'><li id='{{id}}_inputItem'>{{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', " \
<ul class='ipsAutocompleteMenu ipsList_reset' id='{{id}}_results' aria-expanded='false' style='display: none'>\
</ul>\
");
ips.templates.set('core.autocomplete.resultItem', " \
<li class='ipsAutocompleteMenu_item' data-value='{{{value}}}' role='option' role='listitem'>\
<div class='ipsClearfix'>\
{{html}}\
</div>\
</li>\
");
ips.templates.set('core.autocomplete.token', " \
<li class='cToken' data-value='{{value}}'>\
{{{title}}} <span class='cToken_close' data-action='delete'>×</span>\
</li>\
");
ips.templates.set('core.autocomplete.memberItem', " \
<li class='ipsAutocompleteMenu_item ipsClearfix' data-value=\"{{value}}\" role='option' role='listitem'>\
<div class='ipsPhotoPanel ipsPhotoPanel_tiny'>\
<span class='ipsUserPhoto ipsUserPhoto_tiny'><img src='{{{photo}}}'></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='ipsType_normal'><i class='fa fa-plus'></i> {{#lang}}ac_optional{{/lang}}...</a>\
");
ips.templates.set('core.forms.toggle', " \
<span class='ipsToggle {{className}}' id='{{id}}' tabindex='0'>\
<span data-role='status'>{{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}} <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> {{#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> <span class='ipsType_light'> {{#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'>\
×\
</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 class='ipsPad'>\
<ul class='ipsDataList cPollChoices' data-role='choices'>\
<li class='ipsDataItem ipsResponsive_hidePhone'>\
<p class='ipsDataItem_generic ipsDataItem_size1'> </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'> </p>\
</li>\
{{{choices}}}\
</ul>\
<br>\
<div class='ipsDataList'>\
<p class='ipsDataItem_generic ipsDataItem_size1'> </p>\
<ul class='ipsDataItem_main ipsList_inline'>\
{{#removeQuestion}}<li class='ipsPos_right'><a href='#' data-action='removeQuestion' class='ipsButton ipsButton_verySmall ipsButton_light'>{{#lang}}removeQuestion{{/lang}}</a></li>{{/removeQuestion}}\
<li><a href='#' data-action='addChoice' class='ipsButton ipsButton_verySmall ipsButton_normal'>{{#lang}}addChoice{{/lang}}</a></li>\
<li><input type='checkbox' id='elPoll_{{pollName}}_{{questionID}}multi' name='{{pollName}}[questions][{{questionID}}][multichoice]' {{#multiChoice}}checked{{/multiChoice}}> <label for='elPoll_{{pollName}}_{{questionID}}multi'>{{#lang}}multipleChoiceQuestion{{/lang}}</label></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>\
{{#showCounts}}\
<div class='ipsDataItem_generic ipsDataItem_size4' data-voteText='{{#lang}}votesTitle{{/lang}}'>\
<input type='number' name='{{pollName}}[questions][{{questionID}}][answers][{{choiceID}}][count]' value='{{count}}' min='0'>\
</div>\
{{/showCounts}}\
<div class='ipsDataItem_generic ipsDataItem_size1'>\
<a href='#' data-action='removeChoice' class='ipsButton ipsButton_verySmall ipsButton_light ipsButton_narrow'><i class='fa fa-times'></i></a>\
</div>\
</li>\
");
/* COVER PHOTOS */
ips.templates.set('core.coverPhoto.controls', " \
<ul class='ipsList_inline' data-role='coverPhotoControls'>\
<li><a href='#' class='ipsButton ipsButton_positive ipsButton_verySmall' data-action='savePosition'><i class='fa fa-check'></i> {{#lang}}save_position{{/lang}}</a></li>\
<li><a href='#' class='ipsButton ipsButton_negative ipsButton_verySmall' data-action='cancelPosition'><i class='fa fa-times'></i> {{#lang}}cancel{{/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='ipsPad_top 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', " \
<ul class='ipsAutocompleteMenu ipsList_reset' id='{{id}}_results' aria-expanded='false' style='display: none'>\
</ul>\
");
ips.templates.set('core.contentItem.item', " \
<li data-id='{{id}}'>\
<span class='cContentItem_delete' data-action='delete'>×</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>\
");]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="templates" javascript_name="ips.templates.clubs.js" javascript_type="template" javascript_version="103021" javascript_position="1000100"><![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>\
");]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="templates" javascript_name="ips.templates.messages.js" javascript_type="template" javascript_version="103021" javascript_position="1000100"><![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="103021" javascript_position="1000100"><![CDATA[ips.templates.set('core.streams.teaser', "\
<li data-action='insertNewItems' class='ipsStreamItem_loadMore' 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_light 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="103021" javascript_position="1000100"><![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}}\
");
ips.templates.set('notification.granted', "\
<div class='ipsAreaBackground_light cNotificationBox'>\
<div class='ipsPhotoPanel ipsPhotoPanel_tiny ipsAreaBackground_positive ipsPad'>\
<i class='fa fa-check ipsPos_left ipsType_normal'></i>\
<div>\
<span class='ipsType_large'>{{#lang}}notificationsAccepted{{/lang}}</span>\
</div>\
</div>\
<div class='ipsPad'>\
<p class='ipsType_reset ipsType_medium'>\
{{#lang}}notificationsAcceptedBlurb{{/lang}}\
</p>\
</div>\
</div>\
");
ips.templates.set('notification.denied', "\
<div class='ipsAreaBackground_light cNotificationBox'>\
<div class='ipsPhotoPanel ipsPhotoPanel_tiny ipsAreaBackground_negative ipsPad'>\
<i class='fa fa-times ipsPos_left ipsType_normal'></i>\
<div>\
<span class='ipsType_large'>{{#lang}}notificationsDisabled{{/lang}}</span>\
</div>\
</div>\
<div class='ipsPad'>\
<p class='ipsType_reset ipsType_medium ipsSpacer_bottom'>\
{{#lang}}notificationsDisabledBlurb{{/lang}}\
</p>\
</div>\
</div>\
");
ips.templates.set('notification.default', "\
<div class='ipsAreaBackground_light cNotificationBox'>\
<div class='ipsPhotoPanel ipsPhotoPanel_tiny ipsAreaBackground ipsPad'>\
<i class='fa fa-times ipsPos_left ipsType_normal'></i>\
<div>\
<span class='ipsType_large'>{{#lang}}notificationsNotSure{{/lang}}</span>\
</div>\
</div>\
<div class='ipsPad'>\
<p class='ipsType_reset ipsType_medium ipsSpacer_bottom'>\
{{#lang}}notificationsDefaultBlurb{{/lang}}\
</p>\
<button data-action='promptMe' class='ipsButton ipsButton_veryLight ipsButton_fullWidth'>{{#lang}}notificationsAllow{{/lang}}</button>\
<p class='ipsType_small ipsSpacer_top ipsSpacer_half ipsHide' data-role='promptMessage'>\
{{#lang}}notificationsAllowPrompt{{/lang}}\
</p>\
</div>\
</div>\
");]]></file>
<file javascript_app="core" javascript_location="front" javascript_path="templates" javascript_name="ips.templates.vse.js" javascript_type="template" javascript_version="103021" javascript_position="1000100"><![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}}'>\
<a href='#'>\
<span class='vseClass_swatch' style='{{swatch.main}}'>{{#swatch.sub}}<span class='vseClass_swatch' style='{{swatch.sub}}'></span>{{/swatch.sub}}</span>{{title}}\
</a>\
</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'> </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'> </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'> </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_span1'></div>\
<div class='ipsGrid_span4 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_span4 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 class='ipsGrid_span1'></div>\
</div>\
<br>\
<div class='ipsGrid'>\
<div class='ipsGrid_span1'></div>\
<div class='ipsGrid_span4 ipsType_center'>\
<input type='text' class='vseColorizer_swatch color' data-role='tertiaryColor' value='{{tertiaryColor}}'>\
<span class='ipsType_light'>{{#lang}}vseColorizer_tertiary{{/lang}}</span>\
</div>\
<div class='ipsGrid_span2'></div>\
<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>\
<div class='ipsGrid_span1'></div>\
</div>\
<br><br>\
<button class='ipsButton ipsButton_normal 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="103021" javascript_position="1000050"><![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( ' ' + 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="103021" javascript_position="1000050"><![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="103021" javascript_position="1000050"><![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,
squashFields: false
};
var respond = function (elem, options) {
if( !$( elem ).data('_matrix') ){
$( elem ).data('_matrix', matrixObj(elem, _.defaults( options, defaults ) ) );
}
};
ips.ui.registerWidget( 'matrix', ips.ui.matrix, [ 'manageable', 'squashFields' ] );
return {
respond: respond
};
});
/**
* 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();
}
_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 + '_' ) );
}
});
// 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: 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');
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
};
};
}(jQuery, _));]]></file>
<file javascript_app="global" javascript_location="admin" javascript_path="ui" javascript_name="ips.ui.statusToggle.js" javascript_type="ui" javascript_version="103021" javascript_position="1000050">/**
* 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){
"use strict";
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="' + nextState + '"]');
if( !nextBadge.length ){
Debug.warn( "No badge found for " + nextState + " state");
return;
}
elem.attr( 'data-loading', true );
currentBadge.css({ opacity: 0.5 });
// Send ajax request to make the change
ips.getAjax()( url, {
showLoading: true // show our global loading indicator
})
.done( function (response) {
currentBadge.hide().css({ opacity: 1 });
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="103021" javascript_position="1000050"><![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 = $.trim( $( options.searchable ).val() );
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( $.trim( response ) ) ){
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);
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.
if( rootID ){
var dataArray = $( this ).nestedSortable( 'toArray', { key: 'ajax_order'} );
var outputArray = [];
for( var i = 0; i < dataArray.length; i++ ){
outputArray.push( 'ajax_order[' + dataArray[i].item_id + ']=' + ( ( dataArray[i].parent_id == null ) ? rootID : dataArray[i].parent_id ) );
}
data = outputArray.join('&');
} else {
data = $( this ).nestedSortable( 'serialize', { key: 'ajax_order' } );
}
ips.getAjax()( url, {
data: data
})
.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( 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]');
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="103021" javascript_position="50"><![CDATA[// Underscore.js 1.8.3
// http://underscorejs.org
// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Underscore may be freely distributed under the MIT license.
(function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])<u?i=a+1:o=a}return i},m.indexOf=r(1,m.findIndex,m.sortedIndex),m.lastIndexOf=r(-1,m.findLastIndex),m.range=function(n,t,r){null==t&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e<arguments.length;)i.push(arguments[e++]);return E(n,r,this,this,i)};return r},m.bindAll=function(n){var t,r,e=arguments.length;if(1>=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this);
//# sourceMappingURL=underscore-min.map]]></file>
<file javascript_app="global" javascript_location="library" javascript_path="xregexp" javascript_name="xregexp-all.js" javascript_type="framework" javascript_version="103021" javascript_position="1000500"><![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
jstz
Debug.js
app.js</order>
<order app="global" path="/dev/js//library//jquery">jquery.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>