mirror of https://github.com/sveltejs/svelte
332 lines
8.9 KiB
332 lines
8.9 KiB
import Binding from '../../../nodes/Binding';
|
|
import ElementWrapper from '../Element';
|
|
import get_object from '../../../utils/get_object';
|
|
import Block from '../../Block';
|
|
import Node from '../../../nodes/shared/Node';
|
|
import Renderer from '../../Renderer';
|
|
import flatten_reference from '../../../utils/flatten_reference';
|
|
import EachBlock from '../../../nodes/EachBlock';
|
|
import { Node as INode } from '../../../../interfaces';
|
|
|
|
function get_tail(node: INode) {
|
|
const end = node.end;
|
|
while (node.type === 'MemberExpression') node = node.object;
|
|
return { start: node.end, end };
|
|
}
|
|
|
|
export default class BindingWrapper {
|
|
node: Binding;
|
|
parent: ElementWrapper;
|
|
|
|
object: string;
|
|
handler: {
|
|
uses_context: boolean;
|
|
mutation: string;
|
|
contextual_dependencies: Set<string>;
|
|
snippet?: string;
|
|
};
|
|
snippet: string;
|
|
is_readonly: boolean;
|
|
needs_lock: boolean;
|
|
|
|
constructor(block: Block, node: Binding, parent: ElementWrapper) {
|
|
this.node = node;
|
|
this.parent = parent;
|
|
|
|
const { dependencies } = this.node.expression;
|
|
|
|
block.add_dependencies(dependencies);
|
|
|
|
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
|
|
if (parent.node.name === 'select') {
|
|
parent.select_binding_dependencies = dependencies;
|
|
dependencies.forEach((prop: string) => {
|
|
parent.renderer.component.indirect_dependencies.set(prop, new Set());
|
|
});
|
|
}
|
|
|
|
if (node.is_contextual) {
|
|
// we need to ensure that the each block creates a context including
|
|
// the list and the index, if they're not otherwise referenced
|
|
const { name } = get_object(this.node.expression.node);
|
|
const each_block = this.parent.node.scope.get_owner(name);
|
|
|
|
(each_block as EachBlock).has_binding = true;
|
|
}
|
|
|
|
this.object = get_object(this.node.expression.node).name;
|
|
|
|
// TODO unfortunate code is necessary because we need to use `ctx`
|
|
// inside the fragment, but not inside the <script>
|
|
const contextless_snippet = this.parent.renderer.component.source.slice(this.node.expression.node.start, this.node.expression.node.end);
|
|
|
|
// view to model
|
|
this.handler = get_event_handler(this, parent.renderer, block, this.object, contextless_snippet);
|
|
|
|
this.snippet = this.node.expression.render(block);
|
|
|
|
this.is_readonly = this.node.is_readonly;
|
|
|
|
this.needs_lock = this.node.name === 'currentTime'; // TODO others?
|
|
}
|
|
|
|
get_dependencies() {
|
|
const dependencies = new Set(this.node.expression.dependencies);
|
|
|
|
this.node.expression.dependencies.forEach((prop: string) => {
|
|
const indirect_dependencies = this.parent.renderer.component.indirect_dependencies.get(prop);
|
|
if (indirect_dependencies) {
|
|
indirect_dependencies.forEach(indirect_dependency => {
|
|
dependencies.add(indirect_dependency);
|
|
});
|
|
}
|
|
});
|
|
|
|
return dependencies;
|
|
}
|
|
|
|
is_readonly_media_attribute() {
|
|
return this.node.is_readonly_media_attribute();
|
|
}
|
|
|
|
render(block: Block, lock: string) {
|
|
if (this.is_readonly) return;
|
|
|
|
const { parent } = this;
|
|
|
|
const update_conditions: string[] = this.needs_lock ? [`!${lock}`] : [];
|
|
|
|
const dependency_array = [...this.node.expression.dependencies];
|
|
|
|
if (dependency_array.length === 1) {
|
|
update_conditions.push(`changed.${dependency_array[0]}`);
|
|
} else if (dependency_array.length > 1) {
|
|
update_conditions.push(
|
|
`(${dependency_array.map(prop => `changed.${prop}`).join(' || ')})`
|
|
);
|
|
}
|
|
|
|
if (parent.node.name === 'input') {
|
|
const type = parent.node.get_static_attribute_value('type');
|
|
|
|
if (type === null || type === "" || type === "text") {
|
|
update_conditions.push(`(${parent.var}.${this.node.name} !== ${this.snippet})`);
|
|
}
|
|
}
|
|
|
|
// model to view
|
|
let update_dom = get_dom_updater(parent, this);
|
|
|
|
// special cases
|
|
switch (this.node.name) {
|
|
case 'group':
|
|
{
|
|
const binding_group = get_binding_group(parent.renderer, this.node.expression.node);
|
|
|
|
block.builders.hydrate.add_line(
|
|
`ctx.$$binding_groups[${binding_group}].push(${parent.var});`
|
|
);
|
|
|
|
block.builders.destroy.add_line(
|
|
`ctx.$$binding_groups[${binding_group}].splice(ctx.$$binding_groups[${binding_group}].indexOf(${parent.var}), 1);`
|
|
);
|
|
break;
|
|
}
|
|
|
|
case 'currentTime':
|
|
case 'playbackRate':
|
|
case 'volume':
|
|
update_conditions.push(`!@_isNaN(${this.snippet})`);
|
|
break;
|
|
|
|
case 'paused':
|
|
{
|
|
// this is necessary to prevent audio restarting by itself
|
|
const last = block.get_unique_name(`${parent.var}_is_paused`);
|
|
block.add_variable(last, 'true');
|
|
|
|
update_conditions.push(`${last} !== (${last} = ${this.snippet})`);
|
|
update_dom = `${parent.var}[${last} ? "pause" : "play"]();`;
|
|
break;
|
|
}
|
|
|
|
case 'value':
|
|
if (parent.node.get_static_attribute_value('type') === 'file') {
|
|
update_dom = null;
|
|
}
|
|
}
|
|
|
|
if (update_dom) {
|
|
block.builders.update.add_line(
|
|
update_conditions.length ? `if (${update_conditions.join(' && ')}) ${update_dom}` : update_dom
|
|
);
|
|
}
|
|
|
|
if (!/(currentTime|paused)/.test(this.node.name)) {
|
|
block.builders.mount.add_block(update_dom);
|
|
}
|
|
}
|
|
}
|
|
|
|
function get_dom_updater(
|
|
element: ElementWrapper,
|
|
binding: BindingWrapper
|
|
) {
|
|
const { node } = element;
|
|
|
|
if (binding.is_readonly_media_attribute()) {
|
|
return null;
|
|
}
|
|
|
|
if (binding.node.name === 'this') {
|
|
return null;
|
|
}
|
|
|
|
if (node.name === 'select') {
|
|
return node.get_static_attribute_value('multiple') === true ?
|
|
`@select_options(${element.var}, ${binding.snippet})` :
|
|
`@select_option(${element.var}, ${binding.snippet})`;
|
|
}
|
|
|
|
if (binding.node.name === 'group') {
|
|
const type = node.get_static_attribute_value('type');
|
|
|
|
const condition = type === 'checkbox'
|
|
? `~${binding.snippet}.indexOf(${element.var}.__value)`
|
|
: `${element.var}.__value === ${binding.snippet}`;
|
|
|
|
return `${element.var}.checked = ${condition};`;
|
|
}
|
|
|
|
if (binding.node.name === 'text') {
|
|
return `if (${binding.snippet} !== ${element.var}.textContent) ${element.var}.textContent = ${binding.snippet};`;
|
|
}
|
|
|
|
if (binding.node.name === 'html') {
|
|
return `if (${binding.snippet} !== ${element.var}.innerHTML) ${element.var}.innerHTML = ${binding.snippet};`;
|
|
}
|
|
|
|
return `${element.var}.${binding.node.name} = ${binding.snippet};`;
|
|
}
|
|
|
|
function get_binding_group(renderer: Renderer, value: Node) {
|
|
const { parts } = flatten_reference(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.binding_groups.indexOf(keypath);
|
|
if (index === -1) {
|
|
index = renderer.binding_groups.length;
|
|
renderer.binding_groups.push(keypath);
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
function mutate_store(store, value, tail) {
|
|
return tail
|
|
? `${store}.update($$value => ($$value${tail} = ${value}, $$value));`
|
|
: `${store}.set(${value});`;
|
|
}
|
|
|
|
function get_event_handler(
|
|
binding: BindingWrapper,
|
|
renderer: Renderer,
|
|
block: Block,
|
|
name: string,
|
|
snippet: string
|
|
) {
|
|
const value = get_value_from_dom(renderer, binding.parent, binding);
|
|
const store = binding.object[0] === '$' ? binding.object.slice(1) : null;
|
|
|
|
let tail = '';
|
|
if (binding.node.expression.node.type === 'MemberExpression') {
|
|
const { start, end } = get_tail(binding.node.expression.node);
|
|
tail = renderer.component.source.slice(start, end);
|
|
}
|
|
|
|
if (binding.node.is_contextual) {
|
|
const { object, property, snippet } = block.bindings.get(name);
|
|
|
|
return {
|
|
uses_context: true,
|
|
mutation: store
|
|
? mutate_store(store, value, tail)
|
|
: `${snippet}${tail} = ${value};`,
|
|
contextual_dependencies: new Set([object, property])
|
|
};
|
|
}
|
|
|
|
const mutation = store
|
|
? mutate_store(store, value, tail)
|
|
: `${snippet} = ${value};`;
|
|
|
|
if (binding.node.expression.node.type === 'MemberExpression') {
|
|
return {
|
|
uses_context: binding.node.expression.uses_context,
|
|
mutation,
|
|
contextual_dependencies: binding.node.expression.contextual_dependencies,
|
|
snippet
|
|
};
|
|
}
|
|
|
|
return {
|
|
uses_context: false,
|
|
mutation,
|
|
contextual_dependencies: new Set()
|
|
};
|
|
}
|
|
|
|
function get_value_from_dom(
|
|
renderer: Renderer,
|
|
element: ElementWrapper,
|
|
binding: BindingWrapper
|
|
) {
|
|
const { node } = element;
|
|
const { name } = binding.node;
|
|
|
|
if (name === 'this') {
|
|
return `$$node`;
|
|
}
|
|
|
|
// <select bind:value='selected>
|
|
if (node.name === 'select') {
|
|
return node.get_static_attribute_value('multiple') === true ?
|
|
`@select_multiple_value(this)` :
|
|
`@select_value(this)`;
|
|
}
|
|
|
|
const type = node.get_static_attribute_value('type');
|
|
|
|
// <input type='checkbox' bind:group='foo'>
|
|
if (name === 'group') {
|
|
const binding_group = get_binding_group(renderer, binding.node.expression.node);
|
|
if (type === 'checkbox') {
|
|
return `@get_binding_group_value($$binding_groups[${binding_group}])`;
|
|
}
|
|
|
|
return `this.__value`;
|
|
}
|
|
|
|
// <input type='range|number' bind:value>
|
|
if (type === 'range' || type === 'number') {
|
|
return `@to_number(this.${name})`;
|
|
}
|
|
|
|
if ((name === 'buffered' || name === 'seekable' || name === 'played')) {
|
|
return `@time_ranges_to_array(this.${name})`;
|
|
}
|
|
|
|
if (name === 'text') {
|
|
return `this.textContent`;
|
|
}
|
|
|
|
if (name === 'html') {
|
|
return `this.innerHTML`;
|
|
}
|
|
|
|
// everything else
|
|
return `this.${name}`;
|
|
}
|