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/ diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts index b0ffead6e023..d699b8902bb2 100644 --- a/src/compiler/compile/compiler_errors.ts +++ b/src/compiler/compile/compiler_errors.ts @@ -289,5 +289,17 @@ 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"' + }, + 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/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts index d6b2dbc5aa71..07d288a27b7f 100644 --- a/src/compiler/compile/compiler_warnings.ts +++ b/src/compiler/compile/compiler_warnings.ts @@ -212,6 +212,10 @@ 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 propagation with original object will not work` - }) + message: `...${rest_element_name} operator will create a new object and binding propogation with original object will not work` + }), + 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/Element.ts b/src/compiler/compile/nodes/Element.ts index 416f1d7b3169..70e4c3a997d5 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'; @@ -180,25 +179,6 @@ function get_implicit_role(name: string, attribute_map: Map) const invisible_elements = new Set(['meta', 'html', 'script', 'style']); -const valid_modifiers = new Set([ - 'preventDefault', - 'stopPropagation', - 'stopImmediatePropagation', - 'capture', - 'once', - 'passive', - 'nonpassive', - 'self', - 'trusted' -]); - -const passive_events = new Set([ - 'wheel', - 'touchstart', - 'touchmove', - 'touchend', - 'touchcancel' -]); const react_attributes = new Map([ ['className', 'class'], @@ -1041,44 +1021,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()); } is_media_node() { diff --git a/src/compiler/compile/nodes/EventHandler.ts b/src/compiler/compile/nodes/EventHandler.ts index 4b426580b3ae..95ce741f2ac3 100644 --- a/src/compiler/compile/nodes/EventHandler.ts +++ b/src/compiler/compile/nodes/EventHandler.ts @@ -1,19 +1,55 @@ 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', + 'stopImmediatePropagation', + 'capture', + 'once', + 'passive', + 'nonpassive', + 'self', + 'trusted' +]); + +const passive_events = new Set([ + 'wheel', + 'touchstart', + 'touchmove', + 'touchend', + 'touchcancel' +]); + + +function is_valid_any_alias_name(alias: string) { + if (alias === '*') { + return true; + } + const 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; modifiers: Set; expression: Expression; - handler_name: Identifier; + aliasName?: string; + aliasCount: 0; uses_context = false; can_make_passive = false; @@ -47,7 +83,66 @@ 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.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')); + } + + 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.invalid_forward_event_alias_count); + } + 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); + } + } + } } diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index e9ac86a55cf5..7c609437204e 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -98,13 +98,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/Block.ts b/src/compiler/compile/render_dom/Block.ts index c40dedc3b531..b89dc5a98b27 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -1,7 +1,7 @@ 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'; @@ -59,6 +59,7 @@ export default class Block { destroy: Array; }; + event_updaters: Array<{condition:Expression, index:number}> = []; event_listeners: Node[] = []; maintain_context: boolean; @@ -483,6 +484,12 @@ export default class Block { ` ); + if (this.event_updaters.length === 1) { + const {condition} = this.event_updaters[0]; + this.chunks.update.push(b` + if (${condition}) ${dispose}.p()`); + } + this.chunks.destroy.push( b`${dispose}();` ); @@ -496,6 +503,10 @@ export default class Block { } `); + for (const {condition, index} of this.event_updaters) { + this.chunks.update.push(b` if (${condition}) ${dispose}[${index}].p()`); + } + 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 b8c9f70c0f23..7ec77c9357c5 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts @@ -1,7 +1,7 @@ import EventHandler from '../../../nodes/EventHandler'; import Wrapper from '../shared/Wrapper'; import Block from '../../Block'; -import { b, x, p } from 'code-red'; +import { x, p } from 'code-red'; import { Expression } from 'estree'; const TRUE = x`true`; @@ -15,35 +15,35 @@ 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('stopImmediatePropagation')) snippet = x`@stop_immediate_propagation(${snippet})`; - if (this.node.modifiers.has('self')) snippet = x`@self(${snippet})`; - if (this.node.modifiers.has('trusted')) snippet = x`@trusted(${snippet})`; + 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`); + 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 = []; @@ -58,18 +58,25 @@ 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); - args.push(this.node.modifiers.has('stopImmediatePropagation') ? 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 index = block.event_listeners.length; + const condition = block.renderer.dirty(this.node.expression.dynamic_dependencies()); + + block.event_updaters.push({condition, index}); + block.event_listeners.push( + x`@listen_and_update( () => (${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 ab140a75c500..ce3e1db67c36 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -400,13 +400,17 @@ 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}`; + 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); + } + } 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'); @@ -438,7 +442,6 @@ export default class InlineComponentWrapper extends Wrapper { ${name} = @construct_svelte_component(${switch_value}, ${switch_props}(#ctx)); ${munged_bindings} - ${munged_handlers} } `); @@ -491,7 +494,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); @@ -525,7 +527,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 a8a500b25b02..b0db9040eac9 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -1,5 +1,5 @@ import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler'; -import { current_component, set_current_component } 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'; @@ -97,6 +97,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 @@ -179,18 +180,8 @@ if (typeof HTMLElement === 'function') { this.$destroy = noop; } - $on(type, callback) { - // TODO should this delegate to addEventListener? - if (!is_function(callback)) { - return noop; - } - const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); - callbacks.push(callback); - - return () => { - const index = callbacks.indexOf(callback); - if (index !== -1) callbacks.splice(index, 1); - }; + $on(type: string, callback: EventListener, options?: boolean | AddEventListenerOptions | EventListenerOptions) { + return add_callback(this, type, callback, options); } $set($$props) { @@ -215,17 +206,8 @@ export class SvelteComponent { this.$destroy = noop; } - $on(type, callback) { - if (!is_function(callback)) { - return noop; - } - const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); - callbacks.push(callback); - - return () => { - const index = callbacks.indexOf(callback); - if (index !== -1) callbacks.splice(index, 1); - }; + $on(type: string, callback: EventListener, options?: boolean | AddEventListenerOptions | EventListenerOptions) { + return add_callback(this, type, callback, options); } $set($$props) { diff --git a/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index be81a115147c..39657c61169e 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, stop_immediate_propagation } from './dom'; import { SvelteComponent } from './Component'; import { is_void } from '../../shared/utils/names'; +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 })); @@ -49,21 +50,57 @@ 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, has_stop_immediate_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 (has_stop_immediate_propagation) modifiers.push('stopImmediatePropagation'); + if (wrappers) { + if (wrappers.indexOf(prevent_default) >= 0) modifiers.push('preventDefault'); + if (wrappers.indexOf(stop_propagation) >= 0) modifiers.push('stopPropagation'); + if (wrappers.indexOf(stop_immediate_propagation) >= 0) modifiers.push('stopImmediatePropagation'); + // ??? + 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(); + }; +} + +/* 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); @@ -145,7 +182,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 70e7dcd7f8f5..8a391c3c05d7 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. @@ -254,9 +254,39 @@ 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 | false, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) { + if (handler) { + const h = wrap_handler(handler, wrappers); + node.addEventListener(event, h, options); + return () => node.removeEventListener(event, h, options); + } + return noop; +} + +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(); + // update : + dispose.p = () => { + const new_handler = get_handler(); + if (new_handler !== handler) { + dispose_handle(); + handler = new_handler; + dispose_handle = 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..4cb3aaaa30f6 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 { is_function, 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,139 @@ 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(callback.f, callback.o, type); + 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 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) { + start_bubble(type, bubble, 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(this: any, ...args) { + const r = f.call(this, ...args); + 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) { + const dispose = bubble.r.get(callback); + if (dispose) { + dispose(); + bubble.r.delete(callback); + } + } + } + } +} +function remove_callback(comp : SvelteComponent, type: string, callback: Callback) { + const callbacks = comp.$$.callbacks[type]; if (callbacks) { - // @ts-ignore - callbacks.slice().forEach(fn => fn.call(this, event)); + 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] = [])); + 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(); + } + }; +} + +/** + * 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 { + if (type === '*') { + return add_bubble(component, type, (callback, options, eventType) => { + let typeToListen: string = null; + 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); + } + } + if (typeToListen) { + return listen_func(node, typeToListen, callback, options); + } + }); + } + return add_bubble(component, typeName, (callback, options) => { + return listen_func(node, type, callback, options); + }); +} + +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; } diff --git a/src/runtime/internal/types.ts b/src/runtime/internal/types.ts index 41f8f1ca43fe..82e75dc81ee8 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 = (callback: EventListener, options: boolean | AddEventListenerOptions | EventListenerOptions | undefined, type: string) => Function | undefined; + +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; 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() {}