import deindent from '../../../../utils/deindent.js'; import flattenReference from '../../../../utils/flattenReference'; import getSetter from '../shared/binding/getSetter'; import getStaticAttributeValue from './getStaticAttributeValue'; import { DomGenerator } from '../../index'; import Block from '../../Block'; import { Node } from '../../../../interfaces'; import { State } from '../../interfaces'; export default function visitBinding ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node ) { const { name, parts } = flattenReference( attribute.value ); const { snippet, contexts, dependencies } = block.contextualise( attribute.value ); if ( dependencies.length > 1 ) throw new Error( 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' ); contexts.forEach( context => { if ( !~state.allUsedContexts.indexOf( context ) ) state.allUsedContexts.push( context ); }); const eventName = getBindingEventName( node, attribute ); const handler = block.getUniqueName( `${state.parentNode}_${eventName}_handler` ); const isMultipleSelect = node.name === 'select' && node.attributes.find( ( attr: Node ) => attr.name.toLowerCase() === 'multiple' ); // TODO use getStaticAttributeValue const type = getStaticAttributeValue( node, 'type' ); const bindingGroup = attribute.name === 'group' ? getBindingGroup( generator, parts.join( '.' ) ) : null; const value = getBindingValue( generator, block, state, node, attribute, isMultipleSelect, bindingGroup, type ); let setter = getSetter({ block, name, context: '_svelte', attribute, dependencies, value }); let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`; const lock = block.alias( `${state.parentNode}_updating` ); let updateCondition = `!${lock}`; block.addVariable( lock, 'false' ); // special case else if ( attribute.name === 'group' ) { if ( type === 'radio' ) { setter = deindent` if ( !${state.parentNode}.checked ) return; ${setter} `; } const condition = type === 'checkbox' ? `~${snippet}.indexOf( ${state.parentNode}.__value )` : `${state.parentNode}.__value === ${snippet}`; block.builders.create.addLine( `${block.component}._bindingGroups[${bindingGroup}].push( ${state.parentNode} );` ); block.builders.destroy.addBlock( `${block.component}._bindingGroups[${bindingGroup}].splice( ${block.component}._bindingGroups[${bindingGroup}].indexOf( ${state.parentNode} ), 1 );` ); updateElement = `${state.parentNode}.checked = ${condition};`; } 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.addVariable( frame ); setter = deindent` cancelAnimationFrame( ${frame} ); if ( !${state.parentNode}.paused ) ${frame} = requestAnimationFrame( ${handler} ); ${setter} `; updateCondition += ` && !isNaN( ${snippet} )`; } else if ( attribute.name === 'duration' ) { updateCondition = null; } else if ( attribute.name === 'paused' ) { // this is necessary to prevent the audio restarting by itself const last = block.getUniqueName( `${state.parentNode}_paused_value` ); block.addVariable( last, 'true' ); updateCondition = `${last} !== ( ${last} = ${snippet} )`; updateElement = `${state.parentNode}[ ${last} ? 'pause' : 'play' ]();`; } } block.builders.create.addBlock( deindent` function ${handler} () { ${lock} = true; ${setter} ${lock} = false; } ${generator.helper( 'addEventListener' )}( ${state.parentNode}, '${eventName}', ${handler} ); ` ); if ( node.name !== 'audio' && node.name !== 'video' ) node.initialUpdate = 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: Node, attribute: Node ) { if ( node.name === 'input' ) { const typeAttribute = node.attributes.find( ( attr: Node ) => 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 ( attribute.name === 'currentTime' ) return 'timeupdate'; if ( attribute.name === 'duration' ) return 'durationchange'; if ( attribute.name === 'paused' ) return 'pause'; return 'change'; } function getBindingValue ( generator: DomGenerator, block: Block, state: State, node: Node, attribute: Node, isMultipleSelect: boolean, bindingGroup: number, type: string ) { // if ( type === 'range' || type === 'number' ) { return `+${state.parentNode}.${attribute.name}`; } // everything else return `${state.parentNode}.${attribute.name}`; } function getBindingGroup ( generator: DomGenerator, keypath: string ) { // TODO handle contextual bindings — `keypath` should include unique ID of // each block that provides context let index = generator.bindingGroups.indexOf( keypath ); if ( index === -1 ) { index = generator.bindingGroups.length; generator.bindingGroups.push( keypath ); } return index; }