mirror of https://github.com/sveltejs/svelte
parent
744a2bb7f3
commit
40a5f28862
@ -1,111 +0,0 @@
|
||||
import deindent from '../../../../utils/deindent';
|
||||
import flattenReference from '../../../../utils/flattenReference';
|
||||
import validCalleeObjects from '../../../../utils/validCalleeObjects';
|
||||
import { DomGenerator } from '../../index';
|
||||
import Block from '../../Block';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import { State } from '../../interfaces';
|
||||
|
||||
export default function visitEventHandler(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
attribute: Node
|
||||
) {
|
||||
const name = attribute.name;
|
||||
const isCustomEvent = generator.events.has(name);
|
||||
const shouldHoist = !isCustomEvent && state.inEachBlock;
|
||||
|
||||
const context = shouldHoist ? null : state.parentNode;
|
||||
const usedContexts: string[] = [];
|
||||
|
||||
if (attribute.expression) {
|
||||
generator.addSourcemapLocations(attribute.expression);
|
||||
|
||||
const flattened = flattenReference(attribute.expression.callee);
|
||||
if (!validCalleeObjects.has(flattened.name)) {
|
||||
// allow event.stopPropagation(), this.select() etc
|
||||
// TODO verify that it's a valid callee (i.e. built-in or declared method)
|
||||
generator.code.prependRight(
|
||||
attribute.expression.start,
|
||||
`${block.alias('component')}.`
|
||||
);
|
||||
if (shouldHoist) state.usesComponent = true; // this feels a bit hacky but it works!
|
||||
}
|
||||
|
||||
attribute.expression.arguments.forEach((arg: Node) => {
|
||||
const { contexts } = block.contextualise(arg, context, true);
|
||||
|
||||
contexts.forEach(context => {
|
||||
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
|
||||
if (!~state.allUsedContexts.indexOf(context))
|
||||
state.allUsedContexts.push(context);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const _this = context || 'this';
|
||||
const declarations = usedContexts.map(name => {
|
||||
if (name === 'state') {
|
||||
if (shouldHoist) state.usesComponent = true;
|
||||
return `var state = ${block.alias('component')}.get();`;
|
||||
}
|
||||
|
||||
const listName = block.listNames.get(name);
|
||||
const indexName = block.indexNames.get(name);
|
||||
const contextName = block.contexts.get(name);
|
||||
|
||||
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
|
||||
});
|
||||
|
||||
// get a name for the event handler that is globally unique
|
||||
// if hoisted, locally unique otherwise
|
||||
const handlerName = (shouldHoist ? generator : block).getUniqueName(
|
||||
`${name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
|
||||
);
|
||||
|
||||
// create the handler body
|
||||
const handlerBody = deindent`
|
||||
${state.usesComponent &&
|
||||
`var ${block.alias('component')} = ${_this}._svelte.component;`}
|
||||
${declarations}
|
||||
${attribute.expression ?
|
||||
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
|
||||
`${block.alias('component')}.fire("${attribute.name}", event);`}
|
||||
`;
|
||||
|
||||
if (isCustomEvent) {
|
||||
block.addVariable(handlerName);
|
||||
|
||||
block.builders.hydrate.addBlock(deindent`
|
||||
${handlerName} = %events-${name}.call(#component, ${state.parentNode}, function(event) {
|
||||
${handlerBody}
|
||||
});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addLine(deindent`
|
||||
${handlerName}.teardown();
|
||||
`);
|
||||
} else {
|
||||
const handler = deindent`
|
||||
function ${handlerName}(event) {
|
||||
${handlerBody}
|
||||
}
|
||||
`;
|
||||
|
||||
if (shouldHoist) {
|
||||
generator.blocks.push(handler);
|
||||
} else {
|
||||
block.builders.init.addBlock(handler);
|
||||
}
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`@addListener(${state.parentNode}, "${name}", ${handlerName});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`@removeListener(${state.parentNode}, "${name}", ${handlerName});`
|
||||
);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import deindent from '../../../../utils/deindent';
|
||||
import { DomGenerator } from '../../index';
|
||||
import Block from '../../Block';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import { State } from '../../interfaces';
|
||||
|
||||
export default function visitRef(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
attribute: Node
|
||||
) {
|
||||
const name = attribute.name;
|
||||
|
||||
block.builders.mount.addLine(
|
||||
`#component.refs.${name} = ${state.parentNode};`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(deindent`
|
||||
if (#component.refs.${name} === ${state.parentNode}) #component.refs.${name} = null;
|
||||
`);
|
||||
|
||||
generator.usesRefs = true; // so this component.refs object is created
|
||||
}
|
@ -0,0 +1,689 @@
|
||||
import deindent from '../../../../utils/deindent';
|
||||
import flattenReference from '../../../../utils/flattenReference';
|
||||
import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue';
|
||||
import { DomGenerator } from '../../index';
|
||||
import Block from '../../Block';
|
||||
import { Node } from '../../../../interfaces';
|
||||
import { State } from '../../interfaces';
|
||||
import getObject from '../../../../utils/getObject';
|
||||
import getTailSnippet from '../../../../utils/getTailSnippet';
|
||||
import visitBinding from './Binding';
|
||||
import { generateRule } from '../../../../shared/index';
|
||||
|
||||
const types: Record<string, (
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
binding: Node[]
|
||||
) => void> = {
|
||||
input: addInputBinding,
|
||||
select: addSelectBinding,
|
||||
audio: addMediaBinding,
|
||||
video: addMediaBinding
|
||||
};
|
||||
|
||||
const readOnlyMediaAttributes = new Set([
|
||||
'duration',
|
||||
'buffered',
|
||||
'seekable',
|
||||
'played'
|
||||
]);
|
||||
|
||||
export default function addBindings(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node
|
||||
) {
|
||||
const bindings: Node[] = node.attributes.filter((a: Node) => a.type === 'Binding');
|
||||
|
||||
if (bindings.length === 0) return;
|
||||
|
||||
types[node.name](generator, block, state, node, bindings);
|
||||
}
|
||||
|
||||
function addInputBinding(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
bindings: Node[]
|
||||
) {
|
||||
const attribute = bindings[0];
|
||||
|
||||
const { name } = getObject(attribute.value);
|
||||
const { snippet, contexts, dependencies } = block.contextualise(
|
||||
attribute.value
|
||||
);
|
||||
|
||||
contexts.forEach(context => {
|
||||
if (!~state.allUsedContexts.indexOf(context))
|
||||
state.allUsedContexts.push(context);
|
||||
});
|
||||
|
||||
const type = getStaticAttributeValue(node, 'type');
|
||||
const eventName = type === 'radio' || type === 'checkbox' ? 'change' : 'input';
|
||||
|
||||
const handler = block.getUniqueName(
|
||||
`${node.var}_${eventName}_handler`
|
||||
);
|
||||
|
||||
const bindingGroup = attribute.name === 'group'
|
||||
? getBindingGroup(generator, attribute.value)
|
||||
: null;
|
||||
|
||||
const value = (
|
||||
attribute.name === 'group' ?
|
||||
(type === 'checkbox' ? `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])` : `${node.var}.__value`) :
|
||||
(type === 'range' || type === 'number') ?
|
||||
`@toNumber(${node.var}.${attribute.name})` :
|
||||
`${node.var}.${attribute.name}`
|
||||
);
|
||||
|
||||
let setter = getSetter(generator, block, name, snippet, node, attribute, dependencies, value);
|
||||
let updateElement = `${node.var}.${attribute.name} = ${snippet};`;
|
||||
|
||||
const needsLock = !/radio|checkbox|range|color/.test(type); // TODO others?
|
||||
const lock = `#${node.var}_updating`;
|
||||
let updateConditions = needsLock ? [`!${lock}`] : [];
|
||||
|
||||
if (needsLock) block.addVariable(lock, 'false');
|
||||
|
||||
if (attribute.name === 'group') {
|
||||
// <input type='checkbox|radio' bind:group='selected'> special case
|
||||
if (type === 'radio') {
|
||||
setter = deindent`
|
||||
if (!${node.var}.checked) return;
|
||||
${setter}
|
||||
`;
|
||||
}
|
||||
|
||||
const condition = type === 'checkbox'
|
||||
? `~${snippet}.indexOf(${node.var}.__value)`
|
||||
: `${node.var}.__value === ${snippet}`;
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addBlock(
|
||||
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
|
||||
);
|
||||
|
||||
updateElement = `${node.var}.checked = ${condition};`;
|
||||
}
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handler}() {
|
||||
${needsLock && `${lock} = true;`}
|
||||
${setter}
|
||||
${needsLock && `${lock} = false;`}
|
||||
}
|
||||
`);
|
||||
|
||||
if (type === 'range') {
|
||||
// need to bind to `input` and `change`, for the benefit of IE
|
||||
block.builders.hydrate.addBlock(deindent`
|
||||
@addListener(${node.var}, "input", ${handler});
|
||||
@addListener(${node.var}, "change", ${handler});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
@removeListener(${node.var}, "input", ${handler});
|
||||
@removeListener(${node.var}, "change", ${handler});
|
||||
`);
|
||||
} else {
|
||||
block.builders.hydrate.addLine(
|
||||
`@addListener(${node.var}, "${eventName}", ${handler});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`@removeListener(${node.var}, "${eventName}", ${handler});`
|
||||
);
|
||||
}
|
||||
|
||||
node.initialUpdate = updateElement;
|
||||
|
||||
if (updateConditions.length) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${updateConditions.join(' && ')}) {
|
||||
${updateElement}
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
${updateElement}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function addSelectBinding(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
bindings: Node[]
|
||||
) {
|
||||
const attribute = bindings[0];
|
||||
|
||||
const { name } = getObject(attribute.value);
|
||||
const { snippet, contexts, dependencies } = block.contextualise(
|
||||
attribute.value
|
||||
);
|
||||
|
||||
contexts.forEach(context => {
|
||||
if (!~state.allUsedContexts.indexOf(context))
|
||||
state.allUsedContexts.push(context);
|
||||
});
|
||||
|
||||
const eventNames = getBindingEventName(node, attribute);
|
||||
const handler = block.getUniqueName(
|
||||
`${node.var}_${eventNames.join('_')}_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, attribute.value)
|
||||
: null;
|
||||
|
||||
const isMediaElement = node.name === 'audio' || node.name === 'video';
|
||||
const isReadOnly = isMediaElement && readOnlyMediaAttributes.has(attribute.name)
|
||||
|
||||
const value = getBindingValue(
|
||||
generator,
|
||||
block,
|
||||
state,
|
||||
node,
|
||||
attribute,
|
||||
isMultipleSelect,
|
||||
isMediaElement,
|
||||
bindingGroup,
|
||||
type
|
||||
);
|
||||
|
||||
let setter = getSetter(generator, block, name, snippet, node, attribute, dependencies, value);
|
||||
let updateElement = `${node.var}.${attribute.name} = ${snippet};`;
|
||||
|
||||
const needsLock = !isReadOnly && node.name !== 'input' || !/radio|checkbox|range|color/.test(type); // TODO others?
|
||||
const lock = `#${node.var}_updating`;
|
||||
let updateConditions = needsLock ? [`!${lock}`] : [];
|
||||
|
||||
if (needsLock) block.addVariable(lock, 'false');
|
||||
|
||||
// <select> special case
|
||||
if (node.name === 'select') {
|
||||
if (!isMultipleSelect) {
|
||||
setter = `var selectedOption = ${node.var}.querySelector(':checked') || ${node.var}.options[0];\n${setter}`;
|
||||
}
|
||||
|
||||
const value = block.getUniqueName('value');
|
||||
const option = block.getUniqueName('option');
|
||||
|
||||
const ifStatement = isMultipleSelect
|
||||
? deindent`
|
||||
${option}.selected = ~${value}.indexOf(${option}.__value);`
|
||||
: deindent`
|
||||
if (${option}.__value === ${value}) {
|
||||
${option}.selected = true;
|
||||
break;
|
||||
}`;
|
||||
|
||||
const { name } = getObject(attribute.value);
|
||||
const tailSnippet = getTailSnippet(attribute.value);
|
||||
|
||||
updateElement = deindent`
|
||||
var ${value} = ${snippet};
|
||||
for (var #i = 0; #i < ${node.var}.options.length; #i += 1) {
|
||||
var ${option} = ${node.var}.options[#i];
|
||||
|
||||
${ifStatement}
|
||||
}
|
||||
`;
|
||||
|
||||
generator.hasComplexBindings = true;
|
||||
block.builders.hydrate.addBlock(
|
||||
`if (!('${name}' in state)) #component._root._beforecreate.push(${handler});`
|
||||
);
|
||||
} else if (attribute.name === 'group') {
|
||||
// <input type='checkbox|radio' bind:group='selected'> special case
|
||||
if (type === 'radio') {
|
||||
setter = deindent`
|
||||
if (!${node.var}.checked) return;
|
||||
${setter}
|
||||
`;
|
||||
}
|
||||
|
||||
const condition = type === 'checkbox'
|
||||
? `~${snippet}.indexOf(${node.var}.__value)`
|
||||
: `${node.var}.__value === ${snippet}`;
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addBlock(
|
||||
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
|
||||
);
|
||||
|
||||
updateElement = `${node.var}.checked = ${condition};`;
|
||||
} else if (isMediaElement) {
|
||||
generator.hasComplexBindings = true;
|
||||
block.builders.hydrate.addBlock(`#component._root._beforecreate.push(${handler});`);
|
||||
|
||||
if (attribute.name === 'currentTime') {
|
||||
const frame = block.getUniqueName(`${node.var}_animationframe`);
|
||||
block.addVariable(frame);
|
||||
setter = deindent`
|
||||
cancelAnimationFrame(${frame});
|
||||
if (!${node.var}.paused) ${frame} = requestAnimationFrame(${handler});
|
||||
${setter}
|
||||
`;
|
||||
|
||||
updateConditions.push(`!isNaN(${snippet})`);
|
||||
} else if (attribute.name === 'paused') {
|
||||
// this is necessary to prevent the audio restarting by itself
|
||||
const last = block.getUniqueName(`${node.var}_paused_value`);
|
||||
block.addVariable(last, 'true');
|
||||
|
||||
updateConditions = [`${last} !== (${last} = ${snippet})`];
|
||||
updateElement = `${node.var}[${last} ? "pause" : "play"]();`;
|
||||
}
|
||||
}
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handler}() {
|
||||
${needsLock && `${lock} = true;`}
|
||||
${setter}
|
||||
${needsLock && `${lock} = false;`}
|
||||
}
|
||||
`);
|
||||
|
||||
if (node.name === 'input' && type === 'range') {
|
||||
// need to bind to `input` and `change`, for the benefit of IE
|
||||
block.builders.hydrate.addBlock(deindent`
|
||||
@addListener(${node.var}, "input", ${handler});
|
||||
@addListener(${node.var}, "change", ${handler});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
@removeListener(${node.var}, "input", ${handler});
|
||||
@removeListener(${node.var}, "change", ${handler});
|
||||
`);
|
||||
} else {
|
||||
eventNames.forEach(eventName => {
|
||||
block.builders.hydrate.addLine(
|
||||
`@addListener(${node.var}, "${eventName}", ${handler});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`@removeListener(${node.var}, "${eventName}", ${handler});`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!isMediaElement) {
|
||||
node.initialUpdate = updateElement;
|
||||
}
|
||||
|
||||
if (!isReadOnly) { // audio/video duration is read-only, it never updates
|
||||
if (updateConditions.length) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${updateConditions.join(' && ')}) {
|
||||
${updateElement}
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
${updateElement}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
if (attribute.name === 'paused') {
|
||||
block.builders.create.addLine(
|
||||
`@addListener(${node.var}, "play", ${handler});`
|
||||
);
|
||||
block.builders.destroy.addLine(
|
||||
`@removeListener(${node.var}, "play", ${handler});`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function addMediaBinding(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
bindings: Node[]
|
||||
) {
|
||||
const attribute = bindings[0];
|
||||
|
||||
const { name } = getObject(attribute.value);
|
||||
const { snippet, contexts, dependencies } = block.contextualise(
|
||||
attribute.value
|
||||
);
|
||||
|
||||
contexts.forEach(context => {
|
||||
if (!~state.allUsedContexts.indexOf(context))
|
||||
state.allUsedContexts.push(context);
|
||||
});
|
||||
|
||||
const eventNames = getBindingEventName(node, attribute);
|
||||
const handler = block.getUniqueName(
|
||||
`${node.var}_${eventNames.join('_')}_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, attribute.value)
|
||||
: null;
|
||||
|
||||
const isMediaElement = node.name === 'audio' || node.name === 'video';
|
||||
const isReadOnly = isMediaElement && readOnlyMediaAttributes.has(attribute.name)
|
||||
|
||||
const value = getBindingValue(
|
||||
generator,
|
||||
block,
|
||||
state,
|
||||
node,
|
||||
attribute,
|
||||
isMultipleSelect,
|
||||
isMediaElement,
|
||||
bindingGroup,
|
||||
type
|
||||
);
|
||||
|
||||
let setter = getSetter(generator, block, name, snippet, node, attribute, dependencies, value);
|
||||
let updateElement = `${node.var}.${attribute.name} = ${snippet};`;
|
||||
|
||||
const needsLock = !isReadOnly && node.name !== 'input' || !/radio|checkbox|range|color/.test(type); // TODO others?
|
||||
const lock = `#${node.var}_updating`;
|
||||
let updateConditions = needsLock ? [`!${lock}`] : [];
|
||||
|
||||
if (needsLock) block.addVariable(lock, 'false');
|
||||
|
||||
// <select> special case
|
||||
if (node.name === 'select') {
|
||||
if (!isMultipleSelect) {
|
||||
setter = `var selectedOption = ${node.var}.querySelector(':checked') || ${node.var}.options[0];\n${setter}`;
|
||||
}
|
||||
|
||||
const value = block.getUniqueName('value');
|
||||
const option = block.getUniqueName('option');
|
||||
|
||||
const ifStatement = isMultipleSelect
|
||||
? deindent`
|
||||
${option}.selected = ~${value}.indexOf(${option}.__value);`
|
||||
: deindent`
|
||||
if (${option}.__value === ${value}) {
|
||||
${option}.selected = true;
|
||||
break;
|
||||
}`;
|
||||
|
||||
const { name } = getObject(attribute.value);
|
||||
const tailSnippet = getTailSnippet(attribute.value);
|
||||
|
||||
updateElement = deindent`
|
||||
var ${value} = ${snippet};
|
||||
for (var #i = 0; #i < ${node.var}.options.length; #i += 1) {
|
||||
var ${option} = ${node.var}.options[#i];
|
||||
|
||||
${ifStatement}
|
||||
}
|
||||
`;
|
||||
|
||||
generator.hasComplexBindings = true;
|
||||
block.builders.hydrate.addBlock(
|
||||
`if (!('${name}' in state)) #component._root._beforecreate.push(${handler});`
|
||||
);
|
||||
} else if (attribute.name === 'group') {
|
||||
// <input type='checkbox|radio' bind:group='selected'> special case
|
||||
if (type === 'radio') {
|
||||
setter = deindent`
|
||||
if (!${node.var}.checked) return;
|
||||
${setter}
|
||||
`;
|
||||
}
|
||||
|
||||
const condition = type === 'checkbox'
|
||||
? `~${snippet}.indexOf(${node.var}.__value)`
|
||||
: `${node.var}.__value === ${snippet}`;
|
||||
|
||||
block.builders.hydrate.addLine(
|
||||
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addBlock(
|
||||
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
|
||||
);
|
||||
|
||||
updateElement = `${node.var}.checked = ${condition};`;
|
||||
} else if (isMediaElement) {
|
||||
generator.hasComplexBindings = true;
|
||||
block.builders.hydrate.addBlock(`#component._root._beforecreate.push(${handler});`);
|
||||
|
||||
if (attribute.name === 'currentTime') {
|
||||
const frame = block.getUniqueName(`${node.var}_animationframe`);
|
||||
block.addVariable(frame);
|
||||
setter = deindent`
|
||||
cancelAnimationFrame(${frame});
|
||||
if (!${node.var}.paused) ${frame} = requestAnimationFrame(${handler});
|
||||
${setter}
|
||||
`;
|
||||
|
||||
updateConditions.push(`!isNaN(${snippet})`);
|
||||
} else if (attribute.name === 'paused') {
|
||||
// this is necessary to prevent the audio restarting by itself
|
||||
const last = block.getUniqueName(`${node.var}_paused_value`);
|
||||
block.addVariable(last, 'true');
|
||||
|
||||
updateConditions = [`${last} !== (${last} = ${snippet})`];
|
||||
updateElement = `${node.var}[${last} ? "pause" : "play"]();`;
|
||||
}
|
||||
}
|
||||
|
||||
block.builders.init.addBlock(deindent`
|
||||
function ${handler}() {
|
||||
${needsLock && `${lock} = true;`}
|
||||
${setter}
|
||||
${needsLock && `${lock} = false;`}
|
||||
}
|
||||
`);
|
||||
|
||||
if (node.name === 'input' && type === 'range') {
|
||||
// need to bind to `input` and `change`, for the benefit of IE
|
||||
block.builders.hydrate.addBlock(deindent`
|
||||
@addListener(${node.var}, "input", ${handler});
|
||||
@addListener(${node.var}, "change", ${handler});
|
||||
`);
|
||||
|
||||
block.builders.destroy.addBlock(deindent`
|
||||
@removeListener(${node.var}, "input", ${handler});
|
||||
@removeListener(${node.var}, "change", ${handler});
|
||||
`);
|
||||
} else {
|
||||
eventNames.forEach(eventName => {
|
||||
block.builders.hydrate.addLine(
|
||||
`@addListener(${node.var}, "${eventName}", ${handler});`
|
||||
);
|
||||
|
||||
block.builders.destroy.addLine(
|
||||
`@removeListener(${node.var}, "${eventName}", ${handler});`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (!isMediaElement) {
|
||||
node.initialUpdate = updateElement;
|
||||
}
|
||||
|
||||
if (!isReadOnly) { // audio/video duration is read-only, it never updates
|
||||
if (updateConditions.length) {
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${updateConditions.join(' && ')}) {
|
||||
${updateElement}
|
||||
}
|
||||
`);
|
||||
} else {
|
||||
block.builders.update.addBlock(deindent`
|
||||
${updateElement}
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
if (attribute.name === 'paused') {
|
||||
block.builders.create.addLine(
|
||||
`@addListener(${node.var}, "play", ${handler});`
|
||||
);
|
||||
block.builders.destroy.addLine(
|
||||
`@removeListener(${node.var}, "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'];
|
||||
if (attribute.name === 'buffered') return ['progress', 'loadedmetadata'];
|
||||
if (attribute.name === 'seekable') return ['loadedmetadata'];
|
||||
if (attribute.name === 'played') return ['timeupdate'];
|
||||
|
||||
return ['change'];
|
||||
}
|
||||
|
||||
function getBindingValue(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
state: State,
|
||||
node: Node,
|
||||
attribute: Node,
|
||||
isMultipleSelect: boolean,
|
||||
isMediaElement: boolean,
|
||||
bindingGroup: number,
|
||||
type: string
|
||||
) {
|
||||
// <select multiple bind:value='selected>
|
||||
if (isMultipleSelect) {
|
||||
return `[].map.call(${node.var}.querySelectorAll(':checked'), function(option) { return option.__value; })`;
|
||||
}
|
||||
|
||||
// <select bind:value='selected>
|
||||
if (node.name === 'select') {
|
||||
return 'selectedOption && selectedOption.__value';
|
||||
}
|
||||
|
||||
// <input type='checkbox' bind:group='foo'>
|
||||
if (attribute.name === 'group') {
|
||||
if (type === 'checkbox') {
|
||||
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
|
||||
}
|
||||
|
||||
return `${node.var}.__value`;
|
||||
}
|
||||
|
||||
// <input type='range|number' bind:value>
|
||||
if (type === 'range' || type === 'number') {
|
||||
return `@toNumber(${node.var}.${attribute.name})`;
|
||||
}
|
||||
|
||||
if (isMediaElement && (attribute.name === 'buffered' || attribute.name === 'seekable' || attribute.name === 'played')) {
|
||||
return `@timeRangesToArray(${node.var}.${attribute.name})`
|
||||
}
|
||||
|
||||
// everything else
|
||||
return `${node.var}.${attribute.name}`;
|
||||
}
|
||||
|
||||
function getBindingGroup(generator: DomGenerator, value: Node) {
|
||||
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
|
||||
const keypath = parts.join('.');
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
function getSetter(
|
||||
generator: DomGenerator,
|
||||
block: Block,
|
||||
name: string,
|
||||
snippet: string,
|
||||
node: Node,
|
||||
attribute: Node,
|
||||
dependencies: string[],
|
||||
value: string,
|
||||
) {
|
||||
const tail = attribute.value.type === 'MemberExpression'
|
||||
? getTailSnippet(attribute.value)
|
||||
: '';
|
||||
|
||||
if (block.contexts.has(name)) {
|
||||
const prop = dependencies[0];
|
||||
const computed = isComputed(attribute.value);
|
||||
|
||||
return deindent`
|
||||
var list = ${node.var}._svelte.${block.listNames.get(name)};
|
||||
var index = ${node.var}._svelte.${block.indexNames.get(name)};
|
||||
${computed && `var state = #component.get();`}
|
||||
list[index]${tail} = ${value};
|
||||
|
||||
${computed
|
||||
? `#component.set({${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });`
|
||||
: `#component.set({${dependencies.map((prop: string) => `${prop}: #component.get('${prop}')`).join(', ')} });`}
|
||||
`;
|
||||
}
|
||||
|
||||
if (attribute.value.type === 'MemberExpression') {
|
||||
// This is a little confusing, and should probably be tidied up
|
||||
// at some point. It addresses a tricky bug (#893), wherein
|
||||
// Svelte tries to `set()` a computed property, which throws an
|
||||
// error in dev mode. a) it's possible that we should be
|
||||
// replacing computations with *their* dependencies, and b)
|
||||
// we should probably populate `generator.readonly` sooner so
|
||||
// that we don't have to do the `.some()` here
|
||||
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop));
|
||||
|
||||
return deindent`
|
||||
var state = #component.get();
|
||||
${snippet} = ${value};
|
||||
#component.set({ ${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });
|
||||
`;
|
||||
}
|
||||
|
||||
return `#component.set({ ${name}: ${value} });`;
|
||||
}
|
||||
|
||||
function isComputed(node: Node) {
|
||||
while (node.type === 'MemberExpression') {
|
||||
if (node.computed) return true;
|
||||
node = node.object;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
Loading…
Reference in new issue