').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( $('').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 } ); // Add global loading indicator ability var count = 0; $( document ) .ajaxSend( function (event, request, settings) { if( 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( settings.showLoading === true ){ count--; if( count === 0 ){ ips.utils.anim.go( 'fadeOut fast', $('#elAjaxLoading') ); } } // Check for redirect response if( !settings.bypassRedirect ){ if( !_.isUndefined( request.responseJSON ) && !_.isUndefined( request.responseJSON.redirect ) ){ // Do we have a flash message to show? if( !_.isUndefined( request.responseJSON.message ) && request.responseJSON.message != '' ){ ips.utils.cookie.set( 'flmsg', request.responseJSON.message ); } window.location = request.responseJSON.redirect; } } }); }, /** * 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; }); }, /** * 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, { imgURL: ips.getSetting('imgURL'), 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) == '*' && ( replacement.indexOf( x.substring( 1 ) ) != -1 ) ){ 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 || {}, { imgURL: ips.getSetting('imgURL'), 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, 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, _));]]> = 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 ) { Debug.setEnabled( true ).setLevel( 'DEBUG' ).info("Enabled logging"); } }(jQuery, _));]]> controllerOne( mixin1; mixin2 ), controllerTwo, controllerThree, returns: *
		 * {
		 * 	controllerOne: [ mixin1, mixin2 ],
		 *	controllerTwo: [],
		 * 	controllerThree: []
		 * }
		 * 
* * @returns {string} */ _getControllersAndMixins = function (controllerString) { var controllers = {}; var pieces = controllerString.split(','); for( var i = 0; i < pieces.length; i++ ){ pieces[i] = $.trim( 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); }, /** * 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 ) ); // Add a destroy event handler this.on( 'destroy', function (e) { // Important - we don't want this event to bubble e.stopPropagation(); // Remove each event listener that was created in this controller if( self._eventListeners.length ){ for( var i = 0; i < self._eventListeners.length; i++ ){ var data = self._eventListeners[i]; if( data['delegate'] ){ data['elem'].off( data['ev'], data['delegate'], data['fn'] ); } else { data['elem'].off( data['ev'], data['fn'] ); } } } if( _.isFunction( self.destroy ) ){ self.destroy.call( self ); } // Remove reference to scope so that GC can do its thing self.scope = null; }); Debug.info("Initialized " + this.controllerID + " of type " + this.controllerType); }; // 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.trigger('destroy'); for( var i = 0; i < controllers.length; 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 }; }); }( jQuery, _ ));]]>
') .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, 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 = $(''); regionText.attr( 'name', regionSelect.attr('name') ).val( regionSelect.val() ); regionSelect.replaceWith( regionText ); } } } ); if ( typeof elem.attr('data-ipsAddressForm-googlePlaces') !== typeof undefined && elem.attr('data-ipsAddressForm-googlePlaces') !== false ) { 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 { ips.loader.get( [ '//maps.googleapis.com/maps/api/js?libraries=places&sensor=false&callback=ips.ui.addressForm.googlePlacesCallback' ] ); $( window ).on( 'googlePlacesLoaded', function(){ 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').val( value ); } lastLine.after( newLine ); }; var recalculateAddAddressLineButton = function (elem) { elem.find( '[data-role="addAddressLine"]' ).remove(); var button = $(''); 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"]').val( place.address_components[i].long_name ); break; case 'locality': case 'postal_town': elem.find('[data-role="city"]').val( place.address_components[i].long_name ); break; case 'postal_code': elem.find('[data-role="postalCode"]').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] ){ addressLines[0] = streetNumber + ' ' + addressLines[0]; } for( var i = 0; i < addressLines.length; i++ ){ if( existingAddressLines ){ $( elem ).find('[data-role="addressLine"]').slice( i, 1 ).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'] ); return { respond: respond, googlePlacesCallback: googlePlacesCallback }; }); }(jQuery, _));]]> /* global ips, _, Debug */ /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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' ] ); 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: 'Are you sure?', 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' })); 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.type == 'prompt' ){ alert .find('[data-role="promptValue"]') .val( options.value || '' ) .focus(); } else { alert .find('[data-action="ok"], [data-action="yes"]') .focus(); } }; init(); }; }(jQuery, _)); '); } else if( count.length === 0 ){ elem.find('.cAutoCheckIcon').html(''); } else { elem.find('.cAutoCheckIcon').html(''); } 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 }; }; }(jQuery, _));]]> ", "'", "\"" ], 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 }; 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', 'fieldTemplate', 'resultsTemplate', 'resultItemTemplate', 'tokenTemplate', 'addTokenTemplate', 'addTokenText', 'queryParam', 'minLength', 'maxLength', 'forceLower', 'diallowedCharacters', '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; /** * 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 , remotely via ajax * or not look up results from a data source at all. * * @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 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 ); wrapper .on( 'click', _clickWrapper ) .on( 'click', '[data-action="addToken"]', _clickAddToken ) .on( 'keydown', _keydownWrapper ) .on( 'propChanged', _propChanged ) .toggleClass( 'ipsField_autocompleteDisabled', disabled ); _buildInitialTokens(); 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 ); }, /** * 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 = originalTextField.val(); if( !value ){ return; } // Get individual values var splitValues = _.without( value.split( options.itemSep.chr ), '' ); 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] ); } } }, /** * 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('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 = $('').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 = $('').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(''); } }, /** * 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 () { // 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 ){ // 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.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( !_( [ options.itemSep.keycode, 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.keyCode ) ){ ignoreKey = true; } var value = $.trim( textField.val() ); // 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; } if( ignoreKey ){ return; } // Check for duplicates if we're potentially adding a new tag if( _( [ options.itemSep.keycode, ips.ui.key.ENTER, ips.ui.key.TAB ] ).contains( e.keyCode ) && options.unique && _duplicateValue( value ) ){ e.preventDefault(); _showTooltip( ips.getString( 'ac_dupes' ) ); return; } switch(e.keyCode){ // 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; } }, /** * 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(); } }, /** * 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.keyCode == ips.ui.key.TAB && textField.val() == '' ){ return false; } e.preventDefault(); var currentResult = results.getCurrent(); var value = ''; if( currentResult ){ value = currentResult.attr('data-value'); } else { value = _stripHTML( textField.val().replace( options.itemSep.chr, '' ) ); } 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( 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( options.itemSep.chr ) ); if( dataSource.type != 'none' ){ html = resultsElem.find('[data-value="' + value + '"]').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( options.itemSep.chr ) ); 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; } }, /** * Event handler for keydown in the text field. We're only interested in capturing * comma presses here. * * @returns {void} */ _keyupField = function (e) { // Check for prohibited characters var i; for ( i in options.diallowedCharacters ) { if ( $(e.target).val().indexOf( options.diallowedCharacters[i] ) !== -1 ) { $(e.target).val( $(e.target).val().replace( options.diallowedCharacters[i], '' ) ); _showTooltip( ips.getString( 'ac_prohibit_special' ) ); return; } } if( e.keyCode != options.itemSep.keycode ){ return; } if( textField.val().indexOf( options.itemSep.chr ) === -1 ){ return; } _addTokenFromCurrentInput(); e.preventDefault(); }, /** * Creates a token out of the current value in the text field * * @returns {void} */ _addTokenFromCurrentInput = function () { var value = _stripHTML( textField.val().replace( options.itemSep.chr, '' ) ); if( options.minLength && value.length < options.minLength || options.maxLength && value.length > options.maxLength ){ 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(); } // 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 = $('').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, addToken: tokens.add, getTokens: tokens.getValues, removeToken: tokens.remove, removeAll: tokens.removeAll }; }; /** * 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, _)); ]]> '); 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 = $('
'); 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(); }); } }, /** * 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, _));]]> = ( left + width ) && stayOnScreen <= ( left + width + 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( left < stayOnScreen && ( left + width ) > stayOnScreen ){ return true; } return false; }); var nextLeft = $( nextFirst ); if( !nextLeft.length ){ Debug.error("nextLeft didn't exist"); deferred.resolve(); return deferred.promise(); } var nextLeftPos = nextLeft.position().left; // If this would leave space at the end of the list, then we'll simply scroll to the end of the list // ( listWidth - nextLeftPos ) = What's left of the list to show if( ( listWidth - nextLeftPos ) < wrapperWidth ){ nextLeftPos = listWidth - wrapperWidth; } currentLeftPos = nextLeftPos; // Now animate the list to the new position ui.itemList.animate({ left: ( nextLeftPos * -1 ) + 'px' }, 'slow', function () { _checkNav(); 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.outerWidth(); var listWidth = _getCurrentWidth(); 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; ui.itemList.animate({ left: '0px' }, 'slow', function () { _checkNav(); }); 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, idx) { var left = $( item ).position().left; var width = $( item ).outerWidth(); var right = left + width; if( left > idealLeft ){ return false; } if( right > idealLeft && left <= 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 prevFirst = $( prevFirst ); 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; } else { currentLeftPos = prevFirst.outerWidth() + wrapperWidth; } ui.itemList.animate({ left: ( currentLeftPos * -1 ) + 'px' }, 'slow', function () { _checkNav(); 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(); }, /** * Sshows or hides the navigation elements, depending on current position of the carousel * * @returns {void} */ _checkNav = function () { var currentWidth = _getCurrentWidth(); if( ( currentWidth - currentLeftPos ) <= elem.outerWidth() ){ 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( currentLeftPos <= 0 ){ 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 ).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, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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'], '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, _)); div'); var dialogPosition = ips.utils.position.getElemPosition( dialogBit ); var dialogHeight = dialogBit.outerHeight(); var dialogWidth = dialogBit.outerWidth(); dialog.hide(); // 'showFrom' position var fromPosition = ips.utils.position.getElemPosition( $( options.showFrom ) ); // Document sizing var docSize = $( document ).outerWidth(); // Figure out the offset from the halfway mark var widthOffset = dialogPosition.viewportOffset.left + ( fromPosition.viewportOffset.left - ( dialogWidth / 2 ) ); var fromHalfpoint = ( fromPosition.absPos.top + ( $( options.showForm ).outerHeight() / 2 ) ); var heightOffset = ( fromHalfpoint - dialogPosition.absPos.top - ( dialogHeight / 2 ) ); if( dialogPosition.viewportOffset.left > fromPosition.viewportOffset.left ){ widthOffset = widthOffset * -1; } /*if( dialogPosition.viewportOffset.top < fromPosition.viewportOffset.top ){ heightOffset = heightOffset * -1; }*/ $( dialog ) .show(); $( dialogBit ) .css({ transform: 'translateY(' + heightOffset + 'px) translateX(' + widthOffset + 'px) scale(0.1)', opacity: 1 }) .animate( { transform: 'translateY(0px) translateX(0px) scale(1)', opacity: 1 }, { easing: 'swing' } ); }, /** * 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 ) .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 { window.location = elem.href; } 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 }) ); dialog = $( '#' + dialogID ); // Add to body dialog.css( { zIndex: ips.ui.zIndex(), }); _positionDialog(); // Add events dialog.on('click', '[data-action="dialogClose"]', function (e) { Debug.log('closing by close'); // We trigger on the dialog, but watch on the document $( dialog ).trigger('closeDialog', { dialogID: dialogID, originalEvent: e }); }); $( dialog ).on('closeDialog', function () { Debug.log( 'closing by event' ); hide(); }); if( options.close ){ dialog.on( 'click', function (e) { // If target still exists and isn't a child of the dialog, trigger closeDialog if( dialog.find('> div').get(0) != e.target && !$.contains( dialog.find('> div').get(0), e.target ) && $.contains( document, e.target ) ){ Debug.log( e.target ); $( dialog ).trigger('closeDialog', { dialogID: dialogID, originalEvent: e }); } }); } 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 ){ 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 ); hide(); if( options.flashMessage ){ ips.ui.flashMsg.show( options.flashMessage ); } }; // 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 ){ 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 ) { // 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, _));]]> 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() } ); 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 ); } // 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, _));]]> '); bottom.appendTo( $( '[id="cke_' + this.name + '"]' ).children('.cke_inner') ); $( '[id="cke_' + this.name + '"]' ).find( '.cke_toolbox' ).children().each(function(){ if ( $(this).children('.cke_voice_label').html() == 'bottom_row' ) { $(this).appendTo( bottom ); } }); } try { if( options.minimized ){ $( CKEDITOR.instances[ editorName ].container.$ ).hide(); $( elem ).removeAttr('data-loading'); } else { showing = true; } } catch (err) { Debug.error("CKEditor instance couldn't be fetched"); return; } // If the user has clicked into the editor before ckeditor is ready, we've set a flag // which means we need to show and focus it now if( showAfterInit ){ try { $( CKEDITOR.instances[ editorName ].container.$ ).show(); _widthFix(); CKEDITOR.instances[ editorName ].focus(); $( elem ).removeAttr('data-loading'); showAfterInit = false; } catch (err) { Debug.error("CKEditor instance couldn't be fetched"); return; } } var i; var self = this; elem.find('iframe').contents() .on('dragenter', function (e) { $( this ).find('body').addClass('ipsDragging'); }) .on('dragleave drop', function (e) { $( this ).find('body').removeClass('ipsDragging'); }) .on( 'dragover', function (e){ e.preventDefault(); }) .on( 'drop', function (e) { e.preventDefault(); } ) .on( 'click', function (e) { elem.trigger('click'); }); }, key: function(e){ // If... if ( // We hit the enter key e.data.keyCode === 13 && // And we're in a
this.getSelection().getStartElement().getParent().getName() == 'div' && // And at the end of that div this.getSelection().getRanges()[0].checkBoundaryOfElement( this.getSelection().getStartElement().getParent(), CKEDITOR.END ) === true && // And the previous block is a p this.getSelection().getStartElement().getPrevious() !== null && this.getSelection().getStartElement().getPrevious().getName() == 'p' && // And there's nothing in it !this.getSelection().getStartElement().getPrevious().getText().trim() ) // Then break out { var para = this.document.createElement('p'); para.setHtml(' '); para.insertAfter( this.getSelection().getStartElement().getParent() ); this.getSelection().selectElement(para); } } }, disableNativeSpellChecker: false, pbckcode: { cls: 'ipsCode prettyprint', modes : [ ['HTML', 'html'], ['CSS', 'css'], ['PHP', 'php'], ['JS', 'javascript'] ], theme : 'textmate', tab_size : '4', js: ips.getSetting('baseURL') + 'applications/core/interface/ace' }, removeButtons: '', removePlugins: '', skin: options.skin }; $( elem ).trigger('editorCompatibility', { compatible: CKEDITOR.env.isCompatible }); var device = ips.utils.responsive.getCurrentKey(); if( options.autoGrow ) { config.extraPlugins += ',autogrow'; config.autoGrow_onStartup = true; if( device === 'desktop' ){ config.autoGrow_maxHeight = $(window).height() - 200; } } if( options.extraPlugins !== true ){ config.extraPlugins += ',' + options.extraPlugins; } if( !options.allbuttons ){ var toolbars = $.parseJSON( options.toolbars ); config.toolbar = toolbars[ device ]; } else { config.removePlugins = 'sourcearea'; } if( options.minimized ){ $( elem ) .find('textarea') .hide() .end() .find('.ipsComposeArea_dummy') .show(); $( elem ) .find('[data-ipsEditor-toolList]') .hide() .closest('.ipsComposeArea') .find('[data-ipsEditor-toolList]') .hide(); } initializeEditor(); _initEvents(); }, /** * Destroy this editor instance * * @returns {boolean} */ destruct = function () { // Destroy ckeditor instance if( CKEDITOR.instances[ editorName ] ){ CKEDITOR.instances[ editorName ].destroy(); } // Remove event handlers $( document ).off( 'panelUploadProgress', _progressCounter ); $( document ).off( 'insertEmoticon', _insertEmoticon ); $( document ).off( 'insertQuote', _insertQuote ); $( document ).off( 'insertText', _insertText ); $( document ).off( 'resetEditor', _resetEditor ); $( document ).off( 'fileDeleted', _deleteFile ); $( document ).off( 'initializeEditor', _initializeEditor ); }, /** * Shows a hidden editor * * @returns {boolean} */ showEditor = function () { $( elem ) .find('.ipsComposeArea_dummy') .hide() .off('focus click'); // Important - prevents double-initializtion $( elem ) .find('[data-ipsEditor-toolList]') .show() .closest('.ipsComposeArea') .find('[data-ipsEditor-toolList]') .show(); // Load attachment stuff if necessary var minimizedUploader = $(elem).find('[data-ipsEditor-toolListMinimized]'); if ( minimizedUploader.length ) { ips.getAjax()( elem.parentsUntil( '', 'form' ).attr('action'), { 'data': { 'getUploader': minimizedUploader.attr('data-name') } } ).done(function(response){ minimizedUploader.replaceWith( response ); $( document ).trigger( 'contentChange', [ elem ] ); }); } // If the user has clicked into the editor before ckeditor is ready, we set a flag // here so that ckeditor can be shown and focused when it's ready try { if( _.isUndefined( CKEDITOR.instances[ editorName ].container ) ){ Debug.log( 'EDITOR: showafterinit'); showAfterInit = true; } else { $( CKEDITOR.instances[ editorName ].container.$ ).show(); _widthFix(); CKEDITOR.instances[ editorName ].focus(); showing = true; } } catch (err) { Debug.error("CKEditor instance couldn't be fetched"); return; } // Get top postition of table _scrollToEditor(); }, /** * Causes ckeditor to be initialized * * @param {object} newConfig Optional configuration object which will * be merged with the default config * @returns {boolean} */ initializeEditor = function (newConfig) { if( options.minimized ){ $( elem ).attr( 'data-loading', true ); } // If this is already attached, destroy it and start again /*if( CKEDITOR.instances[ editorName ] ){ CKEDITOR.instances[ editorName ].destroy( true ); }*/ // Set an event handler on the instanceReady event from ckeditor, which fires when the instance // is initialized and ready to accept content. We fire our own event to let controllers know. try { CKEDITOR.timestamp = ipsSettings.antiCache; editor = CKEDITOR.replace( editorName, _.extend( config, newConfig || {} ) ); CKEDITOR.instances[ editorName ].on( 'instanceReady', function (){ elem.trigger( 'editorWidgetInitialized', { id: options.name } ); }); } catch (err) { Debug.error("CKEditor instance couldn't be fetched"); return; } // Editor auto-saved content if( ips.utils.db.enabled && options.autoSaveKey ){ var autoSaveValue = ips.utils.db.get( 'editorSave', options.autoSaveKey ); if( ! editor.getData() && autoSaveValue ){ editor.setData( autoSaveValue ); } // Run every 2 seconds to save updated post content var autoSaveInterval = setInterval( function(){ ips.utils.db.set( 'editorSave', options.autoSaveKey, editor.getData() ); }, 2000 ); elem.closest('form').on('submit', function (e) { if( editor.getSelection() && editor.getSelection().getRanges() && editor.getSelection().getRanges()[0] ){ editor.plugins.ipsautolink.replaceLinks( editor, editor.getSelection().getRanges()[0].getCommonAncestor().getParent().$, false ); } window.clearInterval( autoSaveInterval ); ips.utils.db.remove( 'editorSave', options.autoSaveKey ); }); } // Are we pasting as plain text by default? if( ips.utils.db.enabled && ips.utils.db.get('editorPrefs', 'papt') ){ editor.config.forcePasteAsPlainText = true; } }; // ------------------------------------------------- // PRIVATE METHODS /** * Sets up event handlers for events triggered by panels * * @returns {void} */ var _initEvents = function () { $( document ).on( 'panelUploadProgress', _progressCounter ); $( document ).on( 'insertEmoticon', _insertEmoticon ); $( document ).on( 'insertQuote', _insertQuote ); $( document ).on( 'insertText', _insertText ); $( document ).on( 'resetEditor', _resetEditor ); $( document ).on( 'fileDeleted', _deleteFile ); if( options.minimized ){ $( elem ).find('.ipsComposeArea_dummy').on( 'focus click', _focusDummy ); $( document ).on( 'initializeEditor', _initializeEditor ); } }, /** * Resets the editor, removing text and attachment modals * * @param {event} e Event object * @param {mixed} data Event data object. Either array of event data objects, or a single event data object */ _resetEditor = function (e, data) { // Check we're working with this editor if( !_belongsToThisEditor( data ) ){ return; } // Reset editor state try { CKEDITOR.instances[ data.editorID ].setData('

'); CKEDITOR.instances[ data.editorID ].execCommand( 'ipsImageReset' ); } catch (err) { Debug.error("CKEditor instance couldn't be fetched"); return; } // Remove any mention menus $('body').find('[data-mentionMenu]').remove(); // Reset original textarea $( elem ).find('textarea').val(''); // Reset upload state elem.find( '.cke_button__ipsimage .ipsNotificationCount').text( 0 ).hide(); elem.find( '.cke_button__ipsattach .ipsNotificationCount').text( 0 ).hide(); elem.find('[data-role="progress"]').text( 0 ); elem.find('.ipsAttachment_progress span').css( { width: 0 + '%' } ); elem.find('#elEditor_' + data.editorID + '_uploadProgress'); elem.find('[data-ipsUploader]').trigger('resetUploader'); }, /** * Scrolls the page to the editor * * @returns {boolean} */ _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' } ); } }, /** * Event handler for focusing into the 'dummy' object, which causes the editor * to be initialized * * @param {event} e Event object * @param {object} data Data object from the event * @returns {boolean} */ _focusDummy = function () { setTimeout( function () { showEditor(); }, 200); }, /** * Responds to a document event that initializes the editor ready for use * * @param {event} e Event object * @param {mixed} data Event data object. Either array of event data objects, or a single event data object */ _initializeEditor = function (e, data) { // Check we're working with this editor if( !_belongsToThisEditor( data ) ){ return; } if( showing ){ _scrollToEditor(); } else { showEditor(); } }, /** * Responds to a document event that indicates a quote should be added to the editor * Calls _doInsertQuote to do the insertion, after the editor is initialized (if necessary) * * @param {event} e Event object * @param {mixed} data Event data object. Either array of event data objects, or a single event data object */ _insertQuote = function (e, data) { // Check we're working with this editor if( !_belongsToThisEditor( data ) ){ Debug.log( data ); return; } var doInsert = function (data) { if( !_.isUndefined( data.quotes ) ){ _.each( data.quotes, function (value){ _doInsertQuote( value ); }); } else { _doInsertQuote( data ); } }; // If there's no instance for this ID, set a one-time event handler to watch for the // editorWidgetInitialized event. Only when that is fired will we insert the quote data. if( !showing ){ showEditor(); } doInsert( data ); }, /** * Inserts text into the editor * * @param {object} Quote data object passed from _insertQuote */ _insertText = function (e, data) { // Check we're working with this editor if( !_belongsToThisEditor( data ) ){ return; } var doInsert = function () { try { var editor = CKEDITOR.instances[ data.editorID ]; editor.insertHtml( data.content ); editor.focus(); } catch (err) { Debug.error("CKEditor instance could not be fetched"); return; } }; // If there's no instance for this ID, set a one-time event handler to watch for the // editorWidgetInitialized event. Only when that is fired will we insert the quote data. if( !showing ){ showEditor(); } doInsert(); }, /** * Inserts a quote into the editor * * @param {object} Quote data object passed from _insertQuote */ _doInsertQuote = function (data) { try { var editor = CKEDITOR.instances[ editorName ]; } catch (err) { Debug.error( "CKEditor instance couldn't be fetched" ); return; } // Build citation if( data.username ){ if( data.timestamp ){ var citation = ips.getString( 'editorQuoteLineWithTime', { date: ips.utils.time.readable( data.timestamp ), username: data.username } ); } else { var citation = ips.getString( 'editorQuoteLine', { username: data.username } ); } } else { var citation = ips.getString('editorQuote'); } // Set up the quote element var container = editor.document.createElement( 'div' ); var quote = editor.document.createElement( 'blockquote' ); quote.addClass('ipsQuote'); quote .setAttributes( { 'data-cite': citation, 'data-ipsQuote': '', 'data-ipsQuote-timestamp': data.timestamp, 'data-ipsQuote-userid': data.userid, 'data-ipsQuote-username': data.username, 'data-ipsQuote-contapp': data.contentapp, 'data-ipsQuote-contenttype': data.contenttype, 'data-ipsQuote-contentclass': data.contentclass, 'data-ipsQuote-contentid': data.contentid, 'data-ipsQuote-contentcommentid': data.contentcommentid }) .appendHtml( data.quoteHtml ); container.append( quote ); editor.insertHtml( '
' + container.getHtml() + '

​

', 'unfiltered_html' ); _scrollToEditor(); }, /** * Responds to an insertEmoticon event * * @param {event} e Event object * @param {object} data Data object from the event * @returns {boolean} */ _insertEmoticon = function (e, data) { // Check we're working with this editor try { if( _belongsToThisEditor( data ) ){ CKEDITOR.instances[ editorName ].insertHtml( '' + data.text + '' ); CKEDITOR.instances[ editorName ].focus(); } } catch (err) { Debug.error("CKEditor instance couldn't be fetched"); return; } }, /** * Remove a file from the editor * * @param {event} e Event object * @param {object} data Data object from the event * @returns {boolean} */ _deleteFile = function(e, data){ try { var editor = CKEDITOR.instances[ editorName ]; } catch (err) { Debug.error("CKEditor instance couldn't be fetched"); return; } var links = editor.document.getElementsByTag('a'); for (var i = 0; i < links.count(); ++i) { var link = links.getItem(i); if ( link.$.getAttribute('data-fileid') == data.fileElem.attr('data-fileid') || link.$.getAttribute('href') == ips.getSetting('baseURL') + 'applications/core/interface/file/attachment.php?id=' + data.fileElem.attr('data-fileid') ) { link.remove(); } } var images = editor.document.getElementsByTag('img'); for (var i = 0; i < images.count(); ++i) { var image = images.getItem(i); if ( image.$.getAttribute('data-fileid') == data.fileElem.attr('data-fileid') ) { image.remove(); } } }, /** * Determines whether an event relates to this editor instance * * @param {object} data Data object from the event * @returns {boolean} */ _belongsToThisEditor = function (data) { if( _.isUndefined( data.editorID ) || data.editorID !== options.name ){ return false; } return true; }, /** * Updates the editor-wide progress bar for uploads * * @param {event} e The event object * @param {object} data The event data object * @returns {void} */ _progressCounter = function (e, data) { // Check we're working with this editor if( !_belongsToThisEditor( data ) ){ return; } // Update this progress type progressTracked[ data.contentType ] = data.percent; var sumPercent = 0, totalPercent = 0, timeout; // Work out the current total percentage for (var key in progressTracked) { sumPercent += progressTracked[ key ]; } totalPercent = Math.floor( ( sumPercent / ( _.size( progressTracked ) * 100 ) ) * 100 ); elem.find('[data-role="progress"]').text( totalPercent ); elem.find('.ipsAttachment_progress span').animate( { width: totalPercent + '%' }, 'fast' ); if( !elem.find('#elEditor_' + editorName + '_uploadProgress').is(':visible') ){ ips.utils.anim.go( 'fadeIn', elem.find('#elEditor_' + editorName + '_uploadProgress') ); } // If we're finished, hide the progress bar. if( totalPercent === 100 && !timeout ){ timeout = setTimeout( function () { ips.utils.anim.go( 'fadeOut', elem.find('#elEditor_' + editorName + '_uploadProgress') ); timeout = null; }, 400 ); } }, /** * Removes a fixed width on the iframe if necessary * * @returns {void} */ _widthFix = function () { // Bug #321 & 539 - When the editor has been hidden then is shown, iPhone // doesn't update the size of the iframe. If iframe width is 0, just // set it to auto to let the browser calculate it. var iframe = elem.find('iframe'); if( iframe.width() === 0 ){ Debug.log( "EDITOR: Adjusting width of editor iframe..."); iframe.css({ width: '100%', boxSizing: 'border-box' }); } }; return { init: init, initializeEditor: initializeEditor, destruct: destruct }; }; }(jQuery, _));]]> /* global ips, _ */ /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); 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 // http://community.invisionpower.com/4bugtrack/syntax-highlighing-not-loaded-when-error-occurs-r3591/ if( !_.isUndefined( _cmInstances[ elemId ] ) ) { $( _cmInstances[ elemId ].getWrapperElement() ).remove(); delete _cmInstances[ elemId ]; } _cmInstances[ elemId ] = CodeMirror.fromTextArea( document.getElementById(elemId), { mode: $(elem).attr('data-mode'), lineWrapping: true, lineNumbers: false } ); $( '#' + 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' ), }); // Following line caused http://community.invisionpower.com/resources/bugs.html/_/ips-4-0/stats-date-range-fields-not-showing-calendar-selector-r43909 //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) { elem.addClass('color'); 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), {} ); }); }, /** * 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); } }, //-------------------------------------------------------------- // 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; 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 (elem, toggleList, thisElem, form) { // Get the list of selected items var selectedItems = elem.closest('[data-ipsSelectTree]').find('[data-role="nodeValue"]').val().split(','); var show = ( selectedItems != elem.data('id') && selectedItems.indexOf( elem.data('id') ) == -1 ) ? false : true; if( show ){ _showFormRows( toggleList, form ); } else { _hideFormRows( toggleList, 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 * * 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 () { $( document ).off( 'click', '[data-role="actionButton"]', _selectItem ); $( document ).off( 'menuItemSelected', '[data-role="actionMenu"]', _selectMenuItem ); }, /** * Public access for the internal _updateBar method * * @returns {void} */ refresh = function () { _updateBar(); }, /** * Sets up events that this instance will watch for * * @returns {void} */ _setUpEvents = function () { elem.on( 'change', 'input[type="checkbox"]', _toggleCheckbox ); elem.on( 'refresh.pageAction', _refreshPageAction ); elem.on( 'addManualItem.pageAction', _addManualItem ); $( document ).on( 'click', '[data-role="actionButton"]', _selectItem ); $( document ).on( 'menuItemSelected', '[data-role="actionMenu"]', _selectMenuItem ); }, /** * 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(); /*if ( $( e.currentTarget ).attr('data-action') == 'delete' ) { ips.ui.alert.show( { type: 'confirm', icon: 'warn', message: ips.getString('delete_confirm'), callbacks: { ok: function () { _triggerAction( $( e.currentTarget ).attr('data-action') ); } } }); } else {*/ _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.append( $('').attr( 'type', 'hidden' ).attr( 'name', key ).val(1) ); } }); // 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(); $( 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 }; }; ips.ui.registerWidget( 'pageAction', ips.ui.pageAction, [] ); return { respond: respond, destruct: destruct, getObj: getObj }; }); }(jQuery, _));]]> 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 || '#', pageNo: value, perPage: options.perPage, pageParam: options.pageParam, lastPage: ( parseInt( value ) === parseInt( options.pages ) ) }); }); }; init(); return { init: init }; }; }(jQuery, _));]]> ').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'); }, /** * 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 = $('
    ').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('[data-role="patchwordGrid"] > [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, _));]]> '); /** * Sets up this instance * * @returns {void} */ var init = function () { currentWidth = elem.width(); imageData = _getData(); noOfPhotos = imageData.length; elem.empty(); run( imageData ); // Set up events $( window ).on( 'resize', _resizeWindow ); $( document ).trigger( 'contentChange', [ elem ] ); }, /** * Destruct this widget on this element * * @returns {void} */ destruct = function () { $( window ).off( 'resize', _resizeWindow ); }, /** * 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) { var initialHeight = Math.max( options.minHeight, Math.floor( currentWidth / 5 ) ); // 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 ){ return; } 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 < currentWidth ) && ( 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 = currentWidth / 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 < currentWidth ) { 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 > currentWidth ) { var item = row.find('.cGalleryPatchwork_item:nth-child(' + ( bigWidth + 1 ) + ")"); item.css({ width: ( item.width() - 1 ) + 'px' }); bigWidth = ( bigWidth + 1 ) % imagesInRow; widthOfRow--; } } } elem.find('.cGalleryPatchwork_row').width( currentWidth ); }, /** * 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 medium size instead if( multipliedWidth <= imageData.filenames.small[1] && multipliedHeight <= imageData.filenames.small[2] ){ imageData.src = imageData.filenames.small[0]; } else { imageData.src = imageData.filenames.medium[0]; } } return ips.templates.render( options.itemTemplate, { showThumb: showThumb, image: imageData, dims: extra } ); }, /** * Returns a new row element * * @returns {element} */ _getRow = function () { var row = $('
    ').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 (e) { elem.empty(); currentWidth = elem.width(); noOfPhotos = imageData.length; run( imageData ); }; init(); return { init: init, destruct: destruct, refresh: refresh }; }; }(jQuery, _));]]> 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(); // Get dims of the various elems thumbW = elem.width(); thumbH = elem.height(); //-- wrapperW = wrapper.width(); wrapperH = wrapper.height(); //-- fullW = imageElem.width(); fullH = imageElem.height(); //-- ratio = thumbW / fullW; 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 = $('
    ').attr( 'id', 'ipsZoomer' ); zoomArea = $('
    ').attr('id', 'ipsZoomer_area').hide(); imageElem = $('').attr( 'src', imgURL ).css({ position: 'absolute' }); $('body').append( wrapper.append( imageElem ) ); elem.append( zoomArea ).css({ position: 'relative' }); wrapper .css({ 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; }, /** * 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, _));]]> blockquote.ipsQuote') ){ elem .find('> *:not( .ipsQuote_citation )') .hide() .end() .find('> .ipsQuote_citation') .removeClass('ipsQuote_open') .addClass('ipsQuote_closed'); } elem.find('> .ipsQuote_citation').on( 'click', _toggleQuote ); }, /** * 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, _)); ]]> [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( $('
    ').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.find('.ipsSelectTree_item').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') ); 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( $('
      ').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.find('.ipsSelectTree_item').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: '0px', position: ( resultsPosition.fixed ) ? 'fixed' : '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 {}; }; ips.ui.registerWidget( 'selectTree', ips.ui.selectTree, [ 'placeholder', 'multiple', 'selected', 'url', 'searchable' ] ); return { respond: respond, destruct: destruct }; }); }(jQuery, _));]]> ') ) .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 ); }, /** * 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 }); } 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 ); } // 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) { 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, _));]]> ').addClass('ipsStyle_spoilerFancy_inner') ) .append( ips.templates.render('core.posts.spoiler') ); }, /** * Toggles the spoiler content * * @param {event} e Event object * @returns {void} */ _toggleSpoiler = function (e) { var spoiler = $( e.currentTarget ).closest('.ipsStyle_spoilerFancy'); if( spoiler.hasClass('ipsStyle_spoilerFancy_open') ){ spoiler .removeClass('ipsStyle_spoilerFancy_open') .attr('tabindex', '0') .trigger( 'spoilerHidden' ) .find('.ipsStyle_spoilerFancy_text') .replaceWith( ips.templates.render('core.posts.spoiler') ); } else { spoiler .addClass('ipsStyle_spoilerFancy_open') .removeAttr('tabindex') .blur() .trigger( 'spoilerRevealed' ) .find('.ipsStyle_spoilerFancy_text') .replaceWith( ips.templates.render('core.posts.spoilerOpen') ); } }; // Register this widget with ips.ui ips.ui.registerWidget( 'spoiler', ips.ui.spoiler ); return { respond: respond } }); }(jQuery, _));]]> = options.maxItems ){ return; } /* @link http://community.invisionpower.com/resources/bugs.html/_/ips-4-0/stack-and-arrays-r44513 */ var field = stack.find('[data-ipsStack-wrapper]') .first() .html() .replace( /(name=['"][a-zA-Z0-9\-_]+?)\[([^\]]+?)?\]/g, '$1[' + _getItemCount() + ']' ) .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(',') + '"'; }); if( !stack.find('select').length ) { field = field.replace( /value=['"](.+?)['"]/g, 'value="" ' ); } 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(''); 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, _));]]> = ( originalBottom + options.spacing ) ) && status == 'fixed' ){ _makeNormal(); } } else { if( bodyScroll >= ( originalOffsetTop - options.spacing ) && status == 'normal' ){ _makeFixed(); } else if( bodyScroll <= ( originalOffsetTop - options.spacing ) && status == 'fixed' ){ _makeNormal(); } } }, /** * Puts the element into 'fixed' mode at the top * * @returns {void} */ _makeFixed = function () { if( locked ){ return; } var width; if( !dummyElem ){ _makeDummyElem(); } 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'); } dummyElem .css( { width: $( elem ).width(), height: $( elem ).outerHeight() }) .show(); 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'; }, /** * 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 = $('
      ') .insertAfter( 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 ){ $('
      ').attr('id', 'ipsStickySpacer').appendTo('body'); } $('#ipsStickySpacer').css({ height: ( size + 10 ) + 'px' }); }; _init(); return { destruct: destruct }; }; return { respond: respond, destruct: destruct }; }); }(jQuery, _));]]> 1 ){ if( $( elem ).hasClass('ipsTabs_showMenu') ){ $( elem ).removeClass('ipsTabs_showMenu'); } else { $( elem ).addClass('ipsTabs_showMenu'); } } }, /** * 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 }); }); } 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 }); }, /** * 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('#') ){ History.replaceState( {}, title, 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.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 = $('
      ') .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, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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'); } } }; init(); return { init: init } }; }(jQuery, _)); "; } 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( 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; }, /** * 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 $( elem ) .on( 'injectFile', function( e, file ){ inject = true; pluploadObj.addFile( file ); } ) .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...'); pluploadObj.start(); }, /** * Returns a human-readable, translated status string * * @param {number} status Status code * @returns {string} */ _getStatus = function (status) { switch( status ) { case plupload.QUEUED: return ips.getString('attachQueued'); break; case plupload.UPLOADING: return ips.getString('attachUploading'); break; case plupload.FAILED: return ips.getString('attachFailed'); break; case plupload.DONE: return ips.getString('attachDone'); break; } }, /** * Updates a file element with the current status * * @param {object} file File information object from plupload * @returns {element} The file element */ _updateFileElement = function (file) { var fileElem = _findFileElem( file ); _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 = $('').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 ) + '")' ); } 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(); // Delete via ajax ips.getAjax()( options.action + '&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.maxTotalSize && up.total.size > ( options.maxTotalSize * 1048576 ) ) { ips.ui.alert.show( { type: 'alert', icon: 'warn', message: ips.getString( 'uploadTotalErr', { size: ( options.maxTotalSize > 1 ) ? options.maxTotalSize : ( options.maxTotalSize * 1024 ), size_suffix: ( options.maxTotalSize > 1 ) ? ips.getString('size_mb') : ips.getString('size_kb') } ), callbacks: {} }); 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; // Figure out if this is an image based on extension if( [ 'jpg', 'jpeg', 'jpe', 'gif', 'png' ].indexOf( file.name.substr( ( ~-file.name.lastIndexOf(".") >>> 0 ) + 2 ).toLowerCase() ) !== -1 ){ isImage = true; } var data = { uploaderID: uploaderID, id: file.id, title: file.name, 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 }; // 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: ( options.maxFileSize > 1 ) ? options.maxFileSize : ( options.maxFileSize * 1024 ), 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 }); }, /** * 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( $('').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 ( inject ) { fileElem.dblclick(); inject = false; } }, /** * 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: ( options.maxFileSize > 1 ) ? options.maxFileSize : ( options.maxFileSize * 1024 ), 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 }; } }(jQuery, _));]]> 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, _));]]> ' + response + '
      ' ).find('[data-ipsWizard]').html(); if ( !responseWizard ) { responseWizard = response; } 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 = $( '
      ' + response + '
      ' ).find('[data-ipsWizard]').html(); if ( !responseWizard ) { responseWizard = response; } 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 = $('
      ').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, _));]]> = 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, _));]]> 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, _));]]> 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 #fff 0%,#333 50%,#000 100% */ _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 }; }); }(jQuery, _));]]> ' ).replace( '{content}', '' ) ); 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(); 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}', '' ).replace( '{content}', '' ) ); 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 ( optionValue ) { beforeContent = beforeContent.replace( '{option}', optionValue ); afterContent = afterContent.replace( '{option}', optionValue ); var _tagAttributes = tagAttributes; tagAttributes = {}; for ( i in _tagAttributes ) { var newI = i.replace( '{option}', optionValue ); tagAttributes[newI] = _tagAttributes[i].replace( '{option}', 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 += ''; } 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, _));]]> 0) || (navigator.msMaxTouchPoints > 0) ); }, /** * 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 }; }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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(','); }; return { argsToArray: argsToArray, uppercaseFirst: uppercaseFirst, getIDsFromList: getIDsFromList }; }); }(jQuery,_)); 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( 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' ); }, /** * 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 }; }); }(jQuery, _)); ]]> = 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, _));]]> // 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, getURIObject: getURIObject }; }); }(jQuery, _));]]> = 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.getString( 'validation_' + type, { data: args }); }, /** * 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") }());]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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') ) .done( function (response) { var row = elem.closest('div.ipsDataItem'); row.remove(); ips.utils.anim.go( 'fadeOut', row ); }); } } }); } }); }(jQuery, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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]', this.confirmSomething ); 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() ); } // 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"; }) } // 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() ); } }, /** * 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'); ips.ui.alert.show( { type: 'confirm', icon: 'warn', message: ( customMessage ) ? customMessage : ips.getString('generic_confirm'), subText: ( subMessage ) ? subMessage : '', callbacks: { ok: function () { window.location = elem.attr('href'); } } }); }, }); }(jQuery, _)); = ( 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(); // And reinit content $( document ).trigger( 'contentChange', [ this.scope ] ); } }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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', { initialize: function () { this.on( 'load', this.resizeFrame ); }, /** * Event handler to resize frame * * @param {event} e Event object * @returns {void} */ resizeFrame: function (e) { // I know this seems bad, but there's no easy way to detect when the iframe's contents have loaded setTimeout( function(){ e.target.style.height = e.target.contentWindow.document.body.scrollHeight + 'px'; e.target.style.overflow = 'hidden'; e.target.scrolling = "no"; e.target.seamless = "seamless"; }, 1500 ); } }); }(jQuery, _)); ').html( this.scope.find('[data-role="commentContent"]').html() ) ); Debug.log( this._commentID ); // 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( $('
      ').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: 'confirm', icon: 'warn', message: ips.getString('generic_confirm'), subText: '', callbacks: { ok: function () { 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(); } var postBody = $.trim( CKEDITOR.instances['comment_value'].editable().getData().replace( / /g, '' ) ); if( postBody == '' || postBody.match( /^

      (

      |<\/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(); 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; } this.scope.find('[data-action="expandTruncate"], [data-role="commentControls"]').hide(); this.scope.find('[data-role="commentContent"]').html( data.response ); $( document ).trigger( 'contentChange', [ this.scope.find('[data-role="commentContent"]') ] ); }, /** * 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; } this.scope.find('[data-role="commentContent"]').replaceWith( $('

      ' + data.response + '
      ').find('[data-role="commentContent"]') ); this.scope.trigger('initializeImages'); this.scope.find('[data-action="expandTruncate"], [data-role="commentControls"]').show(); $( document ).trigger( 'contentChange', [ this.scope ] ); }, /** * Model event: saving an edit failed * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ saveEditCommentError: function (e, data) { Debug.log( 'save comment error' ); 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 = $('
      ' + data.response + '
      ').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) { if( ips.getSetting('strip_quotes') == 1 ) { // @link http://community.invisionpower.com/resources/bugs.html/_/4-0-0/empty-line-when-quoting-post-with-quote-while-quote-nesting-is-disabled-r46720 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(); } } html.find('blockquote.ipsQuote *').show(); // expand collapsed quotes html.find('.ipsStyle_spoilerFancy,.ipsStyle_spoiler').replaceWith( ips.templates.render( 'core.posts.quotedSpoiler' ) ); // Used for "edited by" byline presently, but can be used by anything html.find("[data-excludequote]").remove(); html.find('.ipsQuote_citation').remove(); html.find( '[data-quote-value]' ).each( function () { $( this ).replaceWith( '

      ' + $( this ).attr('data-quote-value') + '

      ' ); }); 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) { alert(this.scope.attr('data-quoteData')); Debug.log("Couldn't parse quote data"); return {}; } } return this._quoteData; } }); }(jQuery, _)); ]]> ' + response + '
      ' ).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(); // 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 = $('
      ').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 + '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 } ); History.pushState( tmpStateData, document.title, this._baseURL + this._getURL() ); }, /** * 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' ){ 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: {} }); }); }, /** * 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; } }, /** * 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 editorID = this.scope.find('[data-role="replyArea"] [data-role="contentEditor"]').attr('name'); if( editorID ){ this.trigger('insertQuote', _.extend( data, { editorID: editorID } ) ); } }, /** * 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 ); } }, /** * 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 = $( commentsOnThisPage[ commentsOnThisPage.length - 1 ] ).attr('data-commentId'); 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, type: 'post' }) .done( function (response) { if( parseInt( response.count ) > 0 ) { // Reset the poll interval self._currentPoll = self._initialPoll; var html = ''; 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' } ); } } 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(); } }); }, /** * 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 = $( commentsOnThisPage[ commentsOnThisPage.length - 1 ] ).attr('data-commentId'); var self = this; ips.getAjax()( form.attr('action'), { data: 'do=checkForNewReplies&type=fetch&lastSeenID=' + _lastSeenID, type: 'post' }).done( function (response) { 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._clearNewPostFlashMsg(); }); }, /** * Close the flash message for new post notifications * * @returns {void} */ _clearNewPostFlashMsg: function () { 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"]'); // 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, type: 'post' }) .done( function (response) { if ( response.type == 'error' ) { 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() ) ) ); }); } var textarea = self.scope.find('[data-role="replyArea"] textarea'); self.trigger( 'resetEditor', { editorID: textarea.attr('name') } ); 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 }); // 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._clearNewPostFlashMsg(); }) .fail( function () { form.attr('data-noAjax', 'true'); 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 = $('
      ').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 ); // 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() ) ) ); }); } ips.utils.anim.go( 'fadeInDown', comment ); if( _.isUndefined( data.resetEditor ) || data.resetEditor !== false ){ this.trigger( 'resetEditor', { editorID: textarea.attr('name') } ); } if( ips.utils.db.isEnabled() ){ var buttons = comment.find('[data-action="multiQuoteComment"]'); buttons.hide().removeClass('ipsHide'); ips.utils.anim.go('fadeIn', buttons); } this._updateCount(data.totalItems); $( document ).trigger( 'contentChange', [ this.scope ] ); }, /** * Responds to an event indicating thay a comment has been deleted * Show it, and reset the contents of ckeditor * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ deletedComment: function( e, data ) { data = $.parseJSON( data.response ); var self = this; if( data.type == 'redirect' ) { window.location = data.url; } else { this._updateCount( data.total ); } }, /** * Update comment count * * @param {int} newTotal The new total * @returns {void} */ _updateCount: function(newTotal) { if ( $(this.scope).find('[data-role="comment_count"]') ) { var langString = 'js_num_comments'; if ( $(this.scope).find('[data-role="comment_count"]').attr('data-commentCountString') ) { langString = $(this.scope).find('[data-role="comment_count"]').attr('data-commentCountString'); } $(this.scope).find('[data-role="comment_count"]').text( ips.pluralize( ips.getString( langString ), newTotal ) ); } }, /** * Event handler for the 'Quote x posts' button in multiquote popup * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ doMultiQuote: function (e) { var mqData = this._getMultiQuoteData(); var replyArea = this.scope.find('[data-role="replyArea"]'); var editor = replyArea.find('[data-role="contentEditor"]'); var editorID = editor.attr('name'); 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 ); }); self.trigger( 'insertQuote', { editorID: editorID, quotes: 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 ); } } }, /** * 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, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); 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' }); } } } }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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'), {} ); }, /** * 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() ); }, /** * 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) { 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 self.trigger('followingItem', { feedID: self._area + '-' + self._id }); 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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); = 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 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 ) ) != -1 ) { // Only if the link is to an image if( !ips.utils.responsive.enabled() || !ips.utils.responsive.currentIs('phone') ){ // And only if we're not on mobile 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 ){ if( ips.utils.responsive.enabled() && ips.utils.responsive.currentIs('phone') ){ image.wrap( $( "" ) ); } else { image.wrap( $( "" ) ); } } } }); }); } }); }(jQuery, _));]]> = 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( $( e.currentTarget ).attr('id') != this._modal.attr('id') ){ return; } this._hideResults(); }, /** * Timer callback from this.fieldFocus * Compares current value to previous value, and shows/loads new results if it's changed * * @returns {void} */ _timerFocus: function () { var currentValue = this._textField.val(); if( currentValue == this._lastValue || this._textField.val().length < 4 ){ 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( this._ajax ){ for( var i = 0; i < this._ajax.length; i++ ) { if( _.isFunction( this._ajax[i].abort ) ) { this._ajax[i].abort(); } } } 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 = 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(); } }).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( $('
      ') .attr('data-resultsSection', panel ) .addClass('ipsScrollbar') .append( $('
        ') .addClass('ipsList_reset') ) ); }, /** * Builds the modal element * * @returns {void} */ _buildModal: function () { this._modal = ips.ui.getModal(); } }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.com * * ips.core.loginMenu.js - Controller for login popup * * Author: Rikki Tissier */ ;( function($, _, undefined){ "use strict"; ips.controller.register('core.front.core.loginMenu', { initialize: function () { // Events to trigger this.on( 'success.validation', 'form', this.doSignIn ); }, doSignIn: function (e) { e.preventDefault(); var form = this.scope.find('form'); var url = form.attr('action'); this._loading( true ); ips.getAjax()( url, { type: 'post', data: form.serialize(), dataType: 'json' }) .done( function (response) { window.location.reload(); }) .fail( function (jqXHR, textStatus, errorThrown) { Debug.log( textStatus ); }); }, _loading: function (doLoading) { var dims = { width: this.scope.outerWidth(), height: this.scope.outerHeight() }; this.scope .css( { height: dims.height + 'px', width: dims.width + 'px' }) .addClass('ipsLoading') .find('[data-role="loginForm"]') .hide(); } }); }(jQuery, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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( '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(); $('body').toggleClass('cSearchOpen'); } }); }(jQuery, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); ').attr( { type: 'text' } ).attr( 'data-role', 'editTitleField' ).val( anchor.text().trim() ); anchor.after(inputNode); inputNode.focus(); inputNode.on('blur', function(){ inputNode.addClass('ipsField_loading'); ips.getAjax()( anchor.attr('href'), { 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; } }); }, 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', 'narrow'); 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(); } } }); }, /** * 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, _));]]> #acpAppList > li", 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); } }, /** * Hides the submenu when the main body is clicked on * * @param {event} e Event object * @returns {void} */ clickMainArea: function (e) { 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, _));]]> availableSpace ){ if( !item.attr('data-mushed') ){ var newLI = $('
      1. ') .attr('data-originalItem', item.identify().attr('id') ) .addClass('ipsMenu_item') .append( item.contents() ); if( newLI.find('.ipsMenu').length ){ newLI.addClass('ipsMenu_subItems'); } $('#elNavigationMore_dropdown_menu').append( newLI ); item .addClass('ipsHide') .attr('data-mushed', true); } } else if( item.attr('data-mushed') ) { // If this item has previously been mushed, we can unmush it by moving the contents // back to its original location var mushedItem = $('#elNavigationMore_dropdown_menu').find('[data-originalItem="' + item.identify().attr('id') + '"]'); if( mushedItem.length ){ item.append( mushedItem.contents() ).removeClass('ipsHide'); mushedItem.remove(); } item.removeAttr('data-mushed'); } sizeIncrem += itemSize; }); // Show the "More" item if it has any children in the menu moreItem.toggleClass('ipsHide', !$('#elNavigationMore_dropdown_menu').find('> li').length ); } }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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); }) .fail( function () { window.location = data.href; }); } }); }(jQuery, _)); = 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.remove(); 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 ) ); } } 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, _));]]> 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.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 selectedText = ''; var itemValue = ''; if( data.selectedItemID && data.selectedItemID != '-' ){ selectedText = ips.getString( 'selectedPrefix', { tag: data.selectedItemID } ); itemValue = data.selectedItemID; } else { selectedText = ips.getString( 'selectedPrefix', { tag: ips.getString('selectedNone') } ); itemValue = ''; } 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: item, checked: ( item == value ) }); }); return output; }, }); }(jQuery, _));]]> ') .attr( 'type', 'hidden' ) .attr( 'name', 'type' ) .attr( 'value', data.selectedItemID ) .attr( 'data-role', 'searchFilter' ) ); return; } // Build the form inputs from the options data on this item try { options = $.parseJSON( options ); _.each( options, function (val, i) { form.append( $('') .attr( 'type', 'hidden' ) .attr( 'name', i ) .attr( 'value', val ) .attr( 'data-role', 'searchFilter' ) ); }); } catch (err) { Debug.log('Invalid search options json'); return; } } }, /** * Event handler for focusing the search box * * @param {event} e Event object * @returns {void} */ focusSearch: function (e) { if( this._expanded ){ return; } $('#elSearch') .addClass('cSearchExpanded'); ips.utils.anim.go('fadeIn', $('#elSearchFilter') ); var scope = $(this.scope); ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=search&controller=search&do=globalFilterOptions' ).done(function( response ){ scope.find('[data-role="globalSearchMenuOptions"]').replaceWith( response ); }); this._expanded = true; }, /** * Shrinks search box when blurred. Sets a timer and hands off to _cancelSearch to actually shrink it. * * @returns {void} */ blurSearch: function () { var self = this; // First set a small timeout so we can cancel this if we want this._blurTimeout = setTimeout( function (){ self._cancelSearch(); }, 500 ); }, /** * Shrinks search box when blurred. * * @param {boolean} loseFocus * @returns {void} */ _cancelSearch: function () { if( ips.utils.responsive.currentIs('phone') ){ ips.utils.anim.go('fadeIn fast', $('#elHeaderNavigation') ); } $('#elSearch') .removeClass('cSearchExpanded'); ips.utils.anim.go('fadeOut fast', $('#elSearchFilter') ); this._expanded = false; }, /** * Event handler for the filter menu being opened * * @param {event} e Event object * @returns {void} */ menuOpened: function (e, data) { Debug.log( data.elemID ); if( data.elemID == 'elSearchFilter' ){ clearTimeout( this._blurTimeout ); } } }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); ' + response + '
      ').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, _)); ]]>
      /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); 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, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.com * * ips.core.statusFeedWidget.js - Turns content using the page bbcode into paginated content * * Author: Mark Wade */ ;( function($, _, undefined){ "use strict"; ips.controller.register('core.front.core.statusFeedWidget', { initialize: function () { var scope = $(this.scope); $(document).on( 'submitDialog', '[data-role="updateStatus"]', function ( e, data ) { scope.find('[data-role="statusFeedEmpty"]').hide(); var newstatus = $( data.response.content ); scope.find('[data-role="statusFeed"]').prepend( newstatus ); ips.utils.anim.go( 'fadeIn', newstatus ); } ); } }); }(jQuery, _)); 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 } ); //Debug.log( tmpStateData ); History.pushState( tmpStateData, document.title, this._baseURL + this._getURL() ); }, /** * 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 += $.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(); ips.getAjax()( this._baseURL + ( ( urlBits ) ? this._getURL() + '&' : '' ) + this.scope.attr('data-resort') + '=1', { dataType: 'json', showLoading: true }) .done( _.bind( this._getResultsDone, this ) ) .fail( _.bind( this._getResultsFail, this ) ) .always( _.bind( this._getResultsAlways, this ) ); }, /** * 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() ){ 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) { // Table body this.scope.find('[data-role="tableRows"]').html( response.rows ).trigger('tableRowsUpdated'); // Pagination this.scope.find('[data-role="tablePagination"]').html( response.pagination ).trigger('tablePaginationUpdated'); // Eztra this.scope.find('[data-role="extraHtml"]').html( response.extraHtml ); // 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' ){ 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; }, /** * Abstract */ _getSortValue: $.noop, _getFilterValue: $.noop, _getResultsAlways: $.noop }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); ').html( $( item ).html() ).addClass('cke_button').addClass('cke_toolbar_separator').css({ zIndex: 50000 }); } else { return $('
      ').html( $( item ).find('> a').html() ).addClass('cke_button dummy_button').css({ zIndex: 50000 }); } }, /** * 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 = $('
        '); list.sortable({ connectWith: '[data-role="dummyToolbar"]', update: _.bind( this._saveToolbars, this ), helper: this._getHelper, 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 ); $('#' + elem.attr('data-deviceKey') + '_editor_toolbars') .children() .last() .append( $('
      • ') ); }, /** * 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( $('
      • ').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, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.com * * ips.customization.emoticons.js - Emoticons controller * * Author: Rikki Tissier */ ;( function($, _, undefined){ "use strict"; ips.controller.register('core.admin.customization.emoticons', { initialize: function () { 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"]'), update: _.bind( this._saveOrder, this ) }); }, /** * Saves the new emoticon order * * @returns {void} */ _saveSetOrder: function () { var setOrder = []; this.scope.find('[data-emoticonSet]').each( function () { setOrder.push( $( this ).attr('data-emoticonSet') ); }); console.log({ setOrder: setOrder }); ips.getAjax()( this.scope.attr('action'), { type: 'post', data: { setOrder: setOrder } }); }, /** * Saves the new emoticon order * * @returns {void} */ _saveOrder: function () { var output = {}; // 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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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') ) .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, _)); ') .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()( ips.getSetting('baseURL') + 'index.php?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, '$2' ) ); } 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, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.com * * ips.dashboard.delta.js - Controller to make the download bar when downloading a delta update * * Author: Rikki Tissier */ ;( function($, _, undefined){ "use strict"; ips.controller.register('core.admin.dashboard.delta', { initialize: function () { if ( navigator.userAgent.indexOf('Mac OS X') != -1 ) { $('#deltaUpgrade_tab_mac').click(); } var scope = $(this.scope); var done = false; setTimeout( function(){ if ( scope.find('[role="prep"]').is(':visible') ) { scope.find('[data-role="normal"]').hide(); scope.find('[data-role="fallback"]').show(); } }, 3000 ); window.location = scope.attr('data-url'); scope.find('[data-role="prep"]').hide(); scope.find('[data-role="downloading"]').show(); scope.find('[data-action="continue"]').click(function(e){ e.preventDefault(); var button = $(this); ips.ui.alert.show( { type: 'confirm', icon: 'warn', message: ips.getString('delta_upgrade_confirm'), callbacks: { ok: function () { window.location = button.attr('href'); } } }); }); } }); }(jQuery, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); ' + 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, _));]]> 7 ){ existing.splice( ( existing.length - 7 ) * -1, existing.length - 7 ); } ips.utils.cookie.set('recentEmoticons', JSON.stringify( existing ), true ); } } }, /** * The search box has received focus * * @param {event} e Event object * @returns {void} */ searchEmoticons: function (e) { // Find the last-active panel. If search is the last active (because we blurred then // came back), then just carry on without rememberig it var lastActive = this.scope.find('[data-panel]:visible').attr('data-panel'); if( lastActive != 'search' ){ this._lastActive = lastActive; } // Set a continuous timer 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 () { if( this.scope.find('[data-panel="search"]') ){ this.scope.find('[data-panel="search"]').remove(); // Display the previous panel if( !this._lastActive ){ this._lastActive = 'overview'; } this.scope.find('[data-panel="' + this._lastActive + '"]').show(); } }, /** * Finds emoticons matching the value * * @param {string} value Search value * @returns {void} */ _doSearch: function (value) { var panel = this._getSearchPanel(); var matchEmoticons = []; for( var cat in this._emoticonsObj ){ var catLength = this._emoticonsObj[cat].emoticons.length; // Loop through each emoticon, and do a very simple indexOf search on the // src and text properties for( var i = 0; i < catLength; i++ ){ if( this._emoticonsObj[cat].emoticons[i].src.indexOf( value ) !== -1 || this._emoticonsObj[cat].emoticons[i].text.indexOf( value ) !== -1 ){ matchEmoticons.push( { src: this._emoticonsObj[cat].emoticons[i].src, text: this._emoticonsObj[cat].emoticons[i].text }); } } } if( !matchEmoticons.length ){ panel.html( ips.templates.render('core.editor.emoticonNoResults') ); } else { var rows = this._buildEmoticonRows( matchEmoticons ); var content = ips.templates.render('core.editor.emoticonCategory', { emoticons: rows.join(''), title: ips.getString('emoticonSearch'), categoryID: 'search' }); panel.html( ips.templates.render('core.editor.emoticonSection', { content: content, id: 'search' })); } }, /** * Gets the search results panel, or builds it * * @returns {element} */ _getSearchPanel: function () { var panel = this.scope.find('[data-panel="search"]'); if( !panel.length ){ this.scope .find('.ipsMenu_innerContent') .append( ips.templates.render('core.editor.emoticonSection', { content: '', id: 'search' })); panel = this.scope.find('[data-panel="search"]'); // Now hide all panels, then fade in the one we want this.scope.find('[data-panel]').hide(); ips.utils.anim.go( 'fadeIn', this.scope.find('[data-panel="search"]') ); } return panel; }, /** * 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.elemID == 'elEmoticons' ){ data.originalEvent.preventDefault(); // Do we need to init? if( !this._doneInit ){ this._buildEmoticonsPanel(); this._buildOverview(); } } }, /** * 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) { if( !$.contains( this.scope.get(0), data.triggerElem ) ){ return; } data.originalEvent.preventDefault(); e.stopPropagation(); var categoryID = data.selectedItemID || false; // See if we've got an invalid ID for some reason if( categoryID === false || ( categoryID != 'overview' && !this._emoticonsObj[ categoryID ] ) ){ return; } // Already showing this one? if( this.scope.find('[data-panel="' + categoryID + '"]').is(':visible') ){ return; } // If the panel doesn't already exist, build it if( !this.scope.find('[data-panel="' + categoryID + '"]').length ){ var primaryContent = ''; var rows = this._buildEmoticonRows( this._emoticonsObj[ categoryID ].emoticons ); primaryContent = ips.templates.render( 'core.editor.emoticonCategory', { emoticons: rows.join(''), title: this._emoticonsObj[ categoryID ].title, categoryID: categoryID }); this.scope .find('.ipsMenu_innerContent') .append( ips.templates.render('core.editor.emoticonSection', { content: primaryContent, id: categoryID })); } // Now hide all panels, then fade in the one we want this.scope.find('[data-panel]').hide(); ips.utils.anim.go( 'fadeIn', this.scope.find('[data-panel="' + categoryID + '"]') ); }, /** * Called by the initialization function. Sets up the panel, building the initial overview * list and compiling templates we'll need later. * * @returns {void} */ _buildEmoticonsPanel: function () { // Build the categories menu var menuContent = []; for( var cat in this._emoticonsObj ){ menuContent.push( ips.templates.render('core.editor.emoticonMenu', { title: this._emoticonsObj[ cat ].title, count: this._emoticonsObj[ cat ].total, categoryID: cat })); } $( this.scope ).find('[data-role="categoryMenu"]').append( menuContent.join('') ); // Mark as done this._doneInit = true; }, _getJSON: function () { var deferred = $.Deferred(); var self = this; // If there's already a request going, abort it if( this._ajaxObj && this._ajaxObj.abort ){ this._ajaxObj.abort(); } this._ajaxObj = ips.getAjax(); // Set loading to true this._setLoading(); this._ajaxObj( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=editor&do=emoticons', { dataType: 'json' }) .done( function (emoticons) { ips.setSetting('emoticons', emoticons); self._emoticonsObj = emoticons; deferred.resolve(); }) .fail( function () { deferred.reject(); }) .always( function () { self._setLoadingDone(); }); return deferred.promise(); }, /** * Sets the panel as loading - adds a loading thingy, disables the search box * and hides the category menu * * @returns {void} */ _setLoading: function () { this.scope .find('[data-panel="loading"]') .remove() .end() .find('.ipsMenu_innerContent') .append( ips.templates.render('core.editor.emoticonSection', { content: '', id: 'loading' })) .find('[data-panel="loading"]') .css( { height: '150px' } ) .addClass('ipsLoading') .end() .end() .find('[data-role="emoticonSearch"]') .prop('disabled', true) .end() .find('[data-role="categoryTrigger"]') .hide(); }, /** * Removes the loading state - removes loading thing, enabled textbox, shows category menu * * @returns {void} */ _setLoadingDone: function () { this.scope .find('[data-panel="loading"]') .remove() .end() .find('[data-role="emoticonSearch"]') .prop('disabled', false) .end() .find('[data-role="categoryTrigger"]') .show(); }, /** * Fills the overview panel with up to 21 emoticons from each category * * @returns {void} */ _buildOverview: function () { var primaryContent = []; var cookie = ips.utils.cookie.get('recentEmoticons'); try { var recentlyUsed = $.parseJSON( cookie ); var rows = this._buildEmoticonRows( recentlyUsed, 7 ); primaryContent.push( ips.templates.render('core.editor.emoticonCategory', { emoticons: rows.join(''), title: ips.getString('emoticonRecent'), categoryID: 'recent' })); } catch(err) { Debug.warn('Problem loading recently used emoticons cookie.'); } // Build the emoticons // Loop through each category, then loop through each emoticon in each for( var cat in this._emoticonsObj ){ var rows = this._buildEmoticonRows( this._emoticonsObj[ cat ].emoticons, 21 ); primaryContent.push( ips.templates.render('core.editor.emoticonCategory', { emoticons: rows.join(''), title: this._emoticonsObj[ cat ].title, categoryID: cat })); } this.scope .find('.ipsMenu_innerContent') .html( ips.templates.render('core.editor.emoticonSection', { content: primaryContent.join(''), id: 'overview' })); }, /** * Given an array of emoticons, builds rows of emoticons for panels, up to the maximum number of * emoticons specified * * @param {array} emoticons Array of emoticon data * @param {number} max Max number of emoticons to show. Shows all if not specified * @returns {array} Array of rows, ready to be joined as a string */ _buildEmoticonRows: function (emoticons, max) { if( !max ){ max = emoticons.length; } var thisCategory = []; var emoticonArray = []; var pos = 0; for( var i = 0; i < max; i++ ){ if( pos == 0 && _.isUndefined( emoticons[ i ] ) ){ break; } if( !_.isUndefined( emoticons[ i ] ) ){ emoticonArray.push( ips.templates.render('core.editor.emoticonItem', { tag: emoticons[i].text, src: emoticons[i].src, img: "" })); this._testEmoticon( emoticons[i].src ); } else { emoticonArray.push( ips.templates.render('core.editor.emoticonBlank') ); } pos++; if( pos == 7 || ( i + 1 ) == max ){ thisCategory.push( ips.templates.render('core.editor.emoticonRow', { emoticons: emoticonArray.join('') }) ); pos = 0; emoticonArray = []; } } return thisCategory; }, /** * Test if an image actually exists before adding it to the array * * @param {string} emoticon Emoticon URL */ _testEmoticon: function( emoticon ) { var image = new Image(); var noerror = true; image.onerror = function(){ $('[data-src="' + emoticon + '"]').remove(); } image.src = emoticon; } }); }(jQuery, _));]]> parseInt( $(this.scope).find('[data-role="imageWidth"]').attr('max') ) ) { error = true; $(this.scope).find('[data-role="imageWidth"]').closest('.ipsFieldRow').addClass('ipsFieldRow_error'); $(this.scope).find('[data-role="imageSizeWarning"]').text( ips.getString( 'editorImageMaxWidth', { 'maxwidth': $(this.scope).find('[data-role="imageWidth"]').attr('max') } ) ); } if ( parseInt($(this.scope).find('[data-role="imageHeight"]').val() ) > parseInt( $(this.scope).find('[data-role="imageHeight"]').attr('max') ) ) { error = true; $(this.scope).find('[data-role="imageWidth"]').closest('.ipsFieldRow').addClass('ipsFieldRow_error'); $(this.scope).find('[data-role="imageSizeWarning"]').text( ips.getString( 'editorImageMaxHeight', { 'maxheight': $(this.scope).find('[data-role="imageHeight"]').attr('max') } ) ); } if ( !error ) { var editor = CKEDITOR.instances[ $( this.scope ).data('editorid') ]; var selection = editor.getSelection(); var selectedElement = selection.getSelectedElement(); if ( $(this.scope).find('[data-role="imageLink"]').val() ) { var url = $(this.scope).find('[data-role="imageLink"]').val(); if ( !url.match( /^[a-z]+\:\/\//i ) ) { url = 'http://' + url; } if ( $(selectedElement.$).parent().prop('tagName') === 'A' ) { $(selectedElement.$).parent().attr( 'href', url ); } else { $(selectedElement.$).wrap( $('').attr( 'href', url ) ); } } else { if ( $(selectedElement.$).parent().prop('tagName') === 'A' ) { $(selectedElement.$).parent().replaceWith( $(selectedElement.$) ); } } $(selectedElement.$).css({ "width": $(this.scope).find('[data-role="imageWidth"]').val(), "height": $(this.scope).find('[data-role="imageHeight"]').val(), "border": $(this.scope).find('[data-role="imageBorder"]').val() + 'px solid black', "margin": $(this.scope).find('[data-role="imageMargin"]').val() + 'px' }); if ( $(this.scope).find('[data-role="imageAlign"]:checked').val() ) { if ( $(selectedElement.$).parent().prop('tagName') === 'A' ) { $(selectedElement.$).parent().css( 'float', $(this.scope).find('[data-role="imageAlign"]:checked').val() ); } else { $(selectedElement.$).css( 'float', $(this.scope).find('[data-role="imageAlign"]:checked').val() ); } } else { $(selectedElement.$).css( 'float', '' ); if ( $(selectedElement.$).parent().prop('tagName') === 'A' ) { $(selectedElement.$).parent().css( 'float', '' ); } } this.trigger('closeDialog'); } } }); }(jQuery, _));]]> 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-isImage') ) ? 'image' : 'file'; var url = ''; var image = ''; var extension = ''; 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 { 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 }; }, /** * Builds an element that can be inserted into the editor * * @param {object} item Item data used to build element * @returns {void} */ _buildInsert: function (item, editor) { var element = null; if( item.type == 'image' ){ element = editor.document.createElement('img'); element.setAttribute( 'data-fileid', item.fileID ); element.setAttribute( 'src', item.link ); element.addClass('ipsImage ipsImage_thumbnailed'); if ( item.extension ) { element.setAttribute( 'data-extension', item.extension ); } if( item.fullImage ){ var link = editor.document.createElement('a'); link.setAttribute( 'href', item.fullImage ); link.addClass('ipsAttachLink ipsAttachLink_image'); element.addClass( 'ipsImage_thumbnailed'); link.append( element ); element = link; } } else { element = editor.document.createElement('a'); element.setHtml( item.title ); element.addClass('ipsAttachLink'); if( item.link ){ element.setAttribute( 'href', item.link ); } else { element.setAttribute( 'href', ips.getSetting('baseURL') + 'applications/core/interface/file/attachment.php?id=' + item.fileID ); } } if ( item.extension ) { element.setAttribute( 'data-extension', item.extension ); } return element; } }); }(jQuery, _));]]> " ); element.setAttribute( 'src', url.replace(/'/g, ''').replace(/"/g, '"').replace(//g, '>') ); } self.scope.find('input.cEditorURL').val(''); editor.insertElement( element ); dialog.setLoading(false); self.trigger('closeDialog'); }) .fail(function(){ var element = CKEDITOR.dom.element.createFromHtml( "" ); element.setAttribute( 'src', url.replace(/'/g, ''').replace(/"/g, '"').replace(//g, '>') ); self.scope.find('input.cEditorURL').val(''); editor.insertElement( element ); dialog.setLoading(false); self.trigger('closeDialog'); }); } else { var title = this.scope.find('[data-role="linkText"]').val().replace( / {2}/g,'  ' ); if ( !title ) { title = decodeURI( url ); } title = title.replace(/&/g, '&').replace(//g, '>'); var element = CKEDITOR.dom.element.createFromHtml( "" + title + "" ); element.setAttribute( 'href', url.replace(/'/g, ''').replace(/"/g, '"').replace(//g, '>') ); this.scope.find('input.cEditorURL').val(''); editor.insertElement( element ); 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.insertHtml( this.scope.find('[data-role="linkText"]').val().replace( / {2}/g,'  ' ) ); this.trigger('closeDialog'); }, }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.com * * ips.editor.mymedia.js - My media controller * * Author: Rikki Tissier */ ;( function($, _, undefined){ "use strict"; ips.controller.register('core.global.editor.mymedia', { initialize: function () { $( window ).on( '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, _)); 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"]').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 { 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"]').hide(); count++; } }); if( count == 2 ){ this.scope.find('[data-role="fileList"]').hide(); } } }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2015 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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 ) .done( function (response) { self.trigger('refreshResults'); }); } } }); } }); }(jQuery, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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.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._checkEachTab(); }, /** * 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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); ') .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) { 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+'"]').html( 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 ); } } }, /** * Starts a search by triggering on the model * * @param {event} e Event object * @returns {void} */ _startSearch: function (e) { var textField = this.scope.find('[data-role="messageSearchText"]'); var query = textField.val(); this.trigger('searchFolder.messages', { folder: this._currentFolder, q: query }); }, /** * 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(); }, /** * 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') .find('.ipsItemStatus') .remove(); 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(); } }); }(jQuery, _));]]> 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('conversationDelete'), 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( $('
        ') .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 }); // 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, _)); ]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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); 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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.com * * ips.profile.cropper.js - Cropping controller * * Author: Rikki Tissier */ ;( function($, _, undefined){ "use strict"; ips.controller.register('core.front.profile.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"]'), }; 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 this._image.cropper({ aspectRatio: 1 / 1, autoCropArea: 0.9, minContainerWidth: width, minContainerHeight: height, crop: function (data) { self._coords.topLeftX.val( data['x'] ); self._coords.topLeftY.val( data['y'] ); self._coords.bottomRightX.val( data['width'] + data['x'] ); self._coords.bottomRightY.val( data['height'] + data['y'] ); } }); } }); }(jQuery, _)); ').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, _));]]> ').append( data.content ); this.scope.find('[data-role="statusComments"]').append( content ); ips.utils.anim.go( 'fadeInDown', content ); this.trigger( 'resetEditor', { editorID: textarea.attr('name') } ); $( document ).trigger( 'contentChange', [ this.scope ] ); } }); }(jQuery, _));]]> ').append( response ); var comment = content.find('.ipsComment').first(); // Must select first(), because statuses can contain sub-comments feed.prepend( comment ); ips.utils.anim.go( 'fadeInDown', comment ); self.trigger( 'resetEditor', { editorID: textarea.attr('name') } ); $( document ).trigger( 'contentChange', [ comment ] ); }) .fail( function () { //form.submit(); }) .always( function () { submit .prop( 'disabled', false ) .text( currentText ); }); } }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.com * * ips.search.date.js - Date popups for search * * Author: Rikki Tissier */ ;( function($, _, undefined){ "use strict"; ips.controller.register('core.front.search.date', { initialize: function () { this.on( 'click', '[data-action="ok"]', this.ok ); }, /** * Toggles the menu when in responsive mode * * @param {event} e Event object * @returns {void} */ ok: function (e) { // Get the dates var type = this.scope.attr('data-dateType'); var start = this.scope.find('[data-role="start"]').val(); var end = this.scope.find('[data-role="end"]').val(); // Send them up to the main search controller this.trigger( 'chosenDate.search', { type: type.replace('Custom', ''), start: start, end: end }); // Close the dialog this.trigger( 'closeMenu'); } }); }(jQuery, _)); ').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="resultsArea"]'); if( state ){ results .css({ height: results.height() + 'px' }) .html('') .append( $('
        ') .addClass('ipsLoading') .css({ 'height': '200px' }) ); } else { results.css({ height: 'auto' }); ips.utils.anim.go( 'fadeIn fast', results ); } }, /** * Toggles the loading state on the filter sidebar * * @param {boolean} showLoading Enable loading state? * @returns {void} */ _setFilterLoading: function (state) { if( state ){ this._sidebar .css({ pointerEvents: 'none' }) .animate({ opacity: 0.6 }); } else { this._sidebar .css({ pointerEvents: 'auto', opacity: 1 }); } }, /** * Builds a string to use in the browser titlebar * * @returns {string} */ _getBrowserTitle: function () { var title = ips.getString('searchTitle'); var q = ''; for( var i = 0; i < this._formData.length; i++ ){ if( this._formData[i].name == 'q' ){ q = this._formData[i].value; } } if( q !== '' && this._currentType == ''){ title = ips.getString('searchTitleTerm', { term: q }); } else if( q !== '' && this._currentType !== '' ){ title = ips.getString('searchTitleTermType', { term: q, type: $('#elSearch_appFilter').find('[data-ipsMenuValue="' + this._currentType + '"] > a').text() }); } else if( q === '' && this._currentType !== '' ){ title = ips.getString('searchTitleType', { type: $('#elSearch_appFilter').find('[data-ipsMenuValue="' + this._currentType + '"] > a').text() }); } return title; } }); }(jQuery, _));]]> = 0; }; this._highlight( this.scope.attr('data-term') ); }, /** * Event handler for document content change * * @returns {void} */ contentChange: function () { this._highlight( this.scope.attr('data-term') ); }, /** * Highlight search results * * @param {string} term The word(s) to highlight * @returns {void} */ _highlight: function (term) { var terms = term.split( " " ); var elements = this.scope.find( '.cSearchResultHighlight *:icontains("' + term + '")' ); $.each( terms, function( index, term ) { elements.contents().filter( function() { return this.nodeType === 3 } ).each( function(){ $( this ).replaceWith( _.escape( $( this ).text().replace( new RegExp( "(" + term + ")", "i" ), '' + "$1 " + '' ) ).replace( "<mark class="ipsMatch" + ( index + 1 ) + "">", "" ).replace( "</mark>", "" ) ); } ); }); } }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.com * * ips.system.buildApp.js - Present an alert to the user asking them if they want to build the app first * * Author: Rikki Tissier */ ;( function($, _, undefined){ "use strict"; ips.controller.register('core.admin.system.buildApp', { initialize: function () { this.on( 'click', this.launchAlert ); }, /** * Displays a dialog to the user with 'build and download' and 'download' options * * @param {event} e Event object * @returns {void} */ launchAlert: function (e) { var self = this; e.preventDefault(); ips.ui.alert.show({ type: 'verify', message: ips.getString('build_app'), icon: 'question', buttons: { yes: ips.getString('build_download'), no: ips.getString('download'), cancel: ips.getString('cancel') }, callbacks: { yes: function () { window.location = self.scope.attr('data-buildURL'); }, no: function () { window.location = self.scope.attr('data-downloadURL'); } } }); } }); }(jQuery, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); '); 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, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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.setup(); }, setup: function () { this.scope.css({ zIndex: 10000 }); }, /** * Event handler for submitting the meta tags form * * @returns {void} */ formSubmit: function (e) { e.preventDefault(); var self = this; var form = $( e.currentTarget ); 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.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 ); }, /** * 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, _)); ').attr( 'data-role', 'validationCheck' ) ); this.emailField.after( $('').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.timer = 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.timer = setTimeout( _.bind( this._doCheck, this, 'email', this.emailField ), 700 ); } else { this._clearResult( this.emailField ); } }, /** * 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 ]('?app=core&module=system&controller=ajax&do=' + type + 'Exists', { dataType: 'json', data: { input: encodeURIComponent( value ) } }) .done( function (response) { if( response.result == 'ok' ){ resultElem.show().html( ips.templates.render('register.ok') ); field.removeClass('ipsField_error').addClass('ipsField_success'); } else { resultElem.show().html( ips.templates.render( 'register.fail', { message: response.message } ) ); field.removeClass('ipsField_success').addClass('ipsField_error'); } }) .fail( function () {} ) .always( function () { field.removeClass('ipsField_loading'); }); } }); }(jQuery, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); " + response + '
        ' ); $( document ).trigger('contentChange', [ themeHookWindow ]); }) .fail(function(){ window.location = target.attr('href'); }) } target.addClass('cHookEditor_activeTemplate'); themeHookWindow.removeClass('ipsLoading'); } }); }(jQuery, _)); ]]>
        /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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(); }, /** * "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, _)); ') .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 () { ips.ui.alert.show( { type: 'alert', message: ips.getString('saveThemeError'), 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(); 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', { 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 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() == tabParent ){ 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': 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 () { return this._tabContent.outerHeight(); } }); }(jQuery, _));]]> 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]').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, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); ').addClass('ipsLoading').css({ height: '200px' }); this.scope.find('[data-role="vncTable"]').html( loadingDiv ); } else { this.scope.find('[data-role="vncSidebar"], [data-role="vncPager"]') .css({ pointerEvents: 'auto', opacity: 1 }); } } }); }(jQuery, _));]]> 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 = $('
        ').attr('data-role', 'widgets'); var container = $('
        ') .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, _));]]> ') .attr('id', this.scope.attr('id') + '_' + type ) .attr( 'data-controller', 'core.front.vse.panel' + ips.utils.uppercaseFirst( type ) ) .data( 'styleData', data ) ); } }); }(jQuery, _));]]> = 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( $('
        ').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, _));]]> /** * IPS Social Suite 4 * (c) 2013 Invision Power Services - http://www.invisionpower.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, _)); ').attr('type', 'text/css').attr('id', 'elInjectedStyles') ) .append( $('