mirror of https://github.com/sveltejs/svelte
parent
425d01996f
commit
8072f6ad77
@ -1,428 +0,0 @@
|
|||||||
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 stringifyProps from '../../../../utils/stringifyProps';
|
|
||||||
import { generateRule } from '../../../../shared/index';
|
|
||||||
import flatten from '../../../../utils/flattenReference';
|
|
||||||
|
|
||||||
interface Binding {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const readOnlyMediaAttributes = new Set([
|
|
||||||
'duration',
|
|
||||||
'buffered',
|
|
||||||
'seekable',
|
|
||||||
'played'
|
|
||||||
]);
|
|
||||||
|
|
||||||
function isMediaNode(name: string) {
|
|
||||||
return name === 'audio' || name === 'video';
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = [
|
|
||||||
{
|
|
||||||
eventNames: ['input'],
|
|
||||||
filter: (node: Node, binding: Binding) =>
|
|
||||||
node.name === 'textarea' ||
|
|
||||||
node.name === 'input' && !/radio|checkbox/.test(getStaticAttributeValue(node, 'type'))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
eventNames: ['change'],
|
|
||||||
filter: (node: Node, binding: Binding) =>
|
|
||||||
node.name === 'select' ||
|
|
||||||
node.name === 'input' && /radio|checkbox|range/.test(getStaticAttributeValue(node, 'type'))
|
|
||||||
},
|
|
||||||
|
|
||||||
// media events
|
|
||||||
{
|
|
||||||
eventNames: ['timeupdate'],
|
|
||||||
filter: (node: Node, binding: Binding) =>
|
|
||||||
isMediaNode(node.name) &&
|
|
||||||
(binding.name === 'currentTime' || binding.name === 'played')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
eventNames: ['durationchange'],
|
|
||||||
filter: (node: Node, binding: Binding) =>
|
|
||||||
isMediaNode(node.name) &&
|
|
||||||
binding.name === 'duration'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
eventNames: ['play', 'pause'],
|
|
||||||
filter: (node: Node, binding: Binding) =>
|
|
||||||
isMediaNode(node.name) &&
|
|
||||||
binding.name === 'paused'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
eventNames: ['progress'],
|
|
||||||
filter: (node: Node, binding: Binding) =>
|
|
||||||
isMediaNode(node.name) &&
|
|
||||||
binding.name === 'buffered'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
eventNames: ['loadedmetadata'],
|
|
||||||
filter: (node: Node, binding: Binding) =>
|
|
||||||
isMediaNode(node.name) &&
|
|
||||||
(binding.name === 'buffered' || binding.name === 'seekable')
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
if (node.name === 'select' || isMediaNode(node.name)) generator.hasComplexBindings = true;
|
|
||||||
|
|
||||||
const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(getStaticAttributeValue(node, 'type'));
|
|
||||||
|
|
||||||
const mungedBindings = bindings.map(binding => {
|
|
||||||
const isReadOnly = isMediaNode(node.name) && readOnlyMediaAttributes.has(binding.name);
|
|
||||||
|
|
||||||
let updateCondition: string;
|
|
||||||
|
|
||||||
const { name } = getObject(binding.value);
|
|
||||||
const { contexts } = block.contextualise(binding.value);
|
|
||||||
const { snippet } = binding.metadata;
|
|
||||||
|
|
||||||
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
|
|
||||||
// and `selected` is an object chosen with a <select>, then when `checked` changes,
|
|
||||||
// we need to tell the component to update all the values `selected` might be
|
|
||||||
// pointing to
|
|
||||||
// TODO should this happen in preprocess?
|
|
||||||
const dependencies = binding.metadata.dependencies.slice();
|
|
||||||
binding.metadata.dependencies.forEach((prop: string) => {
|
|
||||||
const indirectDependencies = generator.indirectDependencies.get(prop);
|
|
||||||
if (indirectDependencies) {
|
|
||||||
indirectDependencies.forEach(indirectDependency => {
|
|
||||||
if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
contexts.forEach(context => {
|
|
||||||
if (!~state.allUsedContexts.indexOf(context))
|
|
||||||
state.allUsedContexts.push(context);
|
|
||||||
});
|
|
||||||
|
|
||||||
// view to model
|
|
||||||
const valueFromDom = getValueFromDom(generator, node, binding);
|
|
||||||
const handler = getEventHandler(generator, block, name, snippet, binding, dependencies, valueFromDom);
|
|
||||||
|
|
||||||
// model to view
|
|
||||||
let updateDom = getDomUpdater(node, binding, snippet);
|
|
||||||
let initialUpdate = updateDom;
|
|
||||||
|
|
||||||
// special cases
|
|
||||||
if (binding.name === 'group') {
|
|
||||||
const bindingGroup = getBindingGroup(generator, binding.value);
|
|
||||||
|
|
||||||
block.builders.hydrate.addLine(
|
|
||||||
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
|
|
||||||
);
|
|
||||||
|
|
||||||
block.builders.destroy.addLine(
|
|
||||||
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding.name === 'currentTime') {
|
|
||||||
updateCondition = `!isNaN(${snippet})`;
|
|
||||||
initialUpdate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding.name === 'paused') {
|
|
||||||
// this is necessary to prevent audio restarting by itself
|
|
||||||
const last = block.getUniqueName(`${node.var}_is_paused`);
|
|
||||||
block.addVariable(last, 'true');
|
|
||||||
|
|
||||||
updateCondition = `${last} !== (${last} = ${snippet})`;
|
|
||||||
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
|
|
||||||
initialUpdate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: binding.name,
|
|
||||||
object: name,
|
|
||||||
handler,
|
|
||||||
updateDom,
|
|
||||||
initialUpdate,
|
|
||||||
needsLock: !isReadOnly && needsLock,
|
|
||||||
updateCondition
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const lock = mungedBindings.some(binding => binding.needsLock) ?
|
|
||||||
block.getUniqueName(`${node.var}_updating`) :
|
|
||||||
null;
|
|
||||||
|
|
||||||
if (lock) block.addVariable(lock, 'false');
|
|
||||||
|
|
||||||
const groups = events
|
|
||||||
.map(event => {
|
|
||||||
return {
|
|
||||||
events: event.eventNames,
|
|
||||||
bindings: mungedBindings.filter(binding => event.filter(node, binding))
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(group => group.bindings.length);
|
|
||||||
|
|
||||||
groups.forEach(group => {
|
|
||||||
const handler = block.getUniqueName(`${node.var}_${group.events.join('_')}_handler`);
|
|
||||||
|
|
||||||
const needsLock = group.bindings.some(binding => binding.needsLock);
|
|
||||||
|
|
||||||
group.bindings.forEach(binding => {
|
|
||||||
if (!binding.updateDom) return;
|
|
||||||
|
|
||||||
const updateConditions = needsLock ? [`!${lock}`] : [];
|
|
||||||
if (binding.updateCondition) updateConditions.push(binding.updateCondition);
|
|
||||||
|
|
||||||
block.builders.update.addLine(
|
|
||||||
updateConditions.length ? `if (${updateConditions.join(' && ')}) ${binding.updateDom}` : binding.updateDom
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const usesContext = group.bindings.some(binding => binding.handler.usesContext);
|
|
||||||
const usesState = group.bindings.some(binding => binding.handler.usesState);
|
|
||||||
const usesStore = group.bindings.some(binding => binding.handler.usesStore);
|
|
||||||
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
|
|
||||||
|
|
||||||
const props = new Set();
|
|
||||||
const storeProps = new Set();
|
|
||||||
group.bindings.forEach(binding => {
|
|
||||||
binding.handler.props.forEach(prop => {
|
|
||||||
props.add(prop);
|
|
||||||
});
|
|
||||||
|
|
||||||
binding.handler.storeProps.forEach(prop => {
|
|
||||||
storeProps.add(prop);
|
|
||||||
});
|
|
||||||
}); // TODO use stringifyProps here, once indenting is fixed
|
|
||||||
|
|
||||||
// media bindings — awkward special case. The native timeupdate events
|
|
||||||
// fire too infrequently, so we need to take matters into our
|
|
||||||
// own hands
|
|
||||||
let animation_frame;
|
|
||||||
if (group.events[0] === 'timeupdate') {
|
|
||||||
animation_frame = block.getUniqueName(`${node.var}_animationframe`);
|
|
||||||
block.addVariable(animation_frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
block.builders.init.addBlock(deindent`
|
|
||||||
function ${handler}() {
|
|
||||||
${
|
|
||||||
animation_frame && deindent`
|
|
||||||
cancelAnimationFrame(${animation_frame});
|
|
||||||
if (!${node.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
|
|
||||||
}
|
|
||||||
${usesContext && `var context = ${node.var}._svelte;`}
|
|
||||||
${usesState && `var state = #component.get();`}
|
|
||||||
${usesStore && `var $ = #component.store.get();`}
|
|
||||||
${needsLock && `${lock} = true;`}
|
|
||||||
${mutations.length > 0 && mutations}
|
|
||||||
${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
|
|
||||||
${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
|
|
||||||
${needsLock && `${lock} = false;`}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
group.events.forEach(name => {
|
|
||||||
block.builders.hydrate.addLine(
|
|
||||||
`@addListener(${node.var}, "${name}", ${handler});`
|
|
||||||
);
|
|
||||||
|
|
||||||
block.builders.destroy.addLine(
|
|
||||||
`@removeListener(${node.var}, "${name}", ${handler});`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const allInitialStateIsDefined = group.bindings
|
|
||||||
.map(binding => `'${binding.object}' in state`)
|
|
||||||
.join(' && ');
|
|
||||||
|
|
||||||
if (node.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || readOnlyMediaAttributes.has(binding.name))) {
|
|
||||||
generator.hasComplexBindings = true;
|
|
||||||
|
|
||||||
block.builders.hydrate.addLine(
|
|
||||||
`if (!(${allInitialStateIsDefined})) #component._root._beforecreate.push(${handler});`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
node.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDomUpdater(
|
|
||||||
node: Node,
|
|
||||||
binding: Node,
|
|
||||||
snippet: string
|
|
||||||
) {
|
|
||||||
if (readOnlyMediaAttributes.has(binding.name)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.name === 'select') {
|
|
||||||
return getStaticAttributeValue(node, 'multiple') === true ?
|
|
||||||
`@selectOptions(${node.var}, ${snippet})` :
|
|
||||||
`@selectOption(${node.var}, ${snippet})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding.name === 'group') {
|
|
||||||
const type = getStaticAttributeValue(node, 'type');
|
|
||||||
|
|
||||||
const condition = type === 'checkbox'
|
|
||||||
? `~${snippet}.indexOf(${node.var}.__value)`
|
|
||||||
: `${node.var}.__value === ${snippet}`;
|
|
||||||
|
|
||||||
return `${node.var}.checked = ${condition};`
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${node.var}.${binding.name} = ${snippet};`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 getEventHandler(
|
|
||||||
generator: DomGenerator,
|
|
||||||
block: Block,
|
|
||||||
name: string,
|
|
||||||
snippet: string,
|
|
||||||
attribute: Node,
|
|
||||||
dependencies: string[],
|
|
||||||
value: string,
|
|
||||||
) {
|
|
||||||
let storeDependencies = [];
|
|
||||||
|
|
||||||
if (generator.options.store) {
|
|
||||||
storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
|
|
||||||
dependencies = dependencies.filter(prop => prop[0] !== '$');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block.contexts.has(name)) {
|
|
||||||
const tail = attribute.value.type === 'MemberExpression'
|
|
||||||
? getTailSnippet(attribute.value)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const list = `context.${block.listNames.get(name)}`;
|
|
||||||
const index = `context.${block.indexNames.get(name)}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
usesContext: true,
|
|
||||||
usesState: true,
|
|
||||||
usesStore: storeDependencies.length > 0,
|
|
||||||
mutation: `${list}[${index}]${tail} = ${value};`,
|
|
||||||
props: dependencies.map(prop => `${prop}: state.${prop}`),
|
|
||||||
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
usesContext: false,
|
|
||||||
usesState: true,
|
|
||||||
usesStore: storeDependencies.length > 0,
|
|
||||||
mutation: `${snippet} = ${value}`,
|
|
||||||
props: dependencies.map((prop: string) => `${prop}: state.${prop}`),
|
|
||||||
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let props;
|
|
||||||
let storeProps;
|
|
||||||
|
|
||||||
if (generator.options.store && name[0] === '$') {
|
|
||||||
props = [];
|
|
||||||
storeProps = [`${name.slice(1)}: ${value}`];
|
|
||||||
} else {
|
|
||||||
props = [`${name}: ${value}`];
|
|
||||||
storeProps = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
usesContext: false,
|
|
||||||
usesState: false,
|
|
||||||
usesStore: false,
|
|
||||||
mutation: null,
|
|
||||||
props,
|
|
||||||
storeProps
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getValueFromDom(
|
|
||||||
generator: DomGenerator,
|
|
||||||
node: Node,
|
|
||||||
binding: Node
|
|
||||||
) {
|
|
||||||
// <select bind:value='selected>
|
|
||||||
if (node.name === 'select') {
|
|
||||||
return getStaticAttributeValue(node, 'multiple') === true ?
|
|
||||||
`@selectMultipleValue(${node.var})` :
|
|
||||||
`@selectValue(${node.var})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = getStaticAttributeValue(node, 'type');
|
|
||||||
|
|
||||||
// <input type='checkbox' bind:group='foo'>
|
|
||||||
if (binding.name === 'group') {
|
|
||||||
const bindingGroup = getBindingGroup(generator, binding.value);
|
|
||||||
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}.${binding.name})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) {
|
|
||||||
return `@timeRangesToArray(${node.var}.${binding.name})`
|
|
||||||
}
|
|
||||||
|
|
||||||
// everything else
|
|
||||||
return `${node.var}.${binding.name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isComputed(node: Node) {
|
|
||||||
while (node.type === 'MemberExpression') {
|
|
||||||
if (node.computed) return true;
|
|
||||||
node = node.object;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
@ -1,98 +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 addTransitions(
|
|
||||||
generator: DomGenerator,
|
|
||||||
block: Block,
|
|
||||||
state: State,
|
|
||||||
node: Node
|
|
||||||
) {
|
|
||||||
const intro = node.attributes.find((a: Node) => a.type === 'Transition' && a.intro);
|
|
||||||
const outro = node.attributes.find((a: Node) => a.type === 'Transition' && a.outro);
|
|
||||||
|
|
||||||
if (!intro && !outro) return;
|
|
||||||
|
|
||||||
if (intro === outro) {
|
|
||||||
block.contextualise(intro.expression); // TODO remove all these
|
|
||||||
|
|
||||||
const name = block.getUniqueName(`${node.var}_transition`);
|
|
||||||
const snippet = intro.expression
|
|
||||||
? intro.metadata.snippet
|
|
||||||
: '{}';
|
|
||||||
|
|
||||||
block.addVariable(name);
|
|
||||||
|
|
||||||
const fn = `%transitions-${intro.name}`;
|
|
||||||
|
|
||||||
block.builders.intro.addBlock(deindent`
|
|
||||||
#component._root._aftercreate.push(function() {
|
|
||||||
if (!${name}) ${name} = @wrapTransition(#component, ${node.var}, ${fn}, ${snippet}, true, null);
|
|
||||||
${name}.run(true, function() {
|
|
||||||
#component.fire("intro.end", { node: ${node.var} });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
`);
|
|
||||||
|
|
||||||
block.builders.outro.addBlock(deindent`
|
|
||||||
${name}.run(false, function() {
|
|
||||||
#component.fire("outro.end", { node: ${node.var} });
|
|
||||||
if (--#outros === 0) #outrocallback();
|
|
||||||
${name} = null;
|
|
||||||
});
|
|
||||||
`);
|
|
||||||
} else {
|
|
||||||
const introName = intro && block.getUniqueName(`${node.var}_intro`);
|
|
||||||
const outroName = outro && block.getUniqueName(`${node.var}_outro`);
|
|
||||||
|
|
||||||
if (intro) {
|
|
||||||
block.contextualise(intro.expression);
|
|
||||||
|
|
||||||
block.addVariable(introName);
|
|
||||||
const snippet = intro.expression
|
|
||||||
? intro.metadata.snippet
|
|
||||||
: '{}';
|
|
||||||
|
|
||||||
const fn = `%transitions-${intro.name}`; // TODO add built-in transitions?
|
|
||||||
|
|
||||||
if (outro) {
|
|
||||||
block.builders.intro.addBlock(deindent`
|
|
||||||
if (${introName}) ${introName}.abort();
|
|
||||||
if (${outroName}) ${outroName}.abort();
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
block.builders.intro.addBlock(deindent`
|
|
||||||
#component._root._aftercreate.push(function() {
|
|
||||||
${introName} = @wrapTransition(#component, ${node.var}, ${fn}, ${snippet}, true, null);
|
|
||||||
${introName}.run(true, function() {
|
|
||||||
#component.fire("intro.end", { node: ${node.var} });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outro) {
|
|
||||||
block.contextualise(outro.expression);
|
|
||||||
|
|
||||||
block.addVariable(outroName);
|
|
||||||
const snippet = outro.expression
|
|
||||||
? outro.metadata.snippet
|
|
||||||
: '{}';
|
|
||||||
|
|
||||||
const fn = `%transitions-${outro.name}`;
|
|
||||||
|
|
||||||
// TODO hide elements that have outro'd (unless they belong to a still-outroing
|
|
||||||
// group) prior to their removal from the DOM
|
|
||||||
block.builders.outro.addBlock(deindent`
|
|
||||||
${outroName} = @wrapTransition(#component, ${node.var}, ${fn}, ${snippet}, false, null);
|
|
||||||
${outroName}.run(false, function() {
|
|
||||||
#component.fire("outro.end", { node: ${node.var} });
|
|
||||||
if (--#outros === 0) #outrocallback();
|
|
||||||
});
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,271 @@
|
|||||||
import Node from './shared/Node';
|
import Node from './shared/Node';
|
||||||
|
import Element from './Element';
|
||||||
|
import getObject from '../../utils/getObject';
|
||||||
|
import getTailSnippet from '../../utils/getTailSnippet';
|
||||||
|
import flattenReference from '../../utils/flattenReference';
|
||||||
|
import Block from '../dom/Block';
|
||||||
|
import State from '../dom/State';
|
||||||
|
|
||||||
|
const readOnlyMediaAttributes = new Set([
|
||||||
|
'duration',
|
||||||
|
'buffered',
|
||||||
|
'seekable',
|
||||||
|
'played'
|
||||||
|
]);
|
||||||
|
|
||||||
export default class Binding extends Node {
|
export default class Binding extends Node {
|
||||||
name: string;
|
name: string;
|
||||||
value: Node[]
|
value: Node[]
|
||||||
expression: Node
|
expression: Node;
|
||||||
|
|
||||||
|
munge(
|
||||||
|
block: Block,
|
||||||
|
state: State
|
||||||
|
) {
|
||||||
|
const node: Element = this.parent;
|
||||||
|
|
||||||
|
const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type'));
|
||||||
|
const isReadOnly = node.isMediaNode() && readOnlyMediaAttributes.has(this.name);
|
||||||
|
|
||||||
|
let updateCondition: string;
|
||||||
|
|
||||||
|
const { name } = getObject(this.value);
|
||||||
|
const { contexts } = block.contextualise(this.value);
|
||||||
|
const { snippet } = this.metadata;
|
||||||
|
|
||||||
|
// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
|
||||||
|
// and `selected` is an object chosen with a <select>, then when `checked` changes,
|
||||||
|
// we need to tell the component to update all the values `selected` might be
|
||||||
|
// pointing to
|
||||||
|
// TODO should this happen in preprocess?
|
||||||
|
const dependencies = this.metadata.dependencies.slice();
|
||||||
|
this.metadata.dependencies.forEach((prop: string) => {
|
||||||
|
const indirectDependencies = this.generator.indirectDependencies.get(prop);
|
||||||
|
if (indirectDependencies) {
|
||||||
|
indirectDependencies.forEach(indirectDependency => {
|
||||||
|
if (!~dependencies.indexOf(indirectDependency)) dependencies.push(indirectDependency);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
contexts.forEach(context => {
|
||||||
|
if (!~state.allUsedContexts.indexOf(context))
|
||||||
|
state.allUsedContexts.push(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
// view to model
|
||||||
|
const valueFromDom = getValueFromDom(this.generator, node, this);
|
||||||
|
const handler = getEventHandler(this.generator, block, name, snippet, this, dependencies, valueFromDom);
|
||||||
|
|
||||||
|
// model to view
|
||||||
|
let updateDom = getDomUpdater(node, this, snippet);
|
||||||
|
let initialUpdate = updateDom;
|
||||||
|
|
||||||
|
// special cases
|
||||||
|
if (this.name === 'group') {
|
||||||
|
const bindingGroup = getBindingGroup(this.generator, this.value);
|
||||||
|
|
||||||
|
block.builders.hydrate.addLine(
|
||||||
|
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(
|
||||||
|
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.name === 'currentTime') {
|
||||||
|
updateCondition = `!isNaN(${snippet})`;
|
||||||
|
initialUpdate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.name === 'paused') {
|
||||||
|
// this is necessary to prevent audio restarting by itself
|
||||||
|
const last = block.getUniqueName(`${node.var}_is_paused`);
|
||||||
|
block.addVariable(last, 'true');
|
||||||
|
|
||||||
|
updateCondition = `${last} !== (${last} = ${snippet})`;
|
||||||
|
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
|
||||||
|
initialUpdate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
object: name,
|
||||||
|
handler,
|
||||||
|
updateDom,
|
||||||
|
initialUpdate,
|
||||||
|
needsLock: !isReadOnly && needsLock,
|
||||||
|
updateCondition,
|
||||||
|
isReadOnlyMediaAttribute: this.isReadOnlyMediaAttribute()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isReadOnlyMediaAttribute() {
|
||||||
|
return readOnlyMediaAttributes.has(this.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDomUpdater(
|
||||||
|
node: Element,
|
||||||
|
binding: Binding,
|
||||||
|
snippet: string
|
||||||
|
) {
|
||||||
|
if (binding.isReadOnlyMediaAttribute()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.name === 'select') {
|
||||||
|
return node.getStaticAttributeValue('multiple') === true ?
|
||||||
|
`@selectOptions(${node.var}, ${snippet})` :
|
||||||
|
`@selectOption(${node.var}, ${snippet})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.name === 'group') {
|
||||||
|
const type = node.getStaticAttributeValue('type');
|
||||||
|
|
||||||
|
const condition = type === 'checkbox'
|
||||||
|
? `~${snippet}.indexOf(${node.var}.__value)`
|
||||||
|
: `${node.var}.__value === ${snippet}`;
|
||||||
|
|
||||||
|
return `${node.var}.checked = ${condition};`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${node.var}.${binding.name} = ${snippet};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getEventHandler(
|
||||||
|
generator: DomGenerator,
|
||||||
|
block: Block,
|
||||||
|
name: string,
|
||||||
|
snippet: string,
|
||||||
|
attribute: Node,
|
||||||
|
dependencies: string[],
|
||||||
|
value: string,
|
||||||
|
) {
|
||||||
|
let storeDependencies = [];
|
||||||
|
|
||||||
|
if (generator.options.store) {
|
||||||
|
storeDependencies = dependencies.filter(prop => prop[0] === '$').map(prop => prop.slice(1));
|
||||||
|
dependencies = dependencies.filter(prop => prop[0] !== '$');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.contexts.has(name)) {
|
||||||
|
const tail = attribute.value.type === 'MemberExpression'
|
||||||
|
? getTailSnippet(attribute.value)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const list = `context.${block.listNames.get(name)}`;
|
||||||
|
const index = `context.${block.indexNames.get(name)}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
usesContext: true,
|
||||||
|
usesState: true,
|
||||||
|
usesStore: storeDependencies.length > 0,
|
||||||
|
mutation: `${list}[${index}]${tail} = ${value};`,
|
||||||
|
props: dependencies.map(prop => `${prop}: state.${prop}`),
|
||||||
|
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
usesContext: false,
|
||||||
|
usesState: true,
|
||||||
|
usesStore: storeDependencies.length > 0,
|
||||||
|
mutation: `${snippet} = ${value}`,
|
||||||
|
props: dependencies.map((prop: string) => `${prop}: state.${prop}`),
|
||||||
|
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let props;
|
||||||
|
let storeProps;
|
||||||
|
|
||||||
|
if (generator.options.store && name[0] === '$') {
|
||||||
|
props = [];
|
||||||
|
storeProps = [`${name.slice(1)}: ${value}`];
|
||||||
|
} else {
|
||||||
|
props = [`${name}: ${value}`];
|
||||||
|
storeProps = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
usesContext: false,
|
||||||
|
usesState: false,
|
||||||
|
usesStore: false,
|
||||||
|
mutation: null,
|
||||||
|
props,
|
||||||
|
storeProps
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValueFromDom(
|
||||||
|
generator: DomGenerator,
|
||||||
|
node: Element,
|
||||||
|
binding: Node
|
||||||
|
) {
|
||||||
|
// <select bind:value='selected>
|
||||||
|
if (node.name === 'select') {
|
||||||
|
return node.getStaticAttributeValue('multiple') === true ?
|
||||||
|
`@selectMultipleValue(${node.var})` :
|
||||||
|
`@selectValue(${node.var})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = node.getStaticAttributeValue('type');
|
||||||
|
|
||||||
|
// <input type='checkbox' bind:group='foo'>
|
||||||
|
if (binding.name === 'group') {
|
||||||
|
const bindingGroup = getBindingGroup(generator, binding.value);
|
||||||
|
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}.${binding.name})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) {
|
||||||
|
return `@timeRangesToArray(${node.var}.${binding.name})`
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything else
|
||||||
|
return `${node.var}.${binding.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isComputed(node: Node) {
|
||||||
|
while (node.type === 'MemberExpression') {
|
||||||
|
if (node.computed) return true;
|
||||||
|
node = node.object;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
Loading…
Reference in new issue