diff --git a/src/compile/render-dom/wrappers/Element/UNUSED-Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts similarity index 100% rename from src/compile/render-dom/wrappers/Element/UNUSED-Binding.ts rename to src/compile/render-dom/wrappers/Element/Binding.ts diff --git a/src/compile/render-dom/wrappers/Element/Binding/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding/Binding.ts deleted file mode 100644 index 99fb369113..0000000000 --- a/src/compile/render-dom/wrappers/Element/Binding/Binding.ts +++ /dev/null @@ -1,365 +0,0 @@ -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 Element from '../../../../nodes/Element'; -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[]; - dependencies: Set; - - usesStore: boolean; - needsLock: boolean; - lock: string; - mutations: string[]; - props: Set; - storeProps: Set; - handlerName: string; - - object: string; - // handler: Handler; - updateDom: string; - initialUpdate: string; - updateCondition: string; - isReadOnly: boolean; - - constructor(block: Block, element: ElementWrapper, binding: Binding) { - this.element = element; - this.binding = binding; - - element.cannotUseInnerHTML(); - - this.isReadOnly = false; - this.needsLock = false; - this.events = []; - - this.dependencies = new Set(this.binding.value.dependencies); - - // special case: if you have e.g. `` - // and `selected` is an object chosen with a - 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 readOnlyMediaAttributes.has(this.binding.name); - } - - 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; - - - - // view to model - const valueFromDom = this.fromDom(); - const handler = getEventHandler(this.binding, this.element.renderer, block, name, snippet, this.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; - - let animation_frame = null; // TODO media binding only - - this.handlerName = block.getUniqueName(`${this.element.var}_${this.events.join('_')}_handler`); - - block.builders.init.addBlock(deindent` - function ${this.handlerName}() { - ${ - 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 - ); - - this.events.forEach(name => { - block.builders.hydrate.addLine( - `@addListener(${this.element.var}, "${name}", ${this.handlerName});` - ); - - block.builders.destroy.addLine( - `@removeListener(${this.element.var}, "${name}", ${this.handlerName});` - ); - }); - } -} - -function getEventHandler( - binding: Binding, - renderer: Renderer, - block: Block, - name: string, - snippet: string, - dependencies: Set, - 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 -// ) { -// // -// 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};`; -} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/Binding/InputCheckboxBinding.ts b/src/compile/render-dom/wrappers/Element/Binding/InputCheckboxBinding.ts deleted file mode 100644 index 4267068dab..0000000000 --- a/src/compile/render-dom/wrappers/Element/Binding/InputCheckboxBinding.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Binding from '../../../../nodes/Binding'; -import Element from '../../../../nodes/Element'; -import ElementWrapper from '..'; -import BindingWrapper from './Binding'; -import Block from '../../../Block'; - -export default class InputCheckboxBinding extends BindingWrapper { - events = ['change']; - - static filter( - node: Element, - binding_lookup: Record, - type: string - ) { - return ( - node.name === 'input' && - binding_lookup.checked && - type === 'checkbox' - ); - } - - constructor( - block: Block, - element: ElementWrapper, - binding_lookup: Record - ) { - super(block, element, binding_lookup.checked); - } - - fromDom() { - return `${this.element.var}.checked`; - } - - toDom() { - return `${this.element.var}.checked = ${this.binding.value.snippet};`; - } -} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/Binding/InputCheckboxGroupBinding.ts b/src/compile/render-dom/wrappers/Element/Binding/InputCheckboxGroupBinding.ts deleted file mode 100644 index a17587791b..0000000000 --- a/src/compile/render-dom/wrappers/Element/Binding/InputCheckboxGroupBinding.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Binding from '../../../../nodes/Binding'; -import Element from '../../../../nodes/Element'; -import ElementWrapper from '..'; -import InputRadioGroupBinding from './InputRadioGroupBinding'; - -export default class InputCheckboxGroupBinding extends InputRadioGroupBinding { - element: ElementWrapper; - node: Binding; - - static filter( - node: Element, - binding_lookup: Record, - type: string - ) { - if (node.name === 'input' && binding_lookup.group) { - return type === 'checkbox'; - } - } - - fromDom() { - return `@getBindingGroupValue(#component._bindingGroups[${this.bindingGroup}])`; - } - - toDom() { - const condition = `~${this.binding.value.snippet}.indexOf(${this.element.var}.__value)`; - - return `${this.element.var}.checked = ${condition};`; - } -} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/Binding/InputNumberBinding.ts b/src/compile/render-dom/wrappers/Element/Binding/InputNumberBinding.ts deleted file mode 100644 index 169fd69ceb..0000000000 --- a/src/compile/render-dom/wrappers/Element/Binding/InputNumberBinding.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Binding from '../../../../nodes/Binding'; -import Element from '../../../../nodes/Element'; -import ElementWrapper from '..'; -import BindingWrapper from './Binding'; -import Block from '../../../Block'; - -export default class InputNumberBinding extends BindingWrapper { - events = ['input']; - - static filter( - node: Element, - binding_lookup: Record, - type: string - ) { - return ( - node.name === 'input' && - type === 'number' && - binding_lookup.value - ); - } - - constructor( - block: Block, - element: ElementWrapper, - binding_lookup: Record - ) { - super(block, element, binding_lookup.value); - this.needsLock = true; - } - - fromDom() { - return `@toNumber(${this.element.var}.value)`; - } - - toDom() { - return `${this.element.var}.value = ${this.binding.value.snippet};`; - } -} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/Binding/InputRadioGroupBinding.ts b/src/compile/render-dom/wrappers/Element/Binding/InputRadioGroupBinding.ts deleted file mode 100644 index 1036f05d16..0000000000 --- a/src/compile/render-dom/wrappers/Element/Binding/InputRadioGroupBinding.ts +++ /dev/null @@ -1,70 +0,0 @@ -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; - bindingGroup: number; - - events = ['change']; - - static filter( - node: Element, - binding_lookup: Record, - type: string - ) { - if (node.name === 'input' && binding_lookup.group) { - return type === 'radio'; - } - } - - constructor( - block: Block, - element: ElementWrapper, - binding_lookup: Record - ) { - super(block, element, binding_lookup.group); - - // TODO handle cases involving computed member expressions - const { parts } = flattenReference(this.binding.value.node); - const keypath = parts.join('.'); - - // TODO handle contextual bindings — `keypath` should include unique ID of - // each block that provides context - let index = element.renderer.bindingGroups.indexOf(keypath); - if (index === -1) { - index = element.renderer.bindingGroups.length; - element.renderer.bindingGroups.push(keypath); - } - - this.bindingGroup = index; - } - - fromDom() { - return `${this.element.var}.__value`; - } - - toDom() { - const condition = `${this.element.var}.__value === ${this.binding.value.snippet}`; - - return `${this.element.var}.checked = ${condition};` - } - - render(block: Block) { - block.builders.hydrate.addLine( - `#component._bindingGroups[${this.bindingGroup}].push(${this.element.var});` - ); - - block.builders.destroy.addLine( - `#component._bindingGroups[${this.bindingGroup}].splice(#component._bindingGroups[${this.bindingGroup}].indexOf(${this.element.var}), 1);` - ); - - super.render(block); - } -} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/Binding/InputRangeBinding.ts b/src/compile/render-dom/wrappers/Element/Binding/InputRangeBinding.ts deleted file mode 100644 index 473776fece..0000000000 --- a/src/compile/render-dom/wrappers/Element/Binding/InputRangeBinding.ts +++ /dev/null @@ -1,30 +0,0 @@ -import InputNumberBinding from './InputNumberBinding'; -import Binding from '../../../../nodes/Binding'; -import Element from '../../../../nodes/Element'; -import Block from '../../../Block'; -import ElementWrapper from '..'; - -export default class InputRangeBinding extends InputNumberBinding { - events = ['change', 'input']; - - static filter( - node: Element, - binding_lookup: Record, - type: string - ) { - return ( - node.name === 'input' && - type === 'range' && - binding_lookup.value - ); - } - - constructor( - block: Block, - element: ElementWrapper, - binding_lookup: Record - ) { - super(block, element, binding_lookup); - this.needsLock = false; - } -} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/Binding/InputTextBinding.ts b/src/compile/render-dom/wrappers/Element/Binding/InputTextBinding.ts deleted file mode 100644 index 1e4eae3579..0000000000 --- a/src/compile/render-dom/wrappers/Element/Binding/InputTextBinding.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Binding from '../../../../nodes/Binding'; -import Element from '../../../../nodes/Element'; -import ElementWrapper from '..'; -import BindingWrapper from './Binding'; -import Block from '../../../Block'; - -export default class InputTextBinding extends BindingWrapper { - events = ['input']; - - static filter( - node: Element, - binding_lookup: Record, - 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( - block: Block, - element: ElementWrapper, - binding_lookup: Record - ) { - super(block, element, binding_lookup.value); - this.needsLock = true; - } - - fromDom() { - return `${this.element.var}.value`; - } - - toDom() { - return `${this.element.var}.value = ${this.binding.value.snippet};`; - } -} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/Binding/SelectBinding.ts b/src/compile/render-dom/wrappers/Element/Binding/SelectBinding.ts deleted file mode 100644 index 38dea0caab..0000000000 --- a/src/compile/render-dom/wrappers/Element/Binding/SelectBinding.ts +++ /dev/null @@ -1,59 +0,0 @@ -import Binding from '../../../../nodes/Binding'; -import Element from '../../../../nodes/Element'; -import ElementWrapper from '..'; -import BindingWrapper from './Binding'; -import Block from '../../../Block'; - -export default class SelectBinding extends BindingWrapper { - element: ElementWrapper; - node: Binding; - - static filter( - node: Element, - binding_lookup: Record, - type: string - ) { - return node.name === 'select' && binding_lookup.value; - } - - constructor( - block: Block, - element: ElementWrapper, - binding_lookup: Record - ) { - super(block, element, binding_lookup.value); - this.events = ['change']; - this.needsLock = true; - - element.renderer.hasComplexBindings = true; - - // TODO does this also apply to e.g. ``? - const dependencies = this.binding.value.dependencies; - this.element.selectBindingDependencies = dependencies; - dependencies.forEach((prop: string) => { - element.renderer.component.indirectDependencies.set(prop, new Set()); - }); - } - - fromDom() { - return this.element.node.getStaticAttributeValue('multiple') === true ? - `@selectMultipleValue(${this.element.var})` : - `@selectValue(${this.element.var})`; - } - - toDom() { - return this.element.getStaticAttributeValue('multiple') === true ? - `@selectOptions(${this.element.var}, ${this.binding.value.snippet});` : - `@selectOption(${this.element.var}, ${this.binding.value.snippet});`; - } - - render(block: Block) { - super.render(block); - - const allInitialStateIsDefined = `'${this.object}' in ctx`; - - block.builders.hydrate.addLine( - `if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${this.handlerName});` - ); - } -} \ No newline at end of file diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index 86c8f33b07..160f7572f9 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -14,26 +14,8 @@ import namespaces from '../../../../utils/namespaces'; import AttributeWrapper from './Attribute'; import StyleAttributeWrapper from './StyleAttribute'; import { dimensions } from '../../../../utils/patterns'; -import Binding from './UNUSED-Binding'; -// import InputCheckboxBinding from './Binding/InputCheckboxBinding'; -// import InputCheckboxGroupBinding from './Binding/InputCheckboxGroupBinding'; -// import InputNumberBinding from './Binding/InputNumberBinding'; -// import InputRangeBinding from './Binding/InputRangeBinding'; -// import InputTextBinding from './Binding/InputTextBinding'; -// import InputRadioGroupBinding from './Binding/InputRadioGroupBinding'; -// import SelectBinding from './Binding/SelectBinding'; +import Binding from './Binding'; import InlineComponentWrapper from '../InlineComponent'; -import addToSet from '../../../../utils/addToSet'; - -// const bindings = [ -// InputTextBinding, -// InputRadioGroupBinding, -// InputCheckboxBinding, -// InputCheckboxGroupBinding, -// InputNumberBinding, -// InputRangeBinding, -// SelectBinding -// ]; const events = [ {