diff options
Diffstat (limited to 'www')
| -rw-r--r-- | www/css/style.css | 84 | ||||
| -rw-r--r-- | www/external/draggabilly.pkgd.js | 1530 | ||||
| -rw-r--r-- | www/external/imagesloaded.pkgd.js | 487 | ||||
| -rw-r--r-- | www/external/packery.pkgd.js | 3349 | ||||
| -rw-r--r-- | www/index.html | 31 | ||||
| -rw-r--r-- | www/js/MontageCtrl.js | 89 | ||||
| -rw-r--r-- | www/templates/montage.html | 86 |
7 files changed, 5585 insertions, 71 deletions
diff --git a/www/css/style.css b/www/css/style.css index 589bf7e6..d93a3352 100644 --- a/www/css/style.css +++ b/www/css/style.css @@ -194,6 +194,43 @@ Credit: https://css-tricks.com/snippets/css/a-guide-to-flexbox/ } +figure { + position: relative; +} + +figure img { + display: block; + width: 100%; + height: auto; +} +figcaption { + +} + +.normal-figcaption { + background: #575656; + color: #FFF; + position: absolute; + bottom: 0; + left: 0; + right: 0; + opacity:0.7; +} + +.alarmed-figcaption { + + background: #ba3e3e; + color:#ffffff; + position:absolute; + bottom: 0; + left: 0; + right: 0; + opacity:0.7; + + +} + + /* modified from: @@ -738,3 +775,50 @@ input[type=range]::-webkit-slider-thumb { -webkit-animation-delay: 1s; } +/* packery stuff */ + + + +* { box-sizing: border-box; } + +body { font-family: sans-serif; } + +/* ---- grid ---- */ + + +.grid-sizer, +.grid-item { width: 20%; } + +. +/* clear fix */ +.grid:after { + content: ''; + display: block; + clear: both; +} + + +.grid-item img { + display: block; + max-width: 100%; +} +/* ---- .grid-item ---- */ + + +.grid-item:hover { + border-color: hsla(0, 0%, 100%, 0.5); + cursor: move; +} + +.grid-item.is-dragging, +.grid-item.is-positioning-post-drag { + background: #C90; + z-index: 2; +} + +.packery-drop-placeholder { + outline: 3px dashed hsla(0, 0%, 0%, 0.5); + outline-offset: -6px; + -webkit-transition: -webkit-transform 0.2s; + transition: transform 0.2s; +}
\ No newline at end of file diff --git a/www/external/draggabilly.pkgd.js b/www/external/draggabilly.pkgd.js new file mode 100644 index 00000000..bef3db35 --- /dev/null +++ b/www/external/draggabilly.pkgd.js @@ -0,0 +1,1530 @@ +/*! + * Draggabilly PACKAGED v2.1.0 + * Make that shiz draggable + * http://draggabilly.desandro.com + * MIT license + */ + +/** + * Bridget makes jQuery widgets + * v2.0.0 + * MIT license + */ + +/* jshint browser: true, strict: true, undef: true, unused: true */ + +( function( window, factory ) { + + /* globals define: false, module: false, require: false */ + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'jquery-bridget/jquery-bridget',[ 'jquery' ], function( jQuery ) { + factory( window, jQuery ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('jquery') + ); + } else { + // browser global + window.jQueryBridget = factory( + window, + window.jQuery + ); + } + +}( window, function factory( window, jQuery ) { + + +// ----- utils ----- // + +var arraySlice = Array.prototype.slice; + +// helper function for logging errors +// $.error breaks jQuery chaining +var console = window.console; +var logError = typeof console == 'undefined' ? function() {} : + function( message ) { + console.error( message ); + }; + +// ----- jQueryBridget ----- // + +function jQueryBridget( namespace, PluginClass, $ ) { + $ = $ || jQuery || window.jQuery; + if ( !$ ) { + return; + } + + // add option method -> $().plugin('option', {...}) + if ( !PluginClass.prototype.option ) { + // option setter + PluginClass.prototype.option = function( opts ) { + // bail out if not an object + if ( !$.isPlainObject( opts ) ){ + return; + } + this.options = $.extend( true, this.options, opts ); + }; + } + + // make jQuery plugin + $.fn[ namespace ] = function( arg0 /*, arg1 */ ) { + if ( typeof arg0 == 'string' ) { + // method call $().plugin( 'methodName', { options } ) + // shift arguments by 1 + var args = arraySlice.call( arguments, 1 ); + return methodCall( this, arg0, args ); + } + // just $().plugin({ options }) + plainCall( this, arg0 ); + return this; + }; + + // $().plugin('methodName') + function methodCall( $elems, methodName, args ) { + var returnValue; + var pluginMethodStr = '$().' + namespace + '("' + methodName + '")'; + + $elems.each( function( i, elem ) { + // get instance + var instance = $.data( elem, namespace ); + if ( !instance ) { + logError( namespace + ' not initialized. Cannot call methods, i.e. ' + + pluginMethodStr ); + return; + } + + var method = instance[ methodName ]; + if ( !method || methodName.charAt(0) == '_' ) { + logError( pluginMethodStr + ' is not a valid method' ); + return; + } + + // apply method, get return value + var value = method.apply( instance, args ); + // set return value if value is returned, use only first value + returnValue = returnValue === undefined ? value : returnValue; + }); + + return returnValue !== undefined ? returnValue : $elems; + } + + function plainCall( $elems, options ) { + $elems.each( function( i, elem ) { + var instance = $.data( elem, namespace ); + if ( instance ) { + // set options & init + instance.option( options ); + instance._init(); + } else { + // initialize new instance + instance = new PluginClass( elem, options ); + $.data( elem, namespace, instance ); + } + }); + } + + updateJQuery( $ ); + +} + +// ----- updateJQuery ----- // + +// set $.bridget for v1 backwards compatibility +function updateJQuery( $ ) { + if ( !$ || ( $ && $.bridget ) ) { + return; + } + $.bridget = jQueryBridget; +} + +updateJQuery( jQuery || window.jQuery ); + +// ----- ----- // + +return jQueryBridget; + +})); + +/*! + * getSize v2.0.2 + * measure size of elements + * MIT license + */ + +/*jshint browser: true, strict: true, undef: true, unused: true */ +/*global define: false, module: false, console: false */ + +( function( window, factory ) { + + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'get-size/get-size',[],function() { + return factory(); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory(); + } else { + // browser global + window.getSize = factory(); + } + +})( window, function factory() { + + +// -------------------------- helpers -------------------------- // + +// get a number from a string, not a percentage +function getStyleSize( value ) { + var num = parseFloat( value ); + // not a percent like '100%', and a number + var isValid = value.indexOf('%') == -1 && !isNaN( num ); + return isValid && num; +} + +function noop() {} + +var logError = typeof console == 'undefined' ? noop : + function( message ) { + console.error( message ); + }; + +// -------------------------- measurements -------------------------- // + +var measurements = [ + 'paddingLeft', + 'paddingRight', + 'paddingTop', + 'paddingBottom', + 'marginLeft', + 'marginRight', + 'marginTop', + 'marginBottom', + 'borderLeftWidth', + 'borderRightWidth', + 'borderTopWidth', + 'borderBottomWidth' +]; + +var measurementsLength = measurements.length; + +function getZeroSize() { + var size = { + width: 0, + height: 0, + innerWidth: 0, + innerHeight: 0, + outerWidth: 0, + outerHeight: 0 + }; + for ( var i=0; i < measurementsLength; i++ ) { + var measurement = measurements[i]; + size[ measurement ] = 0; + } + return size; +} + +// -------------------------- getStyle -------------------------- // + +/** + * getStyle, get style of element, check for Firefox bug + * https://bugzilla.mozilla.org/show_bug.cgi?id=548397 + */ +function getStyle( elem ) { + var style = getComputedStyle( elem ); + if ( !style ) { + logError( 'Style returned ' + style + + '. Are you running this code in a hidden iframe on Firefox? ' + + 'See http://bit.ly/getsizebug1' ); + } + return style; +} + +// -------------------------- setup -------------------------- // + +var isSetup = false; + +var isBoxSizeOuter; + +/** + * setup + * check isBoxSizerOuter + * do on first getSize() rather than on page load for Firefox bug + */ +function setup() { + // setup once + if ( isSetup ) { + return; + } + isSetup = true; + + // -------------------------- box sizing -------------------------- // + + /** + * WebKit measures the outer-width on style.width on border-box elems + * IE & Firefox<29 measures the inner-width + */ + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.padding = '1px 2px 3px 4px'; + div.style.borderStyle = 'solid'; + div.style.borderWidth = '1px 2px 3px 4px'; + div.style.boxSizing = 'border-box'; + + var body = document.body || document.documentElement; + body.appendChild( div ); + var style = getStyle( div ); + + getSize.isBoxSizeOuter = isBoxSizeOuter = getStyleSize( style.width ) == 200; + body.removeChild( div ); + +} + +// -------------------------- getSize -------------------------- // + +function getSize( elem ) { + setup(); + + // use querySeletor if elem is string + if ( typeof elem == 'string' ) { + elem = document.querySelector( elem ); + } + + // do not proceed on non-objects + if ( !elem || typeof elem != 'object' || !elem.nodeType ) { + return; + } + + var style = getStyle( elem ); + + // if hidden, everything is 0 + if ( style.display == 'none' ) { + return getZeroSize(); + } + + var size = {}; + size.width = elem.offsetWidth; + size.height = elem.offsetHeight; + + var isBorderBox = size.isBorderBox = style.boxSizing == 'border-box'; + + // get all measurements + for ( var i=0; i < measurementsLength; i++ ) { + var measurement = measurements[i]; + var value = style[ measurement ]; + var num = parseFloat( value ); + // any 'auto', 'medium' value will be 0 + size[ measurement ] = !isNaN( num ) ? num : 0; + } + + var paddingWidth = size.paddingLeft + size.paddingRight; + var paddingHeight = size.paddingTop + size.paddingBottom; + var marginWidth = size.marginLeft + size.marginRight; + var marginHeight = size.marginTop + size.marginBottom; + var borderWidth = size.borderLeftWidth + size.borderRightWidth; + var borderHeight = size.borderTopWidth + size.borderBottomWidth; + + var isBorderBoxSizeOuter = isBorderBox && isBoxSizeOuter; + + // overwrite width and height if we can get it from style + var styleWidth = getStyleSize( style.width ); + if ( styleWidth !== false ) { + size.width = styleWidth + + // add padding and border unless it's already including it + ( isBorderBoxSizeOuter ? 0 : paddingWidth + borderWidth ); + } + + var styleHeight = getStyleSize( style.height ); + if ( styleHeight !== false ) { + size.height = styleHeight + + // add padding and border unless it's already including it + ( isBorderBoxSizeOuter ? 0 : paddingHeight + borderHeight ); + } + + size.innerWidth = size.width - ( paddingWidth + borderWidth ); + size.innerHeight = size.height - ( paddingHeight + borderHeight ); + + size.outerWidth = size.width + marginWidth; + size.outerHeight = size.height + marginHeight; + + return size; +} + +return getSize; + +}); + +/** + * EvEmitter v1.0.1 + * Lil' event emitter + * MIT License + */ + +/* jshint unused: true, undef: true, strict: true */ + +( function( global, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'ev-emitter/ev-emitter',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory(); + } else { + // Browser globals + global.EvEmitter = factory(); + } + +}( this, function() { + + + +function EvEmitter() {} + +var proto = EvEmitter.prototype; + +proto.on = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // set events hash + var events = this._events = this._events || {}; + // set listeners array + var listeners = events[ eventName ] = events[ eventName ] || []; + // only add once + if ( listeners.indexOf( listener ) == -1 ) { + listeners.push( listener ); + } + + return this; +}; + +proto.once = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // add event + this.on( eventName, listener ); + // set once flag + // set onceEvents hash + var onceEvents = this._onceEvents = this._onceEvents || {}; + // set onceListeners array + var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || []; + // set flag + onceListeners[ listener ] = true; + + return this; +}; + +proto.off = function( eventName, listener ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + var index = listeners.indexOf( listener ); + if ( index != -1 ) { + listeners.splice( index, 1 ); + } + + return this; +}; + +proto.emitEvent = function( eventName, args ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + var i = 0; + var listener = listeners[i]; + args = args || []; + // once stuff + var onceListeners = this._onceEvents && this._onceEvents[ eventName ]; + + while ( listener ) { + var isOnce = onceListeners && onceListeners[ listener ]; + if ( isOnce ) { + // remove listener + // remove before trigger to prevent recursion + this.off( eventName, listener ); + // unset once flag + delete onceListeners[ listener ]; + } + // trigger listener + listener.apply( this, args ); + // get next listener + i += isOnce ? 0 : 1; + listener = listeners[i]; + } + + return this; +}; + +return EvEmitter; + +})); + +/*! + * Unipointer v2.1.0 + * base class for doing one thing with pointer event + * MIT license + */ + +/*jshint browser: true, undef: true, unused: true, strict: true */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /*global define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'unipointer/unipointer',[ + 'ev-emitter/ev-emitter' + ], function( EvEmitter ) { + return factory( window, EvEmitter ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('ev-emitter') + ); + } else { + // browser global + window.Unipointer = factory( + window, + window.EvEmitter + ); + } + +}( window, function factory( window, EvEmitter ) { + + + +function noop() {} + +function Unipointer() {} + +// inherit EvEmitter +var proto = Unipointer.prototype = Object.create( EvEmitter.prototype ); + +proto.bindStartEvent = function( elem ) { + this._bindStartEvent( elem, true ); +}; + +proto.unbindStartEvent = function( elem ) { + this._bindStartEvent( elem, false ); +}; + +/** + * works as unbinder, as you can ._bindStart( false ) to unbind + * @param {Boolean} isBind - will unbind if falsey + */ +proto._bindStartEvent = function( elem, isBind ) { + // munge isBind, default to true + isBind = isBind === undefined ? true : !!isBind; + var bindMethod = isBind ? 'addEventListener' : 'removeEventListener'; + + if ( window.navigator.pointerEnabled ) { + // W3C Pointer Events, IE11. See https://coderwall.com/p/mfreca + elem[ bindMethod ]( 'pointerdown', this ); + } else if ( window.navigator.msPointerEnabled ) { + // IE10 Pointer Events + elem[ bindMethod ]( 'MSPointerDown', this ); + } else { + // listen for both, for devices like Chrome Pixel + elem[ bindMethod ]( 'mousedown', this ); + elem[ bindMethod ]( 'touchstart', this ); + } +}; + +// trigger handler methods for events +proto.handleEvent = function( event ) { + var method = 'on' + event.type; + if ( this[ method ] ) { + this[ method ]( event ); + } +}; + +// returns the touch that we're keeping track of +proto.getTouch = function( touches ) { + for ( var i=0; i < touches.length; i++ ) { + var touch = touches[i]; + if ( touch.identifier == this.pointerIdentifier ) { + return touch; + } + } +}; + +// ----- start event ----- // + +proto.onmousedown = function( event ) { + // dismiss clicks from right or middle buttons + var button = event.button; + if ( button && ( button !== 0 && button !== 1 ) ) { + return; + } + this._pointerDown( event, event ); +}; + +proto.ontouchstart = function( event ) { + this._pointerDown( event, event.changedTouches[0] ); +}; + +proto.onMSPointerDown = +proto.onpointerdown = function( event ) { + this._pointerDown( event, event ); +}; + +/** + * pointer start + * @param {Event} event + * @param {Event or Touch} pointer + */ +proto._pointerDown = function( event, pointer ) { + // dismiss other pointers + if ( this.isPointerDown ) { + return; + } + + this.isPointerDown = true; + // save pointer identifier to match up touch events + this.pointerIdentifier = pointer.pointerId !== undefined ? + // pointerId for pointer events, touch.indentifier for touch events + pointer.pointerId : pointer.identifier; + + this.pointerDown( event, pointer ); +}; + +proto.pointerDown = function( event, pointer ) { + this._bindPostStartEvents( event ); + this.emitEvent( 'pointerDown', [ event, pointer ] ); +}; + +// hash of events to be bound after start event +var postStartEvents = { + mousedown: [ 'mousemove', 'mouseup' ], + touchstart: [ 'touchmove', 'touchend', 'touchcancel' ], + pointerdown: [ 'pointermove', 'pointerup', 'pointercancel' ], + MSPointerDown: [ 'MSPointerMove', 'MSPointerUp', 'MSPointerCancel' ] +}; + +proto._bindPostStartEvents = function( event ) { + if ( !event ) { + return; + } + // get proper events to match start event + var events = postStartEvents[ event.type ]; + // bind events to node + events.forEach( function( eventName ) { + window.addEventListener( eventName, this ); + }, this ); + // save these arguments + this._boundPointerEvents = events; +}; + +proto._unbindPostStartEvents = function() { + // check for _boundEvents, in case dragEnd triggered twice (old IE8 bug) + if ( !this._boundPointerEvents ) { + return; + } + this._boundPointerEvents.forEach( function( eventName ) { + window.removeEventListener( eventName, this ); + }, this ); + + delete this._boundPointerEvents; +}; + +// ----- move event ----- // + +proto.onmousemove = function( event ) { + this._pointerMove( event, event ); +}; + +proto.onMSPointerMove = +proto.onpointermove = function( event ) { + if ( event.pointerId == this.pointerIdentifier ) { + this._pointerMove( event, event ); + } +}; + +proto.ontouchmove = function( event ) { + var touch = this.getTouch( event.changedTouches ); + if ( touch ) { + this._pointerMove( event, touch ); + } +}; + +/** + * pointer move + * @param {Event} event + * @param {Event or Touch} pointer + * @private + */ +proto._pointerMove = function( event, pointer ) { + this.pointerMove( event, pointer ); +}; + +// public +proto.pointerMove = function( event, pointer ) { + this.emitEvent( 'pointerMove', [ event, pointer ] ); +}; + +// ----- end event ----- // + + +proto.onmouseup = function( event ) { + this._pointerUp( event, event ); +}; + +proto.onMSPointerUp = +proto.onpointerup = function( event ) { + if ( event.pointerId == this.pointerIdentifier ) { + this._pointerUp( event, event ); + } +}; + +proto.ontouchend = function( event ) { + var touch = this.getTouch( event.changedTouches ); + if ( touch ) { + this._pointerUp( event, touch ); + } +}; + +/** + * pointer up + * @param {Event} event + * @param {Event or Touch} pointer + * @private + */ +proto._pointerUp = function( event, pointer ) { + this._pointerDone(); + this.pointerUp( event, pointer ); +}; + +// public +proto.pointerUp = function( event, pointer ) { + this.emitEvent( 'pointerUp', [ event, pointer ] ); +}; + +// ----- pointer done ----- // + +// triggered on pointer up & pointer cancel +proto._pointerDone = function() { + // reset properties + this.isPointerDown = false; + delete this.pointerIdentifier; + // remove events + this._unbindPostStartEvents(); + this.pointerDone(); +}; + +proto.pointerDone = noop; + +// ----- pointer cancel ----- // + +proto.onMSPointerCancel = +proto.onpointercancel = function( event ) { + if ( event.pointerId == this.pointerIdentifier ) { + this._pointerCancel( event, event ); + } +}; + +proto.ontouchcancel = function( event ) { + var touch = this.getTouch( event.changedTouches ); + if ( touch ) { + this._pointerCancel( event, touch ); + } +}; + +/** + * pointer cancel + * @param {Event} event + * @param {Event or Touch} pointer + * @private + */ +proto._pointerCancel = function( event, pointer ) { + this._pointerDone(); + this.pointerCancel( event, pointer ); +}; + +// public +proto.pointerCancel = function( event, pointer ) { + this.emitEvent( 'pointerCancel', [ event, pointer ] ); +}; + +// ----- ----- // + +// utility function for getting x/y coords from event +Unipointer.getPointerPoint = function( pointer ) { + return { + x: pointer.pageX, + y: pointer.pageY + }; +}; + +// ----- ----- // + +return Unipointer; + +})); + +/*! + * Unidragger v2.1.0 + * Draggable base class + * MIT license + */ + +/*jshint browser: true, unused: true, undef: true, strict: true */ + +( function( window, factory ) { + // universal module definition + /*jshint strict: false */ /*globals define, module, require */ + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'unidragger/unidragger',[ + 'unipointer/unipointer' + ], function( Unipointer ) { + return factory( window, Unipointer ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('unipointer') + ); + } else { + // browser global + window.Unidragger = factory( + window, + window.Unipointer + ); + } + +}( window, function factory( window, Unipointer ) { + + + +// ----- ----- // + +function noop() {} + +// -------------------------- Unidragger -------------------------- // + +function Unidragger() {} + +// inherit Unipointer & EvEmitter +var proto = Unidragger.prototype = Object.create( Unipointer.prototype ); + +// ----- bind start ----- // + +proto.bindHandles = function() { + this._bindHandles( true ); +}; + +proto.unbindHandles = function() { + this._bindHandles( false ); +}; + +var navigator = window.navigator; +/** + * works as unbinder, as you can .bindHandles( false ) to unbind + * @param {Boolean} isBind - will unbind if falsey + */ +proto._bindHandles = function( isBind ) { + // munge isBind, default to true + isBind = isBind === undefined ? true : !!isBind; + // extra bind logic + var binderExtra; + if ( navigator.pointerEnabled ) { + binderExtra = function( handle ) { + // disable scrolling on the element + handle.style.touchAction = isBind ? 'none' : ''; + }; + } else if ( navigator.msPointerEnabled ) { + binderExtra = function( handle ) { + // disable scrolling on the element + handle.style.msTouchAction = isBind ? 'none' : ''; + }; + } else { + binderExtra = noop; + } + // bind each handle + var bindMethod = isBind ? 'addEventListener' : 'removeEventListener'; + for ( var i=0; i < this.handles.length; i++ ) { + var handle = this.handles[i]; + this._bindStartEvent( handle, isBind ); + binderExtra( handle ); + handle[ bindMethod ]( 'click', this ); + } +}; + +// ----- start event ----- // + +/** + * pointer start + * @param {Event} event + * @param {Event or Touch} pointer + */ +proto.pointerDown = function( event, pointer ) { + // dismiss range sliders + if ( event.target.nodeName == 'INPUT' && event.target.type == 'range' ) { + // reset pointerDown logic + this.isPointerDown = false; + delete this.pointerIdentifier; + return; + } + + this._dragPointerDown( event, pointer ); + // kludge to blur focused inputs in dragger + var focused = document.activeElement; + if ( focused && focused.blur ) { + focused.blur(); + } + // bind move and end events + this._bindPostStartEvents( event ); + this.emitEvent( 'pointerDown', [ event, pointer ] ); +}; + +// base pointer down logic +proto._dragPointerDown = function( event, pointer ) { + // track to see when dragging starts + this.pointerDownPoint = Unipointer.getPointerPoint( pointer ); + + var canPreventDefault = this.canPreventDefaultOnPointerDown( event, pointer ); + if ( canPreventDefault ) { + event.preventDefault(); + } +}; + +// overwriteable method so Flickity can prevent for scrolling +proto.canPreventDefaultOnPointerDown = function( event ) { + // prevent default, unless touchstart or <select> + return event.target.nodeName != 'SELECT'; +}; + +// ----- move event ----- // + +/** + * drag move + * @param {Event} event + * @param {Event or Touch} pointer + */ +proto.pointerMove = function( event, pointer ) { + var moveVector = this._dragPointerMove( event, pointer ); + this.emitEvent( 'pointerMove', [ event, pointer, moveVector ] ); + this._dragMove( event, pointer, moveVector ); +}; + +// base pointer move logic +proto._dragPointerMove = function( event, pointer ) { + var movePoint = Unipointer.getPointerPoint( pointer ); + var moveVector = { + x: movePoint.x - this.pointerDownPoint.x, + y: movePoint.y - this.pointerDownPoint.y + }; + // start drag if pointer has moved far enough to start drag + if ( !this.isDragging && this.hasDragStarted( moveVector ) ) { + this._dragStart( event, pointer ); + } + return moveVector; +}; + +// condition if pointer has moved far enough to start drag +proto.hasDragStarted = function( moveVector ) { + return Math.abs( moveVector.x ) > 3 || Math.abs( moveVector.y ) > 3; +}; + + +// ----- end event ----- // + +/** + * pointer up + * @param {Event} event + * @param {Event or Touch} pointer + */ +proto.pointerUp = function( event, pointer ) { + this.emitEvent( 'pointerUp', [ event, pointer ] ); + this._dragPointerUp( event, pointer ); +}; + +proto._dragPointerUp = function( event, pointer ) { + if ( this.isDragging ) { + this._dragEnd( event, pointer ); + } else { + // pointer didn't move enough for drag to start + this._staticClick( event, pointer ); + } +}; + +// -------------------------- drag -------------------------- // + +// dragStart +proto._dragStart = function( event, pointer ) { + this.isDragging = true; + this.dragStartPoint = Unipointer.getPointerPoint( pointer ); + // prevent clicks + this.isPreventingClicks = true; + + this.dragStart( event, pointer ); +}; + +proto.dragStart = function( event, pointer ) { + this.emitEvent( 'dragStart', [ event, pointer ] ); +}; + +// dragMove +proto._dragMove = function( event, pointer, moveVector ) { + // do not drag if not dragging yet + if ( !this.isDragging ) { + return; + } + + this.dragMove( event, pointer, moveVector ); +}; + +proto.dragMove = function( event, pointer, moveVector ) { + event.preventDefault(); + this.emitEvent( 'dragMove', [ event, pointer, moveVector ] ); +}; + +// dragEnd +proto._dragEnd = function( event, pointer ) { + // set flags + this.isDragging = false; + // re-enable clicking async + setTimeout( function() { + delete this.isPreventingClicks; + }.bind( this ) ); + + this.dragEnd( event, pointer ); +}; + +proto.dragEnd = function( event, pointer ) { + this.emitEvent( 'dragEnd', [ event, pointer ] ); +}; + +// ----- onclick ----- // + +// handle all clicks and prevent clicks when dragging +proto.onclick = function( event ) { + if ( this.isPreventingClicks ) { + event.preventDefault(); + } +}; + +// ----- staticClick ----- // + +// triggered after pointer down & up with no/tiny movement +proto._staticClick = function( event, pointer ) { + // ignore emulated mouse up clicks + if ( this.isIgnoringMouseUp && event.type == 'mouseup' ) { + return; + } + + // allow click in <input>s and <textarea>s + var nodeName = event.target.nodeName; + if ( nodeName == 'INPUT' || nodeName == 'TEXTAREA' ) { + event.target.focus(); + } + this.staticClick( event, pointer ); + + // set flag for emulated clicks 300ms after touchend + if ( event.type != 'mouseup' ) { + this.isIgnoringMouseUp = true; + // reset flag after 300ms + setTimeout( function() { + delete this.isIgnoringMouseUp; + }.bind( this ), 400 ); + } +}; + +proto.staticClick = function( event, pointer ) { + this.emitEvent( 'staticClick', [ event, pointer ] ); +}; + +// ----- utils ----- // + +Unidragger.getPointerPoint = Unipointer.getPointerPoint; + +// ----- ----- // + +return Unidragger; + +})); + +/*! + * Draggabilly v2.1.0 + * Make that shiz draggable + * http://draggabilly.desandro.com + * MIT license + */ + +/*jshint browser: true, strict: true, undef: true, unused: true */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /*globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( [ + 'get-size/get-size', + 'unidragger/unidragger' + ], + function( getSize, Unidragger ) { + return factory( window, getSize, Unidragger ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('get-size'), + require('unidragger') + ); + } else { + // browser global + window.Draggabilly = factory( + window, + window.getSize, + window.Unidragger + ); + } + +}( window, function factory( window, getSize, Unidragger ) { + + + +// vars +var document = window.document; + +function noop() {} + +// -------------------------- helpers -------------------------- // + +// extend objects +function extend( a, b ) { + for ( var prop in b ) { + a[ prop ] = b[ prop ]; + } + return a; +} + +function isElement( obj ) { + return obj instanceof HTMLElement; +} + +// -------------------------- requestAnimationFrame -------------------------- // + +// get rAF, prefixed, if present +var requestAnimationFrame = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; + +// fallback to setTimeout +var lastTime = 0; +if ( !requestAnimationFrame ) { + requestAnimationFrame = function( callback ) { + var currTime = new Date().getTime(); + var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ); + var id = setTimeout( callback, timeToCall ); + lastTime = currTime + timeToCall; + return id; + }; +} + +// -------------------------- support -------------------------- // + +var docElem = document.documentElement; +var transformProperty = typeof docElem.style.transform == 'string' ? + 'transform' : 'WebkitTransform'; + +var jQuery = window.jQuery; + +// -------------------------- -------------------------- // + +function Draggabilly( element, options ) { + // querySelector if string + this.element = typeof element == 'string' ? + document.querySelector( element ) : element; + + if ( jQuery ) { + this.$element = jQuery( this.element ); + } + + // options + this.options = extend( {}, this.constructor.defaults ); + this.option( options ); + + this._create(); +} + +// inherit Unidragger methods +var proto = Draggabilly.prototype = Object.create( Unidragger.prototype ); + +Draggabilly.defaults = { +}; + +/** + * set options + * @param {Object} opts + */ +proto.option = function( opts ) { + extend( this.options, opts ); +}; + +proto._create = function() { + + // properties + this.position = {}; + this._getPosition(); + + this.startPoint = { x: 0, y: 0 }; + this.dragPoint = { x: 0, y: 0 }; + + this.startPosition = extend( {}, this.position ); + + // set relative positioning + var style = getComputedStyle( this.element ); + if ( style.position != 'relative' && style.position != 'absolute' ) { + this.element.style.position = 'relative'; + } + + this.enable(); + this.setHandles(); + +}; + +/** + * set this.handles and bind start events to 'em + */ +proto.setHandles = function() { + this.handles = this.options.handle ? + this.element.querySelectorAll( this.options.handle ) : [ this.element ]; + + this.bindHandles(); +}; + +/** + * emits events via EvEmitter and jQuery events + * @param {String} type - name of event + * @param {Event} event - original event + * @param {Array} args - extra arguments + */ +proto.dispatchEvent = function( type, event, args ) { + var emitArgs = [ event ].concat( args ); + this.emitEvent( type, emitArgs ); + var jQuery = window.jQuery; + // trigger jQuery event + if ( jQuery && this.$element ) { + if ( event ) { + // create jQuery event + var $event = jQuery.Event( event ); + $event.type = type; + this.$element.trigger( $event, args ); + } else { + // just trigger with type if no event available + this.$element.trigger( type, args ); + } + } +}; + +// -------------------------- position -------------------------- // + +// get x/y position from style +Draggabilly.prototype._getPosition = function() { + var style = getComputedStyle( this.element ); + var x = this._getPositionCoord( style.left, 'width' ); + var y = this._getPositionCoord( style.top, 'height' ); + // clean up 'auto' or other non-integer values + this.position.x = isNaN( x ) ? 0 : x; + this.position.y = isNaN( y ) ? 0 : y; + + this._addTransformPosition( style ); +}; + +Draggabilly.prototype._getPositionCoord = function( styleSide, measure ) { + if ( styleSide.indexOf('%') != -1 ) { + // convert percent into pixel for Safari, #75 + var parentSize = getSize( this.element.parentNode ); + return ( parseFloat( styleSide ) / 100 ) * parentSize[ measure ]; + } + + return parseInt( styleSide, 10 ); +}; + +// add transform: translate( x, y ) to position +proto._addTransformPosition = function( style ) { + var transform = style[ transformProperty ]; + // bail out if value is 'none' + if ( transform.indexOf('matrix') !== 0 ) { + return; + } + // split matrix(1, 0, 0, 1, x, y) + var matrixValues = transform.split(','); + // translate X value is in 12th or 4th position + var xIndex = transform.indexOf('matrix3d') === 0 ? 12 : 4; + var translateX = parseInt( matrixValues[ xIndex ], 10 ); + // translate Y value is in 13th or 5th position + var translateY = parseInt( matrixValues[ xIndex + 1 ], 10 ); + this.position.x += translateX; + this.position.y += translateY; +}; + +// -------------------------- events -------------------------- // + +/** + * pointer start + * @param {Event} event + * @param {Event or Touch} pointer + */ +proto.pointerDown = function( event, pointer ) { + this._dragPointerDown( event, pointer ); + // kludge to blur focused inputs in dragger + var focused = document.activeElement; + // do not blur body for IE10, metafizzy/flickity#117 + if ( focused && focused.blur && focused != document.body ) { + focused.blur(); + } + // bind move and end events + this._bindPostStartEvents( event ); + this.element.classList.add('is-pointer-down'); + this.dispatchEvent( 'pointerDown', event, [ pointer ] ); +}; + +/** + * drag move + * @param {Event} event + * @param {Event or Touch} pointer + */ +proto.pointerMove = function( event, pointer ) { + var moveVector = this._dragPointerMove( event, pointer ); + this.dispatchEvent( 'pointerMove', event, [ pointer, moveVector ] ); + this._dragMove( event, pointer, moveVector ); +}; + +/** + * drag start + * @param {Event} event + * @param {Event or Touch} pointer + */ +proto.dragStart = function( event, pointer ) { + if ( !this.isEnabled ) { + return; + } + this._getPosition(); + this.measureContainment(); + // position _when_ drag began + this.startPosition.x = this.position.x; + this.startPosition.y = this.position.y; + // reset left/top style + this.setLeftTop(); + + this.dragPoint.x = 0; + this.dragPoint.y = 0; + + this.element.classList.add('is-dragging'); + this.dispatchEvent( 'dragStart', event, [ pointer ] ); + // start animation + this.animate(); +}; + +proto.measureContainment = function() { + var containment = this.options.containment; + if ( !containment ) { + return; + } + + // use element if element + var container = isElement( containment ) ? containment : + // fallback to querySelector if string + typeof containment == 'string' ? document.querySelector( containment ) : + // otherwise just `true`, use the parent + this.element.parentNode; + + var elemSize = getSize( this.element ); + var containerSize = getSize( container ); + var elemRect = this.element.getBoundingClientRect(); + var containerRect = container.getBoundingClientRect(); + + var borderSizeX = containerSize.borderLeftWidth + containerSize.borderRightWidth; + var borderSizeY = containerSize.borderTopWidth + containerSize.borderBottomWidth; + + var position = this.relativeStartPosition = { + x: elemRect.left - ( containerRect.left + containerSize.borderLeftWidth ), + y: elemRect.top - ( containerRect.top + containerSize.borderTopWidth ) + }; + + this.containSize = { + width: ( containerSize.width - borderSizeX ) - position.x - elemSize.width, + height: ( containerSize.height - borderSizeY ) - position.y - elemSize.height + }; +}; + +// ----- move event ----- // + +/** + * drag move + * @param {Event} event + * @param {Event or Touch} pointer + */ +proto.dragMove = function( event, pointer, moveVector ) { + if ( !this.isEnabled ) { + return; + } + var dragX = moveVector.x; + var dragY = moveVector.y; + + var grid = this.options.grid; + var gridX = grid && grid[0]; + var gridY = grid && grid[1]; + + dragX = applyGrid( dragX, gridX ); + dragY = applyGrid( dragY, gridY ); + + dragX = this.containDrag( 'x', dragX, gridX ); + dragY = this.containDrag( 'y', dragY, gridY ); + + // constrain to axis + dragX = this.options.axis == 'y' ? 0 : dragX; + dragY = this.options.axis == 'x' ? 0 : dragY; + + this.position.x = this.startPosition.x + dragX; + this.position.y = this.startPosition.y + dragY; + // set dragPoint properties + this.dragPoint.x = dragX; + this.dragPoint.y = dragY; + + this.dispatchEvent( 'dragMove', event, [ pointer, moveVector ] ); +}; + +function applyGrid( value, grid, method ) { + method = method || 'round'; + return grid ? Math[ method ]( value / grid ) * grid : value; +} + +proto.containDrag = function( axis, drag, grid ) { + if ( !this.options.containment ) { + return drag; + } + var measure = axis == 'x' ? 'width' : 'height'; + + var rel = this.relativeStartPosition[ axis ]; + var min = applyGrid( -rel, grid, 'ceil' ); + var max = this.containSize[ measure ]; + max = applyGrid( max, grid, 'floor' ); + return Math.min( max, Math.max( min, drag ) ); +}; + +// ----- end event ----- // + +/** + * pointer up + * @param {Event} event + * @param {Event or Touch} pointer + */ +proto.pointerUp = function( event, pointer ) { + this.element.classList.remove('is-pointer-down'); + this.dispatchEvent( 'pointerUp', event, [ pointer ] ); + this._dragPointerUp( event, pointer ); +}; + +/** + * drag end + * @param {Event} event + * @param {Event or Touch} pointer + */ +proto.dragEnd = function( event, pointer ) { + if ( !this.isEnabled ) { + return; + } + // use top left position when complete + if ( transformProperty ) { + this.element.style[ transformProperty ] = ''; + this.setLeftTop(); + } + this.element.classList.remove('is-dragging'); + this.dispatchEvent( 'dragEnd', event, [ pointer ] ); +}; + +// -------------------------- animation -------------------------- // + +proto.animate = function() { + // only render and animate if dragging + if ( !this.isDragging ) { + return; + } + + this.positionDrag(); + + var _this = this; + requestAnimationFrame( function animateFrame() { + _this.animate(); + }); + +}; + +// left/top positioning +proto.setLeftTop = function() { + this.element.style.left = this.position.x + 'px'; + this.element.style.top = this.position.y + 'px'; +}; + +proto.positionDrag = function() { + this.element.style[ transformProperty ] = 'translate3d( ' + this.dragPoint.x + + 'px, ' + this.dragPoint.y + 'px, 0)'; +}; + +// ----- staticClick ----- // + +proto.staticClick = function( event, pointer ) { + this.dispatchEvent( 'staticClick', event, [ pointer ] ); +}; + +// ----- methods ----- // + +proto.enable = function() { + this.isEnabled = true; +}; + +proto.disable = function() { + this.isEnabled = false; + if ( this.isDragging ) { + this.dragEnd(); + } +}; + +proto.destroy = function() { + this.disable(); + // reset styles + this.element.style[ transformProperty ] = ''; + this.element.style.left = ''; + this.element.style.top = ''; + this.element.style.position = ''; + // unbind handles + this.unbindHandles(); + // remove jQuery data + if ( this.$element ) { + this.$element.removeData('draggabilly'); + } +}; + +// ----- jQuery bridget ----- // + +// required for jQuery bridget +proto._init = noop; + +if ( jQuery && jQuery.bridget ) { + jQuery.bridget( 'draggabilly', Draggabilly ); +} + +// ----- ----- // + +return Draggabilly; + +})); + diff --git a/www/external/imagesloaded.pkgd.js b/www/external/imagesloaded.pkgd.js new file mode 100644 index 00000000..ef23971b --- /dev/null +++ b/www/external/imagesloaded.pkgd.js @@ -0,0 +1,487 @@ +/*! + * imagesLoaded PACKAGED v4.1.0 + * JavaScript is all like "You images are done yet or what?" + * MIT License + */ + +/** + * EvEmitter v1.0.1 + * Lil' event emitter + * MIT License + */ + +/* jshint unused: true, undef: true, strict: true */ + +( function( global, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'ev-emitter/ev-emitter',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory(); + } else { + // Browser globals + global.EvEmitter = factory(); + } + +}( this, function() { + + + +function EvEmitter() {} + +var proto = EvEmitter.prototype; + +proto.on = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // set events hash + var events = this._events = this._events || {}; + // set listeners array + var listeners = events[ eventName ] = events[ eventName ] || []; + // only add once + if ( listeners.indexOf( listener ) == -1 ) { + listeners.push( listener ); + } + + return this; +}; + +proto.once = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // add event + this.on( eventName, listener ); + // set once flag + // set onceEvents hash + var onceEvents = this._onceEvents = this._onceEvents || {}; + // set onceListeners array + var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || []; + // set flag + onceListeners[ listener ] = true; + + return this; +}; + +proto.off = function( eventName, listener ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + var index = listeners.indexOf( listener ); + if ( index != -1 ) { + listeners.splice( index, 1 ); + } + + return this; +}; + +proto.emitEvent = function( eventName, args ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + var i = 0; + var listener = listeners[i]; + args = args || []; + // once stuff + var onceListeners = this._onceEvents && this._onceEvents[ eventName ]; + + while ( listener ) { + var isOnce = onceListeners && onceListeners[ listener ]; + if ( isOnce ) { + // remove listener + // remove before trigger to prevent recursion + this.off( eventName, listener ); + // unset once flag + delete onceListeners[ listener ]; + } + // trigger listener + listener.apply( this, args ); + // get next listener + i += isOnce ? 0 : 1; + listener = listeners[i]; + } + + return this; +}; + +return EvEmitter; + +})); + +/*! + * imagesLoaded v4.1.0 + * JavaScript is all like "You images are done yet or what?" + * MIT License + */ + +( function( window, factory ) { 'use strict'; + // universal module definition + + /*global define: false, module: false, require: false */ + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( [ + 'ev-emitter/ev-emitter' + ], function( EvEmitter ) { + return factory( window, EvEmitter ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('ev-emitter') + ); + } else { + // browser global + window.imagesLoaded = factory( + window, + window.EvEmitter + ); + } + +})( window, + +// -------------------------- factory -------------------------- // + +function factory( window, EvEmitter ) { + + + +var $ = window.jQuery; +var console = window.console; + +// -------------------------- helpers -------------------------- // + +// extend objects +function extend( a, b ) { + for ( var prop in b ) { + a[ prop ] = b[ prop ]; + } + return a; +} + +// turn element or nodeList into an array +function makeArray( obj ) { + var ary = []; + if ( Array.isArray( obj ) ) { + // use object if already an array + ary = obj; + } else if ( typeof obj.length == 'number' ) { + // convert nodeList to array + for ( var i=0; i < obj.length; i++ ) { + ary.push( obj[i] ); + } + } else { + // array of single index + ary.push( obj ); + } + return ary; +} + +// -------------------------- imagesLoaded -------------------------- // + +/** + * @param {Array, Element, NodeList, String} elem + * @param {Object or Function} options - if function, use as callback + * @param {Function} onAlways - callback function + */ +function ImagesLoaded( elem, options, onAlways ) { + // coerce ImagesLoaded() without new, to be new ImagesLoaded() + if ( !( this instanceof ImagesLoaded ) ) { + return new ImagesLoaded( elem, options, onAlways ); + } + // use elem as selector string + if ( typeof elem == 'string' ) { + elem = document.querySelectorAll( elem ); + } + + this.elements = makeArray( elem ); + this.options = extend( {}, this.options ); + + if ( typeof options == 'function' ) { + onAlways = options; + } else { + extend( this.options, options ); + } + + if ( onAlways ) { + this.on( 'always', onAlways ); + } + + this.getImages(); + + if ( $ ) { + // add jQuery Deferred object + this.jqDeferred = new $.Deferred(); + } + + // HACK check async to allow time to bind listeners + setTimeout( function() { + this.check(); + }.bind( this )); +} + +ImagesLoaded.prototype = Object.create( EvEmitter.prototype ); + +ImagesLoaded.prototype.options = {}; + +ImagesLoaded.prototype.getImages = function() { + this.images = []; + + // filter & find items if we have an item selector + this.elements.forEach( this.addElementImages, this ); +}; + +/** + * @param {Node} element + */ +ImagesLoaded.prototype.addElementImages = function( elem ) { + // filter siblings + if ( elem.nodeName == 'IMG' ) { + this.addImage( elem ); + } + // get background image on element + if ( this.options.background === true ) { + this.addElementBackgroundImages( elem ); + } + + // find children + // no non-element nodes, #143 + var nodeType = elem.nodeType; + if ( !nodeType || !elementNodeTypes[ nodeType ] ) { + return; + } + var childImgs = elem.querySelectorAll('img'); + // concat childElems to filterFound array + for ( var i=0; i < childImgs.length; i++ ) { + var img = childImgs[i]; + this.addImage( img ); + } + + // get child background images + if ( typeof this.options.background == 'string' ) { + var children = elem.querySelectorAll( this.options.background ); + for ( i=0; i < children.length; i++ ) { + var child = children[i]; + this.addElementBackgroundImages( child ); + } + } +}; + +var elementNodeTypes = { + 1: true, + 9: true, + 11: true +}; + +ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) { + var style = getComputedStyle( elem ); + if ( !style ) { + // Firefox returns null if in a hidden iframe https://bugzil.la/548397 + return; + } + // get url inside url("...") + var reURL = /url\((['"])?(.*?)\1\)/gi; + var matches = reURL.exec( style.backgroundImage ); + while ( matches !== null ) { + var url = matches && matches[2]; + if ( url ) { + this.addBackground( url, elem ); + } + matches = reURL.exec( style.backgroundImage ); + } +}; + +/** + * @param {Image} img + */ +ImagesLoaded.prototype.addImage = function( img ) { + var loadingImage = new LoadingImage( img ); + this.images.push( loadingImage ); +}; + +ImagesLoaded.prototype.addBackground = function( url, elem ) { + var background = new Background( url, elem ); + this.images.push( background ); +}; + +ImagesLoaded.prototype.check = function() { + var _this = this; + this.progressedCount = 0; + this.hasAnyBroken = false; + // complete if no images + if ( !this.images.length ) { + this.complete(); + return; + } + + function onProgress( image, elem, message ) { + // HACK - Chrome triggers event before object properties have changed. #83 + setTimeout( function() { + _this.progress( image, elem, message ); + }); + } + + this.images.forEach( function( loadingImage ) { + loadingImage.once( 'progress', onProgress ); + loadingImage.check(); + }); +}; + +ImagesLoaded.prototype.progress = function( image, elem, message ) { + this.progressedCount++; + this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded; + // progress event + this.emitEvent( 'progress', [ this, image, elem ] ); + if ( this.jqDeferred && this.jqDeferred.notify ) { + this.jqDeferred.notify( this, image ); + } + // check if completed + if ( this.progressedCount == this.images.length ) { + this.complete(); + } + + if ( this.options.debug && console ) { + console.log( 'progress: ' + message, image, elem ); + } +}; + +ImagesLoaded.prototype.complete = function() { + var eventName = this.hasAnyBroken ? 'fail' : 'done'; + this.isComplete = true; + this.emitEvent( eventName, [ this ] ); + this.emitEvent( 'always', [ this ] ); + if ( this.jqDeferred ) { + var jqMethod = this.hasAnyBroken ? 'reject' : 'resolve'; + this.jqDeferred[ jqMethod ]( this ); + } +}; + +// -------------------------- -------------------------- // + +function LoadingImage( img ) { + this.img = img; +} + +LoadingImage.prototype = Object.create( EvEmitter.prototype ); + +LoadingImage.prototype.check = function() { + // If complete is true and browser supports natural sizes, + // try to check for image status manually. + var isComplete = this.getIsImageComplete(); + if ( isComplete ) { + // report based on naturalWidth + this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' ); + return; + } + + // If none of the checks above matched, simulate loading on detached element. + this.proxyImage = new Image(); + this.proxyImage.addEventListener( 'load', this ); + this.proxyImage.addEventListener( 'error', this ); + // bind to image as well for Firefox. #191 + this.img.addEventListener( 'load', this ); + this.img.addEventListener( 'error', this ); + this.proxyImage.src = this.img.src; +}; + +LoadingImage.prototype.getIsImageComplete = function() { + return this.img.complete && this.img.naturalWidth !== undefined; +}; + +LoadingImage.prototype.confirm = function( isLoaded, message ) { + this.isLoaded = isLoaded; + this.emitEvent( 'progress', [ this, this.img, message ] ); +}; + +// ----- events ----- // + +// trigger specified handler for event type +LoadingImage.prototype.handleEvent = function( event ) { + var method = 'on' + event.type; + if ( this[ method ] ) { + this[ method ]( event ); + } +}; + +LoadingImage.prototype.onload = function() { + this.confirm( true, 'onload' ); + this.unbindEvents(); +}; + +LoadingImage.prototype.onerror = function() { + this.confirm( false, 'onerror' ); + this.unbindEvents(); +}; + +LoadingImage.prototype.unbindEvents = function() { + this.proxyImage.removeEventListener( 'load', this ); + this.proxyImage.removeEventListener( 'error', this ); + this.img.removeEventListener( 'load', this ); + this.img.removeEventListener( 'error', this ); +}; + +// -------------------------- Background -------------------------- // + +function Background( url, element ) { + this.url = url; + this.element = element; + this.img = new Image(); +} + +// inherit LoadingImage prototype +Background.prototype = Object.create( LoadingImage.prototype ); + +Background.prototype.check = function() { + this.img.addEventListener( 'load', this ); + this.img.addEventListener( 'error', this ); + this.img.src = this.url; + // check if image is already complete + var isComplete = this.getIsImageComplete(); + if ( isComplete ) { + this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' ); + this.unbindEvents(); + } +}; + +Background.prototype.unbindEvents = function() { + this.img.removeEventListener( 'load', this ); + this.img.removeEventListener( 'error', this ); +}; + +Background.prototype.confirm = function( isLoaded, message ) { + this.isLoaded = isLoaded; + this.emitEvent( 'progress', [ this, this.element, message ] ); +}; + +// -------------------------- jQuery -------------------------- // + +ImagesLoaded.makeJQueryPlugin = function( jQuery ) { + jQuery = jQuery || window.jQuery; + if ( !jQuery ) { + return; + } + // set local variable + $ = jQuery; + // $().imagesLoaded() + $.fn.imagesLoaded = function( options, callback ) { + var instance = new ImagesLoaded( this, options, callback ); + return instance.jqDeferred.promise( $(this) ); + }; +}; +// try making plugin +ImagesLoaded.makeJQueryPlugin(); + +// -------------------------- -------------------------- // + +return ImagesLoaded; + +}); + diff --git a/www/external/packery.pkgd.js b/www/external/packery.pkgd.js new file mode 100644 index 00000000..c8353909 --- /dev/null +++ b/www/external/packery.pkgd.js @@ -0,0 +1,3349 @@ +/*! + * Packery PACKAGED v2.0.0 + * Gapless, draggable grid layouts + * + * Licensed GPLv3 for open source use + * or Packery Commercial License for commercial use + * + * http://packery.metafizzy.co + * Copyright 2016 Metafizzy + */ + +/** + * Bridget makes jQuery widgets + * v2.0.0 + * MIT license + */ + +/* jshint browser: true, strict: true, undef: true, unused: true */ + +( function( window, factory ) { + 'use strict'; + /* globals define: false, module: false, require: false */ + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'jquery-bridget/jquery-bridget',[ 'jquery' ], function( jQuery ) { + factory( window, jQuery ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('jquery') + ); + } else { + // browser global + window.jQueryBridget = factory( + window, + window.jQuery + ); + } + +}( window, function factory( window, jQuery ) { +'use strict'; + +// ----- utils ----- // + +var arraySlice = Array.prototype.slice; + +// helper function for logging errors +// $.error breaks jQuery chaining +var console = window.console; +var logError = typeof console == 'undefined' ? function() {} : + function( message ) { + console.error( message ); + }; + +// ----- jQueryBridget ----- // + +function jQueryBridget( namespace, PluginClass, $ ) { + $ = $ || jQuery || window.jQuery; + if ( !$ ) { + return; + } + + // add option method -> $().plugin('option', {...}) + if ( !PluginClass.prototype.option ) { + // option setter + PluginClass.prototype.option = function( opts ) { + // bail out if not an object + if ( !$.isPlainObject( opts ) ){ + return; + } + this.options = $.extend( true, this.options, opts ); + }; + } + + // make jQuery plugin + $.fn[ namespace ] = function( arg0 /*, arg1 */ ) { + if ( typeof arg0 == 'string' ) { + // method call $().plugin( 'methodName', { options } ) + // shift arguments by 1 + var args = arraySlice.call( arguments, 1 ); + return methodCall( this, arg0, args ); + } + // just $().plugin({ options }) + plainCall( this, arg0 ); + return this; + }; + + // $().plugin('methodName') + function methodCall( $elems, methodName, args ) { + var returnValue; + var pluginMethodStr = '$().' + namespace + '("' + methodName + '")'; + + $elems.each( function( i, elem ) { + // get instance + var instance = $.data( elem, namespace ); + if ( !instance ) { + logError( namespace + ' not initialized. Cannot call methods, i.e. ' + + pluginMethodStr ); + return; + } + + var method = instance[ methodName ]; + if ( !method || methodName.charAt(0) == '_' ) { + logError( pluginMethodStr + ' is not a valid method' ); + return; + } + + // apply method, get return value + var value = method.apply( instance, args ); + // set return value if value is returned, use only first value + returnValue = returnValue === undefined ? value : returnValue; + }); + + return returnValue !== undefined ? returnValue : $elems; + } + + function plainCall( $elems, options ) { + $elems.each( function( i, elem ) { + var instance = $.data( elem, namespace ); + if ( instance ) { + // set options & init + instance.option( options ); + instance._init(); + } else { + // initialize new instance + instance = new PluginClass( elem, options ); + $.data( elem, namespace, instance ); + } + }); + } + + updateJQuery( $ ); + +} + +// ----- updateJQuery ----- // + +// set $.bridget for v1 backwards compatibility +function updateJQuery( $ ) { + if ( !$ || ( $ && $.bridget ) ) { + return; + } + $.bridget = jQueryBridget; +} + +updateJQuery( jQuery || window.jQuery ); + +// ----- ----- // + +return jQueryBridget; + +})); + +/*! + * getSize v2.0.2 + * measure size of elements + * MIT license + */ + +/*jshint browser: true, strict: true, undef: true, unused: true */ +/*global define: false, module: false, console: false */ + +( function( window, factory ) { + 'use strict'; + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'get-size/get-size',[],function() { + return factory(); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory(); + } else { + // browser global + window.getSize = factory(); + } + +})( window, function factory() { +'use strict'; + +// -------------------------- helpers -------------------------- // + +// get a number from a string, not a percentage +function getStyleSize( value ) { + var num = parseFloat( value ); + // not a percent like '100%', and a number + var isValid = value.indexOf('%') == -1 && !isNaN( num ); + return isValid && num; +} + +function noop() {} + +var logError = typeof console == 'undefined' ? noop : + function( message ) { + console.error( message ); + }; + +// -------------------------- measurements -------------------------- // + +var measurements = [ + 'paddingLeft', + 'paddingRight', + 'paddingTop', + 'paddingBottom', + 'marginLeft', + 'marginRight', + 'marginTop', + 'marginBottom', + 'borderLeftWidth', + 'borderRightWidth', + 'borderTopWidth', + 'borderBottomWidth' +]; + +var measurementsLength = measurements.length; + +function getZeroSize() { + var size = { + width: 0, + height: 0, + innerWidth: 0, + innerHeight: 0, + outerWidth: 0, + outerHeight: 0 + }; + for ( var i=0; i < measurementsLength; i++ ) { + var measurement = measurements[i]; + size[ measurement ] = 0; + } + return size; +} + +// -------------------------- getStyle -------------------------- // + +/** + * getStyle, get style of element, check for Firefox bug + * https://bugzilla.mozilla.org/show_bug.cgi?id=548397 + */ +function getStyle( elem ) { + var style = getComputedStyle( elem ); + if ( !style ) { + logError( 'Style returned ' + style + + '. Are you running this code in a hidden iframe on Firefox? ' + + 'See http://bit.ly/getsizebug1' ); + } + return style; +} + +// -------------------------- setup -------------------------- // + +var isSetup = false; + +var isBoxSizeOuter; + +/** + * setup + * check isBoxSizerOuter + * do on first getSize() rather than on page load for Firefox bug + */ +function setup() { + // setup once + if ( isSetup ) { + return; + } + isSetup = true; + + // -------------------------- box sizing -------------------------- // + + /** + * WebKit measures the outer-width on style.width on border-box elems + * IE & Firefox<29 measures the inner-width + */ + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.padding = '1px 2px 3px 4px'; + div.style.borderStyle = 'solid'; + div.style.borderWidth = '1px 2px 3px 4px'; + div.style.boxSizing = 'border-box'; + + var body = document.body || document.documentElement; + body.appendChild( div ); + var style = getStyle( div ); + + getSize.isBoxSizeOuter = isBoxSizeOuter = getStyleSize( style.width ) == 200; + body.removeChild( div ); + +} + +// -------------------------- getSize -------------------------- // + +function getSize( elem ) { + setup(); + + // use querySeletor if elem is string + if ( typeof elem == 'string' ) { + elem = document.querySelector( elem ); + } + + // do not proceed on non-objects + if ( !elem || typeof elem != 'object' || !elem.nodeType ) { + return; + } + + var style = getStyle( elem ); + + // if hidden, everything is 0 + if ( style.display == 'none' ) { + return getZeroSize(); + } + + var size = {}; + size.width = elem.offsetWidth; + size.height = elem.offsetHeight; + + var isBorderBox = size.isBorderBox = style.boxSizing == 'border-box'; + + // get all measurements + for ( var i=0; i < measurementsLength; i++ ) { + var measurement = measurements[i]; + var value = style[ measurement ]; + var num = parseFloat( value ); + // any 'auto', 'medium' value will be 0 + size[ measurement ] = !isNaN( num ) ? num : 0; + } + + var paddingWidth = size.paddingLeft + size.paddingRight; + var paddingHeight = size.paddingTop + size.paddingBottom; + var marginWidth = size.marginLeft + size.marginRight; + var marginHeight = size.marginTop + size.marginBottom; + var borderWidth = size.borderLeftWidth + size.borderRightWidth; + var borderHeight = size.borderTopWidth + size.borderBottomWidth; + + var isBorderBoxSizeOuter = isBorderBox && isBoxSizeOuter; + + // overwrite width and height if we can get it from style + var styleWidth = getStyleSize( style.width ); + if ( styleWidth !== false ) { + size.width = styleWidth + + // add padding and border unless it's already including it + ( isBorderBoxSizeOuter ? 0 : paddingWidth + borderWidth ); + } + + var styleHeight = getStyleSize( style.height ); + if ( styleHeight !== false ) { + size.height = styleHeight + + // add padding and border unless it's already including it + ( isBorderBoxSizeOuter ? 0 : paddingHeight + borderHeight ); + } + + size.innerWidth = size.width - ( paddingWidth + borderWidth ); + size.innerHeight = size.height - ( paddingHeight + borderHeight ); + + size.outerWidth = size.width + marginWidth; + size.outerHeight = size.height + marginHeight; + + return size; +} + +return getSize; + +}); + +/** + * EvEmitter v1.0.2 + * Lil' event emitter + * MIT License + */ + +/* jshint unused: true, undef: true, strict: true */ + +( function( global, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'ev-emitter/ev-emitter',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory(); + } else { + // Browser globals + global.EvEmitter = factory(); + } + +}( this, function() { + + + +function EvEmitter() {} + +var proto = EvEmitter.prototype; + +proto.on = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // set events hash + var events = this._events = this._events || {}; + // set listeners array + var listeners = events[ eventName ] = events[ eventName ] || []; + // only add once + if ( listeners.indexOf( listener ) == -1 ) { + listeners.push( listener ); + } + + return this; +}; + +proto.once = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // add event + this.on( eventName, listener ); + // set once flag + // set onceEvents hash + var onceEvents = this._onceEvents = this._onceEvents || {}; + // set onceListeners object + var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {}; + // set flag + onceListeners[ listener ] = true; + + return this; +}; + +proto.off = function( eventName, listener ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + var index = listeners.indexOf( listener ); + if ( index != -1 ) { + listeners.splice( index, 1 ); + } + + return this; +}; + +proto.emitEvent = function( eventName, args ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + var i = 0; + var listener = listeners[i]; + args = args || []; + // once stuff + var onceListeners = this._onceEvents && this._onceEvents[ eventName ]; + + while ( listener ) { + var isOnce = onceListeners && onceListeners[ listener ]; + if ( isOnce ) { + // remove listener + // remove before trigger to prevent recursion + this.off( eventName, listener ); + // unset once flag + delete onceListeners[ listener ]; + } + // trigger listener + listener.apply( this, args ); + // get next listener + i += isOnce ? 0 : 1; + listener = listeners[i]; + } + + return this; +}; + +return EvEmitter; + +})); + +/** + * matchesSelector v2.0.1 + * matchesSelector( element, '.selector' ) + * MIT license + */ + +/*jshint browser: true, strict: true, undef: true, unused: true */ + +( function( window, factory ) { + /*global define: false, module: false */ + 'use strict'; + // universal module definition + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'desandro-matches-selector/matches-selector',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory(); + } else { + // browser global + window.matchesSelector = factory(); + } + +}( window, function factory() { + 'use strict'; + + var matchesMethod = ( function() { + var ElemProto = Element.prototype; + // check for the standard method name first + if ( ElemProto.matches ) { + return 'matches'; + } + // check un-prefixed + if ( ElemProto.matchesSelector ) { + return 'matchesSelector'; + } + // check vendor prefixes + var prefixes = [ 'webkit', 'moz', 'ms', 'o' ]; + + for ( var i=0; i < prefixes.length; i++ ) { + var prefix = prefixes[i]; + var method = prefix + 'MatchesSelector'; + if ( ElemProto[ method ] ) { + return method; + } + } + })(); + + return function matchesSelector( elem, selector ) { + return elem[ matchesMethod ]( selector ); + }; + +})); + +/** + * Fizzy UI utils v2.0.1 + * MIT license + */ + +/*jshint browser: true, undef: true, unused: true, strict: true */ + +( function( window, factory ) { + // universal module definition + /*jshint strict: false */ /*globals define, module, require */ + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'fizzy-ui-utils/utils',[ + 'desandro-matches-selector/matches-selector' + ], function( matchesSelector ) { + return factory( window, matchesSelector ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('desandro-matches-selector') + ); + } else { + // browser global + window.fizzyUIUtils = factory( + window, + window.matchesSelector + ); + } + +}( window, function factory( window, matchesSelector ) { + + + +var utils = {}; + +// ----- extend ----- // + +// extends objects +utils.extend = function( a, b ) { + for ( var prop in b ) { + a[ prop ] = b[ prop ]; + } + return a; +}; + +// ----- modulo ----- // + +utils.modulo = function( num, div ) { + return ( ( num % div ) + div ) % div; +}; + +// ----- makeArray ----- // + +// turn element or nodeList into an array +utils.makeArray = function( obj ) { + var ary = []; + if ( Array.isArray( obj ) ) { + // use object if already an array + ary = obj; + } else if ( obj && typeof obj.length == 'number' ) { + // convert nodeList to array + for ( var i=0; i < obj.length; i++ ) { + ary.push( obj[i] ); + } + } else { + // array of single index + ary.push( obj ); + } + return ary; +}; + +// ----- removeFrom ----- // + +utils.removeFrom = function( ary, obj ) { + var index = ary.indexOf( obj ); + if ( index != -1 ) { + ary.splice( index, 1 ); + } +}; + +// ----- getParent ----- // + +utils.getParent = function( elem, selector ) { + while ( elem != document.body ) { + elem = elem.parentNode; + if ( matchesSelector( elem, selector ) ) { + return elem; + } + } +}; + +// ----- getQueryElement ----- // + +// use element as selector string +utils.getQueryElement = function( elem ) { + if ( typeof elem == 'string' ) { + return document.querySelector( elem ); + } + return elem; +}; + +// ----- handleEvent ----- // + +// enable .ontype to trigger from .addEventListener( elem, 'type' ) +utils.handleEvent = function( event ) { + var method = 'on' + event.type; + if ( this[ method ] ) { + this[ method ]( event ); + } +}; + +// ----- filterFindElements ----- // + +utils.filterFindElements = function( elems, selector ) { + // make array of elems + elems = utils.makeArray( elems ); + var ffElems = []; + + elems.forEach( function( elem ) { + // check that elem is an actual element + if ( !( elem instanceof HTMLElement ) ) { + return; + } + // add elem if no selector + if ( !selector ) { + ffElems.push( elem ); + return; + } + // filter & find items if we have a selector + // filter + if ( matchesSelector( elem, selector ) ) { + ffElems.push( elem ); + } + // find children + var childElems = elem.querySelectorAll( selector ); + // concat childElems to filterFound array + for ( var i=0; i < childElems.length; i++ ) { + ffElems.push( childElems[i] ); + } + }); + + return ffElems; +}; + +// ----- debounceMethod ----- // + +utils.debounceMethod = function( _class, methodName, threshold ) { + // original method + var method = _class.prototype[ methodName ]; + var timeoutName = methodName + 'Timeout'; + + _class.prototype[ methodName ] = function() { + var timeout = this[ timeoutName ]; + if ( timeout ) { + clearTimeout( timeout ); + } + var args = arguments; + + var _this = this; + this[ timeoutName ] = setTimeout( function() { + method.apply( _this, args ); + delete _this[ timeoutName ]; + }, threshold || 100 ); + }; +}; + +// ----- docReady ----- // + +utils.docReady = function( callback ) { + if ( document.readyState == 'complete' ) { + callback(); + } else { + document.addEventListener( 'DOMContentLoaded', callback ); + } +}; + +// ----- htmlInit ----- // + +// http://jamesroberts.name/blog/2010/02/22/string-functions-for-javascript-trim-to-camel-case-to-dashed-and-to-underscore/ +utils.toDashed = function( str ) { + return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) { + return $1 + '-' + $2; + }).toLowerCase(); +}; + +var console = window.console; +/** + * allow user to initialize classes via [data-namespace] or .js-namespace class + * htmlInit( Widget, 'widgetName' ) + * options are parsed from data-namespace-options + */ +utils.htmlInit = function( WidgetClass, namespace ) { + utils.docReady( function() { + var dashedNamespace = utils.toDashed( namespace ); + var dataAttr = 'data-' + dashedNamespace; + var dataAttrElems = document.querySelectorAll( '[' + dataAttr + ']' ); + var jsDashElems = document.querySelectorAll( '.js-' + dashedNamespace ); + var elems = utils.makeArray( dataAttrElems ) + .concat( utils.makeArray( jsDashElems ) ); + var dataOptionsAttr = dataAttr + '-options'; + var jQuery = window.jQuery; + + elems.forEach( function( elem ) { + var attr = elem.getAttribute( dataAttr ) || + elem.getAttribute( dataOptionsAttr ); + var options; + try { + options = attr && JSON.parse( attr ); + } catch ( error ) { + // log error, do not initialize + if ( console ) { + console.error( 'Error parsing ' + dataAttr + ' on ' + elem.className + + ': ' + error ); + } + return; + } + // initialize + var instance = new WidgetClass( elem, options ); + // make available via $().data('layoutname') + if ( jQuery ) { + jQuery.data( elem, namespace, instance ); + } + }); + + }); +}; + +// ----- ----- // + +return utils; + +})); + +/** + * Outlayer Item + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'outlayer/item',[ + 'ev-emitter/ev-emitter', + 'get-size/get-size' + ], + factory + ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory( + require('ev-emitter'), + require('get-size') + ); + } else { + // browser global + window.Outlayer = {}; + window.Outlayer.Item = factory( + window.EvEmitter, + window.getSize + ); + } + +}( window, function factory( EvEmitter, getSize ) { +'use strict'; + +// ----- helpers ----- // + +function isEmptyObj( obj ) { + for ( var prop in obj ) { + return false; + } + prop = null; + return true; +} + +// -------------------------- CSS3 support -------------------------- // + + +var docElemStyle = document.documentElement.style; + +var transitionProperty = typeof docElemStyle.transition == 'string' ? + 'transition' : 'WebkitTransition'; +var transformProperty = typeof docElemStyle.transform == 'string' ? + 'transform' : 'WebkitTransform'; + +var transitionEndEvent = { + WebkitTransition: 'webkitTransitionEnd', + transition: 'transitionend' +}[ transitionProperty ]; + +// cache all vendor properties that could have vendor prefix +var vendorProperties = { + transform: transformProperty, + transition: transitionProperty, + transitionDuration: transitionProperty + 'Duration', + transitionProperty: transitionProperty + 'Property' +}; + +// -------------------------- Item -------------------------- // + +function Item( element, layout ) { + if ( !element ) { + return; + } + + this.element = element; + // parent layout class, i.e. Masonry, Isotope, or Packery + this.layout = layout; + this.position = { + x: 0, + y: 0 + }; + + this._create(); +} + +// inherit EvEmitter +var proto = Item.prototype = Object.create( EvEmitter.prototype ); +proto.constructor = Item; + +proto._create = function() { + // transition objects + this._transn = { + ingProperties: {}, + clean: {}, + onEnd: {} + }; + + this.css({ + position: 'absolute' + }); +}; + +// trigger specified handler for event type +proto.handleEvent = function( event ) { + var method = 'on' + event.type; + if ( this[ method ] ) { + this[ method ]( event ); + } +}; + +proto.getSize = function() { + this.size = getSize( this.element ); +}; + +/** + * apply CSS styles to element + * @param {Object} style + */ +proto.css = function( style ) { + var elemStyle = this.element.style; + + for ( var prop in style ) { + // use vendor property if available + var supportedProp = vendorProperties[ prop ] || prop; + elemStyle[ supportedProp ] = style[ prop ]; + } +}; + + // measure position, and sets it +proto.getPosition = function() { + var style = getComputedStyle( this.element ); + var isOriginLeft = this.layout._getOption('originLeft'); + var isOriginTop = this.layout._getOption('originTop'); + var xValue = style[ isOriginLeft ? 'left' : 'right' ]; + var yValue = style[ isOriginTop ? 'top' : 'bottom' ]; + // convert percent to pixels + var layoutSize = this.layout.size; + var x = xValue.indexOf('%') != -1 ? + ( parseFloat( xValue ) / 100 ) * layoutSize.width : parseInt( xValue, 10 ); + var y = yValue.indexOf('%') != -1 ? + ( parseFloat( yValue ) / 100 ) * layoutSize.height : parseInt( yValue, 10 ); + + // clean up 'auto' or other non-integer values + x = isNaN( x ) ? 0 : x; + y = isNaN( y ) ? 0 : y; + // remove padding from measurement + x -= isOriginLeft ? layoutSize.paddingLeft : layoutSize.paddingRight; + y -= isOriginTop ? layoutSize.paddingTop : layoutSize.paddingBottom; + + this.position.x = x; + this.position.y = y; +}; + +// set settled position, apply padding +proto.layoutPosition = function() { + var layoutSize = this.layout.size; + var style = {}; + var isOriginLeft = this.layout._getOption('originLeft'); + var isOriginTop = this.layout._getOption('originTop'); + + // x + var xPadding = isOriginLeft ? 'paddingLeft' : 'paddingRight'; + var xProperty = isOriginLeft ? 'left' : 'right'; + var xResetProperty = isOriginLeft ? 'right' : 'left'; + + var x = this.position.x + layoutSize[ xPadding ]; + // set in percentage or pixels + style[ xProperty ] = this.getXValue( x ); + // reset other property + style[ xResetProperty ] = ''; + + // y + var yPadding = isOriginTop ? 'paddingTop' : 'paddingBottom'; + var yProperty = isOriginTop ? 'top' : 'bottom'; + var yResetProperty = isOriginTop ? 'bottom' : 'top'; + + var y = this.position.y + layoutSize[ yPadding ]; + // set in percentage or pixels + style[ yProperty ] = this.getYValue( y ); + // reset other property + style[ yResetProperty ] = ''; + + this.css( style ); + this.emitEvent( 'layout', [ this ] ); +}; + +proto.getXValue = function( x ) { + var isHorizontal = this.layout._getOption('horizontal'); + return this.layout.options.percentPosition && !isHorizontal ? + ( ( x / this.layout.size.width ) * 100 ) + '%' : x + 'px'; +}; + +proto.getYValue = function( y ) { + var isHorizontal = this.layout._getOption('horizontal'); + return this.layout.options.percentPosition && isHorizontal ? + ( ( y / this.layout.size.height ) * 100 ) + '%' : y + 'px'; +}; + +proto._transitionTo = function( x, y ) { + this.getPosition(); + // get current x & y from top/left + var curX = this.position.x; + var curY = this.position.y; + + var compareX = parseInt( x, 10 ); + var compareY = parseInt( y, 10 ); + var didNotMove = compareX === this.position.x && compareY === this.position.y; + + // save end position + this.setPosition( x, y ); + + // if did not move and not transitioning, just go to layout + if ( didNotMove && !this.isTransitioning ) { + this.layoutPosition(); + return; + } + + var transX = x - curX; + var transY = y - curY; + var transitionStyle = {}; + transitionStyle.transform = this.getTranslate( transX, transY ); + + this.transition({ + to: transitionStyle, + onTransitionEnd: { + transform: this.layoutPosition + }, + isCleaning: true + }); +}; + +proto.getTranslate = function( x, y ) { + // flip cooridinates if origin on right or bottom + var isOriginLeft = this.layout._getOption('originLeft'); + var isOriginTop = this.layout._getOption('originTop'); + x = isOriginLeft ? x : -x; + y = isOriginTop ? y : -y; + return 'translate3d(' + x + 'px, ' + y + 'px, 0)'; +}; + +// non transition + transform support +proto.goTo = function( x, y ) { + this.setPosition( x, y ); + this.layoutPosition(); +}; + +proto.moveTo = proto._transitionTo; + +proto.setPosition = function( x, y ) { + this.position.x = parseInt( x, 10 ); + this.position.y = parseInt( y, 10 ); +}; + +// ----- transition ----- // + +/** + * @param {Object} style - CSS + * @param {Function} onTransitionEnd + */ + +// non transition, just trigger callback +proto._nonTransition = function( args ) { + this.css( args.to ); + if ( args.isCleaning ) { + this._removeStyles( args.to ); + } + for ( var prop in args.onTransitionEnd ) { + args.onTransitionEnd[ prop ].call( this ); + } +}; + +/** + * proper transition + * @param {Object} args - arguments + * @param {Object} to - style to transition to + * @param {Object} from - style to start transition from + * @param {Boolean} isCleaning - removes transition styles after transition + * @param {Function} onTransitionEnd - callback + */ +proto.transition = function( args ) { + // redirect to nonTransition if no transition duration + if ( !parseFloat( this.layout.options.transitionDuration ) ) { + this._nonTransition( args ); + return; + } + + var _transition = this._transn; + // keep track of onTransitionEnd callback by css property + for ( var prop in args.onTransitionEnd ) { + _transition.onEnd[ prop ] = args.onTransitionEnd[ prop ]; + } + // keep track of properties that are transitioning + for ( prop in args.to ) { + _transition.ingProperties[ prop ] = true; + // keep track of properties to clean up when transition is done + if ( args.isCleaning ) { + _transition.clean[ prop ] = true; + } + } + + // set from styles + if ( args.from ) { + this.css( args.from ); + // force redraw. http://blog.alexmaccaw.com/css-transitions + var h = this.element.offsetHeight; + // hack for JSHint to hush about unused var + h = null; + } + // enable transition + this.enableTransition( args.to ); + // set styles that are transitioning + this.css( args.to ); + + this.isTransitioning = true; + +}; + +// dash before all cap letters, including first for +// WebkitTransform => -webkit-transform +function toDashedAll( str ) { + return str.replace( /([A-Z])/g, function( $1 ) { + return '-' + $1.toLowerCase(); + }); +} + +var transitionProps = 'opacity,' + toDashedAll( transformProperty ); + +proto.enableTransition = function(/* style */) { + // HACK changing transitionProperty during a transition + // will cause transition to jump + if ( this.isTransitioning ) { + return; + } + + // make `transition: foo, bar, baz` from style object + // HACK un-comment this when enableTransition can work + // while a transition is happening + // var transitionValues = []; + // for ( var prop in style ) { + // // dash-ify camelCased properties like WebkitTransition + // prop = vendorProperties[ prop ] || prop; + // transitionValues.push( toDashedAll( prop ) ); + // } + // enable transition styles + this.css({ + transitionProperty: transitionProps, + transitionDuration: this.layout.options.transitionDuration + }); + // listen for transition end event + this.element.addEventListener( transitionEndEvent, this, false ); +}; + +// ----- events ----- // + +proto.onwebkitTransitionEnd = function( event ) { + this.ontransitionend( event ); +}; + +proto.onotransitionend = function( event ) { + this.ontransitionend( event ); +}; + +// properties that I munge to make my life easier +var dashedVendorProperties = { + '-webkit-transform': 'transform' +}; + +proto.ontransitionend = function( event ) { + // disregard bubbled events from children + if ( event.target !== this.element ) { + return; + } + var _transition = this._transn; + // get property name of transitioned property, convert to prefix-free + var propertyName = dashedVendorProperties[ event.propertyName ] || event.propertyName; + + // remove property that has completed transitioning + delete _transition.ingProperties[ propertyName ]; + // check if any properties are still transitioning + if ( isEmptyObj( _transition.ingProperties ) ) { + // all properties have completed transitioning + this.disableTransition(); + } + // clean style + if ( propertyName in _transition.clean ) { + // clean up style + this.element.style[ event.propertyName ] = ''; + delete _transition.clean[ propertyName ]; + } + // trigger onTransitionEnd callback + if ( propertyName in _transition.onEnd ) { + var onTransitionEnd = _transition.onEnd[ propertyName ]; + onTransitionEnd.call( this ); + delete _transition.onEnd[ propertyName ]; + } + + this.emitEvent( 'transitionEnd', [ this ] ); +}; + +proto.disableTransition = function() { + this.removeTransitionStyles(); + this.element.removeEventListener( transitionEndEvent, this, false ); + this.isTransitioning = false; +}; + +/** + * removes style property from element + * @param {Object} style +**/ +proto._removeStyles = function( style ) { + // clean up transition styles + var cleanStyle = {}; + for ( var prop in style ) { + cleanStyle[ prop ] = ''; + } + this.css( cleanStyle ); +}; + +var cleanTransitionStyle = { + transitionProperty: '', + transitionDuration: '' +}; + +proto.removeTransitionStyles = function() { + // remove transition + this.css( cleanTransitionStyle ); +}; + +// ----- show/hide/remove ----- // + +// remove element from DOM +proto.removeElem = function() { + this.element.parentNode.removeChild( this.element ); + // remove display: none + this.css({ display: '' }); + this.emitEvent( 'remove', [ this ] ); +}; + +proto.remove = function() { + // just remove element if no transition support or no transition + if ( !transitionProperty || !parseFloat( this.layout.options.transitionDuration ) ) { + this.removeElem(); + return; + } + + // start transition + this.once( 'transitionEnd', function() { + this.removeElem(); + }); + this.hide(); +}; + +proto.reveal = function() { + delete this.isHidden; + // remove display: none + this.css({ display: '' }); + + var options = this.layout.options; + + var onTransitionEnd = {}; + var transitionEndProperty = this.getHideRevealTransitionEndProperty('visibleStyle'); + onTransitionEnd[ transitionEndProperty ] = this.onRevealTransitionEnd; + + this.transition({ + from: options.hiddenStyle, + to: options.visibleStyle, + isCleaning: true, + onTransitionEnd: onTransitionEnd + }); +}; + +proto.onRevealTransitionEnd = function() { + // check if still visible + // during transition, item may have been hidden + if ( !this.isHidden ) { + this.emitEvent('reveal'); + } +}; + +/** + * get style property use for hide/reveal transition end + * @param {String} styleProperty - hiddenStyle/visibleStyle + * @returns {String} + */ +proto.getHideRevealTransitionEndProperty = function( styleProperty ) { + var optionStyle = this.layout.options[ styleProperty ]; + // use opacity + if ( optionStyle.opacity ) { + return 'opacity'; + } + // get first property + for ( var prop in optionStyle ) { + return prop; + } +}; + +proto.hide = function() { + // set flag + this.isHidden = true; + // remove display: none + this.css({ display: '' }); + + var options = this.layout.options; + + var onTransitionEnd = {}; + var transitionEndProperty = this.getHideRevealTransitionEndProperty('hiddenStyle'); + onTransitionEnd[ transitionEndProperty ] = this.onHideTransitionEnd; + + this.transition({ + from: options.visibleStyle, + to: options.hiddenStyle, + // keep hidden stuff hidden + isCleaning: true, + onTransitionEnd: onTransitionEnd + }); +}; + +proto.onHideTransitionEnd = function() { + // check if still hidden + // during transition, item may have been un-hidden + if ( this.isHidden ) { + this.css({ display: 'none' }); + this.emitEvent('hide'); + } +}; + +proto.destroy = function() { + this.css({ + position: '', + left: '', + right: '', + top: '', + bottom: '', + transition: '', + transform: '' + }); +}; + +return Item; + +})); + +/*! + * Outlayer v2.0.1 + * the brains and guts of a layout library + * MIT license + */ + +( function( window, factory ) { + 'use strict'; + // universal module definition + /* jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'outlayer/outlayer',[ + 'ev-emitter/ev-emitter', + 'get-size/get-size', + 'fizzy-ui-utils/utils', + './item' + ], + function( EvEmitter, getSize, utils, Item ) { + return factory( window, EvEmitter, getSize, utils, Item); + } + ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory( + window, + require('ev-emitter'), + require('get-size'), + require('fizzy-ui-utils'), + require('./item') + ); + } else { + // browser global + window.Outlayer = factory( + window, + window.EvEmitter, + window.getSize, + window.fizzyUIUtils, + window.Outlayer.Item + ); + } + +}( window, function factory( window, EvEmitter, getSize, utils, Item ) { +'use strict'; + +// ----- vars ----- // + +var console = window.console; +var jQuery = window.jQuery; +var noop = function() {}; + +// -------------------------- Outlayer -------------------------- // + +// globally unique identifiers +var GUID = 0; +// internal store of all Outlayer intances +var instances = {}; + + +/** + * @param {Element, String} element + * @param {Object} options + * @constructor + */ +function Outlayer( element, options ) { + var queryElement = utils.getQueryElement( element ); + if ( !queryElement ) { + if ( console ) { + console.error( 'Bad element for ' + this.constructor.namespace + + ': ' + ( queryElement || element ) ); + } + return; + } + this.element = queryElement; + // add jQuery + if ( jQuery ) { + this.$element = jQuery( this.element ); + } + + // options + this.options = utils.extend( {}, this.constructor.defaults ); + this.option( options ); + + // add id for Outlayer.getFromElement + var id = ++GUID; + this.element.outlayerGUID = id; // expando + instances[ id ] = this; // associate via id + + // kick it off + this._create(); + + var isInitLayout = this._getOption('initLayout'); + if ( isInitLayout ) { + this.layout(); + } +} + +// settings are for internal use only +Outlayer.namespace = 'outlayer'; +Outlayer.Item = Item; + +// default options +Outlayer.defaults = { + containerStyle: { + position: 'relative' + }, + initLayout: true, + originLeft: true, + originTop: true, + resize: true, + resizeContainer: true, + // item options + transitionDuration: '0.4s', + hiddenStyle: { + opacity: 0, + transform: 'scale(0.001)' + }, + visibleStyle: { + opacity: 1, + transform: 'scale(1)' + } +}; + +var proto = Outlayer.prototype; +// inherit EvEmitter +utils.extend( proto, EvEmitter.prototype ); + +/** + * set options + * @param {Object} opts + */ +proto.option = function( opts ) { + utils.extend( this.options, opts ); +}; + +/** + * get backwards compatible option value, check old name + */ +proto._getOption = function( option ) { + var oldOption = this.constructor.compatOptions[ option ]; + return oldOption && this.options[ oldOption ] !== undefined ? + this.options[ oldOption ] : this.options[ option ]; +}; + +Outlayer.compatOptions = { + // currentName: oldName + initLayout: 'isInitLayout', + horizontal: 'isHorizontal', + layoutInstant: 'isLayoutInstant', + originLeft: 'isOriginLeft', + originTop: 'isOriginTop', + resize: 'isResizeBound', + resizeContainer: 'isResizingContainer' +}; + +proto._create = function() { + // get items from children + this.reloadItems(); + // elements that affect layout, but are not laid out + this.stamps = []; + this.stamp( this.options.stamp ); + // set container style + utils.extend( this.element.style, this.options.containerStyle ); + + // bind resize method + var canBindResize = this._getOption('resize'); + if ( canBindResize ) { + this.bindResize(); + } +}; + +// goes through all children again and gets bricks in proper order +proto.reloadItems = function() { + // collection of item elements + this.items = this._itemize( this.element.children ); +}; + + +/** + * turn elements into Outlayer.Items to be used in layout + * @param {Array or NodeList or HTMLElement} elems + * @returns {Array} items - collection of new Outlayer Items + */ +proto._itemize = function( elems ) { + + var itemElems = this._filterFindItemElements( elems ); + var Item = this.constructor.Item; + + // create new Outlayer Items for collection + var items = []; + for ( var i=0; i < itemElems.length; i++ ) { + var elem = itemElems[i]; + var item = new Item( elem, this ); + items.push( item ); + } + + return items; +}; + +/** + * get item elements to be used in layout + * @param {Array or NodeList or HTMLElement} elems + * @returns {Array} items - item elements + */ +proto._filterFindItemElements = function( elems ) { + return utils.filterFindElements( elems, this.options.itemSelector ); +}; + +/** + * getter method for getting item elements + * @returns {Array} elems - collection of item elements + */ +proto.getItemElements = function() { + return this.items.map( function( item ) { + return item.element; + }); +}; + +// ----- init & layout ----- // + +/** + * lays out all items + */ +proto.layout = function() { + this._resetLayout(); + this._manageStamps(); + + // don't animate first layout + var layoutInstant = this._getOption('layoutInstant'); + var isInstant = layoutInstant !== undefined ? + layoutInstant : !this._isLayoutInited; + this.layoutItems( this.items, isInstant ); + + // flag for initalized + this._isLayoutInited = true; +}; + +// _init is alias for layout +proto._init = proto.layout; + +/** + * logic before any new layout + */ +proto._resetLayout = function() { + this.getSize(); +}; + + +proto.getSize = function() { + this.size = getSize( this.element ); +}; + +/** + * get measurement from option, for columnWidth, rowHeight, gutter + * if option is String -> get element from selector string, & get size of element + * if option is Element -> get size of element + * else use option as a number + * + * @param {String} measurement + * @param {String} size - width or height + * @private + */ +proto._getMeasurement = function( measurement, size ) { + var option = this.options[ measurement ]; + var elem; + if ( !option ) { + // default to 0 + this[ measurement ] = 0; + } else { + // use option as an element + if ( typeof option == 'string' ) { + elem = this.element.querySelector( option ); + } else if ( option instanceof HTMLElement ) { + elem = option; + } + // use size of element, if element + this[ measurement ] = elem ? getSize( elem )[ size ] : option; + } +}; + +/** + * layout a collection of item elements + * @api public + */ +proto.layoutItems = function( items, isInstant ) { + items = this._getItemsForLayout( items ); + + this._layoutItems( items, isInstant ); + + this._postLayout(); +}; + +/** + * get the items to be laid out + * you may want to skip over some items + * @param {Array} items + * @returns {Array} items + */ +proto._getItemsForLayout = function( items ) { + return items.filter( function( item ) { + return !item.isIgnored; + }); +}; + +/** + * layout items + * @param {Array} items + * @param {Boolean} isInstant + */ +proto._layoutItems = function( items, isInstant ) { + this._emitCompleteOnItems( 'layout', items ); + + if ( !items || !items.length ) { + // no items, emit event with empty array + return; + } + + var queue = []; + + items.forEach( function( item ) { + // get x/y object from method + var position = this._getItemLayoutPosition( item ); + // enqueue + position.item = item; + position.isInstant = isInstant || item.isLayoutInstant; + queue.push( position ); + }, this ); + + this._processLayoutQueue( queue ); +}; + +/** + * get item layout position + * @param {Outlayer.Item} item + * @returns {Object} x and y position + */ +proto._getItemLayoutPosition = function( /* item */ ) { + return { + x: 0, + y: 0 + }; +}; + +/** + * iterate over array and position each item + * Reason being - separating this logic prevents 'layout invalidation' + * thx @paul_irish + * @param {Array} queue + */ +proto._processLayoutQueue = function( queue ) { + queue.forEach( function( obj ) { + this._positionItem( obj.item, obj.x, obj.y, obj.isInstant ); + }, this ); +}; + +/** + * Sets position of item in DOM + * @param {Outlayer.Item} item + * @param {Number} x - horizontal position + * @param {Number} y - vertical position + * @param {Boolean} isInstant - disables transitions + */ +proto._positionItem = function( item, x, y, isInstant ) { + if ( isInstant ) { + // if not transition, just set CSS + item.goTo( x, y ); + } else { + item.moveTo( x, y ); + } +}; + +/** + * Any logic you want to do after each layout, + * i.e. size the container + */ +proto._postLayout = function() { + this.resizeContainer(); +}; + +proto.resizeContainer = function() { + var isResizingContainer = this._getOption('resizeContainer'); + if ( !isResizingContainer ) { + return; + } + var size = this._getContainerSize(); + if ( size ) { + this._setContainerMeasure( size.width, true ); + this._setContainerMeasure( size.height, false ); + } +}; + +/** + * Sets width or height of container if returned + * @returns {Object} size + * @param {Number} width + * @param {Number} height + */ +proto._getContainerSize = noop; + +/** + * @param {Number} measure - size of width or height + * @param {Boolean} isWidth + */ +proto._setContainerMeasure = function( measure, isWidth ) { + if ( measure === undefined ) { + return; + } + + var elemSize = this.size; + // add padding and border width if border box + if ( elemSize.isBorderBox ) { + measure += isWidth ? elemSize.paddingLeft + elemSize.paddingRight + + elemSize.borderLeftWidth + elemSize.borderRightWidth : + elemSize.paddingBottom + elemSize.paddingTop + + elemSize.borderTopWidth + elemSize.borderBottomWidth; + } + + measure = Math.max( measure, 0 ); + this.element.style[ isWidth ? 'width' : 'height' ] = measure + 'px'; +}; + +/** + * emit eventComplete on a collection of items events + * @param {String} eventName + * @param {Array} items - Outlayer.Items + */ +proto._emitCompleteOnItems = function( eventName, items ) { + var _this = this; + function onComplete() { + _this.dispatchEvent( eventName + 'Complete', null, [ items ] ); + } + + var count = items.length; + if ( !items || !count ) { + onComplete(); + return; + } + + var doneCount = 0; + function tick() { + doneCount++; + if ( doneCount == count ) { + onComplete(); + } + } + + // bind callback + items.forEach( function( item ) { + item.once( eventName, tick ); + }); +}; + +/** + * emits events via EvEmitter and jQuery events + * @param {String} type - name of event + * @param {Event} event - original event + * @param {Array} args - extra arguments + */ +proto.dispatchEvent = function( type, event, args ) { + // add original event to arguments + var emitArgs = event ? [ event ].concat( args ) : args; + this.emitEvent( type, emitArgs ); + + if ( jQuery ) { + // set this.$element + this.$element = this.$element || jQuery( this.element ); + if ( event ) { + // create jQuery event + var $event = jQuery.Event( event ); + $event.type = type; + this.$element.trigger( $event, args ); + } else { + // just trigger with type if no event available + this.$element.trigger( type, args ); + } + } +}; + +// -------------------------- ignore & stamps -------------------------- // + + +/** + * keep item in collection, but do not lay it out + * ignored items do not get skipped in layout + * @param {Element} elem + */ +proto.ignore = function( elem ) { + var item = this.getItem( elem ); + if ( item ) { + item.isIgnored = true; + } +}; + +/** + * return item to layout collection + * @param {Element} elem + */ +proto.unignore = function( elem ) { + var item = this.getItem( elem ); + if ( item ) { + delete item.isIgnored; + } +}; + +/** + * adds elements to stamps + * @param {NodeList, Array, Element, or String} elems + */ +proto.stamp = function( elems ) { + elems = this._find( elems ); + if ( !elems ) { + return; + } + + this.stamps = this.stamps.concat( elems ); + // ignore + elems.forEach( this.ignore, this ); +}; + +/** + * removes elements to stamps + * @param {NodeList, Array, or Element} elems + */ +proto.unstamp = function( elems ) { + elems = this._find( elems ); + if ( !elems ){ + return; + } + + elems.forEach( function( elem ) { + // filter out removed stamp elements + utils.removeFrom( this.stamps, elem ); + this.unignore( elem ); + }, this ); +}; + +/** + * finds child elements + * @param {NodeList, Array, Element, or String} elems + * @returns {Array} elems + */ +proto._find = function( elems ) { + if ( !elems ) { + return; + } + // if string, use argument as selector string + if ( typeof elems == 'string' ) { + elems = this.element.querySelectorAll( elems ); + } + elems = utils.makeArray( elems ); + return elems; +}; + +proto._manageStamps = function() { + if ( !this.stamps || !this.stamps.length ) { + return; + } + + this._getBoundingRect(); + + this.stamps.forEach( this._manageStamp, this ); +}; + +// update boundingLeft / Top +proto._getBoundingRect = function() { + // get bounding rect for container element + var boundingRect = this.element.getBoundingClientRect(); + var size = this.size; + this._boundingRect = { + left: boundingRect.left + size.paddingLeft + size.borderLeftWidth, + top: boundingRect.top + size.paddingTop + size.borderTopWidth, + right: boundingRect.right - ( size.paddingRight + size.borderRightWidth ), + bottom: boundingRect.bottom - ( size.paddingBottom + size.borderBottomWidth ) + }; +}; + +/** + * @param {Element} stamp +**/ +proto._manageStamp = noop; + +/** + * get x/y position of element relative to container element + * @param {Element} elem + * @returns {Object} offset - has left, top, right, bottom + */ +proto._getElementOffset = function( elem ) { + var boundingRect = elem.getBoundingClientRect(); + var thisRect = this._boundingRect; + var size = getSize( elem ); + var offset = { + left: boundingRect.left - thisRect.left - size.marginLeft, + top: boundingRect.top - thisRect.top - size.marginTop, + right: thisRect.right - boundingRect.right - size.marginRight, + bottom: thisRect.bottom - boundingRect.bottom - size.marginBottom + }; + return offset; +}; + +// -------------------------- resize -------------------------- // + +// enable event handlers for listeners +// i.e. resize -> onresize +proto.handleEvent = utils.handleEvent; + +/** + * Bind layout to window resizing + */ +proto.bindResize = function() { + window.addEventListener( 'resize', this ); + this.isResizeBound = true; +}; + +/** + * Unbind layout to window resizing + */ +proto.unbindResize = function() { + window.removeEventListener( 'resize', this ); + this.isResizeBound = false; +}; + +proto.onresize = function() { + this.resize(); +}; + +utils.debounceMethod( Outlayer, 'onresize', 100 ); + +proto.resize = function() { + // don't trigger if size did not change + // or if resize was unbound. See #9 + if ( !this.isResizeBound || !this.needsResizeLayout() ) { + return; + } + + this.layout(); +}; + +/** + * check if layout is needed post layout + * @returns Boolean + */ +proto.needsResizeLayout = function() { + var size = getSize( this.element ); + // check that this.size and size are there + // IE8 triggers resize on body size change, so they might not be + var hasSizes = this.size && size; + return hasSizes && size.innerWidth !== this.size.innerWidth; +}; + +// -------------------------- methods -------------------------- // + +/** + * add items to Outlayer instance + * @param {Array or NodeList or Element} elems + * @returns {Array} items - Outlayer.Items +**/ +proto.addItems = function( elems ) { + var items = this._itemize( elems ); + // add items to collection + if ( items.length ) { + this.items = this.items.concat( items ); + } + return items; +}; + +/** + * Layout newly-appended item elements + * @param {Array or NodeList or Element} elems + */ +proto.appended = function( elems ) { + var items = this.addItems( elems ); + if ( !items.length ) { + return; + } + // layout and reveal just the new items + this.layoutItems( items, true ); + this.reveal( items ); +}; + +/** + * Layout prepended elements + * @param {Array or NodeList or Element} elems + */ +proto.prepended = function( elems ) { + var items = this._itemize( elems ); + if ( !items.length ) { + return; + } + // add items to beginning of collection + var previousItems = this.items.slice(0); + this.items = items.concat( previousItems ); + // start new layout + this._resetLayout(); + this._manageStamps(); + // layout new stuff without transition + this.layoutItems( items, true ); + this.reveal( items ); + // layout previous items + this.layoutItems( previousItems ); +}; + +/** + * reveal a collection of items + * @param {Array of Outlayer.Items} items + */ +proto.reveal = function( items ) { + this._emitCompleteOnItems( 'reveal', items ); + if ( !items || !items.length ) { + return; + } + items.forEach( function( item ) { + item.reveal(); + }); +}; + +/** + * hide a collection of items + * @param {Array of Outlayer.Items} items + */ +proto.hide = function( items ) { + this._emitCompleteOnItems( 'hide', items ); + if ( !items || !items.length ) { + return; + } + items.forEach( function( item ) { + item.hide(); + }); +}; + +/** + * reveal item elements + * @param {Array}, {Element}, {NodeList} items + */ +proto.revealItemElements = function( elems ) { + var items = this.getItems( elems ); + this.reveal( items ); +}; + +/** + * hide item elements + * @param {Array}, {Element}, {NodeList} items + */ +proto.hideItemElements = function( elems ) { + var items = this.getItems( elems ); + this.hide( items ); +}; + +/** + * get Outlayer.Item, given an Element + * @param {Element} elem + * @param {Function} callback + * @returns {Outlayer.Item} item + */ +proto.getItem = function( elem ) { + // loop through items to get the one that matches + for ( var i=0; i < this.items.length; i++ ) { + var item = this.items[i]; + if ( item.element == elem ) { + // return item + return item; + } + } +}; + +/** + * get collection of Outlayer.Items, given Elements + * @param {Array} elems + * @returns {Array} items - Outlayer.Items + */ +proto.getItems = function( elems ) { + elems = utils.makeArray( elems ); + var items = []; + elems.forEach( function( elem ) { + var item = this.getItem( elem ); + if ( item ) { + items.push( item ); + } + }, this ); + + return items; +}; + +/** + * remove element(s) from instance and DOM + * @param {Array or NodeList or Element} elems + */ +proto.remove = function( elems ) { + var removeItems = this.getItems( elems ); + + this._emitCompleteOnItems( 'remove', removeItems ); + + // bail if no items to remove + if ( !removeItems || !removeItems.length ) { + return; + } + + removeItems.forEach( function( item ) { + item.remove(); + // remove item from collection + utils.removeFrom( this.items, item ); + }, this ); +}; + +// ----- destroy ----- // + +// remove and disable Outlayer instance +proto.destroy = function() { + // clean up dynamic styles + var style = this.element.style; + style.height = ''; + style.position = ''; + style.width = ''; + // destroy items + this.items.forEach( function( item ) { + item.destroy(); + }); + + this.unbindResize(); + + var id = this.element.outlayerGUID; + delete instances[ id ]; // remove reference to instance by id + delete this.element.outlayerGUID; + // remove data for jQuery + if ( jQuery ) { + jQuery.removeData( this.element, this.constructor.namespace ); + } + +}; + +// -------------------------- data -------------------------- // + +/** + * get Outlayer instance from element + * @param {Element} elem + * @returns {Outlayer} + */ +Outlayer.data = function( elem ) { + elem = utils.getQueryElement( elem ); + var id = elem && elem.outlayerGUID; + return id && instances[ id ]; +}; + + +// -------------------------- create Outlayer class -------------------------- // + +/** + * create a layout class + * @param {String} namespace + */ +Outlayer.create = function( namespace, options ) { + // sub-class Outlayer + var Layout = subclass( Outlayer ); + // apply new options and compatOptions + Layout.defaults = utils.extend( {}, Outlayer.defaults ); + utils.extend( Layout.defaults, options ); + Layout.compatOptions = utils.extend( {}, Outlayer.compatOptions ); + + Layout.namespace = namespace; + + Layout.data = Outlayer.data; + + // sub-class Item + Layout.Item = subclass( Item ); + + // -------------------------- declarative -------------------------- // + + utils.htmlInit( Layout, namespace ); + + // -------------------------- jQuery bridge -------------------------- // + + // make into jQuery plugin + if ( jQuery && jQuery.bridget ) { + jQuery.bridget( namespace, Layout ); + } + + return Layout; +}; + +function subclass( Parent ) { + function SubClass() { + Parent.apply( this, arguments ); + } + + SubClass.prototype = Object.create( Parent.prototype ); + SubClass.prototype.constructor = SubClass; + + return SubClass; +} + +// ----- fin ----- // + +// back in global +Outlayer.Item = Item; + +return Outlayer; + +})); + +/** + * Rect + * low-level utility class for basic geometry + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'packery/rect',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory(); + } else { + // browser global + window.Packery = window.Packery || {}; + window.Packery.Rect = factory(); + } + +}( window, function factory() { +'use strict'; + +// -------------------------- Rect -------------------------- // + +function Rect( props ) { + // extend properties from defaults + for ( var prop in Rect.defaults ) { + this[ prop ] = Rect.defaults[ prop ]; + } + + for ( prop in props ) { + this[ prop ] = props[ prop ]; + } + +} + +Rect.defaults = { + x: 0, + y: 0, + width: 0, + height: 0 +}; + +var proto = Rect.prototype; + +/** + * Determines whether or not this rectangle wholly encloses another rectangle or point. + * @param {Rect} rect + * @returns {Boolean} +**/ +proto.contains = function( rect ) { + // points don't have width or height + var otherWidth = rect.width || 0; + var otherHeight = rect.height || 0; + return this.x <= rect.x && + this.y <= rect.y && + this.x + this.width >= rect.x + otherWidth && + this.y + this.height >= rect.y + otherHeight; +}; + +/** + * Determines whether or not the rectangle intersects with another. + * @param {Rect} rect + * @returns {Boolean} +**/ +proto.overlaps = function( rect ) { + var thisRight = this.x + this.width; + var thisBottom = this.y + this.height; + var rectRight = rect.x + rect.width; + var rectBottom = rect.y + rect.height; + + // http://stackoverflow.com/a/306332 + return this.x < rectRight && + thisRight > rect.x && + this.y < rectBottom && + thisBottom > rect.y; +}; + +/** + * @param {Rect} rect - the overlapping rect + * @returns {Array} freeRects - rects representing the area around the rect +**/ +proto.getMaximalFreeRects = function( rect ) { + + // if no intersection, return false + if ( !this.overlaps( rect ) ) { + return false; + } + + var freeRects = []; + var freeRect; + + var thisRight = this.x + this.width; + var thisBottom = this.y + this.height; + var rectRight = rect.x + rect.width; + var rectBottom = rect.y + rect.height; + + // top + if ( this.y < rect.y ) { + freeRect = new Rect({ + x: this.x, + y: this.y, + width: this.width, + height: rect.y - this.y + }); + freeRects.push( freeRect ); + } + + // right + if ( thisRight > rectRight ) { + freeRect = new Rect({ + x: rectRight, + y: this.y, + width: thisRight - rectRight, + height: this.height + }); + freeRects.push( freeRect ); + } + + // bottom + if ( thisBottom > rectBottom ) { + freeRect = new Rect({ + x: this.x, + y: rectBottom, + width: this.width, + height: thisBottom - rectBottom + }); + freeRects.push( freeRect ); + } + + // left + if ( this.x < rect.x ) { + freeRect = new Rect({ + x: this.x, + y: this.y, + width: rect.x - this.x, + height: this.height + }); + freeRects.push( freeRect ); + } + + return freeRects; +}; + +proto.canFit = function( rect ) { + return this.width >= rect.width && this.height >= rect.height; +}; + +return Rect; + +})); + +/** + * Packer + * bin-packing algorithm + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'packery/packer',[ './rect' ], factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + require('./rect') + ); + } else { + // browser global + var Packery = window.Packery = window.Packery || {}; + Packery.Packer = factory( Packery.Rect ); + } + +}( window, function factory( Rect ) { +'use strict'; + +// -------------------------- Packer -------------------------- // + +/** + * @param {Number} width + * @param {Number} height + * @param {String} sortDirection + * topLeft for vertical, leftTop for horizontal + */ +function Packer( width, height, sortDirection ) { + this.width = width || 0; + this.height = height || 0; + this.sortDirection = sortDirection || 'downwardLeftToRight'; + + this.reset(); +} + +var proto = Packer.prototype; + +proto.reset = function() { + this.spaces = []; + + var initialSpace = new Rect({ + x: 0, + y: 0, + width: this.width, + height: this.height + }); + + this.spaces.push( initialSpace ); + // set sorter + this.sorter = sorters[ this.sortDirection ] || sorters.downwardLeftToRight; +}; + +// change x and y of rect to fit with in Packer's available spaces +proto.pack = function( rect ) { + for ( var i=0; i < this.spaces.length; i++ ) { + var space = this.spaces[i]; + if ( space.canFit( rect ) ) { + this.placeInSpace( rect, space ); + break; + } + } +}; + +proto.columnPack = function( rect ) { + for ( var i=0; i < this.spaces.length; i++ ) { + var space = this.spaces[i]; + var canFitInSpaceColumn = space.x <= rect.x && + space.x + space.width >= rect.x + rect.width && + space.height >= rect.height - 0.01; // fudge number for rounding error + if ( canFitInSpaceColumn ) { + rect.y = space.y; + this.placed( rect ); + break; + } + } +}; + +proto.rowPack = function( rect ) { + for ( var i=0; i < this.spaces.length; i++ ) { + var space = this.spaces[i]; + var canFitInSpaceRow = space.y <= rect.y && + space.y + space.height >= rect.y + rect.height && + space.width >= rect.width - 0.01; // fudge number for rounding error + if ( canFitInSpaceRow ) { + rect.x = space.x; + this.placed( rect ); + break; + } + } +}; + +proto.placeInSpace = function( rect, space ) { + // place rect in space + rect.x = space.x; + rect.y = space.y; + + this.placed( rect ); +}; + +// update spaces with placed rect +proto.placed = function( rect ) { + // update spaces + var revisedSpaces = []; + for ( var i=0; i < this.spaces.length; i++ ) { + var space = this.spaces[i]; + var newSpaces = space.getMaximalFreeRects( rect ); + // add either the original space or the new spaces to the revised spaces + if ( newSpaces ) { + revisedSpaces.push.apply( revisedSpaces, newSpaces ); + } else { + revisedSpaces.push( space ); + } + } + + this.spaces = revisedSpaces; + + this.mergeSortSpaces(); +}; + +proto.mergeSortSpaces = function() { + // remove redundant spaces + Packer.mergeRects( this.spaces ); + this.spaces.sort( this.sorter ); +}; + +// add a space back +proto.addSpace = function( rect ) { + this.spaces.push( rect ); + this.mergeSortSpaces(); +}; + +// -------------------------- utility functions -------------------------- // + +/** + * Remove redundant rectangle from array of rectangles + * @param {Array} rects: an array of Rects + * @returns {Array} rects: an array of Rects +**/ +Packer.mergeRects = function( rects ) { + var i = 0; + var rect = rects[i]; + + rectLoop: + while ( rect ) { + var j = 0; + var compareRect = rects[ i + j ]; + + while ( compareRect ) { + if ( compareRect == rect ) { + j++; // next + } else if ( compareRect.contains( rect ) ) { + // remove rect + rects.splice( i, 1 ); + rect = rects[i]; // set next rect + continue rectLoop; // bail on compareLoop + } else if ( rect.contains( compareRect ) ) { + // remove compareRect + rects.splice( i + j, 1 ); + } else { + j++; + } + compareRect = rects[ i + j ]; // set next compareRect + } + i++; + rect = rects[i]; + } + + return rects; +}; + + +// -------------------------- sorters -------------------------- // + +// functions for sorting rects in order +var sorters = { + // top down, then left to right + downwardLeftToRight: function( a, b ) { + return a.y - b.y || a.x - b.x; + }, + // left to right, then top down + rightwardTopToBottom: function( a, b ) { + return a.x - b.x || a.y - b.y; + } +}; + + +// -------------------------- -------------------------- // + +return Packer; + +})); + +/** + * Packery Item Element +**/ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'packery/item',[ + 'outlayer/outlayer', + './rect' + ], + factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + require('outlayer'), + require('./rect') + ); + } else { + // browser global + window.Packery.Item = factory( + window.Outlayer, + window.Packery.Rect + ); + } + +}( window, function factory( Outlayer, Rect ) { +'use strict'; + +// -------------------------- Item -------------------------- // + +var docElemStyle = document.documentElement.style; + +var transformProperty = typeof docElemStyle.transform == 'string' ? + 'transform' : 'WebkitTransform'; + +// sub-class Item +var Item = function PackeryItem() { + Outlayer.Item.apply( this, arguments ); +}; + +var proto = Item.prototype = Object.create( Outlayer.Item.prototype ); + +var __create = proto._create; +proto._create = function() { + // call default _create logic + __create.call( this ); + this.rect = new Rect(); +}; + +var _moveTo = proto.moveTo; +proto.moveTo = function( x, y ) { + // don't shift 1px while dragging + var dx = Math.abs( this.position.x - x ); + var dy = Math.abs( this.position.y - y ); + + var canHackGoTo = this.layout.dragItemCount && !this.isPlacing && + !this.isTransitioning && dx < 1 && dy < 1; + if ( canHackGoTo ) { + this.goTo( x, y ); + return; + } + _moveTo.apply( this, arguments ); +}; + +// -------------------------- placing -------------------------- // + +proto.enablePlacing = function() { + this.removeTransitionStyles(); + // remove transform property from transition + if ( this.isTransitioning && transformProperty ) { + this.element.style[ transformProperty ] = 'none'; + } + this.isTransitioning = false; + this.getSize(); + this.layout._setRectSize( this.element, this.rect ); + this.isPlacing = true; +}; + +proto.disablePlacing = function() { + this.isPlacing = false; +}; + +// ----- ----- // + +// remove element from DOM +proto.removeElem = function() { + this.element.parentNode.removeChild( this.element ); + // add space back to packer + this.layout.packer.addSpace( this.rect ); + this.emitEvent( 'remove', [ this ] ); +}; + +// ----- dropPlaceholder ----- // + +proto.showDropPlaceholder = function() { + var dropPlaceholder = this.dropPlaceholder; + if ( !dropPlaceholder ) { + // create dropPlaceholder + dropPlaceholder = this.dropPlaceholder = document.createElement('div'); + dropPlaceholder.className = 'packery-drop-placeholder'; + dropPlaceholder.style.position = 'absolute'; + } + + dropPlaceholder.style.width = this.size.width + 'px'; + dropPlaceholder.style.height = this.size.height + 'px'; + this.positionDropPlaceholder(); + this.layout.element.appendChild( dropPlaceholder ); +}; + +proto.positionDropPlaceholder = function() { + this.dropPlaceholder.style[ transformProperty ] = 'translate(' + + this.rect.x + 'px, ' + this.rect.y + 'px)'; +}; + +proto.hideDropPlaceholder = function() { + this.layout.element.removeChild( this.dropPlaceholder ); +}; + +// ----- ----- // + +return Item; + +})); + +/*! + * Packery v2.0.0 + * Gapless, draggable grid layouts + * + * Licensed GPLv3 for open source use + * or Packery Commercial License for commercial use + * + * http://packery.metafizzy.co + * Copyright 2016 Metafizzy + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( [ + 'get-size/get-size', + 'outlayer/outlayer', + './rect', + './packer', + './item' + ], + factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + require('get-size'), + require('outlayer'), + require('./rect'), + require('./packer'), + require('./item') + ); + } else { + // browser global + window.Packery = factory( + window.getSize, + window.Outlayer, + window.Packery.Rect, + window.Packery.Packer, + window.Packery.Item + ); + } + +}( window, function factory( getSize, Outlayer, Rect, Packer, Item ) { +'use strict'; + +// ----- Rect ----- // + +// allow for pixel rounding errors IE8-IE11 & Firefox; #227 +Rect.prototype.canFit = function( rect ) { + return this.width >= rect.width - 1 && this.height >= rect.height - 1; +}; + +// -------------------------- Packery -------------------------- // + +// create an Outlayer layout class +var Packery = Outlayer.create('packery'); +Packery.Item = Item; + +var proto = Packery.prototype; + +proto._create = function() { + // call super + Outlayer.prototype._create.call( this ); + + // initial properties + this.packer = new Packer(); + // packer for drop targets + this.shiftPacker = new Packer(); + this.isEnabled = true; + + this.dragItemCount = 0; + + // create drag handlers + var _this = this; + this.handleDraggabilly = { + dragStart: function() { + _this.itemDragStart( this.element ); + }, + dragMove: function() { + _this.itemDragMove( this.element, this.position.x, this.position.y ); + }, + dragEnd: function() { + _this.itemDragEnd( this.element ); + } + }; + + this.handleUIDraggable = { + start: function handleUIDraggableStart( event, ui ) { + // HTML5 may trigger dragstart, dismiss HTML5 dragging + if ( !ui ) { + return; + } + _this.itemDragStart( event.currentTarget ); + }, + drag: function handleUIDraggableDrag( event, ui ) { + if ( !ui ) { + return; + } + _this.itemDragMove( event.currentTarget, ui.position.left, ui.position.top ); + }, + stop: function handleUIDraggableStop( event, ui ) { + if ( !ui ) { + return; + } + _this.itemDragEnd( event.currentTarget ); + } + }; + +}; + + +// ----- init & layout ----- // + +/** + * logic before any new layout + */ +proto._resetLayout = function() { + this.getSize(); + + this._getMeasurements(); + + // reset packer + var width, height, sortDirection; + // packer settings, if horizontal or vertical + if ( this._getOption('horizontal') ) { + width = Infinity; + height = this.size.innerHeight + this.gutter; + sortDirection = 'rightwardTopToBottom'; + } else { + width = this.size.innerWidth + this.gutter; + height = Infinity; + sortDirection = 'downwardLeftToRight'; + } + + this.packer.width = this.shiftPacker.width = width; + this.packer.height = this.shiftPacker.height = height; + this.packer.sortDirection = this.shiftPacker.sortDirection = sortDirection; + + this.packer.reset(); + + // layout + this.maxY = 0; + this.maxX = 0; +}; + +/** + * update columnWidth, rowHeight, & gutter + * @private + */ +proto._getMeasurements = function() { + this._getMeasurement( 'columnWidth', 'width' ); + this._getMeasurement( 'rowHeight', 'height' ); + this._getMeasurement( 'gutter', 'width' ); +}; + +proto._getItemLayoutPosition = function( item ) { + this._setRectSize( item.element, item.rect ); + if ( this.isShifting || this.dragItemCount > 0 ) { + var packMethod = this._getPackMethod(); + this.packer[ packMethod ]( item.rect ); + } else { + this.packer.pack( item.rect ); + } + + this._setMaxXY( item.rect ); + return item.rect; +}; + +proto.shiftLayout = function() { + this.isShifting = true; + this.layout(); + delete this.isShifting; +}; + +proto._getPackMethod = function() { + return this._getOption('horizontal') ? 'rowPack' : 'columnPack'; +}; + + +/** + * set max X and Y value, for size of container + * @param {Packery.Rect} rect + * @private + */ +proto._setMaxXY = function( rect ) { + this.maxX = Math.max( rect.x + rect.width, this.maxX ); + this.maxY = Math.max( rect.y + rect.height, this.maxY ); +}; + +/** + * set the width and height of a rect, applying columnWidth and rowHeight + * @param {Element} elem + * @param {Packery.Rect} rect + */ +proto._setRectSize = function( elem, rect ) { + var size = getSize( elem ); + var w = size.outerWidth; + var h = size.outerHeight; + // size for columnWidth and rowHeight, if available + // only check if size is non-zero, #177 + if ( w || h ) { + w = this._applyGridGutter( w, this.columnWidth ); + h = this._applyGridGutter( h, this.rowHeight ); + } + // rect must fit in packer + rect.width = Math.min( w, this.packer.width ); + rect.height = Math.min( h, this.packer.height ); +}; + +/** + * fits item to columnWidth/rowHeight and adds gutter + * @param {Number} measurement - item width or height + * @param {Number} gridSize - columnWidth or rowHeight + * @returns measurement + */ +proto._applyGridGutter = function( measurement, gridSize ) { + // just add gutter if no gridSize + if ( !gridSize ) { + return measurement + this.gutter; + } + gridSize += this.gutter; + // fit item to columnWidth/rowHeight + var remainder = measurement % gridSize; + var mathMethod = remainder && remainder < 1 ? 'round' : 'ceil'; + measurement = Math[ mathMethod ]( measurement / gridSize ) * gridSize; + return measurement; +}; + +proto._getContainerSize = function() { + if ( this._getOption('horizontal') ) { + return { + width: this.maxX - this.gutter + }; + } else { + return { + height: this.maxY - this.gutter + }; + } +}; + + +// -------------------------- stamp -------------------------- // + +/** + * makes space for element + * @param {Element} elem + */ +proto._manageStamp = function( elem ) { + + var item = this.getItem( elem ); + var rect; + if ( item && item.isPlacing ) { + rect = item.rect; + } else { + var offset = this._getElementOffset( elem ); + rect = new Rect({ + x: this._getOption('originLeft') ? offset.left : offset.right, + y: this._getOption('originTop') ? offset.top : offset.bottom + }); + } + + this._setRectSize( elem, rect ); + // save its space in the packer + this.packer.placed( rect ); + this._setMaxXY( rect ); +}; + +// -------------------------- methods -------------------------- // + +function verticalSorter( a, b ) { + return a.position.y - b.position.y || a.position.x - b.position.x; +} + +function horizontalSorter( a, b ) { + return a.position.x - b.position.x || a.position.y - b.position.y; +} + +proto.sortItemsByPosition = function() { + var sorter = this._getOption('horizontal') ? horizontalSorter : verticalSorter; + this.items.sort( sorter ); +}; + +/** + * Fit item element in its current position + * Packery will position elements around it + * useful for expanding elements + * + * @param {Element} elem + * @param {Number} x - horizontal destination position, optional + * @param {Number} y - vertical destination position, optional + */ +proto.fit = function( elem, x, y ) { + var item = this.getItem( elem ); + if ( !item ) { + return; + } + + // stamp item to get it out of layout + this.stamp( item.element ); + // set placing flag + item.enablePlacing(); + this.updateShiftTargets( item ); + // fall back to current position for fitting + x = x === undefined ? item.rect.x: x; + y = y === undefined ? item.rect.y: y; + // position it best at its destination + this.shift( item, x, y ); + this._bindFitEvents( item ); + item.moveTo( item.rect.x, item.rect.y ); + // layout everything else + this.shiftLayout(); + // return back to regularly scheduled programming + this.unstamp( item.element ); + this.sortItemsByPosition(); + item.disablePlacing(); +}; + +/** + * emit event when item is fit and other items are laid out + * @param {Packery.Item} item + * @private + */ +proto._bindFitEvents = function( item ) { + var _this = this; + var ticks = 0; + function onLayout() { + ticks++; + if ( ticks != 2 ) { + return; + } + _this.dispatchEvent( 'fitComplete', null, [ item ] ); + } + // when item is laid out + item.once( 'layout', onLayout ); + // when all items are laid out + this.once( 'layoutComplete', onLayout ); +}; + +// -------------------------- resize -------------------------- // + +// debounced, layout on resize +proto.resize = function() { + // don't trigger if size did not change + // or if resize was unbound. See #285, outlayer#9 + if ( !this.isResizeBound || !this.needsResizeLayout() ) { + return; + } + + if ( this.options.shiftPercentResize ) { + this.resizeShiftPercentLayout(); + } else { + this.layout(); + } +}; + +/** + * check if layout is needed post layout + * @returns Boolean + */ +proto.needsResizeLayout = function() { + var size = getSize( this.element ); + var innerSize = this._getOption('horizontal') ? 'innerHeight' : 'innerWidth'; + return size[ innerSize ] != this.size[ innerSize ]; +}; + +proto.resizeShiftPercentLayout = function() { + var items = this._getItemsForLayout( this.items ); + + var isHorizontal = this._getOption('horizontal'); + var coord = isHorizontal ? 'y' : 'x'; + var measure = isHorizontal ? 'height' : 'width'; + var segmentName = isHorizontal ? 'rowHeight' : 'columnWidth'; + var innerSize = isHorizontal ? 'innerHeight' : 'innerWidth'; + + // proportional re-align items + var previousSegment = this[ segmentName ]; + previousSegment = previousSegment && previousSegment + this.gutter; + + if ( previousSegment ) { + this._getMeasurements(); + var currentSegment = this[ segmentName ] + this.gutter; + items.forEach( function( item ) { + var seg = Math.round( item.rect[ coord ] / previousSegment ); + item.rect[ coord ] = seg * currentSegment; + }); + } else { + var currentSize = getSize( this.element )[ innerSize ] + this.gutter; + var previousSize = this.packer[ measure ]; + items.forEach( function( item ) { + item.rect[ coord ] = ( item.rect[ coord ] / previousSize ) * currentSize; + }); + } + + this.shiftLayout(); +}; + +// -------------------------- drag -------------------------- // + +/** + * handle an item drag start event + * @param {Element} elem + */ +proto.itemDragStart = function( elem ) { + if ( !this.isEnabled ) { + return; + } + this.stamp( elem ); + // this.ignore( elem ); + var item = this.getItem( elem ); + if ( !item ) { + return; + } + + item.enablePlacing(); + item.showDropPlaceholder(); + this.dragItemCount++; + this.updateShiftTargets( item ); +}; + +proto.updateShiftTargets = function( dropItem ) { + this.shiftPacker.reset(); + + // pack stamps + this._getBoundingRect(); + var isOriginLeft = this._getOption('originLeft'); + var isOriginTop = this._getOption('originTop'); + this.stamps.forEach( function( stamp ) { + // ignore dragged item + var item = this.getItem( stamp ); + if ( item && item.isPlacing ) { + return; + } + var offset = this._getElementOffset( stamp ); + var rect = new Rect({ + x: isOriginLeft ? offset.left : offset.right, + y: isOriginTop ? offset.top : offset.bottom + }); + this._setRectSize( stamp, rect ); + // save its space in the packer + this.shiftPacker.placed( rect ); + }, this ); + + // reset shiftTargets + var isHorizontal = this._getOption('horizontal'); + var segmentName = isHorizontal ? 'rowHeight' : 'columnWidth'; + var measure = isHorizontal ? 'height' : 'width'; + + this.shiftTargetKeys = []; + this.shiftTargets = []; + var boundsSize; + var segment = this[ segmentName ]; + segment = segment && segment + this.gutter; + + if ( segment ) { + var segmentSpan = Math.ceil( dropItem.rect[ measure ] / segment ); + var segs = Math.floor( ( this.shiftPacker[ measure ] + this.gutter ) / segment ); + boundsSize = ( segs - segmentSpan ) * segment; + // add targets on top + for ( var i=0; i < segs; i++ ) { + this._addShiftTarget( i * segment, 0, boundsSize ); + } + } else { + boundsSize = ( this.shiftPacker[ measure ] + this.gutter ) - dropItem.rect[ measure ]; + this._addShiftTarget( 0, 0, boundsSize ); + } + + // pack each item to measure where shiftTargets are + var items = this._getItemsForLayout( this.items ); + var packMethod = this._getPackMethod(); + items.forEach( function( item ) { + var rect = item.rect; + this._setRectSize( item.element, rect ); + this.shiftPacker[ packMethod ]( rect ); + + // add top left corner + this._addShiftTarget( rect.x, rect.y, boundsSize ); + // add bottom left / top right corner + var cornerX = isHorizontal ? rect.x + rect.width : rect.x; + var cornerY = isHorizontal ? rect.y : rect.y + rect.height; + this._addShiftTarget( cornerX, cornerY, boundsSize ); + + if ( segment ) { + // add targets for each column on bottom / row on right + var segSpan = Math.round( rect[ measure ] / segment ); + for ( var i=1; i < segSpan; i++ ) { + var segX = isHorizontal ? cornerX : rect.x + segment * i; + var segY = isHorizontal ? rect.y + segment * i : cornerY; + this._addShiftTarget( segX, segY, boundsSize ); + } + } + }, this ); + +}; + +proto._addShiftTarget = function( x, y, boundsSize ) { + var checkCoord = this._getOption('horizontal') ? y : x; + if ( checkCoord !== 0 && checkCoord > boundsSize ) { + return; + } + // create string for a key, easier to keep track of what targets + var key = x + ',' + y; + var hasKey = this.shiftTargetKeys.indexOf( key ) != -1; + if ( hasKey ) { + return; + } + this.shiftTargetKeys.push( key ); + this.shiftTargets.push({ x: x, y: y }); +}; + +// -------------------------- drop -------------------------- // + +proto.shift = function( item, x, y ) { + var shiftPosition; + var minDistance = Infinity; + var position = { x: x, y: y }; + this.shiftTargets.forEach( function( target ) { + var distance = getDistance( target, position ); + if ( distance < minDistance ) { + shiftPosition = target; + minDistance = distance; + } + }); + item.rect.x = shiftPosition.x; + item.rect.y = shiftPosition.y; +}; + +function getDistance( a, b ) { + var dx = b.x - a.x; + var dy = b.y - a.y; + return Math.sqrt( dx * dx + dy * dy ); +} + +// -------------------------- drag move -------------------------- // + +var DRAG_THROTTLE_TIME = 120; + +/** + * handle an item drag move event + * @param {Element} elem + * @param {Number} x - horizontal change in position + * @param {Number} y - vertical change in position + */ +proto.itemDragMove = function( elem, x, y ) { + var item = this.isEnabled && this.getItem( elem ); + if ( !item ) { + return; + } + + x -= this.size.paddingLeft; + y -= this.size.paddingTop; + + var _this = this; + function onDrag() { + _this.shift( item, x, y ); + item.positionDropPlaceholder(); + _this.layout(); + } + + // throttle + var now = new Date(); + if ( this._itemDragTime && now - this._itemDragTime < DRAG_THROTTLE_TIME ) { + clearTimeout( this.dragTimeout ); + this.dragTimeout = setTimeout( onDrag, DRAG_THROTTLE_TIME ); + } else { + onDrag(); + this._itemDragTime = now; + } +}; + +// -------------------------- drag end -------------------------- // + +/** + * handle an item drag end event + * @param {Element} elem + */ +proto.itemDragEnd = function( elem ) { + var item = this.isEnabled && this.getItem( elem ); + if ( !item ) { + return; + } + + clearTimeout( this.dragTimeout ); + item.element.classList.add('is-positioning-post-drag'); + + var completeCount = 0; + var _this = this; + function onDragEndLayoutComplete() { + completeCount++; + if ( completeCount != 2 ) { + return; + } + // reset drag item + item.element.classList.remove('is-positioning-post-drag'); + item.hideDropPlaceholder(); + _this.dispatchEvent( 'dragItemPositioned', null, [ item ] ); + } + + item.once( 'layout', onDragEndLayoutComplete ); + this.once( 'layoutComplete', onDragEndLayoutComplete ); + item.moveTo( item.rect.x, item.rect.y ); + this.layout(); + this.dragItemCount = Math.max( 0, this.dragItemCount - 1 ); + this.sortItemsByPosition(); + item.disablePlacing(); + this.unstamp( item.element ); +}; + +/** + * binds Draggabilly events + * @param {Draggabilly} draggie + */ +proto.bindDraggabillyEvents = function( draggie ) { + this._bindDraggabillyEvents( draggie, 'on' ); +}; + +proto.unbindDraggabillyEvents = function( draggie ) { + this._bindDraggabillyEvents( draggie, 'off' ); +}; + +proto._bindDraggabillyEvents = function( draggie, method ) { + var handlers = this.handleDraggabilly; + draggie[ method ]( 'dragStart', handlers.dragStart ); + draggie[ method ]( 'dragMove', handlers.dragMove ); + draggie[ method ]( 'dragEnd', handlers.dragEnd ); +}; + +/** + * binds jQuery UI Draggable events + * @param {jQuery} $elems + */ +proto.bindUIDraggableEvents = function( $elems ) { + this._bindUIDraggableEvents( $elems, 'on' ); +}; + +proto.unbindUIDraggableEvents = function( $elems ) { + this._bindUIDraggableEvents( $elems, 'off' ); +}; + +proto._bindUIDraggableEvents = function( $elems, method ) { + var handlers = this.handleUIDraggable; + $elems + [ method ]( 'dragstart', handlers.start ) + [ method ]( 'drag', handlers.drag ) + [ method ]( 'dragstop', handlers.stop ); +}; + +// ----- destroy ----- // + +var _destroy = proto.destroy; +proto.destroy = function() { + _destroy.apply( this, arguments ); + // disable flag; prevent drag events from triggering. #72 + this.isEnabled = false; +}; + +// ----- ----- // + +Packery.Rect = Rect; +Packery.Packer = Packer; + +return Packery; + +})); + diff --git a/www/index.html b/www/index.html index c1d25389..20b68cd3 100644 --- a/www/index.html +++ b/www/index.html @@ -2,6 +2,8 @@ <html> <head> + + <meta charset="utf-8"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width"> <meta http-equiv="Content-Security-Policy" content="img-src * 'self' data:; default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' https://www.google.com 'unsafe-inline' 'unsafe-eval'; "> @@ -35,13 +37,11 @@ <!-- compiled css output --> <link href="css/ionic.app.css" rel="stylesheet"> <link href="css/style.css" rel="stylesheet"> - - <!-- ionic/angularjs js --> - - + + <script src="lib/ionic/js/ionic.bundle.min.js"></script> <script src="external/angular-ios9-uiwebview.patch.js"></script> @@ -58,13 +58,14 @@ - - <!-- your app's js --> <!-- files in external/ are 3rd party libraries I modified. The better way is to fork and make your mods. I'll get to that eventually --> + + + <script src="lib/angular-awesome-slider/dist/angular-awesome-slider.min.js"> </script> @@ -124,12 +125,27 @@ <script src="lib/ionic-content-banner/dist/ionic.content.banner.min.js"></script> <script src="external/FileSaver.min.js"></script> <script src="external/canvas-toBlob.js"></script> + + <script src="external/imagesloaded.pkgd.js"></script> + <script src="external/packery.pkgd.js" ></script> + <script src="external/draggabilly.pkgd.js" ></script> + + <!-- <script src="external/imagesloaded.pkgd.js"></script>--> + + + + + </head> + + + <!-- <body ng-app="starter" > --> <!-- I want to start angular only after cordova device is ready, so I'll tag it on device ready --> + <body bgcolor="#555555"> <!-- For some reason - which I haven't debugged yet, when I was using the ionic side menu template @@ -266,7 +282,10 @@ angular.bootstrap(document, ['zmApp']); }); </script> + + + </body> diff --git a/www/js/MontageCtrl.js b/www/js/MontageCtrl.js index 7e44323c..03bde86c 100644 --- a/www/js/MontageCtrl.js +++ b/www/js/MontageCtrl.js @@ -1,7 +1,7 @@ // Controller for the montage view /* jshint -W041 */ /* jslint browser: true*/ -/* global cordova,StatusBar,angular,console,ionic,Masonry */ +/* global cordova,StatusBar,angular,console,ionic,Packery, Draggabilly, imagesLoaded */ angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', ['$scope', '$rootScope', 'ZMDataModel', 'message', '$ionicSideMenuDelegate', '$timeout', '$interval', '$ionicModal', '$ionicLoading', '$http', '$state', '$ionicPopup', '$stateParams', '$ionicHistory', '$ionicScrollDelegate', '$ionicPlatform', 'zm', '$ionicPopover', '$controller', 'imageLoadingDataShare', '$window', function ($scope, $rootScope, ZMDataModel, message, $ionicSideMenuDelegate, $timeout, $interval, $ionicModal, $ionicLoading, $http, $state, $ionicPopup, $stateParams, $ionicHistory, $ionicScrollDelegate, $ionicPlatform, zm, $ionicPopover, $controller, imageLoadingDataShare, $window) { @@ -35,6 +35,8 @@ angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', ['$scope', ' // while original will have a copy of the order returned by ZM var oldMonitors = []; // To keep old order if user cancels after sort; + + var pckry = ""; // Montage display order may be different so don't // mangle monitors as it will affect other screens @@ -45,6 +47,8 @@ angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', ['$scope', ' var hiddenOrder = []; // 1 = hide, 0 = don't hide var tempMonitors = message; + + if (tempMonitors.length == 0) { $rootScope.zmPopup= $ionicPopup.alert({ @@ -74,19 +78,70 @@ angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', ['$scope', ' var loginData = ZMDataModel.getLogin(); $scope.packMontage = loginData.packMontage; + //var pckry; + // init packery - if (0) - { - var elem = angular.element(document.getElementById('.grid')); - var msnry = new Masonry( elem, { - // options - itemSelector: '.grid-item', - columnWidth: 200 + + +function initPackery() +{ + + var progressCalled = false; + + var elem = angular.element(document.getElementById("mygrid")); + pckry = new Packery('.grid', + { + itemSelector: '.grid-item', + percentPosition: true, + columnWidth: '.grid-sizer', + }); + console.log ("**** mygrid is " + JSON.stringify(elem)); + + imagesLoaded(elem).on('progress', function() { + console.log ("******** SOME IMAGE LOADED"); + progressCalled = true; + pckry.layout(); + }); + + imagesLoaded(elem).on('always', function() { + console.log ("******** ALL IMAGE LOADED"); + + if (!progressCalled) + { + // this is a hack - trying to figure out why this + // problem is occuring - my hack does not work + console.log ("*** BUG SOMECALLED WAS NOT CALLED"); + /* pckry.destroy(); + pckry = new Packery('.grid', + { + itemSelector: '.grid-item', + percentPosition: true, + columnWidth: '.grid-sizer', + }); + pckry.layout();*/ + pckry.reloadItems(); + + } + + pckry.getItemElements().forEach(function (itemElem) { + var draggie = new Draggabilly(itemElem); + pckry.bindDraggabillyEvents(draggie); + console.log ("**** MAPPED DRAG"); + // pckry.shiftLayout(); + }); + + }); + + } + + + + // -------------------------------------------------------- // Handling of back button in case modal is open should // close the modal @@ -378,6 +433,8 @@ angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', ['$scope', ' montageOrder.toString() + " and hidden order as " + hiddenOrder.toString()); $scope.modal.remove(); + ZMDataModel.zmLog ("Reloading packery"); + $timeout (function(){pckry.reloadItems(); pckry.layout();},500); }; $scope.cancelReorder = function () { @@ -702,7 +759,7 @@ angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', ['$scope', ' loadNotifications(); // console.log ("Refreshing Image..."); }.bind(this), ld.refreshSec * 1000); - + // }.bind(this), 60 * 1000); //$interval.cancel(modalIntervalHandle); @@ -859,11 +916,19 @@ angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', ['$scope', ' }); + $scope.$on('$ionicView.afterEnter', function () { + console.log("**VIEW ** Montage Ctrl AFTER ENTER"); + $timeout ( function () {initPackery(); },500); + + }); + + $scope.$on('$ionicView.beforeLeave', function () { // console.log("**VIEW ** Montage Ctrl Left, force removing modal"); console.log ("beforeLeave:Cancelling timer"); $interval.cancel($rootScope.intervalHandle); + pckry.destroy(); // make sure this is applied in scope digest to stop network pull @@ -877,6 +942,12 @@ angular.module('zmApp.controllers').controller('zmApp.MontageCtrl', ['$scope', ' } }); + + $scope.loadedImage = function() + { + // console.log ("IMAGE LOADED"); + }; + $scope.$on('$ionicView.unloaded', function () { // console.log("**************** CLOSING WINDOW ***************************"); diff --git a/www/templates/montage.html b/www/templates/montage.html index 42294e09..46cbbd8b 100644 --- a/www/templates/montage.html +++ b/www/templates/montage.html @@ -20,94 +20,68 @@ <button class="button button-icon button-clear ion-android-contract" ng-click="switchMinimal()"> </button> - <!--<button class="button button-icon ion-ios-help-outline" ng-click="popover.show($event)"></button>--> - - </ion-nav-buttons> - <ion-content has-bouncing="false" style="background-color:#444444"> + <ion-content ng-cloak has-bouncing="false" style="background-color:#444444"> - <ion-refresher pulling-text="Pull to reload Monitors..." spinner="bubbles" on-refresh="doRefresh()"> - </ion-refresher> + <!--<ion-refresher pulling-text="Pull to reload Monitors..." spinner="bubbles" on-refresh="doRefresh()"> + </ion-refresher>--> - <span ng-show="!minimal"> <div class="range range-positive"> <i style="color:#bbbbbb" class="icon ion-grid"></i> <input type="range" ng-model="slider.monsize" min="1" - max="9" ng-change="sliderChanged(slider.monsize)"> + max="5" ng-change="sliderChanged(slider.monsize)"> <i style="color:#bbbbbb" class="icon ion-image"></i> </div> </span> - - <div ng-style="packMontage ? { '-webkit-column-count':10-slider.monsize,'-webkit-column-gap':'0px','line-height':'0px','-webkit-column-fill': 'balance', 'column-fill': 'balance'} : {'-webkit-column-count':10-slider.monsize,'-webkit-column-gap':'0px','line-height':'0px','display':'-webkit-flex','-webkit-flex-direction':'row','flex-direction':'row', 'flex-wrap':'wrap' }"> - - <!--<div ng-style="packMontage ? { '-webkit-column-count':slider.monsize, '-webkit-column-gap':'0px','line-height':'0px' } : { 'flex':'display', '-webkit-column-count':slider.monsize }">--> - - + <div class="grid" id="mygrid"> + <div class="grid-sizer"></div> <span ng-repeat="monitor in MontageMonitors|limitTo: monLimit" ng-if="monitor.Monitor.Function!='None' && monitor.Monitor.listDisplay!='noshow' && monitor.Monitor.Enabled !='0'"> - - <!-- back to ng-src. spinner was not updating when rand changes--> - <!-- moving to single so I can rely on rand for reloads --> - - - <!--src=" /zm/cgi-bin/nph-zms?source=event&mode=jpeg&event=7664&frame=1&scale=100&rate=100&maxfps=5&replay=gapless&auth=6b8c60f81fa2edff94f80f12bdebd98d&connkey=286643&rand=1453671607"--> - - <div style="position: relative;width:{{devWidth/(10-slider.monsize)}}px;"> - <div ng-if="!isModalActive"> + <div class="grid-item"> + <figure> + <div ng-if="!isModalActive" > <div ng-if="$root.authSession!='undefined' && !isBackground()"> <div ng-if = "!minimal"> - <img id="img-{{$index}}" image-spinner-src="{{LoginData.streamingurl}}/nph-zms?mode=single&monitor={{monitor.Monitor.Id}}&scale={{LoginData.montageQuality}}{{$root.authSession}}&rand={{$root.rand}}" ng-click="openModal(monitor.Monitor.Id, monitor.Monitor.Controllable, monitor.Monitor.ControlId, monitor.Monitor.connKey)" image-spinner-loader="lines" style=" - width: 100% !important; - height: auto !important;" /> - </div> - + + <img id="img-{{$index}}" image-spinner-src="{{LoginData.streamingurl}}/nph-zms?mode=single&monitor={{monitor.Monitor.Id}}&scale={{LoginData.montageQuality}}{{$root.authSession}}&rand={{$root.rand}}" ng-click="openModal(monitor.Monitor.Id, monitor.Monitor.Controllable, monitor.Monitor.ControlId, monitor.Monitor.connKey)" image-spinner-loader="lines" /> + + </div> + <div ng-if = "minimal"> - <img id="{{img-$index}}" image-spinner-src="{{LoginData.streamingurl}}/nph-zms?mode=single&monitor={{monitor.Monitor.Id}}&scale={{LoginData.montageQuality}}{{$root.authSession}}&rand={{$root.rand}}" ng-click="openModal(monitor.Monitor.Id, monitor.Monitor.Controllable, monitor.Monitor.ControlId, monitor.Monitor.connKey)" image-spinner-loader="lines" style="margin-top:0px; - width: 100% !important; - height: auto !important;" /> + <img id="{{img-$index}}" image-spinner-src="{{LoginData.streamingurl}}/nph-zms?mode=single&monitor={{monitor.Monitor.Id}}&scale={{LoginData.montageQuality}}{{$root.authSession}}&rand={{$root.rand}}" ng-click="openModal(monitor.Monitor.Id, monitor.Monitor.Controllable, monitor.Monitor.ControlId, monitor.Monitor.connKey)" image-spinner-loader="lines" /> </div> - - </div> <div ng-if="!$root.authSession=='undefined' || isBackground()"> - <img image-spinner-src="img/pausevideo.png" style="width: 100% !important; - height: auto !important;" /> + <img image-spinner-src="img/pausevideo.png" /> </div> - - <span ng-show="!minimal"> - <div id = "slowpulse" style="position:absolute; bottom:0%; right:0%;white-space:nowrap;overflow:hidden;" - ng-class="monitor.Monitor.isAlarmed=='true'?'alarmed-header animated infinite flash':'header'"> - <i class="ion-monitor"></i> - {{monitor.Monitor.Name}} - </div> - </span> - - <div id="slowpulse" ng-class="monitor.Monitor.isAlarmed=='true'?'minimized-alarmed-header animated infinite flash':''" ng-if="minimal"></div> - - - </div> - </div> + + <figcaption id="slowpulse" ng-class="monitor.Monitor.isAlarmed=='true'?'alarmed-figcaption animated infinite flash':'normal-figcaption'" > - <div ng-if="isModalActive"> - <img image-spinner-src="img/pausevideo.png" style="margin-top:0px; - width: 100% !important; - height: auto !important;" /> - </div> + <i class="ion-monitor"></i> + {{monitor.Monitor.Name}} + </figcaption> - </span> + + </div> <!-- modal not active--> + <div ng-if="isModalActive"> + <img image-spinner-src="img/pausevideo.png" /> + </div> + </figure> + </div> + </span> <!-- ngrepeat --> </div> - <!-- ngrepeat --> + |
