From 43ab3156482ec13dbaf6a9f00b234b59627abca3 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 10 Jul 2024 10:30:25 +0200 Subject: [PATCH] fix: more event handling tweaks - ensure we only have a single document listener per event+runtime - add `` listeners to `before_init` similar to the document/window elements - move some code into `events.js` where it belongs --- .../3-transform/client/visitors/template.js | 7 ++++- .../internal/client/dom/elements/events.js | 7 ++++- .../src/internal/client/dom/operations.js | 3 +-- packages/svelte/src/internal/client/render.js | 27 +++++++++++-------- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 0de47791af..b46c17696f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1280,7 +1280,12 @@ function serialize_event(node, context) { } const parent = /** @type {import('#compiler').SvelteNode} */ (context.path.at(-1)); - if (parent.type === 'SvelteDocument' || parent.type === 'SvelteWindow') { + if ( + parent.type === 'SvelteDocument' || + parent.type === 'SvelteWindow' || + parent.type === 'SvelteBody' + ) { + // These nodes are above the component tree, and its events should run parent first state.before_init.push(statement); } else { state.after_update.push(statement); diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 302820829d..fa37594074 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -1,9 +1,14 @@ import { teardown } from '../../reactivity/effects.js'; -import { all_registered_events, root_event_handles } from '../../render.js'; import { define_property, is_array } from '../../utils.js'; import { hydrating } from '../hydration.js'; import { queue_micro_task } from '../task.js'; +/** @type {Set} */ +export const all_registered_events = new Set(); + +/** @type {Set<(events: Array) => void>} */ +export const root_event_handles = new Set(); + /** * SSR adds onload and onerror attributes to catch those events before the hydration. * This function detects those cases, removes the attributes and replays the events. diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index c1eb663053..5d16632bbd 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -1,8 +1,7 @@ -/** @import { Effect, TemplateNode } from '#client' */ +/** @import { TemplateNode } from '#client' */ import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { DEV } from 'esm-env'; import { init_array_prototype_warnings } from '../dev/equality.js'; -import { current_effect } from '../runtime.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index a00fd23d54..961f8dc085 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -16,19 +16,17 @@ import { set_hydrating } from './dom/hydration.js'; import { array_from } from './utils.js'; -import { handle_event_propagation } from './dom/elements/events.js'; +import { + all_registered_events, + handle_event_propagation, + root_event_handles +} from './dom/elements/events.js'; import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import * as w from './warnings.js'; import * as e from './errors.js'; import { validate_component } from '../shared/validate.js'; import { assign_nodes } from './dom/template.js'; -/** @type {Set} */ -export const all_registered_events = new Set(); - -/** @type {Set<(events: Array) => void>} */ -export const root_event_handles = new Set(); - /** * This is normally true — block effects should run their intro transitions — * but is false during hydration (unless `options.intro` is `true`) and @@ -181,6 +179,8 @@ export function hydrate(component, options) { } } +const document_listeners = new Set(); + /** * @template {Record} Exports * @param {import('../../index.js').ComponentType> | import('../../index.js').Component} Component @@ -213,9 +213,13 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro // the outer content stops propagation of the event. target.addEventListener(event_name, handle_event_propagation, { passive }); - // The document listener ensures we catch events that originate from elements that were - // manually moved outside of the container (e.g. via manual portals). - document.addEventListener(event_name, handle_event_propagation, { passive }); + if (!document_listeners.has(event_name)) { + document_listeners.add(event_name); + + // The document listener ensures we catch events that originate from elements that were + // manually moved outside of the container (e.g. via manual portals). + document.addEventListener(event_name, handle_event_propagation, { passive }); + } } } }; @@ -263,7 +267,8 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro return () => { for (const event_name of registered_events) { target.removeEventListener(event_name, handle_event_propagation); - document.removeEventListener(event_name, handle_event_propagation); + // Don't remove document event listeners, they're global per runtime, + // and we don't know whether or not there are other components still using them } root_event_handles.delete(event_handle);