diff --git a/src/compile/render-dom/wrappers/Element/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts index fb5ff10ecd..28c60d53d3 100644 --- a/src/compile/render-dom/wrappers/Element/Binding.ts +++ b/src/compile/render-dom/wrappers/Element/Binding.ts @@ -28,6 +28,7 @@ export default class BindingWrapper { }; snippet: string; initialUpdate: string; + isReadOnly: boolean; needsLock: boolean; constructor(block: Block, node: Binding, parent: ElementWrapper) { @@ -66,14 +67,17 @@ export default class BindingWrapper { this.snippet = this.node.expression.render(); - const isReadOnly = ( + const type = parent.node.getStaticAttributeValue('type'); + + this.isReadOnly = ( + dimensions.test(this.node.name) || (parent.node.isMediaNode() && readOnlyMediaAttributes.has(this.node.name)) || - dimensions.test(this.node.name) + (parent.node.name === 'input' && type === 'file') // TODO others? ); - this.needsLock = !isReadOnly && ( - parent.node.name !== 'input' || - !/radio|checkbox|range|color/.test(parent.node.getStaticAttributeValue('type')) + this.needsLock = !this.isReadOnly && ( + // TODO others? + parent.node.name !== 'input' ); } @@ -97,45 +101,47 @@ export default class BindingWrapper { } render(block: Block, lock: string) { - // bind:offsetWidth and bind:offsetHeight — readonly - if (dimensions.test(this.node.name)) return; + if (this.isReadOnly) return; const { parent } = this; - const { renderer } = parent; let updateConditions: string[] = this.needsLock ? [`!${lock}`] : []; // model to view let updateDom = getDomUpdater(parent, this); - let initialUpdate = updateDom; // special cases - if (this.node.name === 'group') { - const bindingGroup = getBindingGroup(renderer, this.node.expression.node); - - block.builders.hydrate.addLine( - `(#component.$$.binding_groups[${bindingGroup}] || (#component.$$.binding_groups[${bindingGroup}] = [])).push(${parent.var});` - ); - - block.builders.destroy.addLine( - `#component.$$.binding_groups[${bindingGroup}].splice(#component.$$.binding_groups[${bindingGroup}].indexOf(${parent.var}), 1);` - ); - } - - if (this.node.name === 'currentTime' || this.node.name === 'volume') { - updateConditions.push(`!isNaN(${this.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(`${parent.var}_is_paused`); - block.addVariable(last, 'true'); - - updateConditions.push(`${last} !== (${last} = ${this.snippet})`); - updateDom = `${parent.var}[${last} ? "pause" : "play"]();`; - initialUpdate = null; + switch (this.node.name) { + case 'group': + const bindingGroup = getBindingGroup(parent.renderer, this.node.expression.node); + + block.builders.hydrate.addLine( + `(#component.$$.binding_groups[${bindingGroup}] || (#component.$$.binding_groups[${bindingGroup}] = [])).push(${parent.var});` + ); + + block.builders.destroy.addLine( + `#component.$$.binding_groups[${bindingGroup}].splice(#component.$$.binding_groups[${bindingGroup}].indexOf(${parent.var}), 1);` + ); + break; + + case 'currentTime': + case 'volume': + updateConditions.push(`!isNaN(${this.snippet})`); + break; + + case 'paused': + // this is necessary to prevent audio restarting by itself + const last = block.getUniqueName(`${parent.var}_is_paused`); + block.addVariable(last, 'true'); + + updateConditions.push(`${last} !== (${last} = ${this.snippet})`); + updateDom = `${parent.var}[${last} ? "pause" : "play"]();`; + break; + + case 'value': + if (parent.getStaticAttributeValue('type') === 'file') { + updateDom = null; + } } const dependencyArray = [...this.node.expression.dynamic_dependencies] @@ -154,8 +160,8 @@ export default class BindingWrapper { ); } - if (initialUpdate) { - block.builders.mount.addBlock(initialUpdate); + if (!/(currentTime|paused)/.test(this.node.name)) { + block.builders.mount.addBlock(updateDom); } } } diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index 9381e7cf1f..35b73bc4f8 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -370,13 +370,7 @@ export default class ElementWrapper extends Wrapper { renderer.component.has_reactive_assignments = true; - const needsLock = this.node.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type')); - - // TODO munge in constructor - // const mungedBindings = this.bindings.map(binding => binding.munge(block)); - const mungedBindings = this.bindings; - - const lock = mungedBindings.some(binding => binding.needsLock) ? + const lock = this.bindings.some(binding => binding.needsLock) ? block.getUniqueName(`${this.var}_updating`) : null; @@ -385,7 +379,7 @@ export default class ElementWrapper extends Wrapper { const groups = events .map(event => ({ events: event.eventNames, - bindings: mungedBindings + bindings: this.bindings .filter(binding => binding.node.name !== 'this') .filter(binding => event.filter(this.node, binding.node.name)) })) @@ -411,8 +405,6 @@ export default class ElementWrapper extends Wrapper { binding.render(block, lock); }); - const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n'); - // media bindings — awkward special case. The native timeupdate events // fire too infrequently, so we need to take matters into our // own hands @@ -422,47 +414,35 @@ export default class ElementWrapper extends Wrapper { block.addVariable(animation_frame); } + const has_local_function = contextual_dependencies.size > 0 || needsLock || animation_frame; + let callee; // TODO dry this out — similar code for event handlers and component bindings - if (contextual_dependencies.size > 0) { - const deps = Array.from(contextual_dependencies); - + if (has_local_function) { + // need to create a block-local function that calls an instance-level function block.builders.init.addBlock(deindent` function ${handler}() { - ctx.${handler}.call(this, ctx); - } - `); - - this.renderer.component.partly_hoisted.push(deindent` - function ${handler}({ ${deps.join(', ')} }) { - ${ - animation_frame && deindent` - cancelAnimationFrame(${animation_frame}); - if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});` - } - ${mutations.length > 0 && mutations} - ${Array.from(dependencies).map(dep => `$$invalidate('${dep}', ${dep});`)} + ${animation_frame && deindent` + cancelAnimationFrame(${animation_frame}); + if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`} + ${needsLock && `${lock} = true;`} + ctx.${handler}.call(${this.var}${contextual_dependencies.size > 0 ? ', ctx' : ''}); } `); callee = handler; } else { - this.renderer.component.partly_hoisted.push(deindent` - function ${handler}() { - ${ - animation_frame && deindent` - cancelAnimationFrame(${animation_frame}); - if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});` - } - ${mutations.length > 0 && mutations} - ${Array.from(dependencies).map(dep => `$$invalidate('${dep}', ${dep});`)} - } - `); - callee = `ctx.${handler}`; } + this.renderer.component.partly_hoisted.push(deindent` + function ${handler}(${contextual_dependencies.size > 0 ? `{ ${[...contextual_dependencies].join(', ')} }` : ``}) { + ${group.bindings.map(b => b.handler.mutation)} + ${Array.from(dependencies).map(dep => `$$invalidate('${dep}', ${dep});`)} + } + `); + group.events.forEach(name => { if (name === 'resize') { // special case @@ -488,8 +468,9 @@ export default class ElementWrapper extends Wrapper { .join(' || '); if (this.node.name === 'select' || group.bindings.find(binding => binding.node.name === 'indeterminate' || binding.isReadOnlyMediaAttribute())) { + const callback = has_local_function ? handler : `() => ${callee}.call(${this.var})`; block.builders.hydrate.addLine( - `if (${someInitialStateIsUndefined}) @add_render_callback(() => ${callee}.call(${this.var}));` + `if (${someInitialStateIsUndefined}) @add_render_callback(${callback});` ); } @@ -500,6 +481,10 @@ export default class ElementWrapper extends Wrapper { } }); + if (lock) { + block.builders.update.addLine(`${lock} = false;`); + } + const this_binding = this.bindings.find(b => b.node.name === 'this'); if (this_binding) { const name = renderer.component.getUniqueName(`${this.var}_binding`); diff --git a/test/js/samples/input-files/expected.js b/test/js/samples/input-files/expected.js index d4dc4c5b83..8ca98b3b5b 100644 --- a/test/js/samples/input-files/expected.js +++ b/test/js/samples/input-files/expected.js @@ -1,8 +1,8 @@ /* generated by Svelte vX.Y.Z */ -import { SvelteComponent as SvelteComponent_1, addListener, createElement, detachNode, flush, init, insert, run, safe_not_equal, setAttribute } from "svelte/internal"; +import { SvelteComponent as SvelteComponent_1, addListener, createElement, detachNode, flush, init, insert, noop, run, safe_not_equal, setAttribute } from "svelte/internal"; function create_fragment(component, ctx) { - var input, input_updating = false, current, dispose; + var input, current, dispose; return { c() { @@ -14,15 +14,10 @@ function create_fragment(component, ctx) { m(target, anchor) { insert(target, input, anchor); - - input.files = ctx.files; - current = true; }, - p(changed, ctx) { - if (!input_updating && changed.files) input.files = ctx.files; - }, + p: noop, i(target, anchor) { if (current) return; diff --git a/test/js/samples/media-bindings/expected.js b/test/js/samples/media-bindings/expected.js index 0e8c00594f..839982dfeb 100644 --- a/test/js/samples/media-bindings/expected.js +++ b/test/js/samples/media-bindings/expected.js @@ -2,55 +2,40 @@ import { SvelteComponent as SvelteComponent_1, addListener, add_render_callback, createElement, detachNode, flush, init, insert, run, run_all, safe_not_equal, timeRangesToArray } from "svelte/internal"; function create_fragment(component, ctx) { - var audio, audio_is_paused = true, audio_updating = false, audio_animationframe, current, dispose; + var audio, audio_updating = false, audio_animationframe, audio_is_paused = true, current, dispose; function audio_timeupdate_handler() { cancelAnimationFrame(audio_animationframe); if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler); audio_updating = true; - ctx.audio_timeupdate_handler(audio); - } - - function audio_durationchange_handler() { - audio_updating = true; - ctx.audio_durationchange_handler(audio); + ctx.audio_timeupdate_handler.call(audio); } function audio_play_pause_handler() { audio_updating = true; - ctx.audio_durationchange_handler(audio); - } - - function audio_progress_handler() { - audio_updating = true; - ctx.audio_progress_handler(audio); - } - - function audio_loadedmetadata_handler() { - audio_updating = true; - ctx.audio_loadedmetadata_handler(audio); + ctx.audio_play_pause_handler.call(audio); } function audio_volumechange_handler() { audio_updating = true; - ctx.audio_volumechange_handler(audio); + ctx.audio_volumechange_handler.call(audio); } return { c() { audio = createElement("audio"); if (ctx.played === void 0 || ctx.currentTime === void 0) add_render_callback(audio_timeupdate_handler); - if (ctx.duration === void 0) add_render_callback(audio_durationchange_handler); - if (ctx.buffered === void 0) add_render_callback(audio_progress_handler); - if (ctx.buffered === void 0 || ctx.seekable === void 0) add_render_callback(audio_loadedmetadata_handler); + if (ctx.duration === void 0) add_render_callback(() => ctx.audio_durationchange_handler.call(audio)); + if (ctx.buffered === void 0) add_render_callback(() => ctx.audio_progress_handler.call(audio)); + if (ctx.buffered === void 0 || ctx.seekable === void 0) add_render_callback(() => ctx.audio_loadedmetadata_handler.call(audio)); dispose = [ addListener(audio, "timeupdate", audio_timeupdate_handler), - addListener(audio, "durationchange", audio_durationchange_handler), + addListener(audio, "durationchange", ctx.audio_durationchange_handler), addListener(audio, "play", audio_play_pause_handler), addListener(audio, "pause", audio_play_pause_handler), - addListener(audio, "progress", audio_progress_handler), - addListener(audio, "loadedmetadata", audio_loadedmetadata_handler), + addListener(audio, "progress", ctx.audio_progress_handler), + addListener(audio, "loadedmetadata", ctx.audio_loadedmetadata_handler), addListener(audio, "volumechange", audio_volumechange_handler) ]; }, @@ -67,7 +52,6 @@ function create_fragment(component, ctx) { if (!audio_updating && !isNaN(ctx.currentTime) && changed.currentTime) audio.currentTime = ctx.currentTime; if (!audio_updating && audio_is_paused !== (audio_is_paused = ctx.paused) && changed.paused) audio[audio_is_paused ? "pause" : "play"](); if (!audio_updating && !isNaN(ctx.volume) && changed.volume) audio.volume = ctx.volume; - audio_updating = false; }, @@ -91,30 +75,38 @@ function create_fragment(component, ctx) { function instance($$self, $$props, $$invalidate) { let { buffered, seekable, played, currentTime, duration, paused, volume } = $$props; - function audio_timeupdate_handler(audio) { - $$invalidate('played', played = timeRangesToArray(audio.played)); - $$invalidate('currentTime', currentTime = audio.currentTime); + function audio_timeupdate_handler() { + played = timeRangesToArray(this.played); + currentTime = this.currentTime; + $$invalidate('played', played); + $$invalidate('currentTime', currentTime); } - function audio_durationchange_handler(audio) { - $$invalidate('duration', duration = audio.duration); + function audio_durationchange_handler() { + duration = this.duration; + $$invalidate('duration', duration); } - function audio_play_pause_handler(audio) { - $$invalidate('paused', paused = audio.paused); + function audio_play_pause_handler() { + paused = this.paused; + $$invalidate('paused', paused); } - function audio_progress_handler(audio) { - $$invalidate('buffered', buffered = timeRangesToArray(audio.buffered)); + function audio_progress_handler() { + buffered = timeRangesToArray(this.buffered); + $$invalidate('buffered', buffered); } - function audio_loadedmetadata_handler(audio) { - $$invalidate('buffered', buffered = timeRangesToArray(audio.buffered)); - $$invalidate('seekable', seekable = timeRangesToArray(audio.seekable)); + function audio_loadedmetadata_handler() { + buffered = timeRangesToArray(this.buffered); + seekable = timeRangesToArray(this.seekable); + $$invalidate('buffered', buffered); + $$invalidate('seekable', seekable); } - function audio_volumechange_handler(audio) { - $$invalidate('volume', volume = audio.volume); + function audio_volumechange_handler() { + volume = this.volume; + $$invalidate('volume', volume); } $$self.$set = $$props => {