From a816f027116220d28b088d545d6053863d335e0d Mon Sep 17 00:00:00 2001 From: adiguba Date: Wed, 12 Oct 2022 22:04:55 +0200 Subject: [PATCH 01/15] svelte-display implementation --- package-lock.json | 2 +- src/compiler/compile/compiler_errors.ts | 22 ++- src/compiler/compile/nodes/Element.ts | 30 +++- src/compiler/compile/nodes/InlineComponent.ts | 3 + src/compiler/compile/nodes/SvelteDirective.ts | 22 +++ src/compiler/compile/nodes/interfaces.ts | 2 + .../render_dom/wrappers/Element/index.ts | 143 ++++++++++++------ .../compile/render_ssr/handlers/Element.ts | 5 + src/compiler/interfaces.ts | 3 +- src/compiler/parse/state/tag.ts | 12 +- src/runtime/internal/dom.ts | 4 + 11 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 src/compiler/compile/nodes/SvelteDirective.ts diff --git a/package-lock.json b/package-lock.json index dec2c8ef0ecb..1f8f623dee3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "svelte", - "version": "3.49.0", + "version": "3.51.0", "license": "MIT", "devDependencies": { "@ampproject/remapping": "^0.3.0", diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index c1a7d8bc5cbd..63c0e691f9b3 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -281,5 +281,25 @@ export default { invalid_component_style_directive: { code: 'invalid-component-style-directive', message: 'Style directives cannot be used on components' - } + }, + invalid_component_svelte_directive: (name) => ({ + code: 'invalid-component-svelte-directive', + message: `svelte:${name} directives cannot be used on components` + }), + duplicate_directive: (directive) => ({ + code: 'duplicate-directive', + message: `An element can only have one '${directive}' directive` + }), + invalid_directive: (directive) => ({ + code: 'invalid-directive', + message: `'${directive}' is not a valid directive` + }), + invalid_modifier: { + code: 'invalid-modifier', + message: 'No modifier allowed on this directive' + }, + directive_conflict: (directive1, directive2) => ({ + code: 'directive-conflict', + message: `Cannot use ${directive1} and ${directive2} on the same element` + }) }; diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 79f2800437bf..09435f9b060f 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -25,6 +25,7 @@ import compiler_warnings from '../compiler_warnings'; import compiler_errors from '../compiler_errors'; import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query'; import { is_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element } from '../utils/a11y'; +import SvelteDirective from './SvelteDirective'; const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const aria_attribute_set = new Set(aria_attributes); @@ -217,6 +218,7 @@ export default class Element extends Node { intro?: Transition = null; outro?: Transition = null; animation?: Animation = null; + display? : SvelteDirective = null; children: INode[]; namespace: string; needs_manual_style_scoping: boolean; @@ -355,6 +357,22 @@ export default class Element extends Node { this.animation = new Animation(component, this, scope, node); break; + case 'SvelteDirective': + switch (node.name) { + case 'display': + if (this.display) { + component.error(node, + compiler_errors.duplicate_directive('svelte:' + node.name)); + } else { + this.display = new SvelteDirective(component, this, scope, node); + } + break; + default: + component.error(node, + compiler_errors.invalid_directive('svelte:' + node.name)); + } + break; + default: throw new Error(`Not implemented: ${node.type}`); } @@ -385,7 +403,7 @@ export default class Element extends Node { this.validate_bindings(); this.validate_content(); } - + this.validate_display_directive(); } validate_attributes() { @@ -941,6 +959,16 @@ export default class Element extends Node { }); } + validate_display_directive() { + if (!this.display) { + return; + } + if (this.styles.find(d => d.name === 'display')) { + this.component.error(this.display, + compiler_errors.directive_conflict('svelte:display', 'style:display')); + } + } + is_media_node() { return this.name === 'audio' || this.name === 'video'; } diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index 8871c5f306dd..4853f15e0d50 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -77,6 +77,9 @@ export default class InlineComponent extends Node { case 'StyleDirective': return component.error(node, compiler_errors.invalid_component_style_directive); + case 'SvelteDirective': + return component.error(node, compiler_errors.invalid_component_svelte_directive(node.name)); + default: throw new Error(`Not implemented: ${node.type}`); } diff --git a/src/compiler/compile/nodes/SvelteDirective.ts b/src/compiler/compile/nodes/SvelteDirective.ts new file mode 100644 index 000000000000..03d4b0199c0b --- /dev/null +++ b/src/compiler/compile/nodes/SvelteDirective.ts @@ -0,0 +1,22 @@ +import Node from './shared/Node'; +import Expression from './shared/Expression'; +import Component from '../Component'; +import TemplateScope from './shared/TemplateScope'; +import { TemplateNode } from '../../interfaces'; +import Element from './Element'; +import compiler_errors from '../compiler_errors'; + +export default class SvelteDirective extends Node { + type: 'SvelteDirective'; + expression: Expression; + + constructor(component: Component, parent: Element, scope: TemplateScope, info: TemplateNode) { + super(component, parent, scope, info); + + this.expression = new Expression(component, this, scope, info.expression); + + if (info.modifiers && info.modifiers.length) { + component.error(info, compiler_errors.invalid_modifier); + } + } +} diff --git a/src/compiler/compile/nodes/interfaces.ts b/src/compiler/compile/nodes/interfaces.ts index f023cad25c9f..0b8046aa0dc8 100644 --- a/src/compiler/compile/nodes/interfaces.ts +++ b/src/compiler/compile/nodes/interfaces.ts @@ -33,6 +33,7 @@ import ThenBlock from './ThenBlock'; import Title from './Title'; import Transition from './Transition'; import Window from './Window'; +import SvelteDisplayDirective from './SvelteDirective'; // note: to write less types each of types in union below should have type defined as literal // https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions @@ -64,6 +65,7 @@ export type INode = Action | Slot | SlotTemplate | StyleDirective +| SvelteDisplayDirective | Tag | Text | ThenBlock diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 4f0b49729c8c..23fb74041d9f 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -163,7 +163,7 @@ export default class ElementWrapper extends Wrapper { ) { super(renderer, block, parent, node); - if (node.is_dynamic_element && block.type !== CHILD_DYNAMIC_ELEMENT_BLOCK) { + if ((node.display || node.is_dynamic_element) && block.type !== CHILD_DYNAMIC_ELEMENT_BLOCK) { this.child_dynamic_element_block = block.child({ comment: create_debugging_comment(node, renderer.component), name: renderer.component.get_unique_name('create_dynamic_element'), @@ -273,16 +273,16 @@ export default class ElementWrapper extends Wrapper { (x`#nodes` as unknown) as Identifier ); - const previous_tag = block.get_unique_name('previous_tag'); + const tag = this.node.tag_expr.manipulate(block); - block.add_variable(previous_tag, tag); - - block.chunks.init.push(b` - ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} - ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} - let ${this.var} = ${tag} && ${this.child_dynamic_element_block.name}(#ctx); - `); + if (this.renderer.options.dev) { + block.chunks.init.push(b` + @validate_dynamic_element(${tag}); + @validate_void_dynamic_element(${tag}); + `); + } + block.chunks.create.push(b` if (${this.var}) ${this.var}.c(); `); @@ -297,45 +297,56 @@ export default class ElementWrapper extends Wrapper { if (${this.var}) ${this.var}.m(${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'}); `); - const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes); - const has_transitions = !!(this.node.intro || this.node.outro); - const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`; - - block.chunks.update.push(b` - if (${tag}) { - if (!${previous_tag}) { - ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); - ${this.var}.c(); - ${has_transitions && b`@transition_in(${this.var})`} - ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); - } else if (${not_equal}(${previous_tag}, ${tag})) { - ${this.var}.d(1); - ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} - ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} - ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); - ${this.var}.c(); - ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); - } else { - ${this.var}.p(#ctx, #dirty); - } - } else if (${previous_tag}) { - ${ - has_transitions - ? b` - @group_outros(); - @transition_out(${this.var}, 1, 1, () => { + if (this.node.display && tag.type === 'Literal') { + block.chunks.init.push(b`const ${this.var} = ${this.child_dynamic_element_block.name}(#ctx);`); + block.chunks.update.push(b`${this.var}.p(#ctx, #dirty);`); + } else { + const previous_tag = block.get_unique_name('previous_tag'); + block.add_variable(previous_tag, tag); + block.chunks.init.push(b`let ${this.var} = ${tag} && ${this.child_dynamic_element_block.name}(#ctx);`); + + const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes); + const has_transitions = !!(this.node.intro || this.node.outro); + const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`; + + block.chunks.update.push(b` + if (${tag}) { + if (!${previous_tag}) { + ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); + ${this.var}.c(); + ${has_transitions && b`@transition_in(${this.var})`} + ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); + } else if (${not_equal}(${previous_tag}, ${tag})) { + ${this.var}.d(1); + ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} + ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} + ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); + ${this.var}.c(); + ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); + } else { + ${this.var}.p(#ctx, #dirty); + } + } else if (${previous_tag}) { + ${ + has_transitions + ? b` + @group_outros(); + @transition_out(${this.var}, 1, 1, () => { + ${this.var} = null; + }); + @check_outros(); + ` + : b` + ${this.var}.d(1); ${this.var} = null; - }); - @check_outros(); - ` - : b` - ${this.var}.d(1); - ${this.var} = null; - ` + ` + } } - } - ${previous_tag} = ${tag}; - `); + ${previous_tag} = ${tag}; + `); + } + + if (this.child_dynamic_element_block.has_intros) { block.chunks.intro.push(b`@transition_in(${this.var});`); @@ -480,6 +491,7 @@ export default class ElementWrapper extends Wrapper { this.add_animation(block); this.add_classes(block); this.add_styles(block); + this.add_display(block); this.add_manual_style_scoping(block); if (nodes && this.renderer.options.hydratable && !this.void) { @@ -1129,6 +1141,45 @@ export default class ElementWrapper extends Wrapper { }); } + add_display(block: Block) { + const display = this.node.display; + if (display === null) { + return; + } + + const snippet = display.expression.manipulate(block); + const dependencies = display.expression.dynamic_dependencies(); + const has_dependancies = dependencies.length > 0; + + const update_display = b`@set_display(${this.var}, ${snippet})`; + block.chunks.hydrate.push(update_display); + + if (has_dependancies) { + const update_current = (this.node.intro || this.node.outro) + ? x`#current = false` + : null; + + const dirty = block.renderer.dirty(Array.from(dependencies)); + block.chunks.update.push(b` + if (${dirty}) { + if (${snippet}) { + ${update_current} + ${update_display} + @transition_in(this, 1); + } else { + @group_outros(); + @transition_out(this, 1, 0, () => { + ${update_display} + }); + @check_outros(); + } + } + `); + } + } + + + add_manual_style_scoping(block) { if (this.node.needs_manual_style_scoping) { const updater = b`@toggle_class(${this.var}, "${this.node.component.stylesheet.id}", true);`; diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index 2af0343b2aef..c5821aa8dbe9 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -48,6 +48,11 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio return p`"${name}": ${expression}`; }); + if (node.display) { + const snippet = node.display.expression.node; + style_expression_list.push(p`"display": (${snippet} ? "none !important" : null)`); + } + const style_expression = style_expression_list.length > 0 && x`{ ${style_expression_list} }`; diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index 248175da5995..73eb5fc397c0 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -48,7 +48,8 @@ export type DirectiveType = 'Action' | 'EventHandler' | 'Let' | 'Ref' -| 'Transition'; +| 'Transition' +| 'SvelteDirective'; interface BaseDirective extends BaseNode { type: DirectiveType; diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index 4be47f25b089..915770261bc8 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -286,6 +286,15 @@ function read_tag_name(parser: Parser) { return name; } +function use_name_as_expression(type:string, name:string):boolean { + if (type === 'Binding' || type === 'Class') { + return true; + } else if (type === 'SvelteDirective') { + return name === 'display'; + } + return false; +} + function read_attribute(parser: Parser, unique_names: Set) { const start = parser.index; @@ -419,7 +428,7 @@ function read_attribute(parser: Parser, unique_names: Set) { } // Directive name is expression, e.g.

- if (!directive.expression && (type === 'Binding' || type === 'Class')) { + if (!directive.expression && use_name_as_expression(type, directive_name)) { directive.expression = { start: directive.start + colon_index + 1, end: directive.end, @@ -452,6 +461,7 @@ function get_directive_type(name: string): DirectiveType { if (name === 'let') return 'Let'; if (name === 'ref') return 'Ref'; if (name === 'in' || name === 'out' || name === 'transition') return 'Transition'; + if (name === 'svelte') return 'SvelteDirective'; } function read_attribute_value(parser: Parser) { diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index a1c0e1c0aa80..4aa30eb2505d 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -545,6 +545,10 @@ export function set_style(node, key, value, important) { } } +export function set_display(node, value) { + set_style(node, 'display', value ? null : 'none', 1); +} + export function select_option(select, value) { for (let i = 0; i < select.options.length; i += 1) { const option = select.options[i]; From 0ddf94b9347bbaa5c28d30feb09c4f125245b84f Mon Sep 17 00:00:00 2001 From: adiguba Date: Thu, 2 Mar 2023 17:18:42 +0100 Subject: [PATCH 02/15] first version --- src/compiler/compile/compiler_errors.ts | 6 +- src/compiler/compile/compiler_warnings.ts | 6 +- src/compiler/compile/nodes/Element.ts | 58 +--------- src/compiler/compile/nodes/EventHandler.ts | 77 ++++++++++++- src/compiler/compile/nodes/InlineComponent.ts | 8 +- .../wrappers/Element/EventHandler.ts | 74 +++++++----- .../wrappers/InlineComponent/index.ts | 17 ++- src/runtime/index.ts | 1 + src/runtime/internal/Component.ts | 32 ++++-- src/runtime/internal/dev.ts | 42 ++++++- src/runtime/internal/dom.ts | 37 +++++- src/runtime/internal/lifecycle.ts | 106 ++++++++++++++++-- src/runtime/internal/types.ts | 17 ++- 13 files changed, 338 insertions(+), 143 deletions(-) diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index 62103d26d6d4..d1880ca1afb6 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -305,5 +305,9 @@ export default { directive_conflict: (directive1, directive2) => ({ code: 'directive-conflict', message: `Cannot use ${directive1} and ${directive2} on the same element` - }) + }), + too_much_forward_event_modifiers: { + code: 'too-much-forward-event-modifiers', + message: 'Forward-event only accept one modifier (the forward alias)' + }, }; diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index 3a11e04bd1de..43f77cd07d5d 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -198,5 +198,9 @@ export default { invalid_rest_eachblock_binding: (rest_element_name: string) => ({ code: 'invalid-rest-eachblock-binding', message: `...${rest_element_name} operator will create a new object and binding propogation with original object will not work` - }) + }), + incorrect_forward_event_modifier: (modifiers: Set) => ({ + code: 'incorrect-forward-event-modifier', + message: `Forward-event only accept one modifier for the forward alias. Event modifiers should not be used here : ${modifiers}` + }), }; diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 6f61e43c7b14..258c77d64677 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -13,7 +13,6 @@ import { namespaces } from '../../utils/namespaces'; import map_children from './shared/map_children'; import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character } from '../../utils/patterns'; import fuzzymatch from '../../utils/fuzzymatch'; -import list from '../../utils/list'; import Let from './Let'; import TemplateScope from './shared/TemplateScope'; import { INode } from './interfaces'; @@ -118,24 +117,6 @@ const a11y_implicit_semantics = new Map([ const invisible_elements = new Set(['meta', 'html', 'script', 'style']); -const valid_modifiers = new Set([ - 'preventDefault', - 'stopPropagation', - 'capture', - 'once', - 'passive', - 'nonpassive', - 'self', - 'trusted' -]); - -const passive_events = new Set([ - 'wheel', - 'touchstart', - 'touchmove', - 'touchend', - 'touchcancel' -]); const react_attributes = new Map([ ['className', 'class'], @@ -943,44 +924,7 @@ export default class Element extends Node { } validate_event_handlers() { - const { component } = this; - - this.handlers.forEach(handler => { - if (handler.modifiers.has('passive') && handler.modifiers.has('preventDefault')) { - return component.error(handler, compiler_errors.invalid_event_modifier_combination('passive', 'preventDefault')); - } - - if (handler.modifiers.has('passive') && handler.modifiers.has('nonpassive')) { - return component.error(handler, compiler_errors.invalid_event_modifier_combination('passive', 'nonpassive')); - } - - handler.modifiers.forEach(modifier => { - if (!valid_modifiers.has(modifier)) { - return component.error(handler, compiler_errors.invalid_event_modifier(list(Array.from(valid_modifiers)))); - } - - if (modifier === 'passive') { - if (passive_events.has(handler.name)) { - if (handler.can_make_passive) { - component.warn(handler, compiler_warnings.redundant_event_modifier_for_touch); - } - } else { - component.warn(handler, compiler_warnings.redundant_event_modifier_passive); - } - } - - if (component.compile_options.legacy && (modifier === 'once' || modifier === 'passive')) { - // TODO this could be supported, but it would need a few changes to - // how event listeners work - return component.error(handler, compiler_errors.invalid_event_modifier_legacy(modifier)); - } - }); - - if (passive_events.has(handler.name) && handler.can_make_passive && !handler.modifiers.has('preventDefault') && !handler.modifiers.has('nonpassive')) { - // touch/wheel events should be passive by default - handler.modifiers.add('passive'); - } - }); + this.handlers.forEach(h => h.validate()); } validate_display_directive() { diff --git a/src/compiler/compile/nodes/EventHandler.ts b/src/compiler/compile/nodes/EventHandler.ts index 4b426580b3ae..091e4ca77f9d 100644 --- a/src/compiler/compile/nodes/EventHandler.ts +++ b/src/compiler/compile/nodes/EventHandler.ts @@ -1,19 +1,41 @@ import Node from './shared/Node'; import Expression from './shared/Expression'; import Component from '../Component'; -import { sanitize } from '../../utils/names'; import { Identifier } from 'estree'; import TemplateScope from './shared/TemplateScope'; import { TemplateNode } from '../../interfaces'; +import compiler_errors from '../compiler_errors'; +import compiler_warnings from '../compiler_warnings'; +import list from '../../utils/list'; const regex_contains_term_function_expression = /FunctionExpression/; +const valid_modifiers = new Set([ + 'preventDefault', + 'stopPropagation', + 'capture', + 'once', + 'passive', + 'nonpassive', + 'self', + 'trusted' +]); + +const passive_events = new Set([ + 'wheel', + 'touchstart', + 'touchmove', + 'touchend', + 'touchcancel' +]); + export default class EventHandler extends Node { type: 'EventHandler'; name: string; modifiers: Set; expression: Expression; - handler_name: Identifier; + aliasName?: string; + aliasCount: 0; uses_context = false; can_make_passive = false; @@ -47,7 +69,56 @@ export default class EventHandler extends Node { } } } else { - this.handler_name = component.get_unique_name(`${sanitize(this.name)}_handler`); + if (info.modifiers && info.modifiers.length) { + this.aliasCount = info.modifiers.length; + this.aliasName = info.modifiers[0]; + } + } + } + + validate() { + if (this.expression) { + if (this.modifiers.has('passive') && this.modifiers.has('preventDefault')) { + return this.component.error(this, compiler_errors.invalid_event_modifier_combination('passive', 'preventDefault')); + } + + if (this.modifiers.has('passive') && this.modifiers.has('nonpassive')) { + return this.component.error(this, compiler_errors.invalid_event_modifier_combination('passive', 'nonpassive')); + } + + this.modifiers.forEach(modifier => { + if (!valid_modifiers.has(modifier)) { + return this.component.error(this, compiler_errors.invalid_event_modifier(list(Array.from(valid_modifiers)))); + } + + if (modifier === 'passive') { + if (passive_events.has(this.name)) { + if (this.can_make_passive) { + this.component.warn(this, compiler_warnings.redundant_event_modifier_for_touch); + } + } else { + this.component.warn(this, compiler_warnings.redundant_event_modifier_passive); + } + } + + if (this.component.compile_options.legacy && (modifier === 'once' || modifier === 'passive')) { + // TODO this could be supported, but it would need a few changes to + // how event listeners work + return this.component.error(this, compiler_errors.invalid_event_modifier_legacy(modifier)); + } + }); + + if (passive_events.has(this.name) && this.can_make_passive && !this.modifiers.has('preventDefault') && !this.modifiers.has('nonpassive')) { + // touch/wheel events should be passive by default + this.modifiers.add('passive'); + } + } else { + if (this.aliasCount > 1) { + return this.component.error(this, compiler_errors.too_much_forward_event_modifiers); + } + if (this.aliasName && valid_modifiers.has(this.aliasName)) { + this.component.warn(this, compiler_warnings.incorrect_forward_event_modifier(valid_modifiers)); + } } } diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index 87f999331d9c..4908f98cd7ff 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -101,13 +101,7 @@ export default class InlineComponent extends Node { this.scope = scope; } - this.handlers.forEach(handler => { - handler.modifiers.forEach(modifier => { - if (modifier !== 'once') { - return component.error(handler, compiler_errors.invalid_event_modifier_component); - } - }); - }); + this.handlers.forEach(h => h.validate()) const children = []; for (let i = info.children.length - 1; i >= 0; i--) { diff --git a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts index b0ed4a73d6a6..31e3eea64e19 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts @@ -1,8 +1,9 @@ import EventHandler from '../../../nodes/EventHandler'; import Wrapper from '../shared/Wrapper'; import Block from '../../Block'; -import { b, x, p } from 'code-red'; +import { x, p, b } from 'code-red'; import { Expression } from 'estree'; +import { sanitize } from '../../../../utils/names'; const TRUE = x`true`; const FALSE = x`false`; @@ -15,34 +16,34 @@ export default class EventHandlerWrapper { this.node = node; this.parent = parent; - if (!node.expression) { - this.parent.renderer.add_to_context(node.handler_name.name); - - this.parent.renderer.component.partly_hoisted.push(b` - function ${node.handler_name.name}(event) { - @bubble.call(this, $$self, event); - } - `); - } } + get_snippet(block: Block) { - const snippet = this.node.expression ? this.node.expression.manipulate(block) : block.renderer.reference(this.node.handler_name); + return this.node.expression.manipulate(block); + } - if (this.node.reassigned) { - block.maintain_context = true; - return x`function () { if (@is_function(${snippet})) ${snippet}.apply(this, arguments); }`; + + + render(block: Block, target: string | Expression, is_comp: boolean = false) { + const listen = is_comp ? '@listen_comp' : '@listen'; + if (!this.node.expression) { + const self = this.parent.renderer.add_to_context('$$self'); + const selfvar = block.renderer.reference(self.name); + const aliasName = this.node.aliasName ? `"${this.node.aliasName}"` : null; + + block.event_listeners.push(x`@bubble(${selfvar}, ${listen}, ${target}, "${this.node.name}", ${aliasName})`); + return; } - return snippet; - } - render(block: Block, target: string | Expression) { - let snippet = this.get_snippet(block); + const snippet = this.get_snippet(block); - if (this.node.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`; - if (this.node.modifiers.has('stopPropagation')) snippet = x`@stop_propagation(${snippet})`; - if (this.node.modifiers.has('self')) snippet = x`@self(${snippet})`; - if (this.node.modifiers.has('trusted')) snippet = x`@trusted(${snippet})`; + let wrappers = []; + if (this.node.modifiers.has('trusted')) wrappers.push(x`@trusted`); + if (this.node.modifiers.has('self')) wrappers.push(x`@self`); + if (this.node.modifiers.has('stopPropagation')) wrappers.push(x`@stop_propagation`); + if (this.node.modifiers.has('preventDefault')) wrappers.push(x`@prevent_default`); + // TODO : once() on component ???? const args = []; @@ -57,17 +58,30 @@ export default class EventHandlerWrapper { : p`${opt}: true` ) } }`); } - } else if (block.renderer.options.dev) { + } else if (wrappers.length) { args.push(FALSE); } - - if (block.renderer.options.dev) { - args.push(this.node.modifiers.has('preventDefault') ? TRUE : FALSE); - args.push(this.node.modifiers.has('stopPropagation') ? TRUE : FALSE); + if (wrappers.length) { + args.push(x`[${wrappers}]`); } - block.event_listeners.push( - x`@listen(${target}, "${this.node.name}", ${snippet}, ${args})` - ); + if (this.node.reassigned) { + const handle = this.node.component.get_unique_name(`${sanitize(this.node.name)}_handle`); + block.add_variable(handle); + + const condition = block.renderer.dirty(this.node.expression.dynamic_dependencies()); + + block.chunks.update.push(b` + if (${condition}) { + ${handle}.swap(${snippet}) + }`); + block.event_listeners.push( + x`${handle} = @listen_swap(${snippet}, (h)=> ${listen}(${target}, "${this.node.name}", h, ${args}))` + ); + } else { + block.event_listeners.push( + x`${listen}(${target}, "${this.node.name}", ${snippet}, ${args})` + ); + } } } diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index bca01fdb4cd2..d22215ec5976 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -396,13 +396,13 @@ export default class InlineComponentWrapper extends Wrapper { return b`@binding_callbacks.push(() => @bind(${this.var}, '${binding.name}', ${id}));`; }); - const munged_handlers = this.node.handlers.map(handler => { - const event_handler = new EventHandler(handler, this); - let snippet = event_handler.get_snippet(block); - if (handler.modifiers.has('once')) snippet = x`@once(${snippet})`; - - return b`${name}.$on("${handler.name}", ${snippet});`; - }); + if (this.node.handlers.length > 0) { + const target = x`${name}`; + for (const handler of this.node.handlers) { + new EventHandler(handler, this) + .render(block, target, true); + } + } const mount_target = has_css_custom_properties ? css_custom_properties_wrapper : (parent_node || '#target'); const mount_anchor = has_css_custom_properties ? 'null' : (parent_node ? 'null' : '#anchor'); @@ -433,7 +433,6 @@ export default class InlineComponentWrapper extends Wrapper { ${name} = @construct_svelte_component(${switch_value}, ${switch_props}(#ctx)); ${munged_bindings} - ${munged_handlers} } `); @@ -481,7 +480,6 @@ export default class InlineComponentWrapper extends Wrapper { ${name} = @construct_svelte_component(${switch_value}, ${switch_props}(#ctx)); ${munged_bindings} - ${munged_handlers} @create_component(${name}.$$.fragment); @transition_in(${name}.$$.fragment, 1); @@ -515,7 +513,6 @@ export default class InlineComponentWrapper extends Wrapper { ${name} = new ${expression}(${component_opts}); ${munged_bindings} - ${munged_handlers} `); if (has_css_custom_properties) { diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 2029f67a0464..97eef2092dd3 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -11,6 +11,7 @@ export { hasContext, tick, createEventDispatcher, + onEventListener, SvelteComponentDev as SvelteComponent, SvelteComponentTyped // additional exports added through generate-type-definitions.js diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 5aec24c651f7..9979dc6da007 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -1,9 +1,9 @@ import { add_render_callback, flush, schedule_update, dirty_components } from './scheduler'; -import { current_component, set_current_component } from './lifecycle'; +import { current_component, set_current_component, start_callback, stop_callback } from './lifecycle'; import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; import { children, detach, start_hydrating, end_hydrating } from './dom'; import { transition_in } from './transitions'; -import { T$$ } from './types'; +import { Callback, T$$ } from './types'; export function bind(component, name, callback) { const index = component.$$.props[name]; @@ -95,6 +95,7 @@ export function init(component, options, instance, create_fragment, not_equal, p // everything else callbacks: blank_object(), + bubbles: blank_object(), dirty, skip_bound: false, root: options.target || parent_component.$$.root @@ -177,17 +178,22 @@ if (typeof HTMLElement === 'function') { this.$destroy = noop; } - $on(type, callback) { - // TODO should this delegate to addEventListener? + $on(type: string, callback: EventListener, options?: boolean | AddEventListenerOptions | EventListenerOptions) { if (!is_function(callback)) { return noop; } const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); - callbacks.push(callback); - + const c: Callback = {f:callback, o:options}; + callbacks.push(c); + + start_callback(this as any, type, c); + return () => { - const index = callbacks.indexOf(callback); - if (index !== -1) callbacks.splice(index, 1); + const index = callbacks.indexOf(c); + if (index !== -1) { + callbacks.splice(index, 1); + stop_callback(this as any, type, c); + } }; } @@ -213,16 +219,20 @@ export class SvelteComponent { this.$destroy = noop; } - $on(type, callback) { + $on(type: string, callback: EventListener, options?: boolean | AddEventListenerOptions | EventListenerOptions) { if (!is_function(callback)) { return noop; } const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); - callbacks.push(callback); + const c: Callback = {f:callback, o:options}; + callbacks.push(c); + + start_callback(this as any, type, c); return () => { - const index = callbacks.indexOf(callback); + const index = callbacks.indexOf(c); if (index !== -1) callbacks.splice(index, 1); + stop_callback(this as any, type, c); }; } diff --git a/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index 9d24ad2a07ba..1dfe6fdfa8d2 100644 --- a/src/runtime/internal/dev.ts +++ b/src/runtime/internal/dev.ts @@ -1,6 +1,7 @@ -import { custom_event, append, append_hydration, insert, insert_hydration, detach, listen, attr } from './dom'; +import { custom_event, append, append_hydration, insert, insert_hydration, detach, listen, attr, prevent_default, stop_propagation, trusted, self } from './dom'; import { SvelteComponent } from './Component'; import { is_void } from '../../shared/utils/names'; +import { bubble, listen_comp } from './lifecycle'; export function dispatch_dev(type: string, detail?: T) { document.dispatchEvent(custom_event(type, { version: '__VERSION__', ...detail }, { bubbles: true })); @@ -49,20 +50,49 @@ export function detach_after_dev(before: Node) { } } -export function listen_dev(node: Node, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions, has_prevent_default?: boolean, has_stop_propagation?: boolean) { +function build_modifiers(options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { const modifiers = options === true ? [ 'capture' ] : options ? Array.from(Object.keys(options)) : []; - if (has_prevent_default) modifiers.push('preventDefault'); - if (has_stop_propagation) modifiers.push('stopPropagation'); + if (wrappers) { + if (wrappers.indexOf(prevent_default) >= 0) modifiers.push('preventDefault'); + if (wrappers.indexOf(stop_propagation) >= 0) modifiers.push('stopPropagation'); + // ??? + if (wrappers.indexOf(trusted) >= 0) modifiers.push('trusted'); + if (wrappers.indexOf(self) >= 0) modifiers.push('self'); + } + return modifiers; +} +export function listen_dev(node: Node, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { + const modifiers = build_modifiers(options, wrappers); dispatch_dev('SvelteDOMAddEventListener', { node, event, handler, modifiers }); - const dispose = listen(node, event, handler, options); + const dispose = listen(node, event, handler, options, wrappers); return () => { dispatch_dev('SvelteDOMRemoveEventListener', { node, event, handler, modifiers }); dispose(); }; } +export function bubble_dev(component: SvelteComponent, listen_func: Function, node: EventTarget|SvelteComponent, type: string, typeName: string = type): Function { + dispatch_dev('SvelteComponentAddEventBubble', { component, listen_func, node, type, typeName }); + const dispose = bubble(component, listen_func, node, type, typeName); + return () => { + dispatch_dev('SvelteComponentRemoveEventBubble', { component, listen_func, node, type, typeName }); + dispose(); + }; +} + +export function listen_comp_dev(comp: SvelteComponent, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { + const modifiers = build_modifiers(options, wrappers); + dispatch_dev('SvelteComponentAddEventListener', { comp, event, handler, modifiers }); + + const dispose = listen_comp(comp, event, handler, options, wrappers); + return () => { + dispatch_dev('SvelteComponentRemoveEventListener', { comp, event, handler, modifiers }); + dispose(); + }; +} + export function attr_dev(node: Element, attribute: string, value?: string) { attr(node, attribute, value); @@ -144,7 +174,7 @@ export function construct_svelte_component_dev(component, props) { type Props = Record; export interface SvelteComponentDev { $set(props?: Props): void; - $on(event: string, callback: ((event: any) => void) | null | undefined): () => void; + $on(event: string, callback: ((event: any) => void) | null | undefined, options?: boolean | AddEventListenerOptions | EventListenerOptions): () => void; $destroy(): void; [accessor: string]: any; } diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 4aa30eb2505d..f175d5922764 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -1,4 +1,4 @@ -import { has_prop } from './utils'; +import { has_prop, is_function, noop } from './utils'; // Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM // at the end of hydration without touching the remaining nodes. @@ -252,9 +252,38 @@ export function empty() { return text(''); } -export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) { - node.addEventListener(event, handler, options); - return () => node.removeEventListener(event, handler, options); +export function wrap_handler(handler: EventListenerOrEventListenerObject, wrappers?: Function[]): EventListener { + let result = is_function(handler) ? handler : handler.handleEvent.bind(handler); + if (wrappers) { + for (const fn of wrappers) { + result = fn(result); + } + } + return result; +} + +export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject|null|undefined, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { + if (handler) { + handler = wrap_handler(handler, wrappers); + node.addEventListener(event, handler, options); + return () => node.removeEventListener(event, handler, options); + } + return noop; +} + +export function listen_swap(handler: EventListenerOrEventListenerObject|null|undefined, factory: (handler:EventListenerOrEventListenerObject) => Function) { + let disposeHandle: Function = factory(handler); + const dispose = () => { + disposeHandle(); + } + dispose.swap = (new_handler:EventListenerOrEventListenerObject) => { + if (new_handler !== handler) { + disposeHandle(); + handler = new_handler; + disposeHandle = factory(handler); + } + } + return dispose; } export function prevent_default(fn) { diff --git a/src/runtime/internal/lifecycle.ts b/src/runtime/internal/lifecycle.ts index e75bbdc501f4..2e381e669235 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -1,4 +1,7 @@ -import { custom_event } from './dom'; +import { SvelteComponent } from './Component'; +import { custom_event, wrap_handler } from './dom'; +import { Bubble, Callback, CallbackFactory } from './types'; +import { noop } from './utils'; export let current_component; @@ -82,14 +85,14 @@ export function createEventDispatcher(): < const component = get_current_component(); return (type: string, detail?: any, { cancelable = false } = {}): boolean => { - const callbacks = component.$$.callbacks[type]; + const callbacks: Callback[] = component.$$.callbacks[type]; if (callbacks) { // TODO are there situations where events could be dispatched // in a server (non-DOM) environment? const event = custom_event(type, detail, { cancelable }); - callbacks.slice().forEach(fn => { - fn.call(component, event); + callbacks.slice().forEach(callback => { + callback.f.call(component, event); }); return !event.defaultPrevented; } @@ -143,14 +146,93 @@ export function hasContext(key): boolean { return get_current_component().$$.context.has(key); } -// TODO figure out if we still want to support -// shorthand events, or if we want to implement -// a real bubbling mechanism -export function bubble(component, event) { - const callbacks = component.$$.callbacks[event.type]; +function start_bubble(type: string, bubble: Bubble, callback: Callback) { + const dispose = bubble.f(type, callback.f, callback.o); + if (dispose) { + bubble.r.set(callback, dispose); + } +} + +function start_bubbles(comp : SvelteComponent, bubble: Bubble) { + for (const type of Object.keys(comp.$$.callbacks)) { + comp.$$.callbacks[type].forEach( callback => { start_bubble(type, bubble, callback); }) + } +} + +export function start_callback(comp : SvelteComponent, type: string, callback: Callback) { + for (const bubbles of [ comp.$$.bubbles[type], comp.$$.bubbles['*'] ]) { + if (bubbles) { + for(const bubble of bubbles) { + start_bubble(type, bubble, callback); + } + } + } +} - if (callbacks) { - // @ts-ignore - callbacks.slice().forEach(fn => fn.call(this, event)); +export function stop_callback(comp : SvelteComponent, type: string, callback: Callback) { + for (const bubbles of [ comp.$$.bubbles[type], comp.$$.bubbles['*'] ]) { + if (bubbles) { + for (const bubble of bubbles) { + const dispose = bubble.r.get(callback); + if (dispose) { + dispose(); + bubble.r.delete(callback); + } + } + } } } + +function add_bubble(comp: SvelteComponent, type: string, f: CallbackFactory): Function { + const bubble : Bubble = {f, r: new Map()}; + const bubbles = (comp.$$.bubbles[type] || (comp.$$.bubbles[type] = [])); + bubbles.push(bubble); + + start_bubbles(comp, bubble); + + return () => { + const index = bubbles.indexOf(bubble); + if (index !== -1) bubbles.splice(index, 1); + for (const dispose of bubble.r.values()) { + dispose(); + } + } +} + +export function onEventListener(type: string, fn: CallbackFactory) { + add_bubble(get_current_component(), type, fn); +} + + +export function bubble(component: SvelteComponent, listen_func: Function, node: EventTarget|SvelteComponent, type: string, typeName: string = type): Function { + return add_bubble(component, type, (eventType, callback, options) => { + let typeToListen: string; + if (type === '*') { + if (typeName === '*') { + typeToListen = eventType; + } else if (typeName.startsWith('*')) { + const len = typeName.length; + if (eventType.endsWith(typeName.substring(1))) { + typeToListen = eventType.substring(0, eventType.length - (len-1)); + } + } else if (typeName.endsWith('*')) { + const len = typeName.length; + if (eventType.startsWith(typeName.substring(0,len-1))) { + typeToListen = eventType.substring(len-1); + } + } + } else if (eventType === typeName) { + typeToListen = type; + } + if (typeToListen) { + return listen_func(node, typeToListen, callback, options); + } + }); +} + +export function listen_comp(comp: SvelteComponent, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { + if (handler) { + return comp.$on(event, wrap_handler(handler, wrappers), options); + } + return noop; +} \ No newline at end of file diff --git a/src/runtime/internal/types.ts b/src/runtime/internal/types.ts index 41f8f1ca43fe..a957730ca363 100644 --- a/src/runtime/internal/types.ts +++ b/src/runtime/internal/types.ts @@ -19,12 +19,27 @@ export interface Fragment { export type FragmentFactory = (ctx: any) => Fragment; + +export interface Callback { + f: EventListener; + o?: boolean | AddEventListenerOptions | EventListenerOptions; +} + +export type CallbackFactory = (type: string, callback: EventListener, options: boolean | AddEventListenerOptions | EventListenerOptions | undefined) => Function|void; + +export interface Bubble { + f: CallbackFactory; + r: Map; +} + + export interface T$$ { dirty: number[]; ctx: any[]; bound: any; update: () => void; - callbacks: any; + callbacks: Record; + bubbles: Record; after_update: any[]; props: Record; fragment: null | false | Fragment; From 719f3a6b20e6e4aa8f68eb3ee6ea1a5b9f26ab77 Mon Sep 17 00:00:00 2001 From: adiguba Date: Thu, 2 Mar 2023 18:09:12 +0100 Subject: [PATCH 03/15] stopImmediatePropagation --- src/compiler/compile/nodes/EventHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/compiler/compile/nodes/EventHandler.ts b/src/compiler/compile/nodes/EventHandler.ts index 091e4ca77f9d..9e79f6340654 100644 --- a/src/compiler/compile/nodes/EventHandler.ts +++ b/src/compiler/compile/nodes/EventHandler.ts @@ -13,6 +13,7 @@ const regex_contains_term_function_expression = /FunctionExpression/; const valid_modifiers = new Set([ 'preventDefault', 'stopPropagation', + 'stopImmediatePropagation', 'capture', 'once', 'passive', From 617b9116ba2a8c1c3a5884c96d7fcc40784bf118 Mon Sep 17 00:00:00 2001 From: Fred Martini Date: Fri, 3 Mar 2023 10:30:09 +0100 Subject: [PATCH 04/15] fix: bubble alias --- src/runtime/internal/lifecycle.ts | 21 +++++++++++---------- src/runtime/internal/types.ts | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/runtime/internal/lifecycle.ts b/src/runtime/internal/lifecycle.ts index 2e381e669235..b5a29688a7d5 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -147,7 +147,7 @@ export function hasContext(key): boolean { } function start_bubble(type: string, bubble: Bubble, callback: Callback) { - const dispose = bubble.f(type, callback.f, callback.o); + const dispose = bubble.f(callback.f, callback.o, type); if (dispose) { bubble.r.set(callback, dispose); } @@ -205,9 +205,9 @@ export function onEventListener(type: string, fn: CallbackFactory) { export function bubble(component: SvelteComponent, listen_func: Function, node: EventTarget|SvelteComponent, type: string, typeName: string = type): Function { - return add_bubble(component, type, (eventType, callback, options) => { - let typeToListen: string; - if (type === '*') { + if (type === '*') { + return add_bubble(component, type, (callback, options, eventType) => { + let typeToListen: string = null; if (typeName === '*') { typeToListen = eventType; } else if (typeName.startsWith('*')) { @@ -221,12 +221,13 @@ export function bubble(component: SvelteComponent, listen_func: Function, node: typeToListen = eventType.substring(len-1); } } - } else if (eventType === typeName) { - typeToListen = type; - } - if (typeToListen) { - return listen_func(node, typeToListen, callback, options); - } + if (typeToListen) { + return listen_func(node, typeToListen, callback, options); + } + }); + } + return add_bubble(component, typeName, (callback, options) => { + return listen_func(node, type, callback, options); }); } diff --git a/src/runtime/internal/types.ts b/src/runtime/internal/types.ts index a957730ca363..4da02d1bf302 100644 --- a/src/runtime/internal/types.ts +++ b/src/runtime/internal/types.ts @@ -25,7 +25,7 @@ export interface Callback { o?: boolean | AddEventListenerOptions | EventListenerOptions; } -export type CallbackFactory = (type: string, callback: EventListener, options: boolean | AddEventListenerOptions | EventListenerOptions | undefined) => Function|void; +export type CallbackFactory = (callback: EventListener, options: boolean | AddEventListenerOptions | EventListenerOptions | undefined, type: string) => Function|void; export interface Bubble { f: CallbackFactory; From 492dd826666c4e185e67c6153e58a48a051df7e8 Mon Sep 17 00:00:00 2001 From: Fred Martini Date: Fri, 3 Mar 2023 15:09:11 +0100 Subject: [PATCH 05/15] fix ssr --- src/runtime/ssr.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/runtime/ssr.ts b/src/runtime/ssr.ts index cbac1f3ef855..122690a419b3 100644 --- a/src/runtime/ssr.ts +++ b/src/runtime/ssr.ts @@ -13,3 +13,4 @@ export { export function onMount() {} export function beforeUpdate() {} export function afterUpdate() {} +export function onEventListener() {} From d368f6799c54cdad7c7f4d766030651b92dd4e96 Mon Sep 17 00:00:00 2001 From: Fred Martini Date: Fri, 3 Mar 2023 15:17:24 +0100 Subject: [PATCH 06/15] false event --- elements/index.d.ts | 210 ++++++++++++++++++++++---------------------- 1 file changed, 107 insertions(+), 103 deletions(-) diff --git a/elements/index.d.ts b/elements/index.d.ts index d2de6767fff0..37995e245150 100644 --- a/elements/index.d.ts +++ b/elements/index.d.ts @@ -63,148 +63,152 @@ export type MessageEventHandler = EventHandler { + + // forward any event + 'on:*'?: never; + // Clipboard Events - 'on:copy'?: ClipboardEventHandler | undefined | null; - 'on:cut'?: ClipboardEventHandler | undefined | null; - 'on:paste'?: ClipboardEventHandler | undefined | null; + 'on:copy'?: ClipboardEventHandler | undefined | null | false; + 'on:cut'?: ClipboardEventHandler | undefined | null | false; + 'on:paste'?: ClipboardEventHandler | undefined | null | false; // Composition Events - 'on:compositionend'?: CompositionEventHandler | undefined | null; - 'on:compositionstart'?: CompositionEventHandler | undefined | null; - 'on:compositionupdate'?: CompositionEventHandler | undefined | null; + 'on:compositionend'?: CompositionEventHandler | undefined | null | false; + 'on:compositionstart'?: CompositionEventHandler | undefined | null | false; + 'on:compositionupdate'?: CompositionEventHandler | undefined | null | false; // Focus Events - 'on:focus'?: FocusEventHandler | undefined | null; - 'on:focusin'?: FocusEventHandler | undefined | null; - 'on:focusout'?: FocusEventHandler | undefined | null; - 'on:blur'?: FocusEventHandler | undefined | null; + 'on:focus'?: FocusEventHandler | undefined | null | false; + 'on:focusin'?: FocusEventHandler | undefined | null | false; + 'on:focusout'?: FocusEventHandler | undefined | null | false; + 'on:blur'?: FocusEventHandler | undefined | null | false; // Form Events - 'on:change'?: FormEventHandler | undefined | null; - 'on:beforeinput'?: EventHandler | undefined | null; - 'on:input'?: FormEventHandler | undefined | null; - 'on:reset'?: FormEventHandler | undefined | null; - 'on:submit'?: EventHandler | undefined | null; // TODO make this SubmitEvent once we require TS>=4.4 - 'on:invalid'?: EventHandler | undefined | null; - 'on:formdata'?: EventHandler | undefined | null; // TODO make this FormDataEvent once we require TS>=4.4 + 'on:change'?: FormEventHandler | undefined | null | false; + 'on:beforeinput'?: EventHandler | undefined | null | false; + 'on:input'?: FormEventHandler | undefined | null | false; + 'on:reset'?: FormEventHandler | undefined | null | false; + 'on:submit'?: EventHandler | undefined | null | false; // TODO make this SubmitEvent once we require TS>=4.4 + 'on:invalid'?: EventHandler | undefined | null | false; + 'on:formdata'?: EventHandler | undefined | null | false; // TODO make this FormDataEvent once we require TS>=4.4 // Image Events - 'on:load'?: EventHandler | undefined | null; - 'on:error'?: EventHandler | undefined | null; // also a Media Event + 'on:load'?: EventHandler | undefined | null | false; + 'on:error'?: EventHandler | undefined | null | false; // also a Media Event // Detail Events - 'on:toggle'?: EventHandler | undefined | null; + 'on:toggle'?: EventHandler | undefined | null | false; // Keyboard Events - 'on:keydown'?: KeyboardEventHandler | undefined | null; - 'on:keypress'?: KeyboardEventHandler | undefined | null; - 'on:keyup'?: KeyboardEventHandler | undefined | null; + 'on:keydown'?: KeyboardEventHandler | undefined | null | false; + 'on:keypress'?: KeyboardEventHandler | undefined | null | false; + 'on:keyup'?: KeyboardEventHandler | undefined | null | false; // Media Events - 'on:abort'?: EventHandler | undefined | null; - 'on:canplay'?: EventHandler | undefined | null; - 'on:canplaythrough'?: EventHandler | undefined | null; - 'on:cuechange'?: EventHandler | undefined | null; - 'on:durationchange'?: EventHandler | undefined | null; - 'on:emptied'?: EventHandler | undefined | null; - 'on:encrypted'?: EventHandler | undefined | null; - 'on:ended'?: EventHandler | undefined | null; - 'on:loadeddata'?: EventHandler | undefined | null; - 'on:loadedmetadata'?: EventHandler | undefined | null; - 'on:loadstart'?: EventHandler | undefined | null; - 'on:pause'?: EventHandler | undefined | null; - 'on:play'?: EventHandler | undefined | null; - 'on:playing'?: EventHandler | undefined | null; - 'on:progress'?: EventHandler | undefined | null; - 'on:ratechange'?: EventHandler | undefined | null; - 'on:seeked'?: EventHandler | undefined | null; - 'on:seeking'?: EventHandler | undefined | null; - 'on:stalled'?: EventHandler | undefined | null; - 'on:suspend'?: EventHandler | undefined | null; - 'on:timeupdate'?: EventHandler | undefined | null; - 'on:volumechange'?: EventHandler | undefined | null; - 'on:waiting'?: EventHandler | undefined | null; + 'on:abort'?: EventHandler | undefined | null | false; + 'on:canplay'?: EventHandler | undefined | null | false; + 'on:canplaythrough'?: EventHandler | undefined | null | false; + 'on:cuechange'?: EventHandler | undefined | null | false; + 'on:durationchange'?: EventHandler | undefined | null | false; + 'on:emptied'?: EventHandler | undefined | null | false; + 'on:encrypted'?: EventHandler | undefined | null | false; + 'on:ended'?: EventHandler | undefined | null | false; + 'on:loadeddata'?: EventHandler | undefined | null | false; + 'on:loadedmetadata'?: EventHandler | undefined | null | false; + 'on:loadstart'?: EventHandler | undefined | null | false; + 'on:pause'?: EventHandler | undefined | null | false; + 'on:play'?: EventHandler | undefined | null | false; + 'on:playing'?: EventHandler | undefined | null | false; + 'on:progress'?: EventHandler | undefined | null | false; + 'on:ratechange'?: EventHandler | undefined | null | false; + 'on:seeked'?: EventHandler | undefined | null | false; + 'on:seeking'?: EventHandler | undefined | null | false; + 'on:stalled'?: EventHandler | undefined | null | false; + 'on:suspend'?: EventHandler | undefined | null | false; + 'on:timeupdate'?: EventHandler | undefined | null | false; + 'on:volumechange'?: EventHandler | undefined | null | false; + 'on:waiting'?: EventHandler | undefined | null | false; // MouseEvents - 'on:auxclick'?: MouseEventHandler | undefined | null; - 'on:click'?: MouseEventHandler | undefined | null; - 'on:contextmenu'?: MouseEventHandler | undefined | null; - 'on:dblclick'?: MouseEventHandler | undefined | null; - 'on:drag'?: DragEventHandler | undefined | null; - 'on:dragend'?: DragEventHandler | undefined | null; - 'on:dragenter'?: DragEventHandler | undefined | null; - 'on:dragexit'?: DragEventHandler | undefined | null; - 'on:dragleave'?: DragEventHandler | undefined | null; - 'on:dragover'?: DragEventHandler | undefined | null; - 'on:dragstart'?: DragEventHandler | undefined | null; - 'on:drop'?: DragEventHandler | undefined | null; - 'on:mousedown'?: MouseEventHandler | undefined | null; - 'on:mouseenter'?: MouseEventHandler | undefined | null; - 'on:mouseleave'?: MouseEventHandler | undefined | null; - 'on:mousemove'?: MouseEventHandler | undefined | null; - 'on:mouseout'?: MouseEventHandler | undefined | null; - 'on:mouseover'?: MouseEventHandler | undefined | null; - 'on:mouseup'?: MouseEventHandler | undefined | null; + 'on:auxclick'?: MouseEventHandler | undefined | null | false; + 'on:click'?: MouseEventHandler | undefined | null | false; + 'on:contextmenu'?: MouseEventHandler | undefined | null | false; + 'on:dblclick'?: MouseEventHandler | undefined | null | false; + 'on:drag'?: DragEventHandler | undefined | null | false; + 'on:dragend'?: DragEventHandler | undefined | null | false; + 'on:dragenter'?: DragEventHandler | undefined | null | false; + 'on:dragexit'?: DragEventHandler | undefined | null | false; + 'on:dragleave'?: DragEventHandler | undefined | null | false; + 'on:dragover'?: DragEventHandler | undefined | null | false; + 'on:dragstart'?: DragEventHandler | undefined | null | false; + 'on:drop'?: DragEventHandler | undefined | null | false; + 'on:mousedown'?: MouseEventHandler | undefined | null | false; + 'on:mouseenter'?: MouseEventHandler | undefined | null | false; + 'on:mouseleave'?: MouseEventHandler | undefined | null | false; + 'on:mousemove'?: MouseEventHandler | undefined | null | false; + 'on:mouseout'?: MouseEventHandler | undefined | null | false; + 'on:mouseover'?: MouseEventHandler | undefined | null | false; + 'on:mouseup'?: MouseEventHandler | undefined | null | false; // Selection Events - 'on:select'?: EventHandler | undefined | null; - 'on:selectionchange'?: EventHandler | undefined | null; - 'on:selectstart'?: EventHandler | undefined | null; + 'on:select'?: EventHandler | undefined | null | false; + 'on:selectionchange'?: EventHandler | undefined | null | false; + 'on:selectstart'?: EventHandler | undefined | null | false; // Touch Events - 'on:touchcancel'?: TouchEventHandler | undefined | null; - 'on:touchend'?: TouchEventHandler | undefined | null; - 'on:touchmove'?: TouchEventHandler | undefined | null; - 'on:touchstart'?: TouchEventHandler | undefined | null; + 'on:touchcancel'?: TouchEventHandler | undefined | null | false; + 'on:touchend'?: TouchEventHandler | undefined | null | false; + 'on:touchmove'?: TouchEventHandler | undefined | null | false; + 'on:touchstart'?: TouchEventHandler | undefined | null | false; // Pointer Events - 'on:gotpointercapture'?: PointerEventHandler | undefined | null; - 'on:pointercancel'?: PointerEventHandler | undefined | null; - 'on:pointerdown'?: PointerEventHandler | undefined | null; - 'on:pointerenter'?: PointerEventHandler | undefined | null; - 'on:pointerleave'?: PointerEventHandler | undefined | null; - 'on:pointermove'?: PointerEventHandler | undefined | null; - 'on:pointerout'?: PointerEventHandler | undefined | null; - 'on:pointerover'?: PointerEventHandler | undefined | null; - 'on:pointerup'?: PointerEventHandler | undefined | null; - 'on:lostpointercapture'?: PointerEventHandler | undefined | null; + 'on:gotpointercapture'?: PointerEventHandler | undefined | null | false; + 'on:pointercancel'?: PointerEventHandler | undefined | null | false; + 'on:pointerdown'?: PointerEventHandler | undefined | null | false; + 'on:pointerenter'?: PointerEventHandler | undefined | null | false; + 'on:pointerleave'?: PointerEventHandler | undefined | null | false; + 'on:pointermove'?: PointerEventHandler | undefined | null | false; + 'on:pointerout'?: PointerEventHandler | undefined | null | false; + 'on:pointerover'?: PointerEventHandler | undefined | null | false; + 'on:pointerup'?: PointerEventHandler | undefined | null | false; + 'on:lostpointercapture'?: PointerEventHandler | undefined | null | false; // UI Events - 'on:scroll'?: UIEventHandler | undefined | null; - 'on:resize'?: UIEventHandler | undefined | null; + 'on:scroll'?: UIEventHandler | undefined | null | false; + 'on:resize'?: UIEventHandler | undefined | null | false; // Wheel Events - 'on:wheel'?: WheelEventHandler | undefined | null; + 'on:wheel'?: WheelEventHandler | undefined | null | false; // Animation Events - 'on:animationstart'?: AnimationEventHandler | undefined | null; - 'on:animationend'?: AnimationEventHandler | undefined | null; - 'on:animationiteration'?: AnimationEventHandler | undefined | null; + 'on:animationstart'?: AnimationEventHandler | undefined | null | false; + 'on:animationend'?: AnimationEventHandler | undefined | null | false; + 'on:animationiteration'?: AnimationEventHandler | undefined | null | false; // Transition Events - 'on:transitionstart'?: TransitionEventHandler | undefined | null; - 'on:transitionrun'?: TransitionEventHandler | undefined | null; - 'on:transitionend'?: TransitionEventHandler | undefined | null; - 'on:transitioncancel'?: TransitionEventHandler | undefined | null; + 'on:transitionstart'?: TransitionEventHandler | undefined | null | false; + 'on:transitionrun'?: TransitionEventHandler | undefined | null | false; + 'on:transitionend'?: TransitionEventHandler | undefined | null | false; + 'on:transitioncancel'?: TransitionEventHandler | undefined | null | false; // Svelte Transition Events - 'on:outrostart'?: EventHandler, T> | undefined | null; - 'on:outroend'?: EventHandler, T> | undefined | null; - 'on:introstart'?: EventHandler, T> | undefined | null; - 'on:introend'?: EventHandler, T> | undefined | null; + 'on:outrostart'?: EventHandler, T> | undefined | null | false; + 'on:outroend'?: EventHandler, T> | undefined | null | false; + 'on:introstart'?: EventHandler, T> | undefined | null | false; + 'on:introend'?: EventHandler, T> | undefined | null | false; // Message Events - 'on:message'?: MessageEventHandler | undefined | null; - 'on:messageerror'?: MessageEventHandler | undefined | null; + 'on:message'?: MessageEventHandler | undefined | null | false; + 'on:messageerror'?: MessageEventHandler | undefined | null | false; // Document Events - 'on:visibilitychange'?: EventHandler | undefined | null; + 'on:visibilitychange'?: EventHandler | undefined | null | false; // Global Events - 'on:cancel'?: EventHandler | undefined | null; - 'on:close'?: EventHandler | undefined | null; - 'on:fullscreenchange'?: EventHandler | undefined | null; - 'on:fullscreenerror'?: EventHandler | undefined | null; + 'on:cancel'?: EventHandler | undefined | null | false; + 'on:close'?: EventHandler | undefined | null | false; + 'on:fullscreenchange'?: EventHandler | undefined | null | false; + 'on:fullscreenerror'?: EventHandler | undefined | null | false; } // All the WAI-ARIA 1.1 attributes from https://www.w3.org/TR/wai-aria-1.1/ From 301dbc3b8f5da83be21377db7c9adb44ee46a205 Mon Sep 17 00:00:00 2001 From: Fred Martini Date: Fri, 3 Mar 2023 15:17:42 +0100 Subject: [PATCH 07/15] rewrite --- src/compiler/compile/render_dom/Block.ts | 19 +++++++++++++++++++ .../wrappers/Element/EventHandler.ts | 14 ++++---------- src/runtime/internal/dom.ts | 10 +++++----- src/runtime/internal/lifecycle.ts | 2 +- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts index 164e938674ad..2e2568d15652 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -4,6 +4,7 @@ import { b, x } from 'code-red'; import { Node, Identifier, ArrayPattern } from 'estree'; import { is_head } from './wrappers/shared/is_head'; import { regex_double_quotes } from '../../utils/patterns'; +import { Expression } from 'estree'; export interface Bindings { object: Identifier; @@ -58,6 +59,7 @@ export default class Block { destroy: Array; }; + event_updaters: ({condition:Expression, snippet:Node, index:number})[] = []; event_listeners: Node[] = []; maintain_context: boolean; @@ -481,6 +483,15 @@ export default class Block { ` ); + if (this.event_updaters.length === 1) { + const {condition, snippet} = this.event_updaters[0]; + this.chunks.update.push(b` + if (${condition}) { + ${dispose}.swap(${snippet}) + }` + ); + } + this.chunks.destroy.push( b`${dispose}();` ); @@ -494,6 +505,14 @@ export default class Block { } `); + for (const {condition, snippet, index} of this.event_updaters) { + this.chunks.update.push(b` + if (${condition}) { + ${dispose}[${index}].swap(${snippet}) + }` + ); + } + this.chunks.destroy.push( b`@run_all(${dispose});` ); diff --git a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts index 7fda7672a40c..99488ad850ea 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts @@ -1,9 +1,8 @@ import EventHandler from '../../../nodes/EventHandler'; import Wrapper from '../shared/Wrapper'; import Block from '../../Block'; -import { x, p, b } from 'code-red'; +import { x, p } from 'code-red'; import { Expression } from 'estree'; -import { sanitize } from '../../../../utils/names'; const TRUE = x`true`; const FALSE = x`false`; @@ -67,17 +66,12 @@ export default class EventHandlerWrapper { } if (this.node.reassigned) { - const handle = this.node.component.get_unique_name(`${sanitize(this.node.name)}_handle`); - block.add_variable(handle); - + const index = block.event_listeners.length; const condition = block.renderer.dirty(this.node.expression.dynamic_dependencies()); - block.chunks.update.push(b` - if (${condition}) { - ${handle}.swap(${snippet}) - }`); + block.event_updaters.push({condition, snippet, index}); block.event_listeners.push( - x`${handle} = @listen_swap(${snippet}, (h)=> ${listen}(${target}, "${this.node.name}", h, ${args}))` + x`@listen_swap(${snippet}, (h)=> ${listen}(${target}, "${this.node.name}", h, ${args}))` ); } else { block.event_listeners.push( diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 1078daf539be..abdc28d8ecfe 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -264,16 +264,16 @@ export function wrap_handler(handler: EventListenerOrEventListenerObject, wrappe return result; } -export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject|null|undefined, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { +export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject|null|undefined|false, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { if (handler) { - handler = wrap_handler(handler, wrappers); - node.addEventListener(event, handler, options); - return () => node.removeEventListener(event, handler, options); + const h = wrap_handler(handler, wrappers); + node.addEventListener(event, h, options); + return () => node.removeEventListener(event, h, options); } return noop; } -export function listen_swap(handler: EventListenerOrEventListenerObject|null|undefined, factory: (handler:EventListenerOrEventListenerObject) => Function) { +export function listen_swap(handler: EventListenerOrEventListenerObject|null|undefined|false, factory: (handler:EventListenerOrEventListenerObject|null|undefined|false) => Function) { let disposeHandle: Function = factory(handler); const dispose = () => { disposeHandle(); diff --git a/src/runtime/internal/lifecycle.ts b/src/runtime/internal/lifecycle.ts index b5a29688a7d5..b8d2dbd083a4 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -231,7 +231,7 @@ export function bubble(component: SvelteComponent, listen_func: Function, node: }); } -export function listen_comp(comp: SvelteComponent, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { +export function listen_comp(comp: SvelteComponent, event: string, handler: EventListenerOrEventListenerObject|null|undefined|false, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { if (handler) { return comp.$on(event, wrap_handler(handler, wrappers), options); } From 14609492d1ae50bc40beef7ffae9c0dfc4d5c9e0 Mon Sep 17 00:00:00 2001 From: Fred Martini Date: Fri, 3 Mar 2023 17:10:51 +0100 Subject: [PATCH 08/15] optimize --- src/compiler/compile/render_dom/Block.ts | 17 +++++------------ .../render_dom/wrappers/Element/EventHandler.ts | 4 ++-- src/runtime/internal/dom.ts | 15 +++++++++------ 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts index 2e2568d15652..55951c961f0d 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -59,7 +59,7 @@ export default class Block { destroy: Array; }; - event_updaters: ({condition:Expression, snippet:Node, index:number})[] = []; + event_updaters: ({condition:Expression, index:number})[] = []; event_listeners: Node[] = []; maintain_context: boolean; @@ -484,12 +484,9 @@ export default class Block { ); if (this.event_updaters.length === 1) { - const {condition, snippet} = this.event_updaters[0]; + const {condition} = this.event_updaters[0]; this.chunks.update.push(b` - if (${condition}) { - ${dispose}.swap(${snippet}) - }` - ); + if (${condition}) ${dispose}.p()`); } this.chunks.destroy.push( @@ -505,12 +502,8 @@ export default class Block { } `); - for (const {condition, snippet, index} of this.event_updaters) { - this.chunks.update.push(b` - if (${condition}) { - ${dispose}[${index}].swap(${snippet}) - }` - ); + for (const {condition, index} of this.event_updaters) { + this.chunks.update.push(b` if (${condition}) ${dispose}[${index}].p()`); } this.chunks.destroy.push( diff --git a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts index 99488ad850ea..cd313e4c1a40 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts @@ -69,9 +69,9 @@ export default class EventHandlerWrapper { const index = block.event_listeners.length; const condition = block.renderer.dirty(this.node.expression.dynamic_dependencies()); - block.event_updaters.push({condition, snippet, index}); + block.event_updaters.push({condition, index}); block.event_listeners.push( - x`@listen_swap(${snippet}, (h)=> ${listen}(${target}, "${this.node.name}", h, ${args}))` + x`@listen_and_update( () => (${snippet}), (h) => ${listen}(${target}, "${this.node.name}", h, ${args}))` ); } else { block.event_listeners.push( diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index abdc28d8ecfe..5172221f8bf7 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -273,16 +273,19 @@ export function listen(node: EventTarget, event: string, handler: EventListenerO return noop; } -export function listen_swap(handler: EventListenerOrEventListenerObject|null|undefined|false, factory: (handler:EventListenerOrEventListenerObject|null|undefined|false) => Function) { - let disposeHandle: Function = factory(handler); +export function listen_and_update(get_handler: ()=>EventListenerOrEventListenerObject|null|undefined|false, factory: (handler:EventListenerOrEventListenerObject|null|undefined|false) => Function) { + let handler = get_handler(); + let dispose_handle: Function = factory(handler); const dispose = () => { - disposeHandle(); + dispose_handle(); } - dispose.swap = (new_handler:EventListenerOrEventListenerObject) => { + // update : + dispose.p = () => { + const new_handler = get_handler(); if (new_handler !== handler) { - disposeHandle(); + dispose_handle(); handler = new_handler; - disposeHandle = factory(handler); + dispose_handle = factory(handler); } } return dispose; From c18e372369cabda135d039a27cb56456f21b727a Mon Sep 17 00:00:00 2001 From: adiguba Date: Fri, 3 Mar 2023 19:00:11 +0100 Subject: [PATCH 09/15] warnings/errors --- src/compiler/compile/compiler_errors.ts | 10 +++++++--- src/compiler/compile/compiler_warnings.ts | 8 ++++---- src/compiler/compile/nodes/EventHandler.ts | 20 ++++++++++++++++++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index 2b8b9af9b52f..64da987da17f 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -310,8 +310,12 @@ export default { code: 'directive-conflict', message: `Cannot use ${directive1} and ${directive2} on the same element` }), - too_much_forward_event_modifiers: { - code: 'too-much-forward-event-modifiers', - message: 'Forward-event only accept one modifier (the forward alias)' + invalid_forward_event_alias_count: { + code: 'invalid-forward-event-alias-count', + message: 'Forward-event accept only one modifier (the event alias)' + }, + invalid_forward_event_alias_any: { + code: 'invalid-forward-event-alias-any', + message: 'The alias for on:* must be one of the following form: "prefix*" or "*suffix"' }, }; diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index 08ef640d90e6..07d288a27b7f 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -214,8 +214,8 @@ export default { code: 'invalid-rest-eachblock-binding', message: `...${rest_element_name} operator will create a new object and binding propogation with original object will not work` }), - incorrect_forward_event_modifier: (modifiers: Set) => ({ - code: 'incorrect-forward-event-modifier', - message: `Forward-event only accept one modifier for the forward alias. Event modifiers should not be used here : ${modifiers}` - }) + invalid_forward_event_alias: { + code: 'invalid-forward-event-alias', + message: 'Forward-event accept only one modifier : the event alias name' + } }; diff --git a/src/compiler/compile/nodes/EventHandler.ts b/src/compiler/compile/nodes/EventHandler.ts index 9e79f6340654..ac759f166f19 100644 --- a/src/compiler/compile/nodes/EventHandler.ts +++ b/src/compiler/compile/nodes/EventHandler.ts @@ -30,6 +30,19 @@ const passive_events = new Set([ 'touchcancel' ]); + +function is_valid_any_alias_name(alias: string) { + if (alias === '*') { + return true; + } + let idx = alias.indexOf('*'); + if (idx < 0) return false; + if (idx !== alias.lastIndexOf('*')) { + return false; + } + return idx === 0 || alias.endsWith('*'); +} + export default class EventHandler extends Node { type: 'EventHandler'; name: string; @@ -115,10 +128,13 @@ export default class EventHandler extends Node { } } else { if (this.aliasCount > 1) { - return this.component.error(this, compiler_errors.too_much_forward_event_modifiers); + return this.component.error(this, compiler_errors.invalid_forward_event_alias_count); } if (this.aliasName && valid_modifiers.has(this.aliasName)) { - this.component.warn(this, compiler_warnings.incorrect_forward_event_modifier(valid_modifiers)); + this.component.warn(this, compiler_warnings.invalid_forward_event_alias); + } + if (this.name === '*' && !is_valid_any_alias_name(this.aliasName)) { + return this.component.error(this, compiler_errors.invalid_forward_event_alias_any); } } } From ae57475f8b3299215b8ae008934abc7d426736a8 Mon Sep 17 00:00:00 2001 From: adiguba Date: Sat, 4 Mar 2023 09:08:56 +0100 Subject: [PATCH 10/15] rollback to upstream/master --- src/compiler/compile/compiler_errors.ts | 24 +-- src/compiler/compile/nodes/Element.ts | 30 +--- src/compiler/compile/nodes/InlineComponent.ts | 3 - src/compiler/compile/nodes/SvelteDirective.ts | 22 --- src/compiler/compile/nodes/interfaces.ts | 2 - .../render_dom/wrappers/Element/index.ts | 158 ++++++++---------- .../compile/render_ssr/handlers/Element.ts | 5 - src/compiler/interfaces.ts | 3 +- src/compiler/parse/state/tag.ts | 12 +- src/runtime/internal/dom.ts | 4 - 10 files changed, 74 insertions(+), 189 deletions(-) delete mode 100644 src/compiler/compile/nodes/SvelteDirective.ts diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index c5e00be89a5d..b0ffead6e023 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -286,28 +286,8 @@ export default { code: 'invalid-component-style-directive', message: 'Style directives cannot be used on components' }, - invalid_style_directive_modifier: (valid: string) => ({ + invalid_style_directive_modifier: (valid: string) => ({ code: 'invalid-style-directive-modifier', message: `Valid modifiers for style directives are: ${valid}` - }), - invalid_component_svelte_directive: (name) => ({ - code: 'invalid-component-svelte-directive', - message: `svelte:${name} directives cannot be used on components` - }), - duplicate_directive: (directive) => ({ - code: 'duplicate-directive', - message: `An element can only have one '${directive}' directive` - }), - invalid_directive: (directive) => ({ - code: 'invalid-directive', - message: `'${directive}' is not a valid directive` - }), - invalid_modifier: { - code: 'invalid-modifier', - message: 'No modifier allowed on this directive' - }, - directive_conflict: (directive1, directive2) => ({ - code: 'directive-conflict', - message: `Cannot use ${directive1} and ${directive2} on the same element` - }) + }) }; diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index bc7000e38afd..416f1d7b3169 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -25,7 +25,6 @@ import compiler_warnings from '../compiler_warnings'; import compiler_errors from '../compiler_errors'; import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query'; import { is_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element } from '../utils/a11y'; -import SvelteDirective from './SvelteDirective'; const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' '); const aria_attribute_set = new Set(aria_attributes); @@ -286,7 +285,6 @@ export default class Element extends Node { intro?: Transition = null; outro?: Transition = null; animation?: Animation = null; - display? : SvelteDirective = null; children: INode[]; namespace: string; needs_manual_style_scoping: boolean; @@ -427,22 +425,6 @@ export default class Element extends Node { this.animation = new Animation(component, this, scope, node); break; - case 'SvelteDirective': - switch (node.name) { - case 'display': - if (this.display) { - component.error(node, - compiler_errors.duplicate_directive('svelte:' + node.name)); - } else { - this.display = new SvelteDirective(component, this, scope, node); - } - break; - default: - component.error(node, - compiler_errors.invalid_directive('svelte:' + node.name)); - } - break; - default: throw new Error(`Not implemented: ${node.type}`); } @@ -473,7 +455,7 @@ export default class Element extends Node { this.validate_bindings(); this.validate_content(); } - this.validate_display_directive(); + } validate_attributes() { @@ -1099,16 +1081,6 @@ export default class Element extends Node { }); } - validate_display_directive() { - if (!this.display) { - return; - } - if (this.styles.find(d => d.name === 'display')) { - this.component.error(this.display, - compiler_errors.directive_conflict('svelte:display', 'style:display')); - } - } - is_media_node() { return this.name === 'audio' || this.name === 'video'; } diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index 62ba607f94ad..e9ac86a55cf5 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -77,9 +77,6 @@ export default class InlineComponent extends Node { case 'StyleDirective': return component.error(node, compiler_errors.invalid_component_style_directive); - - case 'SvelteDirective': - return component.error(node, compiler_errors.invalid_component_svelte_directive(node.name)); default: throw new Error(`Not implemented: ${node.type}`); diff --git a/src/compiler/compile/nodes/SvelteDirective.ts b/src/compiler/compile/nodes/SvelteDirective.ts deleted file mode 100644 index 03d4b0199c0b..000000000000 --- a/src/compiler/compile/nodes/SvelteDirective.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Node from './shared/Node'; -import Expression from './shared/Expression'; -import Component from '../Component'; -import TemplateScope from './shared/TemplateScope'; -import { TemplateNode } from '../../interfaces'; -import Element from './Element'; -import compiler_errors from '../compiler_errors'; - -export default class SvelteDirective extends Node { - type: 'SvelteDirective'; - expression: Expression; - - constructor(component: Component, parent: Element, scope: TemplateScope, info: TemplateNode) { - super(component, parent, scope, info); - - this.expression = new Expression(component, this, scope, info.expression); - - if (info.modifiers && info.modifiers.length) { - component.error(info, compiler_errors.invalid_modifier); - } - } -} diff --git a/src/compiler/compile/nodes/interfaces.ts b/src/compiler/compile/nodes/interfaces.ts index 0b8046aa0dc8..f023cad25c9f 100644 --- a/src/compiler/compile/nodes/interfaces.ts +++ b/src/compiler/compile/nodes/interfaces.ts @@ -33,7 +33,6 @@ import ThenBlock from './ThenBlock'; import Title from './Title'; import Transition from './Transition'; import Window from './Window'; -import SvelteDisplayDirective from './SvelteDirective'; // note: to write less types each of types in union below should have type defined as literal // https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions @@ -65,7 +64,6 @@ export type INode = Action | Slot | SlotTemplate | StyleDirective -| SvelteDisplayDirective | Tag | Text | ThenBlock diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 0562fc6d897c..eaef3a3b6949 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -191,7 +191,7 @@ export default class ElementWrapper extends Wrapper { this.class_dependencies = []; - if ((node.display || node.is_dynamic_element) && block.type !== CHILD_DYNAMIC_ELEMENT_BLOCK) { + if (node.is_dynamic_element && block.type !== CHILD_DYNAMIC_ELEMENT_BLOCK) { this.child_dynamic_element_block = block.child({ comment: create_debugging_comment(node, renderer.component), name: renderer.component.get_unique_name('create_dynamic_element'), @@ -303,13 +303,12 @@ export default class ElementWrapper extends Wrapper { const is_tag_dynamic = this.node.tag_expr.dynamic_dependencies().length > 0; const tag = this.node.tag_expr.manipulate(block); - if (this.renderer.options.dev) { - block.chunks.init.push(b` - @validate_dynamic_element(${tag}); - @validate_void_dynamic_element(${tag}); - `); - } - + block.chunks.init.push(b` + ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} + ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} + let ${this.var} = ${tag} && ${this.child_dynamic_element_block.name}(#ctx); + `); + block.chunks.create.push(b` if (${this.var}) ${this.var}.c(); `); @@ -324,53 +323,72 @@ export default class ElementWrapper extends Wrapper { if (${this.var}) ${this.var}.m(${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'}); `); - if (this.node.display && tag.type === 'Literal') { - block.chunks.init.push(b`const ${this.var} = ${this.child_dynamic_element_block.name}(#ctx);`); - block.chunks.update.push(b`${this.var}.p(#ctx, #dirty);`); - } else { + if (is_tag_dynamic) { const previous_tag = block.get_unique_name('previous_tag'); block.add_variable(previous_tag, tag); - block.chunks.init.push(b`let ${this.var} = ${tag} && ${this.child_dynamic_element_block.name}(#ctx);`); - - const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes); - const has_transitions = !!(this.node.intro || this.node.outro); - const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`; - - block.chunks.update.push(b` - if (${tag}) { - if (!${previous_tag}) { - ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); - ${this.var}.c(); - ${has_transitions && b`@transition_in(${this.var})`} - ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); - } else if (${not_equal}(${previous_tag}, ${tag})) { - ${this.var}.d(1); - ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} - ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} - ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); - ${this.var}.c(); - ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); - } else { - ${this.var}.p(#ctx, #dirty); - } - } else if (${previous_tag}) { - ${ - has_transitions - ? b` - @group_outros(); - @transition_out(${this.var}, 1, 1, () => { + const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes); + const has_transitions = !!(this.node.intro || this.node.outro); + const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`; + + const tag_will_be_removed = block.get_unique_name('tag_will_be_removed'); + if (has_transitions) { + block.add_variable(tag_will_be_removed, x`false`); + } + + block.chunks.update.push(b` + if (${tag}) { + if (!${previous_tag}) { + ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); + ${previous_tag} = ${tag}; + ${this.var}.c(); + ${has_transitions && b`@transition_in(${this.var})`} + ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); + } else if (${not_equal}(${previous_tag}, ${tag})) { + ${this.var}.d(1); + ${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`} + ${this.renderer.options.dev && this.node.children.length > 0 && b`@validate_void_dynamic_element(${tag});`} + ${this.var} = ${this.child_dynamic_element_block.name}(#ctx); + ${previous_tag} = ${tag}; + ${this.var}.c(); + ${has_transitions && b`if (${tag_will_be_removed}) { + ${tag_will_be_removed} = false; + @transition_in(${this.var}) + }`} + ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor}); + } else { + ${has_transitions && b`if (${tag_will_be_removed}) { + ${tag_will_be_removed} = false; + @transition_in(${this.var}) + }`} + ${this.var}.p(#ctx, #dirty); + } + } else if (${previous_tag}) { + ${has_transitions + ? b` + ${tag_will_be_removed} = true; + @group_outros(); + @transition_out(${this.var}, 1, 1, () => { + ${this.var} = null; + ${previous_tag} = ${tag}; + ${tag_will_be_removed} = false; + }); + @check_outros(); + ` + : b` + ${this.var}.d(1); ${this.var} = null; - }); - @check_outros(); - ` - : b` - ${this.var}.d(1); - ${this.var} = null; - ` + ${previous_tag} = ${tag}; + ` } - } - ${previous_tag} = ${tag}; - `); + } + `); + } else { + block.chunks.update.push(b` + if (${tag}) { + ${this.var}.p(#ctx, #dirty); + } + `); + } if (this.child_dynamic_element_block.has_intros) { block.chunks.intro.push(b`@transition_in(${this.var});`); @@ -519,7 +537,6 @@ export default class ElementWrapper extends Wrapper { this.add_animation(block); this.add_classes(block); this.add_styles(block); - this.add_display(block); this.add_manual_style_scoping(block); if (nodes && this.renderer.options.hydratable && !this.void) { @@ -1188,43 +1205,6 @@ export default class ElementWrapper extends Wrapper { }); } - add_display(block: Block) { - const display = this.node.display; - if (display === null) { - return; - } - - const snippet = display.expression.manipulate(block); - const dependencies = display.expression.dynamic_dependencies(); - const has_dependancies = dependencies.length > 0; - - const update_display = b`@set_display(${this.var}, ${snippet})`; - block.chunks.hydrate.push(update_display); - - if (has_dependancies) { - const update_current = (this.node.intro || this.node.outro) - ? x`#current = false` - : null; - - const dirty = block.renderer.dirty(Array.from(dependencies)); - block.chunks.update.push(b` - if (${dirty}) { - if (${snippet}) { - ${update_current} - ${update_display} - @transition_in(this, 1); - } else { - @group_outros(); - @transition_out(this, 1, 0, () => { - ${update_display} - }); - @check_outros(); - } - } - `); - } - } - add_manual_style_scoping(block: Block) { if (this.node.needs_manual_style_scoping) { const updater = b`@toggle_class(${this.var}, "${this.node.component.stylesheet.id}", true);`; diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index 0d416f779dd3..f1dd86dd2bb7 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -51,11 +51,6 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio return p`"${name}": ${expression}`; }); - if (node.display) { - const snippet = node.display.expression.node; - style_expression_list.push(p`"display": (${snippet} ? "none !important" : null)`); - } - const style_expression = style_expression_list.length > 0 && x`{ ${style_expression_list} }`; diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index ae93eb069eb6..402f9ff5e143 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -48,8 +48,7 @@ export type DirectiveType = 'Action' | 'EventHandler' | 'Let' | 'Ref' -| 'Transition' -| 'SvelteDirective'; +| 'Transition'; interface BaseDirective extends BaseNode { type: DirectiveType; diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index 7a4c8ccb6eaa..cbd3213f531b 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -294,15 +294,6 @@ function read_tag_name(parser: Parser) { return name; } -function use_name_as_expression(type:string, name:string):boolean { - if (type === 'Binding' || type === 'Class') { - return true; - } else if (type === 'SvelteDirective') { - return name === 'display'; - } - return false; -} - // eslint-disable-next-line no-useless-escape const regex_token_ending_character = /[\s=\/>"']/; const regex_starts_with_quote_characters = /^["']/; @@ -440,7 +431,7 @@ function read_attribute(parser: Parser, unique_names: Set) { } // Directive name is expression, e.g.

- if (!directive.expression && use_name_as_expression(type, directive_name)) { + if (!directive.expression && (type === 'Binding' || type === 'Class')) { directive.expression = { start: directive.start + colon_index + 1, end: directive.end, @@ -473,7 +464,6 @@ function get_directive_type(name: string): DirectiveType { if (name === 'let') return 'Let'; if (name === 'ref') return 'Ref'; if (name === 'in' || name === 'out' || name === 'transition') return 'Transition'; - if (name === 'svelte') return 'SvelteDirective'; } function read_attribute_value(parser: Parser) { diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 35cd6d6cc5b0..70e7dcd7f8f5 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -606,10 +606,6 @@ export function set_style(node, key, value, important) { } } -export function set_display(node, value) { - set_style(node, 'display', value ? null : 'none', 1); -} - export function select_option(select, value) { for (let i = 0; i < select.options.length; i += 1) { const option = select.options[i]; From 402ef4d50330a4886d9f84192cef5ece0a7b8738 Mon Sep 17 00:00:00 2001 From: adiguba Date: Sat, 4 Mar 2023 14:49:18 +0100 Subject: [PATCH 11/15] rewrite --- src/runtime/internal/Component.ts | 36 +++--------------------- src/runtime/internal/lifecycle.ts | 46 +++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 2c9c4af3d07d..b0db9040eac9 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -1,9 +1,9 @@ import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler'; -import { current_component, set_current_component, start_callback, stop_callback } from './lifecycle'; +import { add_callback, current_component, set_current_component } from './lifecycle'; import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; import { children, detach, start_hydrating, end_hydrating } from './dom'; import { transition_in } from './transitions'; -import { Callback, T$$ } from './types'; +import { T$$ } from './types'; export function bind(component, name, callback) { const index = component.$$.props[name]; @@ -181,22 +181,7 @@ if (typeof HTMLElement === 'function') { } $on(type: string, callback: EventListener, options?: boolean | AddEventListenerOptions | EventListenerOptions) { - if (!is_function(callback)) { - return noop; - } - const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); - const c: Callback = {f:callback, o:options}; - callbacks.push(c); - - start_callback(this as any, type, c); - - return () => { - const index = callbacks.indexOf(c); - if (index !== -1) { - callbacks.splice(index, 1); - stop_callback(this as any, type, c); - } - }; + return add_callback(this, type, callback, options); } $set($$props) { @@ -222,20 +207,7 @@ export class SvelteComponent { } $on(type: string, callback: EventListener, options?: boolean | AddEventListenerOptions | EventListenerOptions) { - if (!is_function(callback)) { - return noop; - } - const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); - const c: Callback = {f:callback, o:options}; - callbacks.push(c); - - start_callback(this as any, type, c); - - return () => { - const index = callbacks.indexOf(c); - if (index !== -1) callbacks.splice(index, 1); - stop_callback(this as any, type, c); - }; + return add_callback(this, type, callback, options); } $set($$props) { diff --git a/src/runtime/internal/lifecycle.ts b/src/runtime/internal/lifecycle.ts index b8d2dbd083a4..83aef3e0e455 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -1,7 +1,7 @@ import { SvelteComponent } from './Component'; import { custom_event, wrap_handler } from './dom'; import { Bubble, Callback, CallbackFactory } from './types'; -import { noop } from './utils'; +import { is_function, noop } from './utils'; export let current_component; @@ -159,7 +159,15 @@ function start_bubbles(comp : SvelteComponent, bubble: Bubble) { } } -export function start_callback(comp : SvelteComponent, type: string, callback: Callback) { +export function restart_all_callback(comp : SvelteComponent) { + for (const type of Object.keys(comp.$$.callbacks)) { + for (const callback of comp.$$.callbacks[type]) { + start_callback(comp, type, callback); + } + } +} + +function start_callback(comp : SvelteComponent, type: string, callback: Callback) { for (const bubbles of [ comp.$$.bubbles[type], comp.$$.bubbles['*'] ]) { if (bubbles) { for(const bubble of bubbles) { @@ -169,7 +177,28 @@ export function start_callback(comp : SvelteComponent, type: string, callback: C } } -export function stop_callback(comp : SvelteComponent, type: string, callback: Callback) { +export function add_callback(comp : SvelteComponent, type: string, f: EventListener, o?: boolean | AddEventListenerOptions | EventListenerOptions) { + if (!is_function(f)) { + return noop; + } + + const callbacks = (comp.$$.callbacks[type] || (comp.$$.callbacks[type] = [])); + const callback: Callback = {f, o}; + + if (o && (o as any)?.once === true) { + callback.f = function () { + const r = f.apply(this, arguments); + remove_callback(comp, type, callback); + return r; + }; + } + + callbacks.push(callback); + start_callback(comp, type, callback); + return () => remove_callback(comp, type, callback); +} + +function stop_callback(comp : SvelteComponent, type: string, callback: Callback) { for (const bubbles of [ comp.$$.bubbles[type], comp.$$.bubbles['*'] ]) { if (bubbles) { for (const bubble of bubbles) { @@ -183,6 +212,17 @@ export function stop_callback(comp : SvelteComponent, type: string, callback: Ca } } +function remove_callback(comp : SvelteComponent, type: string, callback: Callback) { + const callbacks = comp.$$.callbacks[type]; + if (callbacks) { + const index = callbacks.indexOf(callback); + if (index !== -1) { + callbacks.splice(index, 1); + stop_callback(comp, type, callback); + } + } +} + function add_bubble(comp: SvelteComponent, type: string, f: CallbackFactory): Function { const bubble : Bubble = {f, r: new Map()}; const bubbles = (comp.$$.bubbles[type] || (comp.$$.bubbles[type] = [])); From c2d91137cd3d08b6dba7b38cf7a16b57e6a164a4 Mon Sep 17 00:00:00 2001 From: adiguba Date: Sat, 4 Mar 2023 15:06:40 +0100 Subject: [PATCH 12/15] fix HMR --- .../compile/render_dom/wrappers/InlineComponent/index.ts | 4 ++++ src/runtime/internal/dev.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index 31e7e6246192..ce3e1db67c36 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -402,6 +402,10 @@ export default class InlineComponentWrapper extends Wrapper { if (this.node.handlers.length > 0) { const target = x`${name}`; + if (component.compile_options.dev) { + /* Hook for restoring handlers on components after HMR */ + munged_bindings.push(b`@fix_callbacks_hmr_dev(${target})`); + } for (const handler of this.node.handlers) { new EventHandler(handler, this) .render(block, target, true); diff --git a/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index 9aff69d8134d..fd0090b3d2c5 100644 --- a/src/runtime/internal/dev.ts +++ b/src/runtime/internal/dev.ts @@ -1,7 +1,7 @@ import { custom_event, append, append_hydration, insert, insert_hydration, detach, listen, attr, prevent_default, stop_propagation, trusted, self, stop_immediate_propagation } from './dom'; import { SvelteComponent } from './Component'; import { is_void } from '../../shared/utils/names'; -import { bubble, listen_comp } from './lifecycle'; +import { bubble, listen_comp, restart_all_callback } from './lifecycle'; export function dispatch_dev(type: string, detail?: T) { document.dispatchEvent(custom_event(type, { version: '__VERSION__', ...detail }, { bubbles: true })); @@ -94,6 +94,13 @@ export function listen_comp_dev(comp: SvelteComponent, event: string, handler: E }; } +/* Hook for restarting callbacks after HMR */ +export function fix_callbacks_hmr_dev(comp: any) { + comp.$$.on_hmr && comp.$$.on_hmr.push( (_: any) => { + return (c: SvelteComponent) => restart_all_callback(c); + }); +} + export function attr_dev(node: Element, attribute: string, value?: string) { attr(node, attribute, value); From b7950e34afb40b11ae75d3051c5792c06279be1b Mon Sep 17 00:00:00 2001 From: adiguba Date: Sat, 4 Mar 2023 15:13:28 +0100 Subject: [PATCH 13/15] errors --- src/compiler/compile/compiler_errors.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index b0ffead6e023..b869fdfa5626 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -289,5 +289,13 @@ export default { invalid_style_directive_modifier: (valid: string) => ({ code: 'invalid-style-directive-modifier', message: `Valid modifiers for style directives are: ${valid}` - }) + }), + invalid_forward_event_alias_count: { + code: 'invalid-forward-event-alias-count', + message: 'The on: forward directive accept only one modifier (the forward alias)' + }, + invalid_forward_event_alias_any: { + code: 'invalid-forward-event-alias-any', + message: 'The alias for on:* must be of the following form : "prefix*" or "*suffix"' + } }; From ae1b3b4aeb214e5f3a79e23be5a5f3b2679a3475 Mon Sep 17 00:00:00 2001 From: adiguba Date: Sat, 4 Mar 2023 16:26:42 +0100 Subject: [PATCH 14/15] error for on:*={handler} --- src/compiler/compile/compiler_errors.ts | 4 ++++ src/compiler/compile/nodes/EventHandler.ts | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index b869fdfa5626..d699b8902bb2 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -297,5 +297,9 @@ export default { invalid_forward_event_alias_any: { code: 'invalid-forward-event-alias-any', message: 'The alias for on:* must be of the following form : "prefix*" or "*suffix"' + }, + invalid_foward_event_any: { + code: 'invalid-forward-event-any', + message: 'The directive on:* cannot be used with an handler' } }; diff --git a/src/compiler/compile/nodes/EventHandler.ts b/src/compiler/compile/nodes/EventHandler.ts index ac759f166f19..5872e7e3c50a 100644 --- a/src/compiler/compile/nodes/EventHandler.ts +++ b/src/compiler/compile/nodes/EventHandler.ts @@ -92,6 +92,10 @@ export default class EventHandler extends Node { validate() { if (this.expression) { + if (this.name === '*') { + return this.component.error(this, compiler_errors.invalid_foward_event_any); + } + if (this.modifiers.has('passive') && this.modifiers.has('preventDefault')) { return this.component.error(this, compiler_errors.invalid_event_modifier_combination('passive', 'preventDefault')); } @@ -130,12 +134,15 @@ export default class EventHandler extends Node { if (this.aliasCount > 1) { return this.component.error(this, compiler_errors.invalid_forward_event_alias_count); } - if (this.aliasName && valid_modifiers.has(this.aliasName)) { - this.component.warn(this, compiler_warnings.invalid_forward_event_alias); - } - if (this.name === '*' && !is_valid_any_alias_name(this.aliasName)) { - return this.component.error(this, compiler_errors.invalid_forward_event_alias_any); + if (this.aliasName) { + if (this.name === '*' && !is_valid_any_alias_name(this.aliasName)) { + return this.component.error(this, compiler_errors.invalid_forward_event_alias_any); + } + if (valid_modifiers.has(this.aliasName)) { + this.component.warn(this, compiler_warnings.invalid_forward_event_alias); + } } + } } From 6ce3d2fe942f2266bf0fdb2736e75884c09758aa Mon Sep 17 00:00:00 2001 From: adiguba Date: Sat, 4 Mar 2023 16:40:30 +0100 Subject: [PATCH 15/15] fix lint --- src/compiler/compile/nodes/EventHandler.ts | 2 +- src/compiler/compile/nodes/InlineComponent.ts | 2 +- src/compiler/compile/render_dom/Block.ts | 5 ++-- .../wrappers/Element/EventHandler.ts | 2 +- src/runtime/internal/dev.ts | 2 +- src/runtime/internal/dom.ts | 10 +++---- src/runtime/internal/lifecycle.ts | 27 +++++++++++-------- src/runtime/internal/types.ts | 2 +- 8 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/compiler/compile/nodes/EventHandler.ts b/src/compiler/compile/nodes/EventHandler.ts index 5872e7e3c50a..95ce741f2ac3 100644 --- a/src/compiler/compile/nodes/EventHandler.ts +++ b/src/compiler/compile/nodes/EventHandler.ts @@ -35,7 +35,7 @@ function is_valid_any_alias_name(alias: string) { if (alias === '*') { return true; } - let idx = alias.indexOf('*'); + const idx = alias.indexOf('*'); if (idx < 0) return false; if (idx !== alias.lastIndexOf('*')) { return false; diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index 5949b1c3fc9b..7c609437204e 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -98,7 +98,7 @@ export default class InlineComponent extends Node { this.scope = scope; } - this.handlers.forEach(h => h.validate()) + this.handlers.forEach(h => h.validate()); const children = []; for (let i = info.children.length - 1; i >= 0; i--) { diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts index c7b975d0d146..b89dc5a98b27 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -1,10 +1,9 @@ import Renderer, { BindingGroup } from './Renderer'; import Wrapper from './wrappers/shared/Wrapper'; import { b, x } from 'code-red'; -import { Node, Identifier, ArrayPattern } from 'estree'; +import { Node, Identifier, ArrayPattern, Expression } from 'estree'; import { is_head } from './wrappers/shared/is_head'; import { regex_double_quotes } from '../../utils/patterns'; -import { Expression } from 'estree'; export interface Bindings { object: Identifier; @@ -60,7 +59,7 @@ export default class Block { destroy: Array; }; - event_updaters: ({condition:Expression, index:number})[] = []; + event_updaters: Array<{condition:Expression, index:number}> = []; event_listeners: Node[] = []; maintain_context: boolean; diff --git a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts index cd313e4c1a40..7ec77c9357c5 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts @@ -37,7 +37,7 @@ export default class EventHandlerWrapper { const snippet = this.get_snippet(block); - let wrappers = []; + const wrappers = []; if (this.node.modifiers.has('trusted')) wrappers.push(x`@trusted`); if (this.node.modifiers.has('self')) wrappers.push(x`@self`); if (this.node.modifiers.has('stopImmediatePropagation')) wrappers.push(x`@stop_immediate_propagation`); diff --git a/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index fd0090b3d2c5..39657c61169e 100644 --- a/src/runtime/internal/dev.ts +++ b/src/runtime/internal/dev.ts @@ -74,7 +74,7 @@ export function listen_dev(node: Node, event: string, handler: EventListenerOrEv }; } -export function bubble_dev(component: SvelteComponent, listen_func: Function, node: EventTarget|SvelteComponent, type: string, typeName: string = type): Function { +export function bubble_dev(component: SvelteComponent, listen_func: Function, node: EventTarget | SvelteComponent, type: string, typeName: string = type): Function { dispatch_dev('SvelteComponentAddEventBubble', { component, listen_func, node, type, typeName }); const dispose = bubble(component, listen_func, node, type, typeName); return () => { diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 152c88bfb674..8a391c3c05d7 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -264,7 +264,7 @@ export function wrap_handler(handler: EventListenerOrEventListenerObject, wrappe return result; } -export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject|null|undefined|false, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { +export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject | null | undefined | false, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { if (handler) { const h = wrap_handler(handler, wrappers); node.addEventListener(event, h, options); @@ -273,12 +273,10 @@ export function listen(node: EventTarget, event: string, handler: EventListenerO return noop; } -export function listen_and_update(get_handler: ()=>EventListenerOrEventListenerObject|null|undefined|false, factory: (handler:EventListenerOrEventListenerObject|null|undefined|false) => Function) { +export function listen_and_update(get_handler: ()=>EventListenerOrEventListenerObject | null | undefined | false, factory: (handler:EventListenerOrEventListenerObject | null | undefined | false) => Function) { let handler = get_handler(); let dispose_handle: Function = factory(handler); - const dispose = () => { - dispose_handle(); - } + const dispose = () => dispose_handle(); // update : dispose.p = () => { const new_handler = get_handler(); @@ -287,7 +285,7 @@ export function listen_and_update(get_handler: ()=>EventListenerOrEventListenerO handler = new_handler; dispose_handle = factory(handler); } - } + }; return dispose; } diff --git a/src/runtime/internal/lifecycle.ts b/src/runtime/internal/lifecycle.ts index 83aef3e0e455..4cb3aaaa30f6 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -155,7 +155,7 @@ function start_bubble(type: string, bubble: Bubble, callback: Callback) { function start_bubbles(comp : SvelteComponent, bubble: Bubble) { for (const type of Object.keys(comp.$$.callbacks)) { - comp.$$.callbacks[type].forEach( callback => { start_bubble(type, bubble, callback); }) + comp.$$.callbacks[type].forEach( callback => start_bubble(type, bubble, callback)); } } @@ -170,7 +170,7 @@ export function restart_all_callback(comp : SvelteComponent) { function start_callback(comp : SvelteComponent, type: string, callback: Callback) { for (const bubbles of [ comp.$$.bubbles[type], comp.$$.bubbles['*'] ]) { if (bubbles) { - for(const bubble of bubbles) { + for (const bubble of bubbles) { start_bubble(type, bubble, callback); } } @@ -186,8 +186,8 @@ export function add_callback(comp : SvelteComponent, type: string, f: EventListe const callback: Callback = {f, o}; if (o && (o as any)?.once === true) { - callback.f = function () { - const r = f.apply(this, arguments); + callback.f = function(this: any, ...args) { + const r = f.call(this, ...args); remove_callback(comp, type, callback); return r; }; @@ -236,15 +236,20 @@ function add_bubble(comp: SvelteComponent, type: string, f: CallbackFactory): Fu for (const dispose of bubble.r.values()) { dispose(); } - } + }; } +/** + * Schedules a callback to run when an handler is added to the component + * + * TODO : Docs + */ export function onEventListener(type: string, fn: CallbackFactory) { add_bubble(get_current_component(), type, fn); } -export function bubble(component: SvelteComponent, listen_func: Function, node: EventTarget|SvelteComponent, type: string, typeName: string = type): Function { +export function bubble(component: SvelteComponent, listen_func: Function, node: EventTarget | SvelteComponent, type: string, typeName: string = type): Function { if (type === '*') { return add_bubble(component, type, (callback, options, eventType) => { let typeToListen: string = null; @@ -253,12 +258,12 @@ export function bubble(component: SvelteComponent, listen_func: Function, node: } else if (typeName.startsWith('*')) { const len = typeName.length; if (eventType.endsWith(typeName.substring(1))) { - typeToListen = eventType.substring(0, eventType.length - (len-1)); + typeToListen = eventType.substring(0, eventType.length - (len - 1)); } } else if (typeName.endsWith('*')) { const len = typeName.length; - if (eventType.startsWith(typeName.substring(0,len-1))) { - typeToListen = eventType.substring(len-1); + if (eventType.startsWith(typeName.substring(0,len - 1))) { + typeToListen = eventType.substring(len - 1); } } if (typeToListen) { @@ -271,9 +276,9 @@ export function bubble(component: SvelteComponent, listen_func: Function, node: }); } -export function listen_comp(comp: SvelteComponent, event: string, handler: EventListenerOrEventListenerObject|null|undefined|false, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { +export function listen_comp(comp: SvelteComponent, event: string, handler: EventListenerOrEventListenerObject | null | undefined | false, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { if (handler) { return comp.$on(event, wrap_handler(handler, wrappers), options); } return noop; -} \ No newline at end of file +} diff --git a/src/runtime/internal/types.ts b/src/runtime/internal/types.ts index 4da02d1bf302..82e75dc81ee8 100644 --- a/src/runtime/internal/types.ts +++ b/src/runtime/internal/types.ts @@ -25,7 +25,7 @@ export interface Callback { o?: boolean | AddEventListenerOptions | EventListenerOptions; } -export type CallbackFactory = (callback: EventListener, options: boolean | AddEventListenerOptions | EventListenerOptions | undefined, type: string) => Function|void; +export type CallbackFactory = (callback: EventListener, options: boolean | AddEventListenerOptions | EventListenerOptions | undefined, type: string) => Function | undefined; export interface Bubble { f: CallbackFactory;