diff --git a/README.md b/README.md index 3bfb5ee..285c3fb 100755 --- a/README.md +++ b/README.md @@ -90,8 +90,10 @@ renderMetroContainer() { #### Customizing animations ```javascript -// override Metro´s default animations settings for each unique item in your items +// Override Metro´s default animations settings for each unique item in your items // array, see greensock tweenmax for reference. +// The animation settings are combined with the default animation settings, so +// you only have to specify the values you want to change. const animationMap = [ { in: { @@ -121,6 +123,8 @@ const animationMap = [ // Metro comes with a simple, fade in / out default. This object passed // in as the third argument in the Metro.sequence overrides the default settings. +// The override settings are combined with the built in defaults, so you only +// have to specify the values you want to change. const defaultAnimationOverride = { animation: { out: { diff --git a/__tests__/metroTests.js b/__tests__/metroTests.js index 91a470f..27ccc62 100644 --- a/__tests__/metroTests.js +++ b/__tests__/metroTests.js @@ -4,6 +4,25 @@ import Metro from '../src/index' describe('sequence api', () => { const testArr = [{ name: 'dog' }, { name: 'cat' }] + const defaultAnimation = { + out: { + time: 0.4, + delay: 0 + }, + in: { + time: 0.4, + delay: 0 + }, + willEnter: { + from: { opacity: 0 }, + to: { opacity: 1, ease: 'easeInOut' } + }, + willLeave: { + from: { opacity: 1 }, + to: { opacity: 0 } + } + } + it('transforms an array to a Metro sequence', () => { const sequence = Metro.sequence(testArr, []) expect(sequence[0].content.name).toBe(testArr[0].name) @@ -12,29 +31,6 @@ describe('sequence api', () => { it('uses the default animation settings', () => { const sequence = Metro.sequence(testArr, []) - const defaultAnimation = { - out: { - time: 0.4, - delay: 0 - }, - in: { - time: 0.4, - delay: 0 - }, - willEnter: { - from: { opacity: 0 }, - to: { opacity: 1, ease: 'easeInOut' } - }, - willLeave: { - from: { - opacity: 1 - }, - to: { - opacity: 0 - } - } - } - expect(sequence[0].animation).toEqual(defaultAnimation) }) @@ -69,7 +65,7 @@ describe('sequence api', () => { expect(sequence[0].animation).toEqual(defaultAnimationOverrideBase) }) - it('overrides default animation per array item though a custom animationmMap', () => { + it('overrides default animation per array item though a custom animationMap', () => { const dogMap = { in: { time: 3, @@ -114,4 +110,150 @@ describe('sequence api', () => { expect(sequence[1].animation.out).toEqual(catMap.out) expect(sequence[1].animation.willEnter).toEqual(catMap.willEnter) }) + + it('overrides single properties of the default animation settings', () => { + const defaultAnimationOverride = { + animation: { + willEnter: { + from: { y: -100 }, + to: { y: 0, ease: 'linear' } + }, + willLeave: { + from: { y: 100 }, + to: { y: 0 } + } + } + } + const sequence = Metro.sequence(testArr, [], defaultAnimationOverride) + + const expected = { + out: { + time: 0.4, + delay: 0 + }, + in: { + time: 0.4, + delay: 0 + }, + willEnter: { + from: { opacity: 0, y: -100 }, + to: { opacity: 1, ease: 'linear', y: 0 } + }, + willLeave: { + from: { opacity: 1, y: 100 }, + to: { opacity: 0, y: 0 } + } + } + + expect(sequence[0].animation).toEqual(expected) + }) + + it('applies single properties in the animationMap to the default animation settings', () => { + const animationMap = [ + {}, + { + out: { + delay: 1 + }, + willLeave: { + to: { opacity: 0.5 } + } + } + ] + const sequence = Metro.sequence(testArr, animationMap) + + const expectedSettingsForSecondItem = { + out: { + time: 0.4, + delay: 1 + }, + in: { + time: 0.4, + delay: 0 + }, + willEnter: { + from: { opacity: 0 }, + to: { opacity: 1, ease: 'easeInOut' } + }, + willLeave: { + from: { opacity: 1 }, + to: { opacity: 0.5 } + } + } + + expect(sequence[0].animation).toEqual(defaultAnimation) + expect(sequence[1].animation).toEqual(expectedSettingsForSecondItem) + }) + + it('applies single properties in the animationMap to the overridden animation settings', () => { + const defaultAnimationOverride = { + animation: { + willEnter: { + from: { y: -100 }, + to: { y: 0, ease: 'linear' } + }, + willLeave: { + from: { y: 100 }, + to: { y: 0 } + } + } + } + const animationMap = [ + {}, + { + out: { + delay: 1 + }, + willLeave: { + to: { opacity: 0.5 } + } + } + ] + const sequence = Metro.sequence( + testArr, + animationMap, + defaultAnimationOverride + ) + + const expectedSettingsForFirstItem = { + out: { + time: 0.4, + delay: 0 + }, + in: { + time: 0.4, + delay: 0 + }, + willEnter: { + from: { opacity: 0, y: -100 }, + to: { opacity: 1, y: 0, ease: 'linear' } + }, + willLeave: { + from: { opacity: 1, y: 100 }, + to: { opacity: 0, y: 0 } + } + } + + const expectedSettingsForSecondItem = { + out: { + time: 0.4, + delay: 1 + }, + in: { + time: 0.4, + delay: 0 + }, + willEnter: { + from: { opacity: 0, y: -100 }, + to: { opacity: 1, y: 0, ease: 'linear' } + }, + willLeave: { + from: { opacity: 1, y: 100 }, + to: { opacity: 0.5, y: 0 } + } + } + + expect(sequence[0].animation).toEqual(expectedSettingsForFirstItem) + expect(sequence[1].animation).toEqual(expectedSettingsForSecondItem) + }) }) diff --git a/dist/react-metro.js b/dist/react-metro.js index d6aa5d2..0f1ea9c 100644 --- a/dist/react-metro.js +++ b/dist/react-metro.js @@ -1 +1 @@ -"use strict";function _interopDefault(e){return e&&"object"==typeof e&&"default"in e?e.default:e}Object.defineProperty(exports,"__esModule",{value:!0});var React=_interopDefault(require("react")),gsap=require("gsap"),classCallCheck=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},createClass=function(){function e(e,t){for(var n=0;n=n}},{key:"applySequenceEndIfLastInSequence",value:function(e,t,n){var o=this.getLongestAnimationInSequence(n),i=!0;return t.forEach(function(t,r){t.animation[n].time+t.animation[n].delay===o&&r>e&&(i=!1)}),i}},{key:"componentWillEnter",value:function(e){var t=this,n=this.container;this.props.sequence[this.props.itemIndex].animating=!0,gsap.TweenMax.fromTo(n,this.props.animation.in.time,this.props.animation.willEnter.from,_extends({},this.props.animation.willEnter.to,{delay:this.props.animation.in.delay,onComplete:function(){t.isThisTheLongestAnimation(t.props.animation,"in")&&t.applySequenceEndIfLastInSequence(t.props.index,t.props.sequence,"in")&&t.props.onMount&&t.props.onMount(),t.props.sequence[t.props.itemIndex].animating=!1,e()}}))}},{key:"componentWillLeave",value:function(e){var t=this,n=this.container,o=this.getLongestAnimationInSequence("out")-(this.props.animation.out.time+this.props.animation.out.delay);this.props.sequence[this.props.itemIndex].animating=!0,gsap.TweenMax.fromTo(n,this.props.animation.out.time,this.props.animation.willLeave.from,_extends({},this.props.animation.willLeave.to,{delay:this.props.animation.out.delay,onComplete:function(){setTimeout(function(){t.isThisTheLongestAnimation(t.props.animation,"out")&&t.applySequenceEndIfLastInSequence(t.props.index,t.props.sequence,"out")&&t.props.onUnmount&&t.props.onUnmount(),t.props.sequence[t.props.itemIndex].animating=!1,e()},1e3*o)}}))}},{key:"render",value:function(){var t=this;return this.props.wrapperType?React.createElement(this.props.wrapperType,{ref:function(e){return t.container=e}},React.createElement(e,this.props)):React.createElement("div",{ref:function(e){return t.container=e}},React.createElement(e,this.props))}}]),n}(React.Component)},delayedVerticalPreset=function(e,t,n,o,i){for(var r=[],a={willEnter:{from:{opacity:0,y:50},to:{opacity:1,y:0}}},s={willLeave:{from:{opacity:1,y:0},to:{opacity:0,y:50}}},p=0;p0;m--)a.push(_extends({in:{time:2*o,delay:i*c*2},out:{time:2*o,delay:i*c*2}},u,l)),c-=1;for(var d=r+1;d0;y--)a.push(_extends({in:{time:o,delay:i*f},out:{time:o,delay:i*f}},u,l)),f-=1;for(var h=n-1,v=n;v>r;v--)a.push(_extends({in:{time:o,delay:i*h},out:{time:o,delay:i*h}},u,l)),h-=1}else for(var g=0;gr&&(a.push(_extends({in:{time:o,delay:i*p},out:{time:o,delay:i*p}},u,l)),p+=1);return a.splice(r,0,_extends({in:{time:o,delay:0},out:{time:o,delay:0}},u,l)),a},animations={domino:dominoPreset,delayedVertical:delayedVerticalPreset},defaultAnimation={animation:{out:{time:.4,delay:0},in:{time:.4,delay:0},willEnter:{from:{opacity:0},to:{opacity:1,ease:"easeInOut"}},willLeave:{from:{opacity:1},to:{opacity:0}}}},metroContainer=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=_extends({},t||defaultAnimation),i=_extends({},o,{key:0,itemIndex:0,sequence:[_extends({},o)]});return React.createElement(Metro.animation,_extends({},i,n),e)},metroSequence=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:null)||defaultAnimation,o=e.map(function(e,o){var i=_extends({},n,{animation:_extends({},n.animation,t[o])});return _extends({},i,{content:e})});return o.map(function(e,t){return _extends({key:t,itemIndex:t},e,{sequence:o})})},metroAnimation=MetroHoc(function(e){function t(){return classCallCheck(this,t),possibleConstructorReturn(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return inherits(t,e),createClass(t,[{key:"render",value:function(){var e=this;return React.createElement("div",{onClick:function(){var t=e.props.sequence.some(function(e){return e.animating});!e.props.onClick||t&&!e.props.enableClickDuringAnimation||e.props.onClick(e.props.content,e.props.itemIndex,t)}},this.props.children)}}]),t}(React.Component)),generateFocusAnimationMap=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=arguments[1],n=arguments[2],o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"dominoForwards",i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1,r=(Math.ceil(n/t),.75*i/n),a=[e,n,r,r+.25*i/n];if(o.includes("domino")){var s=o.split("domino")[1].toLowerCase();return animations.domino.apply(animations,[s].concat(a))}if(o.includes("delayed")){var p=o.split("delayed")[1].toLowerCase();return animations.delayedVertical.apply(animations,[p].concat(a))}return[]},Metro={sequence:metroSequence,animation:metroAnimation,container:metroContainer,generateFocusMap:generateFocusAnimationMap};exports.default=Metro; +"use strict";function _interopDefault(e){return e&&"object"==typeof e&&"default"in e?e.default:e}Object.defineProperty(exports,"__esModule",{value:!0});var React=_interopDefault(require("react")),gsap=require("gsap"),classCallCheck=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},createClass=function(){function e(e,t){for(var n=0;n=n}},{key:"applySequenceEndIfLastInSequence",value:function(e,t,n){var i=this.getLongestAnimationInSequence(n),o=!0;return t.forEach(function(t,a){t.animation[n].time+t.animation[n].delay===i&&a>e&&(o=!1)}),o}},{key:"componentWillEnter",value:function(e){var t=this,n=this.container;this.props.sequence[this.props.itemIndex].animating=!0,gsap.TweenMax.fromTo(n,this.props.animation.in.time,this.props.animation.willEnter.from,_extends({},this.props.animation.willEnter.to,{delay:this.props.animation.in.delay,onComplete:function(){t.isThisTheLongestAnimation(t.props.animation,"in")&&t.applySequenceEndIfLastInSequence(t.props.index,t.props.sequence,"in")&&t.props.onMount&&t.props.onMount(),t.props.sequence[t.props.itemIndex].animating=!1,e()}}))}},{key:"componentWillLeave",value:function(e){var t=this,n=this.container,i=this.getLongestAnimationInSequence("out")-(this.props.animation.out.time+this.props.animation.out.delay);this.props.sequence[this.props.itemIndex].animating=!0,gsap.TweenMax.fromTo(n,this.props.animation.out.time,this.props.animation.willLeave.from,_extends({},this.props.animation.willLeave.to,{delay:this.props.animation.out.delay,onComplete:function(){setTimeout(function(){t.isThisTheLongestAnimation(t.props.animation,"out")&&t.applySequenceEndIfLastInSequence(t.props.index,t.props.sequence,"out")&&t.props.onUnmount&&t.props.onUnmount(),t.props.sequence[t.props.itemIndex].animating=!1,e()},1e3*i)}}))}},{key:"render",value:function(){var t=this;return React.createElement(this.props.wrapperType,{ref:function(e){return t.container=e}},React.createElement(e,this.props))}}]),n}(React.Component),t.defaultProps={wrapperType:"div"},n},delayedVerticalPreset=function(e,t,n,i,o){for(var a=[],r={willEnter:{from:{opacity:0,y:50},to:{opacity:1,y:0}}},s={willLeave:{from:{opacity:1,y:0},to:{opacity:0,y:50}}},l=0;l0;m--)r.push(_extends({in:{time:2*i,delay:o*c*2},out:{time:2*i,delay:o*c*2}},u,p)),c-=1;for(var d=a+1;d0;y--)r.push(_extends({in:{time:i,delay:o*f},out:{time:i,delay:o*f}},u,p)),f-=1;for(var h=n-1,v=n;v>a;v--)r.push(_extends({in:{time:i,delay:o*h},out:{time:i,delay:o*h}},u,p)),h-=1}else for(var w=0;wa&&(r.push(_extends({in:{time:i,delay:o*l},out:{time:i,delay:o*l}},u,p)),l+=1);return r.splice(a,0,_extends({in:{time:i,delay:0},out:{time:i,delay:0}},u,p)),r},animations={domino:dominoPreset,delayedVertical:delayedVerticalPreset},defaultAnimation={animation:{out:{time:.4,delay:0},in:{time:.4,delay:0},willEnter:{from:{opacity:0},to:{opacity:1,ease:"easeInOut"}},willLeave:{from:{opacity:1},to:{opacity:0}}}},metroContainer=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=_extends({},t||defaultAnimation),o=_extends({},i,{key:0,itemIndex:0,sequence:[_extends({},i)]});return React.createElement(Metro.animation,_extends({},o,n),e)},combineAnimations=function(e,t){return t?{out:_extends({},e.out,t.out),in:_extends({},e.in,t.in),willEnter:{from:_extends({},e.willEnter&&e.willEnter.from,t.willEnter&&t.willEnter.from),to:_extends({},e.willEnter&&e.willEnter.to,t.willEnter&&t.willEnter.to)},willLeave:{from:_extends({},e.willLeave&&e.willLeave.from,t.willLeave&&t.willLeave.from),to:_extends({},e.willLeave&&e.willLeave.to,t.willLeave&&t.willLeave.to)}}:e},metroSequence=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,i={animation:n?combineAnimations(defaultAnimation.animation,n.animation):defaultAnimation.animation},o=e.map(function(e,n){var o=combineAnimations(i.animation,t[n]),a=_extends({},i,{animation:o});return _extends({},a,{content:e})});return o.map(function(e,t){return _extends({key:t,itemIndex:t},e,{sequence:o})})},metroAnimation=MetroHoc(function(e){function t(){return classCallCheck(this,t),possibleConstructorReturn(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return inherits(t,e),createClass(t,[{key:"render",value:function(){var e=this;return React.createElement("div",{onClick:function(){var t=e.props.sequence.some(function(e){return e.animating});!e.props.onClick||t&&!e.props.enableClickDuringAnimation||e.props.onClick(e.props.content,e.props.itemIndex,t)}},this.props.children)}}]),t}(React.Component)),generateFocusAnimationMap=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=arguments[1],n=arguments[2],i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"dominoForwards",o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1,a=(Math.ceil(n/t),.75*o/n),r=[e,n,a,a+.25*o/n];if(i.includes("domino")){var s=i.split("domino")[1].toLowerCase();return animations.domino.apply(animations,[s].concat(r))}if(i.includes("delayed")){var l=i.split("delayed")[1].toLowerCase();return animations.delayedVertical.apply(animations,[l].concat(r))}return[]},Metro={sequence:metroSequence,animation:metroAnimation,container:metroContainer,generateFocusMap:generateFocusAnimationMap};exports.default=Metro; diff --git a/src/MetroHoc.js b/src/MetroHoc.js index 5553e8f..aef7cf6 100755 --- a/src/MetroHoc.js +++ b/src/MetroHoc.js @@ -3,6 +3,10 @@ import { TweenMax } from 'gsap' const MetroHoc = Component => class MetroContainer extends React.Component { + static defaultProps = { + wrapperType: 'div' + } + // longest animation in sequence getLongestAnimationInSequence(io) { return Math.max( @@ -103,14 +107,10 @@ const MetroHoc = Component => /* eslint-disable */ render() { - return this.props.wrapperType ? ( + return ( (this.container = c)}> - ) : ( -
(this.container = c)}> - -
) } } diff --git a/src/index.js b/src/index.js index 8dc6305..b177846 100755 --- a/src/index.js +++ b/src/index.js @@ -56,6 +56,36 @@ const metroContainer = ( ) } +const combineAnimations = (base, animation) => { + if (!animation) { + return base + } + return { + out: { ...base.out, ...animation.out }, + in: { ...base.in, ...animation.in }, + willEnter: { + from: { + ...(base.willEnter && base.willEnter.from), + ...(animation.willEnter && animation.willEnter.from) + }, + to: { + ...(base.willEnter && base.willEnter.to), + ...(animation.willEnter && animation.willEnter.to) + } + }, + willLeave: { + from: { + ...(base.willLeave && base.willLeave.from), + ...(animation.willLeave && animation.willLeave.from) + }, + to: { + ...(base.willLeave && base.willLeave.to), + ...(animation.willLeave && animation.willLeave.to) + } + } + } +} + // metroSequence // enhances an array of data to a Metro sequence with animation data const metroSequence = ( @@ -63,11 +93,19 @@ const metroSequence = ( animationMap = [], defaultAnimationOverride = null ) => { - const baseAnimation = defaultAnimationOverride || defaultAnimation + const baseAnimation = { + animation: defaultAnimationOverride + ? combineAnimations( + defaultAnimation.animation, + defaultAnimationOverride.animation + ) + : defaultAnimation.animation + } const sequence = dataArray.map((data, i) => { + const combAnim = combineAnimations(baseAnimation.animation, animationMap[i]) const settings = { ...baseAnimation, - animation: { ...baseAnimation.animation, ...animationMap[i] } + animation: combAnim } return { ...settings,