From: dsc Date: Mon, 20 Feb 2012 23:22:50 +0000 (-0800) Subject: Adds isotope for options frobbing. X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=0e29aa8c8ff8e76e3c860ad60f72875309e64973;p=limn.git Adds isotope for options frobbing. --- diff --git a/static/vendor/jquery.isotope-1.5.11.js b/static/vendor/jquery.isotope-1.5.11.js new file mode 100644 index 0000000..8f4f38d --- /dev/null +++ b/static/vendor/jquery.isotope-1.5.11.js @@ -0,0 +1,1391 @@ +/** + * Isotope v1.5.11 + * 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 curly: true, eqeqeq: true, forin: false, immed: false, newcap: true, noempty: true, undef: true */ +/*global window: true, jQuery: true */ + +(function( window, $, undefined ){ + + // 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', + 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, + 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() { + jQuery.event.handle.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 = [ 'overflow', 'position', '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 }, + animationEngine: 'best-available', + animationOptions: { + queue: false, + duration: 800 + }, + sortBy : 'original-order', + sortAscending : true, + resizesContainer : true, + transformsEnabled: !$.browser.opera, // disable transforms in Opera + 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 = {}; + for ( var i=0, len = isoContainerStyles.length; i < len; i++ ) { + var prop = isoContainerStyles[i]; + this.originalStyle[ prop ] = elemStyle[ prop ] || ''; + } + + this.element.css({ + overflow : 'hidden', + position : 'relative' + }); + + 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'), 10 ), + top: parseInt( this.element.css('padding-top'), 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' }; + + 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 += this.offset.left; + 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 ); + } + } + 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, + testElem = this.styleQueue[0].$el, + styleObj; + // get first non-empty jQ object + while ( !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 from Isotope instance in callback + var instance = this; + var removeContent = function() { + instance.$allAtoms = instance.$allAtoms.not( $content ); + $content.remove(); + }; + + 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.$filteredAtoms = this.$filteredAtoms.not( $content ); + this._sort(); + this.reLayout( removeContent, callback ); + } else { + // remove it now + removeContent(); + if ( callback ) { + callback.call( this.element ); + } + } + + }, + + 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 i=0, len = isoContainerStyles.length; i < len; i++ ) { + var prop = isoContainerStyles[i]; + 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 = Math.round( ( col + 0.5 ) * props.columnWidth - $this.outerWidth(true) / 2 ), + y = Math.round( ( 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 = Math.round( ( col + 0.5 ) * props.columnWidth - $this.outerWidth(true) / 2 ), + y = Math.round( ( 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 ) { + if ( event.target.src !== blank && $.inArray( this, loaded ) === -1 ){ + loaded.push(this); + 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 ); \ No newline at end of file diff --git a/static/vendor/jquery.isotope-1.5.11.min.js b/static/vendor/jquery.isotope-1.5.11.min.js new file mode 100644 index 0000000..1c28099 --- /dev/null +++ b/static/vendor/jquery.isotope-1.5.11.min.js @@ -0,0 +1,11 @@ +/** + * Isotope v1.5.11 + * 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 + */ +(function(a,b,c){var d=a.document,e=a.Modernizr,f=function(a){return a.charAt(0).toUpperCase()+a.slice(1)},g="Moz Webkit O Ms".split(" "),h=function(a){var b=d.documentElement.style,c;if(typeof b[a]=="string")return a;a=f(a);for(var e=0,h=g.length;e"+d+"{#modernizr{height:3px}}"+"").appendTo("head"),f=b('
').appendTo("html");a=f.height()===3,f.remove(),e.remove()}return a},csstransitions:function(){return!!j}},l;if(e)for(l in k)e.hasOwnProperty(l)||e.addTest(l,k[l]);else{e=a.Modernizr={_version:"1.6ish: miniModernizr for Isotope"};var m=" ",n;for(l in k)n=k[l](),e[l]=n,m+=" "+(n?"":"no-")+l;b("html").addClass(m)}if(e.csstransforms){var o=e.csstransforms3d?{translate:function(a){return"translate3d("+a[0]+"px, "+a[1]+"px, 0) "},scale:function(a){return"scale3d("+a+", "+a+", 1) "}}:{translate:function(a){return"translate("+a[0]+"px, "+a[1]+"px) "},scale:function(a){return"scale("+a+") "}},p=function(a,c,d){var e=b.data(a,"isoTransform")||{},f={},g,h={},j;f[c]=d,b.extend(e,f);for(g in e)j=e[g],h[g]=o[g](j);var k=h.translate||"",l=h.scale||"",m=k+l;b.data(a,"isoTransform",e),a.style[i]=m};b.cssNumber.scale=!0,b.cssHooks.scale={set:function(a,b){p(a,"scale",b)},get:function(a,c){var d=b.data(a,"isoTransform");return d&&d.scale?d.scale:1}},b.fx.step.scale=function(a){b.cssHooks.scale.set(a.elem,a.now+a.unit)},b.cssNumber.translate=!0,b.cssHooks.translate={set:function(a,b){p(a,"translate",b)},get:function(a,c){var d=b.data(a,"isoTransform");return d&&d.translate?d.translate:[0,0]}}}var q,r;e.csstransitions&&(q={WebkitTransitionProperty:"webkitTransitionEnd",MozTransitionProperty:"transitionend",OTransitionProperty:"oTransitionEnd",transitionProperty:"transitionEnd"}[j],r=h("transitionDuration"));var s=b.event,t;s.special.smartresize={setup:function(){b(this).bind("resize",s.special.smartresize.handler)},teardown:function(){b(this).unbind("resize",s.special.smartresize.handler)},handler:function(a,b){var c=this,d=arguments;a.type="smartresize",t&&clearTimeout(t),t=setTimeout(function(){jQuery.event.handle.apply(c,d)},b==="execAsap"?0:100)}},b.fn.smartresize=function(a){return a?this.bind("smartresize",a):this.trigger("smartresize",["execAsap"])},b.Isotope=function(a,c,d){this.element=b(c),this._create(a),this._init(d)};var u=["overflow","position","width","height"],v=b(a);b.Isotope.settings={resizable:!0,layoutMode:"masonry",containerClass:"isotope",itemClass:"isotope-item",hiddenClass:"isotope-hidden",hiddenStyle:{opacity:0,scale:.001},visibleStyle:{opacity:1,scale:1},animationEngine:"best-available",animationOptions:{queue:!1,duration:800},sortBy:"original-order",sortAscending:!0,resizesContainer:!0,transformsEnabled:!b.browser.opera,itemPositionDataEnabled:!1},b.Isotope.prototype={_create:function(a){this.options=b.extend({},b.Isotope.settings,a),this.styleQueue=[],this.elemCount=0;var c=this.element[0].style;this.originalStyle={};for(var d=0,e=u.length;dg?1:f0&&(i=function(a,b){b.$el[d](b.style,f).one(q,k)},j=!1)}}b.each(this.styleQueue,i),j&&k(),this.styleQueue=[]},resize:function(){this["_"+this.options.layoutMode+"ResizeChanged"]()&&this.reLayout()},reLayout:function(a){this["_"+this.options.layoutMode+"Reset"](),this.layout(this.$filteredAtoms,a)},addItems:function(a,b){var c=this._getAtoms(a);this.$allAtoms=this.$allAtoms.add(c),b&&b(c)},insert:function(a,b){this.element.append(a);var c=this;this.addItems(a,function(a){var d=c._filter(a);c._addHideAppended(d),c._sort(),c.reLayout(),c._revealAppended(d,b)})},appended:function(a,b){var c=this;this.addItems(a,function(a){c._addHideAppended(a),c.layout(a),c._revealAppended(a,b)})},_addHideAppended:function(a){this.$filteredAtoms=this.$filteredAtoms.add(a),a.addClass("no-transition"),this._isInserting=!0,this.styleQueue.push({$el:a,style:this.options.hiddenStyle})},_revealAppended:function(a,b){var c=this;setTimeout(function(){a.removeClass("no-transition"),c.styleQueue.push({$el:a,style:c.options.visibleStyle}),c._isInserting=!1,c._processStyleQueue(a,b)},10)},reloadItems:function(){this.$allAtoms=this._getAtoms(this.element.children())},remove:function(a,b){var c=this,d=function(){c.$allAtoms=c.$allAtoms.not(a),a.remove()};a.filter(":not(."+this.options.hiddenClass+")").length?(this.styleQueue.push({$el:a,style:this.options.hiddenStyle}),this.$filteredAtoms=this.$filteredAtoms.not(a),this._sort(),this.reLayout(d,b)):(d(),b&&b.call(this.element))},shuffle:function(a){this.updateSortData(this.$allAtoms),this.options.sortBy="random",this._sort(),this.reLayout(a)},destroy:function(){var a=this.usingTransforms,b=this.options;this.$allAtoms.removeClass(b.hiddenClass+" "+b.itemClass).each(function(){var b=this.style;b.position="",b.top="",b.left="",b.opacity="",a&&(b[i]="")});var c=this.element[0].style;for(var d=0,e=u.length;dd&&(e.x=0,e.y=e.height),c._pushPosition(a,e.x,e.y),e.height=Math.max(e.y+g,e.height),e.x+=f})},_fitRowsGetContainerSize:function(){return{height:this.fitRows.height}},_fitRowsResizeChanged:function(){return!0},_cellsByRowReset:function(){this.cellsByRow={index:0},this._getSegments(),this._getSegments(!0)},_cellsByRowLayout:function(a){var c=this,d=this.cellsByRow;a.each(function(){var a=b(this),e=d.index%d.cols,f=Math.floor(d.index/d.cols),g=Math.round((e+.5)*d.columnWidth-a.outerWidth(!0)/2),h=Math.round((f+.5)*d.rowHeight-a.outerHeight(!0)/2);c._pushPosition(a,g,h),d.index++})},_cellsByRowGetContainerSize:function(){return{height:Math.ceil(this.$filteredAtoms.length/this.cellsByRow.cols)*this.cellsByRow.rowHeight+this.offset.top}},_cellsByRowResizeChanged:function(){return this._checkIfSegmentsChanged()},_straightDownReset:function(){this.straightDown={y:0}},_straightDownLayout:function(a){var c=this;a.each(function(a){var d=b(this);c._pushPosition(d,0,c.straightDown.y),c.straightDown.y+=d.outerHeight(!0)})},_straightDownGetContainerSize:function(){return{height:this.straightDown.y}},_straightDownResizeChanged:function(){return!0},_masonryHorizontalReset:function(){this.masonryHorizontal={},this._getSegments(!0);var a=this.masonryHorizontal.rows;this.masonryHorizontal.rowXs=[];while(a--)this.masonryHorizontal.rowXs.push(0)},_masonryHorizontalLayout:function(a){var c=this,d=c.masonryHorizontal;a.each(function(){var a=b(this),e=Math.ceil(a.outerHeight(!0)/d.rowHeight);e=Math.min(e,d.rows);if(e===1)c._masonryHorizontalPlaceBrick(a,d.rowXs);else{var f=d.rows+1-e,g=[],h,i;for(i=0;id&&(e.x=e.width,e.y=0),c._pushPosition(a,e.x,e.y),e.width=Math.max(e.x+f,e.width),e.y+=g})},_fitColumnsGetContainerSize:function(){return{width:this.fitColumns.width}},_fitColumnsResizeChanged:function(){return!0},_cellsByColumnReset:function(){this.cellsByColumn={index:0},this._getSegments(),this._getSegments(!0)},_cellsByColumnLayout:function(a){var c=this,d=this.cellsByColumn;a.each(function(){var a=b(this),e=Math.floor(d.index/d.rows),f=d.index%d.rows,g=Math.round((e+.5)*d.columnWidth-a.outerWidth(!0)/2),h=Math.round((f+.5)*d.rowHeight-a.outerHeight(!0)/2);c._pushPosition(a,g,h),d.index++})},_cellsByColumnGetContainerSize:function(){return{width:Math.ceil(this.$filteredAtoms.length/this.cellsByColumn.rows)*this.cellsByColumn.columnWidth}},_cellsByColumnResizeChanged:function(){return this._checkIfSegmentsChanged(!0)},_straightAcrossReset:function(){this.straightAcross={x:0}},_straightAcrossLayout:function(a){var c=this;a.each(function(a){var d=b(this);c._pushPosition(d,c.straightAcross.x,0),c.straightAcross.x+=d.outerWidth(!0)})},_straightAcrossGetContainerSize:function(){return{width:this.straightAcross.x}},_straightAcrossResizeChanged:function(){return!0}},b.fn.imagesLoaded=function(a){function i(a){a.target.src!==f&&b.inArray(this,g)===-1&&(g.push(this),--e<=0&&(setTimeout(h),d.unbind(".imagesLoaded",i)))}function h(){a.call(c,d)}var c=this,d=c.find("img").add(c.filter("img")),e=d.length,f="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==",g=[];e||h(),d.bind("load.imagesLoaded error.imagesLoaded",i).each(function(){var a=this.src;this.src=f,this.src=a});return c};var w=function(b){a.console&&a.console.error(b)};b.fn.isotope=function(a,c){if(typeof a=="string"){var d=Array.prototype.slice.call(arguments,1);this.each(function(){var c=b.data(this,"isotope");if(!c)w("cannot call methods on isotope prior to initialization; attempted to call method '"+a+"'");else{if(!b.isFunction(c[a])||a.charAt(0)==="_"){w("no such method '"+a+"' for isotope instance");return}c[a].apply(c,d)}})}else this.each(function(){var d=b.data(this,"isotope");d?(d.option(a),d._init(c)):b.data(this,"isotope",new b.Isotope(a,this,c))});return this}})(window,jQuery); diff --git a/static/vendor/jquery.isotope.js b/static/vendor/jquery.isotope.js new file mode 120000 index 0000000..f1c0ac5 --- /dev/null +++ b/static/vendor/jquery.isotope.js @@ -0,0 +1 @@ +jquery.isotope-1.5.11.js \ No newline at end of file diff --git a/static/vendor/jquery.isotope.min.js b/static/vendor/jquery.isotope.min.js new file mode 120000 index 0000000..50c20ff --- /dev/null +++ b/static/vendor/jquery.isotope.min.js @@ -0,0 +1 @@ +jquery.isotope-1.5.11.min.js \ No newline at end of file