diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index f38fe91454..d5033ae973 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -53,10 +53,17 @@ export function migrate(source) { props_name: analysis.root.unique('props').name, rest_props_name: analysis.root.unique('rest').name, end: source.length, - run_name: analysis.root.unique('run').name, - listener_name: analysis.root.unique('listener').name, - needs_run: false, - needs_listener: false + legacy_imports_names: new Map([ + ['run', analysis.root.unique('run').name], + ['handlers', analysis.root.unique('handlers').name], + ['stopImmediatePropagation', analysis.root.unique('stopImmediatePropagation').name], + ['preventDefault', analysis.root.unique('preventDefault').name], + ['stopPropagation', analysis.root.unique('stopPropagation').name], + ['once', analysis.root.unique('once').name], + ['self', analysis.root.unique('self').name], + ['trusted', analysis.root.unique('trusted').name] + ]), + legacy_imports: new Set() }; if (parsed.module) { @@ -73,7 +80,16 @@ export function migrate(source) { state = { ...state, scope: analysis.template.scope }; walk(parsed.fragment, state, template); - const legacy_import = `import { ${state.needs_run ? `run${state.run_name === 'run' ? '' : ` as ${state.run_name}`}` : ''}${state.needs_listener ? `${state.needs_run ? ', ' : ''}listener${state.listener_name === 'listener' ? '' : ` as ${state.listener_name}`}` : ''} } from 'svelte/legacy';`; + const imports = [...state.legacy_imports] + .map((legacy_import) => { + const maybe_as = state.legacy_imports_names.get(legacy_import); + if (legacy_import === maybe_as) { + return legacy_import; + } + return `${legacy_import} as ${maybe_as}`; + }) + .join(', '); + const legacy_import = `import { ${imports} } from 'svelte/legacy';`; let added_legacy_import = false; if (state.props.length > 0 || analysis.uses_rest_props || analysis.uses_props) { @@ -147,8 +163,7 @@ export function migrate(source) { props_declaration = `\n${indent}${props_declaration}`; str.appendRight(state.props_insertion_point, props_declaration); } else { - const imports = - state.needs_run || state.needs_listener ? `${indent}${legacy_import}\n` : ''; + const imports = state.legacy_imports.size > 0 ? `${indent}${legacy_import}\n` : ''; str.prepend(`\n\n`); added_legacy_import = true; } @@ -196,7 +211,7 @@ export function migrate(source) { } } - if ((state.needs_run || state.needs_listener) && !added_legacy_import) { + if (state.legacy_imports.size > 0 && !added_legacy_import) { if (parsed.instance) { str.appendRight( /** @type {number} */ (parsed.instance.content.start), @@ -227,10 +242,8 @@ export function migrate(source) { * props_name: string; * rest_props_name: string; * end: number; - * run_name: string; - * listener_name: string; - * needs_run: boolean; - * needs_listener: boolean; + * legacy_imports_names: Map; + * legacy_imports: Set; * }} State */ @@ -481,7 +494,7 @@ const instance_script = { } } - state.needs_run = true; + state.legacy_imports.add('run'); const is_block_stmt = node.body.type === 'BlockStatement'; const start_end = /** @type {number} */ (node.body.start); // TODO try to find out if we can use $derived.by instead? @@ -489,7 +502,7 @@ const instance_script = { state.str.update( /** @type {number} */ (node.start), start_end + 1, - `${state.run_name}(() => {` + `${state.legacy_imports_names.get('run')}(() => {` ); const end = /** @type {number} */ (node.body.end); state.str.update(end - 1, end, '});'); @@ -497,7 +510,7 @@ const instance_script = { state.str.update( /** @type {number} */ (node.start), start_end, - `${state.run_name}(() => {\n${state.indent}` + `${state.legacy_imports_names.get('run')}(() => {\n${state.indent}` ); state.str.indent(state.indent, { exclude: [ @@ -701,25 +714,48 @@ function handle_events(element, state) { // Check if prop already set, could happen when on:click on different elements let local = state.props.find((prop) => prop.exported === exported)?.local; - const last = nodes[nodes.length - 1]; - const payload_name = - last.expression?.type === 'ArrowFunctionExpression' && - last.expression.params[0]?.type === 'Identifier' - ? last.expression.params[0].name - : generate_event_name(last, state); + if (nodes.length > 1) { + state.legacy_imports.add('handlers'); + } + + const handlers = []; for (let i = 0; i < nodes.length; i += 1) { - let prepend = ''; const node = nodes[i]; + const payload_name = + (node.expression?.type === 'ArrowFunctionExpression' || + node.expression?.type === 'FunctionExpression') && + node.expression.params[0]?.type === 'Identifier' + ? node.expression.params[0].name + : generate_event_name(node, state); const indent = get_indent(state, node, element); - const args = `,"${node.name}"${node.modifiers.length > 0 ? `,${JSON.stringify(node.modifiers)}` : ''}`; + const new_line_index = state.str.original.lastIndexOf('\n', node.start); + const needs_line_delete = + state.str.original.substring(new_line_index, node.start).trim() === '' && i !== 0; + + // always move once as the first modifier + const sorted_modifier = [...node.modifiers].sort((a, b) => + a === 'once' ? 1 : b === 'once' ? -1 : 0 + ); + if (node.expression) { let body = state.str.original.substring( /** @type {number} */ (node.expression.start), /** @type {number} */ (node.expression.end) ); - prepend += `use:${state.listener_name}={[${body}${args}]}`; - state.needs_listener = true; + + for (const modifier of sorted_modifier) { + if (modifier !== 'capture' && modifier !== 'passive' && modifier !== 'nonpassive') { + state.legacy_imports.add(modifier); + body = `${state.legacy_imports_names.get(modifier)}(${body})`; + } + } + handlers.push({ + handler: body, + indent, + needs_line_delete + }); + state.str.remove(needs_line_delete ? new_line_index : node.start, node.end); } else { if (!local) { local = state.scope.generate(`on${node.name}`); @@ -732,11 +768,40 @@ function handle_events(element, state) { type: '(event: any) => void' }); } - prepend += `use:${state.listener_name}={[(${payload_name})=>{${local}?.(${payload_name});}${args}]}`; - state.needs_listener = true; + let body = `(${payload_name})=>{`; + + let added_modifiers = 0; + + for (const modifier of sorted_modifier) { + if (modifier !== 'capture' && modifier !== 'passive' && modifier !== 'nonpassive') { + added_modifiers++; + body = `\n${indent}${state.legacy_imports_names.get(modifier)}(${body}`; + state.legacy_imports.add(modifier); + } + } + + body += `${local}?.(${payload_name});}${')'.repeat(added_modifiers)}`; + handlers.push({ + handler: body, + indent + }); + state.str.remove(needs_line_delete ? new_line_index : node.start, node.end); } + } + + const first_node = nodes[0]; - state.str.overwrite(node.start, node.end, prepend); + if (first_node) { + let handlers_body = ''; + for (let handler of handlers) { + handlers_body += `${handler.needs_line_delete || nodes.length > 1 ? `\n${handler.indent}` : ''}${handler.handler},`; + } + handlers_body = handlers_body.substring(0, handlers_body.length - 1); + state.str.overwrite( + first_node.start, + first_node.end, + `${name}={${nodes.length > 1 ? `${state.legacy_imports_names.get('handlers')}(` : ''}${handlers_body}${nodes.length > 1 ? ')' : ''}}` + ); } } } diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 57650a29a5..d3d59ad1ed 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -172,32 +172,110 @@ export function run(fn) { } /** - * @param {EventTarget} node - * @param {[EventListener, string, string[]]} options + * Function to mimic the multiple listeners available in svelte 4 + * @param {EventListener[]} handlers + * @returns {EventListener} */ -export function listener(node, [handler, type, modifiers = []]) { - const aborter = new AbortController(); - node.addEventListener( - type, - (e) => { - for (let modifier of modifiers) { - // @ts-expect-error modifier is just a string - if (modifier in e && typeof e[modifier] === 'function') { - // @ts-expect-error modifier is just a string - e[modifier](); - } +export function handlers(...handlers) { + return function (event) { + const { stopImmediatePropagation } = event; + let stopped = false; + + event.stopImmediatePropagation = () => { + stopped = true; + stopImmediatePropagation.call(event); + }; + + const errors = []; + + for (const handler of handlers) { + try { + // @ts-expect-error `this` is not typed + handler.call(this, event); + } catch (e) { + errors.push(e); } - handler?.(e); - }, - { - signal: aborter.signal, - passive: modifiers.includes('passive'), - capture: modifiers.includes('capture') + + if (stopped) { + break; + } + } + + for (let error of errors) { + queueMicrotask(() => { + throw error; + }); + } + }; +} + +/** + * Migration helper to substitute the `stopImmediatePropagation` event modifier + * @param {EventListener} fn + */ +export function stopImmediatePropagation(fn) { + return (/**@type {Event}*/ e) => { + e.stopImmediatePropagation(); + fn(e); + }; +} + +/** + * Migration helper to substitute the `preventDefault` event modifier + * @param {EventListener} fn + */ +export function preventDefault(fn) { + return (/**@type {Event}*/ e) => { + e.preventDefault(); + fn(e); + }; +} + +/** + * Migration helper to substitute the `stopPropagation` event modifier + * @param {EventListener} fn + */ +export function stopPropagation(fn) { + return (/**@type {Event}*/ e) => { + e.stopPropagation(); + fn(e); + }; +} + +/** + * Migration helper to substitute the `trusted` event modifier + * @param {EventListener} fn + */ +export function trusted(fn) { + return (/**@type {Event}*/ e) => { + if (e.isTrusted) { + fn(e); } - ); - return { - destroy() { - aborter.abort(); + }; +} + +/** + * Migration helper to substitute the `self` event modifier + * @param {EventListener} fn + */ +export function self(fn) { + return (/**@type {Event}*/ e) => { + if (e.target === e.currentTarget) { + fn(e); + } + }; +} + +/** + * Migration helper to substitute the `once` event modifier + * @param {EventListener} fn + */ +export function once(fn) { + let executed = false; + return (/**@type {Event}*/ e) => { + if (!executed) { + executed = true; + fn(e); } }; } diff --git a/packages/svelte/tests/migrate/samples/event-handlers-with-alias/input.svelte b/packages/svelte/tests/migrate/samples/event-handlers-with-alias/input.svelte new file mode 100644 index 0000000000..b30f5cd14a --- /dev/null +++ b/packages/svelte/tests/migrate/samples/event-handlers-with-alias/input.svelte @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
diff --git a/packages/svelte/tests/migrate/samples/event-handlers-with-alias/output.svelte b/packages/svelte/tests/migrate/samples/event-handlers-with-alias/output.svelte new file mode 100644 index 0000000000..cd5d626339 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/event-handlers-with-alias/output.svelte @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
\ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/event-handlers/input.svelte b/packages/svelte/tests/migrate/samples/event-handlers/input.svelte index 2557062727..204d9f9a1d 100644 --- a/packages/svelte/tests/migrate/samples/event-handlers/input.svelte +++ b/packages/svelte/tests/migrate/samples/event-handlers/input.svelte @@ -1,4 +1,5 @@ + @@ -10,12 +11,33 @@ - + + + + + + + + + + + + + diff --git a/packages/svelte/tests/migrate/samples/event-handlers/output.svelte b/packages/svelte/tests/migrate/samples/event-handlers/output.svelte index a31e3e9d54..61ccf736da 100644 --- a/packages/svelte/tests/migrate/samples/event-handlers/output.svelte +++ b/packages/svelte/tests/migrate/samples/event-handlers/output.svelte @@ -1,40 +1,81 @@ - - + + - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - -
click me click me - +
\ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/state-ts/output.svelte b/packages/svelte/tests/migrate/samples/state-ts/output.svelte index e62447a2e2..c6f672c535 100644 --- a/packages/svelte/tests/migrate/samples/state-ts/output.svelte +++ b/packages/svelte/tests/migrate/samples/state-ts/output.svelte @@ -1,10 +1,8 @@
- \ No newline at end of file + \ No newline at end of file