/** * Isotope v1.5.25 * An exquisite jQuery plugin for magical layouts * http://isotope.metafizzy.co * * Commercial use requires one-time license fee * http://metafizzy.co/#licenses * * Copyright 2012 David DeSandro / Metafizzy */ /*jshint asi: true, browser: true, curly: true, eqeqeq: true, forin: false, immed: false, newcap: true, noempty: true, strict: true, undef: true */ /*global jQuery: false */ (function(window, $, undefined ) { 'use strict'; // get global vars var document = window.document; var Modernizr = window.Modernizr; // helper function var capitalize = function(str ) { return str.charAt(0).toUpperCase() + str.slice(1); }; // ========================= getStyleProperty by kangax =============================== // http://perfectionkills.com/feature-testing-css-properties/ var prefixes = 'Moz Webkit O Ms'.split(' '); var getStyleProperty = function(propName ) { var style = document.documentElement.style, prefixed; // test standard property first if (typeof style[propName] === 'string') { return propName; } // capitalize propName = capitalize(propName); // test vendor specific properties for (var i = 0, len = prefixes.length; i < len; i++) { prefixed = prefixes[i] + propName; if (typeof style[prefixed] === 'string') { return prefixed; } } }; var transformProp = getStyleProperty('transform'), transitionProp = getStyleProperty('transitionProperty'); // ========================= miniModernizr =============================== // <3<3<3 and thanks to Faruk and Paul for doing the heavy lifting /*! * Modernizr v1.6ish: miniModernizr for Isotope * http://www.modernizr.com * * Developed by: * - Faruk Ates http://farukat.es/ * - Paul Irish http://paulirish.com/ * * Copyright (c) 2009-2010 * Dual-licensed under the BSD or MIT licenses. * http://www.modernizr.com/license/ */ /* * This version whittles down the script just to check support for * CSS transitions, transforms, and 3D transforms. */ var tests = { csstransforms: function() { return !!transformProp; }, csstransforms3d: function() { var test = !!getStyleProperty('perspective'); // double check for Chrome's false positive if (test) { var vendorCSSPrefixes = ' -o- -moz- -ms- -webkit- -khtml- '.split(' '), mediaQuery = '@media (' + vendorCSSPrefixes.join('transform-3d),(') + 'modernizr)', $style = $('') .appendTo('head'), $div = $('
').appendTo('html'); test = $div.height() === 3; $div.remove(); $style.remove(); } return test; }, csstransitions: function() { return !!transitionProp; } }; var testName; if (Modernizr) { // if there's a previous Modernzir, check if there are necessary tests for (testName in tests) { if (!Modernizr.hasOwnProperty(testName)) { // if test hasn't been run, use addTest to run it Modernizr.addTest(testName, tests[testName]); } } } else { // or create new mini Modernizr that just has the 3 tests Modernizr = window.Modernizr = { _version: '1.6ish: miniModernizr for Isotope' }; var classes = ' '; var result; // Run through tests for (testName in tests) { result = tests[testName](); Modernizr[testName] = result; classes += ' ' + (result ? '' : 'no-') + testName; } // Add the new classes to the element. $('html').addClass(classes); } // ========================= isoTransform =============================== /** * provides hooks for .css({ scale: value, translate: [x, y] }) * Progressively enhanced CSS transforms * Uses hardware accelerated 3D transforms for Safari * or falls back to 2D transforms. */ if (Modernizr.csstransforms) { // i.e. transformFnNotations.scale(0.5) >> 'scale3d( 0.5, 0.5, 1)' var transformFnNotations = Modernizr.csstransforms3d ? { // 3D transform functions translate: function(position ) { return 'translate3d(' + position[0] + 'px, ' + position[1] + 'px, 0) '; }, scale: function(scale ) { return 'scale3d(' + scale + ', ' + scale + ', 1) '; } } : { // 2D transform functions translate: function(position ) { return 'translate(' + position[0] + 'px, ' + position[1] + 'px) '; }, scale: function(scale ) { return 'scale(' + scale + ') '; } }; var setIsoTransform = function(elem, name, value ) { // unpack current transform data var data = $.data(elem, 'isoTransform') || {}, newData = {}, fnName, transformObj = {}, transformValue; // i.e. newData.scale = 0.5 newData[name] = value; // extend new value over current data $.extend(data, newData); for (fnName in data) { transformValue = data[fnName]; transformObj[fnName] = transformFnNotations[fnName](transformValue); } // get proper order // ideally, we could loop through this give an array, but since we only have // a couple transforms we're keeping track of, we'll do it like so var translateFn = transformObj.translate || '', scaleFn = transformObj.scale || '', // sorting so translate always comes first valueFns = translateFn + scaleFn; // set data back in elem $.data(elem, 'isoTransform', data); // set name to vendor specific property elem.style[transformProp] = valueFns; }; // ==================== scale =================== $.cssNumber.scale = true; $.cssHooks.scale = { set: function(elem, value ) { // uncomment this bit if you want to properly parse strings // if ( typeof value === 'string' ) { // value = parseFloat( value ); // } setIsoTransform(elem, 'scale', value); }, get: function(elem, computed ) { var transform = $.data(elem, 'isoTransform'); return transform && transform.scale ? transform.scale : 1; } }; $.fx.step.scale = function(fx ) { $.cssHooks.scale.set(fx.elem, fx.now + fx.unit); }; // ==================== translate =================== $.cssNumber.translate = true; $.cssHooks.translate = { set: function(elem, value ) { // uncomment this bit if you want to properly parse strings // if ( typeof value === 'string' ) { // value = value.split(' '); // } // // var i, val; // for ( i = 0; i < 2; i++ ) { // val = value[i]; // if ( typeof val === 'string' ) { // val = parseInt( val ); // } // } setIsoTransform(elem, 'translate', value); }, get: function(elem, computed ) { var transform = $.data(elem, 'isoTransform'); return transform && transform.translate ? transform.translate : [0, 0]; } }; } // ========================= get transition-end event =============================== var transitionEndEvent, transitionDurProp; if (Modernizr.csstransitions) { transitionEndEvent = { WebkitTransitionProperty: 'webkitTransitionEnd', // webkit MozTransitionProperty: 'transitionend', OTransitionProperty: 'oTransitionEnd otransitionend', transitionProperty: 'transitionend' }[transitionProp]; transitionDurProp = getStyleProperty('transitionDuration'); } // ========================= smartresize =============================== /* * smartresize: debounced resize event for jQuery * * latest version and complete README available on Github: * https://github.com/louisremi/jquery.smartresize.js * * Copyright 2011 @louis_remi * Licensed under the MIT license. */ var $event = $.event, dispatchMethod = $.event.handle ? 'handle' : 'dispatch', resizeTimeout; $event.special.smartresize = { setup: function() { $(this).bind('resize', $event.special.smartresize.handler); }, teardown: function() { $(this).unbind('resize', $event.special.smartresize.handler); }, handler: function(event, execAsap ) { // Save the context var context = this, args = arguments; // set correct event type event.type = 'smartresize'; if (resizeTimeout) { clearTimeout(resizeTimeout); } resizeTimeout = setTimeout(function() { $event[dispatchMethod].apply(context, args); }, execAsap === 'execAsap' ? 0 : 100); } }; $.fn.smartresize = function(fn ) { return fn ? this.bind('smartresize', fn) : this.trigger('smartresize', ['execAsap']); }; // ========================= Isotope =============================== // our "Widget" object constructor $.Isotope = function(options, element, callback ) { this.element = $(element); this._create(options); this._init(callback); }; // styles of container element we want to keep track of var isoContainerStyles = ['width', 'height']; var $window = $(window); $.Isotope.settings = { resizable: true, layoutMode: 'masonry', containerClass: 'isotope', itemClass: 'isotope-item', hiddenClass: 'isotope-hidden', hiddenStyle: { opacity: 0, scale: 0.001 }, visibleStyle: { opacity: 1, scale: 1 }, containerStyle: { position: 'relative', overflow: 'hidden' }, animationEngine: 'best-available', animationOptions: { queue: false, duration: 800 }, sortBy: 'original-order', sortAscending: true, resizesContainer: true, transformsEnabled: true, itemPositionDataEnabled: false }; $.Isotope.prototype = { // sets up widget _create: function(options ) { this.options = $.extend({}, $.Isotope.settings, options); this.styleQueue = []; this.elemCount = 0; // get original styles in case we re-apply them in .destroy() var elemStyle = this.element[0].style; this.originalStyle = {}; // keep track of container styles var containerStyles = isoContainerStyles.slice(0); for (var prop in this.options.containerStyle) { containerStyles.push(prop); } for (var i = 0, len = containerStyles.length; i < len; i++) { prop = containerStyles[i]; this.originalStyle[prop] = elemStyle[prop] || ''; } // apply container style from options this.element.css(this.options.containerStyle); this._updateAnimationEngine(); this._updateUsingTransforms(); // sorting var originalOrderSorter = { 'original-order' : function($elem, instance ) { instance.elemCount++; return instance.elemCount; }, random: function() { return Math.random(); } }; this.options.getSortData = $.extend(this.options.getSortData, originalOrderSorter); // need to get atoms this.reloadItems(); // get top left position of where the bricks should be this.offset = { left: parseInt((this.element.css('padding-left') || 0), 10), top: parseInt((this.element.css('padding-top') || 0), 10) }; // add isotope class first time around var instance = this; setTimeout(function() { instance.element.addClass(instance.options.containerClass); }, 0); // bind resize method if (this.options.resizable) { $window.bind('smartresize.isotope', function() { instance.resize(); }); } // dismiss all click events from hidden events this.element.delegate('.' + this.options.hiddenClass, 'click', function() { return false; }); }, _getAtoms: function($elems ) { var selector = this.options.itemSelector, // filter & find $atoms = selector ? $elems.filter(selector).add($elems.find(selector)) : $elems, // base style for atoms atomStyle = { position: 'absolute' }; // filter out text nodes $atoms = $atoms.filter(function(i, atom ) { return atom.nodeType === 1; }); if (this.usingTransforms) { atomStyle.left = 0; atomStyle.top = 0; } $atoms.css(atomStyle).addClass(this.options.itemClass); this.updateSortData($atoms, true); return $atoms; }, // _init fires when your instance is first created // (from the constructor above), and when you // attempt to initialize the widget again (by the bridge) // after it has already been initialized. _init: function(callback ) { this.$filteredAtoms = this._filter(this.$allAtoms); this._sort(); this.reLayout(callback); }, option: function(opts ) { // change options AFTER initialization: // signature: $('#foo').bar({ cool:false }); if ($.isPlainObject(opts)) { this.options = $.extend(true, this.options, opts); // trigger _updateOptionName if it exists var updateOptionFn; for (var optionName in opts) { updateOptionFn = '_update' + capitalize(optionName); if (this[updateOptionFn]) { this[updateOptionFn](); } } } }, // ====================== updaters ====================== // // kind of like setters _updateAnimationEngine: function() { var animationEngine = this.options.animationEngine.toLowerCase().replace(/[ _\-]/g, ''); var isUsingJQueryAnimation; // set applyStyleFnName switch (animationEngine) { case 'css' : case 'none' : isUsingJQueryAnimation = false; break; case 'jquery' : isUsingJQueryAnimation = true; break; default : // best available isUsingJQueryAnimation = !Modernizr.csstransitions; } this.isUsingJQueryAnimation = isUsingJQueryAnimation; this._updateUsingTransforms(); }, _updateTransformsEnabled: function() { this._updateUsingTransforms(); }, _updateUsingTransforms: function() { var usingTransforms = this.usingTransforms = this.options.transformsEnabled && Modernizr.csstransforms && Modernizr.csstransitions && !this.isUsingJQueryAnimation; // prevent scales when transforms are disabled if (!usingTransforms) { delete this.options.hiddenStyle.scale; delete this.options.visibleStyle.scale; } this.getPositionStyles = usingTransforms ? this._translate : this._positionAbs; }, // ====================== Filtering ====================== _filter: function($atoms ) { var filter = this.options.filter === '' ? '*' : this.options.filter; if (!filter) { return $atoms; } var hiddenClass = this.options.hiddenClass, hiddenSelector = '.' + hiddenClass, $hiddenAtoms = $atoms.filter(hiddenSelector), $atomsToShow = $hiddenAtoms; if (filter !== '*') { $atomsToShow = $hiddenAtoms.filter(filter); var $atomsToHide = $atoms.not(hiddenSelector).not(filter).addClass(hiddenClass); this.styleQueue.push({ $el: $atomsToHide, style: this.options.hiddenStyle }); } this.styleQueue.push({ $el: $atomsToShow, style: this.options.visibleStyle }); $atomsToShow.removeClass(hiddenClass); return $atoms.filter(filter); }, // ====================== Sorting ====================== updateSortData: function($atoms, isIncrementingElemCount ) { var instance = this, getSortData = this.options.getSortData, $this, sortData; $atoms.each(function() { $this = $(this); sortData = {}; // get value for sort data based on fn( $elem ) passed in for (var key in getSortData) { if (!isIncrementingElemCount && key === 'original-order') { // keep original order original sortData[key] = $.data(this, 'isotope-sort-data')[key]; } else { sortData[key] = getSortData[key]($this, instance); } } // apply sort data to element $.data(this, 'isotope-sort-data', sortData); }); }, // used on all the filtered atoms _sort: function() { var sortBy = this.options.sortBy, getSorter = this._getSorter, sortDir = this.options.sortAscending ? 1 : -1, sortFn = function(alpha, beta ) { var a = getSorter(alpha, sortBy), b = getSorter(beta, sortBy); // fall back to original order if data matches if (a === b && sortBy !== 'original-order') { a = getSorter(alpha, 'original-order'); b = getSorter(beta, 'original-order'); } return ((a > b) ? 1 : (a < b) ? -1 : 0) * sortDir; }; this.$filteredAtoms.sort(sortFn); }, _getSorter: function(elem, sortBy ) { return $.data(elem, 'isotope-sort-data')[sortBy]; }, // ====================== Layout Helpers ====================== _translate: function(x, y ) { return { translate: [x, y] }; }, _positionAbs: function(x, y ) { return { left: x, top: y }; }, _pushPosition: function($elem, x, y ) { x = Math.round(x + this.offset.left); y = Math.round(y + this.offset.top); var position = this.getPositionStyles(x, y); this.styleQueue.push({ $el: $elem, style: position }); if (this.options.itemPositionDataEnabled) { $elem.data('isotope-item-position', {x: x, y: y}); } }, // ====================== General Layout ====================== // used on collection of atoms (should be filtered, and sorted before ) // accepts atoms-to-be-laid-out to start with layout: function($elems, callback ) { var layoutMode = this.options.layoutMode; // layout logic this['_' + layoutMode + 'Layout']($elems); // set the size of the container if (this.options.resizesContainer) { var containerStyle = this['_' + layoutMode + 'GetContainerSize'](); this.styleQueue.push({ $el: this.element, style: containerStyle }); } this._processStyleQueue($elems, callback); this.isLaidOut = true; }, _processStyleQueue: function($elems, callback ) { // are we animating the layout arrangement? // use plugin-ish syntax for css or animate var styleFn = !this.isLaidOut ? 'css' : ( this.isUsingJQueryAnimation ? 'animate' : 'css' ), animOpts = this.options.animationOptions, onLayout = this.options.onLayout, objStyleFn, processor, triggerCallbackNow, callbackFn; // default styleQueue processor, may be overwritten down below processor = function(i, obj ) { obj.$el[styleFn](obj.style, animOpts); }; if (this._isInserting && this.isUsingJQueryAnimation) { // if using styleQueue to insert items processor = function(i, obj ) { // only animate if it not being inserted objStyleFn = obj.$el.hasClass('no-transition') ? 'css' : styleFn; obj.$el[objStyleFn](obj.style, animOpts); }; } else if (callback || onLayout || animOpts.complete) { // has callback var isCallbackTriggered = false, // array of possible callbacks to trigger callbacks = [callback, onLayout, animOpts.complete], instance = this; triggerCallbackNow = true; // trigger callback only once callbackFn = function() { if (isCallbackTriggered) { return; } var hollaback; for (var i = 0, len = callbacks.length; i < len; i++) { hollaback = callbacks[i]; if (typeof hollaback === 'function') { hollaback.call(instance.element, $elems, instance); } } isCallbackTriggered = true; }; if (this.isUsingJQueryAnimation && styleFn === 'animate') { // add callback to animation options animOpts.complete = callbackFn; triggerCallbackNow = false; } else if (Modernizr.csstransitions) { // detect if first item has transition var i = 0, firstItem = this.styleQueue[0], testElem = firstItem && firstItem.$el, styleObj; // get first non-empty jQ object while (!testElem || !testElem.length) { styleObj = this.styleQueue[i++]; // HACK: sometimes styleQueue[i] is undefined if (!styleObj) { return; } testElem = styleObj.$el; } // get transition duration of the first element in that object // yeah, this is inexact var duration = parseFloat(getComputedStyle(testElem[0])[transitionDurProp]); if (duration > 0) { processor = function(i, obj ) { obj.$el[styleFn](obj.style, animOpts) // trigger callback at transition end .one(transitionEndEvent, callbackFn); }; triggerCallbackNow = false; } } } // process styleQueue $.each(this.styleQueue, processor); if (triggerCallbackNow) { callbackFn(); } // clear out queue for next time this.styleQueue = []; }, resize: function() { if (this['_' + this.options.layoutMode + 'ResizeChanged']()) { this.reLayout(); } }, reLayout: function(callback ) { this['_' + this.options.layoutMode + 'Reset'](); this.layout(this.$filteredAtoms, callback); }, // ====================== Convenience methods ====================== // ====================== Adding items ====================== // adds a jQuery object of items to a isotope container addItems: function($content, callback ) { var $newAtoms = this._getAtoms($content); // add new atoms to atoms pools this.$allAtoms = this.$allAtoms.add($newAtoms); if (callback) { callback($newAtoms); } }, // convienence method for adding elements properly to any layout // positions items, hides them, then animates them back in <--- very sezzy insert: function($content, callback ) { // position items this.element.append($content); var instance = this; this.addItems($content, function($newAtoms ) { var $newFilteredAtoms = instance._filter($newAtoms); instance._addHideAppended($newFilteredAtoms); instance._sort(); instance.reLayout(); instance._revealAppended($newFilteredAtoms, callback); }); }, // convienence method for working with Infinite Scroll appended: function($content, callback ) { var instance = this; this.addItems($content, function($newAtoms ) { instance._addHideAppended($newAtoms); instance.layout($newAtoms); instance._revealAppended($newAtoms, callback); }); }, // adds new atoms, then hides them before positioning _addHideAppended: function($newAtoms ) { this.$filteredAtoms = this.$filteredAtoms.add($newAtoms); $newAtoms.addClass('no-transition'); this._isInserting = true; // apply hidden styles this.styleQueue.push({ $el: $newAtoms, style: this.options.hiddenStyle }); }, // sets visible style on new atoms _revealAppended: function($newAtoms, callback ) { var instance = this; // apply visible style after a sec setTimeout(function() { // enable animation $newAtoms.removeClass('no-transition'); // reveal newly inserted filtered elements instance.styleQueue.push({ $el: $newAtoms, style: instance.options.visibleStyle }); instance._isInserting = false; instance._processStyleQueue($newAtoms, callback); }, 10); }, // gathers all atoms reloadItems: function() { this.$allAtoms = this._getAtoms(this.element.children()); }, // removes elements from Isotope widget remove: function($content, callback ) { // remove elements immediately from Isotope instance this.$allAtoms = this.$allAtoms.not($content); this.$filteredAtoms = this.$filteredAtoms.not($content); // remove() as a callback, for after transition / animation var instance = this; var removeContent = function() { $content.remove(); if (callback) { callback.call(instance.element); } }; if ($content.filter(':not(.' + this.options.hiddenClass + ')').length) { // if any non-hidden content needs to be removed this.styleQueue.push({ $el: $content, style: this.options.hiddenStyle }); this._sort(); this.reLayout(removeContent); } else { // remove it now removeContent(); } }, shuffle: function(callback ) { this.updateSortData(this.$allAtoms); this.options.sortBy = 'random'; this._sort(); this.reLayout(callback); }, // destroys widget, returns elements and container back (close) to original style destroy: function() { var usingTransforms = this.usingTransforms; var options = this.options; this.$allAtoms .removeClass(options.hiddenClass + ' ' + options.itemClass) .each(function() { var style = this.style; style.position = ''; style.top = ''; style.left = ''; style.opacity = ''; if (usingTransforms) { style[transformProp] = ''; } }); // re-apply saved container styles var elemStyle = this.element[0].style; for (var prop in this.originalStyle) { elemStyle[prop] = this.originalStyle[prop]; } this.element .unbind('.isotope') .undelegate('.' + options.hiddenClass, 'click') .removeClass(options.containerClass) .removeData('isotope'); $window.unbind('.isotope'); }, // ====================== LAYOUTS ====================== // calculates number of rows or columns // requires columnWidth or rowHeight to be set on namespaced object // i.e. this.masonry.columnWidth = 200 _getSegments: function(isRows ) { var namespace = this.options.layoutMode, measure = isRows ? 'rowHeight' : 'columnWidth', size = isRows ? 'height' : 'width', segmentsName = isRows ? 'rows' : 'cols', containerSize = this.element[size](), segments, // i.e. options.masonry && options.masonry.columnWidth segmentSize = this.options[namespace] && this.options[namespace][measure] || // or use the size of the first item, i.e. outerWidth this.$filteredAtoms['outer' + capitalize(size)](true) || // if there's no items, use size of container containerSize; segments = Math.floor(containerSize / segmentSize); segments = Math.max(segments, 1); // i.e. this.masonry.cols = .... this[namespace][segmentsName] = segments; // i.e. this.masonry.columnWidth = ... this[namespace][measure] = segmentSize; }, _checkIfSegmentsChanged: function(isRows ) { var namespace = this.options.layoutMode, segmentsName = isRows ? 'rows' : 'cols', prevSegments = this[namespace][segmentsName]; // update cols/rows this._getSegments(isRows); // return if updated cols/rows is not equal to previous return (this[namespace][segmentsName] !== prevSegments); }, // ====================== Masonry ====================== _masonryReset: function() { // layout-specific props this.masonry = {}; // FIXME shouldn't have to call this again this._getSegments(); var i = this.masonry.cols; this.masonry.colYs = []; while (i--) { this.masonry.colYs.push(0); } }, _masonryLayout: function($elems ) { var instance = this, props = instance.masonry; $elems.each(function() { var $this = $(this), //how many columns does this brick span colSpan = Math.ceil($this.outerWidth(true) / props.columnWidth); colSpan = Math.min(colSpan, props.cols); if (colSpan === 1) { // if brick spans only one column, just like singleMode instance._masonryPlaceBrick($this, props.colYs); } else { // brick spans more than one column // how many different places could this brick fit horizontally var groupCount = props.cols + 1 - colSpan, groupY = [], groupColY, i; // for each group potential horizontal position for (i = 0; i < groupCount; i++) { // make an array of colY values for that one group groupColY = props.colYs.slice(i, i + colSpan); // and get the max value of the array groupY[i] = Math.max.apply(Math, groupColY); } instance._masonryPlaceBrick($this, groupY); } }); }, // worker method that places brick in the columnSet // with the the minY _masonryPlaceBrick: function($brick, setY ) { // get the minimum Y value from the columns var minimumY = Math.min.apply(Math, setY), shortCol = 0; // Find index of short column, the first from the left for (var i = 0, len = setY.length; i < len; i++) { if (setY[i] === minimumY) { shortCol = i; break; } } // position the brick var x = this.masonry.columnWidth * shortCol, y = minimumY; this._pushPosition($brick, x, y); // apply setHeight to necessary columns var setHeight = minimumY + $brick.outerHeight(true), setSpan = this.masonry.cols + 1 - len; for (i = 0; i < setSpan; i++) { this.masonry.colYs[shortCol + i] = setHeight; } }, _masonryGetContainerSize: function() { var containerHeight = Math.max.apply(Math, this.masonry.colYs); return { height: containerHeight }; }, _masonryResizeChanged: function() { return this._checkIfSegmentsChanged(); }, // ====================== fitRows ====================== _fitRowsReset: function() { this.fitRows = { x: 0, y: 0, height: 0 }; }, _fitRowsLayout: function($elems ) { var instance = this, containerWidth = this.element.width(), props = this.fitRows; $elems.each(function() { var $this = $(this), atomW = $this.outerWidth(true), atomH = $this.outerHeight(true); if (props.x !== 0 && atomW + props.x > containerWidth) { // if this element cannot fit in the current row props.x = 0; props.y = props.height; } // position the atom instance._pushPosition($this, props.x, props.y); props.height = Math.max(props.y + atomH, props.height); props.x += atomW; }); }, _fitRowsGetContainerSize: function() { return { height: this.fitRows.height }; }, _fitRowsResizeChanged: function() { return true; }, // ====================== cellsByRow ====================== _cellsByRowReset: function() { this.cellsByRow = { index: 0 }; // get this.cellsByRow.columnWidth this._getSegments(); // get this.cellsByRow.rowHeight this._getSegments(true); }, _cellsByRowLayout: function($elems ) { var instance = this, props = this.cellsByRow; $elems.each(function() { var $this = $(this), col = props.index % props.cols, row = Math.floor(props.index / props.cols), x = (col + 0.5) * props.columnWidth - $this.outerWidth(true) / 2, y = (row + 0.5) * props.rowHeight - $this.outerHeight(true) / 2; instance._pushPosition($this, x, y); props.index++; }); }, _cellsByRowGetContainerSize: function() { return { height: Math.ceil(this.$filteredAtoms.length / this.cellsByRow.cols) * this.cellsByRow.rowHeight + this.offset.top }; }, _cellsByRowResizeChanged: function() { return this._checkIfSegmentsChanged(); }, // ====================== straightDown ====================== _straightDownReset: function() { this.straightDown = { y: 0 }; }, _straightDownLayout: function($elems ) { var instance = this; $elems.each(function(i ) { var $this = $(this); instance._pushPosition($this, 0, instance.straightDown.y); instance.straightDown.y += $this.outerHeight(true); }); }, _straightDownGetContainerSize: function() { return { height: this.straightDown.y }; }, _straightDownResizeChanged: function() { return true; }, // ====================== masonryHorizontal ====================== _masonryHorizontalReset: function() { // layout-specific props this.masonryHorizontal = {}; // FIXME shouldn't have to call this again this._getSegments(true); var i = this.masonryHorizontal.rows; this.masonryHorizontal.rowXs = []; while (i--) { this.masonryHorizontal.rowXs.push(0); } }, _masonryHorizontalLayout: function($elems ) { var instance = this, props = instance.masonryHorizontal; $elems.each(function() { var $this = $(this), //how many rows does this brick span rowSpan = Math.ceil($this.outerHeight(true) / props.rowHeight); rowSpan = Math.min(rowSpan, props.rows); if (rowSpan === 1) { // if brick spans only one column, just like singleMode instance._masonryHorizontalPlaceBrick($this, props.rowXs); } else { // brick spans more than one row // how many different places could this brick fit horizontally var groupCount = props.rows + 1 - rowSpan, groupX = [], groupRowX, i; // for each group potential horizontal position for (i = 0; i < groupCount; i++) { // make an array of colY values for that one group groupRowX = props.rowXs.slice(i, i + rowSpan); // and get the max value of the array groupX[i] = Math.max.apply(Math, groupRowX); } instance._masonryHorizontalPlaceBrick($this, groupX); } }); }, _masonryHorizontalPlaceBrick: function($brick, setX ) { // get the minimum Y value from the columns var minimumX = Math.min.apply(Math, setX), smallRow = 0; // Find index of smallest row, the first from the top for (var i = 0, len = setX.length; i < len; i++) { if (setX[i] === minimumX) { smallRow = i; break; } } // position the brick var x = minimumX, y = this.masonryHorizontal.rowHeight * smallRow; this._pushPosition($brick, x, y); // apply setHeight to necessary columns var setWidth = minimumX + $brick.outerWidth(true), setSpan = this.masonryHorizontal.rows + 1 - len; for (i = 0; i < setSpan; i++) { this.masonryHorizontal.rowXs[smallRow + i] = setWidth; } }, _masonryHorizontalGetContainerSize: function() { var containerWidth = Math.max.apply(Math, this.masonryHorizontal.rowXs); return { width: containerWidth }; }, _masonryHorizontalResizeChanged: function() { return this._checkIfSegmentsChanged(true); }, // ====================== fitColumns ====================== _fitColumnsReset: function() { this.fitColumns = { x: 0, y: 0, width: 0 }; }, _fitColumnsLayout: function($elems ) { var instance = this, containerHeight = this.element.height(), props = this.fitColumns; $elems.each(function() { var $this = $(this), atomW = $this.outerWidth(true), atomH = $this.outerHeight(true); if (props.y !== 0 && atomH + props.y > containerHeight) { // if this element cannot fit in the current column props.x = props.width; props.y = 0; } // position the atom instance._pushPosition($this, props.x, props.y); props.width = Math.max(props.x + atomW, props.width); props.y += atomH; }); }, _fitColumnsGetContainerSize: function() { return { width: this.fitColumns.width }; }, _fitColumnsResizeChanged: function() { return true; }, // ====================== cellsByColumn ====================== _cellsByColumnReset: function() { this.cellsByColumn = { index: 0 }; // get this.cellsByColumn.columnWidth this._getSegments(); // get this.cellsByColumn.rowHeight this._getSegments(true); }, _cellsByColumnLayout: function($elems ) { var instance = this, props = this.cellsByColumn; $elems.each(function() { var $this = $(this), col = Math.floor(props.index / props.rows), row = props.index % props.rows, x = (col + 0.5) * props.columnWidth - $this.outerWidth(true) / 2, y = (row + 0.5) * props.rowHeight - $this.outerHeight(true) / 2; instance._pushPosition($this, x, y); props.index++; }); }, _cellsByColumnGetContainerSize: function() { return { width: Math.ceil(this.$filteredAtoms.length / this.cellsByColumn.rows) * this.cellsByColumn.columnWidth }; }, _cellsByColumnResizeChanged: function() { return this._checkIfSegmentsChanged(true); }, // ====================== straightAcross ====================== _straightAcrossReset: function() { this.straightAcross = { x: 0 }; }, _straightAcrossLayout: function($elems ) { var instance = this; $elems.each(function(i ) { var $this = $(this); instance._pushPosition($this, instance.straightAcross.x, 0); instance.straightAcross.x += $this.outerWidth(true); }); }, _straightAcrossGetContainerSize: function() { return { width: this.straightAcross.x }; }, _straightAcrossResizeChanged: function() { return true; } }; // ======================= imagesLoaded Plugin =============================== /*! * jQuery imagesLoaded plugin v1.1.0 * http://github.com/desandro/imagesloaded * * MIT License. by Paul Irish et al. */ // $('#my-container').imagesLoaded(myFunction) // or // $('img').imagesLoaded(myFunction) // execute a callback when all images have loaded. // needed because .load() doesn't work on cached images // callback function gets image collection as argument // `this` is the container $.fn.imagesLoaded = function(callback ) { var $this = this, $images = $this.find('img').add($this.filter('img')), len = $images.length, blank = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==', loaded = []; function triggerCallback() { callback.call($this, $images); } function imgLoaded(event ) { var img = event.target; if (img.src !== blank && $.inArray(img, loaded) === -1) { loaded.push(img); if (--len <= 0) { setTimeout(triggerCallback); $images.unbind('.imagesLoaded', imgLoaded); } } } // if no images, trigger immediately if (!len) { triggerCallback(); } $images.bind('load.imagesLoaded error.imagesLoaded', imgLoaded).each(function() { // cached images don't fire load sometimes, so we reset src. var src = this.src; // webkit hack from http://groups.google.com/group/jquery-dev/browse_thread/thread/eee6ab7b2da50e1f // data uri bypasses webkit log warning (thx doug jones) this.src = blank; this.src = src; }); return $this; }; // helper function for logging errors // $.error breaks jQuery chaining var logError = function(message ) { if (window.console) { window.console.error(message); } }; // ======================= Plugin bridge =============================== // leverages data method to either create or return $.Isotope constructor // A bit from jQuery UI // https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js // A bit from jcarousel // https://github.com/jsor/jcarousel/blob/master/lib/jquery.jcarousel.js $.fn.isotope = function(options, callback ) { if (typeof options === 'string') { // call method var args = Array.prototype.slice.call(arguments, 1); this.each(function() { var instance = $.data(this, 'isotope'); if (!instance) { logError('cannot call methods on isotope prior to initialization; ' + "attempted to call method '" + options + "'"); return; } if (!$.isFunction(instance[options]) || options.charAt(0) === '_') { logError("no such method '" + options + "' for isotope instance"); return; } // apply method instance[options].apply(instance, args); }); } else { this.each(function() { var instance = $.data(this, 'isotope'); if (instance) { // apply options & init instance.option(options); instance._init(callback); } else { // initialize new instance $.data(this, 'isotope', new $.Isotope(options, this, callback)); } }); } // return jQuery object // so plugin methods do not have to return this; }; })(window, jQuery);