1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168 |
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x7
x65
x65
x65
x7
x7
x79
x1
x1
x84
x79
x31
x31
x78
x79
x9
x10
x10
x10
x14
x10
x1
x1
x10
x9
x79
x88
x37
x37
x88
x70
x70
x88
x14
x14
x14
x74
x74
x78
x78
x4
x4
x4
x4
x4
x4
x4
x4
x4
x78
x78
x37
x37
x37
x42
x8
x8
x8
x78
x74
x88
x2
x2
x2
x5
x2
x2
x88
x1
x1
x1
x50
x47
x47
x3
x3
x3
x3
x3
x3
x1
x1
x88
x1
x1
x1
x15
x15
x15
x15
x1
x1
x74
x143
x4
x4
x143
x1
x1
x143
x1
x1
x142
x74
x88
x88
x88
x88
x79
x7
x7 |
I
|
import { type Cache, type Callback, type Context, type Directive, Phase } from "@mizu/internal/engine"
import { keyboard } from "./keyboard.ts"
export type * from "@mizu/internal/engine"
export const typings = {
modifiers: {
throttle: { type: Date, default: 250 },
debounce: { type: Date, default: 250 },
keys: { type: String },
prevent: { type: Boolean },
stop: { type: Boolean },
self: { type: Boolean },
attach: { type: String, allowed: ["element", "window", "document"] },
passive: { type: Boolean },
once: { type: Boolean },
capture: { type: Boolean },
},
} as const
export const _event = {
name: /^@(?<event>)/,
prefix: "@",
phase: Phase.INTERACTIVITY,
default: "null",
typings,
multiple: true,
init(renderer) {
if (!renderer.cache(this.name)) {
renderer.cache<Cache<typeof this>>(this.name, new WeakMap())
}
},
async execute(renderer, element, { cache, attributes, context, state }) {
if (!renderer.isHtmlElement(element)) {
return
}
const parsed = attributes.map((attribute) => renderer.parseAttribute(attribute, this.typings, { prefix: this.prefix, modifiers: true }))
if ((arguments[2]._event) && (arguments[2].attributes.length === 1)) {
parsed[0].name = arguments[2]._event
}
const shorthands = parsed.filter(({ name }) => !name.length)
if (shorthands.length) {
for (const shorthand of shorthands) {
const [attribute] = parsed.splice(parsed.indexOf(shorthand), 1)
const value = await renderer.evaluate(element, attribute.value, { context, state })
if (typeof value === "object") {
parsed.unshift(...Object.entries(value ?? {}).map(([name, value]) => ({ ...attribute, name, value })))
} else {
renderer.warn(`[${this.name}] empty shorthand expects an object but got ${typeof value}, ignoring`, element)
}
}
}
for (const { name: event, value: expression, modifiers, attribute, ephemeral } of parsed) {
if (!cache.has(element)) {
cache.set(element, new WeakMap())
}
if (!cache.get(element)!.has(attribute)) {
cache.get(element)!.set(attribute, new Map())
}
if (cache.get(element)!.get(attribute)!.has(event)) {
Object.assign(cache.get(element)!.get(attribute)!.get(event)!, { context, state, expression })
continue
}
const _callback = arguments[2]._callback
let callback = function (event: Event) {
const registered = cache.get(element)?.get(attribute)?.get(event.type)
if (((!ephemeral) && (!element.hasAttribute(attribute.name))) || (registered && (registered.target !== element) && (!element.isConnected))) {
if (registered) {
registered.target.removeEventListener(event.type, registered.listener)
}
cache.get(element)?.get(attribute)?.delete(event.type)
if (!cache.get(element)?.get(attribute)?.size) {
cache.get(element)?.delete(attribute)
}
return
}
const { context: _context, state: _state, expression: _expression } = registered ?? { context, state, expression }
if (_callback) {
_callback(event, { attribute, expression: _expression })
return
}
if (typeof (_expression as string | Callback) === "function") {
;(_expression as unknown as Callback)(event)
return
}
renderer.evaluate(element, `${_expression || _event.default}`, { context: _context, state: { ..._state, $event: event }, args: [event] })
} as Callback
if (modifiers.keys) {
const check = keyboard(modifiers.keys)
callback = ((callback) =>
function (event: KeyboardEvent) {
return check(event) ? callback(...arguments) : false
})(callback)
}
if (modifiers.throttle) {
let throttled = false
callback = ((callback) =>
function () {
if (throttled) {
return false
}
throttled = true
try {
return callback(...arguments)
} finally {
setTimeout(() => throttled = false, modifiers.throttle)
}
})(callback)
}
if (modifiers.debounce) {
let timeout = undefined as ReturnType<typeof setTimeout> | undefined
callback = ((callback) =>
function () {
const args = arguments
clearTimeout(timeout)
timeout = setTimeout(() => callback(...args), modifiers.debounce)
return false
})(callback)
}
const listener = function (event: Event) {
if (modifiers.prevent) {
event.preventDefault()
}
if (modifiers.stop) {
event.stopPropagation()
}
if (modifiers.self && event.target !== element) {
return
}
return callback(event)
}
const target = { window: renderer.window, document: renderer.document }[modifiers.attach as string] ?? element
target.addEventListener(event, listener, { passive: modifiers.passive, once: modifiers.once, capture: modifiers.capture })
cache.get(element)?.get(attribute)?.set(event, { target, listener, context, state, expression })
}
},
} as const satisfies Directive<{
Name: RegExp
Cache: WeakMap<HTMLElement, WeakMap<Attr, Map<string, { target: EventTarget; listener: EventListener; context: Context; state: Record<PropertyKey, unknown>; expression: string }>>>
Typings: typeof typings
Default: true
}>
export default _event
|