From 2515044a1937d950434ce3f90b11d6889392387d Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:40:03 +0800 Subject: [PATCH 1/8] feat: implement global-bind event support for decoupled cross-element communication --- .changeset/feat-global-bind-support.md | 38 ++++++ .changeset/red-dragons-tickle.md | 3 + .../web-core-e2e/tests/reactlynx.spec.ts | 13 ++ .../reactlynx/basic-global-bind/index.jsx | 42 +++++++ .../web-core/binary/client/client.d.ts | 2 + .../binary/client/client_bg.wasm.d.ts | 1 + .../web-core/binary/client_legacy/client.d.ts | 2 + .../binary/client_legacy/client_bg.wasm.d.ts | 1 + .../client/element_apis/event_apis.rs | 115 +++++++++++++++++- .../main_thread/client/main_thread_context.rs | 2 + .../web-core/tests/element-apis.spec.ts | 49 ++++++++ 11 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 .changeset/feat-global-bind-support.md create mode 100644 .changeset/red-dragons-tickle.md create mode 100644 packages/web-platform/web-core-e2e/tests/reactlynx/basic-global-bind/index.jsx diff --git a/.changeset/feat-global-bind-support.md b/.changeset/feat-global-bind-support.md new file mode 100644 index 0000000000..94b7428280 --- /dev/null +++ b/.changeset/feat-global-bind-support.md @@ -0,0 +1,38 @@ +--- +"@lynx-js/web-core": patch +--- + +Added support for the `global-bind` event handling modifier in the web platform runtime. + +This mechanism enables seamless cross-element event communication without requiring a formal DOM tree relationship, allowing decoupled elements to observe and respond to standard events occurring anywhere within the component tree. + +### Usage + +Global bindings allow an observer element to react to events triggered on another target element. + +#### 1. Define the Global Subscription + +Attach `global-bindTap` (or any equivalent standard event alias) to your observer element: + +```jsx + { + // This will trigger whenever 'tap' is caught by a globally bound event. + console.log('Global tap handled!', event); + }} +/>; +``` + +#### 2. Trigger the Event anywhere + +The event will be triggered via normal user interaction (such as `tap`) on any other constituent elements: + +```jsx + { + // Note: To successfully propagate globally, ensure the event bubbles. + }} +/>; +``` diff --git a/.changeset/red-dragons-tickle.md b/.changeset/red-dragons-tickle.md new file mode 100644 index 0000000000..853d812bb3 --- /dev/null +++ b/.changeset/red-dragons-tickle.md @@ -0,0 +1,3 @@ +--- + +--- diff --git a/packages/web-platform/web-core-e2e/tests/reactlynx.spec.ts b/packages/web-platform/web-core-e2e/tests/reactlynx.spec.ts index f950026698..063ffce42e 100644 --- a/packages/web-platform/web-core-e2e/tests/reactlynx.spec.ts +++ b/packages/web-platform/web-core-e2e/tests/reactlynx.spec.ts @@ -114,6 +114,19 @@ test.describe('reactlynx3 tests', () => { await wait(100); await expect(await target.getAttribute('style')).toContain('pink'); }); + test('basic-global-bind', async ({ page }, { title }) => { + await goto(page, title); + await wait(100); + const target = page.locator('#target'); + const observer = page.locator('#observer'); + + await target.click(); + await wait(100); + await expect(await observer.getAttribute('style')).toContain('green'); + await target.click(); + await wait(100); + await expect(await observer.getAttribute('style')).toContain('pink'); + }); test('basic-bindtap-detail', async ({ page }, { title }) => { await goto(page, title); await wait(100); diff --git a/packages/web-platform/web-core-e2e/tests/reactlynx/basic-global-bind/index.jsx b/packages/web-platform/web-core-e2e/tests/reactlynx/basic-global-bind/index.jsx new file mode 100644 index 0000000000..1f23184c5e --- /dev/null +++ b/packages/web-platform/web-core-e2e/tests/reactlynx/basic-global-bind/index.jsx @@ -0,0 +1,42 @@ +// Copyright 2023 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import { useState, root } from '@lynx-js/react'; + +function App() { + const [color, setColor] = useState('pink'); + return ( + + { + /* + This is the target element. + When clicking here, the 'tap' event bubbles and should trigger 'global-bindtap'. + */ + } + + { + /* + This is the observer element observing 'global-bindtap'. + */ + } + setColor(color === 'pink' ? 'green' : 'pink')} + style={{ + height: '100px', + width: '100px', + background: color, + }} + /> + + ); +} + +root.render(); diff --git a/packages/web-platform/web-core/binary/client/client.d.ts b/packages/web-platform/web-core/binary/client/client.d.ts index 2c21820086..3557cc760a 100644 --- a/packages/web-platform/web-core/binary/client/client.d.ts +++ b/packages/web-platform/web-core/binary/client/client.d.ts @@ -24,6 +24,7 @@ export class MainThreadWasmContext { common_event_handler(event: any, bubble_unique_id_path: Uint32Array, event_name: string, is_bubble: boolean): void; create_element_common(parent_component_unique_id: number, dom: HTMLElement, css_id?: number | null, component_id?: string | null): number; dispatch_event_by_path(bubble_unique_id_path: Uint32Array, event_name: string, is_capture: boolean, serialized_event: any): boolean; + dispatch_global_bind_event(bubble_unique_id_path: Uint32Array, event_name: string, serialized_event: any): void; get_component_id(unique_id: number): string | undefined; get_config(unique_id: number): object; get_css_id_by_unique_id(unique_id: number): number | undefined; @@ -202,6 +203,7 @@ export interface InitOutput { readonly mainthreadwasmcontext_common_event_handler: (a: number, b: any, c: number, d: number, e: number, f: number, g: number) => void; readonly mainthreadwasmcontext_create_element_common: (a: number, b: number, c: any, d: number, e: number, f: number) => number; readonly mainthreadwasmcontext_dispatch_event_by_path: (a: number, b: number, c: number, d: number, e: number, f: number, g: any) => number; + readonly mainthreadwasmcontext_dispatch_global_bind_event: (a: number, b: number, c: number, d: number, e: number, f: any) => void; readonly mainthreadwasmcontext_get_component_id: (a: number, b: number) => [number, number, number, number]; readonly mainthreadwasmcontext_get_config: (a: number, b: number) => [number, number, number]; readonly mainthreadwasmcontext_get_css_id_by_unique_id: (a: number, b: number) => number; diff --git a/packages/web-platform/web-core/binary/client/client_bg.wasm.d.ts b/packages/web-platform/web-core/binary/client/client_bg.wasm.d.ts index 6e315ce555..05b52af61f 100644 --- a/packages/web-platform/web-core/binary/client/client_bg.wasm.d.ts +++ b/packages/web-platform/web-core/binary/client/client_bg.wasm.d.ts @@ -25,6 +25,7 @@ export const mainthreadwasmcontext_add_run_worklet_event: (a: number, b: number, export const mainthreadwasmcontext_common_event_handler: (a: number, b: any, c: number, d: number, e: number, f: number, g: number) => void; export const mainthreadwasmcontext_create_element_common: (a: number, b: number, c: any, d: number, e: number, f: number) => number; export const mainthreadwasmcontext_dispatch_event_by_path: (a: number, b: number, c: number, d: number, e: number, f: number, g: any) => number; +export const mainthreadwasmcontext_dispatch_global_bind_event: (a: number, b: number, c: number, d: number, e: number, f: any) => void; export const mainthreadwasmcontext_get_component_id: (a: number, b: number) => [number, number, number, number]; export const mainthreadwasmcontext_get_config: (a: number, b: number) => [number, number, number]; export const mainthreadwasmcontext_get_css_id_by_unique_id: (a: number, b: number) => number; diff --git a/packages/web-platform/web-core/binary/client_legacy/client.d.ts b/packages/web-platform/web-core/binary/client_legacy/client.d.ts index c07a8fbd63..942ed1e0dc 100644 --- a/packages/web-platform/web-core/binary/client_legacy/client.d.ts +++ b/packages/web-platform/web-core/binary/client_legacy/client.d.ts @@ -24,6 +24,7 @@ export class MainThreadWasmContext { common_event_handler(event: any, bubble_unique_id_path: Uint32Array, event_name: string, is_bubble: boolean): void; create_element_common(parent_component_unique_id: number, dom: HTMLElement, css_id?: number | null, component_id?: string | null): number; dispatch_event_by_path(bubble_unique_id_path: Uint32Array, event_name: string, is_capture: boolean, serialized_event: any): boolean; + dispatch_global_bind_event(bubble_unique_id_path: Uint32Array, event_name: string, serialized_event: any): void; get_component_id(unique_id: number): string | undefined; get_config(unique_id: number): object; get_css_id_by_unique_id(unique_id: number): number | undefined; @@ -202,6 +203,7 @@ export interface InitOutput { readonly mainthreadwasmcontext_common_event_handler: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; readonly mainthreadwasmcontext_create_element_common: (a: number, b: number, c: number, d: number, e: number, f: number) => number; readonly mainthreadwasmcontext_dispatch_event_by_path: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number; + readonly mainthreadwasmcontext_dispatch_global_bind_event: (a: number, b: number, c: number, d: number, e: number, f: number) => void; readonly mainthreadwasmcontext_get_component_id: (a: number, b: number, c: number) => void; readonly mainthreadwasmcontext_get_config: (a: number, b: number, c: number) => void; readonly mainthreadwasmcontext_get_css_id_by_unique_id: (a: number, b: number) => number; diff --git a/packages/web-platform/web-core/binary/client_legacy/client_bg.wasm.d.ts b/packages/web-platform/web-core/binary/client_legacy/client_bg.wasm.d.ts index 51fabb5823..052e560b78 100644 --- a/packages/web-platform/web-core/binary/client_legacy/client_bg.wasm.d.ts +++ b/packages/web-platform/web-core/binary/client_legacy/client_bg.wasm.d.ts @@ -25,6 +25,7 @@ export const mainthreadwasmcontext_add_run_worklet_event: (a: number, b: number, export const mainthreadwasmcontext_common_event_handler: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; export const mainthreadwasmcontext_create_element_common: (a: number, b: number, c: number, d: number, e: number, f: number) => number; export const mainthreadwasmcontext_dispatch_event_by_path: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number; +export const mainthreadwasmcontext_dispatch_global_bind_event: (a: number, b: number, c: number, d: number, e: number, f: number) => void; export const mainthreadwasmcontext_get_component_id: (a: number, b: number, c: number) => void; export const mainthreadwasmcontext_get_config: (a: number, b: number, c: number) => void; export const mainthreadwasmcontext_get_css_id_by_unique_id: (a: number, b: number) => number; diff --git a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs index 0630ffe44e..6342eccf88 100644 --- a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs +++ b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs @@ -35,6 +35,20 @@ impl MainThreadWasmContext { let event_type = event_type.to_ascii_lowercase(); self.enable_event(&event_name); + if event_type == "global-bindevent" || event_type == "global-bind" { + if event_handler_identifier.is_some() { + self + .global_bind_events + .entry(event_name.clone()) + .or_default() + .insert(unique_id); + } else { + if let Some(set) = self.global_bind_events.get_mut(&event_name) { + set.remove(&unique_id); + } + } + } + let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str); let mut should_enable = false; let mut should_disable = false; @@ -84,6 +98,18 @@ impl MainThreadWasmContext { let event_type = event_type.to_ascii_lowercase(); self.enable_event(&event_name); + if event_type == "global-bindevent" { + if event_handler_identifier.is_some() { + self + .global_bind_events + .entry(event_name.clone()) + .or_default() + .insert(unique_id); + } else if let Some(set) = self.global_bind_events.get_mut(&event_name) { + set.remove(&unique_id); + } + } + let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str); let mut should_enable = false; let mut should_disable = false; @@ -138,7 +164,13 @@ impl MainThreadWasmContext { pub fn get_events(&self, unique_id: usize) -> Vec { let mut event_infos: Vec = vec![]; - let event_types = vec!["bindevent", "capture-bind", "catchevent", "capture-catch"]; + let event_types = vec![ + "bindevent", + "capture-bind", + "catchevent", + "capture-catch", + "global-bind", + ]; let binding = self.get_element_data_by_unique_id(unique_id).unwrap(); let element_data = binding.borrow(); for event_type in event_types { @@ -289,6 +321,87 @@ impl MainThreadWasmContext { self.dispatch_event_by_path(&[*target_id], event_name, false, &event); } } + + if is_bubble { + self.dispatch_global_bind_event(&bubble_unique_id_path, event_name, &event); + } + } + + pub fn dispatch_global_bind_event( + &self, + bubble_unique_id_path: &[usize], + event_name: &str, + serialized_event: &JsValue, + ) { + let event_name = match event_name { + "click" => "tap", + "touchstart" => "touchstart", + "touchmove" => "touchmove", + "touchend" => "touchend", + "touchcancel" => "touchcancel", + _ => event_name, + }; + let event_name_lowercase = event_name.to_ascii_lowercase(); + let target_unique_id = bubble_unique_id_path.first().cloned().unwrap_or_default(); + + let target_element_dataset = + if let Some(binding) = self.get_element_data_by_unique_id(target_unique_id) { + binding.borrow().dataset.clone() + } else { + None + }; + + if let Some(global_bind_ids) = self.global_bind_events.get(&event_name_lowercase) { + for unique_id in global_bind_ids { + let binding = match self.get_element_data_by_unique_id(*unique_id) { + Some(b) => b, + None => continue, + }; + let current_target_element_data = binding.borrow(); + + let bind_handler = current_target_element_data + .get_framework_cross_thread_event_handler(&event_name_lowercase, "global-bindevent") + .or_else(|| { + current_target_element_data + .get_framework_cross_thread_event_handler(&event_name_lowercase, "global-bind") + }); + + if let Some(handler) = bind_handler { + let current_target_parent_component_id = { + let parent_component_unique_id = current_target_element_data.parent_component_unique_id; + if self.page_element_unique_id == Some(parent_component_unique_id) { + None + } else { + self + .get_element_data_by_unique_id(parent_component_unique_id) + .and_then(|binding| binding.borrow().component_id.clone()) + } + }; + self.mts_binding.publish_event( + &handler, + current_target_parent_component_id.as_deref(), + serialized_event, + target_unique_id, + &target_element_dataset.clone().into(), + *unique_id, + ¤t_target_element_data.dataset.clone().into(), + ); + } + + let run_worklet_handler = current_target_element_data + .get_framework_run_worklet_event_handler(&event_name_lowercase, "global-bind"); + if let Some(handler) = run_worklet_handler { + self.mts_binding.publish_mts_event( + &handler, + serialized_event, + target_unique_id, + &target_element_dataset.clone().into(), + *unique_id, + ¤t_target_element_data.dataset.clone().into(), + ); + } + } + } } } diff --git a/packages/web-platform/web-core/src/main_thread/client/main_thread_context.rs b/packages/web-platform/web-core/src/main_thread/client/main_thread_context.rs index ebac923184..d67f7e8c03 100644 --- a/packages/web-platform/web-core/src/main_thread/client/main_thread_context.rs +++ b/packages/web-platform/web-core/src/main_thread/client/main_thread_context.rs @@ -25,6 +25,7 @@ pub struct MainThreadWasmContext { pub(super) mts_binding: RustMainthreadContextBinding, pub(super) config_enable_css_selector: bool, pub(super) style_manager: StyleManager, + pub(super) global_bind_events: FnvHashMap>, } impl MainThreadWasmContext { @@ -57,6 +58,7 @@ impl MainThreadWasmContext { page_element_unique_id: None, config_enable_css_selector, style_manager, + global_bind_events: FnvHashMap::default(), } } diff --git a/packages/web-platform/web-core/tests/element-apis.spec.ts b/packages/web-platform/web-core/tests/element-apis.spec.ts index 844204d601..22fd7e8c67 100644 --- a/packages/web-platform/web-core/tests/element-apis.spec.ts +++ b/packages/web-platform/web-core/tests/element-apis.spec.ts @@ -1461,6 +1461,55 @@ describe('Element APIs', () => { expect(disableSpy).toHaveBeenCalledWith(expect.anything(), 'input'); }); + test('should handle global-bind events', () => { + const root = mtsGlobalThis.__CreatePage('page', 0); + const element1 = mtsGlobalThis.__CreateView(0); + const element2 = mtsGlobalThis.__CreateView(0); + mtsGlobalThis.__AppendElement(root, element1); + mtsGlobalThis.__AppendElement(root, element2); + mtsGlobalThis.__SetID(element1, 'global_bind_target'); + mtsGlobalThis.__SetID(element2, 'global_bind_watcher'); + mtsGlobalThis.__FlushElementTree(); + + const publishSpy = vi.spyOn(mtsBinding, 'publishEvent'); + + // Register global-bind on element2 + mtsGlobalThis.__AddEvent(element2, 'global-bind', 'tap', 'global-handler'); + + // Get events should include global-bind + const events = mtsGlobalThis.__GetEvents(element2); + const found = events.some((e: any) => + e.event_name === 'tap' && e.event_type === 'global-bind' + ); + expect(found).toBe(true); + + // Simulate event on element1 + rootDom.querySelector('#global_bind_target')?.dispatchEvent( + new window.Event('click', { bubbles: true }), + ); + + expect(publishSpy).toHaveBeenCalledTimes(1); + expect(publishSpy).toHaveBeenCalledWith( + 'global-handler', + undefined, + expect.any(Object), + expect.any(Number), + undefined, + expect.any(Number), + undefined, + ); + + // Unregister global-bind + mtsGlobalThis.__AddEvent(element2, 'global-bind', 'tap', null as any); + publishSpy.mockClear(); + + // Simulate event on element1 again, should not broadcast + rootDom.querySelector('#global_bind_target')?.dispatchEvent( + new window.Event('click', { bubbles: true }), + ); + expect(publishSpy).toHaveBeenCalledTimes(0); + }); + test('getClassList', () => { const root = mtsGlobalThis.__CreatePage('page', 0); const element = mtsGlobalThis.__CreateView(0); From b81ef695e2b0e77048f995e6816ff82f39a150b3 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:40:12 +0800 Subject: [PATCH 2/8] + fix --- .../src/main_thread/client/element_apis/event_apis.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs index 6342eccf88..14b7894076 100644 --- a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs +++ b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs @@ -42,10 +42,8 @@ impl MainThreadWasmContext { .entry(event_name.clone()) .or_default() .insert(unique_id); - } else { - if let Some(set) = self.global_bind_events.get_mut(&event_name) { - set.remove(&unique_id); - } + } else if let Some(set) = self.global_bind_events.get_mut(&event_name) { + set.remove(&unique_id); } } From bfaf1389df3fee49588c3a8f61213ab9729be869 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:15:38 +0800 Subject: [PATCH 3/8] + fix --- .../client/element_apis/event_apis.rs | 44 +++++++------------ .../web-core/tests/element-apis.spec.ts | 15 ++++--- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs index 14b7894076..986b1010d3 100644 --- a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs +++ b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs @@ -35,16 +35,14 @@ impl MainThreadWasmContext { let event_type = event_type.to_ascii_lowercase(); self.enable_event(&event_name); - if event_type == "global-bindevent" || event_type == "global-bind" { - if event_handler_identifier.is_some() { - self - .global_bind_events - .entry(event_name.clone()) - .or_default() - .insert(unique_id); - } else if let Some(set) = self.global_bind_events.get_mut(&event_name) { - set.remove(&unique_id); - } + if (event_type == "global-bindevent" || event_type == "global-bind") + && event_handler_identifier.is_some() + { + self + .global_bind_events + .entry(event_name.clone()) + .or_default() + .insert(unique_id); } let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str); @@ -96,16 +94,12 @@ impl MainThreadWasmContext { let event_type = event_type.to_ascii_lowercase(); self.enable_event(&event_name); - if event_type == "global-bindevent" { - if event_handler_identifier.is_some() { - self - .global_bind_events - .entry(event_name.clone()) - .or_default() - .insert(unique_id); - } else if let Some(set) = self.global_bind_events.get_mut(&event_name) { - set.remove(&unique_id); - } + if event_type == "global-bindevent" && event_handler_identifier.is_some() { + self + .global_bind_events + .entry(event_name.clone()) + .or_default() + .insert(unique_id); } let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str); @@ -167,7 +161,7 @@ impl MainThreadWasmContext { "capture-bind", "catchevent", "capture-catch", - "global-bind", + "global-bindevent", ]; let binding = self.get_element_data_by_unique_id(unique_id).unwrap(); let element_data = binding.borrow(); @@ -358,11 +352,7 @@ impl MainThreadWasmContext { let current_target_element_data = binding.borrow(); let bind_handler = current_target_element_data - .get_framework_cross_thread_event_handler(&event_name_lowercase, "global-bindevent") - .or_else(|| { - current_target_element_data - .get_framework_cross_thread_event_handler(&event_name_lowercase, "global-bind") - }); + .get_framework_cross_thread_event_handler(&event_name_lowercase, "global-bindevent"); if let Some(handler) = bind_handler { let current_target_parent_component_id = { @@ -387,7 +377,7 @@ impl MainThreadWasmContext { } let run_worklet_handler = current_target_element_data - .get_framework_run_worklet_event_handler(&event_name_lowercase, "global-bind"); + .get_framework_run_worklet_event_handler(&event_name_lowercase, "global-bindevent"); if let Some(handler) = run_worklet_handler { self.mts_binding.publish_mts_event( &handler, diff --git a/packages/web-platform/web-core/tests/element-apis.spec.ts b/packages/web-platform/web-core/tests/element-apis.spec.ts index 22fd7e8c67..91d376a775 100644 --- a/packages/web-platform/web-core/tests/element-apis.spec.ts +++ b/packages/web-platform/web-core/tests/element-apis.spec.ts @@ -1474,12 +1474,17 @@ describe('Element APIs', () => { const publishSpy = vi.spyOn(mtsBinding, 'publishEvent'); // Register global-bind on element2 - mtsGlobalThis.__AddEvent(element2, 'global-bind', 'tap', 'global-handler'); + mtsGlobalThis.__AddEvent( + element2, + 'global-bindevent', + 'tap', + 'global-handler', + ); - // Get events should include global-bind + // Get events should include global-bindevent const events = mtsGlobalThis.__GetEvents(element2); const found = events.some((e: any) => - e.event_name === 'tap' && e.event_type === 'global-bind' + e.event_name === 'tap' && e.event_type === 'global-bindevent' ); expect(found).toBe(true); @@ -1499,8 +1504,8 @@ describe('Element APIs', () => { undefined, ); - // Unregister global-bind - mtsGlobalThis.__AddEvent(element2, 'global-bind', 'tap', null as any); + // Unregister global-bindevent + mtsGlobalThis.__AddEvent(element2, 'global-bindevent', 'tap', null as any); publishSpy.mockClear(); // Simulate event on element1 again, should not broadcast From 08055fb772cd35ab654daead0fcc44a9111154bd Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:27:19 +0800 Subject: [PATCH 4/8] + fix --- .../src/main_thread/client/element_apis/event_apis.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs index 986b1010d3..fa033e6c31 100644 --- a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs +++ b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs @@ -325,14 +325,6 @@ impl MainThreadWasmContext { event_name: &str, serialized_event: &JsValue, ) { - let event_name = match event_name { - "click" => "tap", - "touchstart" => "touchstart", - "touchmove" => "touchmove", - "touchend" => "touchend", - "touchcancel" => "touchcancel", - _ => event_name, - }; let event_name_lowercase = event_name.to_ascii_lowercase(); let target_unique_id = bubble_unique_id_path.first().cloned().unwrap_or_default(); From 806b09d443ebc81e095c9ae7848c48cb649cb292 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:54:35 +0800 Subject: [PATCH 5/8] + fix --- .../web-platform/web-core/src/constants.rs | 8 ++++ .../client/element_apis/event_apis.rs | 46 +++++++++++-------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/packages/web-platform/web-core/src/constants.rs b/packages/web-platform/web-core/src/constants.rs index 1ace97c68d..32521fc388 100644 --- a/packages/web-platform/web-core/src/constants.rs +++ b/packages/web-platform/web-core/src/constants.rs @@ -103,4 +103,12 @@ lazy_static::lazy_static! { "scrolltoupperedge", "scrolltoloweredge" ]); + + pub static ref EVENT_TYPES: Vec<&'static str> = vec![ + "bindevent", + "capture-bind", + "catchevent", + "capture-catch", + "global-bindevent", + ]; } diff --git a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs index fa033e6c31..cc9a103fc1 100644 --- a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs +++ b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs @@ -35,16 +35,6 @@ impl MainThreadWasmContext { let event_type = event_type.to_ascii_lowercase(); self.enable_event(&event_name); - if (event_type == "global-bindevent" || event_type == "global-bind") - && event_handler_identifier.is_some() - { - self - .global_bind_events - .entry(event_name.clone()) - .or_default() - .insert(unique_id); - } - let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str); let mut should_enable = false; let mut should_disable = false; @@ -63,9 +53,32 @@ impl MainThreadWasmContext { element_data.replace_framework_cross_thread_event_handler( event_name.clone(), - event_type, - event_handler_identifier, + event_type.clone(), + event_handler_identifier.clone(), ); + + if event_type == "global-bindevent" { + if event_handler_identifier.is_some() { + self + .global_bind_events + .entry(event_name.clone()) + .or_default() + .insert(unique_id); + } + if element_data + .get_framework_cross_thread_event_handler(&event_name, &event_type) + .is_none() + && element_data + .get_framework_run_worklet_event_handler(&event_name, &event_type) + .is_none() + { + self + .global_bind_events + .entry(event_name.clone()) + .or_default() + .remove(&unique_id); + } + } } if should_enable { if let Some(element) = self.unique_id_to_dom_map.get(&unique_id) { @@ -156,16 +169,9 @@ impl MainThreadWasmContext { pub fn get_events(&self, unique_id: usize) -> Vec { let mut event_infos: Vec = vec![]; - let event_types = vec![ - "bindevent", - "capture-bind", - "catchevent", - "capture-catch", - "global-bindevent", - ]; let binding = self.get_element_data_by_unique_id(unique_id).unwrap(); let element_data = binding.borrow(); - for event_type in event_types { + for event_type in constants::EVENT_TYPES.iter() { for event_name in self.enabled_events.iter() { if let Some(event_handlers) = element_data.get_framework_cross_thread_event_handler(event_name, event_type) From cd17a169a7e67b3525f67e55e89cdbff5008cc49 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:20:25 +0800 Subject: [PATCH 6/8] + fix --- .../client/element_apis/event_apis.rs | 68 ++++++++++--------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs index cc9a103fc1..6aae405251 100644 --- a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs +++ b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs @@ -35,6 +35,8 @@ impl MainThreadWasmContext { let event_type = event_type.to_ascii_lowercase(); self.enable_event(&event_name); + let has_handler = event_handler_identifier.is_some(); + let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str); let mut should_enable = false; let mut should_disable = false; @@ -54,32 +56,14 @@ impl MainThreadWasmContext { element_data.replace_framework_cross_thread_event_handler( event_name.clone(), event_type.clone(), - event_handler_identifier.clone(), + event_handler_identifier, ); + } - if event_type == "global-bindevent" { - if event_handler_identifier.is_some() { - self - .global_bind_events - .entry(event_name.clone()) - .or_default() - .insert(unique_id); - } - if element_data - .get_framework_cross_thread_event_handler(&event_name, &event_type) - .is_none() - && element_data - .get_framework_run_worklet_event_handler(&event_name, &event_type) - .is_none() - { - self - .global_bind_events - .entry(event_name.clone()) - .or_default() - .remove(&unique_id); - } - } + if event_type == "global-bindevent" { + self.update_global_bind_events(unique_id, &event_name, has_handler); } + if should_enable { if let Some(element) = self.unique_id_to_dom_map.get(&unique_id) { self @@ -107,13 +91,7 @@ impl MainThreadWasmContext { let event_type = event_type.to_ascii_lowercase(); self.enable_event(&event_name); - if event_type == "global-bindevent" && event_handler_identifier.is_some() { - self - .global_bind_events - .entry(event_name.clone()) - .or_default() - .insert(unique_id); - } + let has_handler = event_handler_identifier.is_some(); let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str); let mut should_enable = false; @@ -133,10 +111,15 @@ impl MainThreadWasmContext { element_data.replace_framework_run_worklet_event_handler( event_name.clone(), - event_type, + event_type.clone(), event_handler_identifier, ); } + + if event_type == "global-bindevent" { + self.update_global_bind_events(unique_id, &event_name, has_handler); + } + if should_enable { if let Some(element) = self.unique_id_to_dom_map.get(&unique_id) { self @@ -152,6 +135,29 @@ impl MainThreadWasmContext { } } + fn update_global_bind_events(&mut self, unique_id: usize, event_name: &str, has_handler: bool) { + if has_handler { + self + .global_bind_events + .entry(event_name.to_string()) + .or_default() + .insert(unique_id); + } else if let Some(binding) = self.get_element_data_by_unique_id(unique_id) { + let element_data = binding.borrow(); + if element_data + .get_framework_cross_thread_event_handler(event_name, "global-bindevent") + .is_none() + && element_data + .get_framework_run_worklet_event_handler(event_name, "global-bindevent") + .is_none() + { + if let Some(ids) = self.global_bind_events.get_mut(event_name) { + ids.remove(&unique_id); + } + } + } + } + pub fn get_event( &self, unique_id: usize, From 4daebc87184c71b6a30ecff6826b28857decf25d Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:31:45 +0800 Subject: [PATCH 7/8] refactor: decouple global event iteration from collection lookup to improve readability and safety --- .../client/element_apis/event_apis.rs | 90 ++++++++++--------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs index 6aae405251..b05c571e61 100644 --- a/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs +++ b/packages/web-platform/web-core/src/main_thread/client/element_apis/event_apis.rs @@ -347,51 +347,55 @@ impl MainThreadWasmContext { None }; - if let Some(global_bind_ids) = self.global_bind_events.get(&event_name_lowercase) { - for unique_id in global_bind_ids { - let binding = match self.get_element_data_by_unique_id(*unique_id) { - Some(b) => b, - None => continue, - }; - let current_target_element_data = binding.borrow(); - - let bind_handler = current_target_element_data - .get_framework_cross_thread_event_handler(&event_name_lowercase, "global-bindevent"); + let global_bind_ids: Vec = self + .global_bind_events + .get(&event_name_lowercase) + .map(|ids| ids.iter().copied().collect()) + .unwrap_or_default(); + + for unique_id in global_bind_ids { + let binding = match self.get_element_data_by_unique_id(unique_id) { + Some(b) => b, + None => continue, + }; + let current_target_element_data = binding.borrow(); - if let Some(handler) = bind_handler { - let current_target_parent_component_id = { - let parent_component_unique_id = current_target_element_data.parent_component_unique_id; - if self.page_element_unique_id == Some(parent_component_unique_id) { - None - } else { - self - .get_element_data_by_unique_id(parent_component_unique_id) - .and_then(|binding| binding.borrow().component_id.clone()) - } - }; - self.mts_binding.publish_event( - &handler, - current_target_parent_component_id.as_deref(), - serialized_event, - target_unique_id, - &target_element_dataset.clone().into(), - *unique_id, - ¤t_target_element_data.dataset.clone().into(), - ); - } + let bind_handler = current_target_element_data + .get_framework_cross_thread_event_handler(&event_name_lowercase, "global-bindevent"); + + if let Some(handler) = bind_handler { + let current_target_parent_component_id = { + let parent_component_unique_id = current_target_element_data.parent_component_unique_id; + if self.page_element_unique_id == Some(parent_component_unique_id) { + None + } else { + self + .get_element_data_by_unique_id(parent_component_unique_id) + .and_then(|binding| binding.borrow().component_id.clone()) + } + }; + self.mts_binding.publish_event( + &handler, + current_target_parent_component_id.as_deref(), + serialized_event, + target_unique_id, + &target_element_dataset.clone().into(), + unique_id, + ¤t_target_element_data.dataset.clone().into(), + ); + } - let run_worklet_handler = current_target_element_data - .get_framework_run_worklet_event_handler(&event_name_lowercase, "global-bindevent"); - if let Some(handler) = run_worklet_handler { - self.mts_binding.publish_mts_event( - &handler, - serialized_event, - target_unique_id, - &target_element_dataset.clone().into(), - *unique_id, - ¤t_target_element_data.dataset.clone().into(), - ); - } + let run_worklet_handler = current_target_element_data + .get_framework_run_worklet_event_handler(&event_name_lowercase, "global-bindevent"); + if let Some(handler) = run_worklet_handler { + self.mts_binding.publish_mts_event( + &handler, + serialized_event, + target_unique_id, + &target_element_dataset.clone().into(), + unique_id, + ¤t_target_element_data.dataset.clone().into(), + ); } } } From 0601e37f370605fbab3f3fa11fc8cd1ba2fc2d8c Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 9 Apr 2026 20:13:12 +0800 Subject: [PATCH 8/8] test: add test case for global-bind events with run-worklet handlers --- .../web-core/tests/element-apis.spec.ts | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/web-platform/web-core/tests/element-apis.spec.ts b/packages/web-platform/web-core/tests/element-apis.spec.ts index 91d376a775..2797e3fb26 100644 --- a/packages/web-platform/web-core/tests/element-apis.spec.ts +++ b/packages/web-platform/web-core/tests/element-apis.spec.ts @@ -1461,7 +1461,7 @@ describe('Element APIs', () => { expect(disableSpy).toHaveBeenCalledWith(expect.anything(), 'input'); }); - test('should handle global-bind events', () => { + test('should handle global-bind events for cross-thread handlers', () => { const root = mtsGlobalThis.__CreatePage('page', 0); const element1 = mtsGlobalThis.__CreateView(0); const element2 = mtsGlobalThis.__CreateView(0); @@ -1515,6 +1515,59 @@ describe('Element APIs', () => { expect(publishSpy).toHaveBeenCalledTimes(0); }); + test('should handle global-bind events for run-worklet handlers', () => { + const root = mtsGlobalThis.__CreatePage('page', 0); + const element1 = mtsGlobalThis.__CreateView(0); + const element2 = mtsGlobalThis.__CreateView(0); + mtsGlobalThis.__AppendElement(root, element1); + mtsGlobalThis.__AppendElement(root, element2); + mtsGlobalThis.__SetID(element1, 'global_bind_target_worklet'); + mtsGlobalThis.__SetID(element2, 'global_bind_watcher_worklet'); + mtsGlobalThis.__FlushElementTree(); + + const runWorkletSpy = vi.spyOn(mtsBinding, 'runWorklet'); + + // Register global-bind on element2 + mtsGlobalThis.__AddEvent( + element2, + 'global-bindevent', + 'tap', + { name: 'worklet-handler' } as any, + ); + + // Get events should include global-bindevent + const events = mtsGlobalThis.__GetEvents(element2); + const found = events.some((e: any) => + e.event_name === 'tap' && e.event_type === 'global-bindevent' + ); + expect(found).toBe(true); + + // Simulate event on element1 + rootDom.querySelector('#global_bind_target_worklet')?.dispatchEvent( + new window.Event('click', { bubbles: true }), + ); + + expect(runWorkletSpy).toHaveBeenCalledTimes(1); + expect(runWorkletSpy).toHaveBeenCalledWith( + { name: 'worklet-handler' }, + expect.any(Object), + expect.any(Number), + undefined, + expect.any(Number), + undefined, + ); + + // Unregister global-bindevent + mtsGlobalThis.__AddEvent(element2, 'global-bindevent', 'tap', null as any); + runWorkletSpy.mockClear(); + + // Simulate event on element1 again, should not broadcast + rootDom.querySelector('#global_bind_target_worklet')?.dispatchEvent( + new window.Event('click', { bubbles: true }), + ); + expect(runWorkletSpy).toHaveBeenCalledTimes(0); + }); + test('getClassList', () => { const root = mtsGlobalThis.__CreatePage('page', 0); const element = mtsGlobalThis.__CreateView(0);