From 642b414c9cd9fecaf6623ec2ef8e49be5d833004 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Thu, 4 May 2017 14:06:50 -0400 Subject: [PATCH 1/3] validate transition directives - closes #564 --- src/validate/html/validateElement.js | 28 ++++++++++++++++++- src/validate/index.js | 3 +- src/validate/js/index.js | 2 +- .../errors.json | 8 ++++++ .../input.html | 10 +++++++ .../transition-duplicate-in/errors.json | 8 ++++++ .../transition-duplicate-in/input.html | 10 +++++++ .../errors.json | 8 ++++++ .../input.html | 10 +++++++ .../transition-duplicate-out/errors.json | 8 ++++++ .../transition-duplicate-out/input.html | 10 +++++++ .../errors.json | 8 ++++++ .../input.html | 10 +++++++ .../errors.json | 8 ++++++ .../input.html | 10 +++++++ .../errors.json | 8 ++++++ .../input.html | 10 +++++++ .../samples/transition-missing/errors.json | 8 ++++++ .../samples/transition-missing/input.html | 1 + 19 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 test/validator/samples/transition-duplicate-in-transition/errors.json create mode 100644 test/validator/samples/transition-duplicate-in-transition/input.html create mode 100644 test/validator/samples/transition-duplicate-in/errors.json create mode 100644 test/validator/samples/transition-duplicate-in/input.html create mode 100644 test/validator/samples/transition-duplicate-out-transition/errors.json create mode 100644 test/validator/samples/transition-duplicate-out-transition/input.html create mode 100644 test/validator/samples/transition-duplicate-out/errors.json create mode 100644 test/validator/samples/transition-duplicate-out/input.html create mode 100644 test/validator/samples/transition-duplicate-transition-in/errors.json create mode 100644 test/validator/samples/transition-duplicate-transition-in/input.html create mode 100644 test/validator/samples/transition-duplicate-transition-out/errors.json create mode 100644 test/validator/samples/transition-duplicate-transition-out/input.html create mode 100644 test/validator/samples/transition-duplicate-transition/errors.json create mode 100644 test/validator/samples/transition-duplicate-transition/input.html create mode 100644 test/validator/samples/transition-missing/errors.json create mode 100644 test/validator/samples/transition-missing/input.html diff --git a/src/validate/html/validateElement.js b/src/validate/html/validateElement.js index 42e4c75f30f0..74034ceb0838 100644 --- a/src/validate/html/validateElement.js +++ b/src/validate/html/validateElement.js @@ -3,6 +3,10 @@ import validateEventHandler from './validateEventHandler.js'; export default function validateElement ( validator, node ) { const isComponent = node.name === ':Self' || validator.components.has( node.name ); + let hasIntro; + let hasOutro; + let hasTransition; + node.attributes.forEach( attribute => { if ( !isComponent && attribute.type === 'Binding' ) { const { name } = attribute; @@ -46,9 +50,31 @@ export default function validateElement ( validator, node ) { } } - if ( attribute.type === 'EventHandler' ) { + else if ( attribute.type === 'EventHandler' ) { validateEventHandler( validator, attribute ); } + + else if ( attribute.type === 'Transition' ) { + const bidi = attribute.intro && attribute.outro; + + if ( hasTransition ) { + if ( bidi ) validator.error( `An element can only have one 'transition' directive`, attribute.start ); + validator.error( `An element cannot have both a 'transition' directive and an '${attribute.intro ? 'in' : 'out'}' directive`, attribute.start ); + } + + if ( ( hasIntro && attribute.intro ) || ( hasOutro && attribute.outro ) ) { + if ( bidi ) validator.error( `An element cannot have both an '${hasIntro ? 'in' : 'out'}' directive and a 'transition' directive`, attribute.start ); + validator.error( `An element can only have one '${hasIntro ? 'in' : 'out'}' directive`, attribute.start ); + } + + if ( attribute.intro ) hasIntro = true; + if ( attribute.outro ) hasOutro = true; + if ( bidi ) hasTransition = true; + + if ( !validator.transitions.has( attribute.name ) ) { + validator.error( `Missing transition '${attribute.name}'`, attribute.start ); + } + } }); } diff --git a/src/validate/index.js b/src/validate/index.js index 8040549e5693..8991803ba5a2 100644 --- a/src/validate/index.js +++ b/src/validate/index.js @@ -43,7 +43,8 @@ export default function validate ( parsed, source, { onerror, onwarn, name, file properties: {}, components: new Map(), methods: new Map(), - helpers: new Map() + helpers: new Map(), + transitions: new Map() }; try { diff --git a/src/validate/js/index.js b/src/validate/js/index.js index 4d237e9f016a..5b101502ef94 100644 --- a/src/validate/js/index.js +++ b/src/validate/js/index.js @@ -63,7 +63,7 @@ export default function validateJs ( validator, js ) { } }); - [ 'components', 'methods', 'helpers' ].forEach( key => { + [ 'components', 'methods', 'helpers', 'transitions' ].forEach( key => { if ( validator.properties[ key ] ) { validator.properties[ key ].value.properties.forEach( prop => { validator[ key ].set( prop.key.name, prop.value ); diff --git a/test/validator/samples/transition-duplicate-in-transition/errors.json b/test/validator/samples/transition-duplicate-in-transition/errors.json new file mode 100644 index 000000000000..c48f56ede9a5 --- /dev/null +++ b/test/validator/samples/transition-duplicate-in-transition/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "An element cannot have both an 'in' directive and a 'transition' directive", + "loc": { + "line": 1, + "column": 12 + }, + "pos": 12 +}] \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-in-transition/input.html b/test/validator/samples/transition-duplicate-in-transition/input.html new file mode 100644 index 000000000000..6f47e754f4d1 --- /dev/null +++ b/test/validator/samples/transition-duplicate-in-transition/input.html @@ -0,0 +1,10 @@ +
...
+ + \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-in/errors.json b/test/validator/samples/transition-duplicate-in/errors.json new file mode 100644 index 000000000000..a3cc8b0ec580 --- /dev/null +++ b/test/validator/samples/transition-duplicate-in/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "An element can only have one 'in' directive", + "loc": { + "line": 1, + "column": 12 + }, + "pos": 12 +}] \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-in/input.html b/test/validator/samples/transition-duplicate-in/input.html new file mode 100644 index 000000000000..b9a9218f8e71 --- /dev/null +++ b/test/validator/samples/transition-duplicate-in/input.html @@ -0,0 +1,10 @@ +
...
+ + \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-out-transition/errors.json b/test/validator/samples/transition-duplicate-out-transition/errors.json new file mode 100644 index 000000000000..f4bfa61ef024 --- /dev/null +++ b/test/validator/samples/transition-duplicate-out-transition/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "An element cannot have both an 'out' directive and a 'transition' directive", + "loc": { + "line": 1, + "column": 13 + }, + "pos": 13 +}] \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-out-transition/input.html b/test/validator/samples/transition-duplicate-out-transition/input.html new file mode 100644 index 000000000000..ae2582b9427b --- /dev/null +++ b/test/validator/samples/transition-duplicate-out-transition/input.html @@ -0,0 +1,10 @@ +
...
+ + \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-out/errors.json b/test/validator/samples/transition-duplicate-out/errors.json new file mode 100644 index 000000000000..988dc02bbe3a --- /dev/null +++ b/test/validator/samples/transition-duplicate-out/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "An element can only have one 'out' directive", + "loc": { + "line": 1, + "column": 13 + }, + "pos": 13 +}] \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-out/input.html b/test/validator/samples/transition-duplicate-out/input.html new file mode 100644 index 000000000000..949dae8638ba --- /dev/null +++ b/test/validator/samples/transition-duplicate-out/input.html @@ -0,0 +1,10 @@ +
...
+ + \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-transition-in/errors.json b/test/validator/samples/transition-duplicate-transition-in/errors.json new file mode 100644 index 000000000000..678ad4dd38c9 --- /dev/null +++ b/test/validator/samples/transition-duplicate-transition-in/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "An element cannot have both a 'transition' directive and an 'in' directive", + "loc": { + "line": 1, + "column": 20 + }, + "pos": 20 +}] \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-transition-in/input.html b/test/validator/samples/transition-duplicate-transition-in/input.html new file mode 100644 index 000000000000..bc105c2079d1 --- /dev/null +++ b/test/validator/samples/transition-duplicate-transition-in/input.html @@ -0,0 +1,10 @@ +
...
+ + \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-transition-out/errors.json b/test/validator/samples/transition-duplicate-transition-out/errors.json new file mode 100644 index 000000000000..31dc180b5a8d --- /dev/null +++ b/test/validator/samples/transition-duplicate-transition-out/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "An element cannot have both a 'transition' directive and an 'out' directive", + "loc": { + "line": 1, + "column": 20 + }, + "pos": 20 +}] \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-transition-out/input.html b/test/validator/samples/transition-duplicate-transition-out/input.html new file mode 100644 index 000000000000..ac82cb064e1d --- /dev/null +++ b/test/validator/samples/transition-duplicate-transition-out/input.html @@ -0,0 +1,10 @@ +
...
+ + \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-transition/errors.json b/test/validator/samples/transition-duplicate-transition/errors.json new file mode 100644 index 000000000000..585ff3745106 --- /dev/null +++ b/test/validator/samples/transition-duplicate-transition/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "An element can only have one 'transition' directive", + "loc": { + "line": 1, + "column": 20 + }, + "pos": 20 +}] \ No newline at end of file diff --git a/test/validator/samples/transition-duplicate-transition/input.html b/test/validator/samples/transition-duplicate-transition/input.html new file mode 100644 index 000000000000..29af279564ad --- /dev/null +++ b/test/validator/samples/transition-duplicate-transition/input.html @@ -0,0 +1,10 @@ +
...
+ + \ No newline at end of file diff --git a/test/validator/samples/transition-missing/errors.json b/test/validator/samples/transition-missing/errors.json new file mode 100644 index 000000000000..4f2b88c2f6fc --- /dev/null +++ b/test/validator/samples/transition-missing/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "Missing transition 'foo'", + "loc": { + "line": 1, + "column": 5 + }, + "pos": 5 +}] \ No newline at end of file diff --git a/test/validator/samples/transition-missing/input.html b/test/validator/samples/transition-missing/input.html new file mode 100644 index 000000000000..5d0e1b7067c7 --- /dev/null +++ b/test/validator/samples/transition-missing/input.html @@ -0,0 +1 @@ +
...
\ No newline at end of file From 1a92398101cbdfdda476748828e5ff53369aa01a Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Thu, 4 May 2017 17:47:22 -0400 Subject: [PATCH 2/3] apply delays to bidirectional transitions - fixes #562 --- .../dom/visitors/Element/addTransitions.js | 8 +- src/shared/transitions.js | 82 +++++++++++-------- .../transition-js-delay-in-out/_config.js | 24 ++++++ .../transition-js-delay-in-out/main.html | 29 +++++++ .../samples/transition-js-delay/_config.js | 29 +++++++ .../samples/transition-js-delay/main.html | 19 +++++ .../_config.js | 10 +-- .../main.html | 2 +- 8 files changed, 160 insertions(+), 43 deletions(-) create mode 100644 test/runtime/samples/transition-js-delay-in-out/_config.js create mode 100644 test/runtime/samples/transition-js-delay-in-out/main.html create mode 100644 test/runtime/samples/transition-js-delay/_config.js create mode 100644 test/runtime/samples/transition-js-delay/main.html diff --git a/src/generators/dom/visitors/Element/addTransitions.js b/src/generators/dom/visitors/Element/addTransitions.js index e7fda98331ba..6a5953d02ce2 100644 --- a/src/generators/dom/visitors/Element/addTransitions.js +++ b/src/generators/dom/visitors/Element/addTransitions.js @@ -14,14 +14,14 @@ export default function addTransitions ( generator, block, state, node, intro, o block.builders.intro.addBlock( deindent` ${block.component}._renderHooks.push( function () { if ( !${name} ) ${name} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, true, null ); - ${name}.run( ${name}.t, 1, function () { + ${name}.run( true, function () { ${block.component}.fire( 'intro.end', { node: ${state.name} }); }); }); ` ); block.builders.outro.addBlock( deindent` - ${name}.run( ${name}.t, 0, function () { + ${name}.run( false, function () { ${block.component}.fire( 'outro.end', { node: ${state.name} }); if ( --${block.alias( 'outros' )} === 0 ) ${block.alias( 'outrocallback' )}(); ${name} = null; @@ -49,7 +49,7 @@ export default function addTransitions ( generator, block, state, node, intro, o block.builders.intro.addBlock( deindent` ${block.component}._renderHooks.push( function () { ${introName} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, true, null ); - ${introName}.run( 0, 1, function () { + ${introName}.run( true, function () { ${block.component}.fire( 'intro.end', { node: ${state.name} }); }); }); @@ -66,7 +66,7 @@ export default function addTransitions ( generator, block, state, node, intro, o // group) prior to their removal from the DOM block.builders.outro.addBlock( deindent` ${outroName} = ${wrapTransition}( ${state.name}, ${fn}, ${snippet}, false, null ); - ${outroName}.run( 1, 0, function () { + ${outroName}.run( false, function () { ${block.component}.fire( 'outro.end', { node: ${state.name} }); if ( --${block.alias( 'outros' )} === 0 ) ${block.alias( 'outrocallback' )}(); }); diff --git a/src/shared/transitions.js b/src/shared/transitions.js index 289ded618220..f13f3a357c29 100644 --- a/src/shared/transitions.js +++ b/src/shared/transitions.js @@ -28,7 +28,7 @@ export function generateKeyframes ( a, b, delta, duration, ease, fn, node, style } export function wrapTransition ( node, fn, params, intro, outgroup ) { - var obj = fn( node, params, intro ); + var obj = fn( node, params ); var duration = obj.duration || 300; var ease = obj.easing || linear; @@ -40,26 +40,20 @@ export function wrapTransition ( node, fn, params, intro, outgroup ) { if ( intro && obj.tick ) obj.tick( 0 ); return { - start: null, - end: null, - a: null, - b: null, - d: null, running: false, t: intro ? 0 : 1, - callback: null, - run: function ( a, b, callback ) { - this.a = a; - this.b = b; - this.delta = b - a; - this.start = window.performance.now() + ( obj.delay || 0 ); - this.duration = duration * Math.abs( b - a ); - this.end = this.start + this.duration; - - this.callback = callback; - - if ( obj.css ) { - generateKeyframes( this.a, this.b, this.delta, this.duration, ease, obj.css, node, style ); + pending: null, + run: function ( intro, callback ) { + var program = { + intro: intro, + start: window.performance.now() + ( obj.delay || 0 ), + callback: callback + }; + + if ( obj.delay ) { + this.pending = program; + } else { + this.start( program ); } if ( !this.running ) { @@ -67,21 +61,41 @@ export function wrapTransition ( node, fn, params, intro, outgroup ) { transitionManager.add( this ); } }, + start: function ( program ) { + program.a = this.t; + program.b = program.intro ? 1 : 0; + program.delta = program.b - program.a; + program.duration = duration * Math.abs( program.b - program.a ); + program.end = program.start + program.duration; + + if ( obj.css ) { + generateKeyframes( program.a, program.b, program.delta, program.duration, ease, obj.css, node, style ); + } + + this.program = program; + this.pending = null; + }, update: function ( now ) { - var p = now - this.start; - this.t = this.a + this.delta * ease( p / this.duration ); + var program = this.program; + if ( !program ) return; + + var p = now - program.start; + this.t = program.a + program.delta * ease( p / program.duration ); if ( obj.tick ) obj.tick( this.t ); }, done: function () { - if ( obj.tick ) obj.tick( intro ? 1 : 0 ); + this.t = this.program.b; + if ( obj.tick ) obj.tick( this.t ); if ( obj.css ) document.head.removeChild( style ); - this.callback(); - this.running = false; + this.program.callback(); + this.program = null; + this.running = !!this.pending; }, abort: function () { if ( obj.tick ) obj.tick( 1 ); if ( obj.css ) document.head.removeChild( style ); - this.running = false; + this.program = null; + this.running = !!this.pending; } }; } @@ -108,16 +122,18 @@ export var transitionManager = { while ( i-- ) { var transition = transitionManager.transitions[i]; - if ( transition.running ) { - if ( now >= transition.end ) { - transition.running = false; - transition.done(); - } else if ( now > transition.start ) { - transition.update( now ); - } + if ( transition.program && now >= transition.program.end ) { + transition.done(); + } + if ( transition.pending && now >= transition.pending.start ) { + transition.start( transition.pending ); + } + + if ( transition.running ) { + transition.update( now ); transitionManager.running = true; - } else { + } else if ( !transition.pending ) { transitionManager.transitions.splice( i, 1 ); } } diff --git a/test/runtime/samples/transition-js-delay-in-out/_config.js b/test/runtime/samples/transition-js-delay-in-out/_config.js new file mode 100644 index 000000000000..6e97f19c3b2e --- /dev/null +++ b/test/runtime/samples/transition-js-delay-in-out/_config.js @@ -0,0 +1,24 @@ +export default { + test ( assert, component, target, window, raf ) { + component.set({ visible: true }); + const div = target.querySelector( 'div' ); + assert.equal( div.foo, 0 ); + + raf.tick( 50 ); + assert.equal( div.foo, 0 ); + + raf.tick( 150 ); + assert.equal( div.foo, 1 ); + + component.set({ visible: false }); + assert.equal( div.bar, undefined ); + + raf.tick( 200 ); + assert.equal( div.bar, 1 ); + + raf.tick( 300 ); + assert.equal( div.bar, 0 ); + + component.destroy(); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/transition-js-delay-in-out/main.html b/test/runtime/samples/transition-js-delay-in-out/main.html new file mode 100644 index 000000000000..6a0a3f88a7ae --- /dev/null +++ b/test/runtime/samples/transition-js-delay-in-out/main.html @@ -0,0 +1,29 @@ +{{#if visible}} +
delayed
+{{/if}} + + \ No newline at end of file diff --git a/test/runtime/samples/transition-js-delay/_config.js b/test/runtime/samples/transition-js-delay/_config.js new file mode 100644 index 000000000000..eab832ca3fd7 --- /dev/null +++ b/test/runtime/samples/transition-js-delay/_config.js @@ -0,0 +1,29 @@ +export default { + test ( assert, component, target, window, raf ) { + component.set({ visible: true }); + const div = target.querySelector( 'div' ); + assert.equal( div.foo, 0 ); + + raf.tick( 50 ); + assert.equal( div.foo, 0 ); + + raf.tick( 100 ); + assert.equal( div.foo, 0.5 ); + + component.set({ visible: false }); + + raf.tick( 125 ); + assert.equal( div.foo, 0.75 ); + + raf.tick( 150 ); + assert.equal( div.foo, 1 ); + + raf.tick( 175 ); + assert.equal( div.foo, 0.75 ); + + raf.tick( 250 ); + assert.equal( div.foo, 0 ); + + component.destroy(); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/transition-js-delay/main.html b/test/runtime/samples/transition-js-delay/main.html new file mode 100644 index 000000000000..a4277f34cebb --- /dev/null +++ b/test/runtime/samples/transition-js-delay/main.html @@ -0,0 +1,19 @@ +{{#if visible}} +
delayed
+{{/if}} + + \ No newline at end of file diff --git a/test/runtime/samples/transition-js-dynamic-if-block-bidi/_config.js b/test/runtime/samples/transition-js-dynamic-if-block-bidi/_config.js index fcf391ac60c3..65dece1a13e0 100644 --- a/test/runtime/samples/transition-js-dynamic-if-block-bidi/_config.js +++ b/test/runtime/samples/transition-js-dynamic-if-block-bidi/_config.js @@ -11,7 +11,7 @@ export default { const div = target.querySelector( 'div' ); assert.equal( div.foo, 0 ); - raf.tick( 300 ); + raf.tick( 75 ); component.set({ name: 'everybody' }); assert.equal( div.foo, 0.75 ); assert.htmlEqual( div.innerHTML, 'hello everybody!' ); @@ -19,18 +19,18 @@ export default { component.set({ visible: false, name: 'again' }); assert.htmlEqual( div.innerHTML, 'hello everybody!' ); - raf.tick( 500 ); + raf.tick( 125 ); assert.equal( div.foo, 0.25 ); component.set({ visible: true }); - raf.tick( 700 ); + raf.tick( 175 ); assert.equal( div.foo, 0.75 ); assert.htmlEqual( div.innerHTML, 'hello again!' ); - raf.tick( 800 ); + raf.tick( 200 ); assert.equal( div.foo, 1 ); - raf.tick( 900 ); + raf.tick( 225 ); component.destroy(); } diff --git a/test/runtime/samples/transition-js-dynamic-if-block-bidi/main.html b/test/runtime/samples/transition-js-dynamic-if-block-bidi/main.html index bc0dace68b5d..576b4efc2e91 100644 --- a/test/runtime/samples/transition-js-dynamic-if-block-bidi/main.html +++ b/test/runtime/samples/transition-js-dynamic-if-block-bidi/main.html @@ -8,7 +8,7 @@ foo: function ( node, params ) { global.count += 1; return { - duration: 400, + duration: 100, tick: t => { node.foo = t; } From cfd5d3e3c78fc3ea5929d24edf65c748047e2803 Mon Sep 17 00:00:00 2001 From: Rich-Harris Date: Thu, 4 May 2017 17:57:04 -0400 Subject: [PATCH 3/3] minor tidy up --- src/shared/transitions.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/shared/transitions.js b/src/shared/transitions.js index f13f3a357c29..916a69b3ddc0 100644 --- a/src/shared/transitions.js +++ b/src/shared/transitions.js @@ -40,13 +40,14 @@ export function wrapTransition ( node, fn, params, intro, outgroup ) { if ( intro && obj.tick ) obj.tick( 0 ); return { - running: false, t: intro ? 0 : 1, + running: false, + program: null, pending: null, run: function ( intro, callback ) { var program = { - intro: intro, start: window.performance.now() + ( obj.delay || 0 ), + intro: intro, callback: callback }; @@ -94,8 +95,8 @@ export function wrapTransition ( node, fn, params, intro, outgroup ) { abort: function () { if ( obj.tick ) obj.tick( 1 ); if ( obj.css ) document.head.removeChild( style ); - this.program = null; - this.running = !!this.pending; + this.program = this.pending = null; + this.running = false; } }; }