fusee-framework 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/app/components/Counter.ts +1 -1
- package/app/main.ts +6 -6
- package/framework/core/directives.js +157 -3
- package/framework/core/event-delegation.js +172 -0
- package/framework/core/signal.js +0 -1
- package/framework/types/component.d.ts +0 -4
- package/framework/types/event-delegation.d.ts +90 -0
- package/framework/types/index.d.ts +3 -0
- package/index.html +63 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Fusée Framework 🚀
|
|
2
|
-
**v1.
|
|
2
|
+
**v1.6.0 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
|
|
3
3
|
|
|
4
4
|
Fusée is a custom, high-performance fine-grained reactive JavaScript framework built for speed and simplicity. It features a recursive, non-greedy compiler, a signals-based reactivity engine, Dependency Injection for components and a comprehensive CLI for instant application scaffolding.
|
|
5
5
|
|
|
@@ -80,7 +80,7 @@ export const Welcome = defineComponent({
|
|
|
80
80
|
|
|
81
81
|
Fusée is built with a test-driven mindset to ensure the reliability of its reactivity engine and component lifecycle.
|
|
82
82
|
|
|
83
|
-
- **Total Tests:** `
|
|
83
|
+
- **Total Tests:** `385`
|
|
84
84
|
- **Status:** `All Passed`
|
|
85
85
|
- **Framework:** [Vitest](https://vitest.dev/)
|
|
86
86
|
- **Environment:** JSDOM (Browser simulation)
|
|
@@ -106,6 +106,7 @@ npx vitest
|
|
|
106
106
|
* **Component System:** Props, Slots, Async Components, and Lifecycle Hooks.
|
|
107
107
|
* **Dependency Injection:** Nested Provide/Inject and Shadowing.
|
|
108
108
|
* **Directives:** `f-if`, `f-for`, `f-model`, and Event Modifiers.
|
|
109
|
+
* **Events:** Event delegation and native event handling.
|
|
109
110
|
* **Memory Safety:** Explicit checks for memory leaks and automatic effect disposal.
|
|
110
111
|
* **Type Safety:** Comprehensive TypeScript definitions and type tests.
|
|
111
112
|
* **Integration Tests:** Advanced module combinations and edge cases.
|
|
@@ -23,7 +23,7 @@ export const Counter = defineComponent({
|
|
|
23
23
|
initialValue: { type: String, default: '0' },
|
|
24
24
|
parentTitle: { type: String, default: '' }
|
|
25
25
|
},
|
|
26
|
-
setup(props: CounterProps): CounterResult {
|
|
26
|
+
setup(props: CounterProps, { emit }: { emit: (eventName: string, ...args: any[]) => void }): CounterResult {
|
|
27
27
|
const count = signal<number>(Number(props.initialValue) || 0)
|
|
28
28
|
const multiplier = signal<number>(1)
|
|
29
29
|
inspect(count, multiplier)
|
package/app/main.ts
CHANGED
|
@@ -33,19 +33,19 @@ const routes: Route[] = [
|
|
|
33
33
|
loadingComponent: Loading
|
|
34
34
|
})
|
|
35
35
|
},
|
|
36
|
-
{
|
|
37
|
-
path: '/test-for',
|
|
36
|
+
{
|
|
37
|
+
path: '/test-for',
|
|
38
38
|
component: defineAsyncComponent({
|
|
39
39
|
loader: () => import('./routes/TestFor.ts'),
|
|
40
40
|
loadingComponent: Loading
|
|
41
|
-
})
|
|
41
|
+
})
|
|
42
42
|
},
|
|
43
|
-
{
|
|
44
|
-
path: '/dynamic',
|
|
43
|
+
{
|
|
44
|
+
path: '/dynamic',
|
|
45
45
|
component: defineAsyncComponent({
|
|
46
46
|
loader: () => import('./routes/TestDynamic.ts'),
|
|
47
47
|
loadingComponent: Loading
|
|
48
|
-
})
|
|
48
|
+
})
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
path: '/composables',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { signal, effect, batch } from './signal.js'
|
|
2
2
|
import { evaluateExpression } from './evaluator.js'
|
|
3
3
|
import { compileNode } from './compiler.js'
|
|
4
|
+
import { registerDelegatedEvent, createEventHandler, isDelegatedEvent } from './event-delegation.js'
|
|
4
5
|
|
|
5
6
|
const customDirectives = new Map()
|
|
6
7
|
|
|
@@ -469,8 +470,9 @@ export function processEvents(el, context, effects) {
|
|
|
469
470
|
if (el.nodeType !== 1 || !el.attributes) return
|
|
470
471
|
|
|
471
472
|
for (const attr of [...el.attributes]) {
|
|
472
|
-
|
|
473
|
-
|
|
473
|
+
// Handle native events: on:click, on:scroll, on:custom-event
|
|
474
|
+
if (attr.name.startsWith('on:')) {
|
|
475
|
+
const fullEventName = attr.name.slice(3) // Remove 'on:' prefix
|
|
474
476
|
const parts = fullEventName.split('.')
|
|
475
477
|
const eventName = parts[0]
|
|
476
478
|
const modifiers = parts.slice(1)
|
|
@@ -491,7 +493,7 @@ export function processEvents(el, context, effects) {
|
|
|
491
493
|
if (next.endsWith('ms')) return parseInt(next) || 250
|
|
492
494
|
if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
|
|
493
495
|
}
|
|
494
|
-
return 250
|
|
496
|
+
return 250
|
|
495
497
|
}
|
|
496
498
|
|
|
497
499
|
const debounceIdx = modifiers.indexOf('debounce')
|
|
@@ -569,6 +571,158 @@ export function processEvents(el, context, effects) {
|
|
|
569
571
|
if (throttleTimeoutId) clearTimeout(throttleTimeoutId)
|
|
570
572
|
})
|
|
571
573
|
}
|
|
574
|
+
continue
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Handle delegated events: @click, @input, etc.
|
|
578
|
+
if (attr.name.startsWith('@')) {
|
|
579
|
+
const fullEventName = attr.name.slice(1) // Remove '@' prefix
|
|
580
|
+
const parts = fullEventName.split('.')
|
|
581
|
+
const eventName = parts[0]
|
|
582
|
+
const modifiers = parts.slice(1)
|
|
583
|
+
const expr = attr.value
|
|
584
|
+
|
|
585
|
+
el.removeAttribute(attr.name)
|
|
586
|
+
|
|
587
|
+
const targetNode = modifiers.includes('window') ? window : (modifiers.includes('document') ? document : null)
|
|
588
|
+
|
|
589
|
+
// Window/document events are always native (can't delegate to window/document from document)
|
|
590
|
+
if (targetNode) {
|
|
591
|
+
// Native handler for window/document events
|
|
592
|
+
const handler = createEventHandler(
|
|
593
|
+
typeof context[expr] === 'function' && !context[expr].isSignal
|
|
594
|
+
? context[expr]
|
|
595
|
+
: expr,
|
|
596
|
+
modifiers,
|
|
597
|
+
context,
|
|
598
|
+
expr
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
const options = {
|
|
602
|
+
once: modifiers.includes('once'),
|
|
603
|
+
capture: modifiers.includes('capture') || modifiers.includes('away')
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
targetNode.addEventListener(eventName, handler, options)
|
|
607
|
+
if (effects) {
|
|
608
|
+
effects.push(() => {
|
|
609
|
+
targetNode.removeEventListener(eventName, handler, options)
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
} else if (isDelegatedEvent(eventName) && !modifiers.includes('capture') && !modifiers.includes('away')) {
|
|
613
|
+
// Use event delegation for supported events
|
|
614
|
+
const potentialFn = context[expr]
|
|
615
|
+
const isFunction = typeof potentialFn === 'function' && !potentialFn.isSignal
|
|
616
|
+
|
|
617
|
+
const wrappedHandler = createEventHandler(
|
|
618
|
+
isFunction ? potentialFn : expr,
|
|
619
|
+
modifiers,
|
|
620
|
+
context,
|
|
621
|
+
expr
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
const cleanup = registerDelegatedEvent(el, eventName, wrappedHandler, {
|
|
625
|
+
modifiers,
|
|
626
|
+
context,
|
|
627
|
+
expr
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
if (effects) {
|
|
631
|
+
effects.push(cleanup)
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
// Fallback to native for non-delegated events or when capture/away modifiers are used
|
|
635
|
+
let timeoutId
|
|
636
|
+
let lastRun = 0
|
|
637
|
+
let throttleTimeoutId
|
|
638
|
+
|
|
639
|
+
let debounceMs = 0
|
|
640
|
+
let throttleMs = 0
|
|
641
|
+
|
|
642
|
+
const parseDuration = (idx) => {
|
|
643
|
+
const next = modifiers[idx + 1]
|
|
644
|
+
if (next && /^\d/.test(next)) {
|
|
645
|
+
if (next.endsWith('ms')) return parseInt(next) || 250
|
|
646
|
+
if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
|
|
647
|
+
}
|
|
648
|
+
return 250
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const debounceIdx = modifiers.indexOf('debounce')
|
|
652
|
+
if (debounceIdx > -1) debounceMs = parseDuration(debounceIdx)
|
|
653
|
+
|
|
654
|
+
const throttleIdx = modifiers.indexOf('throttle')
|
|
655
|
+
if (throttleIdx > -1) throttleMs = parseDuration(throttleIdx)
|
|
656
|
+
|
|
657
|
+
const executeLogic = (e) => {
|
|
658
|
+
batch(() => {
|
|
659
|
+
const potentialFn = context[expr]
|
|
660
|
+
if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
|
|
661
|
+
potentialFn(e)
|
|
662
|
+
} else {
|
|
663
|
+
evaluateExpression(expr, context, { $event: e }, false)
|
|
664
|
+
}
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const handler = (e) => {
|
|
669
|
+
if (modifiers.includes('prevent')) e.preventDefault()
|
|
670
|
+
if (modifiers.includes('stop')) e.stopPropagation()
|
|
671
|
+
if (modifiers.includes('self') && e.target !== el) return
|
|
672
|
+
|
|
673
|
+
if (e instanceof KeyboardEvent) {
|
|
674
|
+
const key = e.key.toLowerCase()
|
|
675
|
+
const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
|
|
676
|
+
if (keyModifiers.length > 0) {
|
|
677
|
+
const match = keyModifiers.some(m => {
|
|
678
|
+
if (m === 'escape') return key === 'escape' || key === 'esc'
|
|
679
|
+
if (m === 'space') return key === ' ' || key === 'spacebar'
|
|
680
|
+
return key === m
|
|
681
|
+
})
|
|
682
|
+
if (!match) return
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (throttleMs > 0) {
|
|
687
|
+
const now = Date.now()
|
|
688
|
+
const remaining = throttleMs - (now - lastRun)
|
|
689
|
+
|
|
690
|
+
if (remaining <= 0) {
|
|
691
|
+
clearTimeout(throttleTimeoutId)
|
|
692
|
+
lastRun = now
|
|
693
|
+
executeLogic(e)
|
|
694
|
+
} else {
|
|
695
|
+
clearTimeout(throttleTimeoutId)
|
|
696
|
+
throttleTimeoutId = setTimeout(() => {
|
|
697
|
+
lastRun = Date.now()
|
|
698
|
+
executeLogic(e)
|
|
699
|
+
}, remaining)
|
|
700
|
+
}
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (debounceMs > 0) {
|
|
705
|
+
clearTimeout(timeoutId)
|
|
706
|
+
timeoutId = setTimeout(() => executeLogic(e), debounceMs)
|
|
707
|
+
} else {
|
|
708
|
+
executeLogic(e)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const options = {
|
|
713
|
+
once: modifiers.includes('once'),
|
|
714
|
+
capture: modifiers.includes('capture') || modifiers.includes('away')
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
el.addEventListener(eventName, handler, options)
|
|
718
|
+
if (effects) {
|
|
719
|
+
effects.push(() => {
|
|
720
|
+
el.removeEventListener(eventName, handler, options)
|
|
721
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
722
|
+
if (throttleTimeoutId) clearTimeout(throttleTimeoutId)
|
|
723
|
+
})
|
|
724
|
+
}
|
|
725
|
+
}
|
|
572
726
|
}
|
|
573
727
|
}
|
|
574
728
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { batch } from './signal.js'
|
|
2
|
+
import { evaluateExpression } from './evaluator.js'
|
|
3
|
+
|
|
4
|
+
export const DELEGATED_EVENTS = new Set([
|
|
5
|
+
'beforeinput',
|
|
6
|
+
'click',
|
|
7
|
+
'dblclick',
|
|
8
|
+
'contextmenu',
|
|
9
|
+
'focusin',
|
|
10
|
+
'focusout',
|
|
11
|
+
'input',
|
|
12
|
+
'keydown',
|
|
13
|
+
'keyup',
|
|
14
|
+
'mousedown',
|
|
15
|
+
'mousemove',
|
|
16
|
+
'mouseout',
|
|
17
|
+
'mouseover',
|
|
18
|
+
'mouseup',
|
|
19
|
+
'pointerdown',
|
|
20
|
+
'pointermove',
|
|
21
|
+
'pointerout',
|
|
22
|
+
'pointerover',
|
|
23
|
+
'pointerup',
|
|
24
|
+
'touchend',
|
|
25
|
+
'touchmove',
|
|
26
|
+
'touchstart'
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
const delegatedListeners = new Map()
|
|
30
|
+
const documentListeners = new Map()
|
|
31
|
+
|
|
32
|
+
export function isDelegatedEvent(eventName) {
|
|
33
|
+
return DELEGATED_EVENTS.has(eventName.toLowerCase())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureDocumentListener(eventType) {
|
|
37
|
+
if (documentListeners.has(eventType)) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handler = (e) => {
|
|
42
|
+
const listeners = delegatedListeners.get(eventType)
|
|
43
|
+
if (!listeners || listeners.size === 0) return
|
|
44
|
+
|
|
45
|
+
let target = e.target
|
|
46
|
+
while (target && target !== document.body && target !== document.documentElement) {
|
|
47
|
+
for (const entry of listeners) {
|
|
48
|
+
if (entry.element === target) {
|
|
49
|
+
entry.handler(e)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
target = target.parentNode
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
document.addEventListener(eventType, handler, true)
|
|
57
|
+
documentListeners.set(eventType, handler)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerDelegatedEvent(element, eventType, handler, options = {}) {
|
|
61
|
+
const { modifiers = [], context, expr } = options
|
|
62
|
+
const normalizedEvent = eventType.toLowerCase()
|
|
63
|
+
|
|
64
|
+
ensureDocumentListener(normalizedEvent)
|
|
65
|
+
|
|
66
|
+
if (!delegatedListeners.has(normalizedEvent))
|
|
67
|
+
delegatedListeners.set(normalizedEvent, new Set())
|
|
68
|
+
|
|
69
|
+
const entry = {
|
|
70
|
+
element,
|
|
71
|
+
handler,
|
|
72
|
+
modifiers,
|
|
73
|
+
context,
|
|
74
|
+
expr,
|
|
75
|
+
cleanup: null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
delegatedListeners.get(normalizedEvent).add(entry)
|
|
79
|
+
|
|
80
|
+
return function cleanup() {
|
|
81
|
+
const listeners = delegatedListeners.get(normalizedEvent)
|
|
82
|
+
if (listeners) {
|
|
83
|
+
listeners.delete(entry)
|
|
84
|
+
if (listeners.size === 0) {
|
|
85
|
+
const docHandler = documentListeners.get(normalizedEvent)
|
|
86
|
+
if (docHandler) {
|
|
87
|
+
document.removeEventListener(normalizedEvent, docHandler, true)
|
|
88
|
+
documentListeners.delete(normalizedEvent)
|
|
89
|
+
}
|
|
90
|
+
delegatedListeners.delete(normalizedEvent)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createEventHandler(handler, modifiers, context, expr) {
|
|
97
|
+
let timeoutId
|
|
98
|
+
let lastRun = 0
|
|
99
|
+
let throttleTimeoutId
|
|
100
|
+
|
|
101
|
+
let debounceMs = 0
|
|
102
|
+
let throttleMs = 0
|
|
103
|
+
|
|
104
|
+
const parseDuration = (idx) => {
|
|
105
|
+
const next = modifiers[idx + 1]
|
|
106
|
+
if (next && /^\d/.test(next)) {
|
|
107
|
+
if (next.endsWith('ms')) return parseInt(next) || 250
|
|
108
|
+
if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
|
|
109
|
+
}
|
|
110
|
+
return 250
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const debounceIdx = modifiers.indexOf('debounce')
|
|
114
|
+
if (debounceIdx > -1) debounceMs = parseDuration(debounceIdx)
|
|
115
|
+
|
|
116
|
+
const throttleIdx = modifiers.indexOf('throttle')
|
|
117
|
+
if (throttleIdx > -1) throttleMs = parseDuration(throttleIdx)
|
|
118
|
+
|
|
119
|
+
const executeLogic = (e) => {
|
|
120
|
+
batch(() => {
|
|
121
|
+
if (typeof handler === 'function') {
|
|
122
|
+
handler(e)
|
|
123
|
+
} else {
|
|
124
|
+
evaluateExpression(expr, context, { $event: e }, false)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (e) => {
|
|
130
|
+
if (modifiers.includes('prevent')) e.preventDefault()
|
|
131
|
+
if (modifiers.includes('stop')) e.stopPropagation()
|
|
132
|
+
if (modifiers.includes('self') && e.target !== e.currentTarget) return
|
|
133
|
+
|
|
134
|
+
if (e instanceof KeyboardEvent) {
|
|
135
|
+
const key = e.key.toLowerCase()
|
|
136
|
+
const keyModifiers = modifiers.filter(m => ['enter', 'escape', 'tab', 'space', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(m))
|
|
137
|
+
if (keyModifiers.length > 0) {
|
|
138
|
+
const match = keyModifiers.some(m => {
|
|
139
|
+
if (m === 'escape') return key === 'escape' || key === 'esc'
|
|
140
|
+
if (m === 'space') return key === ' ' || key === 'spacebar'
|
|
141
|
+
return key === m
|
|
142
|
+
})
|
|
143
|
+
if (!match) return
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (throttleMs > 0) {
|
|
148
|
+
const now = Date.now()
|
|
149
|
+
const remaining = throttleMs - (now - lastRun)
|
|
150
|
+
|
|
151
|
+
if (remaining <= 0) {
|
|
152
|
+
clearTimeout(throttleTimeoutId)
|
|
153
|
+
lastRun = now
|
|
154
|
+
executeLogic(e)
|
|
155
|
+
} else {
|
|
156
|
+
clearTimeout(throttleTimeoutId)
|
|
157
|
+
throttleTimeoutId = setTimeout(() => {
|
|
158
|
+
lastRun = Date.now()
|
|
159
|
+
executeLogic(e)
|
|
160
|
+
}, remaining)
|
|
161
|
+
}
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (debounceMs > 0) {
|
|
166
|
+
clearTimeout(timeoutId)
|
|
167
|
+
timeoutId = setTimeout(() => executeLogic(e), debounceMs)
|
|
168
|
+
} else {
|
|
169
|
+
executeLogic(e)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
package/framework/core/signal.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
// ─── Component Types ─────────────────────────────────────────────────────────
|
|
2
|
-
|
|
3
1
|
export type ComponentProps = Record<string, any>
|
|
4
|
-
|
|
5
2
|
export type EmptyProps = Record<string, never>
|
|
6
|
-
|
|
7
3
|
export type PropSchema = string[] | Record<string, PropConfig>
|
|
8
4
|
|
|
9
5
|
export type PropConfig = {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// ─── Event Delegation Types ────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Set of standard events that support delegation.
|
|
5
|
+
* These events bubble and are commonly used in UI interactions.
|
|
6
|
+
*/
|
|
7
|
+
export const DELEGATED_EVENTS: Set<string>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if an event type is delegated.
|
|
11
|
+
* @param eventName - The event name to check (e.g., 'click', 'keydown')
|
|
12
|
+
* @returns True if the event can be delegated
|
|
13
|
+
*/
|
|
14
|
+
export function isDelegatedEvent(eventName: string): boolean
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for registering a delegated event.
|
|
18
|
+
*/
|
|
19
|
+
export interface DelegatedEventOptions {
|
|
20
|
+
/** Event modifiers (e.g., 'prevent', 'stop', 'debounce', 'throttle') */
|
|
21
|
+
modifiers?: string[]
|
|
22
|
+
/** Component context for evaluating expressions */
|
|
23
|
+
context?: Record<string, any>
|
|
24
|
+
/** Expression string to evaluate */
|
|
25
|
+
expr?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register a delegated event handler for an element.
|
|
30
|
+
* Creates a single document-level listener if not already present.
|
|
31
|
+
*
|
|
32
|
+
* @param element - The DOM element to register
|
|
33
|
+
* @param eventType - The event type (e.g., 'click', 'input')
|
|
34
|
+
* @param handler - The event handler function
|
|
35
|
+
* @param options - Optional configuration
|
|
36
|
+
* @returns Cleanup function to remove the handler
|
|
37
|
+
*/
|
|
38
|
+
export function registerDelegatedEvent(
|
|
39
|
+
element: Element,
|
|
40
|
+
eventType: string,
|
|
41
|
+
handler: (event: Event) => void,
|
|
42
|
+
options?: DelegatedEventOptions
|
|
43
|
+
): () => void
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a wrapped event handler with modifier support.
|
|
47
|
+
* Supports: prevent, stop, self, debounce, throttle, keyboard modifiers.
|
|
48
|
+
*
|
|
49
|
+
* @param handler - The original handler (function or expression)
|
|
50
|
+
* @param modifiers - Array of modifier names
|
|
51
|
+
* @param context - Component context for evaluating expressions
|
|
52
|
+
* @param expr - Expression string (when handler is a string)
|
|
53
|
+
* @returns Wrapped handler function
|
|
54
|
+
*/
|
|
55
|
+
export function createEventHandler(
|
|
56
|
+
handler: Function | string,
|
|
57
|
+
modifiers: string[],
|
|
58
|
+
context: Record<string, any>,
|
|
59
|
+
expr: string
|
|
60
|
+
): (event: Event) => void
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Event handler syntax preference.
|
|
64
|
+
* Use @ prefix for delegated events (optimized, single document listener).
|
|
65
|
+
* Use on: prefix for native events (individual element listeners, full event control).
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Delegated - efficient for many elements
|
|
69
|
+
* <button @click="handleClick">Click</button>
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* // Native - full control including stopPropagation
|
|
73
|
+
* <button on:click="handleClick">Click</button>
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // With modifiers
|
|
77
|
+
* <form @submit.prevent="handleSubmit">Submit</form>
|
|
78
|
+
* <input @keydown.enter="handleEnter" />
|
|
79
|
+
* <input @input.debounce="handleInput" />
|
|
80
|
+
*/
|
|
81
|
+
export interface EventHandlerSyntax {
|
|
82
|
+
/** Delegated event: @click, @input, @keydown, etc. */
|
|
83
|
+
delegated: `@${string}`
|
|
84
|
+
/** Native event: on:click, on:input, on:custom, etc. */
|
|
85
|
+
native: `on:${string}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
declare global {
|
|
89
|
+
const isDelegatedEvent: typeof import('./event-delegation').isDelegatedEvent
|
|
90
|
+
}
|
|
@@ -5,6 +5,7 @@ export * from './router'
|
|
|
5
5
|
export * from './compiler'
|
|
6
6
|
export * from './directives'
|
|
7
7
|
export * from './store'
|
|
8
|
+
export * from './event-delegation'
|
|
8
9
|
|
|
9
10
|
declare global {
|
|
10
11
|
type Signal<T = any> = import('./signal').Signal<T>
|
|
@@ -13,7 +14,9 @@ declare global {
|
|
|
13
14
|
type Composable<T extends (...args: any[]) => any> = import('./composable').Composable<T>
|
|
14
15
|
type DirectiveBinding<V = any> = import('./directives').DirectiveBinding<V>
|
|
15
16
|
type DirectiveHooks<V = any> = import('./directives').DirectiveHooks<V>
|
|
17
|
+
type DelegatedEventOptions = import('./event-delegation').DelegatedEventOptions
|
|
16
18
|
const defineStore: typeof import('./store').defineStore
|
|
19
|
+
const isDelegatedEvent: typeof import('./event-delegation').isDelegatedEvent
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export as namespace Fusee
|
package/index.html
CHANGED
|
@@ -546,6 +546,69 @@
|
|
|
546
546
|
margin: 0;
|
|
547
547
|
overflow-x: auto;
|
|
548
548
|
}
|
|
549
|
+
|
|
550
|
+
/* ── Event Delegation Demo Styles ───────────────────────────────── */
|
|
551
|
+
.test-events h1 {
|
|
552
|
+
color: #ff3333;
|
|
553
|
+
text-shadow: 0 0 20px rgba(255, 51, 51, 0.4);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.test-events .subtitle {
|
|
557
|
+
color: #aaa;
|
|
558
|
+
font-size: 0.9rem;
|
|
559
|
+
margin-bottom: 2rem;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.button-grid {
|
|
563
|
+
display: grid;
|
|
564
|
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
565
|
+
gap: 0.75rem;
|
|
566
|
+
margin-bottom: 1rem;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.demo-btn {
|
|
570
|
+
padding: 0.75rem 1rem;
|
|
571
|
+
border: 2px solid #3a3a5a;
|
|
572
|
+
background: #0f0f1a;
|
|
573
|
+
color: #e8e8f0;
|
|
574
|
+
border-radius: 8px;
|
|
575
|
+
cursor: pointer;
|
|
576
|
+
font-weight: 600;
|
|
577
|
+
transition: all 0.15s;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.demo-btn:hover {
|
|
581
|
+
background: #2a2a3a;
|
|
582
|
+
transform: translateY(-2px);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.demo-btn.native {
|
|
586
|
+
border-color: #34d399;
|
|
587
|
+
color: #34d399;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.demo-btn.native:hover {
|
|
591
|
+
background: rgba(52, 211, 153, 0.1);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.counter-display {
|
|
595
|
+
font-size: 1.1rem;
|
|
596
|
+
color: #aaa;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.counter-display strong {
|
|
600
|
+
color: #e8e8f0;
|
|
601
|
+
font-size: 1.3rem;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.stop-demo {
|
|
605
|
+
cursor: pointer;
|
|
606
|
+
transition: background 0.15s;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.stop-demo:hover {
|
|
610
|
+
background: #252535;
|
|
611
|
+
}
|
|
549
612
|
</style>
|
|
550
613
|
</head>
|
|
551
614
|
|