diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 1f80cee89c..b57d24ae9c 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -174,6 +174,8 @@ export default class ElementWrapper extends Wrapper { child_dynamic_element_block?: Block = null; child_dynamic_element?: ElementWrapper = null; + element_data_name = null; + constructor( renderer: Renderer, block: Block, @@ -287,6 +289,8 @@ export default class ElementWrapper extends Wrapper { } this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling); + + this.element_data_name = block.get_unique_name(`${this.var.name}_data`); } render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { @@ -516,7 +520,8 @@ export default class ElementWrapper extends Wrapper { child.render( block, is_template ? x`${node}.content` : node, - nodes + nodes, + { element_data_name: this.element_data_name } ); }); } @@ -824,7 +829,6 @@ export default class ElementWrapper extends Wrapper { add_spread_attributes(block: Block) { const levels = block.get_unique_name(`${this.var.name}_levels`); - const data = block.get_unique_name(`${this.var.name}_data`); const initial_props = []; const updates = []; @@ -855,9 +859,9 @@ export default class ElementWrapper extends Wrapper { block.chunks.init.push(b` let ${levels} = [${initial_props}]; - let ${data} = {}; + let ${this.element_data_name} = {}; for (let #i = 0; #i < ${levels}.length; #i += 1) { - ${data} = @assign(${data}, ${levels}[#i]); + ${this.element_data_name} = @assign(${this.element_data_name}, ${levels}[#i]); } `); @@ -868,13 +872,13 @@ export default class ElementWrapper extends Wrapper { ? x`@set_dynamic_element_data(${this.node.tag_expr.manipulate(block)})` : x`@set_attributes`; - block.chunks.hydrate.push( - b`${fn}(${this.var}, ${data});` + block.chunks.hydrate.unshift( + b`${fn}(${this.var}, ${this.element_data_name});` ); if (this.has_dynamic_attribute) { - block.chunks.update.push(b` - ${fn}(${this.var}, ${data} = @get_spread_update(${levels}, [ + block.chunks.update.unshift(b` + ${fn}(${this.var}, ${this.element_data_name} = @get_spread_update(${levels}, [ ${updates} ])); `); @@ -890,23 +894,23 @@ export default class ElementWrapper extends Wrapper { } block.chunks.mount.push(b` - 'value' in ${data} && (${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value); + 'value' in ${this.element_data_name} && (${this.element_data_name}.multiple ? @select_options : @select_option)(${this.var}, ${this.element_data_name}.value); `); block.chunks.update.push(b` - if (${block.renderer.dirty(Array.from(dependencies))} && 'value' in ${data}) (${data}.multiple ? @select_options : @select_option)(${this.var}, ${data}.value); + if (${block.renderer.dirty(Array.from(dependencies))} && 'value' in ${this.element_data_name}) (${this.element_data_name}.multiple ? @select_options : @select_option)(${this.var}, ${this.element_data_name}.value); `); } else if (this.node.name === 'input' && this.attributes.find(attr => attr.node.name === 'value')) { const type = this.node.get_static_attribute_value('type'); if (type === null || type === '' || type === 'text' || type === 'email' || type === 'password') { block.chunks.mount.push(b` - if ('value' in ${data}) { - ${this.var}.value = ${data}.value; + if ('value' in ${this.element_data_name}) { + ${this.var}.value = ${this.element_data_name}.value; } `); block.chunks.update.push(b` - if ('value' in ${data}) { - ${this.var}.value = ${data}.value; + if ('value' in ${this.element_data_name}) { + ${this.var}.value = ${this.element_data_name}.value; } `); } @@ -1220,8 +1224,8 @@ export default class ElementWrapper extends Wrapper { if (this.dynamic_style_dependencies.size > 0) { maybe_create_style_changed_var(); // If all dependencies are same as the style attribute dependencies, then we can skip the dirty check - condition = - all_deps.size === this.dynamic_style_dependencies.size + condition = + all_deps.size === this.dynamic_style_dependencies.size ? style_changed_var : x`${style_changed_var} || ${condition}`; } @@ -1232,7 +1236,6 @@ export default class ElementWrapper extends Wrapper { } `); } - }); } diff --git a/src/compiler/compile/render_dom/wrappers/MustacheTag.ts b/src/compiler/compile/render_dom/wrappers/MustacheTag.ts index ea6634ec80..cbea2e7bca 100644 --- a/src/compiler/compile/render_dom/wrappers/MustacheTag.ts +++ b/src/compiler/compile/render_dom/wrappers/MustacheTag.ts @@ -5,7 +5,9 @@ import Wrapper from './shared/Wrapper'; import MustacheTag from '../../nodes/MustacheTag'; import RawMustacheTag from '../../nodes/RawMustacheTag'; import { x } from 'code-red'; -import { Identifier } from 'estree'; +import { Identifier, Expression } from 'estree'; +import ElementWrapper from './Element'; +import AttributeWrapper from './Element/Attribute'; export default class MustacheTagWrapper extends Tag { var: Identifier = { type: 'Identifier', name: 't' }; @@ -14,10 +16,32 @@ export default class MustacheTagWrapper extends Tag { super(renderer, block, parent, node); } - render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { + render(block: Block, parent_node: Identifier, parent_nodes: Identifier, data: Record | undefined) { + const contenteditable = + this.parent instanceof ElementWrapper && + this.parent.attributes.filter((a) => a.node.name === 'contenteditable'); + + const spread_attres = + this.parent instanceof ElementWrapper && + this.parent.attributes.filter((a) => a.node.is_spread); + + let is_contenteditable: Expression | undefined = undefined; + if (contenteditable.length > 0) { + const value = (contenteditable[0] as AttributeWrapper).get_value(block); + is_contenteditable = x`~@contenteditable_truly_values.indexOf(${value})`; + } else if (spread_attres.length > 0 && data.element_data_name) { + is_contenteditable = x`~@contenteditable_truly_values.indexOf(${data.element_data_name}['contenteditable'])`; + } + const { init } = this.rename_this_method( block, - value => x`@set_data(${this.var}, ${value})` + value => { + if (is_contenteditable) { + return x`@set_data(${this.var}, ${value}, ${is_contenteditable})`; + } else { + return x`@set_data(${this.var}, ${value})`; + } + } ); block.add_element( diff --git a/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts b/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts index 4bf8c20bd8..53847d8870 100644 --- a/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts +++ b/src/compiler/compile/render_dom/wrappers/shared/Wrapper.ts @@ -85,7 +85,7 @@ export default class Wrapper { ); } - render(_block: Block, _parent_node: Identifier, _parent_nodes: Identifier) { + render(_block: Block, _parent_node: Identifier, _parent_nodes: Identifier, _data: Record = undefined) { throw Error('Wrapper class is not renderable'); } } diff --git a/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index e092c27ee2..51547229cd 100644 --- a/src/runtime/internal/dev.ts +++ b/src/runtime/internal/dev.ts @@ -83,17 +83,15 @@ export function dataset_dev(node: HTMLElement, property: string, value?: any) { dispatch_dev('SvelteDOMSetDataset', { node, property, value }); } -export function set_data_dev(text, data) { +export function set_data_dev(text: Text, data: unknown, is_contenteditable: boolean | undefined) { data = '' + data; - // In particular, when the property is contentEditable, ' 'TEXT_NODE is inserted into the text - if (text.textContent === '' && text.wholeText !== '') { + if (is_contenteditable) { if (text.wholeText === data) return; } else { - if (text.textContent === data) return; + if (text.data === data) return; } - dispatch_dev('SvelteDOMSetData', { node: text, data }); - text.data = data; + text.data = (data as string); } export function validate_each_argument(arg) { diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 775c278bba..9d220ba8cc 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -581,15 +581,14 @@ export function claim_html_tag(nodes, is_svg: boolean) { return new HtmlTagHydration(claimed_nodes, is_svg); } -export function set_data(text, data) { +export function set_data(text: Text, data: unknown, is_contenteditable: boolean | undefined) { data = '' + data; - // In particular, when the property is contentEditable, ' 'TEXT_NODE is inserted into the text - if (text.textContent === '' && text.wholeText !== '') { + if (is_contenteditable) { if (text.wholeText === data) return; } else { - if (text.textContent === data) return; + if (text.data === data) return; } - text.data = data; + text.data = (data as string); } export function set_input_value(input, value) { diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index b1c27a355d..31623eeaab 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -194,3 +194,5 @@ export function split_css_unit(value: number | string): [number, string] { const split = typeof value === 'string' && value.match(/^\s*(-?[\d.]+)([^\s]*)\s*$/); return split ? [parseFloat(split[1]), split[2] || 'px'] : [value as number, 'px']; } + +export const contenteditable_truly_values = ['', true, 1, 'true', 'contenteditable']; diff --git a/test/runtime/samples/component-event-handler-contenteditable/_config.js b/test/runtime/samples/component-event-handler-contenteditable/_config.js index 1628e22d01..cfd51e5005 100644 --- a/test/runtime/samples/component-event-handler-contenteditable/_config.js +++ b/test/runtime/samples/component-event-handler-contenteditable/_config.js @@ -1,7 +1,5 @@ export default { - html: ` -
- `, + html: '
', async test({ assert, target, window }) { const div = target.querySelector('div'); @@ -9,7 +7,6 @@ export default { div.insertBefore(text, null); const event = new window.InputEvent('input'); await div.dispatchEvent(event); - assert.equal(div.textContent, 'a'); } }; diff --git a/test/runtime/samples/component-event-handler-contenteditable2/_config.js b/test/runtime/samples/component-event-handler-contenteditable2/_config.js new file mode 100644 index 0000000000..10736ec1b2 --- /dev/null +++ b/test/runtime/samples/component-event-handler-contenteditable2/_config.js @@ -0,0 +1,12 @@ +export default { + html: '
', + + async test({ assert, target, window }) { + const div = target.querySelector('div'); + const text = window.document.createTextNode('a'); + div.insertBefore(text, null); + const event = new window.InputEvent('input'); + await div.dispatchEvent(event); + assert.equal(div.textContent, 'a'); + } +}; diff --git a/test/runtime/samples/component-event-handler-contenteditable2/main.svelte b/test/runtime/samples/component-event-handler-contenteditable2/main.svelte new file mode 100644 index 0000000000..e540ceff25 --- /dev/null +++ b/test/runtime/samples/component-event-handler-contenteditable2/main.svelte @@ -0,0 +1,6 @@ + + +
{text}
diff --git a/test/runtime/samples/component-event-handler-contenteditable3/_config.js b/test/runtime/samples/component-event-handler-contenteditable3/_config.js new file mode 100644 index 0000000000..957be6388b --- /dev/null +++ b/test/runtime/samples/component-event-handler-contenteditable3/_config.js @@ -0,0 +1,13 @@ +export default { + html: '
', + ssrHtml: '
', + + async test({ assert, target, window }) { + const div = target.querySelector('div'); + const text = window.document.createTextNode('a'); + div.insertBefore(text, null); + const event = new window.InputEvent('input'); + await div.dispatchEvent(event); + assert.equal(div.textContent, 'a'); + } +}; diff --git a/test/runtime/samples/component-event-handler-contenteditable3/main.svelte b/test/runtime/samples/component-event-handler-contenteditable3/main.svelte new file mode 100644 index 0000000000..34bc4300e0 --- /dev/null +++ b/test/runtime/samples/component-event-handler-contenteditable3/main.svelte @@ -0,0 +1,9 @@ + + +
{text}
diff --git a/test/runtime/samples/component-event-handler-contenteditable4/_config.js b/test/runtime/samples/component-event-handler-contenteditable4/_config.js new file mode 100644 index 0000000000..bc9d65d1c3 --- /dev/null +++ b/test/runtime/samples/component-event-handler-contenteditable4/_config.js @@ -0,0 +1,12 @@ +export default { + html: '
', + + async test({ assert, target, component, window }) { + const div = target.querySelector('div'); + const text = window.document.createTextNode('a'); + div.insertBefore(text, null); + assert.equal(div.textContent, 'a'); + component.text = 'bcde'; + assert.equal(div.textContent, 'bcdea'); + } +}; diff --git a/test/runtime/samples/component-event-handler-contenteditable4/main.svelte b/test/runtime/samples/component-event-handler-contenteditable4/main.svelte new file mode 100644 index 0000000000..9555925487 --- /dev/null +++ b/test/runtime/samples/component-event-handler-contenteditable4/main.svelte @@ -0,0 +1,6 @@ + + +
{text}
diff --git a/test/runtime/samples/reactive-values-text-node/_config.js b/test/runtime/samples/reactive-values-text-node/_config.js index 9e0cb2d5d3..fb859285dd 100644 --- a/test/runtime/samples/reactive-values-text-node/_config.js +++ b/test/runtime/samples/reactive-values-text-node/_config.js @@ -1,6 +1,6 @@ export default { html:'
same text
', - async test({ assert, component, target }) { + async test({ assert, target }) { await new Promise(f => setTimeout(f, 10)); assert.htmlEqual(target.innerHTML, `
same text text
diff --git a/test/runtime/samples/reactive-values-text-node/main.svelte b/test/runtime/samples/reactive-values-text-node/main.svelte index 1ca11d19f4..0982622b1a 100644 --- a/test/runtime/samples/reactive-values-text-node/main.svelte +++ b/test/runtime/samples/reactive-values-text-node/main.svelte @@ -1,8 +1,8 @@ -
{text} text
\ No newline at end of file +
{text} text