Skip to content

Commit

Permalink
Merge pull request #496 from sveltejs/gh-406
Browse files Browse the repository at this point in the history
Implement media element bindings
  • Loading branch information
Rich-Harris authored Apr 19, 2017
2 parents 386cb8b + e1a1e65 commit f9432d1
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 22 deletions.
77 changes: 55 additions & 22 deletions src/generators/dom/visitors/Element/Binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ export default function visitBinding ( generator, block, state, node, attribute
if ( !~state.allUsedContexts.indexOf( context ) ) state.allUsedContexts.push( context );
});

const handler = block.getUniqueName( `${state.parentNode}_change_handler` );

const eventName = getBindingEventName( node, attribute );
const handler = block.getUniqueName( `${state.parentNode}_${eventName}_handler` );
const isMultipleSelect = node.name === 'select' && node.attributes.find( attr => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue( node, 'type' );
const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, keypath ) : null;
const value = getBindingValue( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type );
const eventName = getBindingEventName( node );

let setter = getSetter({ block, name, keypath, context: '_svelte', attribute, dependencies, value });
let updateElement;
let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
const lock = block.getUniqueName( `${state.parentNode}_${attribute.name}_updating` );
let updateCondition = `!${lock}`;

// <select> special case
if ( node.name === 'select' ) {
Expand Down Expand Up @@ -77,49 +78,81 @@ export default function visitBinding ( generator, block, state, node, attribute
updateElement = `${state.parentNode}.checked = ${condition};`;
}

// everything else
else {
updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
}
else if ( node.name === 'audio' || node.name === 'video' ) {
generator.hasComplexBindings = true;
block.builders.create.addBlock( `${block.component}._bindings.push( ${handler} );` );

if ( attribute.name === 'currentTime' ) {
const frame = block.getUniqueName( `${state.parentNode}_animationframe` );
block.builders.create.addLine( `var ${frame};` );
setter = deindent`
cancelAnimationFrame( ${frame} );
if ( !${state.parentNode}.paused ) ${frame} = requestAnimationFrame( ${handler} );
${setter}
`;

updateCondition += ` && !isNaN( ${snippet} )`;
}

else if ( attribute.name === 'duration' ) {
updateCondition = null;
}

const updating = block.getUniqueName( `${state.parentNode}_updating` );
else if ( attribute.name === 'paused' ) {
// this is necessary to prevent the audio restarting by itself
const last = block.getUniqueName( `${state.parentNode}_paused_value` );
block.builders.create.addLine( `var ${last} = true;` );

updateCondition += ` && ${last} !== ( ${last} = ${snippet} )`;
updateElement = `${state.parentNode}[ ${last} ? 'pause' : 'play' ]();`;
}
}

block.builders.create.addBlock( deindent`
var ${updating} = false;
var ${lock} = false;
function ${handler} () {
${updating} = true;
${lock} = true;
${setter}
${updating} = false;
${lock} = false;
}
${generator.helper( 'addEventListener' )}( ${state.parentNode}, '${eventName}', ${handler} );
` );

node.initialUpdate = updateElement;
if ( node.name !== 'audio' && node.name !== 'video' ) node.initialUpdate = updateElement;

block.builders.update.addLine( deindent`
if ( !${updating} ) {
${updateElement}
}
` );
if ( updateCondition !== null ) {
// audio/video duration is read-only, it never updates
block.builders.update.addBlock( deindent`
if ( ${updateCondition} ) {
${updateElement}
}
` );
}

block.builders.destroy.addLine( deindent`
${generator.helper( 'removeEventListener' )}( ${state.parentNode}, '${eventName}', ${handler} );
` );

if ( attribute.name === 'paused' ) {
block.builders.create.addLine( `${generator.helper( 'addEventListener' )}( ${state.parentNode}, 'play', ${handler} );` );
block.builders.destroy.addLine( `${generator.helper( 'removeEventListener' )}( ${state.parentNode}, 'play', ${handler} );` );
}
}

function getBindingEventName ( node ) {
function getBindingEventName ( node, attribute ) {
if ( node.name === 'input' ) {
const typeAttribute = node.attributes.find( attr => attr.type === 'Attribute' && attr.name === 'type' );
const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static

return type === 'checkbox' || type === 'radio' ? 'change' : 'input';
}

if ( node.name === 'textarea' ) {
return 'input';
}
if ( node.name === 'textarea' ) return 'input';
if ( attribute.name === 'currentTime' ) return 'timeupdate';
if ( attribute.name === 'duration' ) return 'durationchange';
if ( attribute.name === 'paused' ) return 'pause';

return 'change';
}
Expand Down
6 changes: 6 additions & 0 deletions src/validate/html/validateElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export default function validateElement ( validator, node ) {
}
}

else if ( name === 'currentTime' || name === 'duration' || name === 'paused' ) {
if ( node.name !== 'audio' && node.name !== 'video' ) {
validator.error( `'${name}' binding can only be used with <audio> or <video>` );
}
}

else {
validator.error( `'${attribute.name}' is not a valid binding`, attribute.start );
}
Expand Down
27 changes: 27 additions & 0 deletions test/runtime/samples/binding-audio-currenttime-duration/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default {
// not sure if we can really test this in JSDOM. Refer to
// https://svelte.technology/repl?example=media-elements
// instead
skip: true,

test ( assert, component, target, window ) {
assert.equal( component.get( 't' ), 0 );
assert.equal( component.get( 'd' ), 0 );
assert.equal( component.get( 'paused' ), true );

const audio = target.querySelector( 'audio' );
const timeupdate = new window.Event( 'timeupdate' );
const durationchange = new window.Event( 'durationchange' );

audio.currentTime = 10;
audio.duration = 20;
audio.dispatchEvent( timeupdate );
audio.dispatchEvent( durationchange );
audio.play();

assert.equal( component.get( 't' ), 10 );
assert.equal( component.get( 'd' ), 0 ); // not 20, because read-only. Not sure how to test this!
assert.equal( component.get( 'paused' ), true ); // ditto...
component.destroy();
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<audio bind:currentTime='t' bind:duration='d' bind:paused src='music.mp3'></audio>

0 comments on commit f9432d1

Please sign in to comment.