* {
* controllerOne: [ mixin1, mixin2 ],
* controllerTwo: [],
* controllerThree: []
* }
*
*
* @returns {string}
*/
_getControllersAndMixins = function (controllerString) {
var controllers = {};
var pieces = controllerString.split(',');
for( var i = 0; i < pieces.length; i++ ){
pieces[i] = $.trim( pieces[i] );
// Fix case issues on user-submitted content
if( !_.isUndefined( _controllerCaseMap[ pieces[i] ] ) ){
pieces[i] = _controllerCaseMap[ pieces[i] ];
}
if( pieces[i].indexOf('(') === -1 ){
controllers[ pieces[i] ] = [];
continue;
}
var p = pieces[i].match( /([a-zA-Z0-9.]+)\((.+?)\)/i );
var mixinPieces = [];
_.each( p[2].split(';'), function (val) {
mixinPieces.push( $.trim( val ) );
});
controllers[ p[1] ] = mixinPieces;
}
return controllers;
},
/**
* Returns an incremental controller ID
* Controller IDs are used to enable controllers to identify events that they
* emitted themselves.
*
* @returns {string}
*/
getInstanceID = function () {
return 'ipscontroller' + (++instanceID);
},
/**
* Allows an element to be cleaned externally. If the element passed is not a controller scope,
* it'll search down one level of the DOM to find controllers.
*
* @param {element} elem The element to clean
* @returns {string}
*/
cleanContentsOf = function (elem) {
Debug.log('Cleaning contents of controller');
$( elem ).find('[data-controller]')
.each( function () {
var loopController = $( this );
var controllers = loopController.data( '_controllerObjs' ) || [];
if( controllers.length ){
loopController.data('_controllerObjs', []);
for( var i = 0; i < controllers.length; i++ ){
controllers[i]._destroy.apply( controllers[i] );
delete controllers[i];
}
}
});
// Remove any widgets that exist in this elem
ips.ui.destructAllWidgets( $( elem ) );
},
/**
* Initializes a controller instance by creating a new function, extending it with
* the controller methods then initializing it on the relevant dom node
*
* @param {element} elem The element that will form the scope of this controller
* @param {string} controllerID ID of this controller
* @returns {void}
*/
initControllerOnElem = function (elem, controllerID, mixins) {
if( !_controllers[ controllerID ] ){
Debug.error("Controller '" + controllerID + "' has not been registered");
return;
}
if( _.isUndefined( $( elem ).data('_controllers') )){
$( elem ).data('_controllers', []);
}
$( elem ).data('_controllers').push( controllerID );
if( _.isUndefined( _prototypes[ controllerID ] ) ){
// Fetch our controller prototype
_prototypes[ controllerID ] = getBaseController();
// Extend with our specific controller methods
$.extend( true, _prototypes[ controllerID ].prototype, _controllers[ controllerID ] );
}
// And init
if( _.isUndefined( $( elem ).data( '_controllerObjs' ) ) ){
$( elem ).data( '_controllerObjs', [] );
}
var controllers = $( elem ).data( '_controllerObjs' );
var obj = new _prototypes[ controllerID ](elem, controllerID);
controllers.push( obj );
// Any mixins?
// Auto mixins first
if( !_.isUndefined( _autoMixins[ controllerID ] ) && _.size( _autoMixins[ controllerID ] ) ){
_.each( _autoMixins[ controllerID ], function (val, key) {
_autoMixins[ controllerID ][ key ].call( obj );
});
}
// Then the manually-specified ones
if( mixins.length ){
for( var i = 0; i < mixins.length; i++ ){
if( !_.isUndefined( _manualMixins[ controllerID ] ) && !_.isUndefined( _manualMixins[ controllerID ][ mixins[i] ] ) ){
_manualMixins[ controllerID ][ mixins[i] ].call( obj );
}
}
}
if( _.isFunction( obj.initialize ) ){
obj.initialize.call( obj );
}
$( elem ).removeData( '_controller' + controllerID );
$( document ).trigger( 'controllerReady', {
controllerID: obj.controllerID,
controllerType: obj.controllerType,
controllerElem: elem
});
},
/**
* Finds controllers within a node that have an ID matching the provided name
* Wildcard character * supported at the front or end of the controller parameter
*
* @param {string} controller Controller name to find
* @param {element} node Optional node to search in (document by default)
* @returns {function}
*/
_findSubControllers = function (controller, node) {
var results = [];
node = ( node && ( _.isElement( node ) || node.jquery ) ) ? node : document;
if( controller.indexOf('*') === -1 ){
results = $( node ).find('[data-controller*="' + controller + '"]');
} else {
var pieces = controller.split('.');
if( pieces[0] == '*' ){
pieces.shift();
results = $( node ).find('[data-controller$="' + pieces.join('.') + '"]');
} else if( pieces[ pieces.length - 1 ] == '*' ){
pieces.pop();
results = $( node ).find('[data-controller^="' + pieces.join('.') + '"]');
}
}
return results;
},
/**
* Returns a new function that will form our controller prototype
*
* @returns {function}
*/
getBaseController = function () {
/** Base controller definition */
var baseController = function (scope, type) {
this.controllerType = type;
this.controllerID = getInstanceID();
this.scope = $( scope );
this._eventListeners = [];
var self = this;
// Advice methods - inspired by http://javascriptweblog.wordpress.com/2011/05/31/a-fresh-look-at-javascript-mixins/
// and Twitter Flight
var adviceFuncs = {
before: function (baseFn, newFn) {
return function () {
newFn.apply( this, arguments );
return baseFn.apply( this, arguments );
};
},
after: function (baseFn, newFn) {
return function () {
var toReturn = baseFn.apply( this, arguments );
newFn.apply( this, arguments );
return toReturn;
}
},
around: function (baseFn, newFn) {
return function () {
var args = ips.utils.argsToArray( arguments );
args.unshift( baseFn.bind( this ) );
return newFn.apply( this, args );
}
}
}
_.each( ['before', 'after', 'around'], _.bind( function (type) {
this[ type ] = function (base, fn) {
if( _.isUndefined( this[ base ] ) || !_.isFunction( this[ base ] ) ){
Debug.log( "Method '" + base + '" is not present in controller ' + this.controllerID );
return;
}
// Replace our base method with a wrapped version
this[ base ] = adviceFuncs[ type ]( this[ base ], fn );
};
}, this ) );
//Debug.info("Initialized " + this.controllerID + " of type " + this.controllerType);
};
baseController.method('_destroy', function () {
Debug.log( 'Destroyed instance ' + this.controllerID + ' of ' + this.controllerType );
// Remove each event listener that was created in this controller
if( this._eventListeners.length ){
for( var i = 0; i < this._eventListeners.length; i++ ){
var data = this._eventListeners[i];
if( data['delegate'] ){
data['elem'].off( data['ev'], data['delegate'], data['fn'] );
} else {
data['elem'].off( data['ev'], data['fn'] );
}
}
}
if( _.isFunction( this.destroy ) ){
this.destroy.call( this );
}
// Remove reference to scope so that GC can do its thing
this.scope = null;
});
// Searches for controllers within the current, triggers a destroy event and deletes the controller objs
baseController.method('cleanContents', function () {
Debug.log('Cleaning contents of controller');
this.scope.find('[data-controller]')
.each( function () {
var loopController = $( this );
var controllers = loopController.data( '_controllerObjs' ) || [];
if( controllers.length ){
loopController.data('_controllerObjs', []);
for( var i = 0; i < controllers.length; i++ ){
controllers[i]._destroy.apply( controllers[i] );
delete controllers[i];
}
}
});
// Remove any widgets that exist in this elem
ips.ui.destructAllWidgets( this.scope );
});
baseController.method('trigger', function (elem, ev, data) {
// Convert silly arguments object to an array
var args = ips.utils.argsToArray( arguments );
elem = ( !_.isElement( elem ) && !elem.jquery ) ? this.scope : $( args.shift() );
ev = args[0];
data = args[1] || {};
// Add our origin to the event
if( !data.stack ){
data.stack = [];
}
data.stack.push( 'controllers.' + this.controllerType + '.' + this.controllerID );
elem.trigger( ev, data );
});
baseController.method('on', function (elem, ev, delegate, fn) {
// Convert silly arguments object to an array
var args = ips.utils.argsToArray( arguments );
// Reconfigure our args as necessary
elem = ( !_.isElement( elem ) && elem != document && elem != window ) ? this.scope : $( args.shift() );
ev = args[0];
fn = ( args.length == 3 ) ? args[2] : args[1];
delegate = ( args.length == 3 ) ? args[1] : undefined;
if( !_.isFunction( fn ) ){
Debug.warn("Callback function for " + ev + " doesn't exist in " + this.controllerType
+ " (" + this.controllerID + ")");
return;
}
// Bind our callback to the controller
fn = _.bind( fn, this );
// Set up the event
if( delegate ){
elem.on( ev, delegate, fn );
this._eventListeners.push({
elem: elem,
event: ev,
delegate: delegate,
fn: fn
});
} else {
elem.on( ev, fn );
this._eventListeners.push({
elem: elem,
event: ev,
fn: fn
});
}
});
baseController.method('triggerOn', function (controller, ev, data) {
var toTrigger = _findSubControllers( controller, this.scope );
if( !toTrigger.length ){
return;
}
data = data || {};
// Add our origin to the event
if( !data.stack ){
data.stack = [];
}
data.stack.push( 'controllers.' + this.controllerType + '.' + this.controllerID );
toTrigger.trigger( ev, data );
});
return baseController;
};
return {
initControllerOnElem: initControllerOnElem,
register: register,
mixin: mixin,
isRegistered: isRegistered,
init: init,
cleanContentsOf: cleanContentsOf
};
});
}( jQuery, _ ));]]>' + selectedText + '
'; } if( this._selectedText == selectedText ){ return; } this._selectedText = selectedText; this._showQuoteTooltip(); }, /** * Builds & shows the selective quoting tooltip, displaying it just above the user's cursor * * @returns {void} */ _showQuoteTooltip: function () { var selection = ips.utils.selection.getSelection(); var range = ips.utils.selection.getRange( this.scope.find('[data-role="commentContent"]') ); var tooltip = this.scope.find('[data-role="inlineQuoteTooltip"]'); var scopeOffset = this.scope.offset(); var position = { left: 0, top: 0 }; if( range === false || !_.isObject( range ) || _.isUndefined( range.type ) ){ // No selection found Debug.log("No selection found"); return; } // Create the new tooltip if needed if( !tooltip.length ){ this.scope.append( ips.templates.render('core.selection.quote', { direction: 'bottom'//ips.utils.events.isTouchDevice() ? 'bottom' : 'top' }) ); tooltip = this.scope.find('[data-role="inlineQuoteTooltip"]'); $( document ).on( 'click dblclick', this._clickHandler ); } if ( range.type === 'outside' ){ // Selection was beyond our content area, so we need to limit it to end of the content // Get the bounding of the selection to use as the basis of our tooltip position var boundingBox = range.range.getBoundingClientRect(); var offset = this.scope.offset(); position.left = boundingBox.left + ( boundingBox.width / 2 ) + $( window ).scrollLeft() - offset.left; position.top = boundingBox.top + boundingBox.height + $( window ).scrollTop() - offset.top; } else { // Normal selection, inside content, so we can position based on the Range directly // Clone the range, and insert an element containing an invisible character which we'll use // to fetch the position var cloneRange = range.range.cloneRange(); var invisibleElement = document.createElement('span'); invisibleElement.appendChild( document.createTextNode('\ufeff') ); // Collapse the range so that we only care about the end position cloneRange.collapse( false ); // Insert our invisible element into the range cloneRange.insertNode( invisibleElement ); // Get the position of the invisible element we created var tmpPosition = ips.utils.position.getElemPosition( $( invisibleElement ) ); position.left = tmpPosition.absPos.left - scopeOffset.left; position.top = tmpPosition.absPos.top - scopeOffset.top + 25; } var tooltipSize = { width: tooltip.show().outerWidth(), height: tooltip.show().outerHeight() }; // Set the position. On touch devices, move it a little further to the left to avoid the OS's touch handle var leftAdjustment = ips.utils.events.isTouchDevice() ? tooltipSize.width : ( tooltipSize.width / 2 ); tooltip.css({ position: 'absolute', left: Math.round( position.left - leftAdjustment ) + 'px', top: Math.round( position.top ) + 'px', zIndex: ips.ui.zIndex() }); // If the tooltip isn't already shown, fade it in if( !tooltip.is(':visible') ){ tooltip.hide().fadeIn('fast'); } else { tooltip.show(); } }, /** * Hide the selective quote tooltip * * @returns {void} */ _hideQuoteTooltip: function () { $( document ).off( 'click dblclick', this._clickHandler ); clearInterval( this._quoteTimeout ); this.scope.find('[data-role="inlineQuoteTooltip"]').fadeOut('fast'); this._selectedText = ''; }, /** * Event handler for clicking 'quote this' in the selective quote tooltip * Triggers an event sent to the comment feed * * @param {event} e Event object * @returns {void} */ quoteSelection: function (e) { e.preventDefault(); this._getQuoteData(); if( this._selectedText ){ this.trigger('quoteComment.comment', { userid: this._quoteData.userid, username: this._quoteData.username, timestamp: this._quoteData.timestamp, contentapp: this._quoteData.contentapp, contenttype: this._quoteData.contenttype, contentclass: this._quoteData.contentclass, contentid: this._quoteData.contentid, contentcommentid: this._quoteData.contentcommentid, quoteHtml: this._selectedText }); } this._hideQuoteTooltip(); }, /** * Triggered when the moderation checkbox is changed * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ commentCheckbox: function (e) { var checked = $( e.currentTarget ).is(':checked'); this.scope.closest('.ipsComment').toggleClass( 'ipsComment_selected', checked ); this.trigger('checkedComment.comment', { commentID: this._commentID, actions: $( e.currentTarget ).attr('data-actions'), checked: checked }); }, /** * The comment feed has told us we can't support quoting * * @returns {void} */ disableQuoting: function () { this._quotingDisabled = true; this.scope.find('[data-ipsQuote-editor]').remove(); }, /** * Event handler for the Helpful/Unhelpful buttons. * * @param {event} e Event object * @returns {void} */ rateReview: function (e) { e.preventDefault(); var self = this; ips.getAjax()( $( e.currentTarget ).attr('href') ) .done( function (response) { var content = $("(
|<\/p>|\s)*<\/p>$/)) { ips.ui.alert.show({ type: 'alert', icon: 'warn', message: ips.getString('cantEmptyEdit'), subText: ips.getString('cantEmptyEditDesc') }); return; } } var form = this.scope.find('form'); var url = form.attr('action'); var data = form.serialize(); form.find('[data-action="cancelEditComment"]').remove(); form.find('[type="submit"]').prop( 'disabled', true ).text( ips.getString('saving') ); this.trigger( 'saveEditComment.comment', { form: data, url: url, commentID: this._commentID }); }, /** * Model event: something is loading * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ commentLoading: function (e, data) { if( data.commentID != this._commentID ){ return; } var commentLoading = this.scope.find('[data-role="commentLoading"]'); commentLoading .removeClass('ipsHide') .find('.ipsLoading') .removeClass('ipsLoading_noAnim'); ips.utils.anim.go( 'fadeIn', commentLoading ); }, /** * Model event: something is done loading * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ commentDone: function (e, data) { if( data.commentID != this._commentID ){ return; } this.scope .find('[data-role="commentLoading"]') .addClass('ipsHide') .find('.ipsLoading') .addClass('ipsLoading_noAnim'); }, /** * Model event: edit form has loaded * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ getEditFormDone: function (e, data) { if( data.commentID != this._commentID ){ return; } var self = this; var showForm = _.once( function () { self._isEditing = true; self.scope.find('[data-action="expandTruncate"], [data-role="commentControls"]').hide(); self.scope.find('[data-role="commentContent"]').html( data.response ); $( document ).trigger( 'contentChange', [ self.scope.find('[data-role="commentContent"]') ] ); }); // Scroll to the comment var elemPosition = ips.utils.position.getElemPosition( this.scope ); var windowScroll = $( window ).scrollTop(); var viewHeight = $( window ).height(); // Only scroll if it isn't already on the screen if( elemPosition.absPos.top < windowScroll || elemPosition.absPos.top > ( windowScroll + viewHeight ) ){ $('html, body').animate( { scrollTop: elemPosition.absPos.top + 'px' }, function () { showForm(); }); } else { showForm(); } }, /** * Model event: error loading edit form * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ getEditFormError: function (e, data) { if( data.commentID != this._commentID ){ return; } window.location = data.url; }, /** * Model event: saving an edit is finished * * @param {event} e Event object * @param {object} data Event data object * @returns {void} */ saveEditCommentDone: function (e, data) { if( data.commentID != this._commentID ){ return; } ips.ui.editor.destruct( this.scope.find('[data-ipseditor]') ); this._isEditing = false; this.scope.find('[data-role="commentContent"]').replaceWith( $('
' + $( 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; } }); }(jQuery, _)); ]]>
* {
* 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, _));]]>