/* ****************************** Semantic Module: Popup Author: Jack Lukic Notes: May 30, 2013 Simple plug-in to display popups ****************************** */ ;(function ($, window, document, undefined) { $.fn.popup = function(parameters) { var $allModules = $(this), settings = ( $.isPlainObject(parameters) ) ? $.extend(true, {}, $.fn.popup.settings, parameters) : $.fn.popup.settings, eventNamespace = '.' + settings.namespace, moduleNamespace = 'module-' + settings.namespace, moduleSelector = $allModules.selector || '', moduleCount = $allModules.size(), time = new Date().getTime(), performance = [], selector = settings.selector, className = settings.className, error = settings.error, metadata = settings.metadata, namespace = settings.namespace, query = arguments[0], methodInvoked = (typeof query == 'string'), queryArguments = [].slice.call(arguments, 1), invokedResponse ; $allModules .each(function() { var $module = $(this), $window = $(window), $offsetParent = $module.offsetParent(), $popup = (settings.inline) ? $module.next(selector.popup) : $window.children(selector.popup).last(), searchDepth = 0, element = this, instance = $module.data('module-' + settings.namespace), module ; module = { // binds events initialize: function() { module.debug('Initializing module', $module); if(settings.on == 'hover') { $module .on('mouseenter.' + namespace, module.event.mouseenter) .on('mouseleave.' + namespace, module.event.mouseleave) ; } else { $module .on(settings.on + '.' + namespace, module.event[settings.on]) ; } $window .on('resize.' + namespace, module.event.resize) ; module.instantiate(); }, instantiate: function() { module.verbose('Storing instance of module'); instance = module; $module .data('module-' + namespace, instance) ; }, 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) ; }, event: { mouseenter: function(event) { var element = this; module.timer = setTimeout(function() { $.proxy(module.toggle, element)(); if( $(element).hasClass(className.visible) ) { event.stopPropagation(); } }, settings.delay); }, mouseleave: function(event) { clearTimeout(module.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, variation = $module.data(metadata.variation) || settings.variation, 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) .addClass(variation) .html(html) ; if(settings.inline) { $popup .insertAfter($module) ; } else { $popup .appendTo( $('body') ) ; } $.proxy(settings.onInit, $popup)(); } else { module.error(error.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.width(), 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.verbose('Creating pop-up html'); 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.width(), 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(searchDepth < settings.maxSearchDepth) { position = module.get.nextPosition(position); searchDepth++; module.debug('Trying new position: ', position); return module.position(position); } else { module.error(error.recursion); searchDepth = 0; return false; } } else { module.debug('Position is on stage', position); searchDepth = 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.on == '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.on == '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(); } } }, setting: function(name, value) { if(value !== undefined) { if( $.isPlainObject(name) ) { $.extend(true, settings, name); } else { settings[name] = value; } } else { return settings[name]; } }, internal: function(name, value) { if(value !== undefined) { if( $.isPlainObject(name) ) { $.extend(true, module, name); } else { module[name] = value; } } else { return module[name]; } }, debug: function() { if(settings.debug) { if(settings.performance) { module.performance.log(arguments); } else { module.debug = Function.prototype.bind.call(console.info, console, settings.moduleName + ':'); } } }, verbose: function() { if(settings.verbose && settings.debug) { if(settings.performance) { module.performance.log(arguments); } else { module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':'); } } }, error: function() { console.log($module.next()); module.error = Function.prototype.bind.call(console.warn, console, settings.moduleName + ':'); }, performance: { log: function(message) { var currentTime, executionTime, previousTime ; if(settings.performance) { currentTime = new Date().getTime(); previousTime = time || currentTime, executionTime = currentTime - previousTime; time = currentTime; performance.push({ 'Element' : element, 'Name' : message[0], 'Arguments' : message[1] || '', 'Execution Time' : executionTime }); } clearTimeout(module.performance.timer); module.performance.timer = setTimeout(module.performance.display, 100); }, display: function() { var title = settings.moduleName + ':', totalTime = 0 ; time = false; $.each(performance, function(index, data) { totalTime += data['Execution Time']; }); title += ' ' + totalTime + 'ms'; if(moduleSelector) { title += ' \'' + moduleSelector + '\''; } if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { console.groupCollapsed(title); if(console.table) { console.table(performance); } else { $.each(performance, function(index, data) { console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); }); } console.groupEnd(); } performance = []; } }, invoke: function(query, passedArguments, context) { var maxDepth, found ; passedArguments = passedArguments || queryArguments; context = element || context; 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]; } else if( instance[value] !== undefined ) { found = instance[value]; } else { module.error(error.method); } }); } if ( $.isFunction( found ) ) { instance.verbose('Executing invoked function', found); return found.apply(context, passedArguments); } return found || false; } }; if(methodInvoked) { if(instance === undefined) { module.initialize(); } invokedResponse = module.invoke(query); } else { if(instance === undefined) { module.destroy(); } module.initialize(); } }) ; return (invokedResponse) ? invokedResponse : this ; }; $.fn.popup.settings = { moduleName : 'Popup', debug : false, verbose : false, performance : false, namespace : 'popup', onInit : function(){}, onShow : function(){}, onHide : function(){}, variation : '', content : false, html : false, title : false, on : 'hover', clicktoClose : true, position : 'top center', delay : 150, inline : true, duration : 250, easing : 'easeOutQuint', animation : 'pop', distanceAway : 0, arrowOffset : 0, maxSearchDepth : 10, error: { content : '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.' }, metadata: { arrowOffset : 'arrowOffset', content : 'content', html : 'html', position : 'position', title : 'title', variation : 'variation' }, 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 += '