/* ****************************** Tooltip / Popup Author: Jack Lukic Notes: First Commit Sep 07, 2012 ****************************** */ ;(function ($, window, document, undefined) { $.fn.popup = function(parameters) { var settings = $.extend(true, {}, $.fn.popup.settings, parameters), // make arguments available moduleArguments = arguments || false, invokedResponse ; $(this) .each(function() { var $module = $(this), $window = $(window), $offsetParent = $module.offsetParent(), $popup = (settings.inline) ? $module.next(settings.selector.popup) : $window.children(settings.selector.popup).last(), timer, recursionDepth = 0, instance = $module.data('module-' + settings.namespace), methodInvoked = (instance !== undefined && typeof parameters == 'string'), selector = settings.selector, className = settings.className, errors = settings.errors, metadata = settings.metadata, namespace = settings.namespace, module ; module = { // binds events initialize: function() { if(settings.event == 'hover') { $module .on('mouseenter.' + namespace, module.event.mouseenter) .on('mouseleave.' + namespace, module.event.mouseleave) ; } else { $module .on(settings.event + '.' + namespace, module.event[settings.event]) ; } $window .on('resize.' + namespace, module.event.resize) ; $module .data('module-' + namespace, module) ; }, refresh: function() { $popup = (settings.inline) ? $module.next(selector.popup) : $window.children(selector.popup).last() ; $offsetParent = $module.offsetParent(); }, destroy: function() { module.debug('Destroying existing popups'); $module .off('.' + namespace) ; $popup .remove() ; }, event: { mouseenter: function(event) { var element = this; timer = setTimeout(function() { $.proxy(module.toggle, element)(); if( $(element).hasClass(className.visible) ) { event.stopPropagation(); } }, settings.delay); }, mouseleave: function(event) { clearTimeout(timer); if( $module.is(':visible') ) { module.hide(); } }, click: function(event) { $.proxy(module.toggle, this)(); if( $(this).hasClass(className.visible) ) { event.stopPropagation(); } }, resize: function() { if( $popup.is(':visible') ) { module.position(); } } }, // generates popup html from metadata create: function() { module.debug('Creating pop-up content'); var html = $module.data(metadata.html) || settings.html, title = $module.data(metadata.title) || settings.title, content = $module.data(metadata.content) || $module.attr('title') || settings.content ; if(html || content || title) { if(!html) { html = settings.template({ title : title, content : content }); } $popup = $('
') .addClass(className.popup) .html(html) ; if(settings.inline) { $popup .insertAfter($module) ; } else { $popup .appendTo( $('body') ) ; } } else { module.error(errors.content); } }, remove: function() { $popup .remove() ; }, get: { offstagePosition: function() { var boundary = { top : $(window).scrollTop(), bottom : $(window).scrollTop() + $(window).height(), left : 0, right : $(window).width() }, popup = { width : $popup.outerWidth(), height : $popup.outerHeight(), position : $popup.offset() }, offstage = {}, offstagePositions = [] ; if(popup.position) { offstage = { top : (popup.position.top < boundary.top), bottom : (popup.position.top + popup.height > boundary.bottom), right : (popup.position.left + popup.width > boundary.right), left : (popup.position.left < boundary.left) }; } // return only boundaries that have been surpassed $.each(offstage, function(direction, isOffstage) { if(isOffstage) { offstagePositions.push(direction); } }); return (offstagePositions.length > 0) ? offstagePositions.join(' ') : false ; }, nextPosition: function(position) { switch(position) { case 'top left': position = 'bottom left'; break; case 'bottom left': position = 'top right'; break; case 'top right': position = 'bottom right'; break; case 'bottom right': position = 'top center'; break; case 'top center': position = 'bottom center'; break; case 'bottom center': position = 'right center'; break; case 'right center': position = 'left center'; break; case 'left center': position = 'top center'; break; } return position; } }, // determines popup state toggle: function() { $module = $(this); module.debug('Toggling pop-up'); // refresh state of module module.refresh(); if($popup.size() === 0) { module.create(); } if( !$module.hasClass(className.visible) ) { if( module.position() ) { module.show(); } } else { module.hide(); } }, position: function(position, arrowOffset) { var windowWidth = $(window).width(), windowHeight = $(window).height(), width = $module.outerWidth(), height = $module.outerHeight(), popupWidth = $popup.outerWidth(), popupHeight = $popup.outerHeight(), offset = (settings.inline) ? $module.position() : $module.offset(), parentWidth = (settings.inline) ? $offsetParent.outerWidth() : $window.outerWidth(), parentHeight = (settings.inline) ? $offsetParent.outerHeight() : $window.outerHeight(), positioning, offstagePosition ; position = position || $module.data(metadata.position) || settings.position; arrowOffset = arrowOffset || $module.data(metadata.arrowOffset) || settings.arrowOffset; module.debug('Calculating offset for position', position); switch(position) { case 'top left': positioning = { top : 'auto', bottom : parentHeight - offset.top + settings.distanceAway, left : offset.left + arrowOffset }; break; case 'top center': positioning = { bottom : parentHeight - offset.top + settings.distanceAway, left : offset.left + (width / 2) - (popupWidth / 2) + arrowOffset, top : 'auto', right : 'auto' }; break; case 'top right': positioning = { bottom : parentHeight - offset.top + settings.distanceAway, right : parentWidth - offset.left - width - arrowOffset, top : 'auto', left : 'auto' }; break; case 'left center': positioning = { top : offset.top + (height / 2) - (popupHeight / 2), right : parentWidth - offset.left + settings.distanceAway - arrowOffset, left : 'auto', bottom : 'auto' }; break; case 'right center': positioning = { top : offset.top + (height / 2) - (popupHeight / 2), left : offset.left + width + settings.distanceAway + arrowOffset, bottom : 'auto', right : 'auto' }; break; case 'bottom left': positioning = { top : offset.top + height + settings.distanceAway, left : offset.left + arrowOffset, bottom : 'auto', right : 'auto' }; break; case 'bottom center': positioning = { top : offset.top + height + settings.distanceAway, left : offset.left + (width / 2) - (popupWidth / 2) + arrowOffset, bottom : 'auto', right : 'auto' }; break; case 'bottom right': positioning = { top : offset.top + height + settings.distanceAway, right : parentWidth - offset.left - width - arrowOffset, left : 'auto', bottom : 'auto' }; break; } // true width on popup, avoid rounding error $.extend(positioning, { width: $popup.width() + 1 }); // tentatively place on stage $popup .removeAttr('style') .removeClass('top right bottom left center') .css(positioning) .addClass(position) .addClass(className.loading) ; // check if is offstage offstagePosition = module.get.offstagePosition(); // recursively find new positioning if(offstagePosition) { module.debug('Element is outside boundaries ', offstagePosition); if(recursionDepth < settings.maxRecursion) { position = module.get.nextPosition(position); recursionDepth++; module.debug('Trying new position: ', position); return module.position(position); } else { module.error(errors.recursion); recursionDepth = 0; return false; } } else { module.debug('Position is on stage', position); recursionDepth = 0; return true; } }, show: function() { module.debug('Showing pop-up'); $(selector.popup) .filter(':visible') .stop() .fadeOut(200) .prev($module) .removeClass(className.visible) ; $module .addClass(className.visible) ; $popup .removeClass(className.loading) ; if(settings.animation == 'pop' && $.fn.popIn !== undefined) { $popup .stop() .popIn(settings.duration, settings.easing) ; } else { $popup .stop() .fadeIn(settings.duration, settings.easing) ; } if(settings.event == 'click' && settings.clicktoClose) { module.debug('Binding popup close event'); $(document) .on('click.' + namespace, module.gracefully.hide) ; } $.proxy(settings.onShow, $popup)(); }, hide: function() { $module .removeClass(className.visible) ; if($popup.is(':visible') ) { module.debug('Hiding pop-up'); if(settings.animation == 'pop' && $.fn.popOut !== undefined) { $popup .stop() .popOut(settings.duration, settings.easing, function() { $popup.hide(); }) ; } else { $popup .stop() .fadeOut(settings.duration, settings.easing) ; } } if(settings.event == 'click' && settings.clicktoClose) { $(document) .off('click.' + namespace) ; } $.proxy(settings.onHide, $popup)(); if(!settings.inline) { module.remove(); } }, gracefully: { hide: function(event) { // don't close on clicks inside popup if( $(event.target).closest(selector.popup).size() === 0) { module.hide(); } } }, /* standard module */ setting: function(name, value) { if(value === undefined) { return settings[name]; } settings[name] = value; }, debug: function() { var output = [], message = settings.moduleName + ': ' + arguments[0], variables = [].slice.call( arguments, 1 ), log = console.info || console.log || function(){} ; if(settings.debug) { output.push(message); log.apply(console, output.concat(variables) ); } }, error: function() { var output = [], errorMessage = settings.moduleName + ': ' + arguments[0], variables = [].slice.call( arguments, 1 ), log = console.warn || console.log || function(){} ; if(settings.debug) { output.push(errorMessage); output.concat(variables); log.apply(console, output.concat(variables) ); } }, invoke: function(query, context, passedArguments) { var maxDepth, found ; passedArguments = passedArguments || [].slice.call( arguments, 2 ); if(typeof query == 'string' && instance !== undefined) { query = query.split('.'); maxDepth = query.length - 1; $.each(query, function(depth, value) { if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) { instance = instance[value]; return true; } else if( instance[value] !== undefined ) { found = instance[value]; return true; } module.error(settings.errors.method); return false; }); } if ( $.isFunction( found ) ) { return found.apply(context, passedArguments); } // return retrieved variable or chain return found; } }; // check for invoking internal method if(methodInvoked) { invokedResponse = module.invoke(moduleArguments[0], this, Array.prototype.slice.call( moduleArguments, 1 ) ); } // otherwise initialize else { if(instance) { module.destroy(); } module.initialize(); } }) ; // chain or return queried method return (invokedResponse !== undefined) ? invokedResponse : this ; }; $.fn.popup.settings = { moduleName : 'Pop-up Module', debug : true, namespace : 'popup', onShow : function(){}, onHide : function(){}, content : false, html : false, title : false, position : 'top center', delay : 0, inline : true, duration : 250, easing : 'easeOutQuint', animation : 'pop', errors: { content : 'Warning: Your popup has no content specified', method : 'The method you called is not defined.', recursion : 'Popup attempted to reposition element to fit, but could not find an adequate position.' }, distanceAway : 2, arrowOffset : 0, maxRecursion : 10, event : 'hover', clicktoClose : true, metadata: { content : 'content', html : 'html', title : 'title', position : 'position', arrowOffset : 'arrowOffset' }, className : { popup : 'ui popup', visible : 'visible', loading : 'loading' }, selector : { popup : '.ui.popup' }, template: function(text) { var html = ''; if(typeof text !== undefined) { if(typeof text.title !== undefined && text.title) { html += '

' + text.title + '

'; } if(typeof text.content !== undefined && text.content) { html += '
' + text.content + '
'; } } return html; } }; })( jQuery, window , document );