').addClass('ipsGrid ipsGrid_collapsePhone ipsPatchwork');
itemList.after( containerList );
_redrawLayout( true );
},
/**
* Returns the CSS class span for the given column count (e.g. 2 visual columns spans 6 grid columns)
*
* @returns {number} Span number
*/
_getSpanFromCount = function (columns) {
return possibleSizes[ ( possibleSizes.length - 1 ) - possibleSizes.indexOf( columns ) ];
},
/**
* Returns a valid column count. Using a 12-column grid system, certain values are not supported (e.g. 5).
* This method finds the closest valid column number.
*
* @returns {number} Valid column count
*/
_getCorrectColumnCount = function (columns) {
if( columns > 12 ){
return 12;
}
for( var i = 0; i <= possibleSizes.length; i++ ){
if( columns == possibleSizes[ i ] ){
return possibleSizes[ i ];
}
if( columns > i && columns <= possibleSizes[ i + 1 ] ){
var diffA = columns - i;
var diffB = possibleSizes[ i + 1 ] - columns;
if( diffA > diffB ){
return possibleSizes[ i + 1 ];
} else {
return possibleSizes[ i ];
}
}
}
},
/**
* Return the first grid item
*
* @returns {element} First grid item
*/
_getFirst = function () {
return elem.find('[data-role="patchwordGrid"] > [class*="ipsGrid_span"]').first();
},
/**
* Checks the column width, and if it's less or more than our min/max widths, apply a new span
*
* @returns {void}
*/
_checkItemWidth = function () {
var firstItem = _getFirst();
if( options.minColSize && firstItem.outerWidth() < parseInt( options.minColSize ) || options.maxItemSize && firstItem.outerWidth() >= parseInt( options.maxItemSize ) ){
_redrawLayout();
}
};
init();
return {
init: init,
destruct: destruct
};
};
ips.ui.registerWidget( 'patchwork', ips.ui.patchwork, [
'minColSize', 'maxColSize', 'items'
] );
return {
respond: respond,
destruct: destruct,
getObj: getObj
};
});
}(jQuery, _));]]>');
/**
* Sets up this instance
*
* @returns {void}
*/
var init = function () {
currentWidth = Math.floor( elem.width() );
imageData = _getData();
noOfPhotos = imageData.length;
elem.empty();
run( imageData );
// Set up events
$( window ).on( 'resize', _resizeWindow );
$( document ).trigger( 'contentChange', [ elem ] );
},
/**
* Destruct this widget on this element
*
* @returns {void}
*/
destruct = function () {
$( window ).off( 'resize', _resizeWindow );
},
/**
* Refresh the layout
*
* @returns {void}
*/
refresh = function () {
imageData = _getData();
noOfPhotos = imageData.length;
elem.empty();
run( imageData );
},
/**
* Run the resizing process
*
* @returns {void}
*/
run = function (data) {
var initialHeight = Math.max( options.minHeight, Math.floor( currentWidth / 5 ) );
// Get relative sizes
var relativeSizes = [];
_.each( data, function (image) {
var w = 0;
var h = 0;
if( !_.isString( image.filenames.small[0] ) ){
w = options.minHeight;
h = options.minHeight;
} else {
//alert(image.filenames.small[1]);
w = parseInt( image.filenames.small[1], 10 );
h = parseInt( image.filenames.small[2], 10 );
}
if ( ! w ) {
w = options.minWidth;
}
if ( ! h ) {
w = options.minHeight;
}
if( h != initialHeight ){
w = Math.floor( w * ( initialHeight / h ) );
}
relativeSizes.push( w );
});
var imagesSoFar = 0;
var rowNum = 0;
// Start positioning images
while ( imagesSoFar < noOfPhotos ) {
rowNum++;
if( options.maxRows && rowNum > options.maxRows ){
break;
}
var imagesInRow = 0;
var widthOfRow = 0;
// Figure out how many images to show in this row and how wide it will be
while ( ( widthOfRow * 1.1 < currentWidth ) && ( imagesSoFar + imagesInRow < noOfPhotos ) ) {
var gap = options.gap * 2;
if( imagesInRow === 0 ){
gap = options.gap;
}
widthOfRow += relativeSizes[ imagesSoFar + imagesInRow++ ] + gap;
}
// Subtract one measure of gap for the right-hand side
widthOfRow -= options.gap;
var row = _getRow();
var ratio = currentWidth / widthOfRow;
var lastRowNotFit = false;
var i = 0;
widthOfRow = 0;
var rowHeight = Math.floor( initialHeight * ratio );
row.height( rowHeight + ( options.gap * 2 ) );
// Now loop through each image and insert it
while ( i < imagesInRow ) {
var thisImage = data[ imagesSoFar + i ];
var newWidth = Math.floor( relativeSizes[ imagesSoFar + i ] * ratio );
var thisWidth = ( !_.isNull( thisImage.filenames.small[1] ) && thisImage.filenames.small[1] > 0 ) ? thisImage.filenames.small[1] : options.minWidth;
var thisHeight = ( !_.isNull( thisImage.filenames.small[2] ) && thisImage.filenames.small[2] > 0 ) ? thisImage.filenames.small[2] : options.minHeight;
// If this is the last row, we might not have enough images to fill it
// If the height is bigger than our starting height, we'll reset it, and scale this image to match
if( imagesSoFar + imagesInRow >= noOfPhotos && rowHeight >= initialHeight ){
rowHeight = initialHeight; // Reset height
row.height( rowHeight + ( options.gap * 2 ) ); // Resize row
newWidth = Math.floor( thisWidth * ( initialHeight / thisHeight ) ); // Scale width
lastRowNotFit = true;
}
widthOfRow += newWidth + ( options.gap * 2 );
// Build the item using a template
var item = _buildItem( thisImage, {
width: newWidth,
height: rowHeight,
margin: options.gap,
marginLeft: ( i === 0 ) ? 0 : options.gap,
marginRight: ( i + 1 === imagesInRow ) ? 0 : options.gap
});
row.append( item );
i++;
}
// Subtract two counts of margin, for the left and right sides of the row
widthOfRow -= ( options.gap * 2 );
imagesSoFar += imagesInRow;
// We can now tweak image widths to make sure it fills the full row
// If this is the last row, and we don't have enough images to perfectly fill the row, we don't
// want to tweak the sizes or it'll stretch them strangely. We can skip this bit.
if( !lastRowNotFit ){
var smWidth = 0;
while ( widthOfRow < currentWidth ) {
var item = row.find('.cGalleryPatchwork_item:nth-child(' + ( smWidth + 1 ) + ")");
item.css({ width: ( item.width() + 1 ) + 'px' });
smWidth = ( smWidth + 1 ) % imagesInRow;
widthOfRow++;
}
var bigWidth = 0;
while ( widthOfRow > currentWidth ) {
var item = row.find('.cGalleryPatchwork_item:nth-child(' + ( bigWidth + 1 ) + ")");
item.css({ width: ( item.width() - 1 ) + 'px' });
bigWidth = ( bigWidth + 1 ) % imagesInRow;
widthOfRow--;
}
}
}
elem.find('.cGalleryPatchwork_row').css({
width: currentWidth + 'px'
});
},
/**
* Builds an item using the specified data
*
* @param {object} imageData The image data (that came from JSON on the page)
* @param {pbject} extra The processed extra data for the image, such as dims
* @returns {string} The rendered HTML
*/
_buildItem = function (imageData, extra) {
// Calculate retina sizes
var multipliedWidth = extra.width * window.devicePixelRatio;
var multipliedHeight = extra.height * window.devicePixelRatio;
var showThumb = true;
if( !_.isString( imageData.filenames.small[0] ) ){
showThumb = false
} else {
// If the width/height we'll show at is bigger than the small image size, use the medium size instead
if( multipliedWidth <= imageData.filenames.small[1] && multipliedHeight <= imageData.filenames.small[2] ){
imageData.src = imageData.filenames.small[0];
} else {
imageData.src = imageData.filenames.medium[0];
}
}
return ips.templates.render( options.itemTemplate, {
showThumb: showThumb,
image: imageData,
dims: extra
} );
},
/**
* Returns a new row element
*
* @returns {element}
*/
_getRow = function () {
var row = $('').addClass('cGalleryPatchwork_row ipsClearfix');
elem.append( row );
return row;
},
/**
* Gets the data for the patchwork widget to use, either from the data option or from
* elements inside the widget
*
* @returns {object}
*/
_getData = function () {
// If data has been specified on the widget, just use that
if( options.data ){
return $.parseJSON( options.data );
}
var data = [];
// Find all the images and extract data from it
elem.find('[data-role="patchworkImage"][data-json]').each( function () {
try {
var _data = $.parseJSON( $( this ).attr('data-json') );
} catch (err) {
return;
}
data.push( _data );
});
return data;
},
/**
* Event handler for resizing the window
*
* @returns {void}
*/
_resizeWindow = function (e) {
elem.empty();
currentWidth = Math.floor( elem.width() );
noOfPhotos = imageData.length;
run( imageData );
};
init();
return {
init: init,
destruct: destruct,
refresh: refresh
};
};
}(jQuery, _));]]> thumbW ){
tLeft = thumbW - zoomerSize;
} else {
tLeft = cursorPos.left - halfZoomer;
}
if( cursorPos.top - halfZoomer < 0 ){
tTop = 0;
} else if( cursorPos.top + halfZoomer > thumbH ){
tTop = thumbH - zoomerSize;
} else {
tTop = cursorPos.top - halfZoomer;
}
zoomArea.css({
left: tLeft + 'px',
top: tTop + 'px'
});
var reciprocal = 1 / ratio;
// Get ratio of position
var cursorPosLarge = {
left: cursorPos.left * reciprocal,
top: cursorPos.top * reciprocal
};
// Now position large image in the same way
if( cursorPosLarge.left - halfWrapper < 0 ){
fLeft = 0;
} else if( cursorPosLarge.left + halfWrapper > fullW ){
fLeft = fullW - wrapperW;
} else {
fLeft = cursorPosLarge.left - halfWrapper;
}
if( cursorPosLarge.top - halfWrapper < 0 ){
fTop = 0;
} else if( cursorPosLarge.top + halfWrapper > fullH ){
fTop = fullH - wrapperH;
} else {
fTop = cursorPosLarge.top - halfWrapper;
}
imageElem.css({
left: ( fLeft * -1 ) + 'px',
top: ( fTop * -1 ) + 'px',
});
},
/**
* Called when the large image has finished loading
*
* @returns {void}
*/
_imageLoaded = function () {
wrapper.removeClass('ipsLoading');
if( currentlyOver ){
_showZoomer();
}
},
/**
* Shows the zoomer widget
*
* @returns {void}
*/
_showZoomer = function () {
wrapper.show();
zoomArea.show();
// Get dims of the various elems
thumbW = elem.width();
thumbH = elem.height();
//--
wrapperW = wrapper.width();
wrapperH = wrapper.height();
//--
fullW = imageElem.width();
fullH = imageElem.height();
//--
ratio = thumbW / fullW;
zoomArea.css({
width: ( thumbW * ratio ) + 'px',
height: ( thumbW * ratio ) + 'px',
});
zoomerSize = zoomArea.width();
},
/**
* Sets up the zoomer widget, creating the elements needed
*
* @returns {void}
*/
_setUpZoomer = function () {
$('#ipsZoomer, #ipsZoomer_area').remove();
var elemPosition = ips.utils.position.getElemPosition( elem );
var elemSize = ips.utils.position.getElemDims( elem );
var imgURL = ( options.largeURL ) ? options.largeURL : elem.find('img').attr('src');
wrapper = $('').attr( 'id', 'ipsZoomer' );
zoomArea = $('').attr('id', 'ipsZoomer_area').hide();
imageElem = $('').attr( 'src', imgURL ).css({ position: 'absolute' });
$('body').append( wrapper.append( imageElem ) );
elem.append( zoomArea ).css({
position: 'relative'
});
wrapper
.css({
width: elemSize.outerHeight + 'px',
height: elemSize.outerHeight + 'px',
left: elemPosition.absPos.left + elemSize.outerWidth + 20 + 'px',
top: elemPosition.absPos.top + 'px',
zIndex: ips.ui.zIndex()
})
.addClass('ipsLoading');
imageElem.imagesLoaded( _imageLoaded );
initialized = true;
},
/**
* Returns the cursor position relative to the thumbnail
*
* @param {event} e Event object
* @returns {void}
*/
_cursorPos = function (e) {
var offset = elem.offset();
return {
left: e.pageX - offset.left,
top: e.pageY - offset.top
};
};
init();
return {};
};
}(jQuery, _));]]> .ipsQuote_citation').on( 'click', _toggleQuote );
/* Hide embedded quotes */
if( elem.is('blockquote.ipsQuote > blockquote.ipsQuote') ){
elem
.find('> *:not( .ipsQuote_citation )')
.hide()
.end()
.find('> .ipsQuote_citation')
.removeClass('ipsQuote_open')
.addClass('ipsQuote_closed');
}
/* And save that we've done this */
elem.data( 'quoteBuilt', true );
},
/**
* Event handler for toggling the quote visibility
*
* @param {event} e Event object
* @returns {void}
*/
_toggleQuote = function (e) {
var cite = $( e.currentTarget );
var target = $( e.target );
if( target.is('a:not( [data-action="toggleQuote"] )') || ( target.closest('a').length && !target.closest('a').is('[data-action="toggleQuote"]') ) ){
return;
}
e.preventDefault();
if( cite.hasClass('ipsQuote_closed') ){
ips.utils.anim.go( 'fadeIn', cite.siblings() );
cite.removeClass('ipsQuote_closed').addClass('ipsQuote_open');
} else {
cite.siblings().hide();
cite.removeClass('ipsQuote_open').addClass('ipsQuote_closed');
}
e.stopPropagation();
};
ips.ui.registerWidget('quote', ips.ui.quote,
[ 'timestamp', 'userid', 'username', 'contentapp', 'contenttype', 'contentclass', 'contentid', 'contentcommentid' ]
);
return {
respond: respond
};
});
}(jQuery, _));
]]> [data-role="childWrapper"]').hide();
}
} else {
item.addClass('ipsSelectTree_itemOpen');
// Does this node already have children loaded?
if( item.attr('data-childrenLoaded') ){
ips.utils.anim.go( 'fadeIn fast', listItem.find('> [data-role="childWrapper"]') );
} else {
listItem.append( $('').attr('data-role', 'childWrapper').html( ips.templates.render('core.general.loading', { text: ips.getString('loading') } ) ) );
// Fetch it
ips.getAjax()( url )
.done( function (response) {
item.attr( 'data-childrenLoaded', true );
listItem.find('[data-role="childWrapper"]').html( response.output );
listItem.find('[data-role="childWrapper"] .ipsSelectTree_item').each(function(){
if ( $(this).attr('data-id') && selectedItems.indexOf( $(this).attr('data-id') ) != -1 ) {
$(this).addClass('ipsSelectTree_selected');
}
});
});
}
}
},
/**
* Toggles the selected state of a node
*
* @param {event} e Event object
* @returns {void}
*/
_toggleNodeSelection = function (e) {
var node = $( e.currentTarget );
// Is this node already selected?
if( node.hasClass('ipsSelectTree_selected') ){
_unselectNode( node, e );
} else {
_selectNode( node, e );
}
_updateSelectedValues();
},
/**
* Selects the provided node
*
* @param {event} e Event object
* @returns {void}
*/
_selectNode = function (node, e) {
// Remove selected class from other nodes
if( !options.multiple ){
elem.find('.ipsSelectTree_selected').removeClass('ipsSelectTree_selected');
}
// Add our selected class
node.addClass('ipsSelectTree_selected');
var title = node.find('[data-role="nodeTitle"]').text();
var id = node.attr('data-id');
// Add the value to the select box
if( !options.multiple ){
_setValue( title );
} else {
_addToken( title, id );
}
// Add value to our array
if( options.multiple ){
selectedItems.push( node.attr('data-id') );
} else {
selectedItems = [ node.attr('data-id') ];
}
// If this node has children, we'll also load them
if( e ){
_toggleChildren(e, true);
}
// Emit event that node items have been updated
elem.find('.ipsSelectTree_item').trigger( 'nodeItemSelected', {
title: title,
id: id
});
// If we aren't allowing multiple selections, close the widget now
if( !options.multiple && !node.hasClass('ipsSelectTree_withChildren') ){
setTimeout( function () {
_closeResults();
}, 200 );
}
},
/**
* Unselects the provided node
*
* @param {event} e Event object
* @returns {void}
*/
_unselectNode = function (node, e) {
// Remove selected class from this node
node.removeClass('ipsSelectTree_selected');
// Remove value from our selected items
selectedItems = _.without( selectedItems, node.attr('data-id') );
if( !options.multiple ){
_setValue();
} else {
_removeToken( node );
}
},
/**
* Adds a token to the select
*
* @param {string} value Value to set
* @returns {void}
*/
_addToken = function (title, id) {
var valueElem = elem.find('.ipsSelectTree_value');
var elemHeight = elem.outerHeight();
if( !elem.find('[data-role="tokenList"]').length ){
valueElem.html( $('
').addClass('ipsList_inline').attr('data-role', 'tokenList' ) );
}
elem.find('[data-role="tokenList"]').append( ips.templates.render('core.selectTree.token', {
title: title,
id: id
}) );
// Recheck the height
if( elemHeight != elem.outerHeight() ){
_positionResults();
}
},
/**
* Removes a token from the selector
*
* @param {string} value Value to set
* @returns {void}
*/
_removeToken = function (node) {
var id = node.attr('data-id');
var tokenList = elem.find('[data-role="tokenList"]');
var elemHeight = elem.outerHeight();
// Find the token
var token = tokenList.find('[data-nodeId="' + id + '"]').closest('li').remove();
if( !tokenList.find('[data-nodeId]').length ){
tokenList.remove();
_setValue();
}
// Recheck the height
if( elemHeight != elem.outerHeight() ){
_positionResults();
}
},
/**
* Updates the hidden form field containing our current values
*
* @returns {void}
*/
_updateSelectedValues = function () {
elem.find('[data-role="nodeValue"]').val( _.uniq( selectedItems ).join(',') );
// Emit event that node items have been updated
elem.find('.ipsSelectTree_item').trigger( 'nodeSelectedChanged', {
selectedItems: selectedItems
});
},
/**
* Changes the value of the select box
*
* @param {string} value Value to set
* @returns {void}
*/
_setValue = function (value) {
if( value ){
elem.find('.ipsSelectTree_value').text( value ).removeClass('ipsSelectTree_placeholder');
} else {
elem.find('.ipsSelectTree_value').text( ( options.placeholder ) ? options.placeholder : ips.getString('select') ).addClass('ipsSelectTree_placeholder');
}
},
/**
* Toggle showing the results list
*
* @param {event} e Event object
* @returns {void}
*/
_toggleResults = function (e) {
if( results.is(':visible') ){
_maybeHideResults(e);
} else {
_showResults(e);
}
},
/**
* Hides the results panel if the click is not within the results (i.e. if it's on the select itself)
*
* @param {event} e Event object
* @returns {void}
*/
_maybeHideResults = function (e) {
var rawResults = results.get(0);
if( ( !$.contains( rawResults, e.target ) && rawResults != e.target ) ){
_closeResults();
}
},
/**
* Closes the results list
*
* @param {event} e Event object
* @returns {void}
*/
_closeResults = function () {
ips.utils.anim.go( 'fadeOut fast', results );
$( document ).off('click.' + elemID );
elem.removeClass('ipsSelectTree_active');
elem.trigger( 'nodeSelectionClosed' );
},
/**
* Show the results list
*
* @returns {void}
*/
_showResults = function () {
$( document ).on('click.' + elemID, _closeResultsOnBlur );
_positionResults();
results.show();
elem.addClass('ipsSelectTree_active');
// Focus the text box if searching is enabled
if( elem.find('[data-role="nodeSearch"]') ){
elem.find('[data-role="nodeSearch"]').focus();
}
},
/**
* Position the results panel so that it appears attached to the select
*
* @returns {void}
*/
_positionResults = function () {
// Get position of select box
var positionInfo = {
trigger: elem,
target: results,
targetContainer: elem
};
var selectWidth = elem.outerWidth();
var resultsPosition = ips.utils.position.positionElem( positionInfo );
results.css({
left: -parseFloat( results.css('borderLeftWidth') ) - parseFloat( results.css('marginLeft') ),
position: ( resultsPosition.fixed ) ? 'fixed' : 'absolute',
zIndex: ips.ui.zIndex(),
minWidth: selectWidth + 'px'
});
},
/**
* Close the results list when the element is blurred
*
* @param {event} e Event object
* @returns {void}
*/
_closeResultsOnBlur = function (e) {
if( !_clickIsInElem( e.target ) ){
_closeResults();
}
},
/**
* Determines whether the provided target element is contained within the select or results list
*
* @param {element} target Target element
* @returns {boolean}
*/
_clickIsInElem = function (target) {
var rawElem = elem.get(0);
var rawResults = results.get(0);
if( target == rawElem || target == rawResults || $.contains( rawResults, target ) || $.contains( rawElem, target ) ){
return true;
}
return false;
};
init();
return {
destruct: destruct
};
};
ips.ui.registerWidget( 'selectTree', ips.ui.selectTree, [
'placeholder', 'multiple', 'selected', 'url', 'searchable'
] );
return {
respond: respond,
destruct: destruct
};
});
}(jQuery, _));]]>') )
.end()
.find('.ipsSideMenu_mainTitle + div')
.append( $( elem ).find('.ipsSideMenu_title, .ipsSideMenu_subTitle, .ipsSideMenu_list') );
}
// Set up event
$( elem ).on( 'click', '.ipsSideMenu_item', _clickEvent );
$( elem ).on( 'selectItem.sideMenu', _selectItem );
},
/**
* Handles click events on the items
*
* @param {event} e Event object
* @returns {void}
*/
_clickEvent = function (e) {
_doSelectItem( $( e.currentTarget ), e );
},
/**
* Handles a selectItem event
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
_selectItem = function (e, data) {
_doSelectItem( elem.find('[data-ipsMenuValue="' + data.value + '"]'), e );
},
/**
* Selects an item in the menu
*
* @param {element} item jQuery object containing the menu item to be selected
* @param {event} e Event object
* @returns {void}
*/
_doSelectItem = function (item, e) {
// We'll only handle the click in this widget if the item has a menu value attribute
if( ( _.isUndefined( item.attr('data-ipsMenuValue') ) && !item.find('input[type="radio"], input[type="checkbox"]').length ) ||
!item.length ){
return;
}
if( e ){
e.preventDefault();
}
// If this item is disabled, bail out
if( item.hasClass('ipsSideMenu_itemDisabled') ){
return;
}
var workingItems;
if( !options.group ){
workingItems = $( elem ).find('.ipsSideMenu_item');
} else {
workingItems = item.closest('.ipsSideMenu_list').find('.ipsSideMenu_item');
}
if( options.type == 'check' ){
item
.toggleClass('ipsSideMenu_itemActive')
.find('input[type="radio"], input[type="checkbox"]')
.prop( "checked", function (i, val) {
return !val; // Toggles inputs to their opposite state
}).change();
} else {
workingItems
.removeClass('ipsSideMenu_itemActive')
.find('input[type="radio"], input[type="checkbox"]')
.prop( "checked", false );
Debug.log('here');
item
.addClass('ipsSideMenu_itemActive')
.find('input[type="radio"], input[type="checkbox"]').prop( "checked", true ).change();
}
// Get all selected items
var selectedItems = [];
workingItems.filter('.ipsSideMenu_itemActive').each( function () {
selectedItems.push( $( this ).attr('data-ipsMenuValue') );
});
$( elem ).trigger('itemClicked.sideMenu', {
id: $( elem ).identify().attr('id'),
menuElem: $( elem ),
selectedElem: item,
selectedItemID: item.attr('data-ipsMenuValue'),
selectedItems: selectedItems
});
},
/**
* Toggles the menu when in responsive mode
*
* @param {event} e Event object
* @returns {void}
*/
_toggleSideMenu = function (e) {
e.preventDefault();
var menuContainer = $( elem ).find('.ipsSideMenu_mainTitle + div');
if( $( elem ).hasClass('ipsSideMenu_open') ){
$( elem ).removeClass('ipsSideMenu_open');
} else {
$( elem ).addClass('ipsSideMenu_open');
ips.utils.anim.go('fadeIn', menuContainer );
}
};
init();
return {};
};
return {
respond: respond
};
});
}(jQuery, _));]]> .ipsSpoiler_header').on( 'click', _toggleSpoiler );
},
/**
* Event handler for toggling the spoiler visibility
*
* @param {event} e Event object
* @returns {void}
*/
_toggleSpoiler = function (e) {
var header = $( e.currentTarget );
var target = $( e.target );
if( target.is('a:not( [data-action="toggleSpoiler"] )') || ( target.closest('a').length && !target.closest('a').is('[data-action="toggleSpoiler"]') ) ){
return;
}
e.preventDefault();
if( header.hasClass('ipsSpoiler_closed') ){
ips.utils.anim.go( 'fadeIn', header.siblings() );
header.removeClass('ipsSpoiler_closed').addClass('ipsSpoiler_open').find('span').text( ips.getString('spoilerClickToHide') );
} else {
header.siblings().hide();
header.removeClass('ipsSpoiler_open').addClass('ipsSpoiler_closed').find('span').text( ips.getString('spoilerClickToReveal') );
}
e.stopPropagation();
};
// Register this widget with ips.ui
ips.ui.registerWidget( 'spoiler', ips.ui.spoiler );
return {
respond: respond
}
});
}(jQuery, _));]]>= options.maxItems ){
return;
}
/* @link http://community.invisionpower.com/resources/bugs.html/_/ips-4-0/stack-and-arrays-r44513 */
var field = stack.find('[data-ipsStack-wrapper]')
.first()
.html()
.replace( /(name=['"][a-zA-Z0-9\-_]+?)\[([^\]]+?)?\]/g, '$1[' + _getItemCount() + ']' )
.replace( /id=['"](.+?)['"]/g, 'id="$1_' + _getItemCount() + '"' )
.replace( /data-toggles=['"](.+?)['"]/g, function (match, p1) {
var pieces = p1.split(',');
var newPieces = [];
_.each( pieces, function (val) {
if( val.match( /_[0-9]+$/g) ){
newPieces.push( val + '_' + _getItemCount() );
} else {
newPieces.push( val );
}
});
return 'data-toggles="' + newPieces.join(',') + '"';
});
if( !stack.find('select').length ) {
field = field.replace( /value=['"](.+?)['"]/g, 'value="" ' );
}
var html = ips.templates.render( options.itemTemplate, {
field: field
});
// Insert the new row either at the end of the stack or after the current item
if( after ){
after
.after( html )
.next('[data-role="stackItem"]')
.find('input,textarea')
.focus();
} else {
stack
.append( html )
.find('[data-role="stackItem"] input,[data-role="stackItem"] textarea')
.last()
.focus();
}
if( options.maxItems && _getItemCount() >= options.maxItems ){
elem.find('[data-action="stackAdd"]').hide();
}
$( document ).trigger( 'contentChange', [ elem ] );
$( elem ).trigger( 'stackRowAdded', {
count: _getItemCount()
});
},
/**
* Event handler for the Delete Item link
* Removes the row from the stack
*
* @param {event} e Event object
* @returns {void}
*/
_deleteItem = function (e) {
e.preventDefault();
var row = $( e.currentTarget ).closest('[data-role="stackItem"]');
if( _getItemCount() === 1 ){
// Only one item left, so just empty it
row.find('input,textarea').val('');
return;
}
ips.utils.anim.go( 'fadeOutDown', row )
.done( function () {
row.hide();
// Add a little timeout before removing, so that any widgets
// which rely on clicks work properly, e.g. menus
setTimeout( function () {
row.remove();
}, 100);
});
if( options.maxItems && _getItemCount() < options.maxItems ){
elem.find('[data-action="stackAdd"]').show();
}
},
/**
* Returns a count of the number of items in the stack
*
* @returns {number}
*/
_getItemCount = function () {
return stack.find('[data-role="stackItem"]').length;
};
init();
};
}(jQuery, _));]]>= ( originalBottom + options.spacing ) ) &&
status == 'fixed' ){
_makeNormal();
}
} else {
// If the top of our element goes off the screen and we're currently 'normal', then
// set the element to fixed
if( bodyScroll >= ( originalOffsetTop - options.spacing ) ){
// If we're working relative to a parent, and the sticky element would go outside the bounds
// of the parent, then we'll keep it fixed but adjust the top so it stays inside.
if( relativeTo ){
var relativeHeight = relativeTo.height();
var relativePosition = relativeTo.offset();
if( ( options.spacing + elemSize ) > ( relativePosition.top + relativeHeight - bodyScroll ) ){
_makeFixed( -( ( elemSize ) - ( relativePosition.top + relativeHeight - bodyScroll ) ) );
} else if( status == 'normal' ){
_makeFixed();
}
} else if( status == 'normal' ) {
_makeFixed();
}
} else if( bodyScroll <= ( originalOffsetTop - options.spacing ) ){
if( status == 'fixed' ){
_makeNormal();
}
}
}
},
/**
* Puts the element into 'fixed' mode at the top
*
* @returns {void}
*/
_makeFixed = function (offset) {
if( locked ){
return;
}
var width;
if( !dummyElem && !relativeTo ){
_makeDummyElem();
}
// If we're already fixed, we might just be changing the offset - short circuit if so
if( status == 'fixed' && !_.isUndefined( offset ) ){
$( elem ).css( {
top: ( offset ) + 'px'
});
$('#ipsStickySpacer').remove();
return;
}
var bottomSpacing = $( document ).height() - $( window ).height() - $( document ).scrollTop() - 10;
// Do we need to add bottom spacing too?
// THis is needed for edge cases. If the sticky header is, say, 100px high, but when we reach it
// there's only 80px of scroll left on the document, the header will constantly pop in and out of fixed
// positioning. To get around this, we can add a spacer to the end of the document, allowing sufficient scrolling
// space for the document.
if( options.stickTo == 'top' && bottomSpacing < $( elem ).outerHeight() ){
_makeBottomSpacer( bottomSpacing );
}
// Figure out what width we should set the element to
if( options.width && ( options.width.indexOf('#') === 0 || options.width.indexOf('.') === 0 ) ){
width = $( options.width ).first().outerWidth();
} else if( options.width ){
width = parseInt( options.width );
} else {
//width = originalStyles.width;
width = $( elem ).css('width');
}
$( elem )
.css( {
position: 'fixed',
width: width,
zIndex: ips.ui.zIndex()
})
.addClass( options.stickyClass );
// Fix the element in position, to the correct browser side
if( options.stickTo == 'bottom' ){
$( elem )
.css( { bottom: options.spacing + 'px' } )
.addClass( options.stickyClass + '_bottom');
} else {
$( elem )
.css( { top: options.spacing + 'px' } )
.addClass( options.stickyClass + '_top');
}
if( !relativeTo ){
dummyElem
.css( {
width: $( elem ).width(),
height: $( elem ).outerHeight()
})
.show();
}
status = 'fixed';
},
/**
* Returns the element to 'normal' mode
*
* @returns {void}
*/
_makeNormal = function () {
$( elem )
.css( {
position: originalStyles.position,
width: originalStyles.width
})
.removeClass( options.stickyClass )
.removeClass( options.stickyClass + '_top' )
// Reset the value we set earlier
if( options.stickTo == 'bottom' ){
$( elem )
.css( { bottom: originalStyles.bottom } )
.removeClass( options.stickyClass + '_bottom' );
} else {
$( elem )
.css( { top: originalStyles.bottom } )
.removeClass( options.stickyClass + '_top' );
}
if( dummyElem ){
dummyElem.hide();
}
_makeBottomSpacer( 0 );
status = 'normal';
},
/**
* Builds a dummy element which will take up the space of the main element, when
* the main element is in 'fixed' mode
*
* @returns {void}
*/
_makeDummyElem = function () {
dummyElem = $('')
.insertBefore( elem )
.hide();
},
/**
* Adds an element to the bottom of the document which acts as a spacer allowing proper scrolling
*
* @param {number} size Size the spacer should be
* @returns {void}
*/
_makeBottomSpacer = function (size) {
if( !$('#ipsStickySpacer').length ){
$('').attr('id', 'ipsStickySpacer').insertAfter( elem );
}
$('#ipsStickySpacer').css({
height: ( size + 10 ) + 'px'
});
};
_init();
return {
destruct: destruct
};
};
return {
respond: respond,
destruct: destruct
};
});
}(jQuery, _));]]> 1 ){
if( $( elem ).hasClass('ipsTabs_showMenu') ){
$( elem ).removeClass('ipsTabs_showMenu');
} else {
$( elem ).addClass('ipsTabs_showMenu');
}
}
},
/**
* Clicked on a tab on the phone menu
*
* @param {event} e Event object
* @returns {void}
*/
_tabClickPhone = function (e) {
$( elem ).removeClass('ipsTabs_showMenu');
},
/**
* Switches to the specified tab
*
* @param {string} tabId ID of the tab to make active
* @param {boolean} immediate Show immediately, rather than fading in?
* @returns {void}
*/
_switchTab = function (tabId, immediate) {
// Hide all panels
_hideAllPanels();
// Get the new panel
var thisContent = $( '#' + options.panelPrefix + '_' + barId + '_' + tabId + '_panel' );
// Animate it
if( !immediate ){
ips.utils.anim.go( 'fadeIn', thisContent ).done( function () {
thisContent.attr( 'aria-hidden', 'false' );
$( elem ).trigger('tabShown', {
barID: barId,
tabID: tabId,
tab: active,
panel: thisContent
});
// Let everyone know
$( document ).trigger( 'contentChange', [ thisContent ] );
});
} else {
thisContent.show().attr( 'aria-hidden', 'false' );
$( elem ).trigger('tabShown', {
barID: barId,
tabID: tabId,
tab: active,
panel: thisContent
});
// Let everyone know
$( document ).trigger( 'contentChange', [ thisContent ] );
}
// Set as active
active = $( '#' + tabId );
// Switch tab
_makeTabActive( active );
// Let document know
$( elem ).trigger('tabChanged', {
barID: barId,
tabID: tabId,
tab: active,
panel: thisContent
});
},
/**
* Updates the browser URL
*
* @param {string} tabId ID of the tab to make active
* @returns {void}
*/
_updateURL = function (tabId) {
if( !options.updateURL ){
return;
}
var href = $( '#' + tabId ).attr('href'),
title = ( options.updateTitle && $( '#' + tabId ).attr('title') ) ? $( '#' + tabId ).attr('title') : document.title;
if( !_.isEmpty( href ) && !href.startsWith('#') ){
History.replaceState( {}, title, href );
}
},
/**
* Determines which tab is 'active'
*
* @returns {element} The tab deemed to be 'active'
*/
_getActiveTab = function () {
// Try css class first
var activeTab = elem.find( '.' + options.activeClass );
if( activeTab.length ){
return activeTab.get(0);
}
// Next see if a default is specified
if( options.defaultTab && $( elem ).find( options.defaultTab ) ){
return $( elem ).find( options.defaultTab ).get(0);
}
// Finally just return the first tab
return $( elem ).find( options.itemSelector ).first();
},
/**
* Initializes the tab that is first to be active
*
* @param {element} elem The element this widget is being created on
* @param {object} options The options passed into this instance
* @returns {void}
*/
_initializeActive = function () {
// Do we have an active panel?
var activeId = $( active ).identify().attr('id');
if( !$( '#' + options.panelPrefix + '_' + barId + '_' + activeId + '_panel' ).length ){
if( $( options.contentArea ).children().length ){
// We have content in the content area, so make that this panel
$( options.contentArea ).wrapInner( _createTabPanel( activeId, true ) );
_switchTab( activeId, true );
} else {
// Load content
var newPanel = _createTabPanel( activeId );
_loadContent( active, newPanel ).done( function () {
_switchTab( activeId );
});
}
} else {
_switchTab( activeId, true );
}
},
/**
* Makes a tab active and other tabs inactive
*
* @param {element} activeTab Reference to the tab to make active
* @returns {void}
*/
_makeTabActive = function (activeTab) {
// Unselect all tabs
$( elem )
.find( options.itemSelector )
.removeClass( options.activeClass )
.removeAttr( 'aria-selected' );
// Select the new tab
$( activeTab )
.addClass( options.activeClass )
.attr( 'aria-selected', 'true' );
},
/**
* Loads and inserts content via ajax
*
* @param {element} tab The tab element being loaded
* @param {element} container The container panel for the content
* @returns {promise} Promise object
*/
_loadContent = function (tab, container) {
var deferred = $.Deferred();
// Hide all other panels before we start
_hideAllPanels();
// Which URL should we load?
if( $( tab ).attr('data-tabURL') ){
var url = $( tab ).attr('data-tabURL');
} else {
var url = $( tab ).attr('href');
}
// Set loading class
$( options.contentArea ).addClass( options.loadingClass );
// Get ajax object
ajaxObj = ips.getAjax();
ajaxObj( url )
.done( function (response) {
container.html( response );
// Let everyone know
//$( document ).trigger( 'contentChange', [ container ] );
// Resolve promise so callbacks can execute
deferred.resolve();
})
.fail( function (jqXHR, status, errorThrown) {
window.location = tab.href;
})
.always( function () {
$( options.contentArea ).removeClass( options.loadingClass );
});
return deferred.promise();
},
/**
* Hides all tab panels
* @returns {element} The new panel
*/
_hideAllPanels = function () {
$( options.contentArea )
.find( '.' + options.panelClass )
.hide()
.attr('aria-hidden', 'true');
},
/**
* Creates an empty panel for the specific tab
*
* @param {string} tabId Tab ID from which the panel is being created
* @returns {element} The new panel
*/
_createTabPanel = function (tabId, noAppend) {
var newPanel = $('')
.attr( { 'id': options.panelPrefix + '_' + barId + '_' + tabId + '_panel' } )
.addClass( options.panelClass )
.attr( { 'aria-labelledby': tabId } );
if( !noAppend ){
$( options.contentArea ).append( newPanel );
}
return newPanel;
};
init();
return {
init: init
}
};
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.ui.toggle.js - A toggle UI component that replaces checkboxes
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.createModule('ips.ui.toggle', function(){
// Default widget options
var defaults = {
template: 'core.forms.toggle' // The template used to build the toggle
};
/**
* Responder for toggle widget
*
* @param {element} elem The element this widget is being created on
* @param {object} options The options passed into this instance
* @returns {void}
*/
var respond = function (elem, options) {
if( !$( elem ).data('_toggle') ){
$( elem ).data('_toggle', toggleObj(elem, _.defaults( options, defaults ) ) );
}
};
// Register this widget with ips.ui
ips.ui.registerWidget('toggle', ips.ui.toggle, [
'template'
]);
return {
respond: respond
};
});
/**
* Toggle instance
*
* @param {element} elem The element this widget is being created on
* @param {object} options The options passed into this instance
* @returns {void}
*/
var toggleObj = function (elem, options, e) {
var checkID = $( elem ).identify().attr('id'),
wrapper;
/**
* Sets up this instance
*
* @returns {void}
*/
var init = function () {
var status = $( elem ).prop('checked'),
lang = ( status ) ? ips.getString('toggleOn') : ips.getString('toggleOff'),
className = ( status ) ? 'ipsToggle_on' : 'ipsToggle_off';
// Build toggle wrapper
$( elem )
.after(
ips.templates.render( options.template, {
id: checkID + '_wrapper',
status: lang,
className: className
})
)
.hide();
// Set events on wrapper
wrapper = $('#' + checkID + '_wrapper');
wrapper
.on( 'click', function (e) {
if( !$( elem ).is(':disabled') ){
if( $( elem ).prop('checked') ){
_doToggle('off');
} else {
_doToggle('on');
}
$( elem ).change();
}
e.preventDefault();
})
.on( 'keypress', _keyPress );
// Did checkbox have a tooltip? Put it on the toggle instead
if( $( elem ).is('[data-ipstooltip]') ){
wrapper
.attr('data-ipsTooltip', '')
.attr('title', $( elem ).attr('title') );
$( document ).trigger( 'contentChange', [ wrapper ] );
}
/* Is it disabled? */
if( $( elem ).is(':disabled') ){
wrapper.addClass('ipsToggle_disabled');
}
// Set events on checkbox
$( elem )
.on( 'change', function (e) {
// The action we take here is the opposite of what's called in the wrapper click
// event, because by the time the change event is called, the checkbox value has
// already been changed by the browser.
if( $( elem ).is(':checked') ){
_doToggle('on');
} else {
_doToggle('off');
}
});
},
/**
* Change the value of the toggle widget and the checkbox
*
* @param {string} type 'on' or 'off' - the state the widget will be set to
* @returns {void}
*/
_doToggle = function (type) {
if( type == 'off' ){
wrapper
.removeClass('ipsToggle_on')
.addClass('ipsToggle_off')
.find('[data-role="status"]')
.text( ips.getString('toggleOff') );
elem.get(0).checked = false;
} else {
wrapper
.removeClass('ipsToggle_off')
.addClass('ipsToggle_on')
.find('[data-role="status"]')
.text( ips.getString('toggleOn') );
elem.get(0).checked = true;
}
// Programatically checking a box doesn't fire the change event. Fire it manually so that anything
// observing the checkbox is told about it.
//elem.trigger('change');
},
/**
* Event handler for keypress when the widget has focus. Enables us to toggle the widget
* with the keyboard
*
* @param {event} e Event object
* @returns {void}
*/
_keyPress = function (e) {
if( e.keyCode == ips.ui.key.SPACE || e.keyCode == ips.ui.key.ENTER ){
e.preventDefault();
if( $( elem ).prop('checked') ){
_doToggle('off');
} else {
_doToggle('on');
}
}
};
init();
return {
init: init
}
};
}(jQuery, _));";
}
listContainer.append( ips.templates.render( options.template, data ) );
$('#' + files[i].id)
.trigger( 'newItem', [ $('#' + files[i].id) ] );
};
elem.trigger('fileAdded', {
count: files.length,
uploader: options.name
});
},
/**
* Passes a settings object through to plupload, but does not initialize it yet
*
* @returns {void}
*/
_setUpUploader = function () {
pluploadObj = new plupload.Uploader( _getUploaderSettings() );
pluploadObj.bind('Init', events.init );
listContainer.find( '[data-role="file"]' ).each( function () {
var fileElem = $( this );
fileElem.on( 'click', '[data-role="deleteFile"]', _.bind( _deleteFile, fileElem, fileElem ) );
uploadCount++;
});
},
/**
* Builds the settings object which will be passed to plupload
*
* @returns {object}
*/
_getUploaderSettings = function () {
// If there is no action, find one
var form = elem.parentsUntil( '', 'form' );
if ( options.action === null ) {
options.action = form.attr('action');
}
if ( options.key === null ) {
options.key = form.children("[name='plupload']").val();
}
// Init Plupload Options
var pluploadOptions = {
runtimes : 'html5,flash,silverlight,html4',
multi_selection: options.multiple,
url: encodeURI( options.action ),
file_data_name: options.name,
flash_swf_url: 'applications/core/interface/plupload/Movie.swf',
silverlight_xap_url: 'applications/core/interface/plupload/Movie.xap',
browse_button: elem.find( options.button ).identify().attr('id'),
headers: { 'x-plupload': options.key },
chunk_size: options.maxChunkSize + 'mb'
};
/*if( options.maxFileSize ) {
pluploadOptions.filters = {
'max_file_size': ( options.maxFileSize > 1 ) ? options.maxFileSize + 'mb' : ( options.maxFileSize * 1024 ) + 'kb'
};
}*/
/*if ( options.maxFileSize ) {
pluploadOptions.max_file_size = ( options.maxFileSize > 1 ) ? options.maxFileSize + 'mb' : ( options.maxFileSize * 1024 ) + 'kb';
}*/
// Dragdrop target
if( options.dropTarget ){
pluploadOptions.drop_element = $( options.dropTarget ).attr('id');
} else if( $( elem ).hasClass('ipsAttachment_dropZone') ){
pluploadOptions.drop_element = $( elem ).attr('id');
} else if( $( elem ).find('.ipsAttachment_dropZone').length ){
pluploadOptions.drop_element = $( elem ).find('.ipsAttachment_dropZone').identify().attr('id');
}
return pluploadOptions;
},
/**
* Inits pluploader
*
* @returns {void}
*/
_initUploader = function () {
pluploadObj.init();
},
/**
* Binds all of the post-init events for pluploader
*
* @returns {void}
*/
_postInitEvents = function () {
pluploadObj.bind('Error', events.error ); // An error occured
pluploadObj.bind('FilesAdded', events.filesAdded ); // Files added to the queue
pluploadObj.bind('FilesRemoved', events.filesRemoved ); // Files are removed from the queue
pluploadObj.bind('UploadFile', events.uploadFile ); // A file is starting
pluploadObj.bind('UploadProgress', events.uploadProgress ); // There's progress on a file
pluploadObj.bind('FileUploaded', events.fileUploaded ); // A file finished
pluploadObj.bind('UploadComplete', events.uploadComplete ); // All files in the queue finished
$( elem )
.on( 'injectFile', function( e, file ){
inject = true;
pluploadObj.addFile( file );
} )
.on( 'resetUploader', function ( data ){
_resetUploader( e, data );
} )
},
/**
* Resets this uploader instance, clearing it of files
*
* @returns {void}
*/
_resetUploader = function (data) {
// Update upload count
uploadCount = 0;
_updateCount();
$( elem ).trigger( 'removeAllFiles', { uploaderID: uploaderID } );
},
/**
* Begins the upload process (called automatically when files are added)
*
* @returns {void}
*/
_startUpload = function () {
Debug.log('Starting upload process...');
pluploadObj.start();
},
/**
* Returns a human-readable, translated status string
*
* @param {number} status Status code
* @returns {string}
*/
_getStatus = function (status) {
switch( status ) {
case plupload.QUEUED:
return ips.getString('attachQueued');
break;
case plupload.UPLOADING:
return ips.getString('attachUploading');
break;
case plupload.FAILED:
return ips.getString('attachFailed');
break;
case plupload.DONE:
return ips.getString('attachDone');
break;
}
},
/**
* Updates a file element with the current status
*
* @param {object} file File information object from plupload
* @returns {element} The file element
*/
_updateFileElement = function (file) {
var fileElem = _findFileElem( file );
_updateStatus( fileElem, file.status );
_removeStatusClasses( fileElem );
_updateCount();
return fileElem;
},
/**
* Returns the file element for a given file object
*
* @param {object} file File object from plupload
* @returns {element}
*/
_findFileElem = function (file) {
return $( elem ).find('#' + file.id);
},
/**
* Updates the relevant elements within a file element with the current file status
*
* @param {element} fileElem The element that represents this file
* @param {number} status Current status code
* @returns {void}
*/
_updateStatus = function (fileElem, status) {
fileElem.find('[data-role="status"]').html( _getStatus(status) );
},
/**
* Removes the 4 status classes from the provided file element
*
* @param {element} fileElem The element that represents this file
* @returns {void}
*/
_removeStatusClasses = function (fileElem) {
_.each( ['uploading', 'done', 'error', 'queued'], function (type) {
fileElem.removeClass( 'ipsAttach_' + type );
});
},
/**
* Updates relevant elements with the uploaded count, and fires an event to let everyone know
*
* @returns {void}
*/
_updateCount = function () {
$( elem ).find('[data-role="count"]').text( uploadCount );
elem.trigger('uploadedCountChanged', {
count: uploadCount,
percent: pluploadObj.total.percent,
uploader: options.name
});
},
/**
* Updates relevant element within a file element with the percentage completed
*
* @param {element} fileElem The element that represents this file
* @param {number} percent Percentage complete
* @returns {void}
*/
_setPercent = function (fileElem, percent) {
fileElem.find('[data-role="progressbar"]').css( { width: percent + '%' } );
if( percent === 100 ){
ips.utils.anim.go( 'fadeOut fast', fileElem.find('.ipsAttachment_progress') );
}
},
/**
* Builds a thumbnail element
*
* @param {element} fileElem The element that represents this file
* @param {object} file File information object
* @param {object} info Info object from events.uploadDone
* @returns {void}
*/
_buildThumb = function (fileElem, file, info) {
var toInsert = '';
if( info.imagesrc ){
Debug.log( fileElem.find('[data-role="preview"]') );
toInsert = $('').attr( { src: info.thumbnail ? info.thumbnail : info.imagesrc } ).addClass('ipsImage');
fileElem
.attr( 'data-fullsizeurl', info.imagesrc )
.attr( 'data-thumbnailurl', info.thumbnail ? info.thumbnail : info.imagesrc )
.find('[data-role="preview"]')
.html( toInsert )
.css( 'background-image', 'url( "' + ( info.thumbnail ? info.thumbnail : info.imagesrc ) + '")' );
}
fileElem.attr( 'data-fileid', info.id );
},
/**
* Deletes a pre-existing file
*
* @param {element} fileElem The element that represents this file
* @param {event} e Info object from events.uploadDone
* @returns {void}
*/
_deleteFile = function (fileElem, e) {
e.preventDefault();
e.stopPropagation();
var baseUrl = options.action;
if( baseUrl.match(/\?/) ) {
if( baseUrl.slice(-1) != '?' ){
baseUrl += '&';
}
} else {
baseUrl += '?';
}
// Delete via ajax
ips.getAjax()( baseUrl + 'postKey=' + options.postkey + '&deleteFile=' + encodeURIComponent( fileElem.attr('data-fileid') ) );
// Update upload count
uploadCount--;
_updateCount();
// Remove element
fileElem.animationComplete( function () {
fileElem.remove();
// Let the document know
elem.trigger( 'fileDeleted', { fileElem: fileElem, uploader: options.name, count: uploadCount } );
});
ips.utils.anim.go( 'fadeOutDown', fileElem );
},
/**
* Returns boolean indicating if dragging is supported
*
* @returns {boolean} Supports draggable?
*/
_supportsDraggable = function () {
if('draggable' in document.createElement('span') && typeof FileReader != 'undefined' && !/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ){
return true;
}
return false;
},
// Events object
events = {
/**
* Init
*/
init: function(up, err) {
if( _supportsDraggable() ) {
var dropElement = $( up.settings.drop_element );
// Handles adding the dragging class to the upload element.
// dragleave is a pain and works like mouseout instead of mouseleave in that it fires as soon as we leave the parent,
// even if we're actually in a child now. So we have to get creative about detecting it correctly by listening for
// dragenters on the whole document, and seeing whether they're children of our upload element.
var _drag = function () {
var currentElem = null;
$( document )
.on('dragenter', function (e) {
if( currentElem && !$( e.target ).is( dropElement) && !$.contains( dropElement.get(0), currentElem ) ){
dropElement.removeClass('ipsDragging');
currentElem = null;
}
});
dropElement
.on('dragleave', function (e) {
if( !$( currentElem ).is( dropElement) && !$.contains( dropElement.get(0), currentElem ) ){
dropElement.removeClass('ipsDragging');
currentElem = null;
}
})
.on('dragenter', function (e) {
var target = $( e.target );
if( target.is( dropElement ) || $.contains( dropElement.get(0), e.target ) ){
dropElement.addClass('ipsDragging');
currentElem = e.target;
}
})
.on('drop', function (e) {
dropElement.removeClass('ipsDragging');
currentElem = null;
});
}();
}
},
/**
* Files Added
*/
filesAdded: function(up, files) {
if( !options.multiple ) {
listContainer.find( '[data-role="deleteFile"]' ).click();
if ( files.length > 1 ) {
alert( ips.getString( 'uploadSingleErr' ) );
return false;
}
} else if( options.maxTotalSize && up.total.size > ( options.maxTotalSize * 1048576 ) ) {
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString( 'uploadTotalErr', { size: ( options.maxTotalSize > 1 ) ? options.maxTotalSize : ( options.maxTotalSize * 1024 ), size_suffix: ( options.maxTotalSize > 1 ) ? ips.getString('size_mb') : ips.getString('size_kb') } ),
callbacks: {}
});
return false;
}
var tooLarge = 0;
var badType = 0;
var allowedFileTypes = ( options.allowedFileTypes !== null ) ? $.parseJSON( options.allowedFileTypes ).join(',').toLowerCase().split(',') : '';
_.each(files, function (file, idx) {
// Check the size of this file
if( options.maxFileSize !== null && ( ( file.size / 1024 ) > ( options.maxFileSize * 1024 ) ) ){
// The file is too big, so remove it
tooLarge++;
up.removeFile( file );
return;
}
// And the extension
if ( allowedFileTypes && allowedFileTypes.indexOf( file.name.substr( ( ~-file.name.lastIndexOf(".") >>> 0 ) + 2 ).toLowerCase() ) === -1 ) {
badType++;
up.removeFile( file );
return;
}
var size = plupload.formatSize( file.size ),
status = _getStatus( file.status ),
isImage = false;
// Figure out if this is an image based on extension
if( [ 'jpg', 'jpeg', 'jpe', 'gif', 'png' ].indexOf( file.name.substr( ( ~-file.name.lastIndexOf(".") >>> 0 ) + 2 ).toLowerCase() ) !== -1 ){
isImage = true;
}
var data = {
uploaderID: uploaderID,
id: file.id,
title: file.name,
size: size,
status: status,
statusCode: file.status,
statusText: ips.getString('attachStatus', { size: size, status: status } ),
field_name: elem.attr('data-ipsUploader-name'),
newUpload: true,
insertable: true,
isImage: isImage
};
// Trigger event for adding the file element, to allow controllers to intercept
$( elem ).trigger( 'addUploaderFile', data );
$('#' + file.id)
.addClass( 'ipsAttach_queued' )
.trigger( 'newItem', [ $('#' + file.id) ] );
});
// Do we need to warn the user?
if( tooLarge ){
var errorString = ips.getString( 'uploadSizeErr', {
max_file_size: ( options.maxFileSize > 1 ) ? options.maxFileSize : ( options.maxFileSize * 1024 ),
size_suffix: ( options.maxFileSize > 1 ) ? ips.getString('size_mb') : ips.getString('size_kb')
});
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.pluralize( errorString, [ tooLarge, tooLarge ] ),
callbacks: {}
});
}
if( badType ){
var errorString = ips.getString( 'pluploaderr_-601', {
allowed_extensions: allowedFileTypes.join(', ')
});
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.pluralize( errorString, [ tooLarge, tooLarge ] ),
callbacks: {}
});
}
// If we still have some files, we can start them
if( up.files.length ){
elem.trigger('fileAdded', {
count: up.files.length,
uploader: options.name
});
if( options.autoStart ){
_startUpload();
}
}
},
filesRemoved: function () {
Debug.log('removed');
},
/**
* Upload File
*/
uploadFile: function(up, file) {
var fileElem = _updateFileElement( file );
fileElem.addClass('ipsAttach_uploading');
},
/**
* Upload Progress
*/
uploadProgress: function(up, file) {
var fileElem = _updateFileElement( file );
fileElem.addClass('ipsAttach_uploading');
// Set the progress percent
_setPercent( fileElem, file.percent );
elem.trigger('uploadProgress', {
count: uploadCount,
percent: pluploadObj.total.percent,
uploader: options.name
});
},
/**
* All files have finished uploading
* We'll show a notification & sound if supported
*/
uploadComplete: function (up, files) {
var success = 0;
var error = 0;
// Figure out how many files were successful/errors, ignore other statuses
_.each( files, function (file) {
if( file.status === 5 ){
success++;
} else if ( file.status === 4 ){
error++;
}
});
var total = success + error;
elem.trigger('uploadComplete', {
success: success,
error: error,
total: total,
uploader: options.name
});
// Only if we aren't active on the page
if( _.isUndefined( ips.utils.events.getVisibilityProp() ) || !document[ ips.utils.events.getVisibilityProp() ] ){
return;
}
var text = [];
if( !total ){
return;
}
if( success ){
text.push( ips.pluralize( ips.getString('notifyUploadSuccess'), [ success ] ) );
}
if( error ){
text.push( ips.pluralize( ips.getString('notifyUploadError'), [ error ] ) );
}
if( ips.utils.notification.supported ){
ips.utils.notification.create({
title: ips.pluralize( ips.getString('yourUploadsFinished'), [ total ] ),
body: text.join(' '),
icon: ips.getSetting('imgURL') + '/notifyIcons/upload.png',
onClick: function () {
try {
window.focus();
} catch (err) {}
}
}).show();
}
ips.loader.get( ['core/interface/buzz/buzz.min.js'] ).then( function () {
var sound = new buzz.sound( ips.getSetting('baseURL') + 'applications/core/interface/sounds/success', {
formats: [ "mp3" ]
});
sound.play();
});
},
/**
* File Uploaded
*/
fileUploaded: function(up, file, info) {
// Update count of completed files
uploadCount++;
var fileElem = _updateFileElement( file );
fileElem.addClass('ipsAttach_done');
if( options.insertable ){
ips.utils.anim.go('fadeIn', fileElem.find('[data-role="insert"]') );
}
ips.utils.anim.go('fadeIn', fileElem.find('[data-role="deleteFileWrapper"]') );
// Set the progress percent
_setPercent( fileElem, 100 );
// Do we have an image to process?
try {
var jsonInfo = $.parseJSON( info.response );
elem.before( $('').attr( 'name', elem.attr('data-ipsUploader-name') + '_existing[' + file.id + ']' ).attr( 'value', jsonInfo.id ) );
if( jsonInfo['error'] ){
fileElem.on( 'click', '[data-role="deleteFile"]', _.bind( _deleteFile, fileElem, fileElem ) );
file.status = plupload.FAILED;
up.trigger('error', {
code: jsonInfo['error'],
extra: jsonInfo['extra'],
file: file,
uploader: options.name
});
return;
}
if( jsonInfo ){
_buildThumb( fileElem, file, jsonInfo );
fileElem.on( 'click', '[data-role="deleteFile"]', _.bind( _deleteFile, fileElem, fileElem ) );
}
} catch (err) {
fileElem.on( 'click', '[data-role="deleteFile"]', _.bind( _deleteFile, fileElem, fileElem ) );
file.status = plupload.FAILED;
up.trigger('error', {
code: 'upload_error',
extra: err.message,
file: file,
uploader: options.name
});
Debug.warn( err );
}
// Are we handling this immediately?
if ( inject ) {
fileElem.dblclick();
inject = false;
}
},
/**
* Error
*/
error: function(up, err) {
if( err.file ){
_updateFileElement( err.file );
}
// If this is a 'too large' error, we won't
if( err.code == -600 || err.code == -601 ){
errorCounts[ ( err.code == -600 ) ? 'size' : 'ext' ]++;
return;
}
var errorMessage = ips.getString( 'pluploaderr_' + err.code, {
max_file_size: ( options.maxFileSize > 1 ) ? options.maxFileSize : ( options.maxFileSize * 1024 ),
size_suffix: ( options.maxFileSize > 1 ) ? ips.getString('size_mb') : ips.getString('size_kb'),
allowed_extensions: ( options.allowedFileTypes !== null ) ? $.parseJSON( options.allowedFileTypes ).join(',') : '',
server_error_code: ( err.extra !== null ) ? err.extra : 0
});
if ( !errorMessage ) {
errorMessage = ips.getString( 'pluploaderr_SERVER_CONFIGURATION', {
server_error_code: err.code
} );
}
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: errorMessage,
callbacks: {}
});
}
};
init();
return {
init: init
};
}
}(jQuery, _));]]> 0 ){
e.preventDefault();
$( e.currentTarget ).trigger( 'error.validation', { count: errors } );
} else {
$( e.currentTarget ).trigger( 'success.validation' );
}
},
_selectEvent = function (e) {
},
/**
* Handles events on text-like fields
*
* @param {element} elem The element this widget is being created on
* @param {object} options The options passed into this instance
* @returns {void}
*/
_textEvent = function (e) {
var field = $( e.currentTarget );
// If this is the blur event, only validate if we're above the character limit or this is a numerical field
// If this is the keyup event, only validate if we're currently displaying some errors
if( e.type == 'blur' || e.type == 'focusout' ){
if( field.val().length >= options.characterLimit || field.is('[type="number"], [type="range"]') ){
_validate( field );
}
} else {
if( field.attr('data-hasErrors') ){
_validate( field );
}
}
},
/**
* Validates an individual field, displaying or clearing errors as needed
*
* @param {element} target The element being validated
* @returns {boolean} Whether the field is valid
*/
_validate = function (target) {
var result = ips.utils.validate.validate( target );
if( result !== true && !result.result ){
_displayErrors( target, result );
} else {
_clearErrors( target );
}
return result.result;
},
/**
* Displays errors for a field
*
* @param {element} target The element being validated
* @param {object} results Results object returned from ips.utils.validate.validate
* @returns {void}
*/
_displayErrors = function (target, results) {
var id = target.identify().attr('id');
var errorList = $( '#' + id + '_errors' );
// Build error list if necessary
if( !errorList.length ) {
var wrapper = ips.templates.render( 'core.forms.validationWrapper', {
id: id + '_errors'
} );
target.after( wrapper );
errorList = $('#' + id + '_errors');
}
// Reset contents of list
errorList.html('');
// Loop through each message
for( var i = 0; i < results.messages.length; i++ ){
errorList.append( ips.templates.render( 'core.forms.validationItem', {
message: results.messages[ i ].message
}));
}
// Add error class and attribute to the input
target
.addClass('ipsField_error')
.attr( 'data-hasErrors', true );
},
/**
* Clears errors for a field
*
* @param {element} target The form element being cleared
* @returns {void}
*/
_clearErrors = function (target) {
var id = target.identify().attr('id');
if( $( '#' + id + '_errors').length ){
$( '#' + id + '_errors' ).remove();
}
// Remove classname and attribute
target
.removeClass('ipsField_error')
.removeAttr('data-hasErrors');
}
init();
return { };
};
ips.ui.registerWidget('validation', ips.ui.validation);
return {
respond: respond
};
});
}(jQuery, _));]]>' + response + '
' ).find('[data-ipsWizard]').html();
if ( !responseWizard ) {
responseWizard = response;
}
elem.html( responseWizard );
$( document ).trigger( 'contentChange', [ elem ] );
elem.trigger( 'wizardStepChanged' );
})
.fail(function(response, textStatus, errorThrown){
target.attr( 'data-bypassAjax', true );
target.submit();
})
} else {
e.preventDefault();
ips.getAjax()( target.attr('href') ).done( function (response) {
// If a json object is returned with a redirect key, send the user there
if( _.isObject( response ) && response.redirect ){
window.location = response.redirect;
return;
}
var responseWizard = $( '
' + response + '
' ).find('[data-ipsWizard]').html();
if ( !responseWizard ) {
responseWizard = response;
}
elem.html( responseWizard );
$( document ).trigger( 'contentChange', [ elem ] );
elem.trigger( 'wizardStepChanged' );
});
}
},
/**
* Shows the loading thingy by working out the size of the form its replacing
*
* @param {element} elem The wizard element
* @returns {void}
*/
_showLoading = function (elem) {
var loading = elem.find('[data-role="loading"]');
var formContainer = elem.find('[data-role="wizardContent"]');
if( !loading.length ){
loading = $('').attr('data-role', 'loading').addClass('ipsLoading').hide();
elem.append( loading );
}
var dims = {
width: formContainer.outerWidth(),
height: formContainer.outerHeight()
};
loading
.css({
width: dims.width + 'px',
height: dims.height + 'px'
})
.show();
formContainer
.hide()
.after( loading.show() );
},
/**
* Hides the loading thingy
*
* @returns {void}
*/
_hideLoading = function () {
var loading = elem.find('[data-role="loading"]');
var formContainer = elem.find('[data-role="registerForm"]');
loading.remove();
formContainer.show();
};
// Register this module as a widget to enable the data API and
// jQuery plugin functionality
ips.ui.registerWidget( 'wizard', ips.ui.wizard );
return {
respond: respond
};
});
}(jQuery, _));]]>= elem.length ){
deferred.resolve();
}
};
// Loop through each element, adding to its animation queue
_.each( elem, function () {
_addToQueue( elem, animName, run, thisArgs ).always( _checkCount );
_checkQueue( elem );
});
return deferred.promise();
},
/**
* Add an animation to the queue of the provided element
*
* @param {element} elem Element on which the animation executes
* @param {string} animName Animaton to be run
* @param {string} toRun Type of anim to run (anim, or fallback)
* @param {array} args Array of arguments to be passed to animation method
* @returns {object} Returns a promise object, resolved when this animation has been executed
*/
_addToQueue = function (elem, animName, toRun, args) {
var deferred = $.Deferred();
// If we currently have a queue, then add this item
if( !elem.data('animQueue') || !_.isArray( elem.data('animQueue') ) ){
elem.data( 'animQueue', [] );
}
elem.data('animQueue').push({
animName: animName,
run: toRun,
args: args,
deferred: deferred
});
return deferred.promise();
},
/**
* Checks the queue of the provided element, and executes the next animation if ready
*
* @param {element} elem Element to be checked
* @returns {void}
*/
_checkQueue = function (elem) {
var queue = elem.data('animQueue');
if( elem.attr('animating') == true || !queue || !_.isArray( queue ) || !queue.length ){
return;
}
var item = queue.shift();
if( item.run == 'anim' ){
elem.animationComplete( function () {
elem.attr( 'animating', false );
item.deferred.resolve();
_checkQueue( elem );
});
elem.attr( 'animating', true );
_transitions[ item.animName ][ item.run ].apply( this, item.args );
} else {
item.deferred.resolve();
_transitions[ item.animName ][ item.run ].apply( this, item.args );
_checkQueue( elem );
}
item.deferred.resolve();
},
/**
* Removes all ipsAnim_* classnames from an element
* Used to prepare an element for new animations
*
* @param {element} elem Element to clean
* @returns {element} Cleaned element
*/
cleanClasses = function (elem) {
$( elem ).removeClass('ipsAnim').removeClass( function (index, css) {
return ( css.match( /ipsAnim[0-9a-z_\-]+/gi ) || [] ).join(' ');
});
return elem;
},
/**
* Determines whether a transition already exists
*
* @param {string} name Name of transition to check
* @returns {boolean}
*/
isTransition = function (name) {
return !_.isUndefined( _transitions[ name ] );
},
/**
* Registers a transition
*
* @param {string} name Name of this transitions
* @param {function} cssAnimation Function to execute when CSS animation is used
* @param {function} fallbackAnimation Function to execute when fallback animation is needed
* @returns {void}
*/
addTransition = function (name, cssAnimation, fallbackAnimation) {
if( _transitions[ name ] ){
Debug.warn("A transition with the name '" + name + "' already exists");
return;
}
_transitions[ name ] = {
anim: cssAnimation,
fallback: fallbackAnimation
};
},
/**
* Animates scrolling on an element
*
* @returns {boolean}
*/
scrollTo = function (elem, options) {
};
return {
init: init,
cleanClasses: cleanClasses,
animationSupport: animationSupport,
isTransition: isTransition,
addTransition: addTransition,
go: go,
cancel: cleanClasses
};
});
}(jQuery, _));]]> 1 ){
h -= 1;
}
if( ( 6 * h ) < 1 ){
return ( v1 + ( v2 - v1 ) * 6 * h );
}
if( ( 2 * h ) < 1 ){
return v2;
}
if( ( 3 * h ) < 2 ){
return ( v1 + ( v2 - v1 ) * ( ( 2 / 3 ) - h ) * 6 );
}
return v1;
},
/**
* Converts the provided RGB values to their HSL versions
*
* @param {number} r Red
* @param {number} g Green
* @param {number} b Blue
* @returns {array} [ hue, saturation, lightness ]
*/
RGBtoHSL = function (r, g, b) {
var lightness, hue, saturation = 0;
var min = _.min( [ r, g, b ] );
var max = _.max( [ r, g, b ] );
var delta = max - min;
lightness = ( max + min ) / 2;
if( delta == 0 ){ // Grey
hue = 0;
saturation = 0;
} else {
if( lightness < 0.5 ){
saturation = delta / ( max + min );
} else {
saturation = delta / ( 2 - max - min );
}
var delta_r = ( ( ( max - r ) / 6 ) + ( delta / 2 ) ) / delta;
var delta_g = ( ( ( max - g ) / 6 ) + ( delta / 2 ) ) / delta;
var delta_b = ( ( ( max - b ) / 6 ) + ( delta / 2 ) ) / delta;
if( r == max ){
hue = delta_b - delta_g;
} else if( g == max ){
hue = ( 1 / 3 ) + delta_r - delta_b;
} else if( b == max ){
hue = ( 2 / 3 ) + delta_g - delta_r;
}
if( hue < 0 ){
hue += 1;
}
if( hue > 1 ){
hue -= 1;
}
}
return [ hue, saturation, lightness ];
},
/**
* Converts the provided hex into an RGB value
*
* @param {string} hex Hex value
* @returns {number} RGB value (0-255)
*/
hexToRGB = function (hex) {
return parseInt( hex,16 );
},
/**
* Converts the provided RGB values to their Hex version
*
* @param {number} r Red
* @param {number} g Green
* @param {number} b Blue
* @returns {string} Hex
*/
RGBtoHex = function (r, g, b) {
var hex = [ r.toString(16), g.toString(16), b.toString(16) ];
_.each( hex, function (val, nr) {
if( val.length == 1 ){
hex[ nr ] = '0' + val;
}
});
return hex.join('');
};
return {
convertHex: convertHex,
HSLtoRGB: HSLtoRGB,
hueToRGB: hueToRGB,
RGBtoHSL: RGBtoHSL,
hexToRGB: hexToRGB,
RGBtoHex: RGBtoHex
};
});
}(jQuery, _));]]> 10 ){
expires = "; expires=" + sticky;
}
}
if( !_.isUndefined( ips.getSetting('cookie_domain') ) && ips.getSetting('cookie_domain') != '' ){
domain = "; domain=" + ips.getSetting('cookie_domain');
}
if( !_.isUndefined( ips.getSetting('cookie_path') ) && ips.getSetting('cookie_path') != '' ){
path = ips.getSetting('cookie_path');
}
if( !_.isUndefined( ips.getSetting('cookie_prefix') ) && ips.getSetting('cookie_prefix') != '' ){
prefix = ips.getSetting('cookie_prefix');
}
if( !_.isUndefined( ips.getSetting('cookie_ssl') ) && ips.getSetting('cookie_ssl') != '' ){
ssl = '; secure';
}
document.cookie = prefix + cookieKey + "=" + escape( value ) + "; path=" + path + expires + domain + ssl + ';';
_store[ cookieKey ] = value;
},
/**
* Deletes a cookie
*
* @param {string} cookieKey Key to delete
* @returns {void}
*/
unset = function (cookieKey) {
if( _store[ cookieKey ] ){
set( cookieKey, '', -1 );
}
},
/**
* Parses the provided string as a query string and returns an object representation
*
* @param {string} cookieString Query string to parse
* @returns {object}
*/
_parseCookies = function (cookieString) {
var pairs = cookieString.split(";"),
cookies = {};
for ( var i=0; i#fff 0%,#333 50%,#000 100%
*/
_buildStops = function (stops) {
var line = [];
for( var i = 0; i < stops.length; i++ ){
if( stops[ i ][0].charAt(0) != '#' ){
stops[ i ][0] = '#' + stops[ i ][0];
}
line.push( stops[ i ][0] + ' ' + stops[ i ][1] + '%' );
}
return line.join(',');
},
/**
* Returns the correct angle value for each supported vendor, accounting for w3c difference and directional keywords
*
* @param {number} angle The angle of the gradient (can be negative)
* @returns {object} e.g. { w3c: 'to bottom', moz: 'top', webkit: 'top', o: 'top', ms: 'top' }
*/
_buildAngles = function (angle) {
var mapDegrees = {
'0': 'right',
'90': 'top',
'-90': 'bottom',
'180': 'left'
};
var opposites = { '0':'180', '90':'-90', '-90':'90', '180':'0' };
var output = {};
for( var i = 0; i < prefixes.length; i++ ){
if( !_.isUndefined( mapDegrees[ angle ] ) ){
if( prefixes[ i ] == 'w3c' ){
output[ prefixes[ i ] ] = 'to ' + mapDegrees[ opposites[ angle ] ];
} else {
output[ prefixes[ i ] ] = mapDegrees[ angle ];
}
} else {
output[ prefixes[ i ] ] = angle + 'deg';
}
}
return output;
},
/**
* Builds a vendor-prefixed version of the given style property
* Does not validate that the style property is one that actually needs prefixing
*
* @param {string} vendor Vendor key to use
* @param {string} style Style property to prefix
* @returns {string} e.g. -webkit-linear-gradient
*/
_buildPrefix = function (vendor, style) {
return ( ( vendor != 'w3c' ) ? '-' + vendor + '-': '' ) + style;
};
return {
generateGradient: generateGradient,
replaceStyle: replaceStyle,
getStylesheetRef: getStylesheetRef,
buildStyleBlock: buildStyleBlock
};
});
}(jQuery, _));]]>' ).replace( '{content}', '' ) );
var spans = element.getElementsByTag( 'span' );
for ( var i = 0; i < spans.count(); i++ ) {
if ( spans.getItem( i ).hasAttribute( 'data-content-marker' ) ) {
selected.insertAfterNode( spans.getItem( i ) );
spans.getItem( i ).remove();
}
}
editor.insertHtml( element.getHtml() );
}
}
},
/**
* Single-Line Blocks
*/
singleblock: function( command, tagName, tagAttributes, beforeContent, includeContent, afterContent, optionValue ) {
return {
exec: function( editor ) {
editor.focus();
var style = new CKEDITOR.style( { element: tagName, attributes: tagAttributes } );
var elementPath = editor.elementPath();
if ( style.checkActive( elementPath, editor ) ) {
editor['removeStyle']( style );
} else {
if ( beforeContent || !includeContent || afterContent ) {
var range = editor.getSelection().getRanges()[0];
var selected = range.extractContents();
var element = new CKEDITOR.dom.element( tagName );
element.setAttributes(tagAttributes);
var content = beforeContent;
if ( includeContent ) {
content += "{content}";
}
content += afterContent;
element.setHtml( content.replace( '{contents}', '' ).replace( '{content}', '' ) );
var spans = element.getElementsByTag( 'span' );
for ( var i = 0; i < spans.count(); i++ ) {
if ( spans.getItem( i ).hasAttribute( 'data-content-marker' ) ) {
selected.insertAfterNode( spans.getItem( i ) );
spans.getItem( i ).remove();
}
}
editor.insertElement( element );
} else {
editor['applyStyle']( style );
}
}
}
}
},
/**
* Blocks
*/
block: function( command, tagName, tagAttributes, beforeContent, includeContent, afterContent, optionValue ) {
if ( tagName == 'p' ) {
tagName = 'div';
}
return {
exec: function( editor ) {
var selection = editor.getSelection(),
range = selection && selection.getRanges( true )[ 0 ];
if ( !range )
return;
var bookmarks = selection.createBookmarks();
// Kludge for #1592: if the bookmark nodes are in the beginning of
// blockquote, then move them to the nearest block element in the
// blockquote.
if ( CKEDITOR.env.ie ) {
var bookmarkStart = bookmarks[ 0 ].startNode,
bookmarkEnd = bookmarks[ 0 ].endNode,
cursor;
if ( bookmarkStart && bookmarkStart.getParent().getName() == tagName ) {
cursor = bookmarkStart;
while ( ( cursor = cursor.getNext() ) ) {
if ( cursor.type == CKEDITOR.NODE_ELEMENT && cursor.isBlockBoundary() ) {
bookmarkStart.move( cursor, true );
break;
}
}
}
if ( bookmarkEnd && bookmarkEnd.getParent().getName() == tagName ) {
cursor = bookmarkEnd;
while ( ( cursor = cursor.getPrevious() ) ) {
if ( cursor.type == CKEDITOR.NODE_ELEMENT && cursor.isBlockBoundary() ) {
bookmarkEnd.move( cursor );
break;
}
}
}
}
var iterator = range.createIterator(),
block;
iterator.enlargeBr = editor.config.enterMode != CKEDITOR.ENTER_BR;
var paragraphs = [];
while ( ( block = iterator.getNextParagraph() ) )
paragraphs.push( block );
// If no paragraphs, create one from the current selection position.
if ( paragraphs.length < 1 ) {
var para = editor.document.createElement( editor.config.enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ),
firstBookmark = bookmarks.shift();
range.insertNode( para );
para.append( new CKEDITOR.dom.text( '\ufeff', editor.document ) );
range.moveToBookmark( firstBookmark );
range.selectNodeContents( para );
range.collapse( true );
firstBookmark = range.createBookmark();
paragraphs.push( para );
bookmarks.unshift( firstBookmark );
}
// Make sure all paragraphs have the same parent.
var commonParent = paragraphs[ 0 ].getParent(),
tmp = [];
for ( var i = 0; i < paragraphs.length; i++ ) {
block = paragraphs[ i ];
commonParent = commonParent.getCommonAncestor( block.getParent() );
}
// The common parent must not be the following tags: table, tbody, tr, ol, ul.
var denyTags = { table:1,tbody:1,tr:1,ol:1,ul:1 };
while ( denyTags[ commonParent.getName() ] )
commonParent = commonParent.getParent();
// Reconstruct the block list to be processed such that all resulting blocks
// satisfy parentNode.equals( commonParent ).
var lastBlock = null;
while ( paragraphs.length > 0 ) {
block = paragraphs.shift();
while ( !block.getParent().equals( commonParent ) )
block = block.getParent();
if ( !block.equals( lastBlock ) )
tmp.push( block );
lastBlock = block;
}
// If any of the selected blocks is a blockquote, remove it to prevent
// nested blockquotes.
while ( tmp.length > 0 ) {
block = tmp.shift();
if ( block.getName() == tagName ) {
var docFrag = new CKEDITOR.dom.documentFragment( editor.document );
while ( block.getFirst() ) {
docFrag.append( block.getFirst().remove() );
paragraphs.push( docFrag.getLast() );
}
docFrag.replace( block );
} else
paragraphs.push( block );
}
// Parse option
if ( optionValue ) {
beforeContent = beforeContent.replace( '{option}', optionValue );
afterContent = afterContent.replace( '{option}', optionValue );
var _tagAttributes = tagAttributes;
tagAttributes = {};
for ( i in _tagAttributes ) {
var newI = i.replace( '{option}', optionValue );
tagAttributes[newI] = _tagAttributes[i].replace( '{option}', optionValue );
}
}
// Now we have all the blocks to be included in a new blockquote node.
var bqBlock = editor.document.createElement( tagName );
bqBlock.setAttributes( tagAttributes );
bqBlock.insertBefore( paragraphs[ 0 ] );
var content = '';
if ( beforeContent ) {
content += beforeContent;
}
if ( includeContent ) {
content += '';
}
if ( afterContent ) {
content += afterContent;
}
bqBlock.appendHtml( content );
if ( includeContent ) {
var spans = bqBlock.getElementsByTag( 'span' );
for ( var i = 0; i < spans.count(); i++ ) {
if ( spans.getItem( i ).hasAttribute( 'data-content-marker' ) ) {
while ( paragraphs.length > 0 ) {
block = paragraphs.pop();
block.insertAfter( spans.getItem( i ) );
}
spans.getItem( i ).remove();
}
}
}
selection.selectBookmarks( bookmarks );
editor.focus();
}
}
}
};
});
}(jQuery, _));]]> 0) || (navigator.msMaxTouchPoints > 0) );
},
/**
* Returns the correct Page Visibility API event
*
* @returns string
*/
getVisibilityEvent = function () {
if( !_.isUndefined( document.hidden ) ){
return 'visibilitychange';
} else if( !_.isUndefined( document.mozHidden ) ){
return 'mozvisibilitychange';
} else if( !_.isUndefined( document.msHidden ) ){
return 'msvisibilitychange';
} else if( !_.isUndefined( document.webkitHidden ) ){
return 'webkitvisibilitychange';
}
return '_unsupported';
},
/**
* Returns the correct Page Visibility API property
*
* @returns string or undefined
*/
getVisibilityProp = function () {
if( !_.isUndefined( document.hidden ) ){
return 'hidden';
} else if( !_.isUndefined( document.mozHidden ) ){
return 'mozHidden';
} else if( !_.isUndefined( document.msHidden ) ){
return 'msHidden';
} else if( !_.isUndefined( document.webkitHidden ) ){
return 'webkitHidden';
}
return undefined;
},
/**
* Does the actual firing
*
*/
_fireEvent = function (element, ev) {
if( document.createEvent ) {
var evObj = document.createEvent('MouseEvents');
evObj.initEvent( ev, true, false );
element.dispatchEvent( evObj );
} else if ( document.createEventObject ) {
var evObj = document.createEventObject();
element.fireEvent( 'on' + evt, evObj );
}
};
return {
manualEvent: manualEvent,
isTouchDevice: isTouchDevice,
getVisibilityEvent: getVisibilityEvent,
getVisibilityProp: getVisibilityProp
};
});
}(jQuery, _));]]> theWindow.width() ){
position.horizontal = 'left';
}
}
if( position.horizontal == 'left' ){
if( ( viewportOffset.left + targetDims.outerWidth - offset.left ) > theWindow.width() ){
position.horizontal = 'right';
}
} else if ( position.horizontal == 'right' ) {
if( ( viewportOffset.right - targetDims.outerWidth + offset.left ) < 0 ){
position.horizontal = 'left';
}
}
// Top pos
if( position.vertical == 'top' || preferBottom ){
if( ( viewportOffset.top - targetDims.outerHeight - offset.top ) < 0 ){
position.vertical = 'bottom';
}
} else {
if( ( viewportOffset.top + triggerDims.outerHeight + targetDims.outerHeight + offset.top ) > theWindow.height() ){
position.vertical = 'top';
}
}
return position;
},
/**
* Returns dimensions for the given element
*
* @param {element} elem Element to test
* @returns {object}
*/
getElemDims = function (elem) {
elem = $( elem );
return {
width: elem.width(),
height: elem.height(),
outerWidth: elem.outerWidth(),
outerHeight: elem.outerHeight()
};
},
/**
* Returns the natural width for an image element
*
* @param {element} elem Image element to use
* @returns {number}
*/
naturalWidth = function (elem) {
return _getNatural( elem, 'Width' );
},
/**
* Returns the natural height for an image element
*
* @param {element} elem Image element to use
* @returns {number}
*/
naturalHeight = function (elem) {
return _getNatural( elem, 'Height' );
},
/**
* Attempts to get the line-height of the provided element. Note; there's cases where this won't be reliable.
* For example, if CSS styles differently, it may give a value different to the parent. Test your case
* before relying on this.
*
* @param {element} parent Element to fetch the line-height for
* @returns {number}
*/
lineHeight = function (parent) {
// Create a little dummy element we can use to sniff the line height
var newElem = $('').html('abc').css({
opacity: 0.1
});
parent.append( newElem );
// Get it then remove the dummy element
var height = newElem.height();
newElem.remove();
return height;
},
/**
* Returns the given natural dimension of an image, using the built in naturalWidth/Height property if available
*
* @param {element} elem Element to test
* @param {string} type Width or Height; dimension to return
* @returns {number}
*/
_getNatural = function (elem, type) {
if( ( 'natural' + type ) in new Image() ){
return elem[0][ 'natural' + type ];
} else {
var img = new Image();
img.src = elem[0].src;
return img[ 'natural' + type ];
}
};
return {
getElemPosition: getElemPosition,
getElemDims: getElemDims,
positionElem: positionElem,
hasFixedParents: hasFixedParents,
naturalWidth: naturalWidth,
naturalHeight: naturalHeight,
lineHeight: lineHeight
};
});
}(jQuery, _));
]]>= num;
});
// If we have any breakpoints in range, get the biggest,
// otherwise we'll get the smallest possible size
if( possibleSizes.length ){
curBreak = _.max( possibleSizes, function (num) {
return parseInt( num );
});
} else {
curBreak = _.min( breakpointsByKey, function (num) {
return parseInt( num );
});
}
return curBreak;
},
// --------------------------------------------------------------------
// PRIVATE METHODS
/**
* Initialization method; imports default breakpoints and set up
* window.resize event
*
* @returns {void}
*/
init = function (){
// Add our default breakpoints to start with
$.each( options.breakpoints, function (size, name) {
addBreakpoint(size, name);
});
$( window ).resize( windowResize );
checkForBreakpointChange();
},
/**
* Event handler fired when window resizes
*
* @returns {void}
*/
windowResize = function () {
checkForBreakpointChange();
},
enabled = function () {
return true;
};
// Initialize this module
init();
// Expose public methods
return {
addBreakpoint: addBreakpoint,
addCallback: addCallback,
currentIs: currentIs,
getCurrentKey: getCurrentKey,
getAllBreakpoints: getAllBreakpoints,
enabled: enabled
}
});
}(jQuery, _));]]> 2 ) ){
time = time.substr( time.indexOf(' ') + 1 );
}
time = time.split('').reverse().join('');
return ips.getString( 'time_other', {
time: time
});
}
},
/**
* Returns a boolean indicating whether the user is in DST
*
* @param {object} d Date object to test
* @returns {boolean}
*/
isDST = function () {
var today = new Date();
var jan = new Date( today.getFullYear(), 0, 1);
var jul = new Date( today.getFullYear(), 6, 1);
var stdOffset = Math.max( jan.getTimezoneOffset(), jul.getTimezoneOffset() );
return today.getTimezoneOffset() < stdOffset;
},
/**
* Returns true is the provided object is a valid Date object (containing a valid date)
*
* @param {object} d Date object to test
* @returns {boolean}
*/
isValidDateObj = function (d) {
if( Object.prototype.toString.call( d ) !== "[object Date]" ){
return false;
}
return !isNaN( d.getTime() );
},
/**
* Returns a current unix timestamp
*
* @returns {number}
*/
timestamp = function () {
return Date.now();
},
/**
* Returns javascript's toLocaleDateString, passing the options object
* if the browser supports the parameter
*
* @param {date} date Date object
* @param {object} options Options object (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString)
* @returns {string}
*/
localeDateString = function (date, options) {
if( !_.isBoolean( _supportsLocale ) ){
_supportsLocale = _checkLocaleSupport();
}
if( _supportsLocale && _.isObject( options ) && window.navigator.language ){
return date.toLocaleDateString( window.navigator.language, options );
} else {
// UAs that don't support the options object will show the date in the system timezone, which means a midnight time will
// show on the wrong day if you aren't UTC. To get around that, we'll work out the number of hours offset in the current
// timezone, and adjust our date based on that to normalize it.
// https://community.invisionpower.com/4bugtrack/active-reports/calendar-dates-not-working-properly-with-ios-r6771/
var currentTimeZoneOffsetInHours = new Date().getTimezoneOffset() / 60;
date.setUTCHours( currentTimeZoneOffsetInHours );
return date.toLocaleDateString();
}
},
/**
* Tests whether the browser supports native date pickers
*
* @returns {boolean}
*/
supportsHTMLDate = function () {
var i = document.createElement('input');
i.setAttribute( 'type', 'date' );
return i.type !== 'text';
},
/**
* Tests if the browser supports the options parameter of toLocaleDateString
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString
* @returns {boolean}
*/
_checkLocaleSupport = function () {
try {
new Date().toLocaleDateString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
};
return {
readable: readable,
localeDateString: localeDateString,
isValidDateObj: isValidDateObj,
timestamp: timestamp,
supportsHTMLDate: supportsHTMLDate
};
});
}(jQuery, _));]]>
// MIT License
function parseUri (str) {
var o = parseUri.options,
m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
uri = {},
i = 14;
while (i--) uri[o.key[i]] = m[i] || "";
uri[o.q.name] = {};
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
if ($1) uri[o.q.name][$1] = $2;
});
return uri;
};
parseUri.options = {
strictMode: false,
key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
q: {
name: "queryKey",
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
},
parser: {
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
}
};
return {
getParam: getParam,
getURIObject: getURIObject
};
});
}(jQuery, _));]]>= min;
},
rangelength: function (val, min, max) {
return validators.maxlength( val, max ) && validators.minlength( val, min );
},
min: function (val, min) {
return Number( val ) >= min;
},
max: function (val, max) {
return Number( val ) <= max;
},
range: function (val, min, max) {
return validators.min( val, min ) && validators.max( val, max );
},
required: function (val) {
return val.length > 0
},
regex: function (val, regex) {
return new RegExp( regex ).test( val );
},
format: function (val, format) {
return new RegExp( formats[ format ] ).test( val );
},
remote: function (val, url) {
var deferred = $.Deferred();
ips.getAjax()( url, {
dataType: 'json',
data: {
input: encodeURI( val )
}
})
.done( function (response) {
if( response.result == 'ok' ){
deferred.resolve();
} else {
deferred.reject( response.message || null );
}
})
.fail( function (jqHXR, textStatus) {
deferred.reject( textStatus );
});
return deferred.promise();
}
};
// Setters
/**
* Adds a custom format
*
* @param {string} name Identifying name for this format
* @param {regexp} format The format, as a regexp literal
* @returns {void}
*/
var addFormat = function (name, format) {
formats[ name ] = format;
},
/**
* Adds a custom validator
*
* @param {string} name Identifying name for this validator
* @param {function} fn Function called when this validator is used
* @returns {void}
*/
addValidator = function (name, fn) {
validators[ name ] = fn;
};
// Shortcut methods for individual validators/formats
/**
* Checks whether the value is a valid URL
*
* @param {string} url The URL to validate
* @returns {boolean}
*/
var isUrl = function (url) {
return validators.regex( url, formats.url );
},
/**
* Checks whether the value is an allowed URL
*
* @param {string} url The URL to validate
* @returns {boolean}
*/
isAllowedUrl = function (url) {
var returnValue;
returnValue = true;
if( ips.getSetting('blacklist') )
{
for( var i in ips.getSetting('blacklist') )
{
var blacklistUrl = ips.getSetting('blacklist')[i];
blacklistUrl = escapeRegExp( blacklistUrl );
blacklistUrl = blacklistUrl.replace( /\\\*/g, '(.+?)' );
var index = url.search( new RegExp( blacklistUrl, 'ig' ) );
if( index >= 0 )
{
returnValue = false;
break;
}
}
}
if( ips.getSetting('whitelist') )
{
returnValue = false;
for( var i in ips.getSetting('whitelist') )
{
var whitelistUrl = ips.getSetting('whitelist')[i];
whitelistUrl = escapeRegExp( whitelistUrl );
whitelistUrl = whitelistUrl.replace( /\\\*/g, '(.+?)' );
var index = url.search( new RegExp( whitelistUrl, 'ig' ) );
if( index >= 0 )
{
returnValue = true;
break;
}
}
}
return returnValue;
},
/**
* Checks whether the value is a valid email address
*
* @param {string} email The email address to validate
* @returns {boolean}
*/
isEmail = function (email) {
return validators.regex( email, formats.email );
};
/**
* Main validation method
* Combines provided conditions with HTML5 conditions gleaned from the element. Checks each condition
* by executing the relevant validators.
*
* @param {element} field The element being validated
* @param {object} conditions Object of conditions/values to use when validating
* @param {boolean} ignoreHTML5 Ignore the HTML5 validation properties?
* @returns {object}
*/
var validate = function (field, conditions, ignoreHTML5) {
if( !ignoreHTML5 ){
conditions = _.extend( _getAutomaticConditions( field ), conditions || {} );
}
if( !_.size( conditions ) ){
return true;
}
// Now work through each condition
var validated = true;
var messages = [];
for( var i in conditions ) {
if( !_.isFunction( validators[ i ] ) ){
continue;
}
var value = field.val();
var args = [];
if( _.isObject( conditions[ i ] ) ){
args = _.values( conditions[ i ] )
args.splice( 0, 1 );
} else {
args = [ conditions[ i ] ];
}
args.unshift( value );
if( validators[ i ].apply( this, args ) !== true ) {
validated = false;
messages.push( {
condition: i,
message: _getMessage( i, args )
});
}
}
return {
result: validated,
messages: messages
}
},
/**
* Returns the parsed error message for the given validator type
*
* @param {string} type The validator type
* @param {object} args The values originally passed into the validator
* @returns {string}
*/
_getMessage = function (type, args) {
return ips.getString( 'validation_' + type, {
data: args
});
},
/**
* Builds an object of conditions for an element based on HTML5 attributes (e.g. required)
*
* @param {element} field The element being validated
* @returns {object}
*/
_getAutomaticConditions = function (field) {
var conditions = {};
if( field.is('[required]') ){
conditions.required = true;
}
if( field.is('input[type="number"], input[type="range"], input[type="email"], input[type="url"]') ){
conditions.format = field.attr('type');
}
if( field.is('[max]') ){
conditions.max = field.attr('max');
}
if( field.is('[min]') ){
conditions.min = field.attr('min');
}
if( field.is('[pattern]') ){
conditions.regex = field.attr('pattern');
}
return conditions;
};
// Expose public methods
return {
isUrl: isUrl,
isEmail: isEmail,
addFormat: addFormat,
addValidator: addValidator,
validate: validate,
isAllowedUrl: isAllowedUrl
}
});
}(jQuery, _));
// http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
var escapeRegExp;
(function () {
// Referring to the table here:
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/regexp
// these characters should be escaped
// \ ^ $ * + ? . ( ) | { } [ ]
// These characters only have special meaning inside of brackets
// they do not need to be escaped, but they MAY be escaped
// without any adverse effects (to the best of my knowledge and casual testing)
// : ! , =
// my test "~!@#$%^&*(){}[]`/=?+\|-_;:'\",<.>".match(/[\#]/g)
var specials = [
// order matters for these
"-"
, "["
, "]"
// order doesn't matter for any of these
, "/"
, "{"
, "}"
, "("
, ")"
, "*"
, "+"
, "?"
, "."
, "\\"
, "^"
, "$"
, "|"
]
// I choose to escape every character with '\'
// even though only some strictly require it when inside of []
, regex = RegExp('[' + specials.join('\\') + ']', 'g')
;
escapeRegExp = function (str) {
return str.replace(regex, "\\$&");
};
// test escapeRegExp("/path/to/res?search=this.that")
}());]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.attachments.list.js - My-Attachments controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.attachments.list', {
initialize: function () {
this.on( 'click', '[data-action="deleteAttachment"]', this.deleteAttachment );
},
/**
* Removes the attachment row
*
* @param e
*/
deleteAttachment: function (e) {
e.preventDefault();
var elem = $( e.currentTarget );
// Confirm it
ips.ui.alert.show( {
type: 'confirm',
icon: 'question',
message: ips.getString('delete'),
subText: ips.getString('delete_confirm'),
callbacks: {
ok: function () {
ips.getAjax()( elem.attr('href') )
.done( function (response) {
var row = elem.closest('div.ipsDataItem');
row.remove();
ips.utils.anim.go( 'fadeOut', row );
});
}
}
});
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.acpSearchKeywords.js - Faciliates editing keywords for ACP search when IN_DEV
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.core.acpSearchKeywords', {
initialize: function () {
this.on( 'click', '[data-action="save"]', this.saveKeywords );
},
/**
* Save the keywords
*
* @param {event} e Event object
* @returns {void}
*/
saveKeywords: function (e) {
e.preventDefault();
var scope = this.scope;
var data = {
url: scope.attr('data-url'),
lang_key: scope.find( "[data-role='lang_key']" ).val(),
restriction: scope.find( "[data-role='restriction']" ).val(),
keywords: []
};
scope.find( "[data-role='keywords']" ).each(function(){
if( $(this).val() ){
data.keywords.push( $(this).val() );
}
});
ips.getAjax()( scope.attr('data-action'), {
data: data,
type: 'post',
showLoading: true
})
.done( function(response) {
scope.trigger('closeMenu');
ips.ui.flashMsg.show('Keywords saved');
});
},
});
}(jQuery, _));
= ( this._pages.length - 1 ) ) );
},
/**
* Sets up the content, showing pagination
*
* @returns {void}
*/
_setupPages: function () {
// Find the pages
this._pages = this.scope.find('[data-role="contentPage"]');
if( this._pages.length < 2 ){
return;
}
// Add pagination to the top and bottom
this.scope.prepend( ips.templates.render('core.pagination') );
this.scope.append( ips.templates.render('core.pagination') );
// Hide all pages
this._pages.hide();
// Do we have a page in the URL?
if( !_.isUndefined( ips.utils.url.getParam('page' + this._articleID ) ) ){
this._currentPage = parseInt( ips.utils.url.getParam('page' + this._articleID ) );
}
// Show the right page
$( this._pages[ this._currentPage - 1 ] ).show();
// Hide the 'previous'
this._checkButtons();
// And reinit content
$( document ).trigger( 'contentChange', [ this.scope ] );
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.autoSizeIframe.js - Controller to automatically adjust the height of an iframe
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.autoSizeIframe', {
initialize: function () {
console.log('init');
this.on( 'load', this.resizeFrame );
},
/**
* Event handler to resize frame
*
* @param {event} e Event object
* @returns {void}
*/
resizeFrame: function (e) {
// I know this seems bad, but there's no easy way to detect when the iframe's contents have loaded
setTimeout( function(){
e.target.style.height = e.target.contentWindow.document.body.scrollHeight + 'px';
e.target.style.overflow = 'hidden';
e.target.scrolling = "no";
e.target.seamless = "seamless";
}, 1500 );
}
});
}(jQuery, _));' + selectedText + '';
}
this._selectedText = selectedText;
this._inlineQuoteTimeout = setTimeout( function () {
self._showQuoteTooltip( e.clientX, e.clientY );
}, 300 );
},
/**
* Event handler for clicking 'quote this' in the selective quote tooltip
* Triggers an event sent to the comment feed
*
* @param {event} e Event object
* @returns {void}
*/
quoteSelection: function (e) {
e.preventDefault();
if( this._selectedText ){
this.trigger('quoteComment.comment', {
userid: this._quoteData.userid,
username: this._quoteData.username,
timestamp: this._quoteData.timestamp,
contentapp: this._quoteData.contentapp,
contenttype: this._quoteData.contenttype,
contentclass: this._quoteData.contentclass,
contentid: this._quoteData.contentid,
contentcommentid: this._quoteData.contentcommentid,
quoteHtml: this._selectedText
});
}
this._hideQuoteTooltip();
},
/**
* Triggered when the moderation checkbox is changed
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
commentCheckbox: function (e) {
var checked = $( e.currentTarget ).is(':checked');
this.scope.closest('.ipsComment').toggleClass( 'ipsComment_selected', checked );
this.trigger('checkedComment.comment', {
commentID: this._commentID,
actions: $( e.currentTarget ).attr('data-actions'),
checked: checked
});
},
/**
* The comment feed has told us we can't support quoting
*
* @returns {void}
*/
disableQuoting: function () {
this._quotingDisabled = true;
this.scope.find('[data-ipsQuote-editor]').remove();
},
/**
* Event handler for the Helpful/Unhelpful buttons.
*
* @param {event} e Event object
* @returns {void}
*/
rateReview: function (e) {
e.preventDefault();
var scope = this.scope
ips.getAjax()( $( e.currentTarget ).attr('href') )
.done(function(response){
scope.parent().replaceWith( $(response) );
})
.fail(function(err){
window.location = $( e.currentTarget ).attr('href');
});
},
/**
* User has clicked the share button within the comment; we'll select the text to make it easy to copy
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
shareCommentMenu: function (e, data) {
if( data.menu ){
data.menu.find('input[type="text"]').get(0).select();
}
},
/**
* Event fired on this controller by a core.commentFeed controller to tell us which
* multiquote buttons are enabled presently. Here we check whether this applies to us, and toggle
* the button if so.
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
setMultiQuote: function (e, data) {
var selector = '[data-commentApp="' + data.contentapp + '"]';
selector += '[data-commentType="' + data.contenttype + '"]';
selector += '[data-commentID="' + data.contentcommentid + '"]';
if( this.scope.is( selector ) ){
if( !_.isNull( e ) && e.type == 'setMultiQuoteEnabled') {
this.scope.find('[data-action="multiQuoteComment"]')
.removeClass('ipsButton_simple')
.addClass('ipsButton_alternate')
.attr( 'data-mqActive', true )
.html( ips.templates.render('core.posts.multiQuoteOn') );
} else if( _.isNull( e ) || e.type == 'setMultiQuoteDisabled' ) {
this.scope.find('[data-action="multiQuoteComment"]')
.addClass('ipsButton_simple')
.removeClass('ipsButton_alternate')
.removeAttr( 'data-mqActive' )
.html( ips.templates.render('core.posts.multiQuoteOff') );
}
}
},
/**
* Event handler for the Quote button. Triggers a quoteComment event for the
* commentFeed controller to handle.
*
* @param {event} e Event object
* @returns {void}
*/
quoteComment: function (e) {
e.preventDefault();
if( !this._getQuoteData() ){
Debug.error("Couldn't get quote data");
return;
}
var html = this._prepareQuote( $('').html( this.scope.find('[data-role="commentContent"]').html() ) );
Debug.log( this._commentID );
// Send the event up the chain to the commentFeed controller for handling
this.trigger( 'quoteComment.comment', {
userid: this._quoteData.userid,
username: this._quoteData.username,
timestamp: this._quoteData.timestamp,
contentapp: this._quoteData.contentapp,
contenttype: this._quoteData.contenttype,
contentclass: this._quoteData.contentclass,
contentid: this._quoteData.contentid,
contentcommentid: this._quoteData.contentcommentid,
quoteHtml: html.html()
});
},
/**
* MultiQuote comment handler
*
* @param {event} e Event object
* @returns {void}
*/
multiQuoteComment: function (e) {
e.preventDefault();
if( !this._getQuoteData() ){
Debug.error("Couldn't get quote data");
return;
}
var button = $( e.currentTarget );
var mqActive = button.attr('data-mqActive');
var html = this._prepareQuote( $('').html( this.scope.find('[data-role="commentContent"]').html() ) );
this.trigger( ( mqActive ) ? 'removeMultiQuote.comment' : 'addMultiQuote.comment', {
userid: this._quoteData.userid,
username: this._quoteData.username,
timestamp: this._quoteData.timestamp,
contentapp: this._quoteData.contentapp,
contenttype: this._quoteData.contenttype,
contentclass: this._quoteData.contentclass,
contentid: this._quoteData.contentid,
contentcommentid: this._quoteData.contentcommentid,
quoteHtml: html.html(),
button: button.attr('data-mqId')
});
if( mqActive ){
button
.removeClass('ipsButton_alternate')
.addClass('ipsButton_simple')
.removeAttr('data-mqActive')
.html( ips.templates.render('core.posts.multiQuoteOff') );
} else {
button
.removeClass('ipsButton_simple')
.addClass('ipsButton_alternate')
.attr( 'data-mqActive', true )
.html( ips.templates.render('core.posts.multiQuoteOn') );
}
},
/**
* Edit comment handler
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
editComment: function (e) {
e.preventDefault();
this._commentContents = this.scope.find('[data-role="commentContent"]').html();
var url = $( e.currentTarget ).attr('href');
this.trigger( 'getEditForm.comment', {
url: url,
commentID: this._commentID
});
},
/**
* Called when a cancel link is clicked
*
* @param {event} e Event object
* @returns {void}
*/
cancelEditComment: function (e) {
e.preventDefault();
var self = this;
ips.ui.alert.show( {
type: 'confirm',
icon: 'warn',
message: ips.getString('generic_confirm'),
subText: '',
callbacks: {
ok: function () {
self.scope.find('[data-role="commentContent"]').html( self._commentContents );
self.scope.find('[data-role="commentControls"], [data-action="expandTruncate"]').show();
}
}
});
},
/**
* Called when a comment edit button is clicked
*
* @param {event} e Event object
* @returns {void}
*/
submitEdit: function (e) {
e.preventDefault();
e.stopPropagation(); // This is a form within a form, so we have to prevent it bubbling up otherwise IE gets confused and will try to submit the moderation actions form too
var instance;
var empty = false;
for( instance in CKEDITOR.instances ){
CKEDITOR.instances[ instance ].updateElement();
}
var postBody = $.trim( CKEDITOR.instances['comment_value'].editable().getData().replace( / /g, '' ) );
if( postBody == '' || postBody.match( /^
(
|<\/p>|\s)*<\/p>$/ ) ){
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('cantEmptyEdit'),
subText: ips.getString('cantEmptyEditDesc')
});
return;
}
var form = this.scope.find('form');
var url = form.attr('action');
var data = form.serialize();
this.trigger( 'saveEditComment.comment', {
form: data,
url: url,
commentID: this._commentID
});
},
/**
* Model event: something is loading
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
commentLoading: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
var commentLoading = this.scope.find('[data-role="commentLoading"]');
commentLoading
.removeClass('ipsHide')
.find('.ipsLoading')
.removeClass('ipsLoading_noAnim');
ips.utils.anim.go( 'fadeIn', commentLoading );
},
/**
* Model event: something is done loading
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
commentDone: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
this.scope
.find('[data-role="commentLoading"]')
.addClass('ipsHide')
.find('.ipsLoading')
.addClass('ipsLoading_noAnim');
},
/**
* Model event: edit form has loaded
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
getEditFormDone: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
var self = this;
var showForm = function () {
self._isEditing = true;
self.scope.find('[data-action="expandTruncate"], [data-role="commentControls"]').hide();
self.scope.find('[data-role="commentContent"]').html( data.response );
$( document ).trigger( 'contentChange', [ self.scope.find('[data-role="commentContent"]') ] );
};
// Scroll to the comment
var elemPosition = ips.utils.position.getElemPosition( this.scope );
var windowScroll = $( window ).scrollTop();
var viewHeight = $( window ).height();
// Only scroll if it isn't already on the screen
if( elemPosition.absPos.top < windowScroll || elemPosition.absPos.top > ( windowScroll + viewHeight ) ){
$('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' }, function () {
showForm();
});
} else {
showForm();
}
},
/**
* Model event: error loading edit form
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
getEditFormError: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
window.location = data.url;
},
/**
* Model event: saving an edit is finished
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
saveEditCommentDone: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
this._isEditing = false;
this.scope.find('[data-role="commentContent"]').replaceWith( $('
' + data.response + '
').find('[data-role="commentContent"]') );
this.scope.trigger('initializeImages');
this.scope.find('[data-action="expandTruncate"], [data-role="commentControls"]').show();
$( document ).trigger( 'contentChange', [ this.scope ] );
},
/**
* Model event: saving an edit failed
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
saveEditCommentError: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('editCommentError'),
});
//this.scope.find('form').submit();
},
/**
* Handler for approving a comment
*
* @param {event} e Event object
* @returns {void}
*/
approveComment: function (e) {
e.preventDefault();
var url = $( e.currentTarget ).attr('href');
this.trigger( 'approveComment.comment', {
url: url,
commentID: this._commentID
});
},
/**
* Model indicates it's starting to approve the comment
*
* @param {event} e Event object
* @param {object} data Data object from model
* @returns {void}
*/
approveCommentLoading: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
this.scope
.find('[data-role="commentControls"]')
.addClass('ipsFaded')
.find('[data-action="approveComment"]')
.addClass( 'ipsButton_disabled' )
.text( ips.getString( 'commentApproving' ) );
},
/**
* Model returned success for approving the comment
*
* @param {event} e Event object
* @param {object} data Data object from model
* @returns {void}
*/
approveCommentDone: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
var commentHtml = $('
' + data.response + '
').find('[data-controller="core.front.core.comment"]').html();
// Remove moderated classes and update HTML
this.scope
.html( commentHtml )
.removeClass('ipsModerated')
.closest( '.ipsComment' )
.removeClass('ipsModerated');
// Let document know
$( document ).trigger( 'contentChange', [ this.scope ] );
// Set up multiquote in this comment
if( ips.utils.db.isEnabled() ){
this.scope.find('[data-action="multiQuoteComment"]').removeClass('ipsHide');
}
// And show a flash message
ips.ui.flashMsg.show( ips.getString( 'commentApproved' ) );
},
/**
* Model returned an error for approving the comment
*
* @param {event} e Event object
* @param {object} data Data object from model
* @returns {void}
*/
approveCommentError: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
window.location = data.url;
},
/**
* Handler for delete comment
*
* @param {event} e Event object
* @returns {void}
*/
deleteComment: function (e) {
e.preventDefault();
var self = this;
var url = $( e.currentTarget ).attr('href');
var commentData = this._getQuoteData();
var eventData = _.extend( commentData, {
url: url,
commentID: this._commentID
});
ips.ui.alert.show( {
type: 'confirm',
icon: 'warn',
message: ips.getString('delete_confirm'),
callbacks: {
ok: function(){
self.trigger( 'deleteComment.comment', eventData );
}
}
});
},
/**
* Model event: delete comment finished
*
* @param {event} e Event object
* @returns {void}
*/
deleteCommentDone: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
var deleteLink = this.scope.find('[data-action="deleteComment"]');
// Stuff to HIDE elements on delete
var toHide = null;
var toShow = null;
if( deleteLink.attr('data-hideOnDelete') ){
toHide = this.scope.find( deleteLink.attr('data-hideOnDelete') );
} else {
toHide = this.scope.closest('article');
}
toHide.animationComplete( function () {
toHide.remove();
});
ips.utils.anim.go( 'fadeOutDown', toHide );
// Update count
if ( deleteLink.attr('data-updateOnDelete') ) {
$( deleteLink.attr('data-updateOnDelete') ).text( parseInt( $( deleteLink.attr('data-updateOnDelete') ).text() ) - 1 );
}
// Stuff to SHOW elements on delete
if( deleteLink.attr('data-showOnDelete') ) {
toShow = this.scope.find( deleteLink.attr('data-showOnDelete') );
ips.utils.anim.go( 'fadeIn', toShow );
}
this.trigger( 'deletedComment.comment', {
commentID: this._commentID,
response: data.response
});
},
/**
* Model event: delete comment failed
*
* @param {event} e Event object
* @returns {void}
*/
deleteCommentError: function (e, data) {
if( data.commentID != this._commentID ){
return;
}
window.location = data.url;
},
/**
* Prepares post data for quoting
*
* @param {string} html Post contents
* @returns {string} Transformed post contents
*/
_prepareQuote: function (html) {
/* Remove nested quotes */
if( html.find('blockquote.ipsQuote') &&
html.find('blockquote.ipsQuote').parent() && html.find('blockquote.ipsQuote').parent().get( 0 ) &&
html.find('blockquote.ipsQuote').parent().get( 0 ).tagName == 'DIV' && html.find('blockquote.ipsQuote').siblings().length == 0 )
{
var div = html.find('blockquote.ipsQuote').closest('div');
div.next('p').find("br:first-child").remove();
div.remove();
}
else
{
html.find('blockquote.ipsQuote').remove();
}
/* Expand spoilers */
html.find('.ipsStyle_spoilerFancy,.ipsStyle_spoiler').replaceWith( ips.templates.render( 'core.posts.quotedSpoiler' ) );
/* Remove data-excludequote (used for "edited by" byline presently, but can be used by anything) */
html.find("[data-excludequote]").remove();
/* Remove the citation */
html.find('.ipsQuote_citation').remove();
/* Set the quote value */
html.find( '[data-quote-value]' ).each( function () {
$( this ).replaceWith( '
' + $( this ).attr('data-quote-value') + '
' );
});
return html;
},
/**
* Parses the JSON object containing quote data for the comment
*
* @returns {object} Quote data, or else an empty object
*/
_getQuoteData: function () {
if( !this._quoteData ){
try {
this._quoteData = $.parseJSON( this.scope.attr('data-quoteData') );
return this._quoteData;
} catch (err) {
Debug.log("Couldn't parse quote data");
return {};
}
}
return this._quoteData;
},
/**
* Returns the selected HTML
*
* @returns {string} The selected HTML
*/
_getSelectedText: function () {
var text = '';
var container = this.scope.find('[data-role="commentContent"]').get(0);
var isChild = function (child, parent) {
if(child === parent){
return true;
}
var current = child;
while (current) {
if(current === parent){
return true;
}
current = current.parentNode;
}
return false;
};
// http://stackoverflow.com/questions/29038950/get-selected-text-with-a-div-with-arbitrary-nested-divs
if( window.getSelection || document.getSelection ){
var selection = ( window.getSelection ) ? window.getSelection() : document.getSelection();
if( selection.rangeCount > 0 ){
var range = selection.getRangeAt(0);
var clonedSelection = range.cloneContents().querySelector('[data-role="commentContent"]');
if( clonedSelection ){
text = clonedSelection.innerHTML;
} else {
clonedSelection = range.cloneContents();
var startNode = selection.getRangeAt(0).startContainer.parentNode;
if( isChild( startNode, container ) ) {
var div = document.createElement('div');
div.appendChild( clonedSelection );
text = div.innerHTML;
}
}
return text;
}
} else if ( document.selection ){
return document.selection.createRange().htmlText;
}
return '';
},
/**
* Builds & shows the selective quoting tooltip, displaying it just above the user's cursor
*
* @param {number} clientX X position of the cursor
* @param {number} clientY Y position of the cursor
* @returns {void}
*/
_showQuoteTooltip: function (clientX, clientY) {
var tooltip = this.scope.find('[data-role="inlineQuoteTooltip"]');
if( !tooltip.length ){
// Create the new tooltip
this.scope.append( ips.templates.render('core.selection.quote') );
tooltip = this.scope.find('[data-role="inlineQuoteTooltip"]');
}
// Get the correct positioning
// Unfortunately we can't use our ips.utils.position utility here because that deals
// with elements, not specific coordinates.
var scopeOffset = this.scope.offset();
var lineHeight = ips.utils.position.lineHeight( this.scope.find('[data-role="commentContent"]') );
var documentScroll = {
top: $( document ).scrollTop(),
left: $( document ).scrollLeft()
};
var tooltipSize = {
width: tooltip.show().width(),
height: tooltip.show().height()
};
var tooltipPos = {
left: ( clientX + documentScroll.left ) - scopeOffset.left - ( tooltipSize.width / 2 ),
top: ( clientY + documentScroll.top ) - scopeOffset.top - tooltipSize.height - lineHeight
};
this.scope.find('[data-role="inlineQuoteTooltip"]').css({
position: 'absolute',
left: tooltipPos.left + "px",
top: tooltipPos.top + "px",
}).hide().fadeIn('fast');
$( document ).on( 'click', this._clickHandler );
},
/**
* Hide the selective quote tooltip
*
* @returns {void}
*/
_hideQuoteTooltip: function () {
$( document ).off( 'click', this._clickHandler );
this.scope.find('[data-role="inlineQuoteTooltip"]').fadeOut('fast');
clearTimeout( this._inlineQuoteTimeout );
}
});
}(jQuery, _));
]]>' + response + '' ).find( '[data-feedID="' + this.scope.attr('data-feedID') + '"]' );
var newContents = tmpElement.html();
tmpElement.remove();
this.cleanContents();
//ips.ui.destructAllWidgets( this.scope );
this.scope.hide().html( newContents );
// Show content and hide loading
ips.utils.anim.go( 'fadeIn', this.scope );
this._overlay.hide();
// Open external links in a new window
if( ips.getSetting('links_external') ) {
this.scope.find('a[rel*="external"]').each( function( index, elem ){
elem.target = "_blank";
})
}
// Update multiquote, let document know, then highlight checked comments
this._setUpMultiQuote();
$( document ).trigger( 'contentChange', [ this.scope ] );
this._findCheckedComments();
},
/**
* Callback when the results ajax fails
*
* @param {object} jqXHR jQuery XHR object
* @param {string} textStatus Error message
* @param {string} errorThrown
* @returns {void}
*/
_getResultsFail: function (jqXHR, textStatus, errorThrown) {
if( Debug.isEnabled() ){
Debug.error( "Ajax request failed (" + textStatus + "): " + errorThrown );
Debug.error( jqXHR.responseText );
} else {
// rut-roh, we'll just do a manual redirect
window.location = this._baseURL + this._getURL();
}
},
/**
* Callback always called after ajax request to load results
*
* @returns {void}
*/
_getResultsAlways: function () {
//
},
/**
* Callback always called after ajax request to load results
*
* @returns {void}
*/
_setLoading: function (status) {
var scope = this.scope;
var self = this;
var commentFeed = this.scope.find('[data-role="commentFeed"]');
if( status ){
if( !this._overlay ){
this._overlay = $('').addClass('ipsLoading').hide();
ips.getContainer().append( this._overlay );
}
// Get dims & position
var dims = ips.utils.position.getElemDims( commentFeed );
var position = ips.utils.position.getElemPosition( commentFeed );
this._overlay.show().css({
left: position.viewportOffset.left + 'px',
top: position.viewportOffset.top + $( document ).scrollTop() + 'px',
width: dims.width + 'px',
height: dims.height + 'px',
position: 'absolute',
zIndex: ips.ui.zIndex()
});
commentFeed.animate({
opacity: 0.5
});
// Get top postition of feed
var elemPosition = ips.utils.position.getElemPosition( this.scope );
$('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' } );
} else {
// Stop loading
}
},
/**
* Responds to a pagination click
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
paginationClick: function (e, data) {
data.originalEvent.preventDefault();
if( data.pageNo != this._urlParams[ this._pageParam ] ){
//var newObj = {};
//newObj[ this._pageParam ] = data.pageNo;
var urlObj = ips.utils.url.getURIObject( data.href );
var queryKey = urlObj.queryKey;
// If this is coming from a page jump, the page number may not be in the href passed
// through. So check whether it exists, and manually add it to the object if needed.
if( _.isUndefined( queryKey[ this._pageParam ] ) ){
queryKey[ this._pageParam ] = data.pageNo;
}
// If we're now on the last page, start polling again
if( data.lastPage && !this._pollingActive ){
this.scope.attr( 'data-lastPage', true );
this._currentPoll = this._initialPoll;
this._startPolling();
} else if( !data.lastPage ) {
this.scope.removeAttr( 'data-lastPage' );
this._stopPolling();
}
this._updateURL( queryKey );
}
},
/**
* Pushes a new URL state to the browser
*
* @param {object} newParams Object to be added to the state
* @returns {void}
*/
_updateURL: function (newParams) {
// We don't insert a record into the history when the page loads. That means when a user
// goes to page 1 -> page 2 then hits back, there's no record of 'page 1' in the history, and it doesn't work.
// To fix that, we're tracking a 'doneInitialState' flag in this controller. The first time this method is called
// and doneInitialState == false, we insert the *current* url into the stack before changing the URL. This gives
// the History manager something to go back to when the user clicks back to the initial page.
/*if( !this._doneInitialState ){
History.replaceState(
_.extend( _.clone( this._urlParams ), { controller: this.controllerID, feedID: this._commentFeedID, bypassState: true } ),
document.title,
document.location
);
this._doneInitialState = true;
}*/
_.extend( this._urlParams, newParams );
var tmpStateData = _.extend( _.clone( this._urlParams ), { controller: this.controllerID, feedID: this._commentFeedID } );
History.pushState(
tmpStateData,
document.title,
this._baseURL + this._getURL()
);
},
/**
* Builds a param string from values in this._urlParams, excluding empty values
*
* @returns {string} Param string
*/
_getURL: function () {
var tmpUrlParams = {};
for( var i in this._urlParams ){
if( this._urlParams[ i ] != '' && i != 'controller' && i != 'feedID' && i != 'bypassState' ){
tmpUrlParams[ i ] = this._urlParams[ i ];
}
}
return $.param( tmpUrlParams );
},
/**
* An editor in this feed has indicated its compatibility
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
editorCompatibility: function (e, data) {
if( !data.compatible ){
this.triggerOn( 'core.front.core.comment', 'disableQuoting.comment' );
}
},
/**
* A comment controller triggered an event indicating it was selected
* Adds the comment ID and actions to localStorage so it can be tracked across
* pages of the feed
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
checkedComment: function (e, data) {
var dataStore = ips.utils.db.get( 'moderation', this._commentFeedID ) || {};
if( data.checked ){
if( _.isUndefined( dataStore[ data.commentID ] ) ){
dataStore[ data.commentID ] = data.actions;
}
} else {
delete dataStore[ data.commentID ];
}
// Store the updated value, or delete if it's empty now
if( _.size( dataStore ) ){
ips.utils.db.set( 'moderation', this._commentFeedID, dataStore );
} else {
ips.utils.db.remove( 'moderation', this._commentFeedID );
}
},
/**
* Called on setup, loops through the selected comments for this feedID from localstorage,
* and checks any that are present on this page. For others, instructs the pageAction
* widget to add the ID to its store manually so that they can still be worked with.
*
* @returns {void}
*/
_findCheckedComments: function () {
// Bail if there's no checkboxes anyway
if( !this.scope.find('input[type="checkbox"]').length ){
return;
}
// Fetch the checked comments for this feedID
var dataStore = ips.utils.db.get( 'moderation', this._commentFeedID ) || {};
var self = this;
var pageAction = this.scope.find('[data-ipsPageAction]');
if( _.size( dataStore ) ){
_.each( dataStore, function (val, key) {
if( self.scope.find('[data-commentID="' + key + '"]').length ){
self.scope
.find('[data-commentID="' + key + '"] input[type="checkbox"][data-role="moderation"]')
.attr( 'checked', true )
.trigger('change');
} else {
pageAction.trigger('addManualItem.pageAction', {
id: 'multimod[' + key + ']',
actions: val
});
}
});
}
},
/**
* Options
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
signatureOptions: function (e, data) {
data.originalEvent.preventDefault();
if( data.selectedItemID == 'oneSignature' ){
this._ignoreSingleSignature( $( e.currentTarget ).attr('data-memberID') );
} else {
this._ignoreAllSignatures();
}
},
/**
* Fires a request to hide all signatures in the feed
*
* @returns {void}
*/
_ignoreAllSignatures: function () {
var self = this;
var url = ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=settings&do=toggleSigs';
var signatures = this.scope.find('[data-role="memberSignature"]');
// Hide all signatures on the page
signatures.slideUp();
ips.getAjax()( url )
.done( function (response) {
ips.ui.flashMsg.show( ips.getString('signatures_hidden') );
signatures.remove();
})
.fail( function () {
signatures.show();
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('signatures_error'),
callbacks: {}
});
});
},
/**
* Fires a request to hide a single signature (i.e. a single member's signature)
*
* @param {number} memberID Member ID's signature to hide
* @returns {void}
*/
_ignoreSingleSignature: function (memberID) {
var self = this;
var url = ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=ignore&do=ignoreType&type=signatures';
var signatures = this.scope.find('[data-role="memberSignature"]').find('[data-memberID="' + memberID + '"]').closest('[data-role="memberSignature"]');
signatures.slideUp();
ips.getAjax()( url, {
data: {
member_id: parseInt( memberID )
}
})
.done( function (response) {
ips.ui.flashMsg.show( ips.getString('single_signature_hidden') );
signatures.remove();
})
.fail( function () {
signatures.show();
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('single_signature_error'),
callbacks: {}
});
});
},
/**
* Ignore options
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
commentIgnore: function (e, data) {
switch( data.selectedItemID ){
case 'showPost':
data.originalEvent.preventDefault();
this._showHiddenPost( e, data );
break;
case 'stopIgnoring':
data.originalEvent.preventDefault();
this._stopIgnoringFromComment( e, data );
break;
}
},
/**
* Filter click
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
filterClick: function(e) {
e.preventDefault();
var urlObj = ips.utils.url.getURIObject( $( e.target ).attr('href') );
var queryKey = urlObj.queryKey;
this._updateURL( queryKey );
},
/**
* Responds to a quote event from a comment controller
* Finds the reply box for this feed, and triggers a new event instructing the
* editor to insert the quote
*
* @param {event} e Event object
* @param {object} data Event data object (which should contain all of the properties necessary for a quote)
* @returns {void}
*/
quoteComment: function (e, data) {
var editor = ips.ui.editor.getObj( this.scope.find('[data-role="replyArea"] [data-ipsEditor]') );
if( editor ){
editor.insertQuotes( [ data ] );
}
},
/**
* If the window blurs, we will pause polling, but if a poll is skipped, we'll immediately poll on window focus
*
* @returns {void}
*/
windowBlur: function (e) {
if( this._pollingEnabled ){
Debug.log( 'Window blurred, pausing polling...' );
this._pollingPaused = true;
}
},
/**
* Window focus - if polling was paused and a poll was skipped, trigger it immediately now
*
* @returns {void}
*/
windowFocus: function (e) {
if( this._pollingEnabled && this._pollingPaused ){
Debug.log( 'Window focused...' );
this._pollingPaused = false;
if( this._pollOnUnpaused ){
this._pollOnUnpaused = false;
this.pollForNewReplies();
}
}
},
/**
* Set a timeout for the new post polling process
*
* @returns {void}
*/
_startPolling: function () {
var self = this;
this._pollingActive = true;
Debug.log('Starting polling with interval ' + ( this._currentPoll / 1000 ) + 's' );
this._pollingTimeout = setTimeout( function (){
self.pollForNewReplies();
}, this._currentPoll );
},
/**
* Clear the new post poll timeout
*
* @returns {void}
*/
_stopPolling: function () {
this._pollingActive = false;
if( this._pollingTimeout ){
clearTimeout( this._pollingTimeout );
}
},
/**
* Checks for new replies since we opened the page
*
* @param {event} e Event object
* @returns {void}
*/
pollForNewReplies: function () {
var self = this;
var replyForm = this.scope.find('[data-role="replyArea"] form');
var commentsOnThisPage = this.scope.find('[data-commentid]');
if( !commentsOnThisPage.length ) {
return;
}
var lastSeenId = $( commentsOnThisPage[ commentsOnThisPage.length - 1 ] ).attr('data-commentId');
if( this._pollingPaused ){
Debug.log('Window blurred, delaying poll until focused...');
this._pollOnUnpaused = true;
return;
}
// Abort any running ajax
if( this._pollAjax && !_.isUndefined( this._pollAjax.abort ) ){
this._pollAjax.abort();
}
this._pollAjax = ips.getAjax();
this._pollAjax( replyForm.attr('action'), {
dataType: 'json',
data: 'do=checkForNewReplies&type=count&lastSeenID=' + lastSeenId,
type: 'post'
})
.done( function (response) {
// If auto-polling is now disabled, stop everything
if( response.error && response.error == 'auto_polling_disabled' ){
self._stopPolling();
return;
}
if( parseInt( response.count ) > 0 ) {
// Reset the poll interval
self._currentPoll = self._initialPoll;
self._buildFlashMsg( response );
if( parseInt( response.totalCount ) > parseInt( self._lastSeenTotal ) ){
self._buildNotifications( response );
self._playNotification();
self._lastSeenTotal = parseInt( response.totalCount );
}
} else {
// Add 20 seconds to the poll interval, up to a max of 5 minutes
if( ( self._currentPoll + self._decay ) < self._maxInterval ){
self._currentPoll += self._decay;
} else {
self._currentPoll = self._maxInterval;
}
}
// Start again if we're on the last page
if( !_.isUndefined( self.scope.attr('data-lastPage') ) ){
self._startPolling();
}
});
},
/**
* Builds a flash message to alert user of new posts
*
* @param {object} response Information object
* @returns {void}
*/
_buildFlashMsg: function (response) {
var html = '';
var itemsInFeed = this.scope.find('[data-commentid]').length;
var spaceForMore = ( parseInt( response.perPage ) - itemsInFeed );
// Build our flash message HTML
if( parseInt( response.count ) > spaceForMore ) {
html = ips.templates.render( 'core.postNotify.multipleSpillOver', {
text: ips.pluralize( ips.getString( 'newPostMultipleSpillOver' ), [ response.totalNewCount ] ),
canLoadNew: ( spaceForMore > 0 ),
showFirstX: ips.pluralize( ips.getString( 'showFirstX' ), [ response.count ] ),
spillOverUrl: response.spillOverUrl
});
} else if( parseInt( response.count ) === 1 && !_.isUndefined( response.photo ) && !_.isUndefined( response.name ) ){
html = ips.templates.render( 'core.postNotify.single', {
photo: response.photo,
text: ips.getString( 'newPostSingle', { name: response.name } )
});
} else {
html = ips.templates.render( 'core.postNotify.multiple', {
text: ips.pluralize( ips.getString( 'newPostMultiple' ), [ response.count ] )
});
}
if( $('#elFlashMessage').is(':visible') && $('#elFlashMessage').find('[data-role="newPostNotification"]').length ){
$('#elFlashMessage').find('[data-role="newPostNotification"]').replaceWith( html );
} else {
ips.ui.flashMsg.show(
html,
{ sticky: true, position: 'bottom', extraClasses: 'cPostFlash ipsPad_half' }
);
}
},
/**
* Builds browser notifications to alert users of new posts
*
* @param {object} response Information object
* @returns {void}
*/
_buildNotifications: function (response) {
var self = this;
var hiddenProp = ips.utils.events.getVisibilityProp();
// Build our browser notification if the window isn't in focus *and* we support them
if( _.isUndefined( hiddenProp ) || !document[ hiddenProp ] || !ips.utils.notification.supported ){
return;
}
var notifyData = {
onClick: function (e) {
// Try and focus the window (security settings may prevent it, though)
try {
window.focus();
} catch (err) {}
// And load in those posts
self.loadNewPosts( e );
}
};
// If we already have a notification, then hide it
if( self._notification ){
self._notification.hide();
}
if( parseInt( response.count ) === 1 && !_.isUndefined( response.photo ) && !_.isUndefined( response.name ) ){
notifyData = _.extend( notifyData, {
title: ips.getString('notificationNewPostSingleTitle', {
name: response.name
}),
body: ips.getString('notificationNewPostSingleBody', {
name: response.name,
title: response.title
}),
icon: response.photo
});
} else {
notifyData = _.extend( notifyData, {
title: ips.pluralize( ips.getString('notificationNewPostMultipleTitle'), [ response.count ] ),
body: ips.pluralize( ips.getString('notificationNewPostMultipleBody', {
title: response.title
}), [ response.count ] )
});
}
// Create the new one
self._notification = ips.utils.notification.create( notifyData );
self._notification.show();
},
/**
* Play our notification sound
*
* @param {event} e Event object
* @returns {void}
*/
_playNotification: function () {
if( !ips.getSetting('disableNotificationSounds') ){
ips.loader.get( ['core/interface/buzz/buzz.min.js'] ).then( function () {
var sound = new buzz.sound( ips.getSetting('baseURL') + 'applications/core/interface/sounds/notification', {
formats: [ "mp3" ]
});
sound.play();
});
}
},
/**
* Adds new replies to the display
*
* @param {event} e Event object
* @returns {void}
*/
_importNewReplies: function () {
var form = this.scope.find('[data-role="replyArea"] form');
var commentsOnThisPage = this.scope.find('[data-commentid]');
var _lastSeenID = $( commentsOnThisPage[ commentsOnThisPage.length - 1 ] ).attr('data-commentId');
var self = this;
ips.getAjax()( form.attr('action'), {
data: 'do=checkForNewReplies&type=fetch&lastSeenID=' + _lastSeenID + '&showing=' + commentsOnThisPage.length,
type: 'post'
}).done( function (response) {
if( _.isArray( response.content ) ) {
_.each( response.content, function (item) {
self.trigger( 'addToCommentFeed', {
content: item,
feedID: self._commentFeedID,
resetEditor: false,
totalItems: response.totalCount
});
});
} else {
self.trigger( 'addToCommentFeed', {
content: response.content,
feedID: self._commentFeedID,
resetEditor: false,
totalItems: response.totalCount
});
}
self._clearNotifications();
});
},
/**
* Close the flash message for new post notifications
*
* @returns {void}
*/
_clearNotifications: function () {
if( this._notification && _.isFunction( this._notification.hide() ) ){
this._notification.hide();
}
if( $('#elFlashMessage').find('[data-role="newPostNotification"]').length ){
$('#elFlashMessage').find('[data-role="newPostNotification"]').trigger('closeFlashMsg.flashMsg');
}
},
/**
* Handles quick-reply functionality for this comment feed. Post the content via ajax,
* and trigger events to handle showing the new post (or redirecting to a new page)
*
* @param {event} e Event object
* @returns {void}
*/
quickReply: function (e) {
var form = this.scope.find('[data-role="replyArea"] form');
if ( form.attr('data-noAjax') ) {
return;
}
e.preventDefault();
e.stopPropagation();
var self = this;
var replyArea = this.scope.find('[data-role="replyArea"]');
var submit = form.find('[type="submit"]');
var autoFollow = this.scope.find('input[name$="auto_follow_checkbox"]');
// Set the form to loading
var initialText = submit.text();
submit
.prop( 'disabled', true )
.text( ips.getString('saving') );
var page = ips.utils.url.getParam( this._pageParam );
if( !page ){
page = 1;
}
ips.getAjax()( form.attr('action'), {
data: form.serialize() + '¤tPage=' + page,
type: 'post'
})
.done( function (response) {
if ( response.type == 'error' ) {
if ( response.form ) {
form.replaceWith( $(response.form) );
$( document ).trigger( 'contentChange', [ self.scope ] );
} else {
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: response.message,
callbacks: {}
});
}
}
else if( response.type == 'redirect' ) {
self.paginationClick( e, {
href: response.url,
originalEvent: e
});
} else if( response.type == 'merge' ) {
var comment = self.scope.find('[data-commentid="' + response.id + '"]');
comment.find('[data-role="commentContent"]').html( response.content );
if( comment.find('pre.prettyprint').length ){
comment.find('pre.prettyprint').each( function () {
$( this ).html( window.PR.prettyPrintOne( _.escape( $( this ).text() ) ) );
});
}
ips.ui.editor.getObj( self.scope.find('[data-role="replyArea"] [data-ipsEditor]') ).reset();
form.find("[data-role='commentFormError']").each(function() {
$( this ).remove();
});
var container = comment.closest('.ipsComment');
if ( container.length ) {
ips.utils.anim.go( 'pulseOnce', container );
} else {
ips.utils.anim.go( 'pulseOnce', comment );
}
ips.ui.flashMsg.show( ips.getString('mergedConncurrentPosts') );
$( document ).trigger( 'contentChange', [ self.scope ] );
} else {
self.trigger( 'addToCommentFeed', {
content: response.content,
totalItems: response.total,
feedID: self._commentFeedID
});
form.find("[data-role='commentFormError']").each(function() {
$( this ).remove();
});
// If the user is following this item, we can update the follow button too
if( autoFollow.length ){
self.trigger( 'followingItem', {
feedID: self.scope.attr('data-feedID'),
following: autoFollow.is(':checked')
});
}
}
self._clearNotifications();
})
.fail( function () {
form.attr('data-noAjax', 'true');
form.submit();
})
.always( function () {
submit
.prop( 'disabled', false )
.text( initialText ? initialText : ips.getString('submit_reply') );
});
},
/**
* Event handler for the 'load new posts' link in flash messages
*
* @param {event} e Event object
* @returns {void}
*/
loadNewPosts: function (e) {
e.preventDefault();
this._importNewReplies();
},
/**
* Responds to an event (trigger within this controller) indicating a new comment has been added
* Show it, and reset the contents of ckeditor
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
addToCommentFeed: function (e, data) {
if( !data.content || data.feedID != this._commentFeedID ){
return;
}
var textarea = this.scope.find('[data-role="replyArea"] textarea');
var content = $('').append( data.content );
var comment = content.find('.ipsComment');
var commentFeed = this.scope.find('[data-role="commentFeed"]');
if( commentFeed.find('[data-role="moderationTools"]').length ){
commentFeed = commentFeed.find('[data-role="moderationTools"]');
}
// Hide the 'no comment' text
this.scope.find('[data-role="noComments"]').remove();
// Add comment content
commentFeed.append( comment );
// Do we need to syntax highlight anything?
if( comment.find('pre.prettyprint').length ){
comment.find('pre.prettyprint').each( function () {
$( this ).html( window.PR.prettyPrintOne( _.escape( $( this ).text() ) ) );
});
}
ips.utils.anim.go( 'fadeInDown', comment );
if( _.isUndefined( data.resetEditor ) || data.resetEditor !== false ){
ips.ui.editor.getObj( this.scope.find('[data-role="replyArea"] [data-ipsEditor]') ).reset();
}
if( ips.utils.db.isEnabled() ){
var buttons = comment.find('[data-action="multiQuoteComment"]');
buttons.hide().removeClass('ipsHide');
ips.utils.anim.go('fadeIn', buttons);
}
// Open external links in a new window
if( ips.getSetting('links_external') ) {
this.scope.find('a[rel*="external"]').each( function( index, elem ){
elem.target = "_blank";
})
}
this._updateCount(data.totalItems);
$( document ).trigger( 'contentChange', [ this.scope ] );
},
/**
* Responds to an event indicating thay a comment has been deleted
* Show it, and reset the contents of ckeditor
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
deletedComment: function( e, data ) {
data = $.parseJSON( data.response );
var self = this;
if( data.type == 'redirect' ) {
window.location = data.url;
}
else {
this._updateCount( data.total );
}
},
/**
* Update comment count
*
* @param {int} newTotal The new total
* @returns {void}
*/
_updateCount: function(newTotal) {
if ( this.scope.find('[data-role="comment_count"]') ) {
var langString = 'js_num_comments';
if ( this.scope.find('[data-role="comment_count"]').attr('data-commentCountString') ) {
langString = this.scope.find('[data-role="comment_count"]').attr('data-commentCountString');
}
this.scope.find('[data-role="comment_count"]').text( ips.pluralize( ips.getString( langString ), newTotal ) );
}
},
/**
* Event handler for the 'Quote x posts' button in multiquote popup
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
doMultiQuote: function (e) {
var mqData = this._getMultiQuoteData();
var replyArea = this.scope.find('[data-role="replyArea"]');
var editor = ips.ui.editor.getObj( this.scope.find('[data-role="replyArea"] [data-ipsEditor]') );
var output = [];
var self = this;
if( !_.size( mqData ) || !replyArea.is(':visible') ){
return;
}
// Build quote array and trigger event for the editor widget to deal with
_.each( mqData, function (value){
output.push( value );
});
if( editor ){
editor.insertQuotes( output );
}
this._removeAllMultiQuoted();
},
/**
* Event handler for the 'clear' button in the multiquote popup
* Simply calls _removeAllMultiQuoted to do the clear
*
* @param {event} e Event object
* @returns {void}
*/
clearMultiQuote: function (e) {
e.preventDefault();
this._removeAllMultiQuoted();
},
/**
* Removes all quoted posts
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
_removeAllMultiQuoted: function () {
var mqData = this._getMultiQuoteData();
var self = this;
// Delete all the multi-quoted posts from DB
ips.utils.db.set( 'mq', 'data', {} );
// Hide popup
this._buildMultiQuote(0);
if( !_.size( mqData ) ){
return;
}
// Loop through each quoted posts and see if it exists on this page by building a selector,
// then updating classnames on elements that match it
_.each( mqData, function (value) {
self.triggerOn( 'core.front.core.comment', 'setMultiQuoteDisabled.comment', {
contentapp: value.contentapp,
contenttype: value.contenttype,
contentcommentid: value.contentcommentid
});
});
},
/**
* Responds to an addMultiQuote event
* Adds the provided post data to the multiquote DB entry and updates the popup
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
addMultiQuote: function (e, data) {
var mqData = this._getMultiQuoteData();
var key = data.contentapp + '-' + data.contenttype + '-' + data.contentcommentid;
// Have we hit a limit?
if( _.size( mqData ) == this._maximumMultiQuote )
{
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.pluralize( ips.getString( 'maxmultiquote' ), this._maximumMultiQuote ),
callbacks: {
ok: function () {
$("button[data-mqId='" + data.button + "']").removeClass('ipsButton_alternate')
.addClass('ipsButton_simple')
.removeAttr('data-mqActive')
.html( ips.templates.render('core.posts.multiQuoteOff') );
}
}
});
return false;
}
mqData[ key ] = data;
ips.utils.db.set( 'mq', 'data', mqData );
this._buildMultiQuote( _.size( mqData ) );
},
/**
* Responds to a removeMultiQuote event
* Removes the provided post data from the multiquote DB entry and updates the popup
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
removeMultiQuote: function (e, data) {
var mqData = this._getMultiQuoteData();
var key = data.contentapp + '-' + data.contenttype + '-' + data.contentcommentid;
if( !_.isUndefined( mqData[ key ] ) ){
mqData = _.omit( mqData, key );
ips.utils.db.set( 'mq', 'data', mqData );
this._buildMultiQuote( _.size( mqData ) );
}
},
/**
* Returns the current multiquote data from the localStorage
*
* @returns {object} Multiquote data from localStorage
*/
_getMultiQuoteData: function () {
// Get the IDs we already have saved
var mqData = ips.utils.db.get('mq', 'data');
if( _.isUndefined( mqData ) || !_.isObject( mqData ) ){
return {};
}
return mqData;
},
/**
* Called when the controller is initialized
* Checks whether there's any mq data, and shows the popup if so
*
* @returns {void}
*/
_setUpMultiQuote: function () {
if( !ips.utils.db.isEnabled() ){
return;
}
var buttons = this.scope.find('[data-action="multiQuoteComment"]');
var self = this;
var mqData = this._getMultiQuoteData();
buttons.show();
if( _.size( mqData ) ){
this._buildMultiQuote( _.size( mqData ) );
// Loop through each quoted posts and see if it exists on this page by building a selector,
// then updating classnames on elements that match it
_.each( mqData, function (value) {
self.triggerOn( 'core.front.core.comment', 'setMultiQuoteEnabled.comment', {
contentapp: value.contentapp,
contenttype: value.contenttype,
contentcommentid: value.contentcommentid
});
});
}
},
/**
* Builds the multiquote popup, either from a template if this is the first time,
* or updates the value if it already exists.
*
* @param {number} count Count of quoted posts
* @returns {void}
*/
_buildMultiQuote: function (count) {
var quoterElem = $('#ipsMultiQuoter');
if( !quoterElem.length && count ){
ips.getContainer().append( ips.templates.render('core.posts.multiQuoter', {
count: ips.getString('multiquote_count', {
count: ips.pluralize( ips.getString( 'multiquote_count_plural' ), [ count ] )
})
}));
ips.utils.anim.go( 'zoomIn fast', $('#ipsMultiQuoter') );
} else {
quoterElem.find('[data-role="quotingTotal"]').text(
ips.pluralize( ips.getString( 'multiquote_count_plural' ), [ count ] )
);
if( count && quoterElem.is(':visible') ){
ips.utils.anim.go( 'pulseOnce fast', quoterElem );
} else if( count && !quoterElem.is(':visible') ){
ips.utils.anim.go( 'zoomIn fast', quoterElem );
} else {
ips.utils.anim.go( 'zoomOut fast', quoterElem );
}
}
},
/**
* Shows a hidden post
*
* @param {event} e Event object from the event handler
* @param {object} data Event data object from the event handler
* @returns {void}
*/
_showHiddenPost: function (e, data) {
// Hide the ignore row
var ignoreRow = $( data.triggerElem ).closest('.ipsComment_ignored');
var commentID = ignoreRow.attr('data-ignoreCommentID');
var comment = this.scope.find( '#' + commentID );
ignoreRow.remove();
comment.removeClass('ipsHide');
},
/**
* Stops ignoring posts by a user
*
* @param {event} e Event object from the event handler
* @param {object} data Event data object from the event handler
* @returns {void}
*/
_stopIgnoringFromComment: function (e, data) {
var ignoreRow = $( data.triggerElem ).closest('.ipsComment_ignored');
var userID = ignoreRow.attr('data-ignoreUserID');
var self = this;
var posts = this.scope.find('[data-ignoreUserID="' + userID + '"]');
posts.each( function () {
self.scope.find( '#' + $( this ).attr('data-ignoreCommentID') ).removeClass('ipsHide');
$( this ).remove();
});
var url = ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=ignore&do=ignoreType&type=topics&off=1';
ips.getAjax()( url, {
data: {
member_id: parseInt( userID )
}
})
.done( function () {
ips.ui.flashMsg.show( ips.getString('ignore_prefs_updated') );
})
.fail( function () {
window.location = ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=ignore&do=ignoreType&off=1type=topics&member_id=' + userID;
});
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.commentWrapper.js
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.commentsWrapper', {
initialize: function () {
this.on( document, 'addToCommentFeed', this.addToCommentFeed );
this.on( 'deletedComment.comment', this.deletedComment );
},
/**
* Responds to an event (trigger within this controller) indicating a new comment has been added
* Show it, and reset the contents of ckeditor
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
addToCommentFeed: function(e, data) {
this._updateCount( $(e.target).attr('data-commentsType'), data.totalItems );
},
/**
* Responds to an event indicating thay a comment has been deleted
* Show it, and reset the contents of ckeditor
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
deletedComment: function(e, data) {
this._updateCount( $(e.target).closest('[data-commentsType]').attr('data-commentsType'), data.newTotal );
},
/**
* Update comment count
*
* @param {int} newTotal The new total
* @returns {void}
*/
_updateCount: function( type, number ) {
var langString = 'js_num_' + type;
var elem = $( '#' + $(this.scope).attr('data-tabsId') + '_tab_' + type );
elem.text( ips.pluralize( ips.getString( langString ), number ) );
}
});
}(jQuery, _)); 0 ) {
this._image.css({ top: 0, bottom: 'auto', position: 'absolute' });
} else {
var containerHeight = this.scope.find('.ipsCoverPhoto_container').outerHeight();
var imageHeight = this._image.outerHeight();
if( ( imageTop + imageHeight ) < containerHeight ){
this._image.css({ top: 'auto', bottom: 0, position: 'absolute' });
}
}
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.followButton.js - Controller for follow button
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.followForm', {
initialize: function () {
this.on( 'submit', this.submitForm );
this.on( 'click', '[data-action="unfollow"]', this.unfollow );
this.setup();
},
/**
* Setup method
*
* @returns {void}
*/
setup: function () {
this._app = this.scope.attr('data-followApp');
this._area = this.scope.attr('data-followArea');
this._id = this.scope.attr('data-followID');
},
/**
* Event handler for unfollowing an item
*
* @param {event} e Event object
* @returns {void}
*/
unfollow: function (e) {
e.preventDefault();
this._doFollowAction( $( e.currentTarget ).attr('href'), {}, true );
},
/**
* Event handler for submitting the follow form
*
* @param {event} e Event object
* @returns {void}
*/
submitForm: function (e) {
e.preventDefault();
this._doFollowAction( this.scope.attr('action'), this.scope.serialize(), false );
},
/**
* Performs an ajax action. Shows the hovercard as loading, and calls the URL
*
* @param {string} url URL to call
* @param {object} data Object of data to include in the request
* @returns {void}
*/
_doFollowAction: function (url, data, unfollow) {
var self = this;
var dims = ips.utils.position.getElemDims( this.scope.parent('div') );
// Set it to loading
this.scope
.hide()
.parent('div')
.css({
width: dims.outerWidth + 'px',
height: dims.outerHeight + 'px'
})
.addClass('ipsLoading');
// Update follow preference via ajax
ips.getAjax()( url, {
data: data,
type: 'post'
})
.done( function (response) {
// Success, so trigger event to update button
if( unfollow ){
self.trigger('followingItem', {
feedID: self._area + '-' + self._id,
unfollow: true
});
} else {
self.trigger('followingItem', {
feedID: self._area + '-' + self._id,
notificationType: self.scope.find('[name="follow_type"]:checked').val(),
anonymous: !self.scope.find('[name="follow_public_checkbox"]').is(':checked')
});
}
ips.ui.flashMsg.show( ips.getString('followUpdated') );
})
.fail( function (jqXHR, textStatus, errorThrown) {
window.location = url;
})
.always( function () {
// If we're in a hovercard, remove it
self.scope.parents('.ipsHovercard').remove();
});
}
});
}(jQuery, _));
20s ago.
* If it was, it will fire an ajax request to get any new notifcations, and presnt those to the user. It will
* then store this as the latest result in localStorage. We use localStorage so that multiple browser tabs
* aren't all doing their own poll.
* When the user first loads the page, we'll also compare the current message/notification count to what's in
* localStorage. If there's new notifications, we'll fetch them and show them, to make it feel 'instanty'.
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.instantNotifications', {
_pollTimeout: 20, // Seconds
_windowInactivePoll: 0,
_pollMultiplier: 1, // multiplier for decay
_messagesEnabled: null,
_ajaxObj: null,
_debugPolling: true,
_browserNotifications: {},
_paused: false,
_interval: null,
initialize: function () {
this.on( document, ips.utils.events.getVisibilityEvent(), this.windowVisibilityChange );
this.on( window, 'storage', this.storageChange );
this.setup();
},
/**
* Setup method
*
* @returns {void}
*/
setup: function () {
if( !ips.utils.db.isEnabled() || !_.isFunction( JSON.parse ) ){
Debug.warn("Sorry, your browser doesn't support localStorage or JSON so we can't load instant notifications for you.");
return;
}
this._messagesEnabled = this.scope.find('[data-notificationType="inbox"]').length;
this._setInterval( this._pollTimeout);
this._doInitialCheck();
},
/**
* Responds to window storage event so we can update instantly if any other tab
* changes our data
*
* @param {event} e Event object
* @returns {void}
*/
storageChange: function (e) {
var event = e.originalEvent;
if( event.key !== 'notifications.' + ips.getSetting('baseURL') ){
return;
}
if( this._debugPolling ){
Debug.log('Notifications: updating instantly from storage event');
}
try {
var data = JSON.parse( event.newValue );
var counts = this._getCurrentCounts();
this._updateIcons( {
messages: parseInt( data.messages ),
notifications: parseInt( data.notifications )
}, counts );
} catch(err) {}
},
/**
* Handles window visibiliy changes; removes count from title bar
*
* @returns {void}
*/
windowVisibilityChange: function () {
var hiddenProp = ips.utils.events.getVisibilityProp();
if( !_.isUndefined( hiddenProp ) && !document[ hiddenProp] ){
// Document is now in focus
this._updateBrowserTitle( 0 );
this._pollMultiplier = 1;
this._windowInactivePoll = 0;
if( this._paused ){
document.title = document.title.replace( "❚❚ ", '' );
this._checkNotifications(); // Do an immediate check
this._setInterval( this._pollTimeout );
}
if( this._debugPolling ){
Debug.log( "Notifications: Resetting inactive poll.");
}
}
},
/**
* Handles setting up our interval poll
*
* @param {number} timeoutInSecs Seconds between polls
* @returns {void}
*/
_setInterval: function (timeoutInSecs) {
clearInterval( this._interval );
this._interval = setInterval( _.bind( this._checkNotifications, this ), timeoutInSecs * 1000 );
},
/**
* On page load, does an initial check to see if we need to call the server
*
* @returns {void}
*/
_doInitialCheck: function () {
// Fetch the latest poll from localStorage
var storage = ips.utils.db.get( 'notifications', ips.getSetting('baseURL') );
var counts = this._getCurrentCounts();
if( !storage || !_.isObject( storage ) ){
return;
}
// If our bubble is reporting more notifications or messages than we have in storage,
// we'll fetch them immediately
if( ( this._messagesEnabled && counts.messages > storage.messages ) || counts.notifications > storage.notifications ){
if( this._debugPolling ){
Debug.log("Notifications: bubbles reporting higher counts for notifications or messages.");
}
var dataToSend = {
notifications: storage.notifications
};
if( this._messagesEnabled ){
dataToSend = _.extend( dataToSend, {
messages: storage.messages
});
}
this._doAjaxRequest( dataToSend );
}
},
/**
* The main method to check notification status
* Checks in localstorage to see if the last poll was < 20s ago
*
* @returns {void}
*/
_checkNotifications: function () {
// Fetch the latest poll from localStorage
var storage = ips.utils.db.get( 'notifications', ips.getSetting('baseURL') );
var timestamp = ips.utils.time.timestamp();
var counts = this._getCurrentCounts();
var currentTimeout = this._pollTimeout * this._pollMultiplier;
// If our window is inactive, increase the count
if( document[ ips.utils.events.getVisibilityProp() ] ){
if( this._windowInactivePoll >= 45 && this._pollMultiplier === 1 ){ // 15 minutes @ 20s poll
if( this._debugPolling ){
Debug.log( "Notifications: Polled over 15 minutes, increasing multiplier to 2");
}
this._pollMultiplier = 2;
this._setInterval( this._pollTimeout * this._pollMultiplier );
} else if( this._windowInactivePoll >= 113 && this._pollMultiplier === 2 ) { // 15 minutes @ 20s poll, 45 minutes at 40s poll
if( this._debugPolling ){
Debug.log( "Notifications: Polled over 60 minutes, increasing multiplier to 3");
}
this._pollMultiplier = 3;
this._setInterval( this._pollTimeout * this._pollMultiplier );
} else if( this._windowInactivePoll >= 233 && this._pollMultiplier === 3 ) { // 15 minutes @ 20s, 45 minutes at 40s poll, 2 hours at 60s poll
if( this._debugPolling ){
Debug.log( "Notifications: Polled over 3 hours, stopping polling");
}
this._stopPolling();
return;
}
this._windowInactivePoll++;
}
// Do we need to poll?
// the -1 in the below logic gives us a little fuzziness to account for the delay in processing the script
if( ( storage && _.isObject( storage ) ) && parseInt( storage.timestamp ) > ( timestamp - ( ( currentTimeout - 1 ) * 1000 ) ) ){
// We *don't* need to poll, it has been less than 20s
this._updateIcons( storage, counts );
if( this._debugPolling ){
Debug.log("Notifications: fetching from localStorage");
}
} else {
// We send our currently-displayed bubble count to the backend
// to find out if there's any change in number
var dataToSend = {
notifications: counts.notifications
};
if( this._messagesEnabled ){
dataToSend = _.extend( dataToSend, {
messages: counts.messages
});
}
this._doAjaxRequest( dataToSend );
}
},
/**
* Calls the backend to get new notification data
*
* @param {object} dataToSend Object containing current message and notification counts
* @returns {void}
*/
_doAjaxRequest: function (dataToSend) {
var self = this;
var url = '?app=core&module=system&controller=ajax&do=instantNotifications';
if( this._debugPolling ){
Debug.log("Notifications: sending ajax request");
}
// We'll update the timestamp before polling so that other windows
// don't start polling before this one is finished
this._updateTimestamp();
// We do need to poll, it's been more than 20s
this._ajaxObj = ips.getAjax()( url, {
data: dataToSend
})
.done( _.bind( this._handleResponse, this ) )
.fail( function () {
self._stopPolling(true);
Debug.error("Problem polling for new notifications; stopping.");
});
},
/**
* Processes an ajax response
*
* @param {object} response Server response
* @returns {void}
*/
_handleResponse: function (response) {
// If auto-polling is now disabled, stop everything
if( response.error && response.error == 'auto_polling_disabled' ){
self._stopPolling( true );
return;
}
var counts = this._getCurrentCounts();
if( response.notifications.count > counts.notifications && this._debugPolling ){
Debug.log("Notifications: I'm the winner! I found there's " + response.notifications.count + " new notifications");
}
this._updateIcons( {
messages: response.messages.count,
notifications: response.notifications.count
}, counts );
// Update localStorage with the new count
ips.utils.db.set( 'notifications', ips.getSetting('baseURL'), {
timestamp: ips.utils.time.timestamp(),
messages: response.messages.count,
notifications: response.notifications.count
});
var total = response.messages.data.length + response.notifications.data.length;
// How many NOTIFICATIONS do we have to show?
if( response.notifications.data.length ){
this._showNotification( this._buildNotifyData( response.notifications.data, 'notification' ), 'notification' );
}
// How many MESSAGES do we have to show?
if( response.messages.data.length ){
this._showNotification( this._buildNotifyData( response.messages.data, 'message' ), 'message' );
}
// Do we need to play a sound?
if( total > 0 ){
if( !ips.getSetting('disableNotificationSounds') ){
ips.loader.get( ['core/interface/buzz/buzz.min.js'] ).then( function () {
var sound = new buzz.sound( ips.getSetting('baseURL') + 'applications/core/interface/sounds/notification', {
formats: [ "mp3" ]
});
sound.play();
});
}
// Do we need to update the browser title?
if( document[ ips.utils.events.getVisibilityProp() ] ){
this._updateBrowserTitle( total );
}
}
},
/**
* Updates the browser title bar with a new count (or removes it if 0)
*
* @param {number} count Count to show in the browser title bar
* @returns {void}
*/
_updateBrowserTitle: function (count) {
var cleanTitle = $.trim( document.title.replace( /^\(\d+\)/, '' ) );
if( count ){
document.title = "(" + count + ") " + cleanTitle;
} else {
document.title = cleanTitle;
}
},
/**
* Builds notification data for the given items based on type
*
* @param {array} items Array of items from the backend
* @param {string} type Type of notification being build (message or notification)
* @returns {object} Object of notification data
*/
_buildNotifyData: function (items, type) {
var self = this;
var notifyData = {
count: items.length
};
if( items.length === 1 ){
notifyData = _.extend( notifyData, {
title: ips.getString( type + 'GeneralSingle'),
icon: items[0].author_photo,
body: items[0].title,
url: items[0].url,
onClick: function () {
// Try and focus the window (security settings may prevent it, though)
try {
window.focus();
} catch (err) {}
window.location = items[0].url;
}
});
} else {
notifyData = _.extend( notifyData, {
title: ips.pluralize( ips.getString( type + 'GeneralMultiple'), [ items.length ] ),
body: items[0].title,
icon: ips.getSetting('imgURL') + '/notifyIcons/' + type + '.png',
onClick: function () {
// Try and focus the window (security settings may prevent it, though)
try {
window.focus();
} catch (err) {}
self._getIcon( ( type == 'message' ) ? 'inbox' : 'notify' ).click();
}
});
}
return notifyData;
},
/**
* Determines which is the appropriate notification method to use to let the user know about new data
*
* @param {object} notifyData Notification data to use when building the notification
* @param {string} type Type of notification being build (message or notification)
* @returns {void}
*/
_showNotification: function (notifyData, type) {
if( document[ ips.utils.events.getVisibilityProp() ] && ips.utils.notification.supported ){
// When the window is INACTIVE (and we support HTML5 notifications)
// Show a browser notification & play a sound
this._showBrowserPopup( notifyData, type );
} else {
// When the window is ACTIVE
// Show a flash message
this._showFlashMessage( notifyData, type );
}
},
/**
* Shows a HTML5 browser popup notification
*
* @param {object} notifyData Object containing notification data
* @param {string} type Type of notification (notification or message)
* @returns {void}
*/
_showBrowserPopup: function (notifyData, type) {
notifyData = _.extend( notifyData, {
timeout: 15
});
// Try and hide any existing popup for this type (even though it'll be on a timeout)
if( this._browserNotifications[ type ] ){
try {
this._browserNotifications[ type ].hide();
} catch (err) {}
}
// Create the new one
this._browserNotifications[ type ] = ips.utils.notification.create( notifyData );
this._browserNotifications[ type ].show();
},
/**
* Shows a flash message at the bottom of the user's window
*
* @param {object} notifyData Object containing notification data
* @param {string} type Type of notification (notification or message)
* @returns {void}
*/
_showFlashMessage: function (notifyData, type) {
var html = '';
if( notifyData.count === 1 ){
notifyData = _.extend( notifyData, { text: ips.getString( type + 'FlashSingle') } );
html = ips.templates.render( 'core.notification.flashSingle', notifyData );
} else {
notifyData = _.extend( notifyData, { text: ips.pluralize( ips.getString( type + 'FlashMultiple'), [ notifyData.count ] ) } );
html = ips.templates.render( 'core.notification.flashMultiple', notifyData );
}
if( $('#elFlashMessage').is(':visible') && $('#elFlashMessage').find('[data-role="newNotification"]').length ){
$('#elFlashMessage').find('[data-role="newNotification"]').replaceWith( html );
} else {
ips.ui.flashMsg.show( html, {
timeout: 8,
position: 'bottom',
extraClasses: 'cNotifcationFlash ipsPad_half'
});
}
},
/**
* Updates our storage timestamp to now
*
* @returns {void}
*/
_updateTimestamp: function () {
var storage = ips.utils.db.get( 'notifications', ips.getSetting('baseURL') );
storage = _.extend( storage, {
timestamp: ips.utils.time.timestamp()
});
ips.utils.db.set( 'notifications', ips.getSetting('baseURL'), storage );
},
/**
* Updates the bubble on both icons if the count differs from what's alrady displayed
*
* @param {object} newData The latest counts (either from storage or ajax response)
* @param {object} oldData Existing counts from the bubbles
* @returns {void}
*/
_updateIcons: function (newData, oldData) {
if( this._messagesEnabled ){
if( parseInt( newData.messages ) !== oldData.messages ){
this._updateIcon( 'inbox', newData.messages );
}
}
if( parseInt( newData.notifications ) !== oldData.notifications ){
this._updateIcon( 'notify', newData.notifications );
}
},
/**
* Updates a bubble on an icon, and uses the appropriate animation to show it
*
* @param {string} type notify or inbox
* @param {number} count The new count to show
* @returns {void}
*/
_updateIcon: function (type, count) {
var icon = this._getIcon( type );
icon.attr( 'data-currentCount', count ).text( count );
if( parseInt( count ) ){
ips.utils.anim.go( ( !icon.is(':visible') ) ? 'zoomIn' : 'pulseOnce', icon.removeClass('ipsHide') );
} else {
icon.fadeOut();
}
},
/**
* Returns a reference to the icon of the given type
*
* @param {string} type notify or inbox
* @returns {element} jQuery element
*/
_getIcon: function (type) {
return this.scope.find('[data-notificationType="' + type + '"]');
},
/**
* Gets the current counts from the bubbles on-screen
*
* @returns {object} Contains two keys, messages & notifications, containing the currently-displayed counts
*/
_getCurrentCounts: function () {
var messages = this.scope.find('[data-notificationType="inbox"]');
var notifications = this.scope.find('[data-notificationType="notify"]');
return {
notifications: parseInt( notifications.attr('data-currentCount') ),
messages: ( messages.length ) ? parseInt( messages.attr('data-currentCount') ) : null
};
},
/**
* Stops our internal loop from polling for any more notifications
*
* @returns {void}
*/
_stopPolling: function (fatal) {
Debug.info("Stopping instant notification polling");
clearInterval( this._interval );
this._paused = true;
document.title = "❚❚ " + document.title.replace("❚❚ ", "");
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.license.js - License message
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.global.core.license', {
initialize: function () {
this.on( 'click', '[data-role="closeMessage"]', this.hideMessage );
},
hideMessage: function () {
var date = new Date();
date.setTime( date.getTime() + ( 14 * 86400000 ) );
ips.utils.cookie.set( 'licenseDismiss', true, date.toUTCString() );
this.scope.slideUp();
}
});
}(jQuery, _));= image.naturalWidth && !$( image ).hasClass('ipsImage_thumbnailed') ) {
return;
}
image = $( image );
image.addClass('ipsImage_thumbnailed');
// If the image is already inside an a, then just add the lightbox params; otherwise, wrap in new
if( image.closest('a').length && image.closest('a').hasClass('ipsAttachLink') && image.closest('a').hasClass('ipsAttachLink_image') ){
if ( [ 'gif', 'jpeg', 'jpe', 'jpg', 'png' ].indexOf( image.closest('a').attr('href').substr( image.closest('a').attr('href').lastIndexOf('.') + 1 ).toLowerCase() ) != -1 ) { // Only if the link is to an image
if( !ips.utils.responsive.enabled() || !ips.utils.responsive.currentIs('phone') ){ // And only if we're not on mobile
image.closest('a')
.attr( 'data-fullURL',image.closest('a').attr('src') )
.attr( 'data-ipsLightbox', '' )
.attr( 'data-ipsLightbox-group', self._random );
}
}
} else {
if( !image.closest('a').length ){
if( ips.utils.responsive.enabled() && ips.utils.responsive.currentIs('phone') ){
image.wrap( $( "" ) );
} else {
image.wrap( $( "" ) );
}
}
}
});
});
}
});
}(jQuery, _));]]>= 3 ){
this._showResults();
}
},
/**
* Event handler for field blur
*
* @param {event} e Event object
* @returns {void}
*/
fieldBlur: function (e) {
clearInterval( this._timers );
},
/**
* Event handler for clicking the modal
* We have to check this is our modal by comparing IDs. If it is, we close the results.
*
* @param {event} e Event object
* @returns {void}
*/
clickModal: function (e) {
if( this._modal == null || $( e.currentTarget ).attr('id') != this._modal.attr('id') ){
return;
}
this._hideResults();
},
/**
* Timer callback from this.fieldFocus
* Compares current value to previous value, and shows/loads new results if it's changed
*
* @returns {void}
*/
_timerFocus: function () {
var currentValue = $.trim( this._textField.val() );
if( currentValue == this._lastValue || $.trim( this._textField.val().length ) < 4 ){
return;
}
this._lastValue = currentValue;
this._showResults();
this._loadResults();
},
/**
* Hides the results and modal
*
* @returns {void}
*/
_hideResults: function () {
ips.utils.anim.go( 'fadeOut fast', this._modal );
ips.utils.anim.go( 'fadeOut', this.scope );
},
/**
* Shows the results panel and modal, setting the zIndex on them so they stay in order
*
* @returns {void}
*/
_showResults: function () {
if( !this._modal ){
this._buildModal();
}
// Set new z-indexes to keep everything in order
this._modal.css( { zIndex: ips.ui.zIndex() } );
$('#ipsLayout_header').css( { zIndex: ips.ui.zIndex() } );
this.scope.css( { zIndex: ips.ui.zIndex() } );
// Show the results and or modal
if( !this.scope.is(':visible') ){
ips.utils.anim.go( 'fadeIn fast', this.scope );
}
if( !this._modal.is(':visible') ){
ips.utils.anim.go( 'fadeIn fast', this._modal );
}
},
/**
* Load results from the server
*
* @returns {void}
*/
_loadResults: function () {
var self = this;
// Abort any requests running now
if( this._ajax ){
for( var i = 0; i < this._ajax.length; i++ ) {
if( _.isFunction( this._ajax[i].abort ) ) {
this._ajax[i].abort();
}
}
}
this.scope.find( '[data-ipsMenuValue] [data-role="resultCount"]' ).addClass('ipsLoading ipsPad_top').html(' ');
this.scope.find( '[data-ipsMenuValue] [data-role="resultCount"]' ).removeClass('ipsLoading_dark ipsSideMenu_clearCount');
this.scope.find( '[data-ipsMenuValue].ipsSideMenu_itemActive [data-role="resultCount"]' ).addClass('ipsLoading_dark ipsSideMenu_clearCount');
var value = $.trim( this._lastValue );
this.scope.find('[data-ipsMenuValue]').each( function () {
var tab = this;
var key = $( this ).attr('data-ipsMenuValue');
self._setPanelToLoading( key );
self._ajax[ key ] = ips.getAjax()('?app=core&module=system&controller=livesearch', {
dataType: 'json',
data: {
search_key: key,
search_term: encodeURI( value )
}
}).done( function (response) {
self._results[key] = response;
$( tab )
.find('[data-role="resultCount"]')
.removeClass('ipsLoading ipsPad_top ipsLoading_dark ipsSideMenu_clearCount')
.text( parseInt( response.length ) )
.end()
.toggleClass( 'ipsSideMenu_itemDisabled', ( response.length === 0 ) ? true : false );
if( $( tab ).attr('data-ipsMenuValue') == self._activePanel ){
self._showResultsInPanel( self._activePanel, response, true );
}
if( !self._searchMenu.find('[data-ipsMenuValue].ipsSideMenu_itemActive:not( .ipsSideMenu_itemDisabled )').length ) {
self._selectFirstResultsTab();
}
}).fail( function (err) {
// fail gets called when it's aborted, so deliberately do nothing here
});
});
},
/**
* Selects the first section that has some results to show
*
* @returns {void}
*/
_selectFirstResultsTab: function () {
var first = this._searchMenu.find('[data-ipsMenuValue]:not( .ipsSideMenu_itemDisabled )').first();
first.click();
},
/**
* Sets the given panel into loading state
*
* @returns {void}
*/
_setPanelToLoading: function (panel) {
var panelContainer = this._resultsContainer.find('[data-resultsSection="' + panel + '"]');
panelContainer.addClass('ipsLoading').find('> ol').hide();
},
/**
* Shows the results in the relevant panel, building it if it doesn't exist
*
* @param {string} panel Panel ID
* @param {object} results Results object
* @param {booolean} animate Animate the results being shown?
* @returns {void}
*/
_showResultsInPanel: function (panel, results, animate) {
// Hide all panels
this._resultsContainer.find('[data-resultsSection]').hide();
var panelContainer = this._resultsContainer.find('[data-resultsSection="' + panel + '"]');
var panelList = panelContainer.find('> ol');
// Build the container if needed
if( !panelContainer.length ){
this._buildResultsContainer( panel );
panelContainer = this._resultsContainer.find('[data-resultsSection="' + panel + '"]');
panelList = panelContainer.find('> ol');
}
panelContainer.removeClass('ipsLoading');
panelList.hide().html('');
// Any results to show?
if( _.isUndefined( results ) || _.isUndefined( results ) || _.size( results ) == 0 ){
// No results
panelList.html( ips.templates.render('core.livesearch.noResults') ).show();
return;
}
// Loop through each result to build it
_.each( results, function (val) {
panelList.append( val );
});
// Find all results
var resultItems = panelList.find('[data-role="result"]').hide();
var delay = 25;
panelList.show();
panelContainer.show();
if( animate == true ){
resultItems.each( function () {
var item = $( this );
setTimeout( function () {
ips.utils.anim.go( 'fadeIn fast', item );
}, delay);
delay += 25;
});
} else {
resultItems.show();
}
},
/**
* Builds a results container
*
* @param {string} panel Panel ID
* @returns {void}
*/
_buildResultsContainer: function (panel) {
this._resultsContainer.append(
$('')
.attr('data-resultsSection', panel )
.addClass('ipsScrollbar')
.append( $('')
.addClass('ipsList_reset')
)
);
},
/**
* Builds the modal element
*
* @returns {void}
*/
_buildModal: function () {
this._modal = ips.ui.getModal();
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.mobileNav.js - Mobile navigation controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.mobileNav', {
initialize: function () {
this.on( 'click', '[data-action="mobileSearch"]', this.mobileSearch );
},
/**
* Mobile search; simply adds a class to the body. CSS shows the search box.
*
* @param {event} e Event object
* @returns {void}
*/
mobileSearch: function (e) {
e.preventDefault();
$('body').toggleClass('cSearchOpen');
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.mobileNav.js - ACP mobile navigation
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.core.mobileNav', {
initialize: function () {
this.on( 'click', '[data-action="mobileSearch"]', this.mobileSearch );
},
/**
* Mobile search; simply adds a class to the body. CSS shows the search box.
*
* @param {event} e Event object
* @returns {void}
*/
mobileSearch: function (e) {
e.preventDefault();
if( $('body').hasClass('acpSearchOpen') ){
$('body').find('.ipsModal').trigger('click');
}
$('body').toggleClass('acpSearchOpen');
}
});
}(jQuery, _));').attr( { type: 'text' } ).attr( 'data-role', 'editTitleField' ).val( anchor.text().trim() );
anchor.after(inputNode);
inputNode.focus();
inputNode.on('blur', function(){
inputNode.addClass('ipsField_loading');
ips.getAjax()( anchor.attr('href'), { data: { do: 'ajaxEditTitle', newTitle: inputNode.val() } } )
.done(function(response){
anchor.text( inputNode.val() );
})
.fail(function(response){
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: response.responseJSON,
});
})
.always(function(){
inputNode.remove();
anchor.show();
self._editingTitle = false;
});
});
inputNode.on('keypress', function(e){
if( e.keyCode == ips.ui.key.ENTER ){
e.stopPropagation();
e.preventDefault();
inputNode.blur();
return false;
}
});
}, 1000);
},
/**
* Event handler called when the user clicks up an editable title
*
* @param {event} e Event object
* @returns {void}
*/
editTitleMouseup: function(e) {
clearTimeout( this._editTimeout );
},
/**
* Event handler called when the user clicks up an editable title
*
* @param {event} e Event object
* @returns {void}
*/
editTitleMouseclick: function(e) {
if ( this._editingTitle ) {
e.preventDefault();
}
},
/**
* Event handler called when the moderation bar submits
*
* @param {event} e Event object
* @returns {void}
*/
moderationSubmit: function (e) {
if ( this._editingTitle ) {
e.preventDefault();
}
var action = this.scope.find('[data-role="moderationAction"]').val();
switch (action) {
case 'delete':
this._modActionDelete(e);
break;
case 'move':
this._modActionDialog(e, 'move', 'narrow');
break;
case 'hide':
this._modActionDialog(e, 'hide', 'narrow');
break;
case 'split':
this._modActionDialog(e, 'split', 'wide');
break;
case 'merge':
this._modActionDialog(e, 'merge', 'narrow');
break;
default:
$( document ).trigger('moderationSubmitted');
break;
}
},
/**
* Handles a delete action from the moderation bar
*
* @param {event} e Event object
* @returns {void}
*/
_modActionDelete: function (e) {
var self = this;
var form = this.scope.find('[data-role="moderationTools"]');
if( self._bypassDeleteCheck ){
return;
}
e.preventDefault();
// How many are we deleting?
var count = parseInt( this.scope.find('[data-role="moderation"]:checked').length );
ips.ui.alert.show( {
type: 'confirm',
icon: 'warn',
message: ( count > 1 ) ? ips.pluralize( ips.getString( 'delete_confirm_many' ), count ) : ips.getString('delete_confirm'),
callbacks: {
ok: function () {
$( document ).trigger('moderationSubmitted');
self._bypassDeleteCheck = true;
self.scope.find('[data-role="moderationTools"]').submit();
}
}
});
},
/**
* Handles a move/split action from the moderation bar
*
* @param {event} e Event object
* @returns {void}
*/
_modActionDialog: function (e, title, size) {
e.preventDefault();
var form = this.scope.find('[data-role="moderationTools"]');
// Create dialog to show the form
var moveDialog = ips.ui.dialog.create({
url: form.attr('action') + '&' + form.serialize().replace( /%5B/g, '[' ).replace( /%5D/g, ']' ),
modal: true,
title: ips.getString(title),
forceReload: true,
size: size
});
moveDialog.show();
$( document ).trigger('moderationSubmitted');
}
});
}(jQuery, _));]]> #acpAppList > li",
enter: function (row) {
if( $( row ).attr('id') == 'elReorderAppMenu' || $( row ).attr('id') == 'elHideMenu' ){
activating = self.scope.find('.acpAppList_active');
}
if( self._menuTimer ){
clearTimeout( self._menuTimer );
}
},
activate: function (row) {
if( $( row ).attr('id') == 'elReorderAppMenu' || $( row ).attr('id') == 'elHideMenu' ){
activating.addClass('acpAppList_active');
} else {
$( row ).addClass('acpAppList_active');
}
},
deactivate: function (row) {
$( row ).removeClass('acpAppList_active');
},
exitMenu: function () {
self._menuTimer = setTimeout( function () {
self.scope.find('.acpAppList_active').removeClass('acpAppList_active');
self._currentMenu.addClass('acpAppList_active');
}, 2000 );
return false;
}
});
},
toggleMenu: function (e) {
e.preventDefault();
if( $('body').hasClass('cAdminHideMenu') ){
$('body').removeClass('cAdminHideMenu');
ips.utils.cookie.unset('hideAdminMenu');
} else {
$('body').addClass('cAdminHideMenu');
ips.utils.cookie.set('hideAdminMenu', true, true);
}
this.trigger( 'menuToggle.acpNav' );
},
/**
* Hides the submenu when the main body is clicked on
*
* @param {event} e Event object
* @returns {void}
*/
clickMainArea: function (e) {
if( !this._reordering ){
$('#acpAppList')
.removeClass('acpAppList_childHovering')
.find('> li')
.trigger('mouseleave');
}
},
/**
* Starts the tab reordering interface by building drag handles for each tab
* and setting up sortable
*
* @returns {void}
*/
startReorder: function () {
var self = this;
this.scope.find('> li:not( #elReorderAppMenu ):not( #elHideMenu ) > a').each( function () {
$( this ).append( ips.templates.render('core.appMenu.reorder') );
});
this.scope.find('> li > ul > li h3').each( function () {
$( this ).prepend( ips.templates.render('core.appMenu.reorder') );
});
ips.utils.anim.go( 'zoomIn', this.scope.find('[data-role="reorder"]') );
this.scope
.addClass('acpAppList_reordering')
.sortable({
start: function (e, ui) {
ui.item.addClass('acpAppList_dragging');
},
stop: function (e, ui) {
ui.item.removeClass('acpAppList_dragging');
},
update: function () {
self._orderChanged = true;
}
})
.find('#elReorderAppMenu')
.find('[data-action="reorder"]')
.addClass('ipsHide')
.end()
.find('[data-action="saveOrder"]')
.removeClass('ipsHide');
this.scope
.find('> li > ul')
.sortable({
start: function (e, ui) {
ui.item.addClass('acpAppList_dragging');
},
stop: function (e, ui) {
ui.item.removeClass('acpAppList_dragging');
},
update: function () {
self._orderChanged = true;
}
});
this._reordering = true;
this._orderChanged = false;
},
/**
* Saves the new order of tabs, sending an ajax request with the new order
*
* @returns {void}
*/
saveOrder: function () {
// Remove drag handles
this.scope.find('[data-role="reorder"]').remove();
// Get serialized list
var tabOrder = this.scope.sortable( 'toArray', { attribute: 'data-tab'} );
var subMenus = {};
var self = this;
// Get each submenu
_.each( tabOrder, function (val) {
if( val ){
subMenus[ val ] = self.scope.find('> li[data-tab="' + val + '"] > ul').sortable( 'toArray', { attribute: 'data-menuKey' } );
}
});
// Switch tbe buttons around
this.scope
.removeClass('acpAppList_reordering')
.find('#elReorderAppMenu')
.find('[data-action="reorder"]')
.removeClass('ipsHide')
.end()
.find('[data-action="saveOrder"]')
.addClass('ipsHide');
if( this._orderChanged ){
ips.getAjax()('?app=core&module=system&controller=ajax&do=saveTabs', {
data: {
tabOrder: tabOrder,
menuOrder: subMenus
},
dataType: 'json',
type: 'post'
} )
.done( function (response) {
ips.ui.flashMsg.show( ips.getString('tab_order_saved') );
})
.fail( function ( ) {
ips.ui.alert.show( {
type: 'alert',
icon: 'warning',
message: ips.getString('tab_order_not_saved'),
callbacks: {
ok: function () {}
}
});
});
}
this.scope.sortable( 'destroy' );
this._reordering = false;
}
});
}(jQuery, _));]]> li.ipsNavBar_active').length > 1 ){
$.each( this.scope.find('.ipsNavBar_secondary > li.ipsNavBar_active'), function( i, elem ) {
if ( $(elem).find('a[data-ipsmenu]').length ) {
$(elem).removeClass('ipsNavBar_active');
}
} );
}
// Add a caret to the More menu and move the dropdown if we're not using sub menus
if( !this._usingSubBars ){
this.scope.find('#elNavigationMore_dropdown')
.append(" ")
.after(
this.scope.find('#elNavigationMore_more_dropdown_menu')
.attr('id', 'elNavigationMore_dropdown_menu')
);
}
// If we have secondary menus, we'll do the normal tab-style navigation. Otherwise,
// don't bother with the hover functionality
if( this._usingSubBars ){
if( ips.utils.events.isTouchDevice() ){
this.on( 'click', '[data-role="primaryNavBar"] > li > a', this.intentOver );
} else {
this.scope.hoverIntent( _.bind( this.intentOver, this ), $.noop, '[data-role="primaryNavBar"] > li' );
}
}
this._defaultItem = this.scope.find('[data-role="primaryNavBar"] > li > [data-navDefault]').attr('data-navitem-id');
this._mushAllMenus();
},
/**
* When the user mouses out of the scope completely, then we remove the active class
* on any tabs and hide the submenu within it. Then we immediately show the default
* item and its menu. The effect is 'resetting' the menu after mousing out.
*
* @returns {void}
*/
mouseOutScope: function () {
var self = this;
if( ips.utils.events.isTouchDevice() ){
return;
}
this._mouseOutTimer = setTimeout( function () {
self._makeDefaultActive();
self.scope.find('[data-ipsMenu]').trigger('closeMenu');
}, 500 );
},
/**
* When the user mouses over our scope, we'll cancel any timer that's about to reset the menu
*
* @returns {void}
*/
mouseEnterScope: function () {
clearTimeout( this._mouseOutTimer );
},
/**
* HoverIntent event handler. Here we fade out all submenus and then fade in the requested menu,
* except if the requested is already currently shown.
*
* @param {event} e Event object
* @returns {void}
*/
intentOver: function (e) {
var li = $( e.currentTarget );
var link = li.find('> a');
var allItems = this.scope.find('[data-role="primaryNavBar"] > li');
// On touch devices our event handler is on the a instead, so we need to switch
// out our references here so the following code makes sense.
if( li.is('a') ){
li = li.closest('li');
link = li.find('> a');
}
// If this is a touch device, and it's already highlighted, then navigate to the li instead
if( ips.utils.events.isTouchDevice() && li.hasClass('ipsNavBar_active') ){
window.location = link.attr('href');
return;
}
if( ips.utils.events.isTouchDevice() ){
e.preventDefault();
}
this.scope.find('[data-ipsMenu]').trigger('closeMenu');
allItems.removeClass('ipsNavBar_active').find('> a').removeAttr('data-active');
li.addClass('ipsNavBar_active');
link.attr('data-active', true);
},
/**
* Event handler for resizing the window
*
* @returns {void}
*/
resizeWindow: function () {
this._mushAllMenus();
},
_makeDefaultActive: function () {
// Switch to the default item when we mouse out of the menu completely
var link = this.scope.find('[data-navitem-id="' + this._defaultItem + '"]');
var list = link.closest('li');
var allItems = this.scope.find('[data-role="primaryNavBar"] > li');
allItems.removeClass('ipsNavBar_active').find('> a').removeAttr('data-active');
list.addClass('ipsNavBar_active').find('> a').attr('data-active', true);
// The active item may now be in the more menu
if( link.closest('[data-role="secondaryNavBar"]').length ){
link.closest('[data-role="secondaryNavBar"]').closest('li').addClass('ipsNavBar_active').find('> a').attr('data-active', true);
}
},
/**
* Mushes the given menu bar
*
* @param {element} bar the menu bar being mushed
* @param {number} widthAdjustment A value to subtract from the available space in the bar
* @returns {void}
*/
_mushMenu: function (bar, widthAdjustment) {
var self = this;
var padding = parseInt( this.scope.css('padding-left') ) + parseInt( this.scope.css('padding-right') );
var availableSpace = this.scope.width() - widthAdjustment - padding;
var moreItem = bar.find('> [data-role="navMore"]');
var moreMenuSize = moreItem.outerWidth();
var menuItems = bar.find('> li[data-role="navBarItem"]');
var sizeIncrement = 0;
var dropdown = bar.find('[data-role="moreDropdown"]');
if( !moreItem.is(':visible') ){
moreMenuSize = moreItem.removeClass('ipsHide').outerWidth();
moreItem.addClass('ipsHide');
}
menuItems.each( function () {
var item = $( this );
var itemSize = 0;
// We set the original width on an item so that we can easily
// sum the width of the menu. Even if we don't mush now, we'll set it
// for easy use later
if( item.attr('data-originalWidth' ) ){
itemSize = parseInt( item.attr('data-originalWidth') );
} else {
var o = item.outerWidth() + parseInt( item.css('margin-right') ) + parseInt( item.css('margin-left') );
item.attr( 'data-originalWidth', o );
itemSize = o;
}
// If this item will push us over our available size, then mush it
// We add the more menu manually because we *have* to show that one of course
if( ( sizeIncrement + itemSize + moreMenuSize ) > availableSpace ){
// Have we been mushed already?
if( !item.attr('data-mushed') ){
// Build a new list item containing the contents of our menu item
var newLI = $('')
.attr('data-originalItem', item.identify().attr('id') )
.append( item.contents() );
if( self._usingSubBars ){
// If this is the primary nav, then we put it in the sub menu; otherwise,
// build a dropdown
if( bar.is('[data-role="primaryNavBar"]') ){
bar.find('> [data-role="navMore"] > [data-role="secondaryNavBar"]').prepend( newLI );
} else {
newLI.addClass('ipsMenu_item');
// If we have a dropdown inside of this one, add the sub items class to show the >
if( newLI.find('.ipsMenu').length ){
newLI.addClass('ipsMenu_subItems');
}
dropdown.append( newLI );
}
} else {
// Not using sub bars, so put it in the More dropdown
self.scope.find('#elNavigationMore_dropdown_menu').append( newLI.addClass('ipsMenu_item') );
if( newLI.find('.ipsMenu').length ){
newLI.addClass('ipsMenu_subItems');
}
}
item.addClass('ipsHide').attr('data-mushed', true);
}
} else if( item.attr('data-mushed') ) {
var mushedParent = null;
var mushedItem = null;
// If we're in the primary nav bar, our item will be in the secondayr nav bar; otherwise,
// the item will be in a dropdown
if( !self._usingSubBars ){
mushedParent = self.scope.find('#elNavigationMore_dropdown_menu');
} else if( bar.is('[data-role="primaryNavBar"]') ){
mushedParent = bar.find('> [data-role="navMore"] > [data-role="secondaryNavBar"]');
} else {
mushedParent = dropdown;
}
// If this item has previously been mushed, we can unmush it by moving the contents
// back to its original location
var mushedItem = mushedParent.find('[data-originalItem="' + item.identify().attr('id') + '"]');
// If we found the mushed item, move the contents back to the original place
if( mushedItem.length ){
item.append( mushedItem.contents() ).removeClass('ipsHide');
mushedItem.remove();
}
item.removeAttr('data-mushed');
}
sizeIncrement += itemSize;
});
// Show/hide the "More" item as needed
if( bar.is('[data-role="primaryNavBar"]') ){
if( this._usingSubBars ){
moreItem.toggleClass('ipsHide', bar.find('> [data-role="navMore"] > [data-role="secondaryNavBar"] > li').length <= 1 );
} else {
moreItem.toggleClass('ipsHide', !this.scope.find('#elNavigationMore_dropdown_menu > li').length );
}
} else {
moreItem.toggleClass('ipsHide', dropdown.find('> li').length <= 1 );
}
this._makeDefaultActive();
},
/**
* Handles mushing the primary menu and the currently-visible secondary menu
*
* @returns {void}
*/
_mushAllMenus: function () {
this._mushMenu( this.scope.find('[data-role="primaryNavBar"]'), this.scope.find('#elSearch').outerWidth() );
//this._mushMenu( this.scope.find('[data-role="secondaryNavBar"]:visible'), 0 );
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.pagination.js - Pagination controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.pagination', {
initialize: function () {
this.on( 'paginationClicked paginationJump', this.paginationClick );
},
paginationClick: function (e, data) {
var self = this;
if( !data.href ){
return;
}
ips.getAjax()( data.href )
.done( function (response) {
self.scope.hide().html( response );
ips.utils.anim.go('fadeIn', self.scope);
// Open external links in a new window
if( ips.getSetting('links_external') ) {
this.scope.find('a[rel*="external"]').each( function( index, elem ){
elem.target = "_blank";
})
}
})
.fail( function () {
window.location = data.href;
});
}
});
}(jQuery, _));
= this._maxQuestions ){
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('noMoreQuestionsMlord'),
callbacks: {
ok: $.noop
}
});
return;
}
this._addQuestion( maxQid + 1 );
ips.utils.anim.go( 'fadeIn', this.scope.find('[data-questionID="' + ( maxQid + 1 ) + '"]') );
this._checkQuestionButton();
},
/**
* Event handler for the Remove Question button
*
* @param {event} e Event object
* @returns {void}
*/
removeQuestion: function (e) {
e.preventDefault();
var self = this;
var question = $( e.currentTarget ).closest('[data-questionid]');
var removeQuestion = function () {
question.remove();
self._checkQuestionButton();
};
if( question.find('[data-role="questionTitle"]').val() !== '' ){
ips.ui.alert.show( {
type: 'confirm',
icon: 'question',
message: ips.getString('removeQuestionConfirm'),
callbacks: {
ok: removeQuestion
}
});
} else {
removeQuestion();
}
},
/**
* Event handler for adding a new choice to a question
*
* @param {event} e Event object
* @returns {void}
*/
addChoice: function (e) {
e.preventDefault();
var question = $( e.currentTarget ).closest('[data-questionID]');
// How many choices?
var maxCid = _.max( question.find('[data-choiceID]'), function (item) {
return parseInt( $( item ).attr('data-choiceID') );
});
maxCid = parseInt( $( maxCid ).attr('data-choiceID') );
if( !_.isNumber( maxCid ) || _.isNaN( maxCid ) ){
maxCid = 0;
}
if( maxCid >= this._maxChoices ){
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('noMoreChoices'),
callbacks: {
ok: $.noop
}
});
return;
}
this._addChoice( question, maxCid + 1 );
ips.utils.anim.go( 'fadeIn', question.find('[data-choiceID="' + ( maxCid + 1 ) + '"]') );
this._checkChoiceButton( question );
},
/**
* Event handler for removing a choice
*
* @param {event} e Event object
* @returns {void}
*/
removeChoice: function (e) {
e.preventDefault();
var self = this;
var choice = $( e.currentTarget ).closest('[data-choiceID]');
var question = choice.closest( '[data-questionID]' );
// Check this isn't the only choice left
if( question.find('[data-choiceID]').length <= 2 ){
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('cantRemoveOnlyChoice'),
callbacks: {
ok: $.noop
}
});
return;
}
// Animation complete handler to remove the choice
choice.animationComplete( function () {
choice.remove();
// Need to readjust all the choice numbers for this question
_.each( question.find('[data-choiceID]'), function (item, idx) {
$( item )
.attr( 'data-choiceID', idx + 1 )
.find('[data-role="choiceNumber"]')
.text( idx + 1 );
});
self._checkChoiceButton( question );
});
ips.utils.anim.go( 'fadeOut fast', choice );
},
/**
* Builds a question based on existing data
*
* @param {object} data Data object containing title, multiple choice, etc
* @param {number} qid Question ID
* @returns {void}
*/
_buildQuestion: function (data, qid) {
var choices = [];
if( _.isArray( data.choices ) && data.choices.length ){
for( var i = 0; i < data.choices.length; i++ ){
choices.push( this._getChoiceHTML( i + 1, qid, data.choices[ i ].title, data.choices[ i ].count ) );
}
}
this.scope.find('[data-role="pollContainer"]').append( ips.templates.render('core.pollEditor.question', {
pollName: this._name,
multiChoice: data.multiChoice,
questionID: qid,
question: data.title,
choices: choices.join(''),
removeQuestion: !( qid === 1 )
}));
},
/**
* Adds an empty question block to the form
*
* @param {object} data Message data
* @returns {void}
*/
_addQuestion: function (qid) {
var choices = [];
choices.push( this._getChoiceHTML( 1, qid ) );
choices.push( this._getChoiceHTML( 2, qid ) );
this.scope.find('[data-role="pollContainer"]').append( ips.templates.render('core.pollEditor.question', {
pollName: this._name,
questionTitle: ips.getString( 'questionTitle', { id: qid } ),
questionID: qid,
showCounts: this._showCounts,
choices: choices.join(''),
removeQuestion: !( qid === 1 )
}));
},
/**
* Adds a new choice to the given question
*
* @param {element} question Question block we're adding to
* @param {number} cid ID of new choice
* @returns {void}
*/
_addChoice: function (question, cid) {
var html = this._getChoiceHTML( cid, question.attr('data-questionID'), '' );
question.find('[data-role="choices"]').append( html );
},
/**
* Returns the HTML for a choice row
*
* @param {object} data Message data
* @returns {void}
*/
_getChoiceHTML: function (cid, qid, name, count) {
return ips.templates.render('core.pollEditor.choice', {
choiceID: cid,
questionID: qid,
pollName: this._name,
choiceTitle: name,
showCounts: this._showCounts,
hideCounts: !this._showCounts,
count: count
});
},
/**
* Enables or disables the Add Question button depending on current number of questions
*
* @returns {void}
*/
_checkQuestionButton: function () {
var questions = this.scope.find('[data-questionID]');
this.scope.find('[data-action="addQuestion"]').toggleClass( 'ipsButton_disabled ipsFaded', ( questions.length >= this._maxQuestions ) );
},
/**
* Enables or disables the Add Choice button depending on current number of choices in the given question
*
* @param {element} questionScope The question being worked with
* @returns {void}
*/
_checkChoiceButton: function (questionScope) {
var choices = questionScope.find('[data-choiceID]');
questionScope.find('[data-action="addChoice"]').toggleClass( 'ipsButton_disabled ipsFaded', ( choices.length >= this._maxChoices ) );
questionScope.find('[data-choiceID] [data-action="removeChoice"]').toggleClass( 'ipsButton_disabled ipsFaded', ( choices.length === 2 ) );
}
});
}(jQuery, _));]]> 0 && !this._prefixRow.is(':visible') ){
ips.utils.anim.go( 'fadeIn', this._prefixRow );
} else if( data.totalTokens === 0 && this._prefixRow.is(':visible') ){
ips.utils.anim.go( 'fadeOut', this._prefixRow );
this._prefixRow.find('input[type="checkbox"]').prop( 'checked', false );
}
// Update button
if( e && e.type == 'tokenDeleted' && data.token == this._prefixValue.val() ){
this._prefixButton.find('span').html( ips.getString('selectPrefix') );
this._prefixValue.val('');
}
// Get current value
var value = this._prefixValue.val();
var list = this._buildTokenList( data.tokenList, value );
// Update list contents
this._prefixMenu.html( list );
},
/**
* Event handler for when a prefix menu item is selected
*
* @param {event} e Event object
* @param {object} data Data object from the menu widget
* @returns {void}
*/
prefixSelected: function (e, data) {
data.originalEvent.preventDefault();
var itemValue = ( data.selectedItemID == '-' ) ? '' : data.selectedItemID;
var selectedText = this._getPrefixText( data.selectedItemID );
this._prefixButton.find('span').html( selectedText );
this._prefixValue.val( itemValue );
this._prefixRow.find('input[type="checkbox"]').prop( 'checked', true );
},
/**
* Loops through provided tokens, building menu items for each
*
* @param {array} tokens Tokens array from the autocomplete widget
* @param {string} value Currently-selected item
* @returns {string} Menu HTML
*/
_buildTokenList: function (tokens, value) {
var output = '';
output += ips.templates.render('core.menus.menuItem', {
value: '',
title: ips.getString('selectedNone'),
checked: ( value == '' )
});
output += ips.templates.render('core.menus.menuSep');
$.each( tokens, function (i, item) {
output += ips.templates.render('core.menus.menuItem', {
value: item,
title: item,
checked: ( item == value )
});
});
Debug.log( output );
return output;
},
/**
* Gets the string for the prefix selector
*
* @param {string} prefix A selected prefix
* @returns {string}
*/
_getPrefixText: function (prefix) {
var selectedText = '';
if( prefix && prefix != '-' ){
selectedText = ips.getString( 'selectedPrefix', { tag: prefix } );
} else {
selectedText = ips.getString( 'selectedPrefix', { tag: ips.getString('selectedNone') } );
}
return selectedText;
}
});
}(jQuery, _));]]>')
.attr( 'type', 'hidden' )
.attr( 'name', 'type' )
.attr( 'value', data.selectedItemID )
.attr( 'data-role', 'searchFilter' )
);
return;
}
// Build the form inputs from the options data on this item
try {
options = $.parseJSON( options );
_.each( options, function (val, i) {
form.append(
$('')
.attr( 'type', 'hidden' )
.attr( 'name', i )
.attr( 'value', val )
.attr( 'data-role', 'searchFilter' )
);
});
} catch (err) {
Debug.log('Invalid search options json');
return;
}
}
},
/**
* Event handler for focusing the search box
*
* @param {event} e Event object
* @returns {void}
*/
focusSearch: function (e) {
if( this._expanded ){
return;
}
$('#elSearch')
.addClass('cSearchExpanded');
ips.utils.anim.go('fadeIn', $('#elSearchFilter') );
var scope = $(this.scope);
ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=search&controller=search&do=globalFilterOptions&checked=' + $(this.scope).attr('data-default') ).done(function( response ){
scope.find('[data-role="globalSearchMenuOptions"]').replaceWith( response );
});
this._expanded = true;
},
/**
* Shrinks search box when blurred. Sets a timer and hands off to _cancelSearch to actually shrink it.
*
* @returns {void}
*/
blurSearch: function () {
var self = this;
// First set a small timeout so we can cancel this if we want
this._blurTimeout = setTimeout( function (){
self._cancelSearch();
}, 500 );
},
/**
* Shrinks search box when blurred.
*
* @param {boolean} loseFocus
* @returns {void}
*/
_cancelSearch: function () {
if( ips.utils.responsive.currentIs('phone') ){
ips.utils.anim.go('fadeIn fast', $('#elHeaderNavigation') );
}
$('#elSearch')
.removeClass('cSearchExpanded');
ips.utils.anim.go('fadeOut fast', $('#elSearchFilter') );
this._expanded = false;
},
/**
* Event handler for the filter menu being opened
*
* @param {event} e Event object
* @returns {void}
*/
menuOpened: function (e, data) {
Debug.log( data.elemID );
if( data.elemID == 'elSearchFilter' ){
clearTimeout( this._blurTimeout );
}
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.quickSearch.js - Controller for search in header
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.rating', {
initialize: function () {
this.on( 'ratingSaved', '[data-ipsRating]', this.ratingClick );
var scope = this.scope;
},
ratingClick: function(e, data){
var scope = $(this.scope);
ips.getAjax()( scope.attr('action'), {
data: scope.serialize(),
type: 'post'
})
.done( function (response, textStatus, jqXHR) {
// Don't need to actually do anything here
})
.fail(function(){
scope.submit();
});
}
});
}(jQuery, _));' + response + '').find('[data-controller="core.front.core.reputation"]').html();
self.scope
.html( newHTML )
.css({
opacity: 1
});
})
.fail( function ( jqXHR, textStatus, errorThrown ) {
if ( jqXHR.responseJSON['error'] ) {
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: jqXHR.responseJSON['error'],
callbacks: {}
});
} else {
window.location = url;
}
});
}
});
}(jQuery, _));
]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.reviewForm.js - Review form controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.core.reviewForm', {
initialize: function () {
this.on( 'click', '[data-action="writeReview"]', this.toggleReview );
},
toggleReview: function (e) {
e.preventDefault();
this.scope.find('[data-role="reviewIntro"]').hide();
this.scope.find('[data-role="reviewForm"]').show();
}
});
}(jQuery, _)); 0 ){
statusElem
.find('[data-role="replyComment"] input[type="text"]')
.focus();
return;
}
Debug.log( statusElem.find('.ipsComment_subComments').is(':visible') );
if( !statusElem.find('.ipsComment_subComments').is(':visible') ){
ips.utils.anim.go('fadeIn', statusElem.find('.ipsComment_subComments') );
statusElem
.addClass('ipsComment_hasChildren')
.find('[data-role="replyComment"] input[type="text"]')
.focus();
} else {
if( statusElem.find('[data-commentid]').length == 0 && field.val() == '' ){
statusElem
.removeClass('ipsComment_hasChildren')
.find('.ipsComment_subComments, [data-role="submitReply"]')
.hide();
}
}
},
/**
* User has blurred from the reply text field. Remove the comment box if a) there's no existing comments
* b) they haven't typed anything
*
* @param {event} e Event
* @returns {void}
*/
blurCommentField: function (e) {
e.preventDefault();
// Get status ID
var field = $( e.currentTarget ),
statusElem = field.parents( '[data-statusid]' ),
replyButton = statusElem.find('[data-role="submitReply"]');
if( statusElem.find('[data-commentid]').length == 0 && field.val() == '' ){
statusElem
.removeClass('ipsComment_hasChildren')
.find('.ipsComment_subComments')
.hide();
}
},
/**
* User has blurred from the reply text field. Remove the comment box if a) there's no existing comments
* b) they haven't typed anything
*
* @param {event} e Event
* @returns {void}
*/
keydownCommentField: function (e) {
var field = $( e.currentTarget ),
statusElem = field.parents( '[data-statusid]' ),
statusID = statusID = $( statusElem ).data('statusid');
if( e.keyCode == ips.ui.key.ENTER ){
/**
* Adds a new reply to a status
*
* @event core.statuses#addComment
* @type {object}
* @property {string} content The text of the reply
* @property {number} statusID The ID of the parent status
*/
this.trigger('addComment', {
content: field.val(),
statusID: statusID
});
}
},
/**
* The model is saving a comment
*
* @param {event} e Event
* @param {object} data Event data object
* @returns {void}
*/
addingComment: function (e, data) {
// Find relevant status
var statusElem = $( this.scope ).find( '[data-statusid="' + data.statusID + '"]' ),
replyRow = statusElem.find('[data-role="replyComment"]');
replyRow
.find('input[type="text"]')
.prop('disabled', true)
.addClass('ipsField_disabled');
},
/**
* A comment has been added by the model
*
* @param {event} e Event
* @param {object} data Event data object
* @returns {void}
*/
addedComment: function (e, data) {
// Find relevant status
var statusElem = $( this.scope ).find( '[data-statusid="' + data.statusID + '"]' ),
replyRow = statusElem.find('[data-role="replyComment"]'),
subComments = statusElem.find('.ipsComment_subComments');
if( replyRow.length ){
replyRow.before( data.comment );
} else if( subComments.length ){
subComments.append( data.comment );
}
statusElem
.find('[data-role="replyComment"] input[type="text"]')
.val('')
.blur()
.prop('disabled', false)
.removeClass('ipsField_disabled');
},
/**
* User has focused on the reply field, so we show the reply button
*
* @param {event} e Event
* @returns {void}
*/
/*focusCommentField: function (e) {
e.preventDefault();
// Get status ID
var field = $( e.currentTarget ),
statusElem = field.parents( '[data-statusid]' ),
replyButton = statusElem.find('[data-role="submitReply"]');
if( !replyButton.is(':visible') ){
replyButton.show();
}
}*/
});
}(jQuery, _));]]> page 2 then hits back, there's no record of 'page 1' in the history, and it doesn't work.
// To fix that, we're tracking a 'doneInitialState' flag in this controller. The first time this method is called
// and doneInitialState == false, we insert the *current* url into the stack before changing the URL. This gives
// the History manager something to go back to when the user clicks back to the initial page.
/*if( !this._doneInitialState ){
History.replaceState(
_.extend( _.clone( this._urlParams ), { controller: this.controllerID, bypassState: true } ),
document.title,
document.location
);
this._doneInitialState = true;
}*/
_.extend( this._urlParams, newParams );
var tmpStateData = _.extend( _.clone( this._urlParams ), { controller: this.controllerID } );
//Debug.log( tmpStateData );
History.pushState(
tmpStateData,
document.title,
this._baseURL + this._getURL()
);
},
/**
* Event handler for pagination widget
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
paginationClicked: function (e, data) {
if( data.originalEvent ){
data.originalEvent.preventDefault();
}
if( data.pageNo != this._urlParams[ this._pageParam ] ){
var newObj = {};
newObj[ this._pageParam ] = data.pageNo;
this.updateURL( newObj );
}
},
/**
* Event handler for choosing a new filter
*
* @param {event} e Event object
* @returns {void}
*/
changeFiltering: function (e) {
e.preventDefault();
var newFilter = $( e.currentTarget ).attr('data-filter');
// Select the one that was clicked, unselect others
this._updateFilter( newFilter );
if( newFilter != this._urlParams.filter ){
this.updateURL( {
filter: newFilter,
page: 1
});
}
},
/**
* Event handler for choosing a new filter from a dropdown menu
*
* @param {event} e Event object
* @returns {void}
*/
changeFilteringFromMenu: function (e,data) {
if( data.originalEvent ){
data.originalEvent.preventDefault();
}
//var newFilter = $( data.originalEvent.target ).closest('li').attr('data-filter');
var newFilter = data.selectedItemID || '';
// Select the one that was clicked, unselect others
this._updateFilter( newFilter );
if( newFilter != this._urlParams.filter ){
this.updateURL( {
filter: newFilter,
page: 1
});
}
},
/**
* Cleans the base url of our default params
*
* @returns {void}
*/
_cleanUpBaseURL: function () {
var urlObj = ips.utils.url.getURIObject( this._baseURL );
var params = _.clone( urlObj.queryKey );
var self = this;
this._baseURL = urlObj.protocol + '://' + urlObj.host + ( urlObj.port ? ( ':' + urlObj.port ) : '' ) + urlObj.path + '?';
// If we're using friendly URLs *without* rewriting, we need to
if( urlObj.file == 'index.php' ){
_.each( params, function (val, key) {
if( key.startsWith('/') ){
self._baseURL += encodeURIComponent( key ).replace( /%2f/ig, '/' ); // We don't want '/' being encoded or it breaks URLs
delete params[ key ];
}
});
this._baseURL += '&';
}
// Remove our default URL params
_.each( _.extend( ['sortby', 'sortdirection', 'filter'], this._otherParams ), function (val) {
if( !_.isUndefined( params[ val ] ) ){
delete params[ val ];
}
});
// Decode params as $.param() will encode it again (double encode)
_.each( params, function( v, k ){
delete params[k];
params[ decodeURIComponent( k ).replace( /\+/g, ' ') ] = v.replace(/\+/g, ' ');
});
// When using index.php? URLs, a param key is the path /forums/2-forum/ but as the value is empty, params.length returns false
if( ! _.isEmpty( params ) ){
this._baseURL += $.param( params );
}
// If the last character is &, we can remove that because it'll be added back later
if( this._baseURL.slice(-1) == '&' ){
this._baseURL = this._baseURL.slice( 0, -1)
}
},
/**
* Checks whether any values in the provided state are different and need updating
*
* @param {object} state State from history.js
* @returns {void}
*/
_handleStateChanges: function (state) {
// See what's changed so we can update the display
if( !_.isUndefined( state.data.filter ) && state.data.filter != this._urlParams.filter ){
this._updateFilter( state.data.filter );
}
if( ( !_.isUndefined( state.data.sortby ) && !_.isUndefined( state.data.sortdirection ) ) &&
( state.data.sortby != this._urlParams.sortby || state.data.sortdirection != this._urlParams.sortdirection ) ){
this._updateSort( {
by: state.data.sortby,
order: state.data.sortdirection
});
}
if( !_.isUndefined( state.data[ this._pageParam ] ) && state.data[ this._pageParam ] != this._urlParams[ this._pageParam ] ){
this._updatePage( state.data[ this._pageParam ] );
}
},
/**
* Fetches new results from the server, then calls this._updateTable to update the
* content and pagination. Simply redirects to URL on error.
*
* @returns {void}
*/
_getResults: function () {
var self = this;
var urlBits = this._getURL();
ips.getAjax()( this._baseURL + ( ( urlBits ) ? this._getURL() + '&' : '' ) + this.scope.attr('data-resort') + '=1', {
dataType: 'json',
showLoading: true
})
.done( _.bind( this._getResultsDone, this ) )
.fail( _.bind( this._getResultsFail, this ) )
.always( _.bind( this._getResultsAlways, this ) );
},
/**
* Callback when the results ajax is successful
*
* @param {object} response JSON object containing new HTML pieces
* @returns {void}
*/
_getResultsDone: function (response) {
this._updateTable( response );
},
/**
* Callback when the results ajax fails
*
* @param {object} jqXHR jQuery XHR object
* @param {string} textStatus Error message
* @param {string} errorThrown
* @returns {void}
*/
_getResultsFail: function (jqXHR, textStatus, errorThrown) {
if( Debug.isEnabled() ){
Debug.error( "Ajax request failed (" + textStatus + "): " + errorThrown );
Debug.error( jqXHR.responseText );
} else {
// rut-roh, we'll just do a manual redirect
window.location = this._baseURL + this._getURL();
}
},
/**
* Update the content and pagination elements
*
* @param {object} response JSON object containing new HTML pieces
* @returns {void}
*/
_updateTable: function (response) {
// Table body
this.scope.find('[data-role="tableRows"]').html( response.rows ).trigger('tableRowsUpdated');
// Pagination
this.scope.find('[data-role="tablePagination"]').html( response.pagination ).trigger('tablePaginationUpdated');
// Eztra
this.scope.find('[data-role="extraHtml"]').html( response.extraHtml );
// New content loaded, so trigger contentChange event
$( document ).trigger( 'contentChange', [ this.scope ] );
},
/**
* Update classname on new active page. Pagination actually gets overwritten
* by the ajax response, but by updating the class here, it feels more immediate
* for the user.
*
* @param {number} newPage New active page number
* @returns {void}
*/
_updatePage: function (newPage) {
this.scope
.find('[data-role="tablePagination"] [data-page]')
.removeClass('ipsPagination_pageActive')
.end()
.find('[data-page="' + newPage + '"]')
.addClass('ipsPagination_pageActive');
},
/**
* Updates the sorting order classnames
*
* @param {string} by Key name of sort by value
* @param {string} direction asc or desc order value
* @returns {void}
*/
_updateSort: function ( data ) {
var current = this._getSortValue();
if( !data.by ){
data.by = current.by;
}
if( !data.order ){
data.order = current.order;
}
var obj = {
sortby: data.by,
sortdirection: data.order,
};
obj[ this._pageParam ] = 1;
this.updateURL( obj );
},
/**
* Builds a param string from values in this._urlParams, excluding empty values
*
* @returns {string} Param string
*/
_getURL: function () {
var tmpUrlParams = {};
for( var i in this._urlParams ){
if( this._urlParams[ i ] != '' && i != 'controller' && i != 'bypassState' ){
tmpUrlParams[ i ] = this._urlParams[ i ];
}
}
return $.param( tmpUrlParams );
},
/**
* Returns current parameters to be used in URLs
*
* @returns {object}
*/
_getUrlParams: function () {
var sort = this._getSortValue();
var obj = {
filter: this._getFilterValue() || '',
sortby: sort.by || '',
sortdirection: sort.order || '',
};
obj[ this._pageParam ] = ips.utils.url.getParam( this._pageParam ) || 1
return obj;
},
/**
* Abstract
*/
_getSortValue: $.noop,
_getFilterValue: $.noop,
_getResultsAlways: $.noop
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.license.js - License message
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.global.core.updateBanner', {
initialize: function () {
this.on( 'click', '[data-role="closeMessage"]', this.hideMessage );
},
hideMessage: function () {
var date = new Date();
date.setTime( date.getTime() + ( 7 * 86400000 ) );
ips.utils.cookie.set( 'updateBannerDismiss', true, date.toUTCString() );
this.scope.slideUp();
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.viglink.js - Controller to launch VigLink "convert account" window
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.core.viglink', {
initialize: function () {
this.on( 'submit', this._submitForm );
},
/**
* Event handler for when form is submitted
*
* @param {event} e Event object
* @returns {void}
*/
_submitForm: function (e) {
if( $(e.currentTarget).find('input[name="viglink_account_type"]:checked').val() == 'create' ) {
window.open( $(e.currentTarget).attr('data-viglinkUrl'), 'viglink', 'height=980,width=780' );
}
}
});
}(jQuery, _));
');
list.sortable({
connectWith: '[data-role="dummyToolbar"]',
update: _.bind( this._saveToolbars, this ),
appendTo: document.body
});
this.scope.find('[data-role="dummyToolbar"]').sortable( 'option', 'connectWith', list );
this.scope.find('#' + elem.attr('data-deviceKey') + '_editor_toolbars').append( list );
},
/**
* Event handler for clicking the Add Separator button
*
* @param {event} e Event object
* @returns {void}
*/
addSep: function (e) {
e.preventDefault();
var elem = $( e.currentTarget );
var list = $('#' + elem.attr('data-deviceKey') + '_editor_toolbars')
.children()
.last();
list.append( $('
') );
list.sortable({
connectWith: '[data-role="dummyToolbar"]',
update: _.bind( this._saveToolbars, this ),
appendTo: document.body
});
},
/**
* Sets up CKEditor by cloning each button and inserting it into the dummy toolbars
*
* @returns {void}
*/
_setUpCKEditor: function () {
var self = this;
var instance = null;
for( var i in CKEDITOR.instances ){
instance = CKEDITOR.instances[ i ];
}
//instance.on( 'instanceReady', function(){
var items = CKEDITOR.ui( instance ).items;
// Loop through all toolbar items in ckeditor
// If it's a button or combo, we then clone the button onto dummy toolbars
for( var i in items ){
var elem = null;
switch ( items[i].type ) {
case 'button':
case 'panelbutton':
if( !$( '.' + instance.id ).find('.cke_button__' + items[i].name).length ){
var button = new CKEDITOR.ui.button( items[i] );
var output = [];
button.render( instance, output );
elem = $( output.join('') );
} else {
elem = $( '.' + instance.id ).find('.cke_button__' + items[i].name);
}
break;
case 'richcombo':
elem = $( '.' + instance.id ).find('.cke_combo__' + items[i].name );
break;
case 'separator':
break;
}
if ( elem !== null ) {
self.scope.find('[data-role="dummyEditor"]').each( function () {
var deviceKey = $( this ).attr('data-deviceKey');
// Clone the element
var elemClone = elem.clone().attr( 'data-buttonKey', i );
// Remove onclick from clone and children to prevent ckeditor from taking over
elemClone.removeAttr('onclick').children().removeAttr('onclick');
// If the button wrap already exists, clone the button into it,
// otherwise, create a new wrapper
// and append it to the 'unused' toolbar
if( self.scope.find('#' + deviceKey + '_editorButton_' + i ).length ){
self.scope.find('#' + deviceKey + '_editorButton_' + i ).append( elemClone );
} else {
self.scope.find('#' + deviceKey + '_editor_unusedButtons').append(
$('').attr('id', deviceKey + '_editorButton_' + i ).append( elemClone )
);
}
});
}
}
//});
},
/**
* Save the toolbar arrangement
*
* @returns {void}
*/
_saveToolbars: function () {
var _save = {
desktop: [],
tablet: [],
phone: []
};
this.scope.find('[data-role="devicePanel"]').each( function () {
var deviceKey = $( this ).attr('data-deviceKey');
var save = [];
var i = 1;
$( this ).find('[data-role="dummyToolbar"]').each( function () {
var _id = 'row_' + i;
i++;
if( !$( this ).hasClass( 'editor_unusedButtons' ) ) {
var toolbar = [];
$( this ).children().each( function () {
var buttonKey = null;
if( $( this ).attr('id') ) {
buttonKey = $( this ).attr('id').substr( 14 + deviceKey.length );
} else {
buttonKey = '-';
}
toolbar.push( buttonKey );
});
save.push( toolbar );
}
});
_save[ deviceKey ] = save;
});
ips.getAjax()( this.scope.attr('data-url') + '&do=save', {
type: 'post',
data: {
toolbars: JSON.stringify( _save ),
}
});
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.customization.emoticons.js - Emoticons controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.customization.emoticons', {
initialize: function () {
this.setup();
},
/**
* Setup method
* Makes the emoticon list sortable and sets an event handler for saving the order
*
* @returns {void}
*/
setup: function () {
this.scope.find('[data-role="setList"]').sortable({
update: _.bind( this._saveSetOrder, this )
});
this.scope.find('[data-role="emoticonsList"]').sortable({
connectWith: this.scope.find('[data-role="emoticonsList"]'),
update: _.bind( this._saveOrder, this )
});
},
/**
* Saves the new emoticon order
*
* @returns {void}
*/
_saveSetOrder: function () {
var setOrder = [];
this.scope.find('[data-emoticonSet]').each( function () {
setOrder.push( $( this ).attr('data-emoticonSet') );
});
console.log({ setOrder: setOrder });
ips.getAjax()( this.scope.attr('action'), {
type: 'post',
data: { setOrder: setOrder }
});
},
/**
* Saves the new emoticon order
*
* @returns {void}
*/
_saveOrder: function () {
var output = {};
// Build the array of ordering
this.scope.find('[data-emoticonGroup]').each( function () {
var itemOrder = [];
$( this ).find('[data-emoticonID]').each( function () {
itemOrder.push( parseInt( $( this ).attr('data-emoticonID') ) );
});
output[ $( this ).attr('data-emoticonGroup') ] = itemOrder;
});
ips.getAjax()( this.scope.attr('action'), {
type: 'post',
data: output
});
}
});
}(jQuery, _));
/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.core.mobileNav.js - ACP mobile navigation
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.customization.themes', {
initialize: function () {
this.on( 'click', this.revertSetting );
},
/**
* Reverts a setting
*
* @param {event} e Event object
* @returns {void}
*/
revertSetting: function (e) {
var self = this;
e.preventDefault();
ips.ui.alert.show({
type: 'confirm',
message: ips.getString('theme_revert_setting'),
icon: 'fa fa-question',
buttons: {
ok: ips.getString('ok'),
cancel: ips.getString('cancel')
},
callbacks: {
ok: function () {
ips.getAjax()( self.scope.attr('href') )
.done( function (response) {
var obj = $('#theme_setting_' + self.scope.attr('data-ipsThemeSetting') + ' input[name^=core_theme_setting_title_]');
obj.val( response.value );
obj.focus().blur();
self.scope.hide();
});
},
}
});
}
});
}(jQuery, _));')
.attr( { type: 'text' } )
.addClass( 'ipsField_loading ipsField_vle' )
.attr( 'data-role', 'vle' );
elem.html('').append( inputNode );
// Fire an ajax request to get the raw language string, then update the text box with the returned value
ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=vle&do=get&key=' + elem.attr('data-vle') )
.done( function (response) {
console.log( elem.attr('data-vle') );
inputNode
.val( response )
.attr( { 'data-original': response } )
.removeClass('ipsField_loading')
.focus()
.select()
})
.fail( function () {
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('js_login_both'),
});
});
},
/**
* Removes stray language tags from the provided element
*
* @param {element} element Element from which to remove tags
* @returns {void}
*/
_removeLangTag: function (element) {
var elem = $( element );
elem.contents().filter(function() { return this.nodeType === 3 || this.tagName === "LABEL" || this.tagName === "SPAN"; }).each(function(){
$(this).replaceWith( $(this).text().replace( /\#VLE\#.+?#!#\[(.+?)\]#!##/gm, '$1' ) );
});
elem.find('i[class]').each(function(){
$(this).attr( 'class', $(this).attr('class').replace( /\#VLE\#.+?#!#\[(.+?)\]#!##/gm, '$1' ) );
});
elem.find('[placeholder]').each(function(){
$(this).attr( 'placeholder', $(this).attr('placeholder').replace( /\#VLE\#.+?#!#\[(.+?)\]#!##/gm, '$1' ) );
});
elem.find('[title]').each(function(){
$(this).attr( 'title', $(this).attr('title').replace( /\#VLE\#.+?#!#\[(.+?)\]#!##/gm, '$1' ) );
});
elem.find('[aria-label]').each(function(){
$(this).attr( 'aria-label', $(this).attr('aria-label').replace( /\#VLE\#.+?#!#\[(.+?)\]#!##/gm, '$1' ) );
});
},
/**
* Turns strings into editable spans in the provided element
*
* @param {element} element Element in which to replace language strings
* @returns {void}
*/
_setUpTextNodes: function ( element ) {
var regex = /\#VLE\#(.+?)#!#\[(.+?)\]#!##/gm;
$( element )
.find('*')
.contents()
.filter( function () {
var elem = $( this );
return !elem.is('iframe') && !elem.closest('[data-ipsEditor]').length && !elem.is('textarea') && ( elem.is('[value]') || this.nodeType == 3 );
})
.each( function (idx, elem) {
var elem = $( elem );
if( elem.get(0).nodeType == 3 ){
// Text inputs
elem.replaceWith( elem.text().replace( regex, '$2' ) );
} else if( elem.is('[value]') ){
// Inputs
if( elem.val() != '' ){
elem.attr( 'data-vle', elem.val().replace( regex, '$1' ) ).val( elem.val().replace( regex, '$2' ) );
}
}
});
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.dashboard.adminNotes.js - Admin notes controller for the admin notes widget
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.dashboard.adminNotes', {
initialize: function () {
this.on( 'submit', 'form', this.saveNotes );
},
saveNotes: function (e) {
e.preventDefault();
var url = $( e.currentTarget ).attr('action');
var self = this;
// Show loading
this.scope.find('[data-role="notesInfo"]').hide();
this.scope.find('[data-role="notesLoading"]').removeClass('ipsHide');
ips.getAjax()( url, { type: 'post', data: $('#admin_notes').serialize() } )
.done( function (response) {
self.scope.find('[data-role="notesInfo"]').html( response );
})
.fail( function () {
})
.always( function () {
self.scope.find('[data-role="notesInfo"]').show();
self.scope.find('[data-role="notesLoading"]').addClass('ipsHide');
});
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.dashboard.validation.js - AdminCP users awaiting validation widget
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.dashboard.validation', {
initialize: function () {
this.on( 'click', '[data-action="approve"], [data-action="ban"]', this.validateUser );
},
/**
* Event handler for the approve/ban buttons
*
* @param {event} e Event object
* @returns {void}
*/
validateUser: function (e) {
e.preventDefault();
var self = this;
var button = $( e.currentTarget );
var url = button.attr('href');
var type = button.attr('data-action');
var row = button.closest('.ipsDataItem');
var name = row.find('[data-role="userName"]').text();
var toggles = button.closest('[data-role="validateToggles"]');
button
.text( type == 'approve' ? ips.getString('widgetApproving') : ips.getString('widgetBanning') )
.addClass('ipsButton_disabled');
ips.getAjax()( url )
.done( function () {
// Highlight row
row.addClass( type == 'approve' ? 'ipsDataItem_success' : 'ipsDataItem_error' );
row.attr('data-status', 'done');
button.text( type == 'approve' ? ips.getString('widgetApproved') : ips.getString('widgetBanned') );
setTimeout( function () {
ips.utils.anim.go( 'fadeOut', toggles );
}, 750);
// Show flash msg
ips.ui.flashMsg.show( ips.getString( type == 'approve' ? 'userApproved' : 'userBanned', {
name: name
}));
self._checkForFinished();
});
},
/**
* Checks whether all users shown in the widget have been approved/banned
* If so, triggers an event whch will reload the content to show more users
*
* @param {event} e Event object
* @returns {void}
*/
_checkForFinished: function (e) {
var items = this.scope.find('[data-role="userAwaitingValidation"]');
var doneItems = this.scope.find('[data-status="done"]');
var self = this;
if( items.length === doneItems.length ){
setTimeout( function () {
self.scope.trigger('refreshWidget');
}, 750);
}
}
});
}(jQuery, _));').attr( 'name', i ).text( languageName );
if ( selectedMode == i ) {
option.attr('selected', 'selected');
}
self.scope.find('[data-role="codeModeSelect"]').append( option );
}
self.instance = CodeMirror.fromTextArea( document.getElementById( 'elCodeInput' + self.scope.attr('data-randomstring') ), {
autofocus: true,
mode: selectedMode,
lineWrapping: true
} );
self.scope.find('[data-role="codeLoading"]').remove();
self.scope.find('[data-role="codeContainer"]').removeClass('ipsLoading');
});
},
/**
* Event handler for changing the mode
*
* @param {event} e Event object
* @returns {void}
*/
changeMode: function (e) {
console.log(this.scope.find('[data-role="codeModeSelect"] option:selected').attr('name') );
this.instance.setOption( 'mode', this.scope.find('[data-role="codeModeSelect"] option:selected').attr('name') );
},
/**
* Event handler for submitting the form
*
* @param {event} e Event object
* @returns {void}
*/
formSubmit: function (e) {
e.preventDefault();
this.insertCode(e);
},
/**
* Event handler for 'insert' button
*
* @param {event} e Event object
* @returns {void}
*/
insertCode: function (e) {
var value = this.instance.getValue();
var editor = CKEDITOR.instances[ $( this.scope ).data('editorid') ];
if ( this.scope.find('[data-role="codeModeSelect"] option:selected').attr('name') == 'null' ) {
var element = CKEDITOR.dom.element.createFromHtml( "" );
} else {
var lang = '';
for ( var i in this.languageMap ) {
if ( i == this.scope.find('[data-role="codeModeSelect"] option:selected').attr('name') ) {
lang = 'lang-' + this.languageMap[i];
break;
}
}
var element = CKEDITOR.dom.element.createFromHtml( "" );
}
element.setText( value );
this.scope.find('textarea').val('');
editor.insertElement( element );
editor.widgets.initOn( element, 'ipscode' );
this.trigger('closeDialog');
},
});
}(jQuery, _));]]> 7 ){
existing.splice( ( existing.length - 7 ) * -1, existing.length - 7 );
}
ips.utils.db.set( 'emoticons', 'recent', existing );
}
}
},
/**
* Converts the recent emoticons cookie to localStorage
*
* @returns {void}
*/
_convertEmoticonsToLocalStorage: function () {
var emoticonCookie = ips.utils.cookie.get('recentEmoticons');
var existing = [];
if( !ips.utils.db.isEnabled() ){
return;
}
if( !_.isUndefined( emoticonCookie ) && emoticonCookie !== '-' ){
try {
existing = $.parseJSON( emoticonCookie );
} catch(err) {
Debug.log( err );
}
ips.utils.db.set( 'emoticons', 'recent', existing );
ips.utils.cookie.set('recentEmoticons', '-', true);
ips.utils.cookie.unset('recentEmoticons');
}
},
/**
* The search box has received focus
*
* @param {event} e Event object
* @returns {void}
*/
searchEmoticons: function (e) {
// Find the last-active panel. If search is the last active (because we blurred then
// came back), then just carry on without rememberig it
var lastActive = this.scope.find('[data-panel]:visible').attr('data-panel');
if( lastActive != 'search' ){
this._lastActive = lastActive;
}
// Set a continuous timer
this._typeTimer = setInterval( _.bind( this._typing, this ), 200 );
},
/**
* The search box has blurred
*
* @param {event} e Event object
* @returns {void}
*/
stopSearchEmoticons: function (e) {
if( this._typeTimer ){
clearInterval( this._typeTimer );
this._typeTimer = null;
}
},
/**
* Runs a continuous interval to check the current search value, and call the search function
*
* @param {event} e Event object
* @returns {void}
*/
_typing: function () {
var textElem = this.scope.find('[data-role="emoticonSearch"]');
if( this._lastVal == textElem.val() ){
return;
}
if( textElem.val() == '' ){
this._clearSearch();
} else {
this._doSearch( textElem.val() );
}
this._lastVal = textElem.val();
},
/**
* Clears the search panel (called when value is empty)
*
* @returns {void}
*/
_clearSearch: function () {
if( this.scope.find('[data-panel="search"]') ){
this.scope.find('[data-panel="search"]').remove();
// Display the previous panel
if( !this._lastActive ){
this._lastActive = 'overview';
}
this.scope.find('[data-panel="' + this._lastActive + '"]').show();
}
},
/**
* Finds emoticons matching the value
*
* @param {string} value Search value
* @returns {void}
*/
_doSearch: function (value) {
var panel = this._getSearchPanel();
var matchEmoticons = [];
for( var cat in this._emoticonsObj ){
var catLength = this._emoticonsObj[cat].emoticons.length;
// Loop through each emoticon, and do a very simple indexOf search on the
// src and text properties
for( var i = 0; i < catLength; i++ ){
if( this._emoticonsObj[cat].emoticons[i].src.indexOf( value ) !== -1 ||
this._emoticonsObj[cat].emoticons[i].text.indexOf( value ) !== -1 ){
matchEmoticons.push( {
src: this._emoticonsObj[cat].emoticons[i].src,
text: this._emoticonsObj[cat].emoticons[i].text
});
}
}
}
if( !matchEmoticons.length ){
panel.html( ips.templates.render('core.editor.emoticonNoResults') );
} else {
var rows = this._buildEmoticonRows( matchEmoticons );
var content = ips.templates.render('core.editor.emoticonCategory', {
emoticons: rows.join(''),
title: ips.getString('emoticonSearch'),
categoryID: 'search'
});
panel.html( ips.templates.render('core.editor.emoticonSection', {
content: content,
id: 'search'
}));
}
},
/**
* Gets the search results panel, or builds it
*
* @returns {element}
*/
_getSearchPanel: function () {
var panel = this.scope.find('[data-panel="search"]');
if( !panel.length ){
this.scope
.find('.ipsMenu_innerContent')
.append( ips.templates.render('core.editor.emoticonSection', {
content: '',
id: 'search'
}));
panel = this.scope.find('[data-panel="search"]');
// Now hide all panels, then fade in the one we want
this.scope.find('[data-panel]').hide();
ips.utils.anim.go( 'fadeIn', this.scope.find('[data-panel="search"]') );
}
return panel;
},
/**
* Event handler called when the emoticons menu is opened.
* Iniializes the menu if it hasn't already been done
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
menuOpened: function (e, data) {
if( data.elemID == 'elEmoticons' ){
data.originalEvent.preventDefault();
// Do we need to init?
if( !this._doneInit ){
this._buildEmoticonsPanel();
this._buildOverview();
}
}
},
/**
* Event handler called when the a category is selected
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
changeCategory: function (e, data) {
if( !$.contains( this.scope.get(0), data.triggerElem ) ){
return;
}
data.originalEvent.preventDefault();
e.stopPropagation();
var categoryID = data.selectedItemID || false;
// See if we've got an invalid ID for some reason
if( categoryID === false || ( categoryID != 'overview' && !this._emoticonsObj[ categoryID ] ) ){
return;
}
// Already showing this one?
if( this.scope.find('[data-panel="' + categoryID + '"]').is(':visible') ){
return;
}
// If the panel doesn't already exist, build it
if( !this.scope.find('[data-panel="' + categoryID + '"]').length ){
var primaryContent = '';
var rows = this._buildEmoticonRows( this._emoticonsObj[ categoryID ].emoticons );
primaryContent = ips.templates.render( 'core.editor.emoticonCategory', {
emoticons: rows.join(''),
title: this._emoticonsObj[ categoryID ].title,
categoryID: categoryID
});
this.scope
.find('.ipsMenu_innerContent')
.append( ips.templates.render('core.editor.emoticonSection', {
content: primaryContent,
id: categoryID
}));
}
// Now hide all panels, then fade in the one we want
this.scope.find('[data-panel]').hide();
ips.utils.anim.go( 'fadeIn', this.scope.find('[data-panel="' + categoryID + '"]') );
},
/**
* Called by the initialization function. Sets up the panel, building the initial overview
* list and compiling templates we'll need later.
*
* @returns {void}
*/
_buildEmoticonsPanel: function () {
// Build the categories menu
var menuContent = [];
for( var cat in this._emoticonsObj ){
menuContent.push( ips.templates.render('core.editor.emoticonMenu', {
title: this._emoticonsObj[ cat ].title,
count: this._emoticonsObj[ cat ].total,
categoryID: cat
}));
}
$( this.scope ).find('[data-role="categoryMenu"]').append( menuContent.join('') );
// Mark as done
this._doneInit = true;
},
_getJSON: function () {
var deferred = $.Deferred();
var self = this;
// If there's already a request going, abort it
if( this._ajaxObj && this._ajaxObj.abort ){
this._ajaxObj.abort();
}
this._ajaxObj = ips.getAjax();
// Set loading to true
this._setLoading();
this._ajaxObj( ips.getSetting('baseURL') + '?app=core&module=system&controller=editor&do=emoticons', {
dataType: 'json'
})
.done( function (emoticons) {
ips.setSetting('emoticons', emoticons);
self._emoticonsObj = emoticons;
deferred.resolve();
})
.fail( function () {
deferred.reject();
})
.always( function () {
self._setLoadingDone();
});
return deferred.promise();
},
/**
* Sets the panel as loading - adds a loading thingy, disables the search box
* and hides the category menu
*
* @returns {void}
*/
_setLoading: function () {
this.scope
.find('[data-panel="loading"]')
.remove()
.end()
.find('.ipsMenu_innerContent')
.append( ips.templates.render('core.editor.emoticonSection', {
content: '',
id: 'loading'
}))
.find('[data-panel="loading"]')
.css( { height: '150px' } )
.addClass('ipsLoading')
.end()
.end()
.find('[data-role="emoticonSearch"]')
.prop('disabled', true)
.end()
.find('[data-role="categoryTrigger"]')
.hide();
},
/**
* Removes the loading state - removes loading thing, enabled textbox, shows category menu
*
* @returns {void}
*/
_setLoadingDone: function () {
this.scope
.find('[data-panel="loading"]')
.remove()
.end()
.find('[data-role="emoticonSearch"]')
.prop('disabled', false)
.end()
.find('[data-role="categoryTrigger"]')
.show();
},
/**
* Fills the overview panel with up to 21 emoticons from each category
*
* @returns {void}
*/
_buildOverview: function () {
// 09/09/15 - 4.1 - Converting this cookie to use localStorage to avoid a mod_security issue
// identified in https://community.invisionpower.com/4bugtrack/active-reports/recentemoticons-r4570/
// @todo remove in a future release
this._convertEmoticonsToLocalStorage();
var primaryContent = [];
var recentlyUsed = ips.utils.db.get('emoticons', 'recent' );
if( recentlyUsed ){
var rows = this._buildEmoticonRows( recentlyUsed, 7 );
primaryContent.push( ips.templates.render('core.editor.emoticonCategory', {
emoticons: rows.join(''),
title: ips.getString('emoticonRecent'),
categoryID: 'recent'
}));
}
// Build the emoticons
// Loop through each category, then loop through each emoticon in each
for( var cat in this._emoticonsObj ){
var rows = this._buildEmoticonRows( this._emoticonsObj[ cat ].emoticons, 21 );
primaryContent.push( ips.templates.render('core.editor.emoticonCategory', {
emoticons: rows.join(''),
title: this._emoticonsObj[ cat ].title,
categoryID: cat
}));
}
this.scope
.find('.ipsMenu_innerContent')
.html( ips.templates.render('core.editor.emoticonSection', {
content: primaryContent.join(''),
id: 'overview'
}));
},
/**
* Given an array of emoticons, builds rows of emoticons for panels, up to the maximum number of
* emoticons specified
*
* @param {array} emoticons Array of emoticon data
* @param {number} max Max number of emoticons to show. Shows all if not specified
* @returns {array} Array of rows, ready to be joined as a string
*/
_buildEmoticonRows: function (emoticons, max) {
if( !max ){
max = emoticons.length;
}
var thisCategory = [];
var emoticonArray = [];
var pos = 0;
for( var i = 0; i < max; i++ ){
if( pos == 0 && _.isUndefined( emoticons[ i ] ) ){
break;
}
if( !_.isUndefined( emoticons[ i ] ) ){
/* The user may have an emoticon cookie that doesn't contain these new items */
var opts = {
"srcset": !_.isUndefined( emoticons[i].srcset ) ? emoticons[i].srcset : '',
"width": !_.isUndefined( emoticons[i].width ) ? emoticons[i].width : 'auto',
"height": !_.isUndefined( emoticons[i].height ) ? emoticons[i].height : 'auto'
};
emoticonArray.push( ips.templates.render('core.editor.emoticonItem', {
tag: emoticons[i].text,
src: emoticons[i].src,
srcset: ( opts.width !== 'auto' && opts.height !== 'auto' ) ? opts.srcset : '',
width: opts.width,
height: opts.height,
img: ""
}));
this._testEmoticon( emoticons[i].src );
} else {
emoticonArray.push( ips.templates.render('core.editor.emoticonBlank') );
}
pos++;
if( pos == 7 || ( i + 1 ) == max ){
thisCategory.push( ips.templates.render('core.editor.emoticonRow', { emoticons: emoticonArray.join('') }) );
pos = 0;
emoticonArray = [];
}
}
return thisCategory;
},
/**
* Test if an image actually exists before adding it to the array
*
* @param {string} emoticon Emoticon URL
*/
_testEmoticon: function( emoticon ) {
var image = new Image();
var noerror = true;
image.onerror = function(){
$('[data-src="' + emoticon + '"]').remove();
}
image.src = emoticon;
}
});
}(jQuery, _));]]> parseInt( widthInput.attr('max') ) ) {
error = true;
widthInput.closest('.ipsFieldRow').addClass('ipsFieldRow_error');
this.scope.find('[data-role="imageSizeWarning"]').text( ips.getString( 'editorImageMaxWidth', { 'maxwidth': widthInput.attr('max') } ) );
}
if ( parseInt( heightInput.val() ) > parseInt( heightInput.attr('max') ) ) {
error = true;
widthInput.closest('.ipsFieldRow').addClass('ipsFieldRow_error');
this.scope.find('[data-role="imageSizeWarning"]').text( ips.getString( 'editorImageMaxHeight', { 'maxheight': heightInput.attr('max') } ) );
}
if ( !error ) {
var editor = $( 'textarea[name="' + $( this.scope ).data('editorid') + '"]' ).closest('[data-ipsEditor]').data('_editor');
editor.updateImage( widthInput.val(), heightInput.val(), this.scope.find('[data-role="imageAlign"]:checked').val(), this.scope.find('[data-role="imageLink"]').val() );
this.trigger('closeDialog');
}
}
});
}(jQuery, _));]]> 0 ) );
button.toggleClass('ipsButton_disabled', !( _.size( this._selectedItems ) > 0 ) );
if( !_.size( this._selectedItems ) ){
button.text( ips.getString('insertSelected') );
} else {
button.text( ips.pluralize( ips.getString('insertSelectedNum'), _.size( this._selectedItems ) ) );
}
},
/**
* Builds insertable element data based on the provides attached file element
*
* @param {element} element The element on which the insert is based
* @returns {void}
*/
_buildInsertData: function (element) {
var element = element.closest('.ipsAttach');
var fileID = element.attr('data-fileid');
var type = ( element.attr('data-isImage') ) ? 'image' : 'file';
var url = '';
var image = '';
var extension = '';
if( type == 'image' ){
url = ( element.attr('data-thumbnailurl') ) ? element.attr('data-thumbnailurl') : element.attr('data-fullsizeurl');
if( url != element.attr('data-fullsizeurl') ){
image = element.attr('data-fullsizeurl');
}
} else {
url = ( element.attr('data-filelink') ) ? element.attr('data-filelink') : '';
}
extension = element.closest('[data-extension]').attr('data-extension');
return {
fileID: fileID,
type: type,
title: ( type != 'image' ) ? element.find('[data-role="title"]').html() : '',
link: url,
fullImage: image,
extension: extension
};
},
/**
* Builds an element that can be inserted into the editor
*
* @param {object} item Item data used to build element
* @returns {string}
*/
_buildInsert: function (item) {
var element = null;
if( item.type == 'image' ){
element = $('').attr({
'data-fileid': item.fileID,
'src': item.link
}).addClass('ipsImage ipsImage_thumbnailed');
if ( item.extension ) {
element.attr( 'data-extension', item.extension );
}
if( item.fullImage ){
var link = $('').attr( 'href', item.fullImage ).addClass('ipsAttachLink ipsAttachLink_image');
element.addClass( 'ipsImage_thumbnailed');
link.append( element );
element = link;
}
} else {
element = $('').addClass('ipsAttachLink').html( item.title );
if( item.link ){
element.attr( 'href', item.link );
} else {
element.attr( 'href', ips.getSetting('baseURL') + 'applications/core/interface/file/attachment.php?id=' + item.fileID );
}
}
if ( item.extension )
{
element.attr( 'data-extension', item.extension );
}
return $('').append( element ).html();
}
});
}(jQuery, _));]]>/g, '>') ).removeAttr('data-cke-saved-href');
} else {
if ( $( this.scope ).data('image') ) {
var element = CKEDITOR.dom.element.createFromHtml( "" );
element.setAttribute( 'src', url.replace(/'/g, ''').replace(/"/g, '"').replace(//g, '>') );
} else {
var title = this.scope.find('[data-role="linkText"]').val().replace( / {2}/g,' ' );
if ( !title ) {
title = decodeURI( url );
}
title = title.replace(/&/g, '&').replace(//g, '>');
var element = CKEDITOR.dom.element.createFromHtml( "" + title + "" );
element.setAttribute( 'href', url.replace(/'/g, ''').replace(/"/g, '"').replace(//g, '>') );
}
this.scope.find('input.cEditorURL').val('');
editor.insertElement( element );
}
this.trigger('closeDialog');
},
/**
* Event handler for remove link button
*
* @param {event} e Event object
* @returns {void}
*/
removeLink: function (e) {
e.preventDefault();
e.stopPropagation();
var editor = CKEDITOR.instances[ $( this.scope ).data('editorid') ];
var selection = editor.getSelection();
var selectedElement = selection.getSelectedElement();
if ( selectedElement && selectedElement.is('img') ) {
var selectedElement = $( selection.getSelectedElement().$ );
selectedElement.parent().replaceWith( selectedElement );
} else {
editor.insertHtml( this.scope.find('[data-role="linkText"]').val().replace( / {2}/g,' ' ) );
}
this.trigger('closeDialog');
},
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.editor.mymedia.js - My media controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.global.editor.mymedia', {
initialize: function () {
$( window ).on( 'resize', _.bind( this._resizeContentArea, this ) );
this.setup();
},
setup: function () {
this._resizeContentArea();
},
/**
* Resizes the mymedia content area to be the correct height for the dialog
*
* @returns {void}
*/
_resizeContentArea: function () {
// Get size of dialog content
var dialogHeight = this.scope.closest('.ipsDialog_content').outerHeight();
var controlsHeight = this.scope.find('.cMyMedia_controls').outerHeight();
// Set the content area to that height
this.scope.find('[data-role="myMediaContent"]').css({
height: ( dialogHeight - controlsHeight - 10 ) + 'px'
});
}
});
}(jQuery, _)); 0 ){
$( this ).parent().removeClass('ipsHide');
}
});
},
/**
* Intercepts the addUploaderFile event from ips.ui.uploader, so that we can show
* the attachment differently depending on whether it's a file or image.
*
* @param {event} e Event Object
* @param {event} data Event data object
* @returns {void}
*/
removeAllFiles: function (e, data) {
this.scope.find('[data-role="files"], [data-role="images"]').hide();
this.scope.find('[data-role="fileList"]').hide();
},
/**
* Intercepts the addUploaderFile event from ips.ui.uploader, so that we can show
* the attachment differently depending on whether it's a file or image.
*
* @param {event} e Event Object
* @param {event} data Event data object
* @returns {void}
*/
addUploaderFile: function (e, data) {
e.stopPropagation();
var container = null;
var template = 'core.attachments.';
this.scope.find('[data-role="fileList"]').show();
// Show the appropriate container for this kind of file
if( data.isImage ){
container = this.scope.find('[data-role="images"]');
template += 'imageItem';
} else {
container = this.scope.find('[data-role="files"]');
template += 'fileItem';
}
container
.show()
.find('[data-role="fileContainer"]')
.append( ips.templates.render( template, data ) );
},
/**
* Event handler for the fileDeleted event from ips.ui.uploader. Hides our
* attachment container if no more files exist.
*
* @param {event} e Event Object
* @param {event} data Event data object
* @returns {void}
*/
fileDeleted: function (e, data) {
var count = 0;
// See if we need to hide either of the containers
this.scope.find('[data-role="fileContainer"]').each( function () {
if( !$( this ).find('.ipsAttach').length ){
$( this ).closest('[data-role="files"], [data-role="images"]').hide();
count++;
}
});
if( count == 2 ){
this.scope.find('[data-role="fileList"]').hide();
}
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2015 Invision Power Services - http://www.invisionpower.com
*
* ips.files.form.js - ACP Files Form Stuffs
*
* Author: MTM
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.files.form', {
initialize: function () {
if ( this.scope.find('input[name=filestorage_move]') )
{
$('#form_filestorage_move').hide();
this.on( 'submit', 'form.ipsForm_horizontal', this.submitForm );
}
},
/**
* Check if move is needed
*
* @param {event} e Event object
* @returns {void}
*/
submitForm: function (e) {
var self = this;
if( $( e.currentTarget ).attr('data-bypassValidation') ){
return true;
}
e.preventDefault();
$( e.currentTarget ).attr('data-bypassValidation', true);
// Start the request
ips.getAjax()( $( e.currentTarget ).attr('action').replace('do=configurationForm', 'do=checkMoveNeeded'), {
data: $( e.currentTarget ).serialize(),
type: 'post'
})
.done( function (response) {
if ( response.needsMoving )
{
ips.ui.alert.show({
type: 'confirm',
message: ips.getString('files_overview_move_desc'),
icon: 'fa fa-warning',
buttons: {
ok: ips.getString('files_overview_move'),
cancel: ips.getString('files_overview_leave')
},
callbacks: {
ok: function () {
$('input[name=filestorage_move_checkbox]').prop('checked', true);
$( e.currentTarget ).submit();
},
cancel: function () {
$('input[name=filestorage_move_checkbox]').prop('checked', false);
$( e.currentTarget ).submit();
}
}
});
}
else {
$( e.currentTarget ).submit();
}
});
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.ignore.existing.js - Controller for an ignored user on ignore preferences page
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.ignore.existing', {
initialize: function () {
this.on( 'menuItemSelected', '[data-action="ignoreMenu"]', this.ignoreMenu );
this.on( 'submitDialog', this.editedUser );
},
/**
* Event handler for edit dialog saving
*
* @param {event} e Event object
* @param {object} data Data object from the event. Contains token information.
* @returns {void}
*/
editedUser: function (e, data) {
this.trigger('refreshResults');
ips.ui.flashMsg.show( ips.getString('editedIgnore') );
},
/**
* Event handler for the ignore menu
*
* @param {event} e Event object
* @param {object} data Data object from the event. Contains token information.
* @returns {void}
*/
ignoreMenu: function (e, data) {
data.originalEvent.preventDefault();
switch (data.selectedItemID) {
case 'remove':
this._removeIgnore(e, data);
break;
}
},
/**
* Removes the ignore from this user
*
* @param {event} e Event object
* @param {object} data Data object from the event. Contains token information.
* @returns {void}
*/
_removeIgnore: function (e, data) {
var url = data.menuElem.find('[data-ipsMenuValue="remove"] a').attr('href');
var self = this;
Debug.log('here');
// Confirm it
ips.ui.alert.show( {
type: 'confirm',
icon: 'question',
message: ips.getString('confirm_unignore'),
subText: ips.getString('confirm_unignore_desc'),
callbacks: {
ok: function () {
ips.getAjax()( url )
.done( function (response) {
self.trigger('refreshResults');
});
}
}
});
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.members.restrictions.js - Controller for restrictions screen
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.members.restrictions', {
initialize: function () {
this.on( 'click', '.acpRestrictions_subHeader h3', this.toggleSubHeader );
this.on( 'change', '.acpRestrictions_header input[type="checkbox"]', this.toggleHeader );
this.on( 'change', '.acpAppRestrictions_header input[type="checkbox"]', this.toggleAppHeader );
this.on( 'click', '[data-action="checkAll"]', this.checkAll );
this.on( 'click', '[data-action="checkNone"]', this.checkNone );
this.on( 'click', '[data-action="expandAll"], [data-action="collapseAll"]', this.toggleDisplay );
this.setup();
},
/**
* Event handler for both the expand and collapse links
*
* @param {event} e Event object
* @returns {void}
*/
toggleDisplay: function (e) {
e.preventDefault();
var row = $( e.currentTarget ).closest('.acpRestrictions_header');
var subHeaders = row.next().find('.acpRestrictions_subHeader');
var self = this;
var action = ( $( e.currentTarget ).attr('data-action') == 'expandAll' ) ? 'expand' : 'collapse';
subHeaders.each( function () {
if( action == 'expand') {
self._expandSection( $( this ) );
} else {
self._collapseSection( $( this ) );
}
});
},
/**
* Disables all the toggles in an app when the app header is unchecked
*
* @param {event} e Event object
* @returns {void}
*/
toggleAppHeader: function (e) {
var check = $( e.currentTarget );
var row = check.closest('.acpAppRestrictions_header');
if( !check.is(':checked') ){
row
.siblings('.acpAppRestrictions_panel')
.find('.acpRestrictions_header input[type="checkbox"]')
.each( function () {
// Loops through each checkbox, disables it, stores the original state as an attr,
// unchecks it, and triggers a change event to update the JS toggle widget.
$( this )
.prop('disabled', true)
.attr( 'data-originalState', $( this ).is(':checked') )
.attr( 'checked', false )
.trigger('change');
});
} else {
// Top panel rows
row
.siblings('.acpAppRestrictions_panel')
.find('input[type="checkbox"]')
.each( function () {
var thisCheck = $( this );
thisCheck.prop( 'disabled', false );
if( thisCheck.attr('data-originalState') == 'true' ){
thisCheck
.prop( 'checked', true )
.trigger('change');
}
});
// Sub panel rows
row
.siblings('.acpAppRestrictions_panel')
.find('.acpRestrictions_panel input[type="checkbox"]')
.each( function () {
var thisCheck = $( this );
var checked = thisCheck.closest('.acpRestrictions_panel').siblings('.acpRestrictions_header').find('input[type="checkbox"]').is(':checked');
if( !checked ){
thisCheck.prop('disabled', true);
} else {
thisCheck.prop('disabled', false);
}
if( thisCheck.attr('data-originalState') == 'true' ){
thisCheck
.prop( 'checked', true )
.trigger('change');
}
});
}
},
/**
* Disables all the toggles in a section when the section header is unchecked
*
* @param {event} e Event object
* @returns {void}
*/
toggleHeader: function (e) {
var check = $( e.currentTarget );
var row = check.closest('.acpRestrictions_header');
var unChecked = !check.is(':checked');
row
.next()
.find('input[type="checkbox"]')
.each( function () {
var thisCheck = $( this );
if( unChecked ){
thisCheck
.attr( 'data-originalState', thisCheck.is(':checked') )
.prop( 'checked', false );
} else if ( ( thisCheck.attr('data-originalState') == 'true' ) ){
thisCheck.prop( 'checked', true );
}
thisCheck
.prop( 'disabled', unChecked )
.trigger('change');
});
},
/**
* Toggles the display of a section when a subheader is clicked
*
* @param {event} e Event object
* @returns {void}
*/
toggleSubHeader: function (e) {
var header = $( e.currentTarget ).parent();
if( header.hasClass('acpRestrictions_open') ){
this._collapseSection( header );
} else {
this._expandSection( header );
}
},
/**
* Displays a section with animation
*
* @param {element} section The section to show
* @returns {void}
*/
_expandSection: function (section) {
var next = section.next('ul');
section
.addClass('acpRestrictions_open')
.removeClass('acpRestrictions_closed');
ips.utils.anim.go( 'fadeInDown fast', next );
},
/**
* Hides a section
*
* @param {element} section The section to hide
* @returns {void}
*/
_collapseSection: function (section) {
section
.removeClass('acpRestrictions_open')
.addClass('acpRestrictions_closed');
},
/**
* Checks all toggles in the section, opening the section too if necessary
*
* @param {event} e Event object
* @returns {void}
*/
checkAll: function (e) {
e.preventDefault();
var self = this;
var header = $( e.currentTarget ).parents('.acpRestrictions_subHeader');
var next = header.next('ul');
// If the section isn't visible, do the toggling after the section has
// animated in, so that the user can see the change happen. Otherwise, just do it immediately
if( !next.is(':visible') ){
next.animationComplete( function () {
self._togglePermissions( true, next );
});
this._expandSection( header );
} else {
this._togglePermissions( true, next );
}
},
/**
* Unchecks all toggles in the section, opening the section too if necessary
*
* @param {event} e Event object
* @returns {void}
*/
checkNone: function (e) {
e.preventDefault();
var self = this;
var header = $( e.currentTarget ).parents('.acpRestrictions_subHeader');
var next = header.next('ul');
// If the section isn't visible, do the toggling after the section has
// animated in, so that the user can see the change happen. Otherwise, just do it immediately
if( !next.is(':visible') ){
next.animationComplete( function () {
self._togglePermissions( false, next );
});
this._expandSection( header );
} else {
this._togglePermissions( false, next );
}
},
/**
* Sets all checkboxes to the given state in the given container
*
* @param {boolean} state The state to which checkboxes will be set
* @param {element} container The container in which the checkboxes must exist
* @returns {void}
*/
_togglePermissions: function (state, container) {
container.find('input[type="checkbox"]:not( [disabled] )').prop('checked', state).change();
},
/**
* Setup method
* Collapses all sections initially
*
* @returns {void}
*/
setup: function () {
this.scope
.find('.acpRestrictions_open')
.removeClass('acpRestrictions_open')
.addClass('acpRestrictions_closed');
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.messages.folderDialog.js - Folder naming dialog controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.messages.folderDialog', {
_events: {
add: 'addFolder',
rename: 'renameFolder'
},
initialize: function () {
this.on( 'submit', 'form', this.submitName );
},
/**
* Responds to the model event indicating the folder has been marked as read
*
* @param {event} e Event object
* @returns {void}
*/
submitName: function (e) {
e.preventDefault();
e.stopPropagation();
var type = this.scope.attr('data-type');
var field = this.scope.find('[data-role="folderName"]');
var val = field.val();
var folderID = field.attr('data-folderID');
this.trigger( this._events[ type ] + '.messages', {
folder: folderID,
name: val
});
this.trigger('closeDialog');
}
});
}(jQuery, _)); 1 ) ? ips.pluralize( ips.getString( 'messagesDeleteMany' ), count ) : ips.getString('messagesDelete'),
subText: ( count > 1 ) ? ips.getString( 'messagesDeleteManySubText' ) : ips.getString('messagesDeleteSubText'),
callbacks: {
ok: function () {
// Get IDs
var ids = _.map( self.scope.find('[data-role="moderation"]:checked'), function (item) {
return $( item ).closest('[data-messageid]').attr('data-messageid');
});
self.trigger('deleteMessages.messages', {
id: ids
});
}
}
});
},
/**
* Deleted multiple messages using the pageAction widget
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
deleteMessagesDone: function (e, data) {
// Build a selector to find the messages
var selector = _.map( data.id, function (item) {
return '[data-messageid="' + item + '"]';
}).join(',');
// Get the messages
var self = this;
var messages = this._messageList.find( selector );
if( messages.length ){
messages.slideUp( {
complete: function () {
messages.remove();
// Is our selected message one of those deleted?
if( data.id.indexOf( self._currentMessageID ) !== -1 ){
self._currentMessageID = null;
// Are there any other messages we can show?
if( self._messageList.find('[data-messageid]').length ){
self._messageList.find('[data-messageid]').first().click();
} else {
self.trigger( 'getFolder', {
folderID: self._currentFolder
});
}
}
// Refresh the page action so it hides
try {
ips.ui.pageAction.getObj( self.scope.find('[data-ipsPageAction]') ).reset();
ips.ui.autoCheck.getObj( self.scope.find('[data-ipsAutoCheck]') ).refresh();
} catch (err) {}
},
queue: false
}).fadeOut({
queue: false
});
}
},
/**
* Event handler for the search box. Starts a timer so that a search happens 500ms after
* the user stops typing
*
* @param {event} e Event object
* @returns {void}
*/
inputSearch: function (e) {
clearTimeout( this._searchTimer );
this._searchTimer = setTimeout( _.bind( this._startSearch, this ), 500 );
},
/**
* Event handler for model search loading event
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
searchFolderLoading: function (e, data) {
this.scope.find('[data-role="messageSearchText"]').addClass('ipsField_loading');
},
/**
* Event handler for model search done event
* Updates the message list, hides the filders and shows the cancel button
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
searchFolderDone: function (e, data) {
//this.cleanContents();
this._messageList
.html( data.response )
.show()
.end()
.find('[data-role="loading"]')
.hide()
.end()
.find('[data-role="messageListFilters"]')
.hide();
this.scope.find('[data-action="messageSearchCancel"]').show();
$( document ).trigger( 'contentChange', [ this._messageList ] );
},
/**
* Event handler for model search finished event
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
searchFolderFinished: function (e, data) {
this.scope.find('[data-role="messageSearchText"]').removeClass('ipsField_loading');
},
/**
* Event handler for clicking the cancel search button
*
* @param {event} e Event object
* @returns {void}
*/
cancelSearch: function (e) {
e.preventDefault();
this._resetSearch();
this._getFolder( this._currentFolder );
},
/**
* A reply in a message was deleted
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
deletedReply: function (e, data) {
var count = this._messageList.find('[data-messageid="' + data.messageID + '"] .ipsCommentCount').text();
this._messageList.find('[data-messageid="' + data.messageID + '"] .ipsCommentCount').text( parseInt( count ) - 1 );
},
/**
* Updates the reply count for a message
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
updateReplyCount: function (e, data) {
this._messageList
.find('[data-messageid="' + data.messageID + '"] .ipsCommentCount')
.text( data.count );
},
/**
* Responds to the model event indicating the folder has been marked as read
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
markFolderDone: function (e, data) {
if( data.folder == this._currentFolder ){
this._messageList
.find('[data-messageid]')
.removeClass('ipsDataItem_unread')
.find('.ipsItemStatus')
.remove();
}
},
/**
* Responds to the model event indicating an individual message has been deleted
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
deleteMessageDone: function (e, data) {
// See if the deleted message exists in the list
var message = this._messageList.find('[data-messageid="' + data.id + '"]');
if( message.length ){
ips.utils.anim.go( 'fadeOutDown', message ).done( function () {
message.remove();
});
this._currentMessageID = null;
}
},
/**
* Responds to model event indicating message has moved. If the message is in this list, we remove it.
* If the message is the selected message, we also select the next or previous message
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
moveMessageDone: function (e, data) {
// If this message is in the list, remove it
var message = this._messageList.find('[data-messageid="' + data.id + '"]');
var next = null;
if( this._currentMessageID == data.id ){
// Get the prev or next message
if( message.prev('[data-messageid]').length ){
next = message.prev('[data-messageid]');
} else if( message.next('[data-messageid]').length ){
next = message.next('[data-messageid]');
}
}
if( message.length && data.to != this._currentFolder ){
ips.utils.anim.go( 'fadeOutDown', message ).done( function () {
message.remove();
});
this._currentMessageID = null;
}
ips.ui.flashMsg.show( ips.getString('conversationMoved') );
if( next ){
next.click();
}
},
/**
* Responds to the model event indicating a folder has been successfully loaded into the list
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
loadFolderDone: function (e, data) {
//this.cleanContents();
this._messageList
.html( data.response )
.show()
.end()
.find('[data-role="loading"]')
.hide();
$( document ).trigger( 'contentChange', [ this._messageList ] );
},
/**
* Responds to the model indicating new results are loading
* Shows a loading thingy in place of the list
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
loadFolderLoading: function (e, data) {
if( !this.scope.find('[data-role="loading"]').length ){
this._messageList.after(
$('')
.addClass('ipsLoading')
.html(' ')
.css( { minHeight: '150px' } )
.attr('data-role', 'loading')
);
}
this._messageList.hide();
this._hideEmpty();
this.scope.find('[data-role="loading"]').show();
},
/**
* Responds to the model indicating loading a folder has finished
* Shows the list again
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
loadFolderFinished: function (e, data) {
this._messageList.show();
this._resetSearch();
},
/**
* Responds when all messenger setup is complete.
* Triggers an event that lets the main and view controllers know which is the current message
*
* @returns {void}
*/
messengerReady: function () {
this._currentMessageID = this._messageList.find('.cMessage_active').attr('data-messageid');
this.trigger( 'setInitialMessage.messages', {
messageID: this._currentMessageID
});
},
/**
* Event handler for clicking on a message in the list
* If it's a single message, we trigger an event to load it, and highlight it
* If a meta key is pressed, we select multiple messages, as well as triggering the event
*
* @param {event} e Event object
* @returns {void}
*/
clickMessage: function (e) {
if( $( e.target ).is('input[type="checkbox"]') ){
return;
}
e.preventDefault();
var messageID = $( e.currentTarget ).attr('data-messageid');
var messageURL = $( e.currentTarget ).find('[data-role="messageURL"]').attr('href');
var messageTitle = $( e.currentTarget ).find('[data-role="messageURL"]').text();
// Selecting one message
//if( ( !e.shiftKey && !e.metaKey ) || this._currentMessageID == null ){
this.trigger( 'selectedMessage.messages', {
messageID: messageID,
messageURL: messageURL,
messageTitle: messageTitle
});
this.trigger('switchTo.filterBar', {
switchTo: 'filterContent'
});
this._selectMessage( messageID );
return;
//}
// Selecting multiple messages
// Get all messages
/*var messages = this._messageList.find('[data-messageid]');
var currentMessage = this._messageList.find('[data-messageid="' + this._currentMessageID + '"]');
var newMessage = $( e.currentTarget );
var currentIndex = messages.index( currentMessage );
var newIndex = messages.index( newMessage );
if( currentIndex < newIndex ){
var collection = currentMessage.nextUntil( newMessage );
} else {
var collection = currentMessage.prevUntil( newMessage );
}
collection = collection.add( currentMessage ).add( newMessage );
var IDs = [];
collection.each( function (idx, item) {
IDs.push( $(item).attr('data-messageid') );
});
this.trigger( 'selectedMessage.messages', {
messageID: IDs
});
this.trigger('switchTo.filterBar', {
switchTo: 'filterContent'
});
this._selectMessages( IDs );*/
},
/**
* Event handler for when a new message is sent
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
newMessage: function (e, data) {
this._updateRow( data.feedID.substr( data.feedID.indexOf('-') + 1 ) );
},
/**
* Event handler for when a message is deleted
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
deletedMessage: function (e, data) {
var feedId = $(e.target).closest('[data-feedid]').attr('data-feedid');
this._updateRow( feedId.substr( feedId.indexOf('-') + 1 ) );
},
/**
* Refresh row in list
*
* @param {int} conversationId The conversation ID
* @returns {void}
*/
_updateRow: function(conversationId) {
var scope = $(this.scope);
ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=messaging&controller=messenger&id=' + conversationId + '&getRow=1' ).done(function(response){
scope.find('[data-messageid="'+conversationId+'"]').html( response );
$( document ).trigger( 'contentChange', [ scope ] );
});
},
/**
* Event handler for the 'sort' menu
* Triggers an event which loads new items into the list based on new sort order
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
changeSort: function (e, data) {
if( data.originalEvent ){
data.originalEvent.preventDefault();
}
var sort = data.selectedItemID;
if( sort ){
/*this.trigger('loadFolder.messages', {
sortBy: sort,
folder: this._currentFolder,
filterBy: this._currentOptions.filter,
});*/
this.trigger('changeSort.messages', {
param: 'sortBy',
value: sort
});
}
},
/**
* Event handler for the 'filter' menu
* Triggers an event which loads new items into the list based on the new filter
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
changeFilter: function (e, data) {
if( data.originalEvent ){
data.originalEvent.preventDefault();
}
var filter = data.selectedItemID;
if( filter ){
/*this.trigger('loadFolder.messages', {
sortBy: this._currentOptions.sort,
folder: this._currentFolder,
filterBy: filter
});*/
this.trigger('changeFilter.messages', {
param: 'filter',
value: filter
});
}
},
/**
* Responds to URL state changes
* Checks whether the folder or current message ID has changed
*
* @returns {void}
*/
stateChange: function () {
var state = History.getState();
if( _.isUndefined( state.data.controller ) || state.data.controller != 'messages' ){
return;
}
var newFilters = false;
if( state.data.params && ( state.data.params.sortBy != this._currentOptions.sortBy || state.data.params.filter != this._currentOptions.filter ) ){
this._currentOptions.sortBy = state.data.params.sortBy;
this._currentOptions.filter = state.data.params.filter;
newFilters = true;
}
if( state.data.folder != this._currentFolder || newFilters ){
this._getFolder( state.data.folder );
}
if( state.data.mid != this._currentMessageID ){
if( _.isArray( state.data.mid ) ){
this._selectMessages( state.data.mid );
} else {
this._selectMessage( state.data.mid );
}
}
},
/**
* Starts a search by triggering on the model
*
* @param {event} e Event object
* @returns {void}
*/
_startSearch: function (e) {
var textField = this.scope.find('[data-role="messageSearchText"]');
var query = textField.val();
this.trigger('searchFolder.messages', {
folder: this._currentFolder,
q: query
});
},
/**
* Resets changes made for searching
*
* @returns {void}
*/
_resetSearch: function () {
// Reset the search box
this.scope.find('[data-role="messageSearchText"]')
.removeClass('ipsField_loading')
.val('');
// Hide the cancel button
this.scope.find('[data-action="messageSearchCancel"]').hide();
// Show the filter bar
this.scope.find('[data-role="messageListFilters"]').show();
},
/**
* Internal method which highlights the message with the given ID
*
* @param {number} id ID of message to highlight
* @returns {void}
*/
_selectMessage: function (id) {
this._messageList
.find('[data-messageid]')
.removeClass('cMessage_active ipsDataItem_selected')
.end()
.find('[data-messageid="' + id + '"]')
.addClass('cMessage_active ipsDataItem_selected')
.find('.ipsItemStatus')
.remove();
this._currentMessageID = id;
},
/**
* Internal method which highlights multiple messages with the given IDs
*
* @param {array} IDs Array of IDs of messages to select
* @returns {void}
*/
_selectMessages: function (IDs) {
var self = this;
this._messageList
.find('[data-messageid]')
.removeClass('cMessage_active ipsDataItem_selected');
_.each( IDs, function (id) {
self._messageList
.find('[data-messageid="' + id + '"]')
.addClass('cMessage_active ipsDataItem_selected');
});
this._currentMessageID = IDs;
},
/**
* Internal handler which triggers an event to get the contents of a folder
*
* @param {string} newFolder ID of the new folder to get
* @returns {void}
*/
_getFolder: function ( newFolder ) {
this.trigger('loadFolder.messages', {
folder: newFolder,
filter: this._currentOptions.filter,
sortBy: this._currentOptions.sortBy
});
this._currentFolder = newFolder;
},
/**
* Hides the 'no messages' text
*
* @returns {void}
*/
_hideEmpty: function () {
this.scope.find('[data-role="emptyMsg"]').hide();
}
});
}(jQuery, _));]]> 1 ){
message = ips.pluralize( ips.getString( 'messageUsersAdded' ), numberMembers );
}
ips.ui.flashMsg.show( message );
if( data.failed && parseInt( data.failed ) > 0 ){
ips.ui.flashMsg.show( ips.getString('messageNotAllUsers') );
}
// Hide the 'add' menu
this.scope.find('#elInviteMember' + this._currentMessageID).trigger('closeMenu');
// Clear the autocomplete
var autocomplete = ips.ui.autocomplete.getObj( this.scope.find('input[name="member_names"]') );
autocomplete.removeAll();
},
/**
* Triggered by the invite user menu being opened
*
* @param {event} e Event object
* @returns {void}
*/
inviteMenuOpened: function (e) {
this.scope.find('[data-role="addUser"] input[type="text"][id$="dummyInput"]').focus();
},
/**
* Event handler for submitting the 'invite users' form
* Triggers the addUser event
*
* @param {event} e Event object
* @returns {void}
*/
addUsersSubmit: function (e) {
e.preventDefault();
var names = $( e.currentTarget ).find('[name="member_names"]').val();
this.trigger( 'addUser.messages', {
id: this._currentMessageID,
names: names
});
},
/**
* The model has blocked a user
*
* @param {event} e Event object
* @param {object} data Data object from model
* @returns {void}
*/
blockUserDone: function (e, data) {
if( data.id != this._currentMessageID ){
return;
}
// Find participant & replace
var participant = this.scope.find('.cMessage_members').find('[data-participant="' + data.member + '"]');
participant.replaceWith( data.response );
ips.ui.flashMsg.show( ips.getString('messageRemovedUser') );
},
/**
* Event handler for the user actions menu
*
* @param {event} e Event object
* @param {object} data Data object from model
* @returns {void}
*/
userAction: function (e, data) {
if( data.originalEvent ){
data.originalEvent.preventDefault();
}
var userID = $( data.triggerElem ).closest('[data-participant]').attr('data-participant');
switch( data.selectedItemID ){
case 'block':
this.trigger('blockUser.messages', {
member: userID,
id: this._currentMessageID
});
break;
case 'unblock':
this.trigger('addUser.messages', {
member: userID,
id: this._currentMessageID,
unblock: true
});
break;
}
},
/**
* The model has deleted a message. If it's the one we're viewing, then remove the content and
* show the placeholder.
*
* @param {event} e Event object
* @param {object} data Data object from model
* @returns {void}
*/
deleteMessageDone: function (e, data) {
var url = ipsSettings['baseURL'] + '?app=core&module=messaging&controller=messenger'
window.location = url;
},
/**
* Event handler for selecting a folder into which this conversation will be moved
*
* @param {event} e Event object
* @param {object} data Data object from menu widget
* @returns {void}
*/
moveConversation: function (e, data) {
if( data.originalEvent ){
data.originalEvent.preventDefault();
}
var self = this;
// Get real name of folder
var realName = $('#elConvoMove_menu').find('[data-ipsMenuValue="' + data.selectedItemID + '"] a').html();
ips.ui.alert.show( {
type: 'confirm',
icon: 'question',
message: ips.getString('conversationMove', { name: realName } ),
callbacks: {
ok: function () {
self.trigger( 'moveMessage.messages', {
id: self._currentMessageID,
folder: data.selectedItemID
});
}
}
});
},
/**
* Event handler for clicking the delete conversation button.
* Confirms the user actually wants to delete it
*
* @param {event} e Event object
* @returns {void}
*/
deleteConversation: function (e) {
e.preventDefault();
var self = this;
ips.ui.alert.show( {
type: 'confirm',
icon: 'question',
message: ips.getString('messagesDelete'),
subText: ips.getString('messagesDeleteSubText'),
callbacks: {
ok: function () {
self.trigger( 'deleteMessage.messages', {
id: self._currentMessageID
});
}
}
});
},
/**
* Responds to the model loading message event
* Shows the loading thingy in the message pane
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
loadMessageLoading: function (e, data) {
this.cleanContents();
this.scope.html(
$('')
.addClass('ipsLoading')
.html(' ')
.css( { minHeight: '150px' } )
);
},
/**
* Responds to the model loaded message event
* Displays the loaded message in the message pane
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
loadMessageDone: function (e, data) {
//this.cleanContents();
this.scope.html( data.response );
$( document ).trigger( 'contentChange', [ this.scope ] );
},
/**
* Responds to pagination event in conversation
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
paginationClicked: function (e, data){
if( data.originalEvent ){
data.originalEvent.preventDefault();
}
/*this.trigger('changePage.messages', {
pageNo: data.pageNo,
perPage: data.perPage,
id: this._currentMessageID
});*/
},
/**
* Responds to event from main messages controller, informing us a message (or messages)
* have been selected. For a single message, we emit an event here to load the contents
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
selectedMessage: function (e, data) {
//if( _.isArray( data.messageID ) ){
//this.scope.html('');
//} else {
this.trigger( 'loadMessage.messages', {
messageID: data.messageID,
messageURL: data.messageURL,
messageTitle: data.messageTitle
});
//}
this._currentMessageID = data.messageID;
},
/**
* Responds to the browser url changing
* We're only interested in watching for the message ID here. If it changes, we fetch a new message
*
* @returns {void}
*/
stateChange: function () {
var state = History.getState();
if( _.isUndefined( state.data.controller ) || state.data.controller != 'messages' ){
return;
}
if( state.data.id == null ){
this.cleanContents();
this.scope.html( ips.templates.render('messages.view.placeholder') );
// Reset values
this._currentMessageID = null;
this._currentPage = null;
return;
}
if( state.data.id != this._currentMessageID ){
// Get message from le model
this.trigger( 'fetchMessage.messages', {
id: state.data.id,
page: state.data.page || 1
});
// Reset values
this._currentMessageID = state.data.id;
this._currentPage = state.data.page || 1;
return;
} else if( state.data.page != this._currentPage ){
this.trigger( 'fetchMessage.messages', {
id: this._currentMessageID,
page: state.data.page
});
this._currentPage = state.data.page;
}
},
/**
* Responds to an event from the main controller letting us know the initially-selected message ID
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
setInitialMessage: function (e, data) {
this._currentMessageID = data.messageID;
},
/**
* Responds to an event from the main controller indicating the selected folder has changed
* We remove any message present and replace it with the placeholder
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
getFolder: function (e, data) {
this.cleanContents();
this.scope.html( ips.templates.render('messages.view.placeholder') );
ips.utils.anim.go( 'fadeIn', this.scope );
},
});
}(jQuery, _));
]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.modcp.approveQueue.js - Controller for using approval queue
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.modcp.approveQueue', {
initialize: function () {
this.on( 'click', '[data-action="approvalQueueNext"]', this.doAction );
},
/**
* Respond when an action button is clicked
*
* @param {event} e Event object
* @param {object} data Event data
* @returns {void}
*/
doAction: function(e,data) {
e.preventDefault();
var scope = $(this.scope);
if ( $( e.currentTarget ).hasClass('ipsButton_disabled') ) {
ips.ui.alert.show({
type: 'alert',
icon: 'warn',
message: ips.getString('approvalQueueNoPerm')
});
return;
}
var height = $('#elApprovePanel').height();
$('#elApprovePanel').html('').css( 'height', height ).addClass('ipsLoading');
ips.getAjax()( $( e.currentTarget ).attr('href'), { bypassRedirect: true } )
.done(function(){
ips.getAjax()( scope.attr('data-url') )
.done(function(response){
scope.html( response.html );
$('#elModCPApprovalCount').html( response.count );
})
.fail(function(failresponse){
window.location = scope.attr('data-url');
});
})
.fail(function(){
window.location = $( e.currentTarget ).attr('href');
});
}
});
}(jQuery, _));
/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.modcp.report.js - Controller for viewing a report
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.modcp.report', {
initialize: function () {
this.on( document, 'submitDialog', '[data-role="warnUserDialog"]', this.dialogSubmitted );
this.on( 'menuItemSelected', this.menuItemSelected );
},
/**
* Respond when a menu item is selected
*
* @param {event} e Event object
* @param {object} data Event data
* @returns {void}
*/
menuItemSelected: function(e, data) {
data.originalEvent.preventDefault();
var link = data.menuElem.find('[data-ipsMenuValue="' + data.selectedItemID + '"] a');
var langString = ( data.selectedItemID == 'spamFlagButton' ) ? ips.getString( 'confirmFlagAsSpammer' ) : ips.getString( 'confirmUnFlagAsSpammer' );
var descString = ( data.selectedItemID == 'spamUnFlagButton' ) ? ips.getString( 'confirmUnFlagAsSpammerDesc' ) : '';
var self = this;
if( data.selectedItemID == 'spamFlagButton' || data.selectedItemID == 'spamUnFlagButton' ){
ips.ui.alert.show({
type: 'confirm',
message: langString,
subText: descString,
callbacks: {
ok: function () {
self._startLoading();
ips.getAjax()( link.attr('href'), {
bypassRedirect: true
} )
.done( function (response) {
self._refreshPanel();
});
},
}
});
}
},
/**
* Respond when a dialog is submitted
*
* @param {event} e Event object
* @param {object} data Event data
* @returns {void}
*/
dialogSubmitted: function(e, data) {
this._startLoading();
this._refreshPanel();
},
/**
* Start Loading
*/
_startLoading: function() {
this.scope
.find('[data-role="authorPanel"]')
.css( 'height', this.scope.find('[data-role="authorPanel"]').height() + 'px' )
.addClass('ipsLoading')
.find('*')
.hide();
},
/**
* Refresh Panel
*/
_refreshPanel: function() {
var self = this;
ips.getAjax()( window.location, {
bypassRedirect: true
} )
.done( function(response){
self.scope
.find('[data-role="authorPanel"]')
.css( 'height', 'auto' )
.removeClass('ipsLoading')
.html( response );
} );
}
});
}(jQuery, _));
/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.modcp.reportList.js - Report list controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.modcp.reportList', {
initialize: function () {
this.on( 'menuItemSelected', '[data-action="changeStatus"]', this.changeReportStatus );
},
/**
* When a reports status is changed, check whether the row needs changing
*
* @param {event} e Event object
* @param {object} data Event data
* @returns {void}
*/
changeReportStatus: function (e, data) {
if( data.originalEvent ){
data.originalEvent.preventDefault();
}
var row = $( e.currentTarget ).closest('.ipsDataItem');
row.removeClass('ipsDataItem_new ipsDataItem_warning');
switch( data.selectedItemID ){
case '1':
row.addClass('ipsDataItem_new');
break;
case '2':
row.addClass('ipsDataItem_warning');
break;
}
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.modcp.reportToggle.js - Controller for report toggling
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.modcp.reportToggle', {
initialize: function () {
this.on( 'menuItemSelected', this.reportToggled );
},
/**
* Report status has been changed
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
reportToggled: function (e, data) {
// Get the menu elem
var item = data.menuElem.find('[data-ipsmenuvalue="' + data.selectedItemID + '"]');
var icon = item.find('[data-role="ipsMenu_selectedIcon"]').attr('class');
var status = item.find('[data-role="ipsMenu_selectedText"]').text();
this.scope.find('[data-role="reportIcon"]').get(0).className = icon;
this.scope.find('[data-role="reportStatus"]').text( status );
// And show a flash message
ips.ui.flashMsg.show( ips.getString('reportStatusChanged') );
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.profile.body.js - Profile body controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.profile.body', {
/**
* Initialize controller events
* Sets up the events from the view that this controller will handle
*
* @returns {void}
*/
initialize: function () {
this.on( 'click', '[data-action="showRecentWarnings"]', this.showRecentWarnings );
this.setup();
},
/**
* Non-event-based setup
*
* @returns {void}
*/
setup: function () {
},
showRecentWarnings: function (e) {
e.preventDefault();
this.scope.find('[data-action="showRecentWarnings"]').hide();
ips.utils.anim.go( 'fadeIn fast', this.scope.find('[data-role="recentWarnings"]') );
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.profile.cropper.js - Cropping controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.profile.cropper', {
_image: null,
_coords: {},
initialize: function () {
this.setup();
},
/**
* Setup method
*
* @returns {void}
*/
setup: function () {
var self = this;
this._image = this.scope.find('[data-role="profilePhoto"]');
this._coords = {
topLeftX: this.scope.find('[data-role="topLeftX"]'),
topLeftY: this.scope.find('[data-role="topLeftY"]'),
bottomRightX: this.scope.find('[data-role="bottomRightX"]'),
bottomRightY: this.scope.find('[data-role="bottomRightY"]'),
};
ips.loader.get( ['core/interface/cropper/cropper.min.js'] ).then( function () {
self._image.imagesLoaded( _.bind( self._startCropper, self ) );
});
},
/**
* Starts the cropping function, called after the image has loaded
*
* @returns {void}
*/
_startCropper: function () {
var self = this;
var width = this._image.width();
var height = this._image.height();
// Resize the wrapper
this._image.closest('[data-role="cropper"]').css({
width: width + 'px',
height: height + 'px'
});
// Initialize cropper
this._image.cropper({
aspectRatio: 1 / 1,
autoCropArea: 0.9,
minContainerWidth: width,
minContainerHeight: height,
crop: function (data) {
self._coords.topLeftX.val( data['x'] );
self._coords.topLeftY.val( data['y'] );
self._coords.bottomRightX.val( data['width'] + data['x'] );
self._coords.bottomRightY.val( data['height'] + data['y'] );
}
});
}
});
}(jQuery, _));').addClass('ipsLoading').css({
height: '300px'
})
);
// Add class to the header to shrink it
this.contentHeader.find('#elProfileHeader').toggleClass( 'cProfileHeaderMinimal', small );
// Load the content
ips.getAjax()( url )
.done( function (response) {
self.contentArea
.hide()
.html( response )
.css({
height: 'auto'
});
ips.utils.anim.go( 'fadeIn fast', self.contentArea );
$( document ).trigger( 'contentChange', [ self.contentArea ] );
})
.fail( function () {
window.location = url;
});
},
/**
* Push a new URL to the browser
*
* @param {object} data Object to save as the state data
* @param {string} title Page title
* @param {string} url Page URL
* @returns {void}
*/
_changeURL: function (data, title, url) {
History.pushState( data, title, url );
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.profile.toggleBlock.js - Toggle blocks on the profile
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.profile.toggleBlock', {
initialize: function () {
this.on( 'click', '[data-action="disable"]', this.toggleBlock );
this.on( 'click', '[data-action="enable"]', this.toggleBlock );
},
/**
* Toggles a block on the profile, loading the new contents via ajax from the target URL
*
* @param {event} e Event object
* @returns {void}
*/
toggleBlock: function (e) {
e.preventDefault();
var self = this;
this.scope.css({
opacity: 0.6
});
ips.getAjax()( $( e.currentTarget ).attr('href'), {
showLoading: true
} )
.done( function (response) {
self.scope.html( response );
$( document ).trigger( 'contentChange', [ self.scope ] );
})
.fail( function () {
window.location = $( e.currentTarget ).attr('href');
})
.always( function () {
self.scope.css({
opacity: 1
});
});
}
});
}(jQuery, _)); 1200 ) ? 1200 : d ).animate({
opacity: 1
});
});
});
})
.fail( function (jqXHR, textStatus) {
window.location = url;
})
.always( function () {
self._setContentLoading( false );
});
},
/**
* Responds to pagination event in conversation
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
paginationClicked: function (e, data){
if( data.originalEvent ){
data.originalEvent.preventDefault();
}
this._formData['page'] = data.pageNo;
var url = this._getUrlFromData( this._formData );
History.pushState( {
controller: 'core.front.search.main',
url: url
}, document.title, url );
var elemPosition = ips.utils.position.getElemPosition( this.scope );
$('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' } );
},
/**
* Returns form data, converting jquery serializedArray objects into
* simple key/value pairs
*
* @param {object} data Form data object
* @returns {object}
*/
_getFormData: function (data) {
if( !_.isObject( data ) ){
return;
}
var returnData = {};
var skipData = [ 'sortby', 'sortDirection', 'page', 'csrfKey' ];
for( var i = 0; i < data.length; i++ ){
if( _.indexOf( skipData, data[ i ].name ) === -1 && data[ i ].value !== '' ){
// If we already have a value set for this param and it isn't an array, juggle it
// so that it is an array and push the existing value into it
if( !_.isUndefined( returnData[ data[ i ].name ] ) && !_.isArray( returnData[ data[ i ].name ] ) ){
var tmp = returnData[ data[ i ].name ];
returnData[ data[ i ].name ] = [];
returnData[ data[ i ].name ].push( tmp );
}
// If we're an array, push it into it, otherwise just set the value
if( !_.isUndefined( returnData[ data[ i ].name ] ) ){
returnData[ data[ i ].name ].push( data[ i ].value );
} else {
returnData[ data[ i ].name ] = data[ i ].value;
}
}
}
return returnData;
},
/**
* Builds a string to use in the browser titlebar
*
* @returns {string}
*/
_getBrowserTitle: function () {
var title = ips.getString('searchTitle');
var currentType = this.scope.find('input[type="radio"][name="type"]:checked');
var q = this._formData['q'];
if( q !== '' && !currentType.length ){
title = ips.getString('searchTitleTerm', {
term: q
});
} else if( q !== '' && currentType.length ){
title = ips.getString('searchTitleTermType', {
term: q,
type: currentType.next('[data-role="searchAppTitle"]').text()
});
} else if( q === '' && this._currentType !== '' ){
title = ips.getString('searchTitleType', {
type: currentType.next('[data-role="searchAppTitle"]').text()
});
}
return title;
},
/**
* Adds css files to the page header
*
* @param {array} css Array of CSS urls to add
* @returns {void}
*/
_addCSS: function (css) {
var head = $('head');
if( css && css.length ){
for( var i = 0; i < css.length; i++ ){
head.append( $('').attr( 'href', css[i] ).attr( 'type', 'text/css' ).attr('rel', 'stylesheet') );
}
}
},
/**
* Toggles the loading state on the main search body area
*
* @param {boolean} showLoading Enable loading state?
* @returns {void}
*/
_setContentLoading: function (state) {
var results = this.scope.find('[data-role="resultsContents"]');
if( !results.length ){
return;
}
var dims = ips.utils.position.getElemDims( results );
var position = ips.utils.position.getElemPosition( results );
if( !this._loadingDiv ){
this._loadingDiv = $('').append(
$('')
.css({
height: '200px'
})
.addClass('ipsLoading')
);
ips.getContainer().append( this._loadingDiv );
}
this._loadingDiv
.show()
.css({
left: position.viewportOffset.left + 'px',
top: position.viewportOffset.top + $( document ).scrollTop() + 'px',
width: dims.width + 'px',
height: dims.height + 'px',
position: 'absolute',
zIndex: ips.ui.zIndex()
})
if( state ){
results
.animate({
opacity: 0.6
})
.css({
height: results.height() + 'px'
});
} else {
results.css({
height: 'auto',
opacity: 1
});
this._loadingDiv.hide();
}
}
});
}(jQuery, _));]]>= 0;
};
var self = this;
try {
this._terms = JSON.parse( this.scope.attr('data-term') );
} catch(err) {
Debug.log("Error parsing search terms");
return;
}
// Process each result
this.scope.find('[data-role="activityItem"]').each( function () {
self._processResult( $( this ) );
});
},
/**
* Event handler for document content change
*
* @returns {void}
*/
contentChange: function () {
var self = this;
this.scope.find('[data-role="activityItem"]').each( function () {
self._processResult( $( this ) );
});
},
/**
* Processes the given element as a search result
*
* @param {element} result The search result element
* @returns {void}
*/
_processResult: function (result) {
// Don't process it twice
if( result.attr('data-processed') ){
return;
}
// Start by locating the first hit
var findWords = result.find('[data-findTerm]');
if( findWords.length ){
this._findWords( findWords );
}
// Now highlight the terms
this._highlight( result );
result.attr('data-processed', true);
},
/**
* Takes a search result, and finds the match within it and then reduces the text
* to just the characters surrounding the match
*
* @param {element} result The element containing the text to work on
* @returns {void}
*/
_findWords: function (result) {
var text = $.trim( result.text() );
var firstMatch = text.length;
var startPoint = 0;
var foundMatches = false;
//-----------
// Step 1: Find the first occurrence of each term
for( var i = 0; i < this._terms.length; i++){
// Note: regexp used here because simple indexOf isn't case insensitive
// and toLowercase doesn't work well with some languages
var indexOf = text.search( new RegExp( this._terms[i], 'i' ) );
if( indexOf !== -1 ){
foundMatches = true;
if( indexOf < firstMatch ){
firstMatch = indexOf;
}
}
}
//-----------
// Step 2: Search backwards to find the closest puncutation mark, which is where we'll start our result snippet
// We'll go back up to half of our result length, but stop if we hit the beginning
var punctuationMarks = ['.', ',', '?', '!'];
var searchBack = ( firstMatch - ( this._resultLength / 2 ) < 0 ) ? 0 : firstMatch - ( this._resultLength / 2 );
// if there were no matches, then we'll just set manual values
if( !foundMatches ){
startPoint = 0;
} else {
for( var j = firstMatch; j > searchBack; j-- ){
if( punctuationMarks.indexOf( text[j] ) !== -1 ){
startPoint = j + 1;
break;
}
}
}
//-----------
// Step 3: Count forward from the starting point to get our snippet
var finalSnippet = $.trim( text.substring( startPoint, startPoint + 300 ) );
if( startPoint > 0 && foundMatches ){
finalSnippet = '...' + finalSnippet;
}
if( startPoint + this._resultLength < text.length || ( !foundMatches && text.length > this._resultLength ) ){
finalSnippet = finalSnippet + '...';
}
result.text( finalSnippet );
},
/**
* Highlight search results
*
* @param {string} term The word(s) to highlight
* @returns {void}
*/
_highlight: function (result) {
// Find the elements we're searching in
var self = this;
var elements = result.find('[data-searchable]');
_.each( this._terms, function (term, index) {
elements.each( function () {
if( !$( this ).is(':icontains("' + term + '")' ) ){
return;
}
$( this ).contents().filter(
function() { return this.nodeType === 3 }
).each( function(){
$( this ).replaceWith( _.escape( $( this ).text().replace( new RegExp( "\\b(" + term + ")\\b", "ig" ), '' + "$1 " + '' ) ).replace( new RegExp("<mark class="ipsMatch" + ( index + 1 ) + "">", 'ig'), "" ).replace( new RegExp("</mark>", 'ig'), "" ) );
} );
});
});
}
});
}(jQuery, _));]]>').append( data.content );
this.scope.find('[data-role="statusComments"]').append( content );
ips.utils.anim.go( 'fadeInDown', content );
ips.ui.editor.getObj( this.scope.find('[data-role="replyComment"] [data-ipsEditor]') ).reset();
$( document ).trigger( 'contentChange', [ this.scope ] );
}
});
}(jQuery, _));]]>').append( response );
var comment = content.find('.ipsComment,.ipsStreamItem').first(); // Must select first(), because statuses can contain sub-comments
feed.prepend( comment );
ips.utils.anim.go( 'fadeInDown', comment );
$( 'textarea[name="' + textarea.attr('name') + '"]' ).closest('[data-ipsEditor]').data('_editor').reset();
$( document ).trigger( 'contentChange', [ comment ] );
})
.fail( function () {
//form.submit();
})
.always( function () {
submit
.prop( 'disabled', false )
.text( currentText );
});
}
});
}(jQuery, _));]]>'),
_timestamp: 0,
_waitingCount: 0,
_autoUpdate: false,
_windowFocus: false,
_streamID: 0,
initialize: function () {
// Figure out the visibility event we need
this.on( 'click', '[data-action="loadMore"]', this.loadMore );
this.on( 'click', '[data-action="insertNewItems"]', this.insertNewItems );
this.on( document, ips.utils.events.getVisibilityEvent(), this.windowVisibilityChange );
this.setup();
},
/**
* Setup method
*
* @returns {void}
*/
setup: function () {
if( this.scope.attr('data-streamID') ){
this._streamID = this.scope.attr('data-streamID');
}
// Get the newest timestamp
this._timestamp = parseInt( this.scope.find('[data-timestamp]').first().attr('data-timestamp') );
if( !_.isUndefined( this.scope.attr('data-autoPoll') ) ){
this._autoUpdate = true;
this.windowVisibilityChange(); // Call our visibility method manually to determine window visibility
}
},
/**
* Event handler for windw visibility changing
* Allows us to slow the rate of auto-updates if the user isn't looking at the page
*
* @returns {void}
*/
windowVisibilityChange: function () {
var hiddenProp = ips.utils.events.getVisibilityProp();
if( !_.isUndefined( hiddenProp ) ){
if( document[ hiddenProp] ){
this._windowFocus = false;
this._startTimer( this._longInterval );
this._killTimer = setTimeout( _.bind( this._stopPolling, this ), this._killUpdate * 60 * 1000 );
Debug.log("Set the kill timer");
} else {
this._windowFocus = true;
this._startTimer( this._shortInterval );
this._updateTitle(0);
clearTimeout( this._killTimer );
Debug.log("Cleared the kill timer");
}
} else {
this._startTimer( this._longInterval );
}
},
/**
* Event handler for the "Load More" button
*
* @param {event} e Event object
* @returns {void}
*/
loadMore: function (e) {
e.preventDefault();
// Get the last timestap being shown
var timestamp = this.scope.find('[data-role="activityItem"]').last().attr('data-timestamp');
// Kill previous slow-running requests
if( this._loadMore && _.isFunction( this._loadMore.abort ) ){
this._loadMore.abort();
}
// Set tht button to loading
this.scope.find('[data-action="loadMore"]').addClass('ipsButton_disabled').text( ips.getString("loading") );
var data = ( this._streamID ) ? { id: this._streamID } : {};
if( this.scope.attr('data-view') ){
_.extend( data, {
view: this.scope.attr('data-view')
});
}
this._loadMore = ips.getAjax()( '?app=core&module=discover&controller=streams', {
data: _.extend( data, {
before: timestamp
})
})
.done( _.bind( this._loadMoreResponse, this ) )
.fail( function () {
ips.ui.alert.show( {
type: 'alert',
message: ips.getString('errorLoadingStream'),
icon: 'warn'
});
});
},
/**
* Event handler for clicking the teaser button when there's new results queued to show
*
* @param {event} e Event object
* @returns {void}
*/
insertNewItems: function (e) {
e.preventDefault();
var insertBefore = null;
// First, we need to figure out the timing bubble. Our new results may contain a bubble too,
// so we have to square that away with the ones already showing in the feed.
if( this.scope.find('[data-timeType="past_hour"]').length ){
insertBefore = this.scope.find('[data-timeType="past_hour"]').siblings('[data-role="activityItem"]').first();
// If we already have a 'past_hour' bubble in the stream, then remove it from the new content
this._newUpdates.find('[data-timeType]').remove();
} else {
insertBefore = this.scope.find('[data-timeType], [data-role="activityItem"]').first();
}
// Add a bar at the end of the content to show the user where they've seen up to
this.scope.find('[data-role="unreadBar"]').remove();
this._newUpdates.append( ips.templates.render('core.streams.unreadBar') );
// Lets set some styles on the items to show
this._newUpdates
.find('[data-role="activityItem"]')
.attr('data-new', true)
.css({
opacity: 0
});
// Insert the new content
insertBefore.before( this._newUpdates.html() );
// Reinit
$( document ).trigger( 'contentChange', [ this.scope ] );
// Remove the teaser button and animate the new items in
this.scope.find('[data-action="insertNewItems"]').remove();
this._animateNewItems();
// Reset our tracking vars
this._newUpdates = $('');
this._waitingCount = 0;
},
/**
* Sets or resets the timer to check for new posts
*
* @param {number} interval Interval (in seconds) between checks
* @returns {void}
*/
_startTimer: function (interval) {
if( !this._autoUpdate ){
return;
}
if( !interval ){
interval = this._shortInterval;
}
if( this._timer ){
clearInterval( this._timer );
}
this._timer = setInterval( _.bind( this._autoFetchNew, this ), interval * 1000 );
},
/**
* Finds the new items in the feed and animates them into view
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
_animateNewItems: function () {
var newItems = this.scope.find('[data-new]');
var delay = 200;
// Slide down to make space for them
newItems.slideDown( function () {
// Now fade in one by one, with a delay
newItems.each( function (index) {
var d = ( index * delay );
$( this ).delay( ( d > 1200 ) ? 1200 : d ).animate({
opacity: 1
}).removeAttr('data-new');
});
});
},
/**
* Handles the response to the Load More ajax request
*
* @param {string} response Response from server
* @returns {void}
*/
_loadMoreResponse: function (response) {
// Are there any more results to show?
// If not, hide the button
if( response.count == 0 ){
this.scope.find('[data-role="loadMoreContainer"]').replaceWith( ips.templates.render('core.streams.noMore') );
return;
}
// Reset the button
this.scope.find('[data-action="loadMore"]').removeClass('ipsButton_disabled').text( ips.getString('loadNewActivity') );
var content = $('
' + response.results + '
');
// Get the latest time bubble
var latestBubble = this.scope.find('[data-timeType]').last().attr('data-timeType');
// If the new content has that bubble, get rid of it
content.find('[data-timeType="' + latestBubble + '"]').remove();
// Get items ready to display
content
.find('[data-role="activityItem"]')
.attr('data-new', true)
.css({
opacity: 0
});
// Get th count of current items in the feed
var existingItems = this.scope.find('[data-role="activityItem"]');
var count = existingItems.length;
// Get the last activity item and insert new content
existingItems.last().after( content );
// Init only the new items
this.scope.find('[data-role="activityItem"]').slice( count ).each( function () {
$( document ).trigger( 'contentChange', [ $( this ) ] );
});
this._animateNewItems();
},
/**
* Stops the auto-updating from running for good
*
* @returns {void}
*/
_stopPolling: function () {
clearInterval( this._timer );
this._autoUpdate = false;
this.scope.find('[data-role="updateMessage"]').html( ips.getString('autoUpdateStopped') );
Debug.log("Stopped polling due to user inactivity");
},
/**
* Updates the browser title with an 'unseen count' of new items
*
* @param {number} count Number of unseen items
* @returns {void}
*/
_updateTitle: function (count) {
// Moved to instant notifications
/*var cleanTitle = $.trim( document.title.replace( /^\(\d+\)/, '' ) );
if( count ){
document.title = "(" + count + ") " + cleanTitle;
} else {
document.title = cleanTitle;
}*/
},
/**
* Method to check for new activity results on the server.
* We don't show the results immediately, otherwise it would bounce the user around the page.
* Instead, we store the new results and show a teaser block at the top of the feed. When
* the user clicks the teaser, then we insert the results.
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
_autoFetchNew: function (e) {
var self = this;
if( !_.isNumber( this._timestamp ) || _.isNaN( this._timestamp ) ){
Debug.log("Timestamp not a number");
// Stop further auto updates
clearInterval( this._timer );
return;
}
// Kill previous slow-running requests
if( this._activityUpdate && _.isFunction( this._activityUpdate.abort ) ){
this._activityUpdate.abort();
}
var data = ( this._streamID ) ? { id: this._streamID } : {};
this._activityUpdate = ips.getAjax()( '?app=core&module=discover&controller=streams', {
data: _.extend( data, {
after: this._timestamp
})
})
.done( function (response) {
// If auto-polling is now disabled, stop everything
if( response.error && response.error == 'auto_polling_disabled' ){
self.scope.find('[data-role="updateMessage"]').remove();
clearInterval( self._timer );
return;
}
// Nothing returned?
if( response.count == 0 || _.isNaN( response.count ) ){
return;
}
// Get the count and latest timestamp
var tmp = $('
' + response.results + '
');
var count = parseInt( response.count );
self._timestamp = parseInt( tmp.find('[data-timestamp]').first().attr('data-timestamp') );
self._waitingCount += count;
// If we have a date bubble in this new content, we need to check any other new content we haven't
// shown yet has them too, and remove them.
if( tmp.find('[data-timeType]').length ){
var type = tmp.find('[data-timeType]').attr('data-timeType');
self._newUpdates.find('[data-timeType="' + type + '"]').remove();
}
self._newUpdates.prepend( tmp.contents() );
// If we aren't focused, update the browser title
if( !self._windowFocus ){
self._updateTitle( self._waitingCount );
}
// Build the teaser
var teaser = ips.templates.render('core.streams.teaser', {
count: self._waitingCount,
words: ips.pluralize( ips.getString('newActivityItems'), [ self._waitingCount, self._waitingCount ] )
});
// If a teaser already exists, replace it; otherwise, insert at top
if( self.scope.find('[data-action="insertNewItems"]').length ){
self.scope.find('[data-action="insertNewItems"]').replaceWith( teaser );
self.scope.find('[data-action="insertNewItems"]').show();
} else {
self.scope.find('[data-role="activityItem"]').first().before( teaser );
self.scope.find('[data-action="insertNewItems"]').slideDown();
}
})
.fail( function (jqXHR, textStatus, errorThrown) {
// Removed so user isn't interrupted
/*ips.ui.alert.show( {
type: 'alert',
message: ips.getString('errorLoadingStream'),
icon: 'warn'
});*/
if( Debug.isEnabled() ){
Debug.error( textStatus );
}
// Stop further auto updates
clearInterval( self._timer );
});
}
});
}(jQuery, _));]]> input[type="checkbox"]');
if( data.selectedItemID == '__all' ){
// Did the user click 'all'? If so, uncheck everything else and hide all content filters
allButAll
.removeClass('ipsSideMenu_itemActive')
.find('> input[type="checkbox"]')
.prop( 'checked', false );
// Make sure 'all' is checked
all.addClass('ipsSideMenu_itemActive');
this.scope.find('input[type="radio"][name="stream_classes_type"][value="0"]').prop( 'checked', true );
// Hide any content filters
this.scope.find('[data-contentType]').slideUp();
} else {
// If we have any checked items, uncheck 'all'
if( allButAllChecks.filter(':checked').length ){
all.removeClass('ipsSideMenu_itemActive')
this.scope.find('input[type="radio"][name="stream_classes_type"][value="1"]').prop( 'checked', true );
// If the selected types have extra filters, show those too
allButAllChecks.filter(':checked').each( function () {
var type = $( this ).closest('[data-ipsMenuValue]').attr('data-ipsMenuValue');
if( self.scope.find('[data-contentType="' + type + '"]').length ){
self.scope.find('[data-contentType="' + type + '"]').slideDown();
}
});
// ...and hide any which aren't checked
allButAllChecks.filter(':not( :checked )').each( function () {
var type = $( this ).closest('[data-ipsMenuValue]').attr('data-ipsMenuValue');
if( self.scope.find('[data-contentType="' + type + '"]').length ){
self.scope.find('[data-contentType="' + type + '"]').slideUp();
}
})
} else {
// Nothing is checked now, so check 'all'
all.addClass('ipsSideMenu_itemActive')
.find('> input[type="checkbox"]')
.prop( 'checked', true );
this.scope.find('[data-contentType]').slideUp();
}
}
}
});
}(jQuery, _));]]> a');
menuItem.text( data.title );
}
this.hideStreamForm();
this._setStreamLoading(false);
},
/**
* When the form indicates some loading is happening, we'll put the stream into loading mode
*
* @param {event} e Event object
* @param {object} data Event data object from form controller
* @returns {void}
*/
formUpdateLoading: function (e, data) {
this._setStreamLoading( true );
},
/**
* Toggles showing the stream form editor
*
* @returns {void}
*/
showStreamForm: function (e) {
e.preventDefault();
this.scope.find('[data-role="streamOverview"]').hide();
if ( ! this.scope.find('[data-controller="core.front.streams.form"]').length )
{
this.scope.find('[data-role="filterFormLoading"]').show();
ips.utils.anim.go( 'fadeInDown', this.scope.find('[data-role="filterForm"]') );
var data = { id: $( e.currentTarget ).attr('data-streamID') };
var self = this;
ips.getAjax()( '?app=core&module=discover&controller=streams&form=show', {
data: data
})
.done( function (response) {
self.scope.find('[data-role="filterForm"]').html( response );
$( document ).trigger( 'contentChange', [ self.scope.find('[data-role="filterForm"]') ] );
})
.fail( function () {
ips.ui.alert.show( {
type: 'alert',
message: ips.getString('errorLoadingStream'),
icon: 'warn'
});
});
}
else {
ips.utils.anim.go( 'fadeInDown', this.scope.find('[data-role="filterForm"]') );
}
},
/**
* Toggles hiding the stream form editor
*
* @returns {void}
*/
hideStreamForm: function () {
var self = this;
this.scope.find('[data-role="filterForm"]').hide();
this.scope.find('[data-role="streamOverview"]').fadeIn();
},
/**
* Shows a tooltip on the 'default stream' link to help the user
*
* @param {string} title TItle of the default stream
* @returns {void}
*/
_showStreamTooltip: function (title) {
if( !this._tooltip ){
// Build it from a template
var tooltipHTML = ips.templates.render( 'core.tooltip', {
id: 'elDefaultStreamTooltip_' + this.controllerID
});
// Append to body
ips.getContainer().append( tooltipHTML );
this._tooltip = $('#elDefaultStreamTooltip_' + this.controllerID );
} else {
this._tooltip.hide();
}
if( this._tooltipTimer ){
clearTimeout( this._tooltipTimer );
}
this._tooltip.text( ips.getString('streamDefaultTooltip', {
title: title
}));
// Get image
var streamLink = $('a[data-action="defaultStream"]:visible');
var self = this;
// Now position it
var positionInfo = {
trigger: streamLink.first(),
target: this._tooltip,
center: true,
above: true
};
var tooltipPosition = ips.utils.position.positionElem( positionInfo );
$( this._tooltip ).css({
left: tooltipPosition.left + 'px',
top: tooltipPosition.top + 'px',
position: ( tooltipPosition.fixed ) ? 'fixed' : 'absolute',
zIndex: ips.ui.zIndex()
});
if( tooltipPosition.location.vertical == 'top' ){
this._tooltip.addClass('ipsTooltip_top');
} else {
this._tooltip.addClass('ipsTooltip_bottom');
}
this._tooltip.show();
setTimeout( function () {
if( self._tooltip && self._tooltip.is(':visible') ){
ips.utils.anim.go( 'fadeOut', self._tooltip );
}
}, 3000);
},
/**
* Puts the stream into loading mode by adding an overlaid loadinb div
*
* @param {boolean} loading Are we loading?
* @returns {void}
*/
_setStreamLoading: function (loading) {
var stream = this.scope.find('[data-role="streamContent"]');
if( !loading ){
this._streamLoadingOverlay.hide();
stream.css({
opacity: 1
});
return;
} else {
if( !this._streamLoadingOverlay ){
this._streamLoadingOverlay = $('').addClass('ipsLoading');
ips.getContainer().append( this._streamLoadingOverlay );
}
// Get dims & position
var dims = ips.utils.position.getElemDims( stream );
var position = ips.utils.position.getElemPosition( stream );
this._streamLoadingOverlay.show().css({
left: position.viewportOffset.left + 'px',
top: position.viewportOffset.top + $( document ).scrollTop() + 'px',
width: dims.width + 'px',
height: dims.height + 'px',
position: 'absolute',
zIndex: ips.ui.zIndex()
});
stream.css({
opacity: 0.5
});
}
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.system.buildApp.js - Present an alert to the user asking them if they want to build the app first
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.system.buildApp', {
initialize: function () {
this.on( 'click', this.launchAlert );
},
/**
* Displays a dialog to the user with 'build and download' and 'download' options
*
* @param {event} e Event object
* @returns {void}
*/
launchAlert: function (e) {
var self = this;
e.preventDefault();
ips.ui.alert.show({
type: 'verify',
message: ips.getString('build_app'),
icon: 'question',
buttons: {
yes: ips.getString('build_download'),
no: ips.getString('download'),
cancel: ips.getString('cancel')
},
callbacks: {
yes: function () {
window.location = self.scope.attr('data-buildURL');
},
no: function () {
window.location = self.scope.attr('data-downloadURL');
}
}
});
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.system.codeHook.js - Handles editing code hooks
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.system.codeHook', {
codeMirror: null,
initialize: function () {
this.on( 'click', '[data-codeToInject]', this._itemClick );
},
/**
* Event handler for clicking on an item
*
* @param {event} e Event object
* @returns {void}
*/
_itemClick: function (e) {
var codeMirror = $( this.scope ).find('textarea').data('CodeMirrorInstance');
var regex = new RegExp( $.parseJSON( $(e.currentTarget).attr('data-signature') ) );
var found = false;
var lastLine = codeMirror.doc.lineCount() - 1;
codeMirror.doc.eachLine(function(line){
if ( line.text.match( regex ) ) {
found = true;
codeMirror.setSelection( { line: codeMirror.doc.getLineNumber( line ), ch: 0 }, { line: codeMirror.doc.getLineNumber( line ), ch: line.text.length } );
codeMirror.scrollIntoView( { line: codeMirror.doc.getLineNumber( line ), ch: 0 } );
}
if ( line.text.match( /^\s*}\s*$/ ) ) {
lastLine = codeMirror.doc.getLineNumber( line );
}
});
if ( !found ) {
codeMirror.doc.replaceRange( $.parseJSON( $(e.currentTarget).attr('data-codeToInject') ), { line: lastLine - 1, chr: 0 }, { line: lastLine - 1, chr: 0 } );
codeMirror.scrollIntoView( { line: codeMirror.doc.lineCount(), ch: 0 } );
}
}
});
}(jQuery, _));
');
textArea
.attr( 'data-url', this.scope.attr('href') )
.val( this.scope.html() )
.change( function (e) {
self._change(e);
});
//this.scope.replaceWith( textArea );
},
/**
* Event handler for content changing in the textbox
*
* @param {event} e Event object
* @returns {void}
*/
_change: function (e) {
var elem = $( e.target );
elem.addClass( 'ipsField_loading' );
var url = elem.attr('data-url') + '&form_submitted=1&csrfKey=' + ips.getSetting('csrfKey') +
'&lang_word_custom=' + encodeURIComponent( elem.val() );
// Send the translated string, and show flash message on success
// On failure we'll reload the page
ips.getAjax()( url )
.done( function() {
elem.removeClass('ipsField_loading');
ips.ui.flashMsg.show( ips.getString('saved') );
})
.fail( function () {
window.location = url;
});
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.system.login.js - ACP login screen controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.system.login', {
initialize: function () {
this.on( 'tabChanged', this.tabChanged );
this.on( 'click', '[data-action="upgradeWarningContinue"]', this.upgradeWarningContinue );
this.setup();
},
/**
* Setup method
* Focuses the first text field on the first visible tab automatically
*
* @returns {void}
*/
setup: function () {
// find the active tab, if any, then the first text field to focus it
this.scope
.find("#elTabContent .ipsTabs_panel:visible")
.first()
.find('input[type="text"]')
.first()
.focus();
},
/**
* Event handler for the active tab being changed
* Saves the new active tab in a cookie for the next time the login screen is loaded
* so that we can show the user their correct login method automatically
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
tabChanged: function (e, data) {
// Store the tab they've clicked on in the local DB so that we can
// show it by default next time they log in
ips.utils.cookie.set( 'acpLoginMethod', data.tabID, 1 );
},
/**
* Event handler for when the upgrade warning is skipped
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
upgradeWarningContinue: function (e, data) {
e.preventDefault();
$(this.scope).find('[data-role="upgradeWarning"]').hide();
$(this.scope).find('[data-role="loginForms"]').removeClass('ipsHide').show();
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.system.manageFollowed.js - Followed content controll
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.system.manageFollowed', {
initialize: function () {
$( document ).on( 'followingItem', _.bind( this.followingItemChange, this ) );
this.setup();
},
setup: function () {
this._followID = this.scope.attr('data-followID');
},
followingItemChange: function (e, data) {
if( data.feedID != this._followID ){
return;
}
if( !_.isUndefined( data.unfollow ) ){
this.scope.find('[data-role="followDate"], [data-role="followFrequency"]').html('');
this.scope.find('[data-role="followAnonymous"]').addClass('ipsHide');
this.scope.find('[data-role="followButton"]').addClass('ipsButton_disabled');
this.scope.addClass('ipsFaded');
return;
}
// Update anonymous badge
this.scope.find('[data-role="followAnonymous"]').toggleClass( 'ipsHide', !data.anonymous );
// Update notification type
if( data.notificationType ){
this.scope.find('[data-role="followFrequency"]').html( ips.templates.render( 'follow.frequency', {
hasNotifications: ( data.notificationType !== 'none' ),
text: ips.getString( 'followFrequency_' + data.notificationType )
} ));
}
}
});
}(jQuery, _)); ol').nestedSortable({
forcePlaceholderSize: true,
handle: 'div',
helper: 'clone',
maxLevels: 2,
items: '[data-role="menuNode"]',
isTree: true,
errorClass: 'cMenuManager_emptyError',
placeholder: 'cMenuManager_emptyHover',
start: _.bind( this._startDragging, this ),
toleranceElement: '> div',
tabSize: 30,
update: function (event, ui) {
self._changeOccurred();
ips.getAjax()( '?app=core&module=applications&controller=menu&do=reorder&' + self.scope.find('.cMenuManager_root > ol').nestedSortable( 'serialize', { key: 'menu_order' } ) )
.done(function(){
self._updatePreview();
});
}
});
// Set up sortables for the dropdown menus
this._dropdownManager.find('[data-dropdownID] > ol').sortable({
items: 'li',
update: function (event, ui) {
self._changeOccurred();
ips.getAjax()( '?app=core&module=applications&controller=menu&do=reorder&' + $( this ).sortable( 'serialize', { key: 'menu_order' } ) )
.done(function(){
self._updatePreview();
});
}
});
// Position the live preview
this._positionPreviewWindow();
},
/**
* Event handler called when user leaves the page
* If there's unsaved changes, we'll let the user know
*
* @param {event} e Event object
* @returns {void}
*/
windowUnload: function (e) {
if( this._hasChanges ){
return ips.getString('menuPublishUnsaved');
}
},
/**
* Warns the user before proceeding with restoring the menu
*
* @param {event} e Event object
* @returns {void}
*/
restoreMenu: function (e) {
e.preventDefault();
var url = $( e.currentTarget ).attr('href');
ips.ui.alert.show( {
type: 'confirm',
icon: 'warn',
message: ips.getString('menuRestoreConfirm'),
subText: ips.getString('menuRestoreConfirmSubtext'),
callbacks: {
ok: function () {
window.location = url;
}
}
});
},
/**
* Publishes the menu via ajax
*
* @param {event} e Event object
* @returns {void}
*/
publishMenu: function (e) {
e.preventDefault();
var self = this;
var button = $( e.currentTarget );
var url = button.attr('href');
// Change the text
button
.attr('data-currentTitle', button.find('span').text() )
.addClass('ipsButton_disabled')
.find('span')
.text( ips.getString('publishing') );
// Fire request
ips.getAjax()( url, {
bypassRedirect: true
} )
.done( function (response) {
ips.ui.flashMsg.show( ips.getString("publishedMenu") );
self._hasChanges = false;
button
.find('span')
.text( button.attr('data-currentTitle') );
})
.fail( function () {
window.location = url;
});
},
/**
* Toggles the preview panel
*
* @param {event} e Event object
* @returns {void}
*/
togglePreview: function (e) {
e.preventDefault();
var toggle = $( e.currentTarget );
if( this._previewOpen ){
this._previewOpen = false;
this._previewWindow.stop().animate({
height: '48px'
});
} else {
this._previewOpen = true;
this._previewWindow.stop().animate({
height: '350px'
});
}
toggle.find('[data-role="closePreview"]').toggleClass( 'ipsHide', !this._previewOpen );
toggle.find('[data-role="openPreview"]').toggleClass( 'ipsHide', this._previewOpen );
},
/**
* Handles adding a new item to a dropdown menu
*
* @param {event} e Event object
* @returns {void}
*/
newDropdown: function (e) {
e.preventDefault();
// Add temporary item for now
var self = this;
var newItem = ips.templates.render( 'menuManager.temporaryDropdown', {
selected: true
});
var menuID = this._currentDropdown;
var menu = this._dropdownManager.find( '[data-menuID="' + menuID + '"]' );
var url = $( e.currentTarget ).attr('href');
this._unselectAllItems();
var doNew = function () {
menu.append( newItem );
self._checkForEmptyDropdown( menu );
// Load the edit form
self._loadEditForm( url, {
parent: menuID
});
};
this._checkTempBeforeCallback( doNew );
},
/**
* Handles adding a new item to the main menu
*
* @param {event} e Event object
* @returns {void}
*/
newItem: function (e) {
e.preventDefault();
// Add temporary item for now
var self = this;
var newItem = ips.templates.render( 'menuManager.temporaryMenuItem', {
selected: true
});
var url = $( e.currentTarget ).attr('href');
this._unselectAllItems();
var doNew = function () {
self.scope.find('.cMenuManager_root > ol').prepend( newItem );
// Load the edit form
self._loadEditForm( url, {
parent: 0
});
};
this._checkTempBeforeCallback( doNew );
},
/**
* Event handler for saving an add/edit form
*
* @param {event} e Event object
* @returns {void}
*/
saveEditForm: function (e) {
e.preventDefault();
var self = this;
var form = $( e.currentTarget );
var url = form.attr('action');
var id = form.attr('data-itemID');
var isNew = form.find('input[type="hidden"][name="newItem"]').val();
ips.getAjax()( url, {
type: 'post',
data: form.serialize()
})
.done( function (response) {
// Update the menu item with new HTML
if( isNew ){
self.scope.find('[data-itemID="temp"]').replaceWith( response.menu_item );
} else {
self.scope.find('[data-itemID="' + id + '"]').replaceWith( response.menu_item );
}
// Do we have a dropdown menu to replace?
if( response.dropdown_menu ){
var dropdownContent = $('
' + response.dropdown_menu + '
');
dropdownContent.find('[data-dropdownid]').each( function () {
var id = $( this ).attr('data-dropdownid');
if( self._dropdownManager.find('[data-dropdownid="' + id + '"]').length ){
self._dropdownManager.find('[data-dropdownid="' + id + '"]').html( $( this ).html() );
} else {
var newDropdown = $('').attr('data-dropdownid', id ).html( $( this ).html() );
self._dropdownManager.append( newDropdown );
}
});
}
// Empty the edit form
self._editForm
.removeClass('cMenuManager_formActive')
.find('> div').fadeOut( 'fast', function () {
$( this ).remove();
});
// Animate item
ips.utils.anim.go( 'pulseOnce', self.scope.find('[data-itemID="' + id + '"]') );
self._changeOccurred();
self._updatePreview();
})
.fail( function () {
});
},
/**
* Event handler for removing an item
*
* @param {event} e Event object
* @returns {void}
*/
removeItem: function (e) {
e.preventDefault();
var self = this;
var removeIcon = $( e.currentTarget );
var item = removeIcon.closest('[data-role="menuItem"]');
var li = item.closest('li');
var menu = item.closest('ol');
var url = removeIcon.attr('href');
if( item.attr('data-itemID') == 'temp' ){
// Just remove and reset the form
ips.utils.anim.go( 'fadeOutDown', li )
.done( function () {
li.remove();
self._checkForEmptyDropdown( menu );
});
this._unselectAllItems();
}
var removeItem = function () {
// Remove item first
self._changeOccurred();
ips.utils.anim.go( 'fadeOutDown', li );
ips.getAjax()( '?app=core&module=applications&controller=menu&do=remove&id=' + item.attr('data-itemID') );
};
// Check whether this item has any children
if( item.find('+ ol > li').length ){
ips.ui.alert.show( {
type: 'confirm',
icon: 'warn',
message: ips.getString('menuItemHasChildren'),
callbacks: {
ok: removeItem
}
});
} else {
removeItem();
}
},
/**
* Event handler for clicking on an item to edit it
*
* @param {event} e Event object
* @returns {void}
*/
editItem: function (e) {
// Find edit URL
var self = this;
var clickFocus = $( e.target );
var item = $( e.currentTarget );
var editURL = item.find('[data-action="editItem"]').attr('href');
var doEdit = function () {
// Ignore it if we're inside another link
if( clickFocus.closest('a').length ){
return;
}
// Highlight this item; remove all other highlights
self._menuManager.find('.cMenuManager_active').removeClass('cMenuManager_active');
self._dropdownManager.find('.cMenuManager_active').removeClass('cMenuManager_active');
self._editForm.addClass('cMenuManager_formActive');
item.addClass('cMenuManager_active');
// Load the edit form
self._loadEditForm( editURL, {} );
};
this._checkTempBeforeCallback( doEdit );
},
/**
* Event handler for clicking on the 'edit dropdown' icon
*
* @param {event} e Event object
* @returns {void}
*/
editDropdown: function (e) {
e.preventDefault();
var self = this;
var icon = $( e.currentTarget );
var dropdownID = icon.closest('[data-itemID]').attr('data-itemID');
var menuWrapper = this._dropdownManager.find('[data-dropdownID="' + dropdownID + '"]');
// Main edit functionality
var doEdit = function () {
// If we're currently viewing the roots, we'll slide it across
if( !self._currentDropdown ){
// Get column ready for animation
self.scope.find('.cMenuManager_column').addClass('cMenuManager_readyToSlide');
// Hide all menu wrappers
self._dropdownManager.find('[data-dropdownID]').hide();
// Show this menu
menuWrapper.show();
// Slide columns
self.scope.find('[data-manager="dropdown"], [data-manager="main"]').show().animate({
left: "-50%"
}, function () {
self.scope.find('.cMenuManager_column').addClass('cMenuManager_readyToSlide');
});
self._currentDropdown = 0;
} else {
// If we're already viewing a menu, we'll fade out and in
var currentMenuWrapper = self._dropdownManager.find('[data-dropdownID="' + self._currentDropdown + '"]');
currentMenuWrapper.find(' > ol').fadeOut( 'fast', function () {
currentMenuWrapper.hide();
menuWrapper
.find('> ol')
.hide()
.end()
.show()
.find('> ol')
.fadeIn();
});
}
// Empty the edit form
self._editForm
.removeClass('cMenuManager_formActive')
.find('> div').fadeOut( 'fast', function () {
$( this ).remove();
});
self._currentDropdown = dropdownID;
};
this._checkTempBeforeCallback( doEdit );
},
/**
* Goes back a level in the dropdown menu editor
*
* @param {event} e Event object
* @returns {void}
*/
navBack: function (e) {
e.preventDefault();
var self = this;
var parentID = $( e.currentTarget ).attr('data-parentID');
if( parentID == 0 ){
// Back to the root
this.scope.find('.cMenuManager_column').addClass('cMenuManager_readyToSlide');
this.scope.find('[data-manager="dropdown"], [data-manager="main"]').show().animate({
left: "0"
}, function () {
self._dropdownManager.find('[data-dropdownID]').hide();
self.scope.find('.cMenuManager_column').removeClass('cMenuManager_readyToSlide');
});
this._currentDropdown = 0;
} else {
// Back to a parent dropdown
var menuWrapper = this._dropdownManager.find('[data-dropdownID="' + parentID + '"]');
var currentMenuWrapper = this._dropdownManager.find('[data-dropdownID="' + this._currentDropdown + '"]');
currentMenuWrapper.find(' > ol').fadeOut( 'fast', function () {
currentMenuWrapper.hide();
menuWrapper
.find('> ol')
.hide()
.end()
.show()
.find('> ol')
.fadeIn();
});
}
// Empty the edit form
this._unselectAllItems();
},
/**
* Event handler that responds to the ACP main menu being toggled
* so that we can position the live preview bar
*
* @returns {void}
*/
menuToggled: function () {
this._positionPreviewWindow();
},
/**
* Loads a new add/edit form
*
* @param {string} url URL to the form to load
* @param {object} obj Params object to pass to ajax
* @returns {void}
*/
_loadEditForm: function (url, obj) {
var self = this;
if( this._ajaxObj && _.isFunction( this._ajaxObj.abort ) ){
this._ajaxObj.abort();
}
this._editForm
.addClass('ipsLoading')
.addClass('cMenuManager_formActive')
.find('> div')
.css( { opacity: 0.4 } )
.after( $('').addClass('cMenuManager_formLoading ipsLoading') );
this._ajaxObj = ips.getAjax()( url, {
data: obj
})
.done( function (response){
self._editForm.html( response );
$( document ).trigger( 'contentChange', [ self._editForm ] );
})
.always( function () {
self._editForm.removeClass('ipsLoading cMenuManager_formLoading');
});
},
/**
* Checks whether a dropdown is empty, and removes/adds the empty message
*
* @param {element} menu Menu element
* @returns {void}
*/
_checkForEmptyDropdown: function (menu) {
if( !menu.length ){
return;
}
if( menu.find('[data-itemID]').length || menu.find('.ipsMenu_sep').length ){
menu.find('.cMenuManager_emptyList').remove();
} else {
menu.append( ips.templates.render( 'menuManager.emptyList' ) );
}
},
/**
* Checks for unsaved temporary items in the menu manager, and runs a callback after confirming
*
* @param {function} callback Callback method to run after confirming
* @returns {void}
*/
_checkTempBeforeCallback: function (callback) {
var self = this;
// Are there any temp items to warn about?
if( this.scope.find('[data-itemID="temp"]').length ){
ips.ui.alert.show( {
type: 'confirm',
icon: 'warn',
message: ips.getString('menuManagerUnsavedTemp'),
callbacks: {
ok: function () {
// Remove temp items
self.scope.find('[data-itemID="temp"]').remove();
callback.apply( self );
}
}
});
} else {
callback.apply( self );
}
},
/**
* Clears the add/edit form area
*
* @returns {void}
*/
_unselectAllItems: function () {
this._editForm.find('> div').fadeOut( 'fast', function () {
$( this ).remove();
});
this._menuManager.find('.cMenuManager_active').removeClass('cMenuManager_active');
this._dropdownManager.find('.cMenuManager_active').removeClass('cMenuManager_active');
this._editForm.removeClass('cMenuManager_formActive');
},
/**
* Called when any data change happens, so that we can illuminate the Publish button
* and track state internally
*
* @returns {void}
*/
_changeOccurred: function () {
this._hasChanges = true;
// Light up the buttons
// The buttons are in the header so we need to work out of scope here
$('[data-action="publishMenu"]').removeClass('ipsButton_disabled');
},
/**
* Updates the preview panel
*
* @param {event} e Event object
* @returns {void}
*/
_updatePreview: function (e) {
this.scope.find("[data-role='previewBody'] iframe").get(0).contentWindow.location.reload( true );
},
_startDragging: function () {
//
},
/**
* Positions the preview bar so it accounts for the ACP menu
*
* @returns {void}
*/
_positionPreviewWindow: function () {
if( $('body').find('#acpAppList').is(':visible') ){
var width = $('body').find('#acpAppList').width();
this._previewWindow.css({
left: width + 'px'
});
}
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.system.metaTagEditor.js - Live meta tag editor functionality
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.system.metaTagEditor', {
_changed: false,
initialize: function () {
this.on( 'click', '[data-action="addMeta"]', this.addMetaBlock );
this.on( 'change', 'input, select', this.changed );
this.on( 'submit', 'form', this.formSubmit );
this.on( window, 'beforeunload', this.beforeUnload );
this.setup();
},
setup: function () {
this.scope.css({
zIndex: 10000
});
},
/**
* Event handler for submitting the meta tags form
*
* @returns {void}
*/
formSubmit: function (e) {
e.preventDefault();
var self = this;
var form = $( e.currentTarget );
form.find('.ipsButton').prop( 'disabled', true ).addClass('ipsButton_disabled');
// Send ajax request to save
ips.getAjax()( form.attr('action'), {
data: form.serialize(),
type: 'post'
})
.done( function () {
ips.ui.flashMsg.show( ips.getString('metaTagsSaved') );
form.find('.ipsButton').prop( 'disabled', false ).removeClass('ipsButton_disabled');
self._changed = false;
})
.fail( function () {
form.submit();
});
},
/**
* Warns the user if they've got unsaved changes
*
* @returns {string|null}
*/
beforeUnload: function () {
if( this._changed ){
return ips.getString('metaTagsUnsaved');
}
},
/**
* Clones a new set of meta tag elements
*
* @returns {void}
*/
addMetaBlock: function () {
// Duplicate the metaTemplate block and append it to the list
var copy = this.scope.find('[data-role="metaTemplate"]').clone().removeAttr('data-role').hide();
$('#elMetaTagEditor_tags').append( copy );
ips.utils.anim.go( 'fadeIn', copy );
},
/**
* Called when an input changes, so we can later warn the use rif they leave the page
*
* @returns {void}
*/
changed: function (e) {
this._changed = true;
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2015 Invision Power Services - http://www.invisionpower.com
*
* ips.system.notificationSettings.js - Notification settings controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.system.notificationSettings', {
initialize: function () {
this.on( 'click', '[data-action="promptMe"]', this.promptMe );
this.on( document, 'permissionGranted.notifications', this.permissionChanged );
this.on( document, 'permissionDenied.notifications', this.permissionChanged );
this.setup();
},
/**
* Setup method
*
* @returns {void}
*/
setup: function () {
if( ips.utils.notification.supported ){
this._showNotificationChoice();
}
},
/**
* Event handler for when the notification permission changes
*
* @returns {void}
*/
permissionChanged: function () {
this._showNotificationChoice();
},
/**
* Event handler for clicking the 'enabled notifications' button.
* Shows an info message, and then triggers a browser prompt 2 secs later
*
* @param {event} e Event object
* @returns {void}
*/
promptMe: function (e) {
e.preventDefault();
if( ips.utils.notification.hasPermission() ){
// No need to do anything
return;
}
// Shoe the message then wait a couple of secs before prompting
this.scope.find('[data-role="promptMessage"]').slideDown();
setTimeout( function () {
ips.utils.notification.requestPermission();
}, 2000);
},
/**
* Displays an info panel, with the message depending on whether notifications are enabled in the browser
*
* @returns {void}
*/
_showNotificationChoice: function () {
var type = ips.utils.notification.permissionLevel();
this.scope
.find('[data-role="browserNotifyInfo"]')
.show()
.html( ips.templates.render( 'notification.' + type ) );
}
});
}(jQuery, _));').attr( 'data-role', 'validationCheck' ) );
this.emailField.after( $('').attr( 'data-role', 'validationCheck' ) );
},
/**
* Event handler for a change on the username field
* Waits 700ms, then calls this._doCheck
*
* @param {event} e Event object
* @returns {void}
*/
changeUsername: function (e) {
if( this.timers['username'] ){
clearTimeout( this.timers['username'] );
}
if( this.usernameField.val().length > 4 || e.type != "keyup" ){
this.timer = setTimeout( _.bind( this._doCheck, this, 'username', this.usernameField ), 700 );
} else {
this._clearResult( this.usernameField );
}
},
/**
* Event handler for a change on the email field
* Waits 700ms, then calls this._doCheck
*
* @param {event} e Event object
* @returns {void}
*/
changeEmail: function (e) {
if( this.timers['email'] ){
clearTimeout( this.timers['email'] );
}
if( ( this.emailField.val().length > 5 && ips.utils.validate.isEmail(this.emailField.val()) ) || e.type != "keyup" ){
this.timer = setTimeout( _.bind( this._doCheck, this, 'email', this.emailField ), 700 );
} else {
this._clearResult( this.emailField );
}
},
/**
* Clears a previous validation result
*
* @returns {void}
*/
_clearResult: function (field) {
field
.removeClass('ipsField_error')
.removeClass('ipsField_success')
.next('[data-role="validationCheck"]')
.html('');
},
/**
* Fires an ajax request to check whether the username is already in use
* Updates the result element depending on the result
*
* @returns {void}
*/
_doCheck: function (type, field) {
var value = field.val();
var resultElem = field.next('[data-role="validationCheck"]');
var self = this;
if( this.ajax[ type ] && this.ajax[ type ].abort ){
this.ajax[ type ].abort();
}
// Set loading
field.addClass('ipsField_loading');
// Do ajax
this.ajax[ type ]('?app=core&module=system&controller=ajax&do=' + type + 'Exists', {
dataType: 'json',
data: {
input: encodeURIComponent( value )
}
})
.done( function (response) {
if( response.result == 'ok' ){
resultElem.show().html( ips.templates.render('register.ok') );
field.removeClass('ipsField_error').addClass('ipsField_success');
} else {
resultElem.show().html( ips.templates.render( 'register.fail', { message: response.message } ) );
field.removeClass('ipsField_success').addClass('ipsField_error');
}
})
.fail( function () {} )
.always( function () {
field.removeClass('ipsField_loading');
});
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.system.codeHook.js - Handles editing theme hooks
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.system.themeHook', {
initialize: function () {
var scope = this.scope;
scope.find( '[data-action="showTemplate"]' ).removeClass('ipsHide');
this.on( 'openDialog', function(e, data) {
$( '#' + data.dialogID ).on( 'click', 'li[data-selector]', function(e){
e.stopPropagation();
ips.ui.dialog.getObj( scope.find( '[data-action="showTemplate"]' ) ).hide();
scope.find( 'input[name="plugin_theme_hook_selector"]' ).val( $(e.currentTarget).attr('data-selector') );
} )
});
},
});
}(jQuery, _));
" + response + '' );
$( document ).trigger('contentChange', [ themeHookWindow ]);
})
.fail(function(){
window.location = target.attr('href');
})
}
target.addClass('cHookEditor_activeTemplate');
themeHookWindow.removeClass('ipsLoading');
}
});
}(jQuery, _));
]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.system.codeHook.js - Handles editing code hooks
*
* Author: Mark Wade
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.system.themeHookTemplate', {
initialize: function () {
this.on( 'click', 'li[data-selector]', this._itemClick );
},
/**
* Event handler for clicking on an item
*
* @param {event} e Event object
* @returns {void}
*/
_itemClick: function (e) {
this.trigger( $( e.currentTarget ), 'templateClicked' );
}
});
}(jQuery, _));
/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.templates.addForm.js - Controller for the 'add' form in the template editor
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.templates.addForm', {
initialize: function () {
this.on( 'submit', 'form.ipsForm', this.submitForm );
this.on( document, 'fileListRefreshed.templates', this.closeDialog );
},
submitForm: function (e) {
e.preventDefault();
var self = this;
if( !$( e.currentTarget ).attr('data-bypassValidation') ){
// The form hasn't been validated by the genericDialog controller yet, so bail for now
return;
}
// Gather form values and send them
ips.getAjax()( $( e.currentTarget ).attr('action'), {
dataType: 'json',
data: $( e.currentTarget ).serialize(),
type: 'post'
})
.done( function (response) {
self.trigger( 'addedFile.templates', {
type: self.scope.attr('data-type'),
fileID: response.id,
name: response.name
});
});
},
closeDialog: function (e, data) {
this.trigger('closeDialog');
}
});
}(jQuery, _));/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.templates.conflict.js - Templates: Parent controller for the template conflict manager
*
* Author: Matt Mecham
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.templates.conflict', {
initialize: function () {
this.on( 'click', '.ipsButton', this.makeSelection );
this.setup();
},
setup: function () {
$('span[data-conflict-name] input[type=radio]').hide();
},
/**
* "Use this version" button is clicked
*
* @param {event} e Event object
* @returns {void}
*/
makeSelection: function (e) {
var span = $( e.target ).closest('span');
var radio = $( span ).find('input[type=radio]');
var name = $( span ).attr('data-conflict-name');
var id = $( radio ).attr('name').replace( /conflict_/, '' );
/* Button disabled */
if ( $(span).hasClass('ipsButton_disabled') )
{
return false;
}
/* Undo selection */
else if ( $(span).hasClass('ipsButton_alternate') )
{
$(radio).removeAttr('checked');
$(span).removeClass('ipsButton_alternate')
.addClass('ipsButton_primary')
.find('strong').html( ips.getString('sc_use_this_version') );
$('input[type=radio][name=conflict_' + id + ']').closest('span.ipsButton[data-conflict-name=' + ( name == 'new' ? 'old' : 'new' ) +']').removeClass('ipsButton_disabled');
$('th span[data-conflict-id=' + id + '][data-conflict-name=' + name + ']').removeClass('ipsPos_left ipsBadge ipsBadge_positive');
ips.utils.anim.go( 'blindDown', this.scope.find('div[data-conflict-id=' + id + ']') );
}
/* Make selection */
else
{
$(radio).attr('checked', 'checked');
$(span).removeClass('ipsButton_primary')
.addClass('ipsButton_alternate')
.find('strong').html( ips.getString('sc_remove_selection') );
$('input[type=radio][name=conflict_' + id + ']').closest('span.ipsButton[data-conflict-name=' + ( name == 'new' ? 'old' : 'new' ) +']').addClass('ipsButton_disabled');
$('th span[data-conflict-id=' + id + '][data-conflict-name=' + name + ']').addClass('ipsPos_left ipsBadge ipsBadge_positive');
this.scope.find('div[data-conflict-id=' + id + ']').slideUp();
}
}
});
}(jQuery, _));')
.attr( 'type', 'hidden' )
.attr( 'name', original.attr('name') )
.attr( 'value', original.attr('value') )
.attr( 'data-role', 'variables' )
)
original.remove();
}
ips.loader.get( ['core/interface/codemirror/diff_match_patch.js','core/interface/codemirror/codemirror.js'] ).then( function () {
self._initCodeMirror( $( item ).attr('data-fileid'), $( item ).attr('data-type') );
});
});
},
/**
* Event handler for the editor preference menu
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
menuSelected: function (e, data) {
if( data.originalEvent ){
data.originalEvent.preventDefault();
}
if( data.triggerID == 'elTemplateEditor_preferences' ){
if ( data.selectedItemID == 'diff' ) {
this.toggleDiff();
}
else if( data.selectedItemID == 'wrap' ){
this._changeEditorPreference( !ips.utils.db.get('templateEditor', 'wrap'), 'wrap', 'lineWrapping' );
} else {
this._changeEditorPreference( !ips.utils.db.get('templateEditor', 'lines'), 'lines', 'lineNumbers' );
}
}
},
/**
* Method that updates an editor preference
*
* @param {object} data Event data object from this.menuSelected
* @returns {void}
*/
_changeEditorPreference: function (toValue, type, cmName) {
// Set the menu appropriately
if( toValue ){
$('#elTemplateEditor_preferences_menu')
.find('[data-ipsMenuValue="' + type + '"]').addClass('ipsMenu_itemChecked');
} else {
$('#elTemplateEditor_preferences_menu')
.find('[data-ipsMenuValue="' + type + '"]').removeClass('ipsMenu_itemChecked');
}
// Update controller variable
this._editorPreferences[ type ] = toValue;
// Update local DB
ips.utils.db.set( 'templateEditor', type, toValue );
// Update all CM instances
for( var i in this._cmInstances ){
if ( this._cmInstances[ i ] instanceof CodeMirror.MergeView ) {
this._cmInstances[ i ].edit.setOption( cmName, toValue );
this._cmInstances[ i ].left.orig.setOption( cmName, toValue );
} else {
this._cmInstances[ i ].setOption( cmName, toValue );
}
}
},
/**
* A dialog has been opened
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
openedDialog: function (e, data) {
if( data.elemID == 'elTemplateEditor_variables' || data.elemID == 'elTemplateEditor_attributes' ){
this._insertVariablesIntoDialog( data );
}
},
/**
* Inserts the current variables value into the dialog
*
* @param {object} data Event data object from the dialog
* @returns {void}
*/
_insertVariablesIntoDialog: function (data) {
// First get the variables
var active = this._getActiveTab();
if( !active.tabPanel ){
return;
}
var value = $.trim( active.tabPanel.find('[data-role="variables"]').val() );
data.dialog
.find('[data-role="variables"]')
.val( value )
.end()
.find('[name="_variables_fileid"]')
.val( active.tabPanel.attr('data-fileid') )
.end()
.find('[name="_variables_type"]')
.val( active.tabPanel.attr('data-type') );
},
/**
* Updates the value of the variables field in the tab with the given file ID
*
* @param {event} e Event object
* @param {event} e Event data object from templates.variablesDialog
* @returns {void}
*/
updateVariables: function (e, data) {
// Find the panel with the correct fileID, and update it's variables value
this._tabContent
.find('[data-type="' + data.type + '"][data-fileid="' + data.fileID + '"]')
.find('[data-role="variables"]')
.val( data.value );
},
/**
* Toggle diff
*
* @returns {void}
*/
toggleDiff: function () {
var self = this;
var active = this._getActiveTab();
var panel = active.tabPanel;
var key = panel.attr('data-fileid');
// Toggle off
if ( self._cmInstances[ key ] instanceof CodeMirror.MergeView ) {
self.scope.find( '#editor_' + key ).val( this._cmInstances[ key ].edit.doc.getValue() );
panel.find('.CodeMirror-merge,.cTemplateMergeHeaders').remove();
self._initCodeMirror( key, panel.attr('data-type') );
}
// Toggle on
else {
// Get the contents from the current CodeMirror instance and remove it
self._cmInstances[ key ].save();
panel.find('.CodeMirror').remove();
panel.addClass('ipsLoading');
// Fire AJAX to get the original content
ips.getAjax()( this._normalURL + '&do=diffTemplate', {
dataType: 'json',
data: this._getParametersFromPanel( panel )
})
.done( function (response) {
// Initiate the merge view
self._cmInstances[ key ] = CodeMirror.MergeView( document.getElementById( panel.attr('id') ), {
value: self.scope.find( '#editor_' + key ).val(),
origLeft: response,
lineWrapping: self._editorPreferences['wrap'],
lineNumbers: self._editorPreferences['lines'],
mode: ( panel.attr('data-type') == 'templates' ? 'htmlmixed' : 'css' ),
} );
panel.removeClass('ipsLoading');
// Add headers
var headers = $( ips.templates.render( 'templates.editor.diffHeaders' ) );
panel.prepend( headers );
// Set size
var height = self._getTabContentHeight() - headers.outerHeight();
self._cmInstances[ key ].edit.setSize( null, height );
self._cmInstances[ key ].left.orig.setSize( null, height );
$( self._cmInstances[ key ].left.gap ).css( 'height', height );
// Add change handler
self._cmInstances[ key ].edit.on( 'change', function (doc, cm) {
self._setChanged( true, key );
});
});
}
},
/**
* Saves the contents of the editor
*
* @param {event} e Event object
* @returns {void}
*/
save: function (e) {
e.preventDefault();
var self = this;
var active = this._getActiveTab();
var panel = active.tabPanel;
var key = panel.attr('data-fileid');
if( !active.tab || !active.tabPanel ){
Debug.warn('No active tab or tab panel');
return;
}
var save = this._getParametersFromPanel( panel );
// We call .save() on the CodeMirror instance, which will cause it to update the
// contents of the original textbox, unless we're in merge view, when we have to
// do that ourselves
if ( this._cmInstances[ key ] instanceof CodeMirror.MergeView ) {
self.scope.find( '#editor_' + key ).val( this._cmInstances[ key ].edit.doc.getValue() );
} else {
this._cmInstances[ key ].save();
}
// Add it to the save object
save[ 'editor_' + key ] = this.scope.find( '#editor_' + key ).val();
// If this is a template, add its variables
if( panel.attr('data-type') == 'templates' ){
save[ 'variables_' + save.t_key ] = panel.find('[data-role="variables"]').val();
}
// Show loading
this.trigger( 'savingFile.templates' );
// Send it
ips.getAjax()( this._normalURL + '&do=saveTemplate', {
dataType: 'json',
data: save,
type: 'post'
})
.done( function (response) {
// Let everyone know
self.trigger( 'savedFile.templates', {
key: key,
oldID: parseInt( panel.attr('data-itemID') ),
newID: parseInt( response.item_id ),
status: 'changed'
});
// Remove the unsaved status from the tab
self._setChanged( false, key );
// Update the toolbar
self._updateToolbar( active.tab );
})
.fail( function () {
ips.ui.alert.show( {
type: 'alert',
message: ips.getString('saveThemeError'),
icon: 'warn'
});
})
.always( function () {
self.trigger( 'saveFileFinished.templates' );
});
},
/**
* Reverts or deletes a file
* If the bypass parameter is false, this method will confirm the action with the user first
*
* @param {event} e Event object
* @param {boolean} bypass Bypass the user confirmation?
* @returns {void}
*/
revert: function (e, bypass) {
e.preventDefault();
if ( $(e.currentTarget).hasClass('ipsButton_disabled') )
{
return false;
}
var self = this;
var active = this._getActiveTab();
var panel = active.tabPanel;
var key = panel.attr('data-fileid');
var message = ( $( e.currentTarget ).attr('data-actionType') == 'revert' ) ?
ips.getString('skin_revert_confirm') : ips.getString('skin_delete_confirm');
if( bypass !== true ){
ips.ui.alert.show({
type: 'confirm',
message: message,
icon: 'warn',
callbacks: {
ok: function () {
self.revert( e, true );
}
}
});
return;
}
var save = this._getParametersFromPanel( panel );
// Send it
ips.getAjax()( this._normalURL + '&do=deleteTemplate', {
dataType: 'json',
data: save,
type: 'post'
})
.done( function (response) {
if( _.isUndefined( response.type ) ){
self._deletedFile( key, active );
} else {
if( response.template_id || response.css_id ){
self._revertedFile( response, key, active );
} else {
self._deletedFile( key, active );
}
}
});
},
/**
* Handles updating the editor when a file is reverted
*
* @param {object} response JSON response object from ajax request
* @param {string} key Key of the file that's been reverted
* @param {object} active Object containing keys 'tab' and 'tabPanel' referencing active items
* @returns {void}
*/
_revertedFile: function (response, key, active) {
// Let the document know
this.trigger( 'revertedFile.templates', {
key: key,
oldID: parseInt( active.tabPanel.attr('data-itemID') ),
newID: parseInt( response.template_id || response.css_id ),
status: response.InheritedValue
});
// Update the raw textarea
$( '#editor_' + key ).val( response.template_content || response.css_content );
// Update codemirror
if ( this._cmInstances[ key ] instanceof CodeMirror.MergeView ) {
this._cmInstances[ key ].edit.setValue( response.template_content || response.css_content );
} else {
this._cmInstances[ key ].setValue( response.template_content || response.css_content );
}
// Remove the unsaved status from the tab
this._setChanged( false, key );
// Update the toolbar
this._updateToolbar( active.tab );
},
/**
* Handles updating the editor when a file is deleted
*
* @param {string} key Key of the file that's been reverted
* @param {object} active Object containing keys 'tab' and 'tabPanel' referencing active items
* @returns {void}
*/
_deletedFile: function (key, active) {
this.trigger( 'deletedFile.templates', {
key: key,
fileID: active.tabPanel.attr('data-itemID'),
type: active.tabPanel.attr('data-type')
});
// Find close link in the tab
active.tab.find('[data-action="closeTab"]').click();
},
/**
* Returns an object of parameters used by the ajax requests
*
* @param {element} panel The panel being used as the source
* @returns {object}
*/
_getParametersFromPanel: function (panel) {
return {
t_type: panel.attr('data-type'),
t_item_id: panel.attr('data-itemID'),
t_app: panel.attr('data-app'),
t_location: panel.attr('data-location'),
t_group: panel.attr('data-group'),
t_name: panel.attr('data-name'),
t_key: panel.attr('data-fileid')
};
},
/**
* A file has been saved or reverted
* Updates the ID of any element with the old ID, and changes the state
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
updateFile: function (e, data) {
this.scope
.find('[data-itemID="' + data.oldID + '"]')
.attr( 'data-itemID', data.newID )
.attr( 'data-inherited-value', data.status );
},
/**
* Event handler for clicking a close tab button
*
* @param {event} e Event object
* @returns {void}
*/
closeTab: function (e) {
var tab = $( e.currentTarget ).closest('.ipsTabs_item');
this._doCloseTab( tab );
},
/**
* Handles closing a tab.
* We first check if the tab is in an 'unsaved' state, and if so, prompt the user to confirm losing changes.
* We then destroy the codemirror instance, remove the tab and panel, and switch to another open tab.
*
* @param {element} tab The tab to be closed
* @param {boolean} bypass Whether to bypass the unsaved check
* @returns {void}
*/
_doCloseTab: function (tab, bypass) {
var self = this;
var tabParent = tab.closest('[data-fileid]');
var key = tabParent.attr('data-fileid');
var allTabs = this._tabBar.find('.ipsTabs_item').closest('[data-fileid]');
var newTab = null;
// Check if there's unsaved content
if( tabParent.attr('data-state') == 'unsaved' && bypass != true ){
ips.ui.alert.show({
type: 'confirm',
message: ips.getString('themeUnsavedContent'),
icon: 'warn',
callbacks: {
ok: function () {
self._doCloseTab( tab, true );
}
}
});
return;
}
// Is this tab active?
var active = tab.hasClass('ipsTabs_activeItem');
// Let the document know what we're up to
this.trigger( 'closedTab.templates', {
fileID: key
});
// Remove the codemirrrrr element & instance
delete( this._cmInstances[ key ] );
// Find the next or prev tab, if this tab is active, and switch to it
if( active && allTabs.length > 1 ){
if( allTabs.first() == tabParent ){
newTab = tabParent.next();
} else {
newTab = tabParent.prev();
}
}
if( newTab ){
newTab.find('> a').click();
}
// Close the tab
ips.utils.anim.go('fadeOutDown fast', tabParent)
.done( function () {
tabParent.remove();
self._recalculatePanelWrapper();
});
// Remove the panel
this._tabContent.find('[data-fileid="' + key + '"]').remove();
},
/**
* Tab widget has indicated that the user has changed tab
* If there's a file ID, trigger a new event with it, to enable the file listing to highlight it
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
changedTab: function (e, data) {
var tab = data.tab;
if( !_.isUndefined( tab.closest('[data-fileid]').attr('data-fileid') ) ){
this.trigger( 'fileSelected.templates', {
fileID: tab.closest('[data-fileid]').attr('data-fileid')
});
}
this._updateToolbar( tab );
},
/**
* Updates the toolbar buttons
*
* @param {element} tab The current tab
* @returns {void}
*/
_updateToolbar: function (tab) {
var tabParent = tab.closest('[data-fileid]').attr('data-fileid');
var tabPanel = this._tabContent.find('[data-fileid="' + tabParent + '"]');
var status = tabPanel.attr('data-inherited-value');
var type = tabPanel.attr('data-type');
var revert = this.scope.find('[data-action="revert"]');
var key = tabPanel.attr('data-fileid');
switch( status ){
case 'original':
case 'inherit':
revert
.addClass('ipsButton_disabled')
break;
case 'custom':
revert
.html( ips.getString('skin_delete') )
.removeClass('ipsButton_disabled')
.attr('data-actionType', 'delete')
.show();
break;
case 'changed':
revert
.html( ips.getString('skin_revert') )
.removeClass('ipsButton_disabled')
.attr('data-actionType', 'revert')
.show();
break;
}
if( type == 'templates' ){
$('#elTemplateEditor_variables').show();
$('#elTemplateEditor_attributes').hide();
} else {
$('#elTemplateEditor_variables').hide();
$('#elTemplateEditor_attributes').show();
}
if ( this._cmInstances[ key ] instanceof CodeMirror.MergeView ) {
$('[data-ipsmenuvalue="diff"]').addClass('ipsMenu_itemChecked');
} else {
$('[data-ipsmenuvalue="diff"]').removeClass('ipsMenu_itemChecked');
}
},
/**
* Reponds to the openFile event, to open a file
* Either switch to it if already open, or hand off to _buildTab to load it
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
openFile: function (e, data) {
// Is this file already open?
if( !this._tabBar.find('[data-fileid="' + data.meta.key + '"]').length ){
this._buildTab( data.meta );
} else {
this._tabBar.find('[data-fileid="' + data.meta.key + '"] > a').click();
}
},
/**
* Builds a tab for the file with the given metadata
*
* @param {object} meta Object of file metadata
* @returns {void}
*/
_buildTab: function (meta) {
var self = this;
// Build the actual tab
this._tabBar.append( ips.templates.render('templates.editor.newTab', {
title: meta.title,
fileid: meta.key,
id: 'tab_' + meta.key
}));
// Build the content container
this._tabContent.append( ips.templates.render('templates.editor.tabPanel', {
fileid: meta.key,
name: meta.name,
type: meta.type,
app: meta.app,
location: meta.location,
group: meta.group,
id: meta.id,
inherited: meta.inherited
}));
// We may need to rejig the tab pane wrap to account for wrapped tabs,
// so do that now both tab and panel have been added
this._recalculatePanelWrapper();
// Toggle the new tab
this._tabBar.find('[data-fileid="' + meta.key + '"] > a').click();
// Manually set the content area to loading since we aren't using ui.tabbar's load methods
this._tabContent.addClass('ipsLoading ipsTabs_loadingContent');
// Load the content
ips.getAjax()( this._ajaxURL + '&do=loadTemplate', {
dataType: 'json',
data: {
't_app': meta.app,
't_location': meta.location,
't_group': meta.group,
't_name': meta.name,
't_key': meta.key,
't_type': meta.type
}
})
.done( function (response) {
self._postProcessNewTab( response, meta );
})
.always( function () {
self._tabContent.removeClass('ipsLoading ipsTabs_loadingContent');
});
},
/**
* Once tab content has been returned by ajax, this method builds the content of a tab,
* and initializes codemirrior for syntax highlighting
*
* @param {object} response Response JSON object from ajax request
* @param {object} meta Object of meta data for the tab being created
* @returns {void}
*/
_postProcessNewTab: function (response, meta) {
var content = ips.templates.render('templates.editor.tabContent', {
fileid: meta.key,
content: response.template_content || response.css_content,
variables: response.template_data || response.css_attributes
});
this._tabContent.find('[data-fileid="' + meta.key + '"]').html( content );
this._initCodeMirror( meta.key, meta.type );
},
/**
* Initializes CodeMirror on a textarea with the provided key
*
* @param {string} key Key of the textarea to be turned into codemirrior
* @param {string} type 'templates' or 'css'
* @returns {void}
*/
_initCodeMirror: function (key, type) {
var self = this;
this._cmInstances[ key ] = CodeMirror.fromTextArea( document.getElementById('editor_' + key ), {
mode: (type == 'templates' ? 'htmlmixed' : 'css'),
lineWrapping: this._editorPreferences['wrap'],
lineNumbers: this._editorPreferences['lines']
} );
this._cmInstances[ key ].setSize( null, this._getTabContentHeight() );
this._cmInstances[ key ].on( 'change', function (doc, cm) {
self._setChanged( true, key );
});
},
/**
* Sets a tab to 'unsaved' state
*
* @returns {void}
*/
_setChanged: function (state, key) {
if( state == true ){
// Update 'x' in tab to an unsaved version, then set state on the tab
this._tabBar
.find('[data-fileid="' + key + '"]')
.attr('data-state', 'unsaved')
.find('[data-action="closeTab"]')
.html( ips.templates.render('templates.editor.unsaved') );
} else {
this._tabBar
.find('[data-fileid="' + key + '"]')
.attr('data-state', 'saved')
.find('[data-action="closeTab"]')
.html( ips.templates.render('templates.editor.saved') );
}
},
/**
* Calculates whether the tab bar has wrapped, and if so, resizes the panel wrapper and updates
* CodeMirror instances with the new height
*
* @returns {void}
*/
_recalculatePanelWrapper: function () {
// Get height of the tab bar
var tabHeight = this._tabBar.outerHeight();
if( tabHeight == this._currentHeight ){
return;
}
// Set the top value of the panel
this._tabContent.css( { top: tabHeight + 'px' } );
// Get the height of it
var contentHeight = this._getTabContentHeight();
// Find all codemirror instances and resize those
this._tabContent.find('.CodeMirror').css( { height: contentHeight + 'px' } );
this._currentHeight = tabHeight;
},
/**
* Returns references to both the active tab and the active tab panel
*
* @returns {object} Contains keys 'tab' and 'tabPanel', which are jQuery objects
*/
_getActiveTab: function () {
var toReturn = {
tab: null,
tabPanel: null
};
var tab = this._tabBar.find('.ipsTabs_item.ipsTabs_activeItem').first().parent();
if( !tab.length ){
return toReturn;
}
// Get the associated panel
toReturn = {
tab: tab,
tabPanel: this._tabContent.find('[data-fileid="' + tab.attr('data-fileid') + '"]')
};
return toReturn;
},
/**
* Returns the current height of the tab panel wrapper
*
* @returns {number}
*/
_getTabContentHeight: function () {
return this._tabContent.outerHeight();
}
});
}(jQuery, _));]]> a').attr('data-key');
// If the panel isn't actually open, we'll just show it
if( !panel.length ){
this._tabBar.find('[data-type="' + type + '"]').click();
} else {
var open = this._getOpenNodes( panel );
// Now fetch the new list
var url = this._tabBar.find('[data-type="' + type + '"]').attr('data-tabURL');
ips.getAjax()( url )
.done( function (response) {
panel.html( response );
// Now reopen all the nodes
self._openNodes( open, panel, activeItem );
if( data.fileID ){
// Click it
panel.find('[data-itemid="' + data.fileID + '"]').click();
}
// Let everyone know
self.trigger('fileListRefreshed.templates');
});
}
},
/**
* Opens the nodes provided in the toOpen param
*
* @param {object} toOpen Object of nodes to open, containing three keys: apps, locations, groups
* @returns {void}
*/
_openNodes: function (toOpen, panel, activeItem) {
var selector = [];
// Get the apps
if( toOpen.apps.length ){
for( var i = 0; i < toOpen.apps.length; i++ ){
selector.push('[data-app="' + toOpen.apps[i] + '"]');
}
}
// Get locations
if( toOpen.locations.length ){
for( var i = 0; i < toOpen.locations.length; i++ ){
selector.push('[data-app="' + toOpen.locations[i][0] + '"] [data-location="' + toOpen.locations[i][1] + '"]');
}
}
// Get groups
if( toOpen.groups.length ){
for( var i = 0; i < toOpen.groups.length; i++ ){
var str = '[data-app="' + toOpen.groups[i][0] + '"] ';
str += '[data-location="' + toOpen.groups[i][1] + '"] ';
str += '[data-group="' + toOpen.groups[i][2] + '"]';
selector.push( str );
}
}
// Now close all branches, then reopen the ones matching our selector
panel
.find('.cTemplateList_activeBranch')
.removeClass('cTemplateList_activeBranch')
.addClass('cTemplateList_inactiveBranch')
.end()
.find( selector.join(',') )
.removeClass('cTemplateList_inactiveBranch')
.addClass('cTemplateList_activeBranch');
// Anything to make active?
if( activeItem ){
panel
.find('[data-key="' + activeItem + '"]')
.click();
}
},
/**
* Returns an object containing the open nodes in the provided panel
*
* @param {element} panel Panel element to fetch from
* @returns {object} Three array keys: apps, locations, groups
*/
_getOpenNodes: function (panel) {
var apps = [];
var locations = [];
var groups = [];
// Fetch all open nodes
panel.find('.cTemplateList_activeBranch').each( function (i, item) {
var el = $( item );
if( el.attr('data-app') ){
apps.push( el.attr('data-app') );
}
if( el.attr('data-location') ){
locations.push( [
el.closest('[data-app]').attr('data-app'),
el.attr('data-location')
]);
}
if( el.attr('data-group') ){
groups.push( [
el.closest('[data-app]').attr('data-app'),
el.closest('[data-location]').attr('data-location'),
el.attr('data-group')
]);
}
});
return {
apps: apps,
locations: locations,
groups: groups
};
},
/**
* The editor controller has indicated that a file tab has been selected
* We respond to this event by highlighting the file in the list
*
* @param {event} e Event object
* @returns {void}
*/
selectFile: function (e, data) {
if( data.fileID ){
this._makeActive( data.fileID );
}
},
/**
* Event handler for clicking a file node in the listing.
* Gather metadata from the file, then trigger an event so that the editor controller
* can load it.
*
* @param {event} e Event object
* @returns {void}
*/
openFile: function (e) {
e.preventDefault();
var elem = $( e.currentTarget );
// Get meta data for this file
var meta = {
name: elem.attr('data-name'),
key: elem.attr('data-key'),
title: elem.text(),
group: elem.closest('[data-group]').attr('data-group'),
location: elem.closest('[data-location]').attr('data-location'),
app: elem.closest('[data-app]').attr('data-app'),
type: elem.closest('[data-type]').attr('data-type'),
id: elem.closest('[data-itemID]').attr('data-itemID'),
inherited: elem.closest('[data-inherited-value]').attr('data-inherited-value')
};
Debug.log( meta );
this.trigger( 'openFile.templates', {
meta: meta
});
},
/**
* Event handler for clicking a branch in the listing.
* Expends or collapses the branch
*
* @param {event} e Event object
* @returns {void}
*/
toggleBranch: function (e) {
e.preventDefault();
var branchTrigger = $( e.currentTarget );
var branchItem = branchTrigger.parent();
if( branchItem.hasClass('cTemplateList_inactiveBranch') ){
ips.utils.anim.go( 'fadeInDown', branchItem.find(' > ul') );
branchItem
.removeClass('cTemplateList_inactiveBranch')
.addClass('cTemplateList_activeBranch');
} else {
branchItem.find(' > ul').hide();
branchItem
.removeClass('cTemplateList_activeBranch')
.addClass('cTemplateList_inactiveBranch');
}
},
/**
* Updates the ID of any element with the old ID
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
updateItemID: function (e, data) {
if( data.oldID != data.newID ){
this.scope
.find('[data-itemID="' + data.oldID + '"]')
.attr( 'data-itemID', data.newID );
}
},
/**
* A file's status has changed, so we update it with the new status
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
fileChangedStatus: function (e, data) {
this.scope
.find('[data-key="' + data.key + '"]')
.attr( 'data-inherited-value', data.status );
},
/**
* Finds the provided fileID in the list, highlights it and opens all branches to it
*
* @param {string} fileID fileID of node to higlight
* @returns {void}
*/
_makeActive: function (fileID) {
// Find the file entry
var file = this.scope.find('[data-key="' + fileID +'"]');
var fileType = file.closest('[data-type]').attr('data-type');
var currentType = this._currentType();
// Make all others inactive
this.scope.find('[data-key]').parent().removeClass('cTemplateList_activeNode');
// Make this one active
file.parent().addClass('cTemplateList_activeNode');
// Get all parent nodes, and show them
file.parents('li[data-group], li[data-location], li[data-app]').each( function (idx, parent) {
if( $( parent ).hasClass('cTemplateList_inactiveBranch') ){
$( parent )
.removeClass('cTemplateList_inactiveBranch')
.addClass('cTemplateList_activeBranch')
.find('> ul')
.show();
}
});
// Do we need to change tab?
if( fileType == 'templates' && currentType != 'templates' ){
this._tabBar.find('[data-type="templates"]').click();
} else if( fileType == 'css' && currentType != 'css' ){
this._tabBar.find('[data-type="css"]').click();
}
},
/**
* Returns the currently-selected type being shown (templates or css)
*
* @returns {string}
*/
_currentType: function () {
if( this._tabBar.find('[data-type="templates"]').hasClass('ipsTabs_activeItem') ){
return 'templates';
}
return 'css';
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.templates.variablesDialog.js - Controller for the variables dialog
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.admin.templates.variablesDialog', {
initialize: function () {
this.on( 'click', 'input[type="submit"]', this.submitChange );
},
/**
* Event handler called when the submit button within the dialog is clicked
* Fires an event that the editor controller can respond to
*
* @param {event} e Event object
* @returns {void}
*/
submitChange: function (e) {
this.trigger( 'variablesUpdated.templates', {
fileID: this.scope.find('[name="_variables_fileid"]').val(),
type: this.scope.find('[name="_variables_type"]').val(),
value: this.scope.find('[data-role="variables"]').val()
});
this.trigger( 'closeDialog' );
}
});
}(jQuery, _)); ul').prepend( ips.templates.render('vse.classes.title', {
title: ips.getString('vseMatchedStyles'),
role: "xrayResultsTitle"
}));
},
/**
* Event handler for clicking a class in the list
*
* @param {event} e Event object
* @returns {void}
*/
selectClass: function (e) {
var styleID = $( e.currentTarget ).attr('data-styleID');
if( !styleID ){
return;
}
var styleData = this._getStyleData( styleID );
// Do we need to build panels?
if( !$('#vseClassEditor').find('[data-styleid="' + styleID + '"]').length ){
this._buildPanel( styleID, styleData );
}
// Update title
$('#vseClassEditor').find('[data-role="classTitle"]').text( styleData.title );
// Hide all panels but show the one we're working on
$('#vseClassEditor')
.find('[data-styleid]')
.hide()
.end()
.find('[data-styleid="' + styleID + '"]')
.show();
// Add class to the wrapper which will start the animation
$('#vseClassWrap').addClass('vseShow_editor');
},
/**
* Event handler for clicking 'back' in the class editor
*
* @param {event} e Event object
* @returns {void}
*/
editorBack: function (e) {
$('#vseClassWrap').removeClass('vseShow_editor');
},
/**
* Event handler for the 'show custom css' pane
*
* @param {event} e Event object
* @returns {void}
*/
toggleCustomCSS: function (e) {
e.preventDefault();
if( this._customCSSOpen ){
$( e.currentTarget ).removeClass('ipsButton_normal').addClass('ipsButton_primary');
this._customCSSOpen = false;
$('#vseCustomCSS').hide();
$('#vseMainWrapper').css({ bottom: '0px' });
} else {
$( e.currentTarget ).removeClass('ipsButton_primary').addClass('ipsButton_normal');
this._customCSSOpen = true;
ips.utils.anim.go( 'fadeIn', $('#vseCustomCSS') );
$('#vseMainWrapper').css({ bottom: '300px' });
}
},
/**
* Event handler for the 'select element' button. Toggles xray functionality.
*
* @param {event} e Event object
* @returns {void}
*/
toggleXRay: function (e) {
e.preventDefault();
if( this._xrayOn ){
$( e.currentTarget ).removeClass('ipsButton_normal').addClass('ipsButton_primary');
this._xrayOn = false;
this.stopXRay();
} else {
$( e.currentTarget ).removeClass('ipsButton_primary').addClass('ipsButton_normal');
this._xrayOn = true;
this.startXRay();
}
},
/**
* Sends command to frame to start XRay
*
* @returns {void}
*/
startXRay: function () {
this.sendCommand('xrayStart');
},
/**
* Sends command to frame to stop XRay
*
* @returns {void}
*/
stopXRay: function () {
this.sendCommand('xrayCancel');
// Remove results if any
this.scope
.find('#vseClassList > ul')
.find('> li[data-role="xrayResultsTitle"]')
.remove()
.end()
.find('> li')
.show();
},
/**
* Builds/shows the colorizer panel
*
* @param {event} e Event object
* @returns {void}
*/
startColorizer: function (e) {
e.preventDefault();
// Disable the two buttons
$('#vseColorize, #vseStartXRay').attr('disabled', true);
this.scope.find('#vseClassWrap').hide();
ips.utils.anim.go('fadeIn', this.scope.find('#vseColorizerPanel') );
},
/**
* Event handler for back button on the colorizer panel
*
* @param {event} e Event object
* @returns {void}
*/
colorizerBack: function (e) {
e.preventDefault();
// Disable the two buttons
$('#vseColorize, #vseStartXRay').attr( 'disabled', false );
this.scope.find('#vseColorizerPanel').hide();
ips.utils.anim.go('fadeIn', this.scope.find('#vseClassWrap') );
},
/**
* Close the editor by calling the close method, then redirecting.
*
* @returns {void}
*/
_closeEditor: function ( url ) {
ips.getAjax()( ipsSettings['baseURL'] + '?app=core&module=system&controller=vse&do=close', {
showLoading: true
})
.always( function () {
window.location = url || ips.getSetting('baseURL');
});
},
/**
* Tells the window to update the custom CSS contents
*
* @returns {void}
*/
_updateCustomCSS: function () {
this._unsaved = true;
this._codeMirror.save();
this.sendCommand('updateCustomCSS', {
css: $('#vseCustomCSS_editor').val()
});
},
/**
* Generates an object of style properties that can be passed into ips.utils.css to build a style block
*
* @param {string} selector If provided, will only returns the styles for the given selector. Otherwise, all selectors are built.
* @returns {object}
*/
_generateLiveStyleObject: function (selector) {
var self = this;
var returnedObject = {};
_.each( this._data.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
if( selector && styleData.selector == selector ){
returnedObject = self._getStyleObjectForSelector( styleData, styleKey, sectionKey );
} else if( _.isUndefined( selector ) ) {
if( _.isUndefined( returnedObject[ styleData.selector ] ) ){
returnedObject[ styleData.selector ] = {};
}
returnedObject[ styleData.selector ] = self._getStyleObjectForSelector( styleData, styleKey, sectionKey );
}
});
});
return returnedObject;
},
/**
* Takes style data from our main data object and returns an object containing CSS rules, ready to be used by ips.utils.css
* Intended to be executed by this._generateLiveStyleObject, not directly
*
* @param {object} styleData The data for the selector this iteration is building
* @param {string} styleKey The key in our master data object that identifies this style
* @param {string} sectionKey The key in our master data object that identifies the section this style belongs to
* @returns {object}
*/
_getStyleObjectForSelector: function (styleData, styleKey, sectionKey) {
var self = this;
var returnedObject = {};
var orig = this._originalData;
// Background
if( !_.isUndefined( styleData.background ) ){
var tmpNew = styleData.background;
var tmpOrig = orig.sections[ sectionKey ][ styleKey ];
var doGradient = false;
// Background COLOR
if( !_.isUndefined( tmpNew.color ) ){
if( _.isUndefined( tmpOrig.background ) || _.isUndefined( tmpOrig.background.color ) ||
tmpOrig.background.color != tmpNew.color ) {
returnedObject[ ( !_.isUndefined( tmpNew.gradient ) ) ? 'background-color' : 'background' ] = '#' + tmpNew.color.replace('#', '');
}
}
// Background GRADIENT
if( !_.isUndefined( tmpNew.gradient ) ){
if( _.isUndefined( tmpOrig.background ) || _.isUndefined( tmpOrig.background.gradient ) ) {
doGradient = true;
} else if ( tmpOrig.background.gradient.angle != tmpNew.gradient.angle ) {
doGradient = true;
} else {
// Compare each stop
for( var i = 0; i < tmpNew.gradient.stops.length; i++ ){
if( _.isUndefined( tmpOrig.background.gradient.stops[ i ] ) ){
doGradient = true;
break;
}
if( tmpNew.gradient.stops[ i ][ 0 ] != tmpOrig.background.gradient.stops[ i ][ 0 ] ){
doGradient = true;
break;
}
if( tmpNew.gradient.stops[ i ][ 1 ] != tmpOrig.background.gradient.stops[ i ][ 1 ] ){
doGradient = true;
break;
}
}
}
if( doGradient ){
returnedObject['background-image'] = ips.utils.css.generateGradient( tmpNew.gradient.angle, tmpNew.gradient.stops, false );
}
} else {
// No gradient here, but if our original data does, we need to remove it
if( !_.isUndefined( tmpOrig.background.gradient ) ){
returnedObject['background-image'] = 'none';
}
}
}
// Font
if( !_.isUndefined( styleData.font ) ){
var tmpNew = styleData.font;
var tmpOrig = orig.sections[ sectionKey ][ styleKey ];
// Background COLOR
if( !_.isUndefined( tmpNew.color ) ){
if( _.isUndefined( tmpOrig.font ) || _.isUndefined( tmpOrig.font.color ) ||
tmpOrig.font.color != tmpNew.color ) {
returnedObject['color'] = '#' + tmpNew.color.replace('#', '');
}
}
}
return returnedObject;
},
/**
* Builds the class list, from which the user can choose which class to edit
*
* @returns {void}
*/
_buildClassList: function () {
var self = this;
var output = '';
_.each( this.getVSEData().sections, function (value, key) {
output += ips.templates.render('vse.classes.title', {
title: ips.getString( 'vseSection_' + key ),
key: key
});
if( _.isObject( value ) && _.size( value ) ){
_.each( value, function (item, itemKey) {
output += ips.templates.render('vse.classes.item', {
title: item.title,
hasFont: !_.isUndefined( item.font ),
styleid: key + '_' + itemKey,
swatch: self._buildSwatch( item, true )
});
});
}
});
this.scope.find('#vseClassList > ul').html( output );
},
/**
* Builds style properties to be used on the swatch in the class list
*
* @param {object} data Key/value pairs of styles to be changed
* @param {boolean} toString If true, object is turned into string to use in style=''
* @returns {mixed} Object or string
*/
_buildSwatch: function (data, toString) {
var defaultStyle = {
'display': 'block',
'background-color': 'transparent',
'background-image': 'none',
'border': 'none'
};
var main = _.clone( defaultStyle );
var sub = _.clone( defaultStyle );
if( !_.isUndefined( data.background ) ){
if( !_.isUndefined( data.background.color ) ){
_.extend( main, { 'background-color': '#' + data.background.color } );
}
if( !_.isUndefined( data.background.gradient ) ){
_.extend( main, { 'background-image': ips.utils.css.generateGradient( data.background.gradient.angle, data.background.gradient.stops ) } );
}
} else {
_.extend( main, { background: 'transparent' } );
}
if( !_.isUndefined( data.font ) && !_.isUndefined( data.font.color ) ){
_.extend( sub, { background: '#' + data.font.color } );
}
if( toString ){
var finalObj = {};
_.each( { main: main, sub: sub }, function (obj, key) {
var output = [];
var pairs = _.pairs( obj );
_.each( pairs, function (value, idx ){
if( _.isArray( value[ 1 ] ) ){
for( var i = 0; i < value[ 1 ].length; i++ ){
output.push( value[ 0 ] + ': ' + value[ 1 ][ i ] );
}
} else {
output.push( value.join(': ') );
}
});
finalObj[ key ] = output.join('; ');
});
return finalObj;
} else {
return { main: main, sub: sub };
}
},
/**
* Updates an existing swatch based on its selector
*
* @param {string} selector Selector in which to find the swatch
* @returns {void}
*/
_updateSwatch: function (selector) {
var selectorData = this._findDataBySelector( selector );
var newStyles = this._buildSwatch( selectorData.styleData, true );
// Find the swatch
var row = this.scope.find('[data-styleid="' + selectorData.sectionKey + '_' + selectorData.styleKey + '"]');
var mainSwatch = row.find('a > .vseClass_swatch');
var subSwatch = row.find('a > .vseClass_swatch > .vseClass_swatch');
// Update css
// Use the style attribute here because _buildSwatch is returning a string like background: ...; color: ...;, so .css() won't work
mainSwatch.attr( 'style', newStyles.main );
if( subSwatch.length && newStyles.sub ){
subSwatch.attr( 'style', newStyles.sub );
}
},
/**
* Returns data from our main data object based on the selector provided. Returns the style data, style key, section data and section key.
*
* @param {string} selector Selector to return data for
* @returns {object|null}
*/
_findDataBySelector: function (selector) {
var data = null;
_.each( this._data.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
if( styleData.selector == selector ){
data = {
styleData: styleData,
styleKey: styleKey,
sectionData: sectionData,
sectionKey: sectionKey
};
}
});
});
return data;
},
/**
* Returns style data for the given style ID
*
* @param {string} styleID Style ID to fetch
* @returns {mixed} Object or false
*/
_getStyleData: function (styleID) {
var pieces = styleID.split('_');
if( !_.isUndefined( this.getVSEData().sections[ pieces[0] ][ pieces[ 1 ] ] ) ){
return this.getVSEData().sections[ pieces[0] ][ pieces[ 1 ] ];
}
return false;
},
/**
* Returns an array of selectors the VSE can work with
*
* @returns {array}
*/
_getSelectors: function () {
var selectors = [];
_.each( this._data.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
selectors.push( styleData.selector );
});
});
return selectors;
},
/**
* Builds an empty panel
*
* @param {string} styleID The styleID of the panel being created
* @param {object} styleData Data for this style from our main data object
* @returns {void}
*/
_buildPanel: function (styleID, styleData) {
var output = [];
var innerContainer = $('').attr('data-role', 'widgets');
var container = $('')
.attr( 'id', styleID + '_panel' )
.attr( 'data-styleid', styleID )
.attr('data-controller', 'core.front.vse.panel')
.append( innerContainer );
if( !styleData ){
return;
}
$('#vseClassEditor')
.find('[data-role="panels"]')
.append( container )
.end()
.find( '#' + styleID + '_panel' )
.data( 'styleData', styleData );
$( document ).trigger( 'contentChange', [ container ] );
},
/**
* Takes our data object and turns it into a stylesheet and an object representing settings to be updated
*
* @returns {object}
*/
_buildFinalStyleData: function () {
var self = this;
var settingsObj = {};
var stylesObj = {};
var styleBlock = '';
var orig = this._originalData;
var setStyle = function (selector) {
if( _.isUndefined( stylesObj[ selector ] ) ){
stylesObj[ selector ] = {};
}
return stylesObj[ selector ];
};
var updateSetting = function (key, value) {
settingsObj[ key ] = value;
};
// Loop through every section then every style, and build
_.each( this._data.sections, function (sectionData, sectionKey) {
_.each( sectionData, function (styleData, styleKey) {
if( !_.isUndefined( styleData.background ) ){
var tmpNew = styleData.background;
var tmpOrig = orig.sections[ sectionKey ][ styleKey ].background || false;
// Background COLOR
if( !_.isUndefined( tmpNew.color ) ){
if( ( !tmpOrig || _.isUndefined( tmpOrig.color ) ) || tmpOrig.color != tmpNew.color ) {
var settingKeyColor = self._settingKey( styleData, 'background', 'color' );
// Do we have a setting for this one?
if( settingKeyColor ){
updateSetting( settingKeyColor, '#' + tmpNew.color.replace('#', '') );
} else {
setStyle( styleData.selector )[ ( !_.isUndefined( tmpNew.gradient ) ) ? 'background-color' : 'background' ] = '#' + tmpNew.color.replace('#', '');
}
}
}
// Background GRADIENT
if( !_.isUndefined( tmpNew.gradient ) ){
// First stage - figure out if our gradients have the same angle, stop locations and stop length
// If so, and if settings exist, we can just update settings. Otherwise we need to build a new gradient block
var settingKeyFrom = self._settingKey( styleData, 'background', 'from' );
var settingKeyTo = self._settingKey( styleData, 'background', 'to' );
var gradientDone = false;
if( settingKeyFrom && settingKeyTo && !_.isUndefined( tmpOrig.gradient ) ){
var stopsMatch = self._doStopLocationsMatch( tmpOrig.gradient.stops, tmpNew.gradient.stops );
if( stopsMatch && tmpOrig.gradient.angle == tmpNew.gradient.angle ){
updateSetting( settingKeyFrom, tmpNew.gradient.stops[0][0] );
updateSetting( settingKeyTo, tmpNew.gradient.stops[1][0] );
gradientDone = true;
}
}
if( !gradientDone ){
setStyle( styleData.selector )['background-image'] = ips.utils.css.generateGradient( tmpNew.gradient.angle, tmpNew.gradient.stops, false );
}
} else {
// No gradient here, but if our original data does, we need to remove it
if( !_.isUndefined( tmpOrig.gradient ) ){
setStyle( styleData.selector )['background-image'] = 'none';
}
}
}
// Font
if( !_.isUndefined( styleData.font ) ){
var tmpNew = styleData.font;
var tmpOrig = orig.sections[ sectionKey ][ styleKey ].font || false;
var settingKeyFont = self._settingKey( styleData, 'color' );
// Background COLOR
if( !_.isUndefined( tmpNew.color ) ){
if( !tmpOrig || _.isUndefined( tmpOrig.color ) || tmpOrig.color != tmpNew.color ) {
if( settingKeyFont ){
updateSetting( settingKeyFont, '#' + tmpNew.color.replace('#', '') );
} else {
setStyle( styleData.selector )['color'] = '#' + tmpNew.color.replace('#', '');
}
}
}
}
});
});
if( _.size( stylesObj ) ){
_.each( stylesObj, function (value, selector) {
styleBlock += ips.utils.css.buildStyleBlock( selector, value );
});
}
// Get other form fields
this.scope.find('#ipsTabs_vseSection_vseSettingsTab_panel :input').each( function () {
settingsObj[ $( this ).attr('name') ] = $( this ).val();
});
return {
customcss: $('#vseCustomCSS_editor').val(),
values: JSON.stringify( this._data ),
css: styleBlock,
settings: settingsObj
};
},
/**
* Finds and returns a setting key in the provided data object
*
* @param {object} data Data object to look in. 'settings' should be a child value.
* @param {string} levelOne Key of the first level inside settings
* @param {string} levelTwo Key of the second level inside settings. Optional.
* @returns {string|void}
*/
_settingKey: function (data, levelOne, levelTwo) {
if( !data.settings || !levelOne ){
return;
}
if( levelTwo ){
if( !_.isUndefined( data.settings[ levelOne ][ levelTwo ] ) ){
return data.settings[ levelOne ][ levelTwo ];
}
}
return data.settings[ levelOne ];
return;
},
/**
* Compares two gradient stop arrays, returning false if their lengths don't match or any locations differ
*
* @param {array} originalStops Original array of stops
* @param {string} newStops New array of stops
* @returns {boolean}
*/
_doStopLocationsMatch: function (originalStops, newStops) {
if( originalStops.length != newStops.length ){
return false;
}
// originalStops[ i ][ 0 ] != newStops[ i ][ 0 ] ||
for( var i = 0; i < originalStops.length; i++ ){
if( originalStops[ i ][ 1 ] != newStops[ i ][ 1 ] ){
return false;
}
}
return true;
}
});
}(jQuery, _));]]>')
.attr('id', this.scope.attr('id') + '_' + type )
.attr( 'data-controller', 'core.front.vse.panel' + ips.utils.uppercaseFirst( type ) )
.data( 'styleData', data )
);
}
});
}(jQuery, _));]]>= next ){
// If this is the first stop, and the value is bigger than the next stop, halve it
$( this ).val( Math.floor( next / 2 ) );
} else if( index == ( stops.length - 1 ) && value <= prev ){
// If this is the last stop, and the value is smaller than the prev stop, add one (up to 100)
$( this ).val( _.min( [ 100, prev + 1 ] ) );
} else if( ( index > 0 && index < ( stops.length - 1 ) ) && ( value <= prev || value >= next ) ){
// If the value is less than the prev or more than the next, then adjust it
if( next > prev ){
$( this ).val( prev + ( ( next - prev ) / 2 ) );
} else {
$( this ).val( prev + 1 );
}
}
});
}
this.refreshGradientPreview();
},
/**
* Removes a gradient stop from the list
*
* @param {event} e Event object
* @returns {void}
*/
removeGradientStop: function (e) {
// Find the row
$( e.currentTarget ).closest('li').remove();
this.refreshGradientPreview();
},
/**
* Resets the gradient editor, removing it from the dom and resetting controller properties
*
* @returns {void}
*/
_resetGradientEditor: function () {
// Hide the editor
this._gradientEditor.hide().remove();
ips.utils.anim.go( 'fadeIn', this.scope.find('[data-role="backgroundControls"]') );
// Reset data
this._gradientEditor = null;
this._currentAngle = 0;
this._currentStops = null;
},
/**
* Updates the background preview chip using current data
*
* @returns {void}
*/
_updateBackgroundPreview: function () {
var chip = this.scope.find('[data-role="backgroundPreview"]');
// Reset the properties first
chip.css({
backgroundColor: 'transparent',
backgroundImage: 'none'
});
// Add color
if( this._data.color ){
chip.css({
backgroundColor: '#' + this._data.color
});
}
// Gradient
if( this._data.gradient ){
var gradient = ips.utils.css.generateGradient( this._data.gradient.angle, this._data.gradient.stops, false );
_.each( gradient, function (val) {
chip.css({
backgroundImage: val
});
});
}
},
/**
* Builds the gradient editor HTML, either in a default state or based on the data we have for an existing gradient
*
* @returns {void}
*/
_buildGradientEditor: function () {
var self = this;
var buttons = ( _.isUndefined( this._data.gradient ) ) ? ips.templates.render('vse.gradient.twoButtons') : ips.templates.render('vse.gradient.threeButtons');
this.scope.find('[data-role="backgroundControls"]').after(
$('').attr('data-role', 'gradientEditor').html( ips.templates.render('vse.gradient.editor', {
buttons: buttons
}) )
);
this._gradientEditor = this.scope.find('[data-role="gradientEditor"]');
this._gradientStops = this._gradientEditor.find('[data-role="gradientStops"]');
// Do we have an existing gradient to show?
if( !_.isUndefined( this._data.gradient ) ){
_.each( this._data.gradient.stops, function (value) {
self._gradientStops
.find('li:last-child')
.before( ips.templates.render('vse.gradient.stop', {
color: value[0],
location: parseInt( value[1] )
}));
});
} else {
// No existing gradient - add two default rows
this._gradientStops
.find('li:last-child')
.before( ips.templates.render('vse.gradient.stop', { color: '6592c4', location: 0 } ))
.before( ips.templates.render('vse.gradient.stop', { color: '2b517b', location: 100 } ));
}
this._currentAngle = ( _.isUndefined( this._data.gradient ) ) ? 90 : this._data.gradient.angle || 90;
this.refreshGradientPreview();
// Highlight the correct angle button
this._gradientEditor
.find('[data-action="gradientAngle"][data-angle="' + this._currentAngle + '"]')
.addClass('ipsButton_normal')
.removeClass('ipsButton_primary');
// Set up jscolor on inputs
this._gradientStops.find('input[type="text"]').each( function () {
new jscolor.color( this );
});
// Disable the save & cancel buttons for now
this._gradientEditor.find('[data-action="cancelGradient"], [data-action="saveGradient"]').prop( 'disabled', true );
// Allow stops to be sortable
ips.loader.get( ['core/interface/jquery/jquery-ui.js'] ).then( function () {
self._gradientStops.sortable({
handle: '.fa-bars',
axis: 'y',
update: _.bind( self.changeStopOrdering, self )
})
});
},
/**
* Returns an array of the current stops the gradient editor has
*
* @returns {array} Format: [ [ color, location ], [ color, location ] ]
*/
_getGradientStops: function () {
var self = this;
var output = [];
this._gradientStops.find('> li:not( :first-child ):not( :last-child )').each( function () {
output.push( [ $( this ).find('input[type="text"]').val(), $( this ).find('input[type="range"]').val() ] );
});
return output;
}
});
}(jQuery, _));]]>/**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
* ips.vse.panelFont.js - VSE font panel controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('core.front.vse.panelFont', {
_data: {},
initialize: function () {
this.on( 'change', '[data-role="fontColor"]', this.changeFontColor );
this.setup();
},
/**
* Setup: builds the font panel
*
* @returns {void}
*/
setup: function () {
this._data = this.scope.data('styleData');
this.scope.append( ips.templates.render('vse.panels.wrapper', {
content: ips.templates.render('vse.panels.font', {
fontColor: this._data.color
}),
type: 'font'
}) );
// Add jscolor to boxes
this.scope.find('input[type="text"].color').each( function () {
new jscolor.color( this );
});
},
/**
* Event handler for the font color box changing value
*
* @param {event} e Event object
* @returns {void}
*/
changeFontColor: function (e) {
this._data.color = $( e.currentTarget ).val();
this.trigger('widgetStyleUpdated');
}
});
}(jQuery, _));').attr('type', 'text/css').attr('id', 'elInjectedStyles') )
.append( $('').attr('type', 'text/css').attr('id', 'elCustomCSS') );
this._stylesheet = $('#elInjectedStyles');
this._custom = $('#elCustomCSS');
// Build URL
var url = ips.utils.url.getURIObject( window.location.href );
this._url = url.protocol + '://' + url.host;
if ( url.port && url.port != 80 ) {
this._url = this._url + ':' + url.port;
}
this.sendCommand('windowReady');
this.sendCommand('getStylesheet'); // Ask the window for the initial stylesheet
},
/**
* Handles commands from the main window, executing the appropriate method here.
*
* @param {event} e Event object
* @returns {void}
*/
handleCommand: function (e) {
if( e.originalEvent.origin != this._url ){
Debug.error("Invalid origin");
return;
}
var pieces = e.originalEvent.data.command.split('.');
var obj = this;
for( var i = 0; i < pieces.length - 1; i++ ){
if( !_.isUndefined( obj[ pieces[ i ] ] ) ){
obj = obj[ pieces[ i ] ];
} else {
Debug.error( "Couldn't run vse.window." + e.originalEvent.data.command );
return;
}
}
if( obj[ pieces[ pieces.length - 1 ] ] ){
obj[ pieces[ pieces.length - 1 ] ]( e.originalEvent.data );
} else {
Debug.error( "Couldn't run vse.window." + e.originalEvent.data.command );
return;
}
},
/**
* Sends a command to the main window
*
* @param {string} command Command name
* @param {object} data Data object
* @returns {void}
*/
sendCommand: function (command, data) {
top.postMessage( _.extend( data || {}, { command: command } ), this._url );
},
/**
* Command from the main controller letting us know a style has been updated
*
* @param {object} data Data object
* @returns {void}
*/
updateStyle: function (data) {
ips.utils.css.replaceStyle( 'elInjectedStyles', data.selector, data.styles );
},
/**
* Updates the freeform style tag with custom css
*
* @param {object} data Data object
* @returns {void}
*/
updateCustomCSS: function (data) {
this._custom.html( data.css );
},
/**
* The main window has sent us selector data, which the xRay will use to find elements
*
* @param {object} data Data object
* @returns {void}
*/
selectorData: function (data) {
this._selectors = data.selectors;
},
/**
* Replaces the injected stylesheet contents with new content, built from the data object
*
*
* {
* classes: {
* 'body': {
* 'background-color': 'red'
* },
* '.ipsButton': {
* 'color': 'blue'
* }
* }
* }
*
* @param {object} data Object of style data
* @returns {void}
*/
createStylesheet: function (data) {
var update = '';
_.each( data.classes, function (val, key) {
if( ( _.isObject( val ) && !_.size( val ) ) || !val ){
return;
}
update += key + "{\n";
_.each( val, function (styleVal, styleKey) {
if( _.isObject( styleVal ) ){
_.each( styleVal, function (thisVal) {
update += styleKey + ': ' + thisVal + ";\n";
});
} else {
update += styleKey + ': ' + styleVal + ";\n";
}
});
update += "}\n\n";
});
this._stylesheet.html( update );
this._custom.html( data.custom || '' );
},
/**
* Starts the xray
*
* @returns {void}
*/
xrayStart: function () {
this._createXRayElem();
this._pointerEvents = this._supportsPointerEvents();
this._events['down'] = $( document ).on( 'mousedown.xray', _.bind( this._doDown, this ) );
this._events['move'] = $( document ).on( 'mousemove.xray', _.bind( this._doFalse, this ) );
this._events['over'] = $( document ).on( 'mouseover.xray', _.bind( this._doOver, this ) );
},
/**
* Cancels the xray
*
* @returns {void}
*/
xrayCancel: function () {
if( this._xrayElem && this._xrayElem.length ){
this._xrayElem.remove();
}
this._stop();
},
/**
* Stops the xray
*
* @returns {void}
*/
_stop: function () {
$( document ).off('.xray');
},
/**
* Initializes the xray function by resizing/positioning the xray element to fit the currently-hovered element
*
* @param {event} e Event object
* @returns {void}
*/
_doXray: function (e) {
e.preventDefault();
var elem = $( e.target );
if( elem.is( this._exclude ) ){
return;
}
var elemPosition = ips.utils.position.getElemPosition( elem );
var elemDims = { width: elem.outerWidth(), height: elem.outerHeight() };
this._xrayElem.css({
width: elemDims.width + 'px',
height: elemDims.height + 'px',
left: elemPosition.absPos.left + 'px',
top: elemPosition.absPos.top + 'px',
zIndex: ips.ui.zIndex()
});
},
/**
* Event handler for mousedown
*
* @param {event} e Event object
* @returns {void}
*/
_doDown: function (e) {
e.preventDefault();
this._doXray(e);
this._stop();
this._findMatchingSelectors( $( e.target ) );
},
/**
* Event handler for mouseover
* If browser supports pointerEvents we highlight the hovered element, otherwise do nothing
*
* @param {event} e Event object
* @returns {void}
*/
_doOver: function (e) {
if( this._pointerEvents ){
this._doXray(e);
} else {
this._doFalse(e);
}
},
/**
* Called to prevent xray events from bubbling
*
* @param {event} e Event object
* @returns {void}
*/
_doFalse: function (e) {
e.preventDefault();
},
/**
* Creates the xray element and attaches it to body ready for this._doXray
*
* @param {event} e Event object
* @returns {void}
*/
_createXRayElem: function () {
if( $('#vseXRay').length ){
$('#vseXRay').remove();
}
$('body').append( $('').attr( 'id', 'vseXRay' ) );
this._xrayElem = $('#vseXRay');
},
/**
* Finds the ancestors of the clicked elemented, and matches selectors from our list
* Sends command to the main controller with our results
*
* @param {element} elem Clicked element
* @returns {void}
*/
_findMatchingSelectors: function (elem) {
// Get all ancestors
var ancestors = elem.parents().addBack( elem );
var matched = [];
var primaryMatch = null;
var self = this;
var selectorsLength = this._selectors.length;
_.each( ancestors.get().reverse(), function (value, idx) {
for( var i = 0; i < selectorsLength; i++ ){
if( $( value ).is( self._selectors[ i ] ) ){
if( idx == 0 ){
primaryMatch = self._selectors[ i ];
} else {
matched.push( self._selectors[ i ] );
}
}
}
});
this.sendCommand( 'selectorsMatched', {
primary: primaryMatch,
other: matched
});
},
/**
* Determines whether the browser supports css pointer-events style
*
* @param {event} e Event object
* @returns {void}
*/
_supportsPointerEvents: function () {
// From https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js
var element = document.createElement('x');
element.style.cssText = 'pointer-events:auto';
return element.style.pointerEvents === 'auto';
}
});
}(jQuery, _));]]> ul');
this._registerArea();
// Determine whether we're showing any blocks to start with
if( this.scope.find('[data-blockID]').length ){
this._wasUnused = false;
}
},
/**
* Called when we're managing widgets
*
* @returns {void}
*/
managingStarted: function () {
this._managing = true;
this.scope.addClass('cWidgetContainer_managing');
this._setWidgetsToManaging( true );
},
/**
* Called when we're no longer managing widgets
*
* @returns {void}
*/
managingFinished: function () {
this._managing = false;
this.scope.removeClass('cWidgetContainer_managing');
this._setWidgetsToManaging( false );
if( this._readyForDragging ){
this._list.sortable('destroy');
}
// See whether we have anything to display
this.scope.toggleClass('ipsHide', !this.scope.find('[data-blockID]').length );
},
/**
* Prepares the area for drag and drop functionality
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
prepareForDragging: function (e, data) {
var self = this;
this._list.css({
zIndex: ips.ui.zIndex(),
});
this._list.sortable({
dragHandle: '.cSidebarBlock_managing',
receive: _.bind( self.receiveWidget, self ),
placeholder: 'cSidebarManager_placeholder',
update: _.bind( self.updateOrdering, self ),
connectWith: data.selector,
scroll: true
});
this._readyForDragging = true;
},
/**
* Event handler for the sortable receiving a new widget
*
* @param {event} e Event object
* @param {object} ui jQuery UI object
* @returns {void}
*/
receiveWidget: function (e, ui) {
var blockID = ui.item.attr('data-blockID');
var hasConfig = ui.item.attr('data-blockConfig');
var title = ui.item.attr('data-blockTitle');
// Create a new widget
this._buildNewWidget( blockID, this.scope.find('> ul > li').index( ui.item.get(0) ), hasConfig, title );
// Cancel the move because we actually just want to hide it
ui.sender.sortable('cancel');
if ( ! ui.item.attr('data-allowReuse') )
{
ui.item.hide().attr('data-hidden', true);
}
},
/**
* Updates the ordering of widgets in this area
*
* @returns {void}
*/
updateOrdering: function (without) {
if( !this._readyForDragging ){
Debug.log('trying...');
setTimeout( _.bind( this.updateOrdering, this ), 500 );
return;
}
var body = $('body');
var order = this.scope.find('> ul').sortable('toArray', {
attribute: 'data-blockID'
});
order = ( without ) ? _.without( _.uniq( order ), without ) : _.uniq( order );
// Remove hidden blocks as these should not be stored
var self = this;
_.each( order, function( value, key )
{
if ( self.scope.find('li[data-blockID=' + value + ']').attr('data-hidden') == 'true' )
{
order = _.without( order, value );
}
} );
ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=widgets&do=saveOrder', {
data: {
order: order,
pageApp: body.attr('data-pageApp'),
pageModule: body.attr('data-pageModule'),
pageController: body.attr('data-pageController'),
area: this._areaID
}
})
.fail( function () {
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('sidebarError'),
callbacks: {}
});
});
},
/**
* A widget has finished loading itself - if we're in managing state, tell it to set itself so
*
* @param {event} e The event
* @param {object} data Event data object
* @returns {void}
*/
widgetLoaded: function (e, data) {
if( this._managing ) {
$( e.target ).trigger('startManaging.widgets');
}
},
/**
* A widget has been removed
*
* @param {event} e The event
* @param {object} data Event data object
* @returns {void}
*/
widgetRemoved: function (e, data) {
this.updateOrdering( data.blockID );
},
/**
* Builds and loads a new widget into the list
*
* @param {string} blockID Block ID to load
* @param {number} idx Index of the element we'll place the new widget before
* @param {string} title Title of the new widget
* @returns {void}
*/
_buildNewWidget: function (blockID, idx, hasConfig, title) {
/* Does this already have a unique ID? */
var bits = blockID.split('_');
var newBlockID = blockID;
if ( _.isUndefined( bits[3] ) )
{
newBlockID = blockID + '_' + Math.random().toString(36).substr(2, 9);
}
var newWidget = $('')
.attr('data-blockID', newBlockID)
.attr('data-blockTitle', title )
.addClass('ipsWidget')
.removeClass('ipsWidget_horizontal ipsWidget_vertical')
.addClass('ipsWidget_' + this._orientation)
.attr('data-controller', 'core.front.widgets.block');
if( hasConfig ){
newWidget.attr( 'data-blockConfig', "true" );
}
var before = $( this.scope.find('> ul > li:not( .cSidebarBlock_placeholder )').get( idx ) );
if( !_.isUndefined( idx ) && before.length){
before.before( newWidget );
} else {
this.scope.find('> ul').prepend( newWidget );
}
// Init new widget
$( document ).trigger( 'contentChange', [ newWidget ] );
// Instruct it to load itself
newWidget.trigger('reloadContents.sidebar');
},
/**
* Triggers an event on all widgets, to instruct them to set turn on or off 'managing' state
*
* @param {boolean} status Managing status
* @returns {void}
*/
_setWidgetsToManaging: function (status) {
this.triggerOn( 'core.front.widgets.block', ( status ) ? 'startManaging.widgets' : 'stopManaging.widgets' );
},
/**
* Fires an event that registers this area with the main manager controller
*
* @returns {void}
*/
_registerArea: function () {
var usedBlocks = this.scope.find('[data-blockID]');
var blockIDs = [];
if( usedBlocks.length ){
usedBlocks.each( function (idx, val) {
var blockID = $( val ).attr('data-blockID');
if( blockID ){
blockIDs.push( blockID );
}
});
}
this.trigger( 'registerArea.widgets', {
areaID: this._areaID,
areaElem: this.scope,
ids: blockIDs
});
}
});
}(jQuery, _));]]>').addClass('ipsLoading').css({ height: '100px' }) );
setTimeout( function () {
ips.getAjax()( ips.getSetting('baseURL') + 'index.php?app=core&module=system&controller=widgets&do=getConfiguration', {
data: {
block: block,
pageApp: body.attr('data-pageApp'),
pageModule: body.attr('data-pageModule'),
pageController: body.attr('data-pageController'),
pageArea: area
}
} )
.done( function (response) {
data.menu
.html( response )
.find('form')
.on( 'submit', _.bind( self._configurationForm, self, data.menu ) );
$( document ).trigger('contentChange', [ data.menu ] );
});
}, 1000);
}
},
/**
* Triggered by a modal form save
*
*/
submitDialog: function( e, data )
{
/* Dialog event triggers in all blocks, is this the one we have open? */
if ( this._modalOpen == this._blockID )
{
this._modalOpen = false;
this.reloadContent();
}
},
/**
* Marks lists within this block as read
*
* @returns {void}
*/
markAllRead: function () {
// Update row
this.scope
.find('.ipsDataItem, .ipsDataItem_subList')
.removeClass('ipsDataItem_unread')
.find('.ipsItemStatus')
.addClass('ipsItemStatus_read');
},
/**
* Submit handler for the configuration form
*
* @returns {void}
*/
_configurationForm: function (menu, e) {
var self = this;
e.preventDefault();
ips.getAjax()( $( e.currentTarget ).attr('action'), {
data: $( e.currentTarget ).serialize(),
type: 'post'
})
.done( function (response) {
if( response === 'OK' ){
self.reloadContent();
menu.trigger('closeMenu');
} else {
menu.html( response );
}
})
.fail( function () {
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('sidebarConfigError'),
callbacks: {}
});
});
},
/**
* Sets the loading status of this widget
*
* @returns {void}
*/
_setLoading: function (status) {
if( status ){
this.scope.html('').addClass('ipsLoading cSidebarBlock_loading');
} else {
this.scope.removeClass('ipsLoading cSidebarBlock_loading');
}
},
/**
* Removes the unique key from the block ID
*
* @param {string} block Block ID with unique key (app_core_whosOnline_4vbvzbw)
* @returns {string}
*/
_getBlockIDWithoutUniqueKey: function (block) {
var bits = block.split('_');
return bits[0] + '_' + bits[1] + '_' + bits[2];
}
});
}(jQuery, _));]]> div:first-child').css({
overflow: '',
position: '',
top: ''
});
}
this.triggerOn( 'core.front.widgets.area', 'managingStarted.widgets');
this._showManager();
this.scope.addClass('cWidgetsManaging');
},
/**
* Closes the manager panel
*
* @param {event} e The event
* @returns {void}
*/
closeSidebarManager: function (e) {
e.preventDefault();
if( this._inManagingState ){
this._hideManager();
this._cancelDragging();
this.triggerOn( 'core.front.widgets.area', 'managingFinished.widgets' );
this.scope.removeClass('cWidgetsManaging');
}
},
/**
* Toggles a block list in the manager panel
*
* @param {event} e The event
* @returns {void}
*/
toggleSection: function (e) {
var target = $( e.currentTarget );
if( target.hasClass('cSidebarManager_open') ){
target
.removeClass('cSidebarManager_open')
.addClass('cSidebarManager_closed')
.find('+ ul, + ul + p')
.hide();
} else {
target
.removeClass('cSidebarManager_closed')
.addClass('cSidebarManager_open');
if( target.find('+ ul > li:not( [data-hidden] )').length ) {
ips.utils.anim.go( 'fadeIn', target.find('+ ul') );
} else {
target.find('+ ul + p').show();
}
}
},
widgetRemoved: function (e, data) {
ips.utils.anim.go( 'fadeIn', this.scope.find('[data-role="availableBlocks"]').find('[data-blockID="' + this._getBlockIDWithoutUniqueKey( data.blockID ) + '"]').removeAttr('data-hidden') );
},
/**
* Set up drags on the main list and manager lists
*
* @param {event} e The event
* @returns {void}
*/
_setUpDragging: function () {
var self = this;
var managerList = this.scope.find('[data-role="availableBlocks"] ul');
var selectors = self._buildAreaSelector();
ips.loader.get( ['core/interface/jquery/jquery-ui.js'] ).then( function () {
managerList.css({
zIndex: ips.ui.zIndex(),
})
.sortable({
dragHandle: '.cSidebarManager_block',
connectWith: selectors,
placeholder: 'cSidebarManager_placeholder',
scroll: true,
start: _.bind( self._startDragging, self ),
stop: _.bind( self._cancelDragging, self )
});
self.triggerOn( 'core.front.widgets.area', 'prepareForDragging.widgets', {
selector: selectors
});
});
},
/**
* When we start dragging, we need to set the overflow on the list to be 'visible' (rather
* than 'hidden') so that items in the list will be visible when they leave the element.
* At the same time, we have to adjust the top position of the list to imitate the previous scrolled
* position, then refresh the sortable so that the dragged element is in the right place. Phew.
*
* @returns {void}
*/
_startDragging: function () {
var sidebarPanel = this.scope.find('#elSidebarManager > div:first-child');
var scrollTop = sidebarPanel.scrollTop();
sidebarPanel.css({
overflow: 'visible',
position: 'relative',
top: '-' + scrollTop + 'px'
});
var managerList = this.scope.find('[data-role="availableBlocks"] ul');
managerList.sortable('refresh');
},
/**
* Cancel the sorting, reversing the changes the above method makes.
*
* @returns {void}
*/
_cancelDragging: function () {
this.scope.find('#elSidebarManager > div:first-child').css({
overflow: '',
position: '',
top: ''
});
},
/**
* Builds a selector list for selecting all widget areas
*
* @returns {void}
*/
_buildAreaSelector: function () {
var output = [];
for( var i = 0; i < this._areasInUse.length; i++ ){
output.push( "[data-widgetArea='" + this._areasInUse[ i ] + "'] > ul" );
}
return output.join(',');
},
/**
* Shows the manager panel, loading the contents remotely if needed
*
* @returns {void}
*/
_showManager: function () {
var self = this;
if( $('html').attr("dir") == "rtl"){
$('body').css({
marginRight: '+=300px'
});
}
else{
$('body').css({
marginLeft: '+=300px'
});
}
this.scope.find('[data-action="openSidebar"]').hide();
ips.utils.anim.go( 'fadeIn', this.scope.find('[data-role="manager"]') )
.done( function () {
// Fade in the submit button
$('#elSidebarManager_submit').hide().delay(700).fadeIn('fast');
});
// Fetch the sidebar list
if( !this._loadedManager && !this._loadingManager ){
ips.getAjax()( ips.getSetting('baseURL') + '?app=core&module=system&controller=widgets&do=getBlockList', {
data: {
pageApp: $('body').attr('data-pageApp')
}
} )
.done( function (response) {
self._loadedManager = true;
self.scope.find('[data-role="availableBlocks"]').html( response );
self._setUpDragging();
self._hideUsedBlocks();
// Content change again
$( document ).trigger('contentChange', [ $('#elSidebarManager') ] );
})
.fail( function () {
ips.ui.alert.show( {
type: 'alert',
icon: 'warn',
message: ips.getString('sidebar_fetch_blocks_error'),
callbacks: {}
});
})
.always( function () {
self.scope.find('[data-role="availableBlocks"]').removeClass('ipsLoading ipsLoading_dark');
self._loadingManager = false;
});
} else {
// If we've loaded the data already, we still need to call these
this._setUpDragging();
this._hideUsedBlocks();
}
this._inManagingState = true;
},
/**
* Hides the manager panel
*
* @returns {void}
*/
_hideManager: function () {
var self = this;
if ($('html').attr("dir") == "rtl"){
$('body').css({
marginLeft: '+=300px'
}, 'fast');
} else {
$('body').css({
marginLeft: '-=300px'
}, 'fast');
}
this.scope.find('[data-action="openSidebar"]').show();
this.scope.find('#elSidebarManager').hide();
this._inManagingState = false;
},
/**
* Hides blocks in the available list which have been used in the live list
*
* @returns {void}
*/
_hideUsedBlocks: function () {
var self = this;
var manager = this.scope.find('[data-role="availableBlocks"]');
// Show all blocks to start
manager.find('[data-blockID]').show().removeAttr('data-hidden');
if( this._blocksInUse.length ){
for( var i = 0; i < this._blocksInUse.length; i++ ){
var listedBlock = manager.find('[data-blockID="' + this._getBlockIDWithoutUniqueKey( self._blocksInUse[ i ] ) + '"]');
if ( ! listedBlock.attr('data-allowReuse') )
{
listedBlock.hide().attr('data-hidden', true);
}
}
}
},
/**
* Add the manager sidebar HTML to the page
*
* @param {event} e The original event
* @returns {void}
*/
_buildSidebar: function () {
this.scope.append( ips.templates.render('core.sidebar.managerWrapper') );
this.scope.find('[data-role="availableBlocks"]').css({
zIndex: ips.ui.zIndex()
});
$( document ).trigger( 'contentChange', [ this.scope ] );
},
/**
* Removes the unique key from the block ID
*
* @param {string} block Block ID with unique key (app_core_whosOnline_4vbvzbw)
* @returns {string}
*/
_getBlockIDWithoutUniqueKey: function (block) {
var bits = block.split('_');
return bits[0] + '_' + bits[1] + '_' + bits[2];
}
});
}(jQuery, _));]]> ul')
.css({
minHeight: height
});
},
/**
* Called when we've finished managing widgets
* Hides the sidebar completely if there's no widgets or contextual tools displaying
*
* @returns {void}
*/
managingFinished: function () {
this._addBodyClass();
this.scope
.find('[data-role="widgetReceiver"], [data-role="widgetReceiver"] > ul')
.css({
height: 'auto'
});
},
/**
* Adds a class to the body that indicates if the sidebar is shown
*
* @returns {void}
*/
_addBodyClass: function () {
if( !this.scope.find('[data-blockID]').not('.ipsHide').length && !$('#elContextualTools').length ){
$('body').addClass('ipsLayout_sidebarUnused').removeClass('ipsLayout_sidebarUsed');
} else {
$('body').addClass('ipsLayout_sidebarUsed').removeClass('ipsLayout_sidebarUnused');
}
}
});
}(jQuery, _));]]>-1)){var n="keypress"!==e.type&&t.hotkeys.specialKeys[e.which],i=String.fromCharCode(e.which).toLowerCase(),r="",o={};t.each(["alt","ctrl","shift"],function(t,a){e[a+"Key"]&&n!==a&&(r+=a+"+")}),e.metaKey&&!e.ctrlKey&&"meta"!==n&&(r+="meta+"),e.metaKey&&"meta"!==n&&r.indexOf("alt+ctrl+shift+")>-1&&(r=r.replace("alt+ctrl+shift+","hyper+")),n?o[r+n]=!0:(o[r+i]=!0,o[r+t.hotkeys.shiftNums[i]]=!0,"shift+"===r&&(o[t.hotkeys.shiftNums[i]]=!0));for(var p=0,l=s.length;l>p;p++)if(o[s[p]])return a.apply(this,arguments)}}}}t.hotkeys={version:"0.8",specialKeys:{8:"backspace",9:"tab",10:"return",13:"return",16:"shift",17:"ctrl",18:"alt",19:"pause",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"insert",46:"del",59:";",61:"=",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9",106:"*",107:"+",109:"-",110:".",111:"/",112:"f1",113:"f2",114:"f3",115:"f4",116:"f5",117:"f6",118:"f7",119:"f8",120:"f9",121:"f10",122:"f11",123:"f12",144:"numlock",145:"scroll",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},shiftNums:{"`":"~",1:"!",2:"@",3:"#",4:"$",5:"%",6:"^",7:"&",8:"*",9:"(",0:")","-":"_","=":"+",";":": ","'":'"',",":"<",".":">","/":"?","\\":"|"},textAcceptingInputTypes:["text","password","number","email","url","range","date","month","week","time","datetime","datetime-local","search","color","tel"],textInputTypes:/textarea|input|select/i,options:{filterInputAcceptingElements:!0,filterTextInputs:!0,filterContentEditable:!0}},t.each(["keydown","keyup","keypress"],function(){t.event.special[this]={add:e}})}(jQuery||this.jQuery||window.jQuery);]]>i;i++){var l=r.eq(i);if(t.append(l),n&&t.append(n),a(t,e)){l.remove(),o=!0;break}n&&n.detach()}return o}function r(e,n,i,d,l){var s=!1,c="table, thead, tbody, tfoot, tr, col, colgroup, object, embed, param, ol, ul, dl, blockquote, select, optgroup, option, textarea, script, style",u="script, .dotdotdot-keep";return e.contents().detach().each(function(){var f=this,h=t(f);if("undefined"==typeof f||3==f.nodeType&&0==t.trim(f.data).length)return!0;if(h.is(u))e.append(h);else{if(s)return!0;e.append(h),l&&e[e.is(c)?"after":"append"](l),a(i,d)&&(s=3==f.nodeType?o(h,n,i,d,l):r(h,n,i,d,l),s||(h.detach(),s=!0)),s||l&&l.detach()}}),s}function o(e,n,r,o,d){var c=e[0];if(!c)return!1;var f=s(c),h=-1!==f.indexOf(" ")?" ":" ",p="letter"==o.wrap?"":h,g=f.split(p),v=-1,w=-1,b=0,y=g.length-1;for(o.fallbackToLetter&&0==b&&0==y&&(p="",g=f.split(p),y=g.length-1);y>=b&&(0!=b||0!=y);){var m=Math.floor((b+y)/2);if(m==w)break;w=m,l(c,g.slice(0,w+1).join(p)+o.ellipsis),a(r,o)?(y=w,o.fallbackToLetter&&0==b&&0==y&&(p="",g=g[0].split(p),v=-1,w=-1,b=0,y=g.length-1)):(v=w,b=w)}if(-1==v||1==g.length&&0==g[0].length){var x=e.parent();e.detach();var T=d&&d.closest(x).length?d.length:0;x.contents().length>T?c=u(x.contents().eq(-1-T),n):(c=u(x,n,!0),T||x.detach()),c&&(f=i(s(c),o),l(c,f),T&&d&&t(c).parent().append(d))}else f=i(g.slice(0,v+1).join(p),o),l(c,f);return!0}function a(t,e){return t.innerHeight()>e.maxHeight}function i(e,n){for(;t.inArray(e.slice(-1),n.lastCharacter.remove)>-1;)e=e.slice(0,-1);return t.inArray(e.slice(-1),n.lastCharacter.noEllipsis)<0&&(e+=n.ellipsis),e}function d(t){return{width:t.innerWidth(),height:t.innerHeight()}}function l(t,e){t.innerText?t.innerText=e:t.nodeValue?t.nodeValue=e:t.textContent&&(t.textContent=e)}function s(t){return t.innerText?t.innerText:t.nodeValue?t.nodeValue:t.textContent?t.textContent:""}function c(t){do t=t.previousSibling;while(t&&1!==t.nodeType&&3!==t.nodeType);return t}function u(e,n,r){var o,a=e&&e[0];if(a){if(!r){if(3===a.nodeType)return a;if(t.trim(e.text()))return u(e.contents().last(),n)}for(o=c(a);!o;){if(e=e.parent(),e.is(n)||!e.length)return!1;o=c(e[0])}if(o)return u(t(o),n)}return!1}function f(e,n){return e?"string"==typeof e?(e=t(e,n),e.length?e:!1):e.jquery?e:!1:!1}function h(t){for(var e=t.innerHeight(),n=["paddingTop","paddingBottom"],r=0,o=n.length;o>r;r++){var a=parseInt(t.css(n[r]),10);isNaN(a)&&(a=0),e-=a}return e}if(!t.fn.dotdotdot){t.fn.dotdotdot=function(e){if(0==this.length)return t.fn.dotdotdot.debug('No element found for "'+this.selector+'".'),this;if(this.length>1)return this.each(function(){t(this).dotdotdot(e)});var o=this;o.data("dotdotdot")&&o.trigger("destroy.dot"),o.data("dotdotdot-style",o.attr("style")||""),o.css("word-wrap","break-word"),"nowrap"===o.css("white-space")&&o.css("white-space","normal"),o.bind_events=function(){return o.bind("update.dot",function(e,d){e.preventDefault(),e.stopPropagation(),l.maxHeight="number"==typeof l.height?l.height:h(o),l.maxHeight+=l.tolerance,"undefined"!=typeof d&&(("string"==typeof d||d instanceof HTMLElement)&&(d=t("").append(d).contents()),d instanceof t&&(i=d)),g=o.wrapInner('').children(),g.contents().detach().end().append(i.clone(!0)).find("br").replaceWith(" ").end().css({height:"auto",width:"auto",border:"none",padding:0,margin:0});var c=!1,u=!1;return s.afterElement&&(c=s.afterElement.clone(!0),c.show(),s.afterElement.detach()),a(g,l)&&(u="children"==l.wrap?n(g,l,c):r(g,o,g,l,c)),g.replaceWith(g.contents()),g=null,t.isFunction(l.callback)&&l.callback.call(o[0],u,i),s.isTruncated=u,u}).bind("isTruncated.dot",function(t,e){return t.preventDefault(),t.stopPropagation(),"function"==typeof e&&e.call(o[0],s.isTruncated),s.isTruncated}).bind("originalContent.dot",function(t,e){return t.preventDefault(),t.stopPropagation(),"function"==typeof e&&e.call(o[0],i),i}).bind("destroy.dot",function(t){t.preventDefault(),t.stopPropagation(),o.unwatch().unbind_events().contents().detach().end().append(i).attr("style",o.data("dotdotdot-style")||"").data("dotdotdot",!1)}),o},o.unbind_events=function(){return o.unbind(".dot"),o},o.watch=function(){if(o.unwatch(),"window"==l.watch){var e=t(window),n=e.width(),r=e.height();e.bind("resize.dot"+s.dotId,function(){n==e.width()&&r==e.height()&&l.windowResizeFix||(n=e.width(),r=e.height(),u&&clearInterval(u),u=setTimeout(function(){o.trigger("update.dot")},100))})}else c=d(o),u=setInterval(function(){if(o.is(":visible")){var t=d(o);(c.width!=t.width||c.height!=t.height)&&(o.trigger("update.dot"),c=t)}},500);return o},o.unwatch=function(){return t(window).unbind("resize.dot"+s.dotId),u&&clearInterval(u),o};var i=o.contents(),l=t.extend(!0,{},t.fn.dotdotdot.defaults,e),s={},c={},u=null,g=null;return l.lastCharacter.remove instanceof Array||(l.lastCharacter.remove=t.fn.dotdotdot.defaultArrays.lastCharacter.remove),l.lastCharacter.noEllipsis instanceof Array||(l.lastCharacter.noEllipsis=t.fn.dotdotdot.defaultArrays.lastCharacter.noEllipsis),s.afterElement=f(l.after,o),s.isTruncated=!1,s.dotId=p++,o.data("dotdotdot",!0).bind_events().trigger("update.dot"),l.watch&&o.watch(),o},t.fn.dotdotdot.defaults={ellipsis:"... ",wrap:"word",fallbackToLetter:!0,lastCharacter:{},tolerance:0,callback:null,after:null,height:null,watch:!1,windowResizeFix:!0},t.fn.dotdotdot.defaultArrays={lastCharacter:{remove:[" "," ",",",";",".","!","?"],noEllipsis:[]}},t.fn.dotdotdot.debug=function(){};var p=1,g=t.fn.html;t.fn.html=function(n){return n!=e&&!t.isFunction(n)&&this.data("dotdotdot")?this.trigger("update",[n]):g.apply(this,arguments)};var v=t.fn.text;t.fn.text=function(n){return n!=e&&!t.isFunction(n)&&this.data("dotdotdot")?(n=t("").text(n).html(),this.trigger("update",[n])):v.apply(this,arguments)}}}(jQuery);]]>")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window)]]>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;
return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="