/** * Outlayer Item */ ( function( window, factory ) { 'use strict'; // universal module definition if ( typeof define === 'function' && define.amd ) { // AMD define( [ 'eventEmitter/EventEmitter', 'get-size/get-size', 'get-style-property/get-style-property', 'fizzy-ui-utils/utils' ], function( EventEmitter, getSize, getStyleProperty, utils ) { return factory( window, EventEmitter, getSize, getStyleProperty, utils ); } ); } else if (typeof exports === 'object') { // CommonJS module.exports = factory( window, require('wolfy87-eventemitter'), require('get-size'), require('desandro-get-style-property'), require('fizzy-ui-utils') ); } else { // browser global window.Outlayer = {}; window.Outlayer.Item = factory( window, window.EventEmitter, window.getSize, window.getStyleProperty, window.fizzyUIUtils ); } }( window, function factory( window, EventEmitter, getSize, getStyleProperty, utils ) { 'use strict'; // ----- helpers ----- // var getComputedStyle = window.getComputedStyle; var getStyle = getComputedStyle ? function( elem ) { return getComputedStyle( elem, null ); } : function( elem ) { return elem.currentStyle; }; function isEmptyObj( obj ) { for ( var prop in obj ) { return false; } prop = null; return true; } // -------------------------- CSS3 support -------------------------- // var transitionProperty = getStyleProperty('transition'); var transformProperty = getStyleProperty('transform'); var supportsCSS3 = transitionProperty && transformProperty; var is3d = !!getStyleProperty('perspective'); var transitionEndEvent = { WebkitTransition: 'webkitTransitionEnd', MozTransition: 'transitionend', OTransition: 'otransitionend', transition: 'transitionend' }[ transitionProperty ]; // properties that could have vendor prefix var prefixableProperties = [ 'transform', 'transition', 'transitionDuration', 'transitionProperty' ]; // cache all vendor properties var vendorProperties = ( function() { var cache = {}; for ( var i=0, len = prefixableProperties.length; i < len; i++ ) { var prop = prefixableProperties[i]; var supportedProp = getStyleProperty( prop ); if ( supportedProp && supportedProp !== prop ) { cache[ prop ] = supportedProp; } } return cache; })(); // -------------------------- 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 EventEmitter utils.extend( Item.prototype, EventEmitter.prototype ); Item.prototype._create = function() { // transition objects this._transn = { ingProperties: {}, clean: {}, onEnd: {} }; this.css({ position: 'absolute' }); }; // trigger specified handler for event type Item.prototype.handleEvent = function( event ) { var method = 'on' + event.type; if ( this[ method ] ) { this[ method ]( event ); } }; Item.prototype.getSize = function() { this.size = getSize( this.element ); }; /** * apply CSS styles to element * @param {Object} style */ Item.prototype.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 Item.prototype.getPosition = function() { var style = getStyle( this.element ); var layoutOptions = this.layout.options; var isOriginLeft = layoutOptions.isOriginLeft; var isOriginTop = layoutOptions.isOriginTop; 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 Item.prototype.layoutPosition = function() { var layoutSize = this.layout.size; var layoutOptions = this.layout.options; var style = {}; // x var xPadding = layoutOptions.isOriginLeft ? 'paddingLeft' : 'paddingRight'; var xProperty = layoutOptions.isOriginLeft ? 'left' : 'right'; var xResetProperty = layoutOptions.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 = layoutOptions.isOriginTop ? 'paddingTop' : 'paddingBottom'; var yProperty = layoutOptions.isOriginTop ? 'top' : 'bottom'; var yResetProperty = layoutOptions.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 ] ); }; Item.prototype.getXValue = function( x ) { var layoutOptions = this.layout.options; return layoutOptions.percentPosition && !layoutOptions.isHorizontal ? ( ( x / this.layout.size.width ) * 100 ) + '%' : x + 'px'; }; Item.prototype.getYValue = function( y ) { var layoutOptions = this.layout.options; return layoutOptions.percentPosition && layoutOptions.isHorizontal ? ( ( y / this.layout.size.height ) * 100 ) + '%' : y + 'px'; }; Item.prototype._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 }); }; Item.prototype.getTranslate = function( x, y ) { // flip cooridinates if origin on right or bottom var layoutOptions = this.layout.options; x = layoutOptions.isOriginLeft ? x : -x; y = layoutOptions.isOriginTop ? y : -y; if ( is3d ) { return 'translate3d(' + x + 'px, ' + y + 'px, 0)'; } return 'translate(' + x + 'px, ' + y + 'px)'; }; // non transition + transform support Item.prototype.goTo = function( x, y ) { this.setPosition( x, y ); this.layoutPosition(); }; // use transition and transforms if supported Item.prototype.moveTo = supportsCSS3 ? Item.prototype._transitionTo : Item.prototype.goTo; Item.prototype.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 Item.prototype._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 */ Item.prototype._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( vendorProperties.transform || 'transform' ); Item.prototype.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 ); }; Item.prototype.transition = Item.prototype[ transitionProperty ? '_transition' : '_nonTransition' ]; // ----- events ----- // Item.prototype.onwebkitTransitionEnd = function( event ) { this.ontransitionend( event ); }; Item.prototype.onotransitionend = function( event ) { this.ontransitionend( event ); }; // properties that I munge to make my life easier var dashedVendorProperties = { '-webkit-transform': 'transform', '-moz-transform': 'transform', '-o-transform': 'transform' }; Item.prototype.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 ] ); }; Item.prototype.disableTransition = function() { this.removeTransitionStyles(); this.element.removeEventListener( transitionEndEvent, this, false ); this.isTransitioning = false; }; /** * removes style property from element * @param {Object} style **/ Item.prototype._removeStyles = function( style ) { // clean up transition styles var cleanStyle = {}; for ( var prop in style ) { cleanStyle[ prop ] = ''; } this.css( cleanStyle ); }; var cleanTransitionStyle = { transitionProperty: '', transitionDuration: '' }; Item.prototype.removeTransitionStyles = function() { // remove transition this.css( cleanTransitionStyle ); }; // ----- show/hide/remove ----- // // remove element from DOM Item.prototype.removeElem = function() { this.element.parentNode.removeChild( this.element ); // remove display: none this.css({ display: '' }); this.emitEvent( 'remove', [ this ] ); }; Item.prototype.remove = function() { // just remove element if no transition support or no transition if ( !transitionProperty || !parseFloat( this.layout.options.transitionDuration ) ) { this.removeElem(); return; } // start transition var _this = this; this.once( 'transitionEnd', function() { _this.removeElem(); }); this.hide(); }; Item.prototype.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 }); }; Item.prototype.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} */ Item.prototype.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; } }; Item.prototype.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 }); }; Item.prototype.onHideTransitionEnd = function() { // check if still hidden // during transition, item may have been un-hidden if ( this.isHidden ) { this.css({ display: 'none' }); this.emitEvent('hide'); } }; Item.prototype.destroy = function() { this.css({ position: '', left: '', right: '', top: '', bottom: '', transition: '', transform: '' }); }; return Item; }));