From a9ea4ce0413e58a0730cd908e1db258620f62137 Mon Sep 17 00:00:00 2001 From: pk Date: Fri, 9 Nov 2018 20:18:16 +0100 Subject: [PATCH 001/320] Failing test for #1844 --- .../Nested.html | 3 +++ .../_config.js | 2 ++ .../main.html | 13 +++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/Nested.html create mode 100644 test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/_config.js create mode 100644 test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/main.html diff --git a/test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/Nested.html b/test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/Nested.html new file mode 100644 index 0000000000..6ddd61d4d1 --- /dev/null +++ b/test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/Nested.html @@ -0,0 +1,3 @@ +

+ +

diff --git a/test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/_config.js b/test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/_config.js new file mode 100644 index 0000000000..02a61bed5e --- /dev/null +++ b/test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/_config.js @@ -0,0 +1,2 @@ +export default { +}; diff --git a/test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/main.html b/test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/main.html new file mode 100644 index 0000000000..3f5fad9363 --- /dev/null +++ b/test/runtime/samples/component-slot-binding-dimensions-destroys-cleanly/main.html @@ -0,0 +1,13 @@ + + Hello + + + From 2491b163413e554689f5f1357152e9d9a06068ac Mon Sep 17 00:00:00 2001 From: pk Date: Wed, 30 Jan 2019 18:27:11 +0100 Subject: [PATCH 002/320] Failing test for #1999 --- .../each-block-keyed-recursive/_config.js | 26 +++++++++++++++++++ .../each-block-keyed-recursive/main.html | 12 +++++++++ 2 files changed, 38 insertions(+) create mode 100644 test/runtime/samples/each-block-keyed-recursive/_config.js create mode 100644 test/runtime/samples/each-block-keyed-recursive/main.html diff --git a/test/runtime/samples/each-block-keyed-recursive/_config.js b/test/runtime/samples/each-block-keyed-recursive/_config.js new file mode 100644 index 0000000000..e124f14ccf --- /dev/null +++ b/test/runtime/samples/each-block-keyed-recursive/_config.js @@ -0,0 +1,26 @@ +export default { + props: { + titles: [{ name: 'b' }, { name: 'c' }], + tree: [ + {id: 1, sub: null}, + {id: 2, sub: [{id: 11}]} + ] + }, + + html: ` +
1
+
2\n
11
+ `, + + test({ assert, component, target }) { + component.tree = [ + {id: 1, sub: null}, + {id: 2, sub: null} + ]; + + assert.htmlEqual(target.innerHTML, ` +
1
+
2
+ `); + } +}; diff --git a/test/runtime/samples/each-block-keyed-recursive/main.html b/test/runtime/samples/each-block-keyed-recursive/main.html new file mode 100644 index 0000000000..5e90ec501c --- /dev/null +++ b/test/runtime/samples/each-block-keyed-recursive/main.html @@ -0,0 +1,12 @@ + + +{#each tree as item, i (item.id)} +
+ {item.id} + {#if item.sub} + + {/if} +
+{/each} From 6550fe8116ae249f1f074e1a5c8a403ca84c0813 Mon Sep 17 00:00:00 2001 From: pk Date: Wed, 30 Jan 2019 18:45:24 +0100 Subject: [PATCH 003/320] Oops --- test/runtime/samples/each-block-keyed-recursive/_config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/runtime/samples/each-block-keyed-recursive/_config.js b/test/runtime/samples/each-block-keyed-recursive/_config.js index e124f14ccf..7ad044228b 100644 --- a/test/runtime/samples/each-block-keyed-recursive/_config.js +++ b/test/runtime/samples/each-block-keyed-recursive/_config.js @@ -1,6 +1,5 @@ export default { props: { - titles: [{ name: 'b' }, { name: 'c' }], tree: [ {id: 1, sub: null}, {id: 2, sub: [{id: 11}]} From 256fd84fcb72681cd0d1afa1b9b0290fdc29403a Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Sun, 17 Mar 2019 16:14:41 -0400 Subject: [PATCH 004/320] allow transition functions to return nothing --- src/internal/transitions.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/internal/transitions.js b/src/internal/transitions.js index a6579b0cef..8af0d37f89 100644 --- a/src/internal/transitions.js +++ b/src/internal/transitions.js @@ -40,6 +40,8 @@ export function on_outro(callback) { outros.callbacks.push(callback); } +const null_transition = { duration: 0 }; + export function create_in_transition(node, fn, params) { let config = fn(node, params); let running = false; @@ -58,7 +60,7 @@ export function create_in_transition(node, fn, params) { easing = linear, tick = noop, css - } = config; + } = config || null_transition; if (css) animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++); tick(0, 1); @@ -132,7 +134,7 @@ export function create_out_transition(node, fn, params) { easing = linear, tick = noop, css - } = config; + } = config || null_transition; if (css) animation_name = create_rule(node, 1, 0, duration, delay, easing, css); @@ -221,7 +223,7 @@ export function create_bidirectional_transition(node, fn, params, intro) { easing = linear, tick = noop, css - } = config; + } = config || null_transition; const program = { start: window.performance.now() + delay, From cf7c7d15eda52e0c6da08e8dd7b10805e6679910 Mon Sep 17 00:00:00 2001 From: Vlad Glushchuk Date: Mon, 8 Apr 2019 20:28:16 +0200 Subject: [PATCH 005/320] Add bind:text and bind:html support for contenteditable elements Fixes #310 --- src/compile/nodes/Element.ts | 21 ++++++++- .../render-dom/wrappers/Element/Binding.ts | 16 +++++++ .../render-dom/wrappers/Element/index.ts | 6 +++ src/compile/render-ssr/handlers/Element.ts | 23 +++++++--- .../samples/contenteditable-html/_config.js | 43 +++++++++++++++++++ .../samples/contenteditable-html/main.svelte | 6 +++ .../samples/contenteditable-text/_config.js | 37 ++++++++++++++++ .../samples/contenteditable-text/main.svelte | 6 +++ .../contenteditable-dynamic/errors.json | 15 +++++++ .../contenteditable-dynamic/input.svelte | 6 +++ .../contenteditable-missing/errors.json | 15 +++++++ .../contenteditable-missing/input.svelte | 4 ++ 12 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 test/runtime/samples/contenteditable-html/_config.js create mode 100644 test/runtime/samples/contenteditable-html/main.svelte create mode 100644 test/runtime/samples/contenteditable-text/_config.js create mode 100644 test/runtime/samples/contenteditable-text/main.svelte create mode 100644 test/validator/samples/contenteditable-dynamic/errors.json create mode 100644 test/validator/samples/contenteditable-dynamic/input.svelte create mode 100644 test/validator/samples/contenteditable-missing/errors.json create mode 100644 test/validator/samples/contenteditable-missing/input.svelte diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index bc1991c57e..2838129e36 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -570,7 +570,26 @@ export default class Element extends Node { message: `'${binding.name}' is not a valid binding on void elements like <${this.name}>. Use a wrapper element instead` }); } - } else if (name !== 'this') { + } else if ( + name === 'text' || + name === 'html' + ){ + const contenteditable = this.attributes.find( + (attribute: Attribute) => attribute.name === 'contenteditable' + ); + + if (!contenteditable) { + component.error(binding, { + code: `missing-contenteditable-attribute`, + message: `'contenteditable' attribute is required for text and html two-way bindings` + }); + } else if (contenteditable && !contenteditable.is_static) { + component.error(contenteditable, { + code: `dynamic-contenteditable-attribute`, + message: `'contenteditable' attribute cannot be dynamic if element uses two-way binding` + }); + } + } else if (name !== 'this') { component.error(binding, { code: `invalid-binding`, message: `'${binding.name}' is not a valid binding` diff --git a/src/compile/render-dom/wrappers/Element/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts index 828d664001..d0e5375aef 100644 --- a/src/compile/render-dom/wrappers/Element/Binding.ts +++ b/src/compile/render-dom/wrappers/Element/Binding.ts @@ -201,6 +201,14 @@ function get_dom_updater( return `${element.var}.checked = ${condition};` } + if (binding.node.name === 'text') { + return `${element.var}.textContent = ${binding.snippet};`; + } + + if (binding.node.name === 'html') { + return `${element.var}.innerHTML = ${binding.snippet};`; + } + return `${element.var}.${binding.node.name} = ${binding.snippet};`; } @@ -313,6 +321,14 @@ function get_value_from_dom( return `@time_ranges_to_array(this.${name})` } + if (name === 'text') { + return `this.textContent`; + } + + if (name === 'html') { + return `this.innerHTML`; + } + // everything else return `this.${name}`; } diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index 80a8308b93..10a56eaace 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -29,6 +29,12 @@ const events = [ node.name === 'textarea' || node.name === 'input' && !/radio|checkbox|range/.test(node.get_static_attribute_value('type')) }, + { + event_names: ['input'], + filter: (node: Element, name: string) => + (name === 'text' || name === 'html') && + node.attributes.some(attribute => attribute.name === 'contenteditable') + }, { event_names: ['change'], filter: (node: Element, name: string) => diff --git a/src/compile/render-ssr/handlers/Element.ts b/src/compile/render-ssr/handlers/Element.ts index 0c2cdc489e..f38bc0e151 100644 --- a/src/compile/render-ssr/handlers/Element.ts +++ b/src/compile/render-ssr/handlers/Element.ts @@ -48,7 +48,12 @@ const boolean_attributes = new Set([ export default function(node, renderer, options) { let opening_tag = `<${node.name}`; - let textarea_contents; // awkward special case + let node_contents; // awkward special case + const contenteditable = ( + node.name !== 'textarea' && + node.name !== 'input' && + node.attributes.some((attribute: Node) => attribute.name === 'contenteditable') + ); const slot = node.get_static_attribute_value('slot'); if (slot && node.has_ancestor('InlineComponent')) { @@ -77,7 +82,7 @@ export default function(node, renderer, options) { args.push(snip(attribute.expression)); } else { if (attribute.name === 'value' && node.name === 'textarea') { - textarea_contents = stringify_attribute(attribute, true); + node_contents = stringify_attribute(attribute, true); } else if (attribute.is_true) { args.push(`{ ${quote_name_if_necessary(attribute.name)}: true }`); } else if ( @@ -99,7 +104,7 @@ export default function(node, renderer, options) { if (attribute.type !== 'Attribute') return; if (attribute.name === 'value' && node.name === 'textarea') { - textarea_contents = stringify_attribute(attribute, true); + node_contents = stringify_attribute(attribute, true); } else if (attribute.is_true) { opening_tag += ` ${attribute.name}`; } else if ( @@ -128,6 +133,14 @@ export default function(node, renderer, options) { if (name === 'group') { // TODO server-render group bindings + } else if (contenteditable && (node === 'text' || node === 'html')) { + const snippet = snip(expression) + if (name == 'text') { + node_contents = '${@escape(' + snippet + ')}' + } else { + // Do not escape HTML content + node_contents = '${' + snippet + '}' + } } else { const snippet = snip(expression); opening_tag += ' ${(v => v ? ("' + name + '" + (v === true ? "" : "=" + JSON.stringify(v))) : "")(' + snippet + ')}'; @@ -142,8 +155,8 @@ export default function(node, renderer, options) { renderer.append(opening_tag); - if (node.name === 'textarea' && textarea_contents !== undefined) { - renderer.append(textarea_contents); + if ((node.name === 'textarea' || contenteditable) && node_contents !== undefined) { + renderer.append(node_contents); } else { renderer.render(node.children, options); } diff --git a/test/runtime/samples/contenteditable-html/_config.js b/test/runtime/samples/contenteditable-html/_config.js new file mode 100644 index 0000000000..cd2a822655 --- /dev/null +++ b/test/runtime/samples/contenteditable-html/_config.js @@ -0,0 +1,43 @@ +export default { + props: { + name: 'world', + }, + + html: ` + world +

hello world

+ `, + + ssrHtml: ` + world +

hello world

+ `, + + async test({ assert, component, target, window }) { + const el = target.querySelector('editor'); + assert.equal(el.innerHTML, 'world'); + + el.innerHTML = 'everybody'; + + // No updates to data yet + assert.htmlEqual(target.innerHTML, ` + everybody +

hello world

+ `); + + // Handle user input + const event = new window.Event('input'); + await el.dispatchEvent(event); + assert.htmlEqual(target.innerHTML, ` + everybody +

hello everybody

+ `); + + component.name = 'goodbye'; + assert.equal(el.innerHTML, 'goodbye'); + assert.htmlEqual(target.innerHTML, ` + goodbye +

hello goodbye

+ `); + }, +}; diff --git a/test/runtime/samples/contenteditable-html/main.svelte b/test/runtime/samples/contenteditable-html/main.svelte new file mode 100644 index 0000000000..53b4e81c88 --- /dev/null +++ b/test/runtime/samples/contenteditable-html/main.svelte @@ -0,0 +1,6 @@ + + + +

hello {@html name}

\ No newline at end of file diff --git a/test/runtime/samples/contenteditable-text/_config.js b/test/runtime/samples/contenteditable-text/_config.js new file mode 100644 index 0000000000..4935a3a9a7 --- /dev/null +++ b/test/runtime/samples/contenteditable-text/_config.js @@ -0,0 +1,37 @@ +export default { + props: { + name: 'world', + }, + + html: ` + world +

hello world

+ `, + + ssrHtml: ` + world +

hello world

+ `, + + async test({ assert, component, target, window }) { + const el = target.querySelector('editor'); + assert.equal(el.textContent, 'world'); + + const event = new window.Event('input'); + + el.textContent = 'everybody'; + await el.dispatchEvent(event); + + assert.htmlEqual(target.innerHTML, ` + everybody +

hello everybody

+ `); + + component.name = 'goodbye'; + assert.equal(el.textContent, 'goodbye'); + assert.htmlEqual(target.innerHTML, ` + goodbye +

hello goodbye

+ `); + }, +}; diff --git a/test/runtime/samples/contenteditable-text/main.svelte b/test/runtime/samples/contenteditable-text/main.svelte new file mode 100644 index 0000000000..a71d9f0c5b --- /dev/null +++ b/test/runtime/samples/contenteditable-text/main.svelte @@ -0,0 +1,6 @@ + + + +

hello {name}

\ No newline at end of file diff --git a/test/validator/samples/contenteditable-dynamic/errors.json b/test/validator/samples/contenteditable-dynamic/errors.json new file mode 100644 index 0000000000..0c4c5585a6 --- /dev/null +++ b/test/validator/samples/contenteditable-dynamic/errors.json @@ -0,0 +1,15 @@ +[{ + "code": "dynamic-contenteditable-attribute", + "message": "'contenteditable' attribute cannot be dynamic if element uses two-way binding", + "start": { + "line": 6, + "column": 8, + "character": 73 + }, + "end": { + "line": 6, + "column": 32, + "character": 97 + }, + "pos": 73 +}] \ No newline at end of file diff --git a/test/validator/samples/contenteditable-dynamic/input.svelte b/test/validator/samples/contenteditable-dynamic/input.svelte new file mode 100644 index 0000000000..97d2c9228c --- /dev/null +++ b/test/validator/samples/contenteditable-dynamic/input.svelte @@ -0,0 +1,6 @@ + + diff --git a/test/validator/samples/contenteditable-missing/errors.json b/test/validator/samples/contenteditable-missing/errors.json new file mode 100644 index 0000000000..9cadb20629 --- /dev/null +++ b/test/validator/samples/contenteditable-missing/errors.json @@ -0,0 +1,15 @@ +[{ + "code": "missing-contenteditable-attribute", + "message": "'contenteditable' attribute is required for text and html two-way bindings", + "start": { + "line": 4, + "column": 8, + "character": 48 + }, + "end": { + "line": 4, + "column": 24, + "character": 64 + }, + "pos": 48 +}] \ No newline at end of file diff --git a/test/validator/samples/contenteditable-missing/input.svelte b/test/validator/samples/contenteditable-missing/input.svelte new file mode 100644 index 0000000000..47f125894a --- /dev/null +++ b/test/validator/samples/contenteditable-missing/input.svelte @@ -0,0 +1,4 @@ + + From 8deee95f141f32cd027467eb7c1d2bb8ff597007 Mon Sep 17 00:00:00 2001 From: Vlad Glushchuk Date: Mon, 8 Apr 2019 20:56:52 +0200 Subject: [PATCH 006/320] Fix a typo --- src/compile/render-ssr/handlers/Element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compile/render-ssr/handlers/Element.ts b/src/compile/render-ssr/handlers/Element.ts index f38bc0e151..cefe44ede8 100644 --- a/src/compile/render-ssr/handlers/Element.ts +++ b/src/compile/render-ssr/handlers/Element.ts @@ -133,7 +133,7 @@ export default function(node, renderer, options) { if (name === 'group') { // TODO server-render group bindings - } else if (contenteditable && (node === 'text' || node === 'html')) { + } else if (contenteditable && (name === 'text' || name === 'html')) { const snippet = snip(expression) if (name == 'text') { node_contents = '${@escape(' + snippet + ')}' From ac3bbbaa55c4908537106292d37b7f95db8abc60 Mon Sep 17 00:00:00 2001 From: Colin Casey Date: Mon, 13 May 2019 22:43:47 -0300 Subject: [PATCH 007/320] FIX: #2281 - trigger onMount callbacks in same order as child components --- src/internal/Component.js | 10 +++++----- src/internal/scheduler.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/internal/Component.js b/src/internal/Component.js index 871dbd6054..bf3398f449 100644 --- a/src/internal/Component.js +++ b/src/internal/Component.js @@ -14,9 +14,11 @@ export function mount_component(component, target, anchor) { fragment.m(target, anchor); - // onMount happens after the initial afterUpdate. Because - // afterUpdate callbacks happen in reverse order (inner first) - // we schedule onMount callbacks before afterUpdate callbacks + // afterUpdate callbacks happen in reverse order (inner first) so + // reverse their position so callbacks are properly ordered + after_render.reverse().forEach(add_render_callback); + + // onMount happens after the initial afterUpdate add_render_callback(() => { const new_on_destroy = on_mount.map(run).filter(is_function); if (on_destroy) { @@ -28,8 +30,6 @@ export function mount_component(component, target, anchor) { } component.$$.on_mount = []; }); - - after_render.forEach(add_render_callback); } function destroy(component, detaching) { diff --git a/src/internal/scheduler.js b/src/internal/scheduler.js index 749c3971dc..2ef79c6b69 100644 --- a/src/internal/scheduler.js +++ b/src/internal/scheduler.js @@ -52,7 +52,7 @@ export function flush() { // afterUpdate functions. This may cause // subsequent updates... while (render_callbacks.length) { - const callback = render_callbacks.pop(); + const callback = render_callbacks.shift(); if (!seen_callbacks.has(callback)) { callback(); From 59c4b763837f66c42c8518dfc90269c15421243d Mon Sep 17 00:00:00 2001 From: Colin Casey Date: Thu, 16 May 2019 23:25:28 -0300 Subject: [PATCH 008/320] unit test for child rendering lifecycle --- .../Item.svelte | 30 +++++++++++++++++++ .../_config.js | 24 +++++++++++++++ .../main.svelte | 11 +++++++ .../order.js | 1 + 4 files changed, 66 insertions(+) create mode 100755 test/runtime/samples/lifecycle-render-order-for-children/Item.svelte create mode 100644 test/runtime/samples/lifecycle-render-order-for-children/_config.js create mode 100644 test/runtime/samples/lifecycle-render-order-for-children/main.svelte create mode 100644 test/runtime/samples/lifecycle-render-order-for-children/order.js diff --git a/test/runtime/samples/lifecycle-render-order-for-children/Item.svelte b/test/runtime/samples/lifecycle-render-order-for-children/Item.svelte new file mode 100755 index 0000000000..405c656998 --- /dev/null +++ b/test/runtime/samples/lifecycle-render-order-for-children/Item.svelte @@ -0,0 +1,30 @@ + + +
  • + {logRender()} +
  • diff --git a/test/runtime/samples/lifecycle-render-order-for-children/_config.js b/test/runtime/samples/lifecycle-render-order-for-children/_config.js new file mode 100644 index 0000000000..02ff0d8d95 --- /dev/null +++ b/test/runtime/samples/lifecycle-render-order-for-children/_config.js @@ -0,0 +1,24 @@ +import order from './order.js'; + +export default { + skip_if_ssr: true, + + test({ assert, component, target }) { + assert.deepEqual(order, [ + '1: beforeUpdate', + '1: render', + '2: beforeUpdate', + '2: render', + '3: beforeUpdate', + '3: render', + '1: afterUpdate', + '1: onMount', + '2: afterUpdate', + '2: onMount', + '3: afterUpdate', + '3: onMount' + ]); + + order.length = 0; + } +}; diff --git a/test/runtime/samples/lifecycle-render-order-for-children/main.svelte b/test/runtime/samples/lifecycle-render-order-for-children/main.svelte new file mode 100644 index 0000000000..8320b86d79 --- /dev/null +++ b/test/runtime/samples/lifecycle-render-order-for-children/main.svelte @@ -0,0 +1,11 @@ + + +
      + {#each [1,2,3] as index} + + {/each} +
    + + diff --git a/test/runtime/samples/lifecycle-render-order-for-children/order.js b/test/runtime/samples/lifecycle-render-order-for-children/order.js new file mode 100644 index 0000000000..109fa8b38c --- /dev/null +++ b/test/runtime/samples/lifecycle-render-order-for-children/order.js @@ -0,0 +1 @@ +export default []; \ No newline at end of file From 5dc35283052d98c32f894431e06352e0adf4f765 Mon Sep 17 00:00:00 2001 From: Colin Casey Date: Thu, 16 May 2019 23:32:18 -0300 Subject: [PATCH 009/320] include parent component in test scenario --- .../Item.svelte | 1 - .../_config.js | 6 ++++- .../main.svelte | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/test/runtime/samples/lifecycle-render-order-for-children/Item.svelte b/test/runtime/samples/lifecycle-render-order-for-children/Item.svelte index 405c656998..91411e1e73 100755 --- a/test/runtime/samples/lifecycle-render-order-for-children/Item.svelte +++ b/test/runtime/samples/lifecycle-render-order-for-children/Item.svelte @@ -1,6 +1,5 @@ +{logRender()}