/*! * * Homebrewed plugin functions! * * Basically it's a bunch of common functions used in the websites we * build. They're built in the format of jQuery plugins for reusability. * * You may remove those that you don't need. * * - HC * * @TODO: Make these more extensible for easier future usage. */ var homebrew = {}; (function($) { /* Avoid `console` errors in browsers that lack a console. */ var method, noop = function () {}, methods = [ 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeStamp', 'trace', 'warn' ], length = methods.length, console = (window.console = window.console || {}); while (length--) { method = methods[length]; // Only stub undefined methods. if (!console[method]) { console[method] = noop; } } /* Setup homebrew object */ var root = $('html'); $.extend(homebrew, { browser : { ie : root.hasClass('ie'), ie9 : root.hasClass('ie9'), lt9 : root.hasClass('lt9'), ie8 : root.hasClass('ie8'), lt8 : root.hasClass('lt8'), ie7 : root.hasClass('ie7'), firefox : (window.mozIndexedDB !== undefined) }, events : { transitionEnd : 'oTransitionEnd otransitionend webkitTransitionEnd transitionend' }, classes : { transitionable: 'is-transitionable' }, screenSize : { small : false, medium : false, large : true }, mediaQueries : { small : 'only screen and (max-width: 40em)', medium : 'only screen and (min-width: 40.063em)', large : 'only screen and (min-width: 64.063em)' }, mediaQueriesIE9 : { small : { size : 640, method : 'max-width' }, medium : { size : 641, method : 'min-width' }, large : { size : 1025, method : 'min-width' } } }); $.extend(homebrew, { utils : { /* * Executes a function a max of once every n milliseconds * * Arguments: * Func (Function): Function to be throttled. * * Delay (Integer): Function execution threshold in milliseconds. * * Returns: * Lazy_function (Function): Function with throttling applied. */ throttle : function(func, delay) { var timer = null; return function () { var context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { func.apply(context, args); }, delay); }; }, /* End throttle */ /* * Executes a function when it stops being invoked for n seconds * Modified version of _.debounce() http://underscorejs.org * * Arguments: * Func (Function): Function to be debounced. * * Delay (Integer): Function execution threshold in milliseconds. * * Immediate (Bool): Whether the function should be called at the beginning * of the delay instead of the end. Default is false. * * Returns: * Lazy_function (Function): Function with debouncing applied. */ debounce : function(func, delay, immediate) { var timeout, result; return function() { var context = this, args = arguments, later = function() { timeout = null; if (!immediate) result = func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, delay); if (callNow) result = func.apply(context, args); return result; }; } /* End debounce */ }, /* End utils */ makePlugin : function(plugin) { var pluginName = plugin.prototype.name; $.fn[pluginName] = function(options) { var args = $.makeArray(arguments), after = args.slice(1); return this.each(function() { var instance = $.data(this, pluginName); if(instance) { if(typeof options === 'string') { instance[options].apply(instance, after); } else if(instance.update) { instance.update.apply(instance, args); } } else { new plugin(this, options); } }); }; }, /* End makePlugin */ generateUniqueID : function() { return String(new Date().getTime()) + String(Math.round(Math.random()*100000, 10)); }, getSelectorsFromHTMLString : function(str) { if(typeof str !== 'string') { console.error('homebrew.getSelectorsFromHTMLString(): Expecting a string as the first argument. Please check:'); console.log(str); return {}; } var tagMatch = str.match(/<(.*?) /g), attributesMatch = str.match(/ (.*?)=("|')(.*?)("|')/g), selectorsObj = {}; if(tagMatch) { selectorsObj.tag = tagMatch[0].replace(/(<| )/g, ''); } if(attributesMatch) { var attributeSplitArray, classesArray; while(attributesMatch.length) { attributeSplitArray = $.trim(attributesMatch.shift()).split('='); switch(attributeSplitArray[0]) { case 'class' : classesArray = attributeSplitArray[1].replace(/("|')/g, '').split(' '); for(var i = classesArray.length-1; i > -1; i--) { if(classesArray[i] === '') { classesArray.splice(i, 1); } } selectorsObj.classes = classesArray; break; default : selectorsObj[attributeSplitArray[0]] = attributeSplitArray[1].replace(/("|')/g, ''); break; } } } return selectorsObj; }, /* End getSelectorsFromHTMLString */ getClassFromHTMLString : function(str) { var selectorsObj = this.getSelectorsFromHTMLString(str); if(selectorsObj.classes && selectorsObj.classes.length) { return selectorsObj.classes; } else { return []; } }, getKeyValuePairsFromString : function(str, pairSeparator, keyValueSeparator) { pairSeparator = pairSeparator || ';'; keyValueSeparator = keyValueSeparator || ':'; var splitArray = str.split(pairSeparator), keyValuePairs = {}, currentPair; while(splitArray.length) { currentPair = splitArray.shift(); if(!currentPair) continue; currentPair = currentPair.split(keyValueSeparator); currentPair = currentPair.map(function(str) { return $.trim(str); }); if(currentPair[1] === 'true') { currentPair[1] = true; } else if(currentPair[1] === 'false') { currentPair[1] = false; } keyValuePairs[currentPair[0]] = currentPair[1]; } return keyValuePairs; }, watchSize : function(mediaQuery, callback) { var self = this, _mediaQuery = self.mediaQueries[mediaQuery]; if(typeof _mediaQuery === 'undefined') { throw new Error('homebrew.watchSize(): No match media query. mediaQuery provided is ' + mediaQuery); } /* For modern browsers, use native matchMedia.addListener */ if(typeof matchMedia === 'function' && matchMedia.addListener) { var matchMediaObj = matchMedia(_mediaQuery); callback(matchMediaObj.matches); matchMediaObj.addListener(function(mq) { callback(mq.matches); }); /* For IE9, simulate the matchMedia.addListener behaviour using * a resize handler. */ } else if(!self.browser.lt9) { var mediaQueryProps = self.mediaQueriesIE9[mediaQuery]; if(typeof mediaQueryProps === 'undefined') return; var currentScreen, getCurrentScreen; if(mediaQueryProps.method === 'min-width') { getCurrentScreen = function() { return ($(window).width() >= mediaQueryProps.size); }; } else if(mediaQueryProps.method === 'max-width') { getCurrentScreen = function() { return ($(window).width() <= mediaQueryProps.size); }; } currentScreen = getCurrentScreen(); callback(currentScreen); $(window).on('resize.watchMedia', self.utils.throttle(function() { if(currentScreen !== getCurrentScreen()) { currentScreen = !currentScreen; callback(currentScreen); } }, 100)); /* For legacy browsers, only run the functions for medium * screens and above. */ } else { if(mediaQuery === 'small' || mediaQuery === 'xsmall') { callback(false); } else { callback(true); } } } /* End watchSize */ }); homebrew.watchSize('small', function(isMediumScreen) { homebrew.screenSize.small = isMediumScreen; homebrew.screenSize.medium = !isMediumScreen; }); homebrew.watchSize('large', function(isLargeScreen) { homebrew.screenSize.medium = !isLargeScreen; homebrew.screenSize.large = isLargeScreen; }); /**---- Carouselify ---**\ * Turn a list of elements into a rotating carousel. * * Arguments: * $('.carousel').carouselify({ * items : '.carousel-item', * activeItem : null, * classes : { * active : 'is-active', * hidden : 'is-hidden' * }, * loop : true, * transitions : { * enable : true, * classes : { * transitionIn : 'is-transitioning-in', * transitionOut : 'is-transitioning-out', * reverse : 'is-reverse' * }, * onStart : null, * onEnd : null * }, * switchers : { * enable : true, * markups : { * nextSwitcher : '
', * prevSwitcher : '' * } * }, * pagers : { * enable : true, * markup : '', * holder : '' * }, * timer : { * enable : true, * duration : 10000, * showBar : true, * barMarkup : '' * }, * onSwitch : null * }); * * - items * |-- Type: String * |-- Default: '.carousel-item' * |-- Pass in the selector of the carousel items. * - activeItem * |-- Type: Number * |-- Default: null * |-- Pass in the index of the carousel item that should be active * when the plugin is initialised. * - classes * |-- Type: Object * |-- Pass in an object containing strings of the classes to use. * Properties in this object: * - active * |-- Type: String * |-- Default: 'is-active' * |-- The class that will be added onto the active item. * - hidden * |-- Type: String * |-- Default: 'is-hidden' * |-- The class that will be added onto the switcher * buttons when an unloopable carousel is at the first * item and the last item. (At the first time, the * prevSwitcher button will receive this class, while * at the last item, the nextSwitcher button will * receive it.) * - loop * |-- Type: Boolean * |-- Default: true * |-- Allows the carousel to loop back to the first item after it * reaches the last item and vice versa. * - transitions * |-- Type: Object * |-- Pass in an arguments object. Arguments in this object are: * - enable * |-- Type: Boolean * |-- Default: true * |-- Pass in `true` to have the plugin attempt to use * CSS transitions by toggling the respective classes * on the items and leveraging the `transitionEnd` * event. * - classes * |-- Type: Object * |-- Pass in an object containing strings of the classes * to use. Properties in this object: * - transitionIn: * |-- Type: String * |-- Default: 'is-transitioning-in' * |-- The class that will be added onto the next * active item to trigger the transition. * - transitionOut: * |-- Type: String * |-- Default: 'is-transitioning-out' * |-- The class that will be added onto the current * active item to trigger the transition. * - reverse: * |-- Type: String * |-- Default: 'is-reverse' * |-- The class that will be added onto both the * current and the next active item when the * carousel is triggered in the opposite direction. * This is useful to trigger a reverse transition. * - onStart * |-- Type: Function * |-- Default: null * |-- Pass in a callback function to be executed after the * plugin adds the general homebrew transition class but * before the plugin adds the transitionIn class to the * target item. The function will receive two arguments: * (1) the jQuery object of the item that the carousel * is switching to, and * (2) the jQuery object of the item that the carousel * is switching away from. * - onEnd * |-- Type: Function * |-- Default: null * |-- Pass in a callback function to be executed after the * transition completes and the plugin removes all * transition-related classes from the items. The * function will receive two arguments: * (1) the jQuery object of the item that the carousel * is switching to, and * (2) the jQuery object of the item that the carousel * is switching away from. * - switchers * |-- Type: Object * |-- Pass in an arguments object. Arguments in this object are: * - enable * |-- Type: Boolean * |-- Default: true * |-- Option to have the plugin use switcher buttons. * Switcher buttons make the carousel rotate between the * next and previous items. * - markups * |-- Type: Object * |-- Pass in an object containing strings of the markups * to use. These markup strings have TWO uses: * (1) the plugin will extract the classes in the markup * string and use it to find any pre-existing switcher * buttons in the carousel; if no pre-existing * switcher buttons are found, then... * (2) the plugin will create its own switcher buttons * using the provided markup. * |-- Properties in this object: * - nextSwitcher: * |-- Type: String * |-- Default: ''' * |-- The markup to be used for the switcher that * rotates the carousel to the next item. * - prevSwitcher: * |-- Type: String * |-- Default: ''' * |-- The markup to be used for the switcher that * rotates the carousel to the previous item. * - pagers * |-- Type: Object * |-- Pass in an arguments object. Arguments in this object are: * - enable * |-- Type: Boolean * |-- Default: true * |-- Option to have the plugin use pager buttons. * Pager buttons make the carousel rotate directly to the * corresponding item. * - markup * |-- Type: String * |-- Default: '' * |-- Pass in the markup string to be used for pagers. * |-- Markup strings have TWO uses: * (1) the plugin will extract the classes in the markup * string and use it to find any pre-existing pager * buttons in the carousel; if no pre-existing * pager buttons are found, then... * (2) the plugin will create its own pager buttons * using the provided markup. * - holder * |-- Type: String * |-- Default: '' * |-- Pass in the markup string to use for the holder of the * holders. If a holder is used, then the plugin will look * for pagers within the provided holders. * |-- Pass in `null` or an empty string to refrain from using * a holder. If a holder isn't used, then the plugin will * search for pagers within the carousel element. * |-- Markup strings have TWO uses: * (1) the plugin will extract the classes in the markup * string and use it to find any pre-existing pager * buttons in the carousel; if no pre-existing * pager buttons are found, then... * (2) the plugin will create its own pager buttons * using the provided markup. * - timer * |-- Type: Object * |-- Pass in an arguments object. Arguments in this object are: * - enable * |-- Type: Boolean * |-- Default: true * |-- Option to have the plugin automatically rotate through * the items based on a timer. * - duration * |-- Type: Number * |-- Default: 10000 * |-- Determines the delay between each rotation. A larger * number would cause the carousel to rotate less often. * - onSwitch * |-- Type: Function * |-- Default: null * |-- Pass in a callback function to be executed right before the * carousel switches between items. The function will receive two * arguments: * (1) the jQuery object of the item that the carousel is * switching to, and * (2) the jQuery object of the item that the carousel is * switching away from. */ homebrew.Carousel = function(el, args) { if(!el) return; this.init(el, args); }; $.extend(homebrew.Carousel.prototype, { name : 'carouselify', options : { items : '.carousel-item', activeItem : null, classes : { active : 'is-active', hidden : 'is-hidden' }, loop : true, transitions : { enable : true, classes : { transitionIn : 'is-transitioning-in', transitionOut : 'is-transitioning-out', reverse : 'is-reverse' }, onStart : null, onEnd : null }, switchers : { enable : true, markups : { nextSwitcher : '', prevSwitcher : '' } }, pagers : { enable : true, markup : '', holder : '' }, timer : { enable : true, duration : 10000, showBar : true, barMarkup : '' }, onSwitch : null }, init : function(el, args) { var instance = this, options = $.extend(true, {}, instance.options, args); if(typeof options.items !== 'string') { console.error('$.fn.carouselify: Expecting String type from `items` argument. Please check:'); console.log(options.items); return; } var $el = $(el), $items = $el.find(options.items); if(!$items.length) return; instance.$el = $el; instance.$items = $items; instance.totalItems = $items.length; instance.options = options; if(typeof options.activeItem === 'number') { $items.eq(instance.activeItem).addClass(options.classes.active); instance.activeItem = options.activeItem; } else if(instance.$items.filter(options.classes.active).length) { instance.activeItem = instance.$items.filter(options.classes.active).index(); } else { instance.activeItem = 0; } if(options.switchers.enable) { var $nextSwitcher, nextSwitcherClasses = homebrew.getClassFromHTMLString(options.switchers.markups.nextSwitcher); if(nextSwitcherClasses.length) { $nextSwitcher = $el.find('.' + nextSwitcherClasses.join('.')); } if(!$nextSwitcher || !$nextSwitcher.length) { $nextSwitcher = $(options.switchers.markups.nextSwitcher); $el.append($prevSwitcher); instance.addDestroyable($nextSwitcher); } instance.$nextSwitcher = $nextSwitcher; var $prevSwitcher, prevSwitcherClasses = homebrew.getClassFromHTMLString(options.switchers.markups.prevSwitcher); if(prevSwitcherClasses.length) { $prevSwitcher = $el.find('.' + prevSwitcherClasses.join('.')); } if(!$prevSwitcher || !$prevSwitcher.length) { $prevSwitcher = $(options.switchers.markups.prevSwitcher); $el.prepend($prevSwitcher); instance.addDestroyable($prevSwitcher); } instance.$prevSwitcher = $prevSwitcher; } if(options.pagers.enable) { var $holders, $pagers, pagerClasses = homebrew.getClassFromHTMLString(options.pagers.markup); if(typeof options.pagers.holder === 'string' && options.pagers.holder !== '') { var holderClasses = homebrew.getClassFromHTMLString(options.pagers.holder); if(holderClasses.length) { $holders = $el.find('.' + holderClasses.join('.')); } if(!$holders || !$holders.length) { $holders = $(options.pagers.holder).appendTo($el); } } if(!$holders || !$holders.length) { $holders = $el; } if(pagerClasses.length) { $pagers = $holders.find('.' + pagerClasses.join('.')); } if(!$pagers || !$pagers.length) { var finalPagersStr = []; for(var i = instance.totalItems-1; i > -1; i--) { finalPagersStr.push(options.pagers.markup); } $pagers = $(finalPagersStr.join('')).appendTo($holders); instance.addDestroyable($pagers); } $pagers.eq(instance.activeItem).addClass(options.classes.active); instance.$pagers = $pagers; } if(options.timer.enable && options.showBar) { var $timerBar, timerBarClasses = homebrew.getClassFromHTMLString(options.timer.barMarkup); if(timerBarClasses.length) { $timerBar = $el.find('.' + timerBarClasses.join('.')); } if(!$timerBar || !$timerBar.length) { $timerBar = $(options.timer.barMarkup); instance.addDestroyable($timerBar); } instance.$timerBar = $timerBar; if(typeof options.timer.duration !== 'number') { console.error('$.fn.carouselify(): Expecting a number for the timer duration. Please check:'); console.log(options.timer.duration); console.error('Reverting back to default.'); options.timer.duration = homebrew.Carousel.prototype.options.timer.duration; } } instance.enable(); $.data(el, instance.name, instance); return instance; }, enable : function() { var instance = this; if(instance.$nextSwitcher && instance.$prevSwitcher) { instance.$nextSwitcher.on('click.' + instance.name, function() { instance.switchTo('next'); }); instance.$prevSwitcher.on('click.' + instance.name, function() { instance.switchTo('prev'); }); } if(instance.$pagers) { instance.$pagers.each(function(index) { $(this).on('click.' + instance.name, function(e) { e.preventDefault(); instance.switchTo(index); }); }); } instance.runTimer(); if(instance.options.timer.enable) { $(window) .on('focus.' + instance.name, function() { instance.runTimer(); }) .on('blur.' + instance.name, function() { clearTimeout(instance.timer); }); } return instance; }, disable : function() { var instance = this; clearTimeout(instance.timer); if(instance.$nextSwitcher && instance.$prevSwitcher) { instance.$nextSwitcher.add(instance.$prevSwitcher).off('click.' + instance.name); } if(instance.$pagers) { instance.$pagers.off('click.' + instance.name); } return instance; }, switchTo : function(itemIndex) { var instance = this; if(instance.isSwitching === true) return instance; instance.isSwitching = true; var activeItem = instance.activeItem, options = instance.options; if(typeof itemIndex === 'string') { switch(itemIndex) { case 'next' : activeItem++; if(activeItem >= instance.totalItems) { activeItem = 0; } break; case 'prev' : activeItem--; if(activeItem < 0) { activeItem = instance.totalItems - 1; } break; default: console.error('Homebrew.Carousel.switchTo(): Unrecognised string method `' + itemIndex + '`.'); return; break; } } else if(typeof itemIndex === 'number') { activeItem = itemIndex; } else { console.error('Homebrew.Carousel.switchTo(): Unsupported argument type: `' + typeof itemIndex + '`.'); console.log(itemIndex); return; } if(activeItem === instance.activeItem) return instance; var $currentItem = instance.$items.eq(activeItem), $prevItem = instance.$items.eq(instance.activeItem), activeClass = options.classes.active; if(typeof options.onSwitch === 'function') { options.onSwitch.call(instance.$el[0], $currentItem, $prevItem); } instance.runTimer(); if(options.transitions.enable) { var transitionEvent = homebrew.events.transitionEnd, transitionClass = homebrew.classes.transitionable, transitionInClass = options.transitions.classes.transitionIn, transitionOutClass = options.transitions.classes.transitionOut, reverseClass = options.transitions.classes.reverse; instance.$items.removeClass([ reverseClass, transitionClass ].join(' ')); instance.$items.not($prevItem).removeClass(activeClass); $currentItem .one(transitionEvent, function() { $currentItem .off(transitionEvent) .add($prevItem) .removeClass([ transitionInClass, transitionOutClass, reverseClass, transitionClass, activeClass ].join(' ')) .end() .addClass(activeClass); instance.isSwitching = false; if(typeof options.transitions.onEnd === 'function') { options.transitions.onEnd.call(instance.$el[0], $currentItem, $prevItem); } }); if(itemIndex === 'prev' && instance.activeItem === 0 && activeItem === instance.totalItems-1 || itemIndex !== 'next' && instance.activeItem > activeItem) { $prevItem.add($currentItem).addClass(reverseClass); } setTimeout(function() { $prevItem.add($currentItem).addClass(transitionClass); $prevItem.addClass(transitionOutClass); $currentItem.addClass(transitionInClass); if(typeof options.transitions.onStart === 'function') { options.transitions.onStart.call(instance.$el[0], $currentItem, $prevItem); } }, 10); } else { instance.$items.removeClass(activeClass); $currentItem.addClass(activeClass); instance.isSwitching = false; } instance.$pagers.removeClass(activeClass) .eq(activeItem) .addClass(activeClass); instance.activeItem = activeItem; return instance; }, runTimer : function() { var instance = this, options = instance.options; if(!options.timer.enable) return instance; clearTimeout(instance.timer); if(options.loop || activeItem < instance.totalItems-1) { instance.timer = setTimeout(function() { instance.switchTo('next'); }, options.timer.duration); } return instance; }, addDestroyable : function($obj) { var instance = this; if(!instance.destroyables) instance.destroyables = []; instance.destroyables.push($obj); return instance; }, destroy : function() { var instance = this; instance.disable(); if(instance.destroyable) { while(instance.destroyable.length) { instance.destoryable.shift().remove(); } } $.removeData(instance.$el[0], instance.name); } }); homebrew.makePlugin(homebrew.Carousel); /**---- Height Syncer ----**\ * Sync the height of a collection of items. * * Arguments: * $('.items-holder').heightSyncify({ * items : [ * $('.items-holder').find('.item') * ] * }); * * - items * |-- Type: Array * |-- Pass in an Array of the items to sync. The Array can consist * of either selector strings or jQuery Objects. If selector * strings are provided, the plugin will try to select the targets * within the currently iterated element. The sequence determines * which set of items' height get synced first. This is important * if you need to sync two items that are of parent-child relation * (you would most likely want to sync the children's height first * so that they also count towards the parents' height) * * Methods: * - init * |-- $('.items-holder').heightSyncify(); * |-- Initialise the plugin. Once the plugin is initialised, you can * pass in a string to trigger a specific method on it. * |-- Accepts an arguments object. Refer to the above for the list * of arguments available. * - update * |-- $('.items-holder').heightSyncify(); * |-- Init the plugin again to update its options. * |-- Accepts an arguments object. Refer to the above for the list * of arguments available. * - enable * |-- $('.items-holder').heightSyncify('enable'); * |-- Automatically re-sync the height when the window is resized. * - disable * |-- $('.items-holder').heightSyncify('disable'); * |-- Stop re-syncing the height when the window is resized. * - sync * |-- $('.items-holder').heightSyncify('sync'); * |-- Sync the items height. * - destroy * |-- $('.items-holder').heightSyncify('destroy'); * |-- Removes the plugin. */ homebrew.HeightSyncer = function(el, args) { if(!el) return; this.init(el, args); }; $.extend(homebrew.HeightSyncer.prototype, { name : 'heightSyncify', init : function(el, args) { args = args || {}; var instance = this; instance.$el = $(el); instance.uniqueID = homebrew.generateUniqueID(); instance.update(args) .enable(); $.data(el, instance.name, instance); return instance; }, update : function(args) { args = args || {}; var instance = this; instance.items = args.items.slice(0); for(var i = instance.items.length-1; i > -1; i--) { if(typeof instance.items[i] === 'string') { instance.items[i] = instance.$el.find(instance.items[i]); } instance.items[i].find('img').each(function() { if(this.complete) return; $(this).one('load', function() { clearTimeout(instance.timer); instance.timer = setTimeout(function() { instance.sync(); }, 100); }); }); } if(args.options) { $.extend(instance, args.options); } instance.sync(); return instance; }, enable : function() { var instance = this; if(instance.enabled) return instance; instance.enabled = true; $(window).on('resize.' + instance.uniqueID, homebrew.utils.throttle(function() { instance.sync(); }, 30)); return instance; }, disable : function() { this.enabled = false; $(window).off('.' + this.uniqueID); return this; }, sync : function() { var instance = this, $currentCollection, $items = $(), leftOffset, currentLeftThreshold, heights, tallestHeight; if(!instance.items || !instance.items.length) return instance; for(var i = 0, ii = instance.items.length; i < ii; i++) { leftOffset = currentLeftThreshold = -9999; $currentCollection = instance.items[i]; $currentCollection.each(function(index) { $items = $items.add($(this)); leftOffset = $(this).offset().left; if(!$currentCollection.eq(index+1).length || $currentCollection.eq(index+1).offset().left <= leftOffset) { heights = []; $items.css('height', ''); $items.each(function() { heights.push($(this).outerHeight()); }); tallestHeight = Math.max.apply(null, heights); $items.outerHeight(tallestHeight); $items = $(); } }); } instance.$el.trigger('afterSync'); return instance; }, destroy : function() { var instance = this; while(instance.items.length) { instance.items.shift().css('height', ''); } instance.disable(); $.removeData(instance.$el[0], instance.name); } }); homebrew.makePlugin(homebrew.HeightSyncer); /**---- Tooltipify ---**\ * Initialise a custom tooltip on the element. * * Arguments: * $('.my-element').tooltipify({ * appendTo : 'body', * classes : { * active : 'is-active' * }, * markups : { * tooltip : '' * }, * contents : function() { * var instance = this, * title = instance.$el.attr('title'); * * instance.$el.data(instance.name + '-title', title); * instance.$el.removeAttr('title'); * * return title; * }, * transitions : { * enable : true, * classes : { * transitionIn : 'is-transitioning-in', * transitionOut : 'is-transitioning-out' * } * }, * hoverDuration : 400 * }); * * - appendTo * |-- Type: Object | String * |-- Default: 'body' * |-- Determines where the tooltip will be appended to when it is * is created to be shown. * |-- You can pass in either a string, a Node element or a jQuery * object. If what you passed in results in nothing being selected, * the plugin will fallback to appending to the element. * - classes * |-- An Object that contains strings of general classes to be used * in the plugin. The classes are: * - active * |-- Default: 'is-active' * |-- The class that will be added to the tooltip after it is * created and shown. * - markups * |-- An Object that contains strings of markups to be used in * the plugin. The markups are: * - tooltip * |-- Default: '' * |-- The markup used to contain the tooltip. * - contents * |-- Type: Function | String * |-- Default: function() { * var instance = this, * title = instance.$el.attr('title'); * * instance.$el.data(instance.name + '-title', title); * instance.$el.removeAttr('title'); * * return title; * } * |-- Determines the content of the tooltip. There are two ways * to set the content: * (1) Use a function that returns the content string when it is * run. This is useful if the element itself has a title * attribute, as you can use this function to save the value * and then proceed to remove the attribute (to prevent the * default tooltip), * OR * (2) Directly pass in the content string itself. * |-- The content string is inserted using the `.html()` method. * - hoverDuration * |-- Type: Number * |-- Default: 400 * |-- Determines how long the mouse needs to hover over the element * in order to trigger the tooltip. Lower number means a shorter * duration to trigger. * - transitions * |-- An Object that contains the various properties to be used * in the plugin. The properties are: * - enable * |-- Type: Boolean * |-- Default: true * |-- Determines whether or not the plugin should attempt * to leverage CSS transitions. * - classes * |-- An Object that contains strings of transition classes * to be used in the plugin. The classes are: * - transition * |-- Default: homebrew.classes.transition * |-- This class is used to enable the transition * effect on the element. * - transitionIn * |-- Default: 'is-transitioning-in' * |-- This class is used to make the tooltip * transition in. * - transitionOut * |-- Default: 'is-transitioning-out' * |-- This class is used to make the tooltip * transition out. */ homebrew.Tooltip = function(el, args) { if(!el) return; this.init(el, args); }; $.extend(homebrew.Tooltip.prototype, { name : 'tooltipify', options : { appendTo : 'body', markups : { tooltip : '', closer : '' }, classes : { active : 'is-active' }, contents : function() { var instance = this, title = instance.$el.attr('title'); instance.$el.data(instance.name + '-title', title); instance.$el.removeAttr('title'); return title; }, hoverDuration : 400, transitions : { enable : true, classes : { transition : homebrew.classes.transitionable, transitionIn : 'is-transitioning-in', transitionOut : 'is-transitioning-out' } } }, init : function(el, args) { var instance = this, options = $.extend({}, instance.options, args); instance.$el = $(el).addClass(instance.name); instance.uniqueID = homebrew.generateUniqueID(); instance.options = options; instance.$appendTo = $(options.appendTo); if(!instance.$appendTo.length) { instance.$appendTo = 'body'; } if(typeof options.contents === 'function') { options.contents = options.contents.call(instance); } instance.enable(); $.data(el, instance.name, instance); }, enable : function() { var instance = this, $el = instance.$el, options = instance.options; $el.on('click.' + instance.uniqueID, function(e) { e.preventDefault(); clearTimeout(instance.timer); instance.open(); }); $el.on('mouseenter.' + instance.uniqueID, function(e) { clearTimeout(instance.timer); instance.timer = setTimeout(function() { instance.open(); }, options.hoverDuration); }); $el.on('mouseleave.' + instance.uniqueID, function(e) { clearTimeout(instance.timer); instance.timer = setTimeout(function() { instance.close(); }, options.hoverDuration); }); return instance; }, disable : function() { this.$el.off('.' + instance.uniqueID); return this; }, getAltRender : function() { return Modernizr.touch && homebrew.screenSize.small; }, open : function() { var instance = this; if(instance.$tooltip) return; var $el = instance.$el, options = instance.options, $tooltip = $(options.markups.tooltip).appendTo(instance.$appendTo).html(options.contents), activeClass = options.classes.active, altRender = instance.getAltRender(); instance.$tooltip = $tooltip; if(altRender) { var $closer = $('.' + homebrew.getClassFromHTMLString(options.markups.closer).join('.')); if(!$closer.length) { $closer = $(options.markups.closer).prependTo($tooltip); } $closer.on('click', function(e) { e.preventDefault(); instance.close(); }); } else { if(homebrew.screenSize.small) { if($tooltip.outerWidth() + parseInt($tooltip.css('margin-left'), 10) === $(window).width()) { $tooltip.css({ left : '0px', marginLeft : '0px' }); } else if($el.offset().left + $tooltip.outerWidth() > $(window).width()) { $tooltip.css('right', '0px'); } else { $tooltip .css('left', $el.offset().left + 'px'); } } else { if($el.offset().left + $tooltip.outerWidth() > $(window).width()) { $tooltip .addClass('is-opposite') .css('right', '0px'); } else { $tooltip .css('left', $el.offset().left + 'px'); } } $tooltip.css('top', $el.offset().top - $('#mainContent').offset().top - $tooltip.outerHeight() + 'px') } if(options.transitions.enable) { var transitionEvent = homebrew.events.transitionEnd, transitionClass = homebrew.classes.transitionable, transitionInClass = options.transitions.classes.transitionIn; $tooltip .one(transitionEvent, function() { $tooltip.removeClass([transitionClass, transitionInClass].join(' ')) .addClass(activeClass); $(document).on([ 'click.fauxBlur.', instance.uniqueID, ' touchstart.fauxBlur.', instance.uniqueID ].join(''), function(e) { if(!$tooltip.is(e.target) && $tooltip.has(e.target).length === 0) { instance.close(); } }); }) .on({ mouseenter : function() { clearTimeout(instance.timer); }, mouseleave : function() { clearTimeout(instance.timer); instance.timer = setTimeout(function() { instance.close(); }, options.hoverDuration); } }); setTimeout(function() { $tooltip.addClass(transitionClass); if(altRender) { $tooltip.css('margin-top', -$tooltip.outerHeight() + 'px'); } else { $tooltip.addClass(transitionInClass); } }, 10); } else { $tooltip.addClass(activeClass); } return instance; }, close : function(args) { args = args || {}; var instance = this, $tooltip = instance.$tooltip; if(!$tooltip) return; var options = instance.options, activeClass = options.classes.active, altRender = instance.getAltRender(); $(document).off('.fauxBlur.' + instance.uniqueID); if(options.transitions.enable) { var transitionEvent = homebrew.events.transitionEnd, transitionClass = homebrew.classes.transitionable, transitionOutClass = options.transitions.classes.transitionOut; $tooltip .trigger(transitionEvent) .off('mouseenter mouseleave') .one(transitionEvent, function() { $tooltip.removeClass([transitionClass, transitionOutClass].join(' ')) .removeClass(activeClass) .remove(); instance.$tooltip = null; if(typeof args.onCloseEnd === 'function') { args.onCloseEnd(); } }); setTimeout(function() { $tooltip.addClass(transitionClass); if(altRender) { $tooltip.css('margin-top', ''); } else { $tooltip.addClass(transitionOutClass); } }, 10); } else { $tooltip.removeClass(activeClass); instance.$tooltip = null; if(typeof args.onClose === 'function') { args.onClose(); } } return instance; }, destroy : function() { var instance = this, el = instance.$el[0], options = instance.options; instance.disable().close(); $.removeData(el, instance.name); if($.data(el, instance.name + '-title')) { el.title = $.data(el, instance.name + '-title'); $.removeData(el, instance.name + '-title'); } } }); homebrew.makePlugin(homebrew.Tooltip); /* Extend jQuery with our custom functions built in plugins format. */ $.fn.extend({ /**---- Dropdownify ----**\ * Call this `$('select').dropdownify()` function on the