mirror of https://github.com/sveltejs/svelte
parent
2dce34c53c
commit
e55a806a52
@ -1,290 +0,0 @@
|
|||||||
import Binding from '../../../nodes/Binding';
|
|
||||||
import ElementWrapper from '.';
|
|
||||||
import { dimensions } from '../../../../utils/patterns';
|
|
||||||
import getObject from '../../../../utils/getObject';
|
|
||||||
import Block from '../../Block';
|
|
||||||
|
|
||||||
type Handler = {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const readOnlyMediaAttributes = new Set([
|
|
||||||
'duration',
|
|
||||||
'buffered',
|
|
||||||
'seekable',
|
|
||||||
'played'
|
|
||||||
]);
|
|
||||||
|
|
||||||
export default class BindingWrapper {
|
|
||||||
node: Binding;
|
|
||||||
parent: ElementWrapper;
|
|
||||||
|
|
||||||
object: string;
|
|
||||||
handler: Handler;
|
|
||||||
updateDom: string;
|
|
||||||
initialUpdate: string;
|
|
||||||
needsLock: boolean;
|
|
||||||
updateCondition: string;
|
|
||||||
isReadOnlyMediaAttribute: boolean;
|
|
||||||
|
|
||||||
constructor(node: Binding, parent: ElementWrapper) {
|
|
||||||
this.node = node;
|
|
||||||
this.parent = parent;
|
|
||||||
|
|
||||||
parent.cannotUseInnerHTML();
|
|
||||||
|
|
||||||
const needsLock = (
|
|
||||||
parent.node.name !== 'input' ||
|
|
||||||
!/radio|checkbox|range|color/.test(parent.getStaticAttributeValue('type'))
|
|
||||||
);
|
|
||||||
|
|
||||||
const isReadOnly = (
|
|
||||||
(parent.isMediaNode() && readOnlyMediaAttributes.has(this.node.name)) ||
|
|
||||||
dimensions.test(this.node.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
let updateConditions: string[] = [];
|
|
||||||
|
|
||||||
const { name } = getObject(this.node.value.node);
|
|
||||||
const { snippet } = this.node.value;
|
|
||||||
|
|
||||||
// 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 = new Set(this.node.value.dependencies);
|
|
||||||
this.node.value.dependencies.forEach((prop: string) => {
|
|
||||||
const indirectDependencies = parent.renderer.component.indirectDependencies.get(prop);
|
|
||||||
if (indirectDependencies) {
|
|
||||||
indirectDependencies.forEach(indirectDependency => {
|
|
||||||
dependencies.add(indirectDependency);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// view to model
|
|
||||||
const valueFromDom = getValueFromDom(this.component, parent.node, this);
|
|
||||||
const handler = getEventHandler(this, this.component, block, name, snippet, dependencies, valueFromDom);
|
|
||||||
|
|
||||||
// model to view
|
|
||||||
let updateDom = getDomUpdater(node, this, snippet);
|
|
||||||
let initialUpdate = updateDom;
|
|
||||||
|
|
||||||
// special cases
|
|
||||||
if (this.node.name === 'group') {
|
|
||||||
const bindingGroup = getBindingGroup(this.component, this.value.node);
|
|
||||||
|
|
||||||
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.node.name === 'currentTime' || this.node.name === 'volume') {
|
|
||||||
updateConditions.push(`!isNaN(${snippet})`);
|
|
||||||
|
|
||||||
if (this.node.name === 'currentTime') initialUpdate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.node.name === 'paused') {
|
|
||||||
// this is necessary to prevent audio restarting by itself
|
|
||||||
const last = block.getUniqueName(`${node.var}_is_paused`);
|
|
||||||
block.addVariable(last, 'true');
|
|
||||||
|
|
||||||
updateConditions.push(`${last} !== (${last} = ${snippet})`);
|
|
||||||
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
|
|
||||||
initialUpdate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bind:offsetWidth and bind:offsetHeight
|
|
||||||
if (dimensions.test(this.node.name)) {
|
|
||||||
initialUpdate = null;
|
|
||||||
updateDom = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dependencyArray = [...this.value.dependencies]
|
|
||||||
|
|
||||||
if (dependencyArray.length === 1) {
|
|
||||||
updateConditions.push(`changed.${dependencyArray[0]}`)
|
|
||||||
} else if (dependencyArray.length > 1) {
|
|
||||||
updateConditions.push(
|
|
||||||
`(${dependencyArray.map(prop => `changed.${prop}`).join(' || ')})`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.object = name;
|
|
||||||
this.handler = handler;
|
|
||||||
this.updateDom = updateDom;
|
|
||||||
this.initialUpdate = initialUpdate;
|
|
||||||
this.needsLock = !isReadOnly && needsLock;
|
|
||||||
this.updateCondition = updateConditions.length ? updateConditions.join(' && ') : undefined;
|
|
||||||
this.isReadOnlyMediaAttribute = readOnlyMediaAttributes.has(this.node.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(block: Block) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(component: Component, 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 = component.bindingGroups.indexOf(keypath);
|
|
||||||
if (index === -1) {
|
|
||||||
index = component.bindingGroups.length;
|
|
||||||
component.bindingGroups.push(keypath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEventHandler(
|
|
||||||
binding: Binding,
|
|
||||||
component: Component,
|
|
||||||
block: Block,
|
|
||||||
name: string,
|
|
||||||
snippet: string,
|
|
||||||
dependencies: Set<string>,
|
|
||||||
value: string
|
|
||||||
) {
|
|
||||||
const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
|
|
||||||
let dependenciesArray = [...dependencies].filter(prop => prop[0] !== '$');
|
|
||||||
|
|
||||||
if (binding.isContextual) {
|
|
||||||
const tail = binding.value.node.type === 'MemberExpression'
|
|
||||||
? getTailSnippet(binding.value.node)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const head = block.bindings.get(name);
|
|
||||||
|
|
||||||
return {
|
|
||||||
usesContext: true,
|
|
||||||
usesState: true,
|
|
||||||
usesStore: storeDependencies.length > 0,
|
|
||||||
mutation: `${head}${tail} = ${value};`,
|
|
||||||
props: dependenciesArray.map(prop => `${prop}: ctx.${prop}`),
|
|
||||||
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (binding.value.node.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 `component.target.readonly` sooner so
|
|
||||||
// that we don't have to do the `.some()` here
|
|
||||||
dependenciesArray = dependenciesArray.filter(prop => !component.computations.some(computation => computation.key === prop));
|
|
||||||
|
|
||||||
return {
|
|
||||||
usesContext: false,
|
|
||||||
usesState: true,
|
|
||||||
usesStore: storeDependencies.length > 0,
|
|
||||||
mutation: `${snippet} = ${value}`,
|
|
||||||
props: dependenciesArray.map((prop: string) => `${prop}: ctx.${prop}`),
|
|
||||||
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let props;
|
|
||||||
let storeProps;
|
|
||||||
|
|
||||||
if (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(
|
|
||||||
component: Component,
|
|
||||||
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(component, binding.value.node);
|
|
||||||
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;
|
|
||||||
}
|
|
@ -0,0 +1,360 @@
|
|||||||
|
import ElementWrapper from '..';
|
||||||
|
import Block from '../../../Block';
|
||||||
|
import deindent from '../../../../../utils/deindent';
|
||||||
|
import Binding from '../../../../nodes/Binding';
|
||||||
|
import getObject from '../../../../../utils/getObject';
|
||||||
|
import Renderer from '../../../Renderer';
|
||||||
|
import getTailSnippet from '../../../../../utils/getTailSnippet';
|
||||||
|
import { Node } from '../../../../../interfaces';
|
||||||
|
import Element from '../../../../nodes/Element';
|
||||||
|
import flattenReference from '../../../../../utils/flattenReference';
|
||||||
|
import { dimensions } from '../../../../../utils/patterns';
|
||||||
|
|
||||||
|
// TODO this should live in a specific binding
|
||||||
|
const readOnlyMediaAttributes = new Set([
|
||||||
|
'duration',
|
||||||
|
'buffered',
|
||||||
|
'seekable',
|
||||||
|
'played'
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default class BindingWrapper {
|
||||||
|
element: ElementWrapper;
|
||||||
|
binding: Binding;
|
||||||
|
events: string[];
|
||||||
|
|
||||||
|
usesStore: boolean;
|
||||||
|
needsLock: boolean;
|
||||||
|
lock: string;
|
||||||
|
mutations: string[];
|
||||||
|
props: Set<string>;
|
||||||
|
storeProps: Set<string>;
|
||||||
|
|
||||||
|
object: string;
|
||||||
|
// handler: Handler;
|
||||||
|
updateDom: string;
|
||||||
|
initialUpdate: string;
|
||||||
|
updateCondition: string;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
isReadOnlyMediaAttribute: boolean;
|
||||||
|
|
||||||
|
constructor(element: ElementWrapper, binding: Binding) {
|
||||||
|
this.element = element;
|
||||||
|
this.binding = binding;
|
||||||
|
|
||||||
|
element.cannotUseInnerHTML();
|
||||||
|
|
||||||
|
this.isReadOnly = false;
|
||||||
|
this.needsLock = false;
|
||||||
|
this.events = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
fromDom() {
|
||||||
|
throw new Error(`TODO implement in subclass`);
|
||||||
|
|
||||||
|
// <select bind:value='selected>
|
||||||
|
if (this.element.node.name === 'select') {
|
||||||
|
return this.element.node.getStaticAttributeValue('multiple') === true ?
|
||||||
|
`@selectMultipleValue(${this.element.var})` :
|
||||||
|
`@selectValue(${this.element.var})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = this.element.node.getStaticAttributeValue('type');
|
||||||
|
|
||||||
|
// <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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toDom() {
|
||||||
|
throw new Error(`TODO implement in subclass`);
|
||||||
|
|
||||||
|
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};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
isReadOnlyMediaAttribute() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block) {
|
||||||
|
// const needsLock = (
|
||||||
|
// parent.node.name !== 'input' ||
|
||||||
|
// !/radio|checkbox|range|color/.test(parent.getStaticAttributeValue('type'))
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const isReadOnly = (
|
||||||
|
// (parent.isMediaNode() && readOnlyMediaAttributes.has(this.node.name)) ||
|
||||||
|
// dimensions.test(this.node.name)
|
||||||
|
// );
|
||||||
|
|
||||||
|
const lock = this.needsLock ?
|
||||||
|
block.getUniqueName(`${this.element.var}_updating`) :
|
||||||
|
null;
|
||||||
|
|
||||||
|
if (lock) block.addVariable(lock, 'false');
|
||||||
|
|
||||||
|
const updateConditions: string[] = [];
|
||||||
|
if (lock) updateConditions.push(`!${lock}`);
|
||||||
|
|
||||||
|
const { name } = getObject(this.binding.value.node);
|
||||||
|
const { snippet } = this.binding.value;
|
||||||
|
|
||||||
|
// 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 = new Set(this.binding.value.dependencies);
|
||||||
|
this.binding.value.dependencies.forEach((prop: string) => {
|
||||||
|
const indirectDependencies = this.element.renderer.component.indirectDependencies.get(prop);
|
||||||
|
if (indirectDependencies) {
|
||||||
|
indirectDependencies.forEach(indirectDependency => {
|
||||||
|
dependencies.add(indirectDependency);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// view to model
|
||||||
|
const valueFromDom = this.fromDom();
|
||||||
|
const handler = getEventHandler(this.binding, this.element.renderer, block, name, snippet, dependencies, valueFromDom);
|
||||||
|
|
||||||
|
// model to view
|
||||||
|
let updateDom = this.toDom();
|
||||||
|
let initialUpdate = updateDom;
|
||||||
|
|
||||||
|
if (this.binding.name === 'currentTime' || this.binding.name === 'volume') {
|
||||||
|
updateConditions.push(`!isNaN(${snippet})`);
|
||||||
|
|
||||||
|
if (this.binding.name === 'currentTime') initialUpdate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.binding.name === 'paused') {
|
||||||
|
// this is necessary to prevent audio restarting by itself
|
||||||
|
const last = block.getUniqueName(`${this.element.var}_is_paused`);
|
||||||
|
block.addVariable(last, 'true');
|
||||||
|
|
||||||
|
updateConditions.push(`${last} !== (${last} = ${snippet})`);
|
||||||
|
updateDom = `${this.element.var}[${last} ? "pause" : "play"]();`;
|
||||||
|
initialUpdate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind:offsetWidth and bind:offsetHeight
|
||||||
|
if (dimensions.test(this.binding.name)) {
|
||||||
|
initialUpdate = null;
|
||||||
|
updateDom = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencyArray = [...this.binding.value.dependencies]
|
||||||
|
|
||||||
|
if (dependencyArray.length === 1) {
|
||||||
|
updateConditions.push(`changed.${dependencyArray[0]}`)
|
||||||
|
} else if (dependencyArray.length > 1) {
|
||||||
|
updateConditions.push(
|
||||||
|
`(${dependencyArray.map(prop => `changed.${prop}`).join(' || ')})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.object = name;
|
||||||
|
this.handler = handler;
|
||||||
|
this.updateDom = updateDom;
|
||||||
|
this.initialUpdate = initialUpdate;
|
||||||
|
this.needsLock = !this.isReadOnly && this.needsLock; // TODO ????
|
||||||
|
this.updateCondition = updateConditions.length ? updateConditions.join(' && ') : undefined;
|
||||||
|
this.isReadOnlyMediaAttribute = readOnlyMediaAttributes.has(this.binding.name);
|
||||||
|
|
||||||
|
let animation_frame = null; // TODO media binding only
|
||||||
|
|
||||||
|
const handler_name = block.getUniqueName(`${this.element.var}_${this.events.join('_')}_handler`);
|
||||||
|
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
function ${handler_name}() {
|
||||||
|
${
|
||||||
|
animation_frame && deindent`
|
||||||
|
cancelAnimationFrame(${animation_frame});
|
||||||
|
if (!${this.element.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
|
||||||
|
}
|
||||||
|
${handler.usesStore && `var $ = #component.store.get();`}
|
||||||
|
${this.needsLock && `${lock} = true;`}
|
||||||
|
${handler.mutation}
|
||||||
|
${handler.props.length > 0 && `#component.set({ ${handler.props.join(', ')} });`}
|
||||||
|
${handler.storeProps.length > 0 && `#component.store.set({ ${handler.storeProps.join(', ')} });`}
|
||||||
|
${this.needsLock && `${lock} = false;`}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.update.addLine(
|
||||||
|
updateConditions.length ? `if (${updateConditions.join(' && ')}) ${this.updateDom}` : this.updateDom
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.hydrate.addLine(
|
||||||
|
`@addListener(${this.element.var}, "${name}", ${handler_name});`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(
|
||||||
|
`@removeListener(${this.element.var}, "${name}", ${handler_name});`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventHandler(
|
||||||
|
binding: Binding,
|
||||||
|
renderer: Renderer,
|
||||||
|
block: Block,
|
||||||
|
name: string,
|
||||||
|
snippet: string,
|
||||||
|
dependencies: Set<string>,
|
||||||
|
value: string
|
||||||
|
) {
|
||||||
|
const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
|
||||||
|
let dependenciesArray = [...dependencies].filter(prop => prop[0] !== '$');
|
||||||
|
|
||||||
|
if (binding.isContextual) {
|
||||||
|
const tail = binding.value.node.type === 'MemberExpression'
|
||||||
|
? getTailSnippet(binding.value.node)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const head = block.bindings.get(name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
usesContext: true,
|
||||||
|
usesState: true,
|
||||||
|
usesStore: storeDependencies.length > 0,
|
||||||
|
mutation: `${head}${tail} = ${value};`,
|
||||||
|
props: dependenciesArray.map(prop => `${prop}: ctx.${prop}`),
|
||||||
|
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.value.node.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 `component.target.readonly` sooner so
|
||||||
|
// that we don't have to do the `.some()` here
|
||||||
|
dependenciesArray = dependenciesArray.filter(prop => !renderer.component.computations.some(computation => computation.key === prop));
|
||||||
|
|
||||||
|
return {
|
||||||
|
usesContext: false,
|
||||||
|
usesState: true,
|
||||||
|
usesStore: storeDependencies.length > 0,
|
||||||
|
mutation: `${snippet} = ${value}`,
|
||||||
|
props: dependenciesArray.map((prop: string) => `${prop}: ctx.${prop}`),
|
||||||
|
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let props;
|
||||||
|
let storeProps;
|
||||||
|
|
||||||
|
if (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 fromDom(
|
||||||
|
// renderer: Renderer,
|
||||||
|
// node: Element,
|
||||||
|
// binding: Binding
|
||||||
|
// ) {
|
||||||
|
// // <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(renderer, binding.value.node);
|
||||||
|
// 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 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};`;
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
import Binding from '../../../../nodes/Binding';
|
||||||
|
import Element from '../../../../nodes/Element';
|
||||||
|
import ElementWrapper from '..';
|
||||||
|
import Block from '../../../Block';
|
||||||
|
import Renderer from '../../../Renderer';
|
||||||
|
import flattenReference from '../../../../../utils/flattenReference';
|
||||||
|
import { Node } from '../../../../../interfaces';
|
||||||
|
import BindingWrapper from './Binding';
|
||||||
|
|
||||||
|
export default class InputRadioGroupBinding extends BindingWrapper {
|
||||||
|
element: ElementWrapper;
|
||||||
|
node: Binding;
|
||||||
|
|
||||||
|
static filter(
|
||||||
|
node: Element,
|
||||||
|
binding_lookup: Record<string, Binding>,
|
||||||
|
type: string
|
||||||
|
) {
|
||||||
|
if (node.name === 'input' && binding_lookup.group) {
|
||||||
|
return type === 'radio';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
element: ElementWrapper,
|
||||||
|
binding_lookup: Record<string, Binding>
|
||||||
|
) {
|
||||||
|
super(element, binding_lookup.group);
|
||||||
|
this.events = ['change'];
|
||||||
|
}
|
||||||
|
|
||||||
|
fromDom() {
|
||||||
|
const bindingGroup = getBindingGroup(this.element.renderer, this.binding.value.node);
|
||||||
|
if (this.element.node.getStaticAttributeValue('type') === 'checkbox') {
|
||||||
|
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${this.element.var}.__value`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toDom() {
|
||||||
|
const type = this.element.node.getStaticAttributeValue('type');
|
||||||
|
|
||||||
|
const condition = type === 'checkbox'
|
||||||
|
? `~${this.binding.value.snippet}.indexOf(${this.element.var}.__value)`
|
||||||
|
: `${this.element.var}.__value === ${this.binding.value.snippet}`;
|
||||||
|
|
||||||
|
return `${this.element.var}.checked = ${condition};`
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block) {
|
||||||
|
const bindingGroup = getBindingGroup(
|
||||||
|
this.element.renderer,
|
||||||
|
this.binding.value.node
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.hydrate.addLine(
|
||||||
|
`#component._bindingGroups[${bindingGroup}].push(${this.element.var});`
|
||||||
|
);
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(
|
||||||
|
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${this.element.var}), 1);`
|
||||||
|
);
|
||||||
|
|
||||||
|
super.render(block);
|
||||||
|
|
||||||
|
// this.renderHandler(block, 'TODO');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBindingGroup(renderer: Renderer, 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 = renderer.bindingGroups.indexOf(keypath);
|
||||||
|
if (index === -1) {
|
||||||
|
index = renderer.bindingGroups.length;
|
||||||
|
renderer.bindingGroups.push(keypath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import Binding from '../../../../nodes/Binding';
|
||||||
|
import Element from '../../../../nodes/Element';
|
||||||
|
import ElementWrapper from '..';
|
||||||
|
import Block from '../../../Block';
|
||||||
|
import BindingWrapper from './Binding';
|
||||||
|
|
||||||
|
export default class InputTextBinding extends BindingWrapper {
|
||||||
|
static filter(
|
||||||
|
node: Element,
|
||||||
|
binding_lookup: Record<string, Binding>,
|
||||||
|
type: string
|
||||||
|
) {
|
||||||
|
if (node.name === 'textarea' && binding_lookup.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.name === 'input' && binding_lookup.value) {
|
||||||
|
return !/radio|checkbox|range/.test(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
element: ElementWrapper,
|
||||||
|
binding_lookup: Record<string, Binding>
|
||||||
|
) {
|
||||||
|
super(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
import Binding from '../../../nodes/Binding';
|
||||||
|
import ElementWrapper from '.';
|
||||||
|
import { dimensions } from '../../../../utils/patterns';
|
||||||
|
import getObject from '../../../../utils/getObject';
|
||||||
|
import Block from '../../Block';
|
||||||
|
|
||||||
|
type Handler = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class BindingWrapper {
|
||||||
|
node: Binding;
|
||||||
|
parent: ElementWrapper;
|
||||||
|
|
||||||
|
object: string;
|
||||||
|
handler: Handler;
|
||||||
|
updateDom: string;
|
||||||
|
initialUpdate: string;
|
||||||
|
needsLock: boolean;
|
||||||
|
updateCondition: string;
|
||||||
|
isReadOnlyMediaAttribute: boolean;
|
||||||
|
|
||||||
|
constructor(node: Binding, parent: ElementWrapper) {
|
||||||
|
this.node = node;
|
||||||
|
this.parent = parent;
|
||||||
|
|
||||||
|
parent.cannotUseInnerHTML();
|
||||||
|
|
||||||
|
const needsLock = (
|
||||||
|
parent.node.name !== 'input' ||
|
||||||
|
!/radio|checkbox|range|color/.test(parent.getStaticAttributeValue('type'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const isReadOnly = (
|
||||||
|
(parent.isMediaNode() && readOnlyMediaAttributes.has(this.node.name)) ||
|
||||||
|
dimensions.test(this.node.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
let updateConditions: string[] = [];
|
||||||
|
|
||||||
|
const { name } = getObject(this.node.value.node);
|
||||||
|
const { snippet } = this.node.value;
|
||||||
|
|
||||||
|
// 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 = new Set(this.node.value.dependencies);
|
||||||
|
this.node.value.dependencies.forEach((prop: string) => {
|
||||||
|
const indirectDependencies = parent.renderer.component.indirectDependencies.get(prop);
|
||||||
|
if (indirectDependencies) {
|
||||||
|
indirectDependencies.forEach(indirectDependency => {
|
||||||
|
dependencies.add(indirectDependency);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// view to model
|
||||||
|
const valueFromDom = getValueFromDom(this.component, parent.node, this);
|
||||||
|
const handler = getEventHandler(this, this.component, block, name, snippet, dependencies, valueFromDom);
|
||||||
|
|
||||||
|
// model to view
|
||||||
|
let updateDom = getDomUpdater(node, this, snippet);
|
||||||
|
let initialUpdate = updateDom;
|
||||||
|
|
||||||
|
if (this.node.name === 'currentTime' || this.node.name === 'volume') {
|
||||||
|
updateConditions.push(`!isNaN(${snippet})`);
|
||||||
|
|
||||||
|
if (this.node.name === 'currentTime') initialUpdate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.node.name === 'paused') {
|
||||||
|
// this is necessary to prevent audio restarting by itself
|
||||||
|
const last = block.getUniqueName(`${node.var}_is_paused`);
|
||||||
|
block.addVariable(last, 'true');
|
||||||
|
|
||||||
|
updateConditions.push(`${last} !== (${last} = ${snippet})`);
|
||||||
|
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
|
||||||
|
initialUpdate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bind:offsetWidth and bind:offsetHeight
|
||||||
|
if (dimensions.test(this.node.name)) {
|
||||||
|
initialUpdate = null;
|
||||||
|
updateDom = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencyArray = [...this.value.dependencies]
|
||||||
|
|
||||||
|
if (dependencyArray.length === 1) {
|
||||||
|
updateConditions.push(`changed.${dependencyArray[0]}`)
|
||||||
|
} else if (dependencyArray.length > 1) {
|
||||||
|
updateConditions.push(
|
||||||
|
`(${dependencyArray.map(prop => `changed.${prop}`).join(' || ')})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.object = name;
|
||||||
|
this.handler = handler;
|
||||||
|
this.updateDom = updateDom;
|
||||||
|
this.initialUpdate = initialUpdate;
|
||||||
|
this.needsLock = !isReadOnly && needsLock;
|
||||||
|
this.updateCondition = updateConditions.length ? updateConditions.join(' && ') : undefined;
|
||||||
|
this.isReadOnlyMediaAttribute = readOnlyMediaAttributes.has(this.node.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(block: Block) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isComputed(node: Node) {
|
||||||
|
while (node.type === 'MemberExpression') {
|
||||||
|
if (node.computed) return true;
|
||||||
|
node = node.object;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
@ -0,0 +1,476 @@
|
|||||||
|
import Wrapper from './shared/wrapper';
|
||||||
|
import Renderer from '../Renderer';
|
||||||
|
import Block from '../Block';
|
||||||
|
import EachBlock from '../../nodes/EachBlock';
|
||||||
|
import IfBlock from '../../nodes/IfBlock';
|
||||||
|
import createDebuggingComment from '../../../utils/createDebuggingComment';
|
||||||
|
import ElseBlock from '../../nodes/ElseBlock';
|
||||||
|
import FragmentWrapper from './Fragment';
|
||||||
|
import deindent from '../../../utils/deindent';
|
||||||
|
|
||||||
|
function isElseIf(node: ElseBlock) {
|
||||||
|
return (
|
||||||
|
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isElseBranch(branch) {
|
||||||
|
return branch.block && !branch.condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
class IfBlockBranch extends Wrapper {
|
||||||
|
block: Block;
|
||||||
|
fragment: FragmentWrapper;
|
||||||
|
condition: string;
|
||||||
|
isDynamic: boolean;
|
||||||
|
|
||||||
|
var = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
renderer: Renderer,
|
||||||
|
block: Block,
|
||||||
|
parent: IfBlockWrapper,
|
||||||
|
node: IfBlock | ElseBlock,
|
||||||
|
stripWhitespace: boolean,
|
||||||
|
nextSibling: Wrapper
|
||||||
|
) {
|
||||||
|
super(renderer, block, parent, node);
|
||||||
|
|
||||||
|
this.condition = (<IfBlock>node).expression && (<IfBlock>node).expression.snippet;
|
||||||
|
|
||||||
|
this.block = block.child({
|
||||||
|
comment: createDebuggingComment(node, parent.renderer.component),
|
||||||
|
name: parent.renderer.component.getUniqueName(
|
||||||
|
(<IfBlock>node).expression ? `create_if_block` : `create_else_block`
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fragment = new FragmentWrapper(renderer, block, node.children, parent.parent, stripWhitespace, nextSibling);
|
||||||
|
|
||||||
|
this.isDynamic = this.block.dependencies.size > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class IfBlockWrapper extends Wrapper {
|
||||||
|
node: IfBlock;
|
||||||
|
branches: IfBlockBranch[];
|
||||||
|
|
||||||
|
var = 'if_block';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
renderer: Renderer,
|
||||||
|
block: Block,
|
||||||
|
parent: Wrapper,
|
||||||
|
node: EachBlock,
|
||||||
|
stripWhitespace: boolean,
|
||||||
|
nextSibling: Wrapper
|
||||||
|
) {
|
||||||
|
super(renderer, block, parent, node);
|
||||||
|
|
||||||
|
const { component } = renderer;
|
||||||
|
|
||||||
|
this.cannotUseInnerHTML();
|
||||||
|
|
||||||
|
this.branches = [];
|
||||||
|
|
||||||
|
const blocks: Block[] = [];
|
||||||
|
let dynamic = false;
|
||||||
|
let hasIntros = false;
|
||||||
|
let hasOutros = false;
|
||||||
|
|
||||||
|
const createBranches = (node: IfBlock) => {
|
||||||
|
const branch = new IfBlockBranch(
|
||||||
|
renderer,
|
||||||
|
block,
|
||||||
|
this,
|
||||||
|
node,
|
||||||
|
stripWhitespace,
|
||||||
|
nextSibling
|
||||||
|
);
|
||||||
|
|
||||||
|
this.branches.push(branch);
|
||||||
|
|
||||||
|
blocks.push(branch.block);
|
||||||
|
block.addDependencies(node.expression.dependencies);
|
||||||
|
|
||||||
|
if (branch.isDynamic) {
|
||||||
|
dynamic = true;
|
||||||
|
block.addDependencies(branch.block.dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (branch.block.hasIntros) hasIntros = true;
|
||||||
|
if (branch.block.hasOutros) hasOutros = true;
|
||||||
|
|
||||||
|
if (isElseIf(node.else)) {
|
||||||
|
createBranches(node.else.children[0]);
|
||||||
|
} else if (node.else) {
|
||||||
|
const branch = new IfBlockBranch(
|
||||||
|
renderer,
|
||||||
|
block,
|
||||||
|
this,
|
||||||
|
node.else,
|
||||||
|
stripWhitespace,
|
||||||
|
nextSibling
|
||||||
|
);
|
||||||
|
|
||||||
|
this.branches.push(branch);
|
||||||
|
|
||||||
|
blocks.push(branch.block);
|
||||||
|
|
||||||
|
if (branch.block.dependencies.size > 0) {
|
||||||
|
dynamic = true;
|
||||||
|
block.addDependencies(branch.block.dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (branch.block.hasIntros) hasIntros = true;
|
||||||
|
if (branch.block.hasOutros) hasOutros = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createBranches(this.node);
|
||||||
|
|
||||||
|
if (component.options.nestedTransitions) {
|
||||||
|
if (hasIntros) block.addIntro();
|
||||||
|
if (hasOutros) block.addOutro();
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.forEach(block => {
|
||||||
|
block.hasUpdateMethod = dynamic;
|
||||||
|
block.hasIntroMethod = hasIntros;
|
||||||
|
block.hasOutroMethod = hasOutros;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.blocks.push(...blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
block: Block,
|
||||||
|
parentNode: string,
|
||||||
|
parentNodes: string
|
||||||
|
) {
|
||||||
|
const name = this.var;
|
||||||
|
|
||||||
|
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
|
||||||
|
const anchor = needsAnchor
|
||||||
|
? block.getUniqueName(`${name}_anchor`)
|
||||||
|
: (this.next && this.next.var) || 'null';
|
||||||
|
|
||||||
|
const hasElse = !(this.branches[this.branches.length - 1].condition);
|
||||||
|
const if_name = hasElse ? '' : `if (${name}) `;
|
||||||
|
|
||||||
|
const dynamic = this.branches[0].block.hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value
|
||||||
|
const hasOutros = this.branches[0].block.hasOutroMethod;
|
||||||
|
|
||||||
|
const vars = { name, anchor, if_name, hasElse };
|
||||||
|
|
||||||
|
if (this.node.else) {
|
||||||
|
if (hasOutros) {
|
||||||
|
this.renderCompoundWithOutros(block, parentNode, parentNodes, branches, dynamic, vars);
|
||||||
|
|
||||||
|
if (this.renderer.options.nestedTransitions) {
|
||||||
|
block.builders.outro.addBlock(deindent`
|
||||||
|
if (${name}) ${name}.o(#outrocallback);
|
||||||
|
else #outrocallback();
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.renderCompound(block, parentNode, parentNodes, dynamic, vars);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.renderSimple(block, parentNode, parentNodes, dynamic, vars);
|
||||||
|
|
||||||
|
if (hasOutros && this.renderer.options.nestedTransitions) {
|
||||||
|
block.builders.outro.addBlock(deindent`
|
||||||
|
if (${name}) ${name}.o(#outrocallback);
|
||||||
|
else #outrocallback();
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.create.addLine(`${if_name}${name}.c();`);
|
||||||
|
|
||||||
|
if (parentNodes) {
|
||||||
|
block.builders.claim.addLine(
|
||||||
|
`${if_name}${name}.l(${parentNodes});`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsAnchor) {
|
||||||
|
block.addElement(
|
||||||
|
anchor,
|
||||||
|
`@createComment()`,
|
||||||
|
parentNodes && `@createComment()`,
|
||||||
|
parentNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.branches.forEach(branch => {
|
||||||
|
branch.fragment.render(branch.block, parentNode, parentNodes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCompound(
|
||||||
|
block: Block,
|
||||||
|
parentNode: string,
|
||||||
|
parentNodes: string,
|
||||||
|
dynamic,
|
||||||
|
{ name, anchor, hasElse, if_name }
|
||||||
|
) {
|
||||||
|
const select_block_type = this.component.getUniqueName(`select_block_type`);
|
||||||
|
const current_block_type = block.getUniqueName(`current_block_type`);
|
||||||
|
const current_block_type_and = hasElse ? '' : `${current_block_type} && `;
|
||||||
|
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
function ${select_block_type}(ctx) {
|
||||||
|
${this.branches
|
||||||
|
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block.name};`)
|
||||||
|
.join('\n')}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
var ${current_block_type} = ${select_block_type}(ctx);
|
||||||
|
var ${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
|
||||||
|
`);
|
||||||
|
|
||||||
|
const mountOrIntro = this.branches[0].block.hasIntroMethod ? 'i' : 'm';
|
||||||
|
|
||||||
|
const initialMountNode = parentNode || '#target';
|
||||||
|
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||||
|
block.builders.mount.addLine(
|
||||||
|
`${if_name}${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});`
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||||
|
|
||||||
|
const changeBlock = deindent`
|
||||||
|
${if_name}${name}.d(1);
|
||||||
|
${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
|
||||||
|
${if_name}${name}.c();
|
||||||
|
${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (dynamic) {
|
||||||
|
block.builders.update.addBlock(deindent`
|
||||||
|
if (${current_block_type} === (${current_block_type} = ${select_block_type}(ctx)) && ${name}) {
|
||||||
|
${name}.p(changed, ctx);
|
||||||
|
} else {
|
||||||
|
${changeBlock}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
block.builders.update.addBlock(deindent`
|
||||||
|
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(ctx))) {
|
||||||
|
${changeBlock}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(`${if_name}${name}.d(${parentNode ? '' : 'detach'});`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if any of the siblings have outros, we need to keep references to the blocks
|
||||||
|
// (TODO does this only apply to bidi transitions?)
|
||||||
|
renderCompoundWithOutros(
|
||||||
|
block: Block,
|
||||||
|
parentNode: string,
|
||||||
|
parentNodes: string,
|
||||||
|
dynamic,
|
||||||
|
{ name, anchor, hasElse }
|
||||||
|
) {
|
||||||
|
const select_block_type = this.renderer.component.getUniqueName(`select_block_type`);
|
||||||
|
const current_block_type_index = block.getUniqueName(`current_block_type_index`);
|
||||||
|
const previous_block_index = block.getUniqueName(`previous_block_index`);
|
||||||
|
const if_block_creators = block.getUniqueName(`if_block_creators`);
|
||||||
|
const if_blocks = block.getUniqueName(`if_blocks`);
|
||||||
|
|
||||||
|
const if_current_block_type_index = hasElse
|
||||||
|
? ''
|
||||||
|
: `if (~${current_block_type_index}) `;
|
||||||
|
|
||||||
|
block.addVariable(current_block_type_index);
|
||||||
|
block.addVariable(name);
|
||||||
|
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
var ${if_block_creators} = [
|
||||||
|
${this.branches.map(branch => branch.block.name).join(',\n')}
|
||||||
|
];
|
||||||
|
|
||||||
|
var ${if_blocks} = [];
|
||||||
|
|
||||||
|
function ${select_block_type}(ctx) {
|
||||||
|
${this.branches
|
||||||
|
.map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`)
|
||||||
|
.join('\n')}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (hasElse) {
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
${current_block_type_index} = ${select_block_type}(ctx);
|
||||||
|
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
if (~(${current_block_type_index} = ${select_block_type}(ctx))) {
|
||||||
|
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mountOrIntro = this.branches[0].block.hasIntroMethod ? 'i' : 'm';
|
||||||
|
const initialMountNode = parentNode || '#target';
|
||||||
|
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||||
|
|
||||||
|
block.builders.mount.addLine(
|
||||||
|
`${if_current_block_type_index}${if_blocks}[${current_block_type_index}].${mountOrIntro}(${initialMountNode}, ${anchorNode});`
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||||
|
|
||||||
|
const destroyOldBlock = deindent`
|
||||||
|
@groupOutros();
|
||||||
|
${name}.o(function() {
|
||||||
|
${if_blocks}[${previous_block_index}].d(1);
|
||||||
|
${if_blocks}[${previous_block_index}] = null;
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const createNewBlock = deindent`
|
||||||
|
${name} = ${if_blocks}[${current_block_type_index}];
|
||||||
|
if (!${name}) {
|
||||||
|
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
|
||||||
|
${name}.c();
|
||||||
|
}
|
||||||
|
${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const changeBlock = hasElse
|
||||||
|
? deindent`
|
||||||
|
${destroyOldBlock}
|
||||||
|
|
||||||
|
${createNewBlock}
|
||||||
|
`
|
||||||
|
: deindent`
|
||||||
|
if (${name}) {
|
||||||
|
${destroyOldBlock}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (~${current_block_type_index}) {
|
||||||
|
${createNewBlock}
|
||||||
|
} else {
|
||||||
|
${name} = null;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (dynamic) {
|
||||||
|
block.builders.update.addBlock(deindent`
|
||||||
|
var ${previous_block_index} = ${current_block_type_index};
|
||||||
|
${current_block_type_index} = ${select_block_type}(ctx);
|
||||||
|
if (${current_block_type_index} === ${previous_block_index}) {
|
||||||
|
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx);
|
||||||
|
} else {
|
||||||
|
${changeBlock}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
block.builders.update.addBlock(deindent`
|
||||||
|
var ${previous_block_index} = ${current_block_type_index};
|
||||||
|
${current_block_type_index} = ${select_block_type}(ctx);
|
||||||
|
if (${current_block_type_index} !== ${previous_block_index}) {
|
||||||
|
${changeBlock}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(deindent`
|
||||||
|
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].d(${parentNode ? '' : 'detach'});
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSimple(
|
||||||
|
block: Block,
|
||||||
|
parentNode: string,
|
||||||
|
parentNodes: string,
|
||||||
|
dynamic,
|
||||||
|
{ name, anchor, if_name }
|
||||||
|
) {
|
||||||
|
const branch = this.branches[0];
|
||||||
|
|
||||||
|
block.builders.init.addBlock(deindent`
|
||||||
|
var ${name} = (${branch.condition}) && ${branch.block.name}(#component, ctx);
|
||||||
|
`);
|
||||||
|
|
||||||
|
const mountOrIntro = branch.block.hasIntroMethod ? 'i' : 'm';
|
||||||
|
const initialMountNode = parentNode || '#target';
|
||||||
|
const anchorNode = parentNode ? 'null' : 'anchor';
|
||||||
|
|
||||||
|
block.builders.mount.addLine(
|
||||||
|
`if (${name}) ${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});`
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateMountNode = this.getUpdateMountNode(anchor);
|
||||||
|
|
||||||
|
const enter = dynamic
|
||||||
|
? (branch.block.hasIntroMethod || branch.block.hasOutroMethod)
|
||||||
|
? deindent`
|
||||||
|
if (${name}) {
|
||||||
|
${name}.p(changed, ctx);
|
||||||
|
} else {
|
||||||
|
${name} = ${branch.block.name}(#component, ctx);
|
||||||
|
if (${name}) ${name}.c();
|
||||||
|
}
|
||||||
|
|
||||||
|
${name}.i(${updateMountNode}, ${anchor});
|
||||||
|
`
|
||||||
|
: deindent`
|
||||||
|
if (${name}) {
|
||||||
|
${name}.p(changed, ctx);
|
||||||
|
} else {
|
||||||
|
${name} = ${branch.block.name}(#component, ctx);
|
||||||
|
${name}.c();
|
||||||
|
${name}.m(${updateMountNode}, ${anchor});
|
||||||
|
}
|
||||||
|
`
|
||||||
|
: (branch.block.hasIntroMethod || branch.block.hasOutroMethod)
|
||||||
|
? deindent`
|
||||||
|
if (!${name}) {
|
||||||
|
${name} = ${branch.block.name}(#component, ctx);
|
||||||
|
${name}.c();
|
||||||
|
}
|
||||||
|
${name}.i(${updateMountNode}, ${anchor});
|
||||||
|
`
|
||||||
|
: deindent`
|
||||||
|
if (!${name}) {
|
||||||
|
${name} = ${branch.block.name}(#component, ctx);
|
||||||
|
${name}.c();
|
||||||
|
${name}.m(${updateMountNode}, ${anchor});
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// no `p()` here — we don't want to update outroing nodes,
|
||||||
|
// as that will typically result in glitching
|
||||||
|
const exit = branch.block.hasOutroMethod
|
||||||
|
? deindent`
|
||||||
|
@groupOutros();
|
||||||
|
${name}.o(function() {
|
||||||
|
${name}.d(1);
|
||||||
|
${name} = null;
|
||||||
|
});
|
||||||
|
`
|
||||||
|
: deindent`
|
||||||
|
${name}.d(1);
|
||||||
|
${name} = null;
|
||||||
|
`;
|
||||||
|
|
||||||
|
block.builders.update.addBlock(deindent`
|
||||||
|
if (${branch.condition}) {
|
||||||
|
${enter}
|
||||||
|
} else if (${name}) {
|
||||||
|
${exit}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
block.builders.destroy.addLine(`${if_name}${name}.d(${parentNode ? '' : 'detach'});`);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,3 @@
|
|||||||
{#if visible}
|
{#if visible}
|
||||||
<input ref:input autofocus>
|
<input ref:input autofocus>
|
||||||
{/if}
|
{/if}
|
Loading…
Reference in new issue