mirror of https://github.com/sveltejs/svelte
commit
5f84375d42
@ -1,4 +1,5 @@
|
|||||||
--require babel-register
|
--compilers ts-node/register
|
||||||
|
--require source-map-support/register
|
||||||
|
--full-trace
|
||||||
--recursive
|
--recursive
|
||||||
./**/__test__.js
|
test/test.js
|
||||||
test/*/index.js
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export const test = typeof process !== 'undefined' && process.env.TEST;
|
@ -1,349 +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';
|
|
||||||
|
|
||||||
const readOnlyMediaAttributes = new Set([
|
|
||||||
'duration',
|
|
||||||
'buffered',
|
|
||||||
'seekable',
|
|
||||||
'played'
|
|
||||||
]);
|
|
||||||
|
|
||||||
export default function visitBinding(
|
|
||||||
generator: DomGenerator,
|
|
||||||
block: Block,
|
|
||||||
state: State,
|
|
||||||
node: Node,
|
|
||||||
attribute: Node
|
|
||||||
) {
|
|
||||||
const { name } = getObject(attribute.value);
|
|
||||||
const { snippet, contexts, dependencies } = block.contextualise(
|
|
||||||
attribute.value
|
|
||||||
);
|
|
||||||
|
|
||||||
contexts.forEach(context => {
|
|
||||||
if (!~state.allUsedContexts.indexOf(context))
|
|
||||||
state.allUsedContexts.push(context);
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventNames = getBindingEventName(node, attribute);
|
|
||||||
const handler = block.getUniqueName(
|
|
||||||
`${state.parentNode}_${eventNames.join('_')}_handler`
|
|
||||||
);
|
|
||||||
const isMultipleSelect =
|
|
||||||
node.name === 'select' &&
|
|
||||||
node.attributes.find(
|
|
||||||
(attr: Node) => attr.name.toLowerCase() === 'multiple'
|
|
||||||
); // TODO use getStaticAttributeValue
|
|
||||||
const type = getStaticAttributeValue(node, 'type');
|
|
||||||
const bindingGroup = attribute.name === 'group'
|
|
||||||
? getBindingGroup(generator, attribute.value)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const isMediaElement = node.name === 'audio' || node.name === 'video';
|
|
||||||
const isReadOnly = isMediaElement && readOnlyMediaAttributes.has(attribute.name)
|
|
||||||
|
|
||||||
const value = getBindingValue(
|
|
||||||
generator,
|
|
||||||
block,
|
|
||||||
state,
|
|
||||||
node,
|
|
||||||
attribute,
|
|
||||||
isMultipleSelect,
|
|
||||||
isMediaElement,
|
|
||||||
bindingGroup,
|
|
||||||
type
|
|
||||||
);
|
|
||||||
|
|
||||||
let setter = getSetter(generator, block, name, snippet, state.parentNode, attribute, dependencies, value);
|
|
||||||
let updateElement = `${state.parentNode}.${attribute.name} = ${snippet};`;
|
|
||||||
|
|
||||||
const needsLock = !isReadOnly && node.name !== 'input' || !/radio|checkbox|range|color/.test(type); // TODO others?
|
|
||||||
const lock = `#${state.parentNode}_updating`;
|
|
||||||
let updateConditions = needsLock ? [`!${lock}`] : [];
|
|
||||||
|
|
||||||
if (needsLock) block.addVariable(lock, 'false');
|
|
||||||
|
|
||||||
// <select> special case
|
|
||||||
if (node.name === 'select') {
|
|
||||||
if (!isMultipleSelect) {
|
|
||||||
setter = `var selectedOption = ${state.parentNode}.querySelector(':checked') || ${state.parentNode}.options[0];\n${setter}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = block.getUniqueName('value');
|
|
||||||
const option = block.getUniqueName('option');
|
|
||||||
|
|
||||||
const ifStatement = isMultipleSelect
|
|
||||||
? deindent`
|
|
||||||
${option}.selected = ~${value}.indexOf(${option}.__value);`
|
|
||||||
: deindent`
|
|
||||||
if (${option}.__value === ${value}) {
|
|
||||||
${option}.selected = true;
|
|
||||||
break;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const { name } = getObject(attribute.value);
|
|
||||||
const tailSnippet = getTailSnippet(attribute.value);
|
|
||||||
|
|
||||||
updateElement = deindent`
|
|
||||||
var ${value} = ${snippet};
|
|
||||||
for (var #i = 0; #i < ${state.parentNode}.options.length; #i += 1) {
|
|
||||||
var ${option} = ${state.parentNode}.options[#i];
|
|
||||||
|
|
||||||
${ifStatement}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
generator.hasComplexBindings = true;
|
|
||||||
block.builders.hydrate.addBlock(
|
|
||||||
`if (!('${name}' in state)) #component._root._beforecreate.push(${handler});`
|
|
||||||
);
|
|
||||||
} else if (attribute.name === 'group') {
|
|
||||||
// <input type='checkbox|radio' bind:group='selected'> special case
|
|
||||||
if (type === 'radio') {
|
|
||||||
setter = deindent`
|
|
||||||
if (!${state.parentNode}.checked) return;
|
|
||||||
${setter}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const condition = type === 'checkbox'
|
|
||||||
? `~${snippet}.indexOf(${state.parentNode}.__value)`
|
|
||||||
: `${state.parentNode}.__value === ${snippet}`;
|
|
||||||
|
|
||||||
block.builders.hydrate.addLine(
|
|
||||||
`#component._bindingGroups[${bindingGroup}].push(${state.parentNode});`
|
|
||||||
);
|
|
||||||
|
|
||||||
block.builders.destroy.addBlock(
|
|
||||||
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${state.parentNode}), 1);`
|
|
||||||
);
|
|
||||||
|
|
||||||
updateElement = `${state.parentNode}.checked = ${condition};`;
|
|
||||||
} else if (isMediaElement) {
|
|
||||||
generator.hasComplexBindings = true;
|
|
||||||
block.builders.hydrate.addBlock(`#component._root._beforecreate.push(${handler});`);
|
|
||||||
|
|
||||||
if (attribute.name === 'currentTime') {
|
|
||||||
const frame = block.getUniqueName(`${state.parentNode}_animationframe`);
|
|
||||||
block.addVariable(frame);
|
|
||||||
setter = deindent`
|
|
||||||
cancelAnimationFrame(${frame});
|
|
||||||
if (!${state.parentNode}.paused) ${frame} = requestAnimationFrame(${handler});
|
|
||||||
${setter}
|
|
||||||
`;
|
|
||||||
|
|
||||||
updateConditions.push(`!isNaN(${snippet})`);
|
|
||||||
} else if (attribute.name === 'paused') {
|
|
||||||
// this is necessary to prevent the audio restarting by itself
|
|
||||||
const last = block.getUniqueName(`${state.parentNode}_paused_value`);
|
|
||||||
block.addVariable(last, 'true');
|
|
||||||
|
|
||||||
updateConditions = [`${last} !== (${last} = ${snippet})`];
|
|
||||||
updateElement = `${state.parentNode}[${last} ? "pause" : "play"]();`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
block.builders.init.addBlock(deindent`
|
|
||||||
function ${handler}() {
|
|
||||||
${needsLock && `${lock} = true;`}
|
|
||||||
${setter}
|
|
||||||
${needsLock && `${lock} = false;`}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (node.name === 'input' && type === 'range') {
|
|
||||||
// need to bind to `input` and `change`, for the benefit of IE
|
|
||||||
block.builders.hydrate.addBlock(deindent`
|
|
||||||
@addListener(${state.parentNode}, "input", ${handler});
|
|
||||||
@addListener(${state.parentNode}, "change", ${handler});
|
|
||||||
`);
|
|
||||||
|
|
||||||
block.builders.destroy.addBlock(deindent`
|
|
||||||
@removeListener(${state.parentNode}, "input", ${handler});
|
|
||||||
@removeListener(${state.parentNode}, "change", ${handler});
|
|
||||||
`);
|
|
||||||
} else {
|
|
||||||
eventNames.forEach(eventName => {
|
|
||||||
block.builders.hydrate.addLine(
|
|
||||||
`@addListener(${state.parentNode}, "${eventName}", ${handler});`
|
|
||||||
);
|
|
||||||
|
|
||||||
block.builders.destroy.addLine(
|
|
||||||
`@removeListener(${state.parentNode}, "${eventName}", ${handler});`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMediaElement) {
|
|
||||||
node.initialUpdate = updateElement;
|
|
||||||
node.initialUpdateNeedsStateObject = !block.contexts.has(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isReadOnly) { // audio/video duration is read-only, it never updates
|
|
||||||
if (updateConditions.length) {
|
|
||||||
block.builders.update.addBlock(deindent`
|
|
||||||
if (${updateConditions.join(' && ')}) {
|
|
||||||
${updateElement}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
} else {
|
|
||||||
block.builders.update.addBlock(deindent`
|
|
||||||
${updateElement}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attribute.name === 'paused') {
|
|
||||||
block.builders.create.addLine(
|
|
||||||
`@addListener(${state.parentNode}, "play", ${handler});`
|
|
||||||
);
|
|
||||||
block.builders.destroy.addLine(
|
|
||||||
`@removeListener(${state.parentNode}, "play", ${handler});`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBindingEventName(node: Node, attribute: Node) {
|
|
||||||
if (node.name === 'input') {
|
|
||||||
const typeAttribute = node.attributes.find(
|
|
||||||
(attr: Node) => attr.type === 'Attribute' && attr.name === 'type'
|
|
||||||
);
|
|
||||||
const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static
|
|
||||||
|
|
||||||
return [type === 'checkbox' || type === 'radio' ? 'change' : 'input'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.name === 'textarea') return ['input'];
|
|
||||||
if (attribute.name === 'currentTime') return ['timeupdate'];
|
|
||||||
if (attribute.name === 'duration') return ['durationchange'];
|
|
||||||
if (attribute.name === 'paused') return ['pause'];
|
|
||||||
if (attribute.name === 'buffered') return ['progress', 'loadedmetadata'];
|
|
||||||
if (attribute.name === 'seekable') return ['loadedmetadata'];
|
|
||||||
if (attribute.name === 'played') return ['timeupdate'];
|
|
||||||
|
|
||||||
return ['change'];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBindingValue(
|
|
||||||
generator: DomGenerator,
|
|
||||||
block: Block,
|
|
||||||
state: State,
|
|
||||||
node: Node,
|
|
||||||
attribute: Node,
|
|
||||||
isMultipleSelect: boolean,
|
|
||||||
isMediaElement: boolean,
|
|
||||||
bindingGroup: number,
|
|
||||||
type: string
|
|
||||||
) {
|
|
||||||
// <select multiple bind:value='selected>
|
|
||||||
if (isMultipleSelect) {
|
|
||||||
return `[].map.call(${state.parentNode}.querySelectorAll(':checked'), function(option) { return option.__value; })`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// <select bind:value='selected>
|
|
||||||
if (node.name === 'select') {
|
|
||||||
return 'selectedOption && selectedOption.__value';
|
|
||||||
}
|
|
||||||
|
|
||||||
// <input type='checkbox' bind:group='foo'>
|
|
||||||
if (attribute.name === 'group') {
|
|
||||||
if (type === 'checkbox') {
|
|
||||||
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${state.parentNode}.__value`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// <input type='range|number' bind:value>
|
|
||||||
if (type === 'range' || type === 'number') {
|
|
||||||
return `@toNumber(${state.parentNode}.${attribute.name})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMediaElement && (attribute.name === 'buffered' || attribute.name === 'seekable' || attribute.name === 'played')) {
|
|
||||||
return `@timeRangesToArray(${state.parentNode}.${attribute.name})`
|
|
||||||
}
|
|
||||||
|
|
||||||
// everything else
|
|
||||||
return `${state.parentNode}.${attribute.name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBindingGroup(generator: DomGenerator, value: Node) {
|
|
||||||
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
|
|
||||||
const keypath = parts.join('.');
|
|
||||||
|
|
||||||
// TODO handle contextual bindings — `keypath` should include unique ID of
|
|
||||||
// each block that provides context
|
|
||||||
let index = generator.bindingGroups.indexOf(keypath);
|
|
||||||
if (index === -1) {
|
|
||||||
index = generator.bindingGroups.length;
|
|
||||||
generator.bindingGroups.push(keypath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSetter(
|
|
||||||
generator: DomGenerator,
|
|
||||||
block: Block,
|
|
||||||
name: string,
|
|
||||||
snippet: string,
|
|
||||||
_this: string,
|
|
||||||
attribute: Node,
|
|
||||||
dependencies: string[],
|
|
||||||
value: string,
|
|
||||||
) {
|
|
||||||
const tail = attribute.value.type === 'MemberExpression'
|
|
||||||
? getTailSnippet(attribute.value)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
if (block.contexts.has(name)) {
|
|
||||||
const prop = dependencies[0];
|
|
||||||
const computed = isComputed(attribute.value);
|
|
||||||
|
|
||||||
return deindent`
|
|
||||||
var list = ${_this}._svelte.${block.listNames.get(name)};
|
|
||||||
var index = ${_this}._svelte.${block.indexNames.get(name)};
|
|
||||||
${computed && `var state = #component.get();`}
|
|
||||||
list[index]${tail} = ${value};
|
|
||||||
|
|
||||||
${computed
|
|
||||||
? `#component.set({${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });`
|
|
||||||
: `#component.set({${dependencies.map((prop: string) => `${prop}: #component.get('${prop}')`).join(', ')} });`}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attribute.value.type === 'MemberExpression') {
|
|
||||||
// This is a little confusing, and should probably be tidied up
|
|
||||||
// at some point. It addresses a tricky bug (#893), wherein
|
|
||||||
// Svelte tries to `set()` a computed property, which throws an
|
|
||||||
// error in dev mode. a) it's possible that we should be
|
|
||||||
// replacing computations with *their* dependencies, and b)
|
|
||||||
// we should probably populate `generator.readonly` sooner so
|
|
||||||
// that we don't have to do the `.some()` here
|
|
||||||
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop));
|
|
||||||
|
|
||||||
return deindent`
|
|
||||||
var state = #component.get();
|
|
||||||
${snippet} = ${value};
|
|
||||||
#component.set({ ${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `#component.set({ ${name}: ${value} });`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isComputed(node: Node) {
|
|
||||||
while (node.type === 'MemberExpression') {
|
|
||||||
if (node.computed) return true;
|
|
||||||
node = node.object;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
import deindent from '../../../../utils/deindent';
|
|
||||||
import flattenReference from '../../../../utils/flattenReference';
|
|
||||||
import validCalleeObjects from '../../../../utils/validCalleeObjects';
|
|
||||||
import { DomGenerator } from '../../index';
|
|
||||||
import Block from '../../Block';
|
|
||||||
import { Node } from '../../../../interfaces';
|
|
||||||
import { State } from '../../interfaces';
|
|
||||||
|
|
||||||
export default function visitEventHandler(
|
|
||||||
generator: DomGenerator,
|
|
||||||
block: Block,
|
|
||||||
state: State,
|
|
||||||
node: Node,
|
|
||||||
attribute: Node
|
|
||||||
) {
|
|
||||||
const name = attribute.name;
|
|
||||||
const isCustomEvent = generator.events.has(name);
|
|
||||||
const shouldHoist = !isCustomEvent && state.inEachBlock;
|
|
||||||
|
|
||||||
const context = shouldHoist ? null : state.parentNode;
|
|
||||||
const usedContexts: string[] = [];
|
|
||||||
|
|
||||||
if (attribute.expression) {
|
|
||||||
generator.addSourcemapLocations(attribute.expression);
|
|
||||||
|
|
||||||
const flattened = flattenReference(attribute.expression.callee);
|
|
||||||
if (!validCalleeObjects.has(flattened.name)) {
|
|
||||||
// allow event.stopPropagation(), this.select() etc
|
|
||||||
// TODO verify that it's a valid callee (i.e. built-in or declared method)
|
|
||||||
generator.code.prependRight(
|
|
||||||
attribute.expression.start,
|
|
||||||
`${block.alias('component')}.`
|
|
||||||
);
|
|
||||||
if (shouldHoist) state.usesComponent = true; // this feels a bit hacky but it works!
|
|
||||||
}
|
|
||||||
|
|
||||||
attribute.expression.arguments.forEach((arg: Node) => {
|
|
||||||
const { contexts } = block.contextualise(arg, context, true);
|
|
||||||
|
|
||||||
contexts.forEach(context => {
|
|
||||||
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
|
|
||||||
if (!~state.allUsedContexts.indexOf(context))
|
|
||||||
state.allUsedContexts.push(context);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const _this = context || 'this';
|
|
||||||
const declarations = usedContexts.map(name => {
|
|
||||||
if (name === 'state') {
|
|
||||||
if (shouldHoist) state.usesComponent = true;
|
|
||||||
return `var state = ${block.alias('component')}.get();`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listName = block.listNames.get(name);
|
|
||||||
const indexName = block.indexNames.get(name);
|
|
||||||
const contextName = block.contexts.get(name);
|
|
||||||
|
|
||||||
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// get a name for the event handler that is globally unique
|
|
||||||
// if hoisted, locally unique otherwise
|
|
||||||
const handlerName = (shouldHoist ? generator : block).getUniqueName(
|
|
||||||
`${name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
|
|
||||||
);
|
|
||||||
|
|
||||||
// create the handler body
|
|
||||||
const handlerBody = deindent`
|
|
||||||
${state.usesComponent &&
|
|
||||||
`var ${block.alias('component')} = ${_this}._svelte.component;`}
|
|
||||||
${declarations}
|
|
||||||
${attribute.expression ?
|
|
||||||
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
|
|
||||||
`${block.alias('component')}.fire("${attribute.name}", event);`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (isCustomEvent) {
|
|
||||||
block.addVariable(handlerName);
|
|
||||||
|
|
||||||
block.builders.hydrate.addBlock(deindent`
|
|
||||||
${handlerName} = %events-${name}.call(#component, ${state.parentNode}, function(event) {
|
|
||||||
${handlerBody}
|
|
||||||
});
|
|
||||||
`);
|
|
||||||
|
|
||||||
block.builders.destroy.addLine(deindent`
|
|
||||||
${handlerName}.teardown();
|
|
||||||
`);
|
|
||||||
} else {
|
|
||||||
const handler = deindent`
|
|
||||||
function ${handlerName}(event) {
|
|
||||||
${handlerBody}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (shouldHoist) {
|
|
||||||
generator.blocks.push(handler);
|
|
||||||
} else {
|
|
||||||
block.builders.init.addBlock(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
block.builders.hydrate.addLine(
|
|
||||||
`@addListener(${state.parentNode}, "${name}", ${handlerName});`
|
|
||||||
);
|
|
||||||
|
|
||||||
block.builders.destroy.addLine(
|
|
||||||
`@removeListener(${state.parentNode}, "${name}", ${handlerName});`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import deindent from '../../../../utils/deindent';
|
|
||||||
import { DomGenerator } from '../../index';
|
|
||||||
import Block from '../../Block';
|
|
||||||
import { Node } from '../../../../interfaces';
|
|
||||||
import { State } from '../../interfaces';
|
|
||||||
|
|
||||||
export default function visitRef(
|
|
||||||
generator: DomGenerator,
|
|
||||||
block: Block,
|
|
||||||
state: State,
|
|
||||||
node: Node,
|
|
||||||
attribute: Node
|
|
||||||
) {
|
|
||||||
const name = attribute.name;
|
|
||||||
|
|
||||||
block.builders.mount.addLine(
|
|
||||||
`#component.refs.${name} = ${state.parentNode};`
|
|
||||||
);
|
|
||||||
|
|
||||||
block.builders.destroy.addLine(deindent`
|
|
||||||
if (#component.refs.${name} === ${state.parentNode}) #component.refs.${name} = null;
|
|
||||||
`);
|
|
||||||
|
|
||||||
generator.usesRefs = true; // so this component.refs object is created
|
|
||||||
}
|
|
@ -0,0 +1,396 @@
|
|||||||
|
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 mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
|
||||||
|
|
||||||
|
const props = new Set();
|
||||||
|
group.bindings.forEach(binding => {
|
||||||
|
binding.handler.props.forEach(prop => {
|
||||||
|
props.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();`}
|
||||||
|
${needsLock && `${lock} = true;`}
|
||||||
|
${mutations.length > 0 && mutations}
|
||||||
|
#component.set({ ${Array.from(props).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,
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
mutation: `${list}[${index}]${tail} = ${value};`,
|
||||||
|
props: dependencies.map(prop => `${prop}: state.${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,
|
||||||
|
mutation: `${snippet} = ${value}`,
|
||||||
|
props: dependencies.map((prop: string) => `${prop}: state.${prop}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
usesContext: false,
|
||||||
|
usesState: false,
|
||||||
|
mutation: null,
|
||||||
|
props: [`${name}: ${value}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
import { Node } from '../interfaces';
|
||||||
|
|
||||||
|
export default function getMethodName(node: Node) {
|
||||||
|
if (node.type === 'Identifier') return node.name;
|
||||||
|
if (node.type === 'Literal') return String(node.value);
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
export default function stringifyProps(props: string[]) {
|
||||||
|
if (!props.length) return '{}';
|
||||||
|
|
||||||
|
const joined = props.join(', ');
|
||||||
|
if (joined.length > 40) {
|
||||||
|
// make larger data objects readable
|
||||||
|
return `{\n\t${props.join(',\n\t')}\n}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `{ ${joined} }`;
|
||||||
|
}
|
@ -0,0 +1,256 @@
|
|||||||
|
function noop() {}
|
||||||
|
|
||||||
|
function assign(target) {
|
||||||
|
var k,
|
||||||
|
source,
|
||||||
|
i = 1,
|
||||||
|
len = arguments.length;
|
||||||
|
for (; i < len; i++) {
|
||||||
|
source = arguments[i];
|
||||||
|
for (k in source) target[k] = source[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendNode(node, target) {
|
||||||
|
target.appendChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertNode(node, target, anchor) {
|
||||||
|
target.insertBefore(node, anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function detachNode(node) {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElement(name) {
|
||||||
|
return document.createElement(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createText(data) {
|
||||||
|
return document.createTextNode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function blankObject() {
|
||||||
|
return Object.create(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy(detach) {
|
||||||
|
this.destroy = noop;
|
||||||
|
this.fire('destroy');
|
||||||
|
this.set = this.get = noop;
|
||||||
|
|
||||||
|
if (detach !== false) this._fragment.u();
|
||||||
|
this._fragment.d();
|
||||||
|
this._fragment = this._state = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function differs(a, b) {
|
||||||
|
return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchObservers(component, group, changed, newState, oldState) {
|
||||||
|
for (var key in group) {
|
||||||
|
if (!changed[key]) continue;
|
||||||
|
|
||||||
|
var newValue = newState[key];
|
||||||
|
var oldValue = oldState[key];
|
||||||
|
|
||||||
|
var callbacks = group[key];
|
||||||
|
if (!callbacks) continue;
|
||||||
|
|
||||||
|
for (var i = 0; i < callbacks.length; i += 1) {
|
||||||
|
var callback = callbacks[i];
|
||||||
|
if (callback.__calling) continue;
|
||||||
|
|
||||||
|
callback.__calling = true;
|
||||||
|
callback.call(component, newValue, oldValue);
|
||||||
|
callback.__calling = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fire(eventName, data) {
|
||||||
|
var handlers =
|
||||||
|
eventName in this._handlers && this._handlers[eventName].slice();
|
||||||
|
if (!handlers) return;
|
||||||
|
|
||||||
|
for (var i = 0; i < handlers.length; i += 1) {
|
||||||
|
handlers[i].call(this, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(key) {
|
||||||
|
return key ? this._state[key] : this._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(component, options) {
|
||||||
|
component.options = options;
|
||||||
|
|
||||||
|
component._observers = { pre: blankObject(), post: blankObject() };
|
||||||
|
component._handlers = blankObject();
|
||||||
|
component._root = options._root || component;
|
||||||
|
component._bind = options._bind;
|
||||||
|
}
|
||||||
|
|
||||||
|
function observe(key, callback, options) {
|
||||||
|
var group = options && options.defer
|
||||||
|
? this._observers.post
|
||||||
|
: this._observers.pre;
|
||||||
|
|
||||||
|
(group[key] || (group[key] = [])).push(callback);
|
||||||
|
|
||||||
|
if (!options || options.init !== false) {
|
||||||
|
callback.__calling = true;
|
||||||
|
callback.call(this, this._state[key]);
|
||||||
|
callback.__calling = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cancel: function() {
|
||||||
|
var index = group[key].indexOf(callback);
|
||||||
|
if (~index) group[key].splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function on(eventName, handler) {
|
||||||
|
if (eventName === 'teardown') return this.on('destroy', handler);
|
||||||
|
|
||||||
|
var handlers = this._handlers[eventName] || (this._handlers[eventName] = []);
|
||||||
|
handlers.push(handler);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cancel: function() {
|
||||||
|
var index = handlers.indexOf(handler);
|
||||||
|
if (~index) handlers.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(newState) {
|
||||||
|
this._set(assign({}, newState));
|
||||||
|
if (this._root._lock) return;
|
||||||
|
this._root._lock = true;
|
||||||
|
callAll(this._root._beforecreate);
|
||||||
|
callAll(this._root._oncreate);
|
||||||
|
callAll(this._root._aftercreate);
|
||||||
|
this._root._lock = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _set(newState) {
|
||||||
|
var oldState = this._state,
|
||||||
|
changed = {},
|
||||||
|
dirty = false;
|
||||||
|
|
||||||
|
for (var key in newState) {
|
||||||
|
if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
|
||||||
|
}
|
||||||
|
if (!dirty) return;
|
||||||
|
|
||||||
|
this._state = assign({}, oldState, newState);
|
||||||
|
this._recompute(changed, this._state);
|
||||||
|
if (this._bind) this._bind(changed, this._state);
|
||||||
|
|
||||||
|
if (this._fragment) {
|
||||||
|
dispatchObservers(this, this._observers.pre, changed, this._state, oldState);
|
||||||
|
this._fragment.p(changed, this._state);
|
||||||
|
dispatchObservers(this, this._observers.post, changed, this._state, oldState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function callAll(fns) {
|
||||||
|
while (fns && fns.length) fns.pop()();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _mount(target, anchor) {
|
||||||
|
this._fragment.m(target, anchor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _unmount() {
|
||||||
|
this._fragment.u();
|
||||||
|
}
|
||||||
|
|
||||||
|
var proto = {
|
||||||
|
destroy: destroy,
|
||||||
|
get: get,
|
||||||
|
fire: fire,
|
||||||
|
observe: observe,
|
||||||
|
on: on,
|
||||||
|
set: set,
|
||||||
|
teardown: destroy,
|
||||||
|
_recompute: noop,
|
||||||
|
_set: _set,
|
||||||
|
_mount: _mount,
|
||||||
|
_unmount: _unmount
|
||||||
|
};
|
||||||
|
|
||||||
|
/* generated by Svelte vX.Y.Z */
|
||||||
|
function create_main_fragment(state, component) {
|
||||||
|
var window_updating = false, text, p, text_1, text_2;
|
||||||
|
|
||||||
|
function onwindowscroll(event) {
|
||||||
|
window_updating = true;
|
||||||
|
|
||||||
|
component.set({
|
||||||
|
y: this.scrollY
|
||||||
|
});
|
||||||
|
window_updating = false;
|
||||||
|
}
|
||||||
|
window.addEventListener("scroll", onwindowscroll);
|
||||||
|
|
||||||
|
component.observe("y", function(y) {
|
||||||
|
if (window_updating) return;
|
||||||
|
window.scrollTo(window.scrollX, y);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
c: function create() {
|
||||||
|
text = createText("\n\n");
|
||||||
|
p = createElement("p");
|
||||||
|
text_1 = createText("scrolled to ");
|
||||||
|
text_2 = createText(state.y);
|
||||||
|
},
|
||||||
|
|
||||||
|
m: function mount(target, anchor) {
|
||||||
|
insertNode(text, target, anchor);
|
||||||
|
insertNode(p, target, anchor);
|
||||||
|
appendNode(text_1, p);
|
||||||
|
appendNode(text_2, p);
|
||||||
|
},
|
||||||
|
|
||||||
|
p: function update(changed, state) {
|
||||||
|
if (changed.y) {
|
||||||
|
text_2.data = state.y;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
u: function unmount() {
|
||||||
|
detachNode(text);
|
||||||
|
detachNode(p);
|
||||||
|
},
|
||||||
|
|
||||||
|
d: function destroy$$1() {
|
||||||
|
window.removeEventListener("scroll", onwindowscroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function SvelteComponent(options) {
|
||||||
|
init(this, options);
|
||||||
|
this._state = assign({}, options.data);
|
||||||
|
this._state.y = window.scrollY;
|
||||||
|
|
||||||
|
this._fragment = create_main_fragment(this._state, this);
|
||||||
|
|
||||||
|
if (options.target) {
|
||||||
|
this._fragment.c();
|
||||||
|
this._fragment.m(options.target, options.anchor || null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(SvelteComponent.prototype, proto);
|
||||||
|
|
||||||
|
export default SvelteComponent;
|
@ -0,0 +1,68 @@
|
|||||||
|
/* generated by Svelte vX.Y.Z */
|
||||||
|
import { appendNode, assign, createElement, createText, detachNode, init, insertNode, proto } from "svelte/shared.js";
|
||||||
|
|
||||||
|
function create_main_fragment(state, component) {
|
||||||
|
var window_updating = false, text, p, text_1, text_2;
|
||||||
|
|
||||||
|
function onwindowscroll(event) {
|
||||||
|
window_updating = true;
|
||||||
|
|
||||||
|
component.set({
|
||||||
|
y: this.scrollY
|
||||||
|
});
|
||||||
|
window_updating = false;
|
||||||
|
};
|
||||||
|
window.addEventListener("scroll", onwindowscroll);
|
||||||
|
|
||||||
|
component.observe("y", function(y) {
|
||||||
|
if (window_updating) return;
|
||||||
|
window.scrollTo(window.scrollX, y);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
c: function create() {
|
||||||
|
text = createText("\n\n");
|
||||||
|
p = createElement("p");
|
||||||
|
text_1 = createText("scrolled to ");
|
||||||
|
text_2 = createText(state.y);
|
||||||
|
},
|
||||||
|
|
||||||
|
m: function mount(target, anchor) {
|
||||||
|
insertNode(text, target, anchor);
|
||||||
|
insertNode(p, target, anchor);
|
||||||
|
appendNode(text_1, p);
|
||||||
|
appendNode(text_2, p);
|
||||||
|
},
|
||||||
|
|
||||||
|
p: function update(changed, state) {
|
||||||
|
if (changed.y) {
|
||||||
|
text_2.data = state.y;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
u: function unmount() {
|
||||||
|
detachNode(text);
|
||||||
|
detachNode(p);
|
||||||
|
},
|
||||||
|
|
||||||
|
d: function destroy() {
|
||||||
|
window.removeEventListener("scroll", onwindowscroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function SvelteComponent(options) {
|
||||||
|
init(this, options);
|
||||||
|
this._state = assign({}, options.data);
|
||||||
|
this._state.y = window.scrollY;
|
||||||
|
|
||||||
|
this._fragment = create_main_fragment(this._state, this);
|
||||||
|
|
||||||
|
if (options.target) {
|
||||||
|
this._fragment.c();
|
||||||
|
this._fragment.m(options.target, options.anchor || null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(SvelteComponent.prototype, proto);
|
||||||
|
export default SvelteComponent;
|
@ -0,0 +1,3 @@
|
|||||||
|
<:Window bind:scrollY=y/>
|
||||||
|
|
||||||
|
<p>scrolled to {{y}}</p>
|
@ -0,0 +1,21 @@
|
|||||||
|
export default {
|
||||||
|
// This is a bit of a funny one — there's no equivalent attribute,
|
||||||
|
// so it can't be server-rendered
|
||||||
|
'skip-ssr': true,
|
||||||
|
|
||||||
|
data: {
|
||||||
|
indeterminate: true
|
||||||
|
},
|
||||||
|
|
||||||
|
html: `
|
||||||
|
<input type='checkbox'>
|
||||||
|
`,
|
||||||
|
|
||||||
|
test(assert, component, target) {
|
||||||
|
const input = target.querySelector('input');
|
||||||
|
|
||||||
|
assert.ok(input.indeterminate);
|
||||||
|
component.set({ indeterminate: false });
|
||||||
|
assert.ok(!input.indeterminate);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
<input type='checkbox' indeterminate='{{indeterminate}}'>
|
@ -0,0 +1,42 @@
|
|||||||
|
export default {
|
||||||
|
'skip-ssr': true,
|
||||||
|
|
||||||
|
data: {
|
||||||
|
indeterminate: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
html: `
|
||||||
|
<input type="checkbox">
|
||||||
|
<p>checked? false</p>
|
||||||
|
<p>indeterminate? true</p>
|
||||||
|
`,
|
||||||
|
|
||||||
|
test(assert, component, target, window) {
|
||||||
|
const input = target.querySelector('input');
|
||||||
|
assert.equal(input.checked, false);
|
||||||
|
assert.equal(input.indeterminate, true);
|
||||||
|
|
||||||
|
const event = new window.Event('change');
|
||||||
|
|
||||||
|
input.checked = true;
|
||||||
|
input.indeterminate = false;
|
||||||
|
input.dispatchEvent(event);
|
||||||
|
|
||||||
|
assert.equal(component.get('indeterminate'), false);
|
||||||
|
assert.equal(component.get('checked'), true);
|
||||||
|
assert.htmlEqual(target.innerHTML, `
|
||||||
|
<input type="checkbox">
|
||||||
|
<p>checked? true</p>
|
||||||
|
<p>indeterminate? false</p>
|
||||||
|
`);
|
||||||
|
|
||||||
|
component.set({ indeterminate: true });
|
||||||
|
assert.equal(input.indeterminate, true);
|
||||||
|
assert.equal(input.checked, true);
|
||||||
|
assert.htmlEqual(target.innerHTML, `
|
||||||
|
<input type="checkbox">
|
||||||
|
<p>checked? true</p>
|
||||||
|
<p>indeterminate? true</p>
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
<input type='checkbox' bind:checked bind:indeterminate>
|
||||||
|
<p>checked? {{checked}}</p>
|
||||||
|
<p>indeterminate? {{indeterminate}}</p>
|
@ -0,0 +1,40 @@
|
|||||||
|
export default {
|
||||||
|
data: {
|
||||||
|
values: [1, 2, 3],
|
||||||
|
foo: 2
|
||||||
|
},
|
||||||
|
|
||||||
|
html: `
|
||||||
|
<select>
|
||||||
|
<option value='1'>1</option>
|
||||||
|
<option value='2'>2</option>
|
||||||
|
<option value='3'>3</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<p>foo: 2</p>
|
||||||
|
`,
|
||||||
|
|
||||||
|
test(assert, component, target, window) {
|
||||||
|
const select = target.querySelector('select');
|
||||||
|
const options = [...target.querySelectorAll('option')];
|
||||||
|
|
||||||
|
assert.ok(options[1].selected);
|
||||||
|
assert.equal(component.get('foo'), 2);
|
||||||
|
|
||||||
|
const change = new window.Event('change');
|
||||||
|
|
||||||
|
options[2].selected = true;
|
||||||
|
select.dispatchEvent(change);
|
||||||
|
|
||||||
|
assert.equal(component.get('foo'), 3);
|
||||||
|
assert.htmlEqual( target.innerHTML, `
|
||||||
|
<select>
|
||||||
|
<option value='1'>1</option>
|
||||||
|
<option value='2'>2</option>
|
||||||
|
<option value='3'>3</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<p>foo: 3</p>
|
||||||
|
` );
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,7 @@
|
|||||||
|
<select bind:value='foo'>
|
||||||
|
{{#each values as v}}
|
||||||
|
<option>{{v}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<p>foo: {{foo}}</p>
|
@ -0,0 +1,61 @@
|
|||||||
|
export default {
|
||||||
|
html: `
|
||||||
|
<div class="todo done">
|
||||||
|
<input type="checkbox">
|
||||||
|
<input type="text">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="todo done">
|
||||||
|
<input type="checkbox">
|
||||||
|
<input type="text">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="todo ">
|
||||||
|
<input type="checkbox">
|
||||||
|
<input type="text">
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
data: {
|
||||||
|
todos: {
|
||||||
|
first: {
|
||||||
|
description: 'Buy some milk',
|
||||||
|
done: true,
|
||||||
|
},
|
||||||
|
second: {
|
||||||
|
description: 'Do the laundry',
|
||||||
|
done: true,
|
||||||
|
},
|
||||||
|
third: {
|
||||||
|
description: "Find life's true purpose",
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
test(assert, component, target, window) {
|
||||||
|
const input = document.querySelectorAll('input[type="checkbox"]')[2];
|
||||||
|
const change = new window.Event('change');
|
||||||
|
|
||||||
|
input.checked = true;
|
||||||
|
input.dispatchEvent(change);
|
||||||
|
|
||||||
|
assert.ok(component.get('todos').third.done);
|
||||||
|
assert.htmlEqual(target.innerHTML, `
|
||||||
|
<div class="todo done">
|
||||||
|
<input type="checkbox">
|
||||||
|
<input type="text">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="todo done">
|
||||||
|
<input type="checkbox">
|
||||||
|
<input type="text">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="todo done">
|
||||||
|
<input type="checkbox">
|
||||||
|
<input type="text">
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
{{#each Object.keys(todos) as key}}
|
||||||
|
<div class='todo {{todos[key].done ? "done": ""}}'>
|
||||||
|
<input type='checkbox' bind:checked='todos[key].done'>
|
||||||
|
<input type='text' bind:value='todos[key].description'>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
@ -0,0 +1,8 @@
|
|||||||
|
[{
|
||||||
|
"message": "'type' attribute must be specified",
|
||||||
|
"loc": {
|
||||||
|
"line": 1,
|
||||||
|
"column": 24
|
||||||
|
},
|
||||||
|
"pos": 24
|
||||||
|
}]
|
@ -0,0 +1 @@
|
|||||||
|
<input bind:value='foo' type>
|
@ -0,0 +1 @@
|
|||||||
|
[]
|
@ -0,0 +1,10 @@
|
|||||||
|
<button on:click='foo()'></button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
'foo': () => {},
|
||||||
|
'bar': () => {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,8 @@
|
|||||||
|
[{
|
||||||
|
"message": "Invalid namespace 'lol'",
|
||||||
|
"pos": 29,
|
||||||
|
"loc": {
|
||||||
|
"line": 3,
|
||||||
|
"column": 2
|
||||||
|
}
|
||||||
|
}]
|
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
namespace: 'lol'
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,8 @@
|
|||||||
|
[{
|
||||||
|
"message": "The 'namespace' property must be a string literal representing a valid namespace",
|
||||||
|
"pos": 79,
|
||||||
|
"loc": {
|
||||||
|
"line": 5,
|
||||||
|
"column": 2
|
||||||
|
}
|
||||||
|
}]
|
@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
const namespace = 'http://www.w3.org/1999/svg';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespace
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,8 @@
|
|||||||
|
[{
|
||||||
|
"message": "The 'components' property must be an object literal",
|
||||||
|
"loc": {
|
||||||
|
"line": 3,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"pos": 29
|
||||||
|
}]
|
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
components: 'not an object literal'
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,8 @@
|
|||||||
|
[{
|
||||||
|
"message": "The 'events' property must be an object literal",
|
||||||
|
"loc": {
|
||||||
|
"line": 3,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"pos": 29
|
||||||
|
}]
|
@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
events: 'not an object literal'
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,8 @@
|
|||||||
|
[{
|
||||||
|
"message": "The 'helpers' property must be an object literal",
|
||||||
|
"loc": {
|
||||||
|
"line": 3,
|
||||||
|
"column": 2
|
||||||
|
},
|
||||||
|
"pos": 29
|
||||||
|
}]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue