').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, refresh: refresh }; }; }(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', 'disallowedCharacters', 'minAjaxLength' ] ); return { respond: respond, destruct: destruct, getObj: getObj }; }); /** * autocomplete instance * * @param {element} elem The element this widget is being created on * @param {object} options The options passed into this instance * @returns {void} */ var autocompleteObj = function (elem, options, e) { var timer, blurTimer, lastValue = '', originalTextField, valueField, textField, dataSource, elemID = $( elem ).identify().attr('id'), wrapper, inputItem, resultsElem, selectedToken, disabled = false, required = false, tooltip = null, tooltipTimer = null, mouseOverResults = false; /** * 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(); } try { options.disallowedCharacters = $.parseJSON( options.disallowedCharacters ); } catch (err) { Debug.error("Couldn't parse disallowed characters option"); } // Add our autocomplete wrapper to the page, and move the element into it _buildWrapper(); // Set up the data source for this control _getDataSource(); // Remove list from original field originalTextField.removeAttr('list'); // Build the list if( dataSource.type != 'none' ){ _buildResultsList(); } if( originalTextField.is(':disabled') ){ disabled = true; } if( originalTextField.is('[required]') ){ required = true; originalTextField .removeProp('required') .removeAttr('aria-required'); } // Turn off autocomplete and spellcheck so the browser menu doesn't get in the way textField .prop( 'autocomplete', 'off' ) .prop( 'spellcheck', false ) .prop( 'disabled', disabled ) .attr( 'aria-autocomplete', 'list' ) .attr( 'aria-haspopup', 'true' ) .attr( 'tabindex', originalTextField.attr('tabindex') || '' ); if( options.maxLength ){ textField.attr( 'maxlength', options.maxLength + 1 ); } $( document ).on( 'click', _documentClick ); wrapper.click(function(e) { e.stopPropagation(); return false; }); // Set up events textField .on( 'focus', _focusField ) .on( 'blur', _blurField ) .on( 'keydown', _keydownField ) .on( 'keyup', _keyupField ); 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 = _getOriginalValue(); 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] ); } } }, /** * Returns the value from the original text field (i.e. the value provided by the backend on pag load) * * @returns {string} */ _getOriginalValue = function () { return originalTextField.val(); //return _.unescape( originalTextField.val() ).replace("'", "'").replace("'", "'"); }, /** * Builds the element that results will appear in * * @returns {void} */ _buildResultsList = function () { if( options.resultsElem && $( options.resultsElem ).length ){ resultsElem = $( options.resultsElem ); return; } var resultsList = ips.templates.render( options.resultsTemplate, { id: elemID }); wrapper.append( resultsList ); resultsElem = $('#' + elemID + '_results'); resultsElem .on('mouseover', '[data-value]', function (e) { results.select( $( e.currentTarget ) ); }) .on('mouseenter', function () { mouseOverResults = true; }) .on('mouseleave', function () { mouseOverResults = false; }) .on('click', '[data-value]', function (e) { _addToken( $( e.currentTarget ).attr('data-value') ); textField.focus(); }) .attr( 'aria-busy', 'false' ); }, /** * Builds the wrapper element that looks like a text input, but allows us to insert * items as tokens * * @returns {void} */ _buildWrapper = function () { var existingClasses = elem[0].className; $( elem ) .after( ips.templates.render( options.fieldTemplate, { id: elemID })) .removeClass( existingClasses ); wrapper = $( '#' + elemID + '_wrapper' ); inputItem = $( '#' + elemID + '_inputItem' ); // If users have to choose from a predefined list, we'll hide the text field // and build a link which will show the results panel if( !options.freeChoice ){ var insertElem = ips.templates.render('core.autocomplete.addToken', { text: options.addTokenText }); textField = elem; } else { var insertElem = $('').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) { if( mouseOverResults ){ return; } clearInterval( timer ); _.delay( _timerBlurField, 300 ); }, /** * Timed event hides the results list * * @returns {void} */ _timerBlurField = function () { // See #47772 /*if( dataSource.type == 'none' ){ return; }*/ if( textField.val() ){ _addTokenFromCurrentInput(); } _closeResults(); }, /** * Timed event, checks whether the value has changed, and fetches the results * * @returns {void} */ _timerFocusField = function () { if( dataSource.type == 'none' ){ return; } // Fetch the current item value var currentValue = _getCurrentValue(); // If the value hasn't changed, we can leave if( currentValue == lastValue ){ return; } lastValue = currentValue; _loadResults( currentValue ); }, /** * Requests results from the data source, and shows/hides the loading widget * while that is happening. * * @returns {void} */ _loadResults = function (value) { _toggleLoading('show'); // Set elem to busy resultsElem.attr( 'aria-busy', 'true' ); // Get the results dataSource.getResults( value ) .done( function (results) { // Show the results after processing them _showResults( _processResults( results, value ) ); }) .fail( function () { }) .always( function () { resultsElem.attr( 'aria-busy', 'false' ); _toggleLoading('hide'); }); }, /** * Toggles the loading thingy in the control to signify data is loading * * @param {string} doWhat Acceptable values: 'show' or 'hide' * @returns {void} */ _toggleLoading = function (doWhat) { if( doWhat == 'show' ){ wrapper.addClass('ipsField_loading'); } else { wrapper.removeClass('ipsField_loading'); } }, /** * Closes the suggestions menu, sets the aria attrib, and tells the data source * to stop loading new results * * @returns {void} */ _closeResults = function (e) { if( e ){ e.preventDefault(); } if( resultsElem && resultsElem.length ){ resultsElem .hide() .attr('aria-expanded', 'false'); } dataSource.stop(); }, /** * Handles a click on the document, closing the results dropdown * * @returns {void} */ _documentClick = function () { _closeResults(); }, /** * Processes the results that are returned by the data source * * @returns {void} */ _processResults = function (results, text) { var existingTokens = tokens.getValues(), newResults = {}; if( options.unique ){ $.each( results, function (key, data) { if( !data.value || _.indexOf( existingTokens, data.value ) === -1 ){ newResults[ key ] = data; } }); return newResults; } return results; }, /** * Gets the current item value from the text field * * @returns {string} */ _showResults = function (results) { var output = ''; $.each( results, function (idx, value) { output += ips.templates.render( options.resultItemTemplate, value ); }); if( resultsElem.attr('id') == ( elemID + '_results' ) ){ _positionResults(); } resultsElem .show() .html( output ) .attr('aria-expanded', 'true'); }, /** * Sizes and positions the results menu to match the wrapper * * @returns {void} */ _positionResults = function () { resultsElem.css( { width: wrapper.outerWidth() + 'px' }); var positionInfo = { trigger: wrapper, targetContainer: wrapper, target: resultsElem, center: false }; var resultsPosition = ips.utils.position.positionElem( positionInfo ); $( resultsElem ).css({ left: '0px', top: resultsPosition.top + 'px', position: ( resultsPosition.fixed ) ? 'fixed' : 'absolute', zIndex: ips.ui.zIndex() }); }, /** * Gets the current item value from the text field * * @returns {string} */ _getCurrentValue = function () { var value = textField.val(); if( options.multiValues ){ if( value.indexOf( options.itemSep.chr ) === -1 ){ // 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( _.escape( value ) ); if( options.minLength && value.length < options.minLength ){ return false; } if( options.maxLength && value.length > options.maxLength ){ return false; } if( options.forceLower ){ value = value.toLowerCase(); } tokens.deselectAll(); inputItem.before( ips.templates.render( options.tokenTemplate, { id: elemID, value: value, title: value })); if( resultsElem ){ _closeResults(); } // Update hidden textbox valueField.val( tokens.getValues().join( options.itemSep.chr ) ); if( dataSource.type != 'none' ){ html = resultsElem.find('[data-value="' + value.replace("\\", "\\\\") + '"]').html(); } else { html = value; } elem.trigger('tokenAdded', { token: value, html: html, tokenList: tokens.getValues(), totalTokens: tokens.total() }); return true; }, /** * Deletes the given token * * @param {element} token The token element to select * @returns {void} */ remove: function (token) { if( tokens.selected == token ){ tokens.selected = null; } var value = $( token ).attr('data-value'); $( token ).remove(); if( options.maxItems && tokens.total() < options.maxItems ){ inputItem.show(); } if( options.unique && options.freeChoice == false && ( dataSource.totalItems() === -1 || dataSource.totalItems() > tokens.total() ) ) { wrapper.find('[data-action="addToken"]').show(); } // Update text field valueField.val( tokens.getValues().join( 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.disallowedCharacters ){ if ( $(e.target).val().indexOf( options.disallowedCharacters[i] ) !== -1 ) { $(e.target).val( $(e.target).val().replace( options.disallowedCharacters[i], '' ) ); _showTooltip( ips.getString( 'ac_prohibit_special', { chars: options.disallowedCharacters.join(' ') } ) ); 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, _));]]> = otherEdge && stayOnScreen <= ( otherEdge + margin ) ){ forceNext = true; return false; } // If the left of this item is on screen and the right isn't, this is our next item to show. if( edge < stayOnScreen && otherEdge > stayOnScreen ){ return true; } return false; }); var nextFirst = $( nextFirst ); if( !nextFirst.length ){ Debug.error("nextFirst didn't exist"); deferred.resolve(); return deferred.promise(); } var nextFirstPos = nextFirst.position().left; var nextFirstMargin = ( rtlMode ) ? parseInt( $( nextFirst ).css('marginLeft') ) : parseInt( $( nextFirst ).css('marginRight') ); // For RTL we need the right-hand edge if( rtlMode ){ nextFirstPos = ui.itemList.outerWidth() - ( nextFirstPos + nextFirst.outerWidth() ) - nextFirstMargin; } // If this would leave space at the end of the list, then we'll simply scroll to the end of the list // ( listWidth - nextFirstPos ) = What's left of the list to show if( ( listWidth - nextFirstPos ) < wrapperWidth ){ nextFirstPos = listWidth - wrapperWidth; } currentLeftPos = nextFirstPos; // Now animate the list to the new position ui.itemList.animate( ( rtlMode ) ? { right: ( nextFirstPos * -1 ) + 'px' } : { left: ( nextFirstPos * -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 stayOnScreen = currentLeftPos + wrapperWidth; var forcePrev = false; var deferred = $.Deferred(); // We ideal want to scroll back the width of the widget, but not so much that // our current left item goes off screen var idealLeft = ( currentLeftPos * -1 ) + wrapperWidth; // If we need to go back to the bottom, we can shortcut if( idealLeft >= 0 || backToBeginning ){ currentLeftPos = 0; ui.itemList.animate( ( rtlMode ) ? { right: '0px' } : { 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) { var edge = $( item ).position().left; var width = $( item ).outerWidth(); var otherEdge = edge + width; var margin = ( rtlMode ) ? parseInt( $( item ).css('marginLeft') ) : parseInt( $( item ).css('marginRight') ); if( rtlMode ){ edge = ui.itemList.outerWidth() - ( otherEdge ); // right edge } // If we set forcePrev = true on the last loop, return the item now. if( forcePrev ){ return true; } if( edge > idealLeft ){ return false; } // If left + width of this item perfectly equals the width of the wrapper, then the next item // to be shown is actually the first one that's offscreen. This also applies if the fullSizeItems // option is applied. Set forceNext to true so that on the next iteration we can return the item. if( stayOnScreen >= otherEdge && stayOnScreen <= ( otherEdge + margin ) ){ forcePrev = true; return false; } if( otherEdge > idealLeft && edge <= idealLeft ){ return true; } return false; }); Debug.log('prev'); // The method above has given us the first that is *partially* on screen // We actually want the next one though so we know it's fully on screen, // except on mobile prevFirst = $( prevFirst ); Debug.log( prevFirst ); if( !ips.utils.responsive.currentIs('phone') ){ prevFirst = $( prevFirst ).next( options.item ); } currentFirstItem = prevFirst; // Set the left position so that prevFirst is still on-screen as the last item // E.g. // // Before After // |5 ][ 6 ][ 7 ][ 8 ][ 9 ]| |[ 1 ][ 2 ][ 3 ][ 4 ][ 5 ]| // if( prevFirst.position() != null ) { currentLeftPos = prevFirst.position().left + prevFirst.outerWidth() - wrapperWidth; if( rtlMode ){ var prevFirstMargin = ( rtlMode ) ? parseInt( $( prevFirst ).css('marginLeft') ) : parseInt( $( prevFirst ).css('marginRight') ); currentLeftPos = ui.itemList.outerWidth() - ( prevFirst.position().left + prevFirst.outerWidth() ) - prevFirstMargin - wrapperWidth; // right edge } } else { currentLeftPos = prevFirst.outerWidth() + wrapperWidth; } ui.itemList.animate( ( rtlMode ) ? { right: ( currentLeftPos * -1 ) + 'px' } : { 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 if ( options.type != 'Table' ) { 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) { if ( options.type != 'Table' ) { 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, _)); ').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(); hiddenValueField = $('input[name=' + name + '_values]'); itemListWrapper = $('[data-contentitem-results=' + name + ']'); originalTextField.hide(); // Move any classnames on the original element onto our new wrapper to maintain styling, // then move the original element into our reserved list element wrapper .addClass( existingClasses ) .append( elem ) .append( valueField ) .find('#' + elemID + '_inputItem') .append( insertElem ); if ( options.maxItems && contentItems.total() >= options.maxItems ) { wrapper.hide(); } // Set events for clicking on item itemListWrapper .on('click', '[data-action="delete"]', function (e) { _deleteContentItem( $( e.currentTarget ).parent('[data-id]') ); }); }, /** * Gets the apprioriate data source for this control * * @returns {void} */ _getDataSource = function () { if( ips.utils.validate.isUrl( options.dataSource ) ){ dataSource = remoteData( options.dataSource, options ); } else { dataSource = noData(); } }, /** * When the wrapper is clicked, we see if a item was clicked. If it was, select it. If not, focus the textbox. * * @returns {void} */ _clickWrapper = function (e) { if( !$( e.target ).is( textField ) && ( !resultsElem || !$.contains( resultsElem.get(0), e.target ) ) ){ textField.focus(); } }, /** * Event handler for focusing on the text field * * @returns {void} */ _focusField = function (e) { if( dataSource.type == 'none' ){ return; } timer = setInterval( _timerFocusField, 400 ); }, /** * Event handler for blurring on the text field * * @returns {void} */ _blurField = function (e) { clearInterval( timer ); _.delay( _timerBlurField, 300 ); }, /** * Timed event hides the results list * * @returns {void} */ _timerBlurField = function () { _closeResults(); }, /** * Timed event, checks whether the value has changed, and fetches the results * * @returns {void} */ _timerFocusField = function () { if( dataSource.type == 'none' ){ return; } // Fetch the current item value var currentValue = _getCurrentValue(); // If the value hasn't changed, we can leave if( currentValue == lastValue ){ return; } lastValue = currentValue; _loadResults( currentValue ); }, /** * Requests results from the data source, and shows/hides the loading widget * while that is happening. * * @returns {void} */ _loadResults = function (value) { _toggleLoading('show'); // Set elem to busy resultsElem.attr( 'aria-busy', 'true' ); // Get the results dataSource.getResults( value ) .done( function (results) { // Show the results after processing them _showResults( _processResults( results, value ) ); }) .fail( function () { }) .always( function () { resultsElem.attr( 'aria-busy', 'false' ); _toggleLoading('hide'); }); }, /** * Toggles the loading thingy in the control to signify data is loading * * @param {string} doWhat Acceptable values: 'show' or 'hide' * @returns {void} */ _toggleLoading = function (doWhat) { if( doWhat == 'show' ){ wrapper.addClass('ipsField_loading'); } else { wrapper.removeClass('ipsField_loading'); } }, /** * Closes the suggestions menu, sets the aria attrib, and tells the data source * to stop loading new results * * @returns {void} */ _closeResults = function (e) { if( e ){ e.preventDefault(); } if( resultsElem && resultsElem.length ){ resultsElem .hide() .attr('aria-expanded', 'false'); } dataSource.stop(); }, /** * Handles a click on the document, closing the results dropdown * * @returns {void} */ _documentClick = function () { _closeResults(); }, /** * Processes the results that are returned by the data source * * @returns {void} */ _processResults = function (results, text) { var existingItems = contentItems.getValues(), newResults = {}; $.each( results, function (key, data) { if( !data.id || _.indexOf( existingItems, data.id ) === -1 ){ newResults[ key ] = data; } }); return newResults; }, /** * Gets the current item value from the text field * * @returns {string} */ _showResults = function (results) { var output = ''; $.each( results, function (idx, value) { output += ips.templates.render( options.resultItemTemplate, value ); }); if( resultsElem.attr('id') == ( elemID + '_results' ) ){ _positionResults(); } resultsElem .show() .html( output ) .attr('aria-expanded', 'true'); }, /** * Sizes and positions the results menu to match the wrapper * * @returns {void} */ _positionResults = function () { resultsElem.css( { width: wrapper.outerWidth() + 'px' }); var positionInfo = { trigger: wrapper, targetContainer: wrapper, target: resultsElem, center: false }; var resultsPosition = ips.utils.position.positionElem( positionInfo ); $( resultsElem ).css({ left: '0px', top: resultsPosition.top + 'px', position: ( resultsPosition.fixed ) ? 'fixed' : 'absolute', zIndex: ips.ui.zIndex() }); }, /** * Gets the current item value from the text field * * @returns {string} */ _getCurrentValue = function () { var value = textField.val(); return value; }, /** * Event handler for keydown event in wrapper. * We check for esape here, because if options.freeChoice is disabled, there's no textbox to * watch for events. By watching for escape on the wrapper, we can still close the menu. * * @returns {void} */ _keydownWrapper = function (e) { if( e.keyCode == ips.ui.key.ESCAPE ){ keyEvents.escape(e); } }, /** * Event handler for keydown event in text field * * @returns {void} */ _keydownField = function (e) { _expandField(); var ignoreKey = false; // Ignore irrelevant keycodes if( !_( [ ips.ui.key.UP, ips.ui.key.DOWN, ips.ui.key.ESCAPE, ips.ui.key.ENTER ] ).contains( e.keyCode ) ){ ignoreKey = true; } var value = $.trim( textField.val() ); if( ignoreKey ){ return; } switch(e.keyCode){ // Suggestions keys case ips.ui.key.UP: keyEvents.up(e); break; case ips.ui.key.DOWN: keyEvents.down(e); break; case ips.ui.key.ESCAPE: keyEvents.escape(e); break; case ips.ui.key.ENTER: keyEvents.enter(e); break; } }, /** * A wrapper method for contentItems.add which also clears the text field * and hides it if options.maxItems is reached * * @returns {void} */ _addContentItem = function (elem) { contentItems.add( elem ); textField.val(''); lastValue = ''; _resetField(); if( options.maxItems && contentItems.total() >= options.maxItems ){ wrapper.hide(); } }, /** * A wrapper method for contentItems.remove which shows the text field if we're under * our options.maxItems limit * * @returns {void} */ _deleteContentItem = function (item) { if( disabled ){ return; } contentItems.remove( item ); }, /** * Object containing event handlers bound to individual keys */ keyEvents = { /** * Handler for 'up' key press. Selects previous item in the results list. * * @returns {void} */ up: function (e) { if( !resultsElem || !resultsElem.is(':visible') ){ return; } e.preventDefault(); var selected = results.getCurrent(); if( !selected ){ results.selectLast(); } else { var prev = results.getPrevious( selected ); if( prev ){ results.select( prev ); } else { results.selectLast(); } } }, /** * Handler for 'down' key press. Selects next item in the results list. * * @returns {void} */ down: function (e) { if( !resultsElem || !resultsElem.is(':visible') ){ return; } e.preventDefault(); var selected = results.getCurrent(); if( !selected ){ results.selectFirst(); } else { var next = results.getNext( selected ); if( next ){ results.select( next ); } else { results.selectFirst(); } } }, /** * Enter/tab handler. If text has been entered, we add it as a item, otherwise pass through * to the browser to handle. * * @param {event} e Event object * @returns {void,boolean} */ enter: function (e) { e.preventDefault(); var currentResult = results.getCurrent(); var value = ''; if( currentResult ){ value = currentResult.attr('data-id'); } if( !value ){ return false; } _addContentItem( currentResult ); }, /** * Handler for 'escape' key press. Closes the suggestions menu, if it's open. * * @returns {void} */ escape: function (e) { if( resultsElem && resultsElem.is(':visible') ){ _closeResults(); } } }, /** * Object containing methods for dealing with the results list. */ results = { /** * Deselects any selected results * * @returns {void} */ deselectAll: function () { resultsElem .find('[data-selected]') .removeAttr('data-selected'); }, /** * Returns the currently selected result * * @returns {element,boolean} Returns the jQuery object containing the selected result, or false */ getCurrent: function () { if( dataSource.type == 'none' ){ return; } var cur = resultsElem.find('[data-selected]'); if( cur.length && resultsElem.is(':visible') ){ return cur; } return false; }, /** * Gets the result preceding the provided result * * @returns {element,boolean} Returns the jQuery object containing the selected result, or false */ getPrevious: function (result) { var prev = $( result ).prev('[data-id]'); if( prev.length ){ return prev; } return false; }, /** * Gets the result following the provided result * * @returns {element,boolean} Returns the jQuery object containing the selected result, or false */ getNext: function (result) { var next = $( result ).next('[data-id]'); if( next.length ){ return next; } return false; }, /** * Selects the first result * * @returns {void} */ selectFirst: function () { results.select( resultsElem.find('[data-id]').first() ); }, /** * Selects the last result * * @returns {void} */ selectLast: function () { results.select( resultsElem.find('[data-id]').last() ); }, /** * Selects the provided item * * @returns {void} */ select: function (result) { results.deselectAll(); result.attr('data-selected', true); } }, /* ! Content Items */ /** * Object containing item methods */ contentItems = { selected: null, /** * Adds an item to the control * * @param {object} elem Element from result list to add * @returns {void} */ add: function (elem) { var html = ''; var obj = $(elem).find('[data-role=contentItemRow]'); html = obj.html(); itemListWrapper.append( ips.templates.render( options.itemTemplate, { id: obj.attr('data-itemid'), html: html })); if( resultsElem ){ _closeResults(); } // Update hidden field hiddenValueField.val( contentItems.getValues().join( ',' ) ); if ( options.maxItems && contentItems.total() >= options.maxItems ) { wrapper.hide(); } elem.trigger('contentItemAdded', { html: html, itemList: contentItems.getValues(), totalItems: contentItems.total() }); return true; }, /** * Deletes the given item * * @param {element} item The item element to select * @returns {void} */ remove: function (item) { if( contentItems.selected == item ){ contentItems.selected = null; } var value = $( item ).attr('data-value'); $( item ).remove(); if( options.maxItems && contentItems.total() < options.maxItems ){ wrapper.show(); } // Update text field hiddenValueField.val( contentItems.getValues().join( ',' ) ); elem.trigger('contentItemDeleted', { item: item, itemList: contentItems.getValues(), totalItems: contentItems.total() }); }, /** * Returns total number of items entered * * @returns {number} */ total: function () { return itemListWrapper.find('[data-id]').length; }, /** * Returns all of the values * * @param {element} item The item element to select * @returns {void} */ getValues: function () { var values = []; var allContentItems = itemListWrapper.find('[data-id]'); if( allContentItems.length ){ values = _.map( allContentItems, function( item ){ return $( item ).attr('data-id'); }); } return values; } }, /** * Determines whether the value would be a duplicate * * @param {string} value Value to check * @returns {void} */ _duplicateValue = function (value) { var values = contentItems.getValues(); if( values.indexOf( value ) !== -1 ){ return true; } return false; }, /** * Expands the text field to fit the given text * * @returns {void} */ _expandField = function () { var text = textField.val(); var widthOfElem = wrapper.width(); widthOfElem -= ( parseInt( wrapper.css('padding-left') ) + parseInt( wrapper.css('padding-right') ) ); // Create temporary span var span = $('').text( text ).css({ 'font-size': textField.css('font-size'), 'letter-spacing': textField.css('letter-spacing'), 'position': 'absolute', 'top': '-100px', 'left': '-300px', 'opacity': 0.1 }); ips.getContainer().append( span ); // Get the width var width = span.width() + 20; // Remove it span.remove(); textField.css({ width: ( ( width >= widthOfElem ) ? widthOfElem : width ) + 'px' }); }, /** * Resets the width of the text input * * @returns {void} */ _resetField = function () { textField.css({ width: '15px' }); }; init(); return { init: init, destruct: destruct, addContentItem: contentItems.add, getContentItem: contentItems.getValues, removeContentItem: contentItems.remove }; }; /** * Handler for remote data retrieval */ var remoteData = function (source, options) { var ajaxObj, loadedCache = false, cache = {}; /** * Initiates either a remote search or a remote fetch * * @returns {promise} */ var getResults = function (text) { return _remoteSearch( text ); }, /** * Returns the number of items in the result set * * @returns {number} */ totalItems = function () { return -1; }, /** * Does a remote search (i.e. passing search string to backend, and returning results) * * @param {string} String to search for * @returns {promise} */ _remoteSearch = function (text) { var deferred = $.Deferred(); if( ajaxObj ){ ajaxObj.abort(); } if( options.minAjaxLength > text.length ){ deferred.reject(); return deferred.promise(); } if( cache[ text ] ){ deferred.resolve( cache[ text ] ); } else { ajaxObj = ips.getAjax()( source + '&' + options.queryParam + '=' + encodeURI( text ), { dataType: 'json' } ) .done( function (response) { deferred.resolve( response ); cache[ text ] = response; }) .fail( function (jqXHR, status, errorThrown) { if( status != 'abort' ){ Debug.log('aborting'); } deferred.reject(); }); } return deferred.promise(); }, /** * Fetches remote data, and then performs a local search on the data to find results * * @param {string} String to search for * @returns {promise} */ _remoteFetch = function (text) { var deferred = $.Deferred(); if( !loadedCache ){ if( ajaxObj ){ return; } if( options.minAjaxLength > text.length ){ return; } ajaxObj = ips.getAjax()( source, { dataType: 'json' } ) .done( function (response) { loadedCache = true; cache = response; _remoteFetch( text ); }) .fail( function (jqXHR, status, errorThrown) { if( status != 'abort' ){ Debug.log('aborting'); } deferred.reject(); }); } // Search through the cache for results cache.each( function (idx, item) { if( item.value.toLowerCase().startsWith( text ) ){ output.push( item ); } }); return deferred.promise(); }, /** * Aborts the ajax request * * @param {string} String to search for * @returns {void} */ stop = function () { if( ajaxObj ){ ajaxObj.abort(); } }; return { type: 'remote', getResults: getResults, totalItems: totalItems, stop: stop }; }; var noData = function () { return { type: 'none', getResults: $.noop, totalItems: -1, stop: $.noop }; }; }(jQuery, _)); ]]> 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 ); contentLoaded = false; // This will cause the dialog to be reloaded again if we open it again, which we want so our previous values aren't still inputted hide(); if( options.flashMessage ){ ips.ui.flashMsg.show( options.flashMessage ); } }; // 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() } ); // Hide close elem drawerElem.find('.ipsDrawer_close').hide(); ips.utils.anim.go( 'fadeIn fast', modal ); // Show drawer drawerElem .css( { zIndex: ips.ui.zIndex() } ) .show(); if( $('html').attr('dir') === 'rtl' ) { ips.utils.anim.go( 'slideRight fast', drawerElem ); } else { ips.utils.anim.go( 'slideLeft fast', drawerElem ); } drawerElem.find('.ipsDrawer_close').delay(500).fadeIn(); // Make body non-scrolly $('body').css( { overflow: 'hidden' }); }, hide = function () { ips.utils.anim.go( 'fadeOut fast', modal ); drawerElem.hide(); $('body').css( { overflow: 'auto' }); }; init(); return { init: init, show: show, hide: hide }; }; }(jQuery, _));]]> 700 ) { size = 'desktop'; } else if ( width > 400 ) { size = 'tablet'; } config.toolbar = toolbars[ size ]; } else { config.removePlugins = 'sourcearea'; } // Extra plugins if( options.extraPlugins !== true ){ config.extraPlugins += ',' + options.extraPlugins; } // Actually initiate instance = CKEDITOR.replace( name, config ); instance.once('instanceReady', function(){ elem.trigger( 'editorWidgetInitialized', { id: name } ); if( _.isFunction( callback ) ){ callback(); } }); // Resize the editor as the element resizes if( !options.allbuttons ){ $( window ).on( 'resize', resize ); } // When we delete a file from the uploader, we need to remove it from the editor $( document ).on( 'fileDeleted', _deleteFile ); // And listen for emoticon inserts $( document ).on( 'insertEmoticon', _insertEmoticon ); }; /** * Destructs this object * * @returns CKEDITOR.editor */ var destruct = function () { try { instance.destroy(); $( window ).off( 'resize', resize ); $( document ).off( 'fileDeleted', _deleteFile ); $( document ).off( 'initializeEditor', _initializeEditor ); $( document ).off( 'insertEmoticon', _insertEmoticon ); } catch (err) { } }; /** * Handles window resizes * * @returns CKEDITOR.editor */ var resize = function (focus) { var width = elem.width(); var newSize = 'phone'; if ( width > 700 ) { newSize = 'desktop'; } else if ( width > 400 ) { newSize = 'tablet'; } if ( newSize != size ) { size = newSize; instance.destroy(); init( function () { if( focus ){ instance.focus(); } }); } }; /** * Focus * * @returns CKEDITOR.editor */ var focus = function () { instance.focus(); }; /** * Unminimize * * @param {callback} callback Function to run after unminimized * @returns CKEDITOR.editor */ var unminimize = function ( callback ) { if( !_.isFunction(callback) ){ callback = $.noop; } if ( minimized ) { // Hide the dummy area and show the actual editor $( elem ) .find('.ipsComposeArea_dummy') .hide() .end() .find('[data-role="mainEditorArea"]') .show() .end() .closest('.ipsComposeArea') .find('[data-ipsEditor-toolList]') .show(); // Focus it. If it isn't ready yet, wait until it is if ( instance.status == 'ready' ) { minimized = false; callback(); if( hiddenAtStart ){ resize(true); hiddenAtStart = false; } } else { instance.once( 'instanceReady', function(){ minimized = false; callback(); if( hiddenAtStart ){ resize(true); hiddenAtStart = false; } }); } // Load the upload area var minimizedUploader = $(elem).find('[data-ipsEditor-toolListMinimized]'); if ( minimizedUploader.length ) { minimizedUploader.show(); ips.getAjax()( elem.parentsUntil( '', 'form' ).attr('action'), { 'data': { 'getUploader': minimizedUploader.attr('data-name') } }) .done( function (response) { minimizedUploader.replaceWith( response ); $( document ).trigger( 'contentChange', [ elem ] ); }); } } else { callback(); } }; /** * Insert quotes into editor * * @param {array} quotes Array of data objects (which should contain all of the properties necessary for a quote) * @returns void */ var insertQuotes = function ( quotes ) { unminimize( function () { /* If we're up against another widget, insert a blank paragraph between them */ var ranges = instance.getSelection().getRanges(); for ( var i = 0; i < ranges.length; i ++ ) { var previousNode = ranges[i].getCommonAncestor( true, true ).getPrevious(); if ( previousNode && previousNode.hasClass('cke_widget_wrapper') ) { var blankParagraph = new CKEDITOR.dom.element('p'); ( new CKEDITOR.dom.element( 'br' ) ).appendTo( blankParagraph ); instance.insertElement( blankParagraph ); } } /* Now insert the posts */ for ( var i = 0; i < quotes.length; i++ ) { var data = quotes[i]; /* Build quote */ var quote = $( ips.templates.render( 'core.editor.quote', { citation: ips.utils.getCitation( data ), contents: data.quoteHtml.trim() } ) ); var attrs = [ 'timestamp', 'userid', 'username', 'contentapp', 'contenttype', 'contentclass', 'contentid', 'contentcommentid' ]; var j = 0; for ( j in attrs ) { if ( data[ attrs[j] ] ) { quote.attr( 'data-ipsQuote-' + attrs[j], data[ attrs[j] ] ); } } /* Insert it */ var element = CKEDITOR.dom.element.createFromHtml( $('
').append( quote ).html() ); instance.insertElement( element ); instance.widgets.initOn( element, 'ipsquote' ); /* Insert a blank paragraph between multiple quotes */ if ( i + 1 < quotes.length ) { var blankParagraph = new CKEDITOR.dom.element('p'); ( new CKEDITOR.dom.element( 'br' ) ).appendTo( blankParagraph ); instance.insertElement( blankParagraph ); } } }); }; /** * Update a selected image * * @param {number} width Width in pixels * @param {number} height Height in pixels * @param {string} align 'left', 'right' or '' * @param {string} url URL to link to * @returns {void} */ var updateImage = function ( width, height, align, url ) { var selection = instance.getSelection(); var selectedElement = $( selection.getSelectedElement().$ ); if ( url ) { if ( !url.match( /^[a-z]+\:\/\//i ) ) { url = 'http://' + url; } if ( selectedElement.parent().prop('tagName') === 'A' ) { selectedElement.parent().attr( 'href', url ).removeAttr('data-cke-saved-href'); } else { selectedElement.wrap( $('').attr( 'href', url ) ); } } else { if ( selectedElement.parent().prop('tagName') === 'A' ) { selectedElement.parent().replaceWith( selectedElement ); } } selectedElement.css({ "width": width, "height": height }); if ( align ) { if ( selectedElement.parent().prop('tagName') === 'A' ) { selectedElement.parent().css( 'float', align ).addClass('ipsAttachLink_' + align); } else { selectedElement.css( 'float', align ).addClass('ipsAttachLink_' + align); } } else { selectedElement.css( 'float', '' ).removeClass('ipsAttachLink_left').removeClass('ipsAttachLink_right'); if ( selectedElement.parent().prop('tagName') === 'A' ) { selectedElement.parent().css( 'float', '' ).removeClass('ipsAttachLink_left').removeClass('ipsAttachLink_right'); } } }; /** * Insert arbitrary HTML into editor * * @param {string} html HTML to insert * @returns void */ var insertHtml = function ( html ) { instance.insertHtml( html ); }; /** * Reset the editor * * @param {string} html HTML to insert * @returns void */ var reset = function () { instance.setData('

'); elem.find('[data-ipsUploader]').trigger('resetUploader'); }; /** * Save and clear autosave * * @returns void */ var saveAndClearAutosave = function () { instance.updateElement(); ips.utils.db.remove( 'editorSave', options.autoSaveKey ); }; /** * Determines whether the provided editor ID matches this widget * * @param {object} Event data; requires editorID key which is the editor name to check * @returns boolean */ var _belongsToThisEditor = function (data) { if( _.isUndefined( data.editorID ) || data.editorID !== name ){ return false; } return true; }; /** * Allows other JS to initialize the editor * * @returns void */ var _initializeEditor = function (e, data) { if( !_belongsToThisEditor( data ) ){ return; } unminimize( function () { _scrollToEditor(); focus(); }); }; /** * Remove a file from the editor * * @param {event} e Event object * @param {object} data Data object from the event * @returns {boolean} */ var _deleteFile = function(e, data){ var links = instance.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 = instance.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(); } } }; /** * Scrolls the page to the editor * * @returns {boolean} */ var _scrollToEditor = function () { var elemPosition = ips.utils.position.getElemPosition( elem ); // Is it on the page? var windowScroll = $( window ).scrollTop(); var viewHeight = $( window ).height(); // Only scroll if it isn't already on the screen if( elemPosition.absPos.top < windowScroll || elemPosition.absPos.top > ( windowScroll + viewHeight ) ){ $('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' } ); } }; /** * Responds to an insertEmoticon event * * @param {event} e Event object * @param {object} data Data object from the event * @returns {boolean} */ var _insertEmoticon = function (e, data) { // Check we're working with this editor try { if( _belongsToThisEditor( data ) ){ var imgTag = '' + data.text + ' /* 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 ] ) ) { // 10/23/15 we were still losing the contents of codemirror when switching tabs. To fix this, we need to save // the contents to the textarea *before* removing the CM instance. // https://community.invisionpower.com/4bugtrack/active-reports/toggling-between-tabs-when-adding-a-page-loses-content-r8510/ _cmInstances[ elemId ].save(); //----- $( _cmInstances[ elemId ].getWrapperElement() ).remove(); delete _cmInstances[ elemId ]; } _cmInstances[ elemId ] = CodeMirror.fromTextArea( document.getElementById(elemId), { mode: $(elem).attr('data-mode'), lineWrapping: true, lineNumbers: false } ); if ( $(elem).attr('data-height') ){ _cmInstances[ elemId ].setSize( null, $(elem).attr('data-height') ); $('div[data-codemirrorid=' + elemId + '] ul[data-role=tagsList]').css('max-height', $(elem).attr('data-height') ); } $( '#' + elemId ).data('CodeMirrorInstance', _cmInstances[ elemId ] ); // Support custom tags $('[data-codemirrorcustomtag]').on( 'click', function( e ){ _cmInstances[ elemId ].replaceRange( $( e.currentTarget ).attr('data-codemirrorcustomtag'), _cmInstances[ elemId ].getCursor( "end" ) ); }); }); }, /** * Makes range inputs a little nicer to use * * @param {element} elem The range element * @returns {void} */ range: function (elem) { if( _.isUndefined( _support['range'] ) ){ var i = document.createElement("input"); i.setAttribute("type", "range"); _support['range'] = !( i.type === 'text' ); } if( !_support['range'] ){ elem.siblings('[data-role="rangeBoundary"]').hide(); } else { var valueElem = $( '#' + elem.attr('name') + '_rangeValue' ); valueElem.text( elem.val() ); elem.on( 'change', function () { valueElem.text( elem.val() ); }); } }, /** * Enables functionality for 'unlimited' toggles * * @param {element} elem The checkbox element * @returns {void} */ unlimited: function (elem) { elem.on( 'change', function () { _unlimitedCheck( elem ); }); _unlimitedCheck( elem ); }, /** * Disables select boxes when the selected option element has a data-disable attribute * * @param {element} elem The select box * @returns {void} */ selectDisable: function (elem) { elem.on( 'change', function () { _selectDisable( elem ); }); _selectDisable( elem ); }, /** * Handles date fields by adding a jquery plugin if the browser doesn't natively support type='date' * * @param {element} elem The date form control * @returns {void} */ date: function (elem) { if( _.isUndefined( _support['date'] ) ){ var i = document.createElement("input"); i.setAttribute("type", "date"); _support['date'] = !( i.type === 'text' ); } if( !_support['date'] ){ if( $(elem).attr('data-preferredFormat') ) { $(elem).val( $(elem).attr('data-preferredFormat') ); } ips.loader.get( ['core/interface/jquery/jquery-ui.js'] ).then( function () { var _buildDatepicker = function () { $.datepicker.regional['xx'] = { closeText: ips.getString('date_picker_done'), // Display text for close link prevText: ips.getString('date_picker_prev'), // Display text for previous month link nextText: ips.getString('date_picker_next'), // Display text for next month link currentText: ips.getString('date_picker_next'), // Display text for current month link monthNames: [ips.getString('month_0'),ips.getString('month_1'),ips.getString('month_2'),ips.getString('month_3'),ips.getString('month_4'),ips.getString('month_5'),ips.getString('month_6'),ips.getString('month_7'),ips.getString('month_8'),ips.getString('month_9'),ips.getString('month_10'),ips.getString('month_11')], // Names of months for drop-down and formatting monthNamesShort: [ips.getString('month_0'),ips.getString('month_1'),ips.getString('month_2'),ips.getString('month_3'),ips.getString('month_4'),ips.getString('month_5'),ips.getString('month_6'),ips.getString('month_7'),ips.getString('month_8'),ips.getString('month_9'),ips.getString('month_10'),ips.getString('month_11')], // For formatting dayNames: [ips.getString('day_0'),ips.getString('day_1'),ips.getString('day_2'),ips.getString('day_3'),ips.getString('day_4'),ips.getString('day_5'),ips.getString('day_6')], // For formatting dayNamesShort: [ips.getString('day_0_short'),ips.getString('day_1_short'),ips.getString('day_2_short'),ips.getString('day_3_short'),ips.getString('day_4_short'),ips.getString('day_5_short'),ips.getString('day_6_short')], // For formatting dayNamesMin: [ips.getString('day_0_short'),ips.getString('day_1_short'),ips.getString('day_2_short'),ips.getString('day_3_short'),ips.getString('day_4_short'),ips.getString('day_5_short'),ips.getString('day_6_short')], // Column headings for days starting at Sunday weekHeader: ips.getString('date_picker_week'), // Column header for week of the year dateFormat: ips.getSetting( 'date_format' ), // See format options on parseDate firstDay: ips.getSetting( 'date_first_day' ), // The first day of the week, Sun = 0, Mon = 1, ... isRTL: $('html').attr('dir') == 'rtl', // True if right-to-left language, false if left-to-right showMonthAfterYear: false, // True if the year select precedes month, false for month then year yearSuffix: "", // Additional text to append to the year in the month headers shortYearCutoff: 10 }; $.datepicker.setDefaults($.datepicker.regional['xx']); elem.datepicker( { changeMonth: true, changeYear: true, yearRange: "-120:+10", dateFormat: ips.getSetting( 'date_format' ), firstDay: ips.getSetting( 'date_first_day' ), }); // 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.closest('.ipsSelectTree'); eventType = 'nodeSelectedChanged'; } else { toCall = _toggleGeneric; triggerElem = elem; } var reverse = !toggleOn; // Set the event triggerElem.on( eventType, function () { toCall.call( this, triggerElem, selectorList, elem, form, reverse ); }); // And call immediately to initialize, if it's currently visible if( triggerElem.is(':visible') || ( triggerElem.attr('data-toggle-visibleCheck') && $( triggerElem.attr('data-toggle-visibleCheck') ).is(':visible') ) ){ toCall.call( this, triggerElem, selectorList, elem, form, reverse ); } }, /** * Handles the 'unlimited' checkbox for dimension controls * * @param {element} elem The checkbox element * @returns {void} */ _dimensionsUnlimitedCheck = function (elem) { var container = elem.closest('.ipsWidthHeight_container'); if( elem.is(':checked') ){ container .find('[data-control="dimensions"]') .hide() .end() .find('input.ipsWidthHeight_width, input.ipsWidthHeight_height') .val('') .prop( 'disabled', true ); } else { container .find('[data-control="dimensions"]') .show() .end() .find('input.ipsWidthHeight_width, input.ipsWidthHeight_height') .change() .prop( 'disabled', false ); } }, /** * Toggle behavior for radio buttons * Hides all toggle panes assosciated with radio buttons sharing the same name, then shows * panes necessary if this radio button is checked * * @param {array} radioList All radio buttons that share the same name * @param {string} toggleList Selector list of elements to toggle * @param {element} thisElem The radio button that was clicked * @returns {void} */ _toggleRadio = function (radioList, toggleList, thisElem, form) { // Hide all toggles radioList.each( function () { var thisToggles = ips.utils.getIDsFromList( $( this ).attr('data-toggles') ); if( thisToggles ){ _hideFormRows( thisToggles, form ); } }); // Find the checked one radioList.each( function () { if( $( this ).is(':checked') ){ var thisToggles = ips.utils.getIDsFromList( $( this ).attr('data-toggles') ); if( thisToggles ){ _showFormRows( thisToggles, form ); } } }); }, /** * Toggle behavior for select boxes * * @param {element} elem Checkbox that was changed * @param {string} toggleList Selector list of elements to toggle * @returns {void} */ _toggleSelect = function (selectElem, toggleList, thisElem, form) { selectElem.find('option').each( function (idx, elem) { if( $( this ).attr('data-toggles') ){ _hideFormRows( ips.utils.getIDsFromList( $( this ).attr('data-toggles') ), form ); } }); // Get selected items selectElem.find('option:selected').each( function (i, elem) { if( $( elem ).attr('data-toggles') ){ _showFormRows( ips.utils.getIDsFromList( $( this ).attr('data-toggles') ), form ); } }); }, /** * Toggle behavior for checkboxes * * @param {element} elem Checkbox that was changed * @param {string} toggleList Selector list of elements to toggle * @returns {void} */ _toggleCheckbox = function (elem, toggleList, thisElem, form, reverse) { // Get the value var show = elem.is(':checked'); if( reverse ){ show = !show; } if( show ){ _showFormRows( toggleList, form ); } else { _hideFormRows( toggleList, form ); } }, /** * Toggle behavior for node selector * * @param {element} elem Input field that was changed * @param {string} toggleList Selector list of elements to toggle * @returns {void} */ _toggleNode = function (nodeElem, toggleList, thisElem, form) { nodeElem.find('[data-action="nodeSelect"][data-toggles]').each( function (idx, elem) { _hideFormRows( ips.utils.getIDsFromList( $( this ).attr('data-toggles') ), form ); }); // Now get the selected ones nodeElem.find('[data-action="nodeSelect"][data-toggles].ipsSelectTree_selected').each( function (idx, elem) { _showFormRows( ips.utils.getIDsFromList( $( this ).attr('data-toggles') ), form ); }); }, /** * Toggle behavior for other input types * * @param {element} elem Input field that was changed * @param {string} toggleList Selector list of elements to toggle * @returns {void} */ _toggleGeneric = function (elem, toggleList, thisElem, form) { // Get the value var show = elem.val() == 0 ? false : true; if( !_.isUndefined( elem.attr('data-togglereverse') ) ){ show = !show; } if( show ){ _showFormRows( toggleList, form ); } else { _hideFormRows( toggleList, form ); } }, /** * Hides the form rows contained in the provided selector. Works recursively to hide any toggled * panes of elements that become hidden * * @param {string} hide Selector containing elements to hide * @returns {void} */ _hideFormRows = function (hide, form) { if( _.isArray( hide ) ){ hide = hide.join(','); } $( form || document ).find( hide ) .hide() .addClass('ipsHide') .find('[data-toggles],[data-togglesOn],[data-togglesOff]') .each( function (i, elem) { _hideFormRows( ips.utils.getIDsFromList( $( elem ).attr('data-toggles') ), form ); _hideFormRows( ips.utils.getIDsFromList( $( elem ).attr('data-togglesOn') ), form ); _hideFormRows( ips.utils.getIDsFromList( $( elem ).attr('data-togglesOff') ), form ); }); }, /** * Shows the form rows contained in the provided selector, and sets up any toggles contained on * elements that become visible * * @param {string} show Selector containing elements to show * @returns {void} */ _showFormRows = function (show, form) { if( _.isArray( show ) ){ show = show.join(','); } $( form || document ) .find( show ) .not('[data-ipsToggle]') .show() .end() .removeClass('ipsHide') .find('[data-toggles],[data-togglesOn]') .each( function (i, elem) { _controlMethods.toggle( $( elem ), form ); }); }, /** * Disables elements when an option within the * * 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 () { _refreshPageAction(); _updateBar(); }, /** * Resets the pageAction bar * * @returns {void} */ reset = function () { _checkedItems = {}; _updateBar(); }, /** * Sets up events that this instance will watch for * * @returns {void} */ _setUpEvents = function () { elem.on( 'change', 'input[type="checkbox"][data-actions]', _toggleCheckbox ); elem.on( 'refresh.pageAction', _refreshPageAction ); elem.on( 'addManualItem.pageAction', _addManualItem ); $( 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, reset: reset }; }; 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 || '#', paginationElem: $(elem), 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 = $('