From 0ddf94b9347bbaa5c28d30feb09c4f125245b84f Mon Sep 17 00:00:00 2001 From: adiguba Date: Thu, 2 Mar 2023 17:18:42 +0100 Subject: [PATCH] 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 62103d26d6..d1880ca1af 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 3a11e04bd1..43f77cd07d 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 6f61e43c7b..258c77d646 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 4b426580b3..091e4ca77f 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 87f999331d..4908f98cd7 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 b0ed4a73d6..31e3eea64e 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 bca01fdb4c..d22215ec59 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 2029f67a04..97eef2092d 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 5aec24c651..9979dc6da0 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 9d24ad2a07..1dfe6fdfa8 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 4aa30eb250..f175d59227 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 e75bbdc501..2e381e6692 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 41f8f1ca43..a957730ca3 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;