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 |
x6
x6
x6
x6
x24
x24
x18
x18
x18
x18
x42
x18
x18
x18
x6
x6
x6
x6
x6
x6
x6
x6
x6
x6
x63
x63
x63
x6
x6
x75
x76
x76
x720
x75
x106
x106
x143
x75
x84
x94
x376
x94
x557
x94
x95
x95
x94
x84
x75
x153
x183
x183
x153
x216
x216
x153
x164
x164
x220
x220
x290
x292
x292
x292
x292
x292
x292
x292
x292
x292
x292
x290
x1308
x327
x327
x324
x332
x332
x332
x2610
x220
x153
x155
x155
x155
x162
x155
x155
x153
x154
x154
x154
x205
x252
x252
x208
x208
x208
x208
x208
x208
x154
x154
x153
x154
x154
x154
x170
x170
x170
x170
x154
x154
x288
x423
x427
x427
x423
x424
x424
x423
x424
x424
x557
x288
x1017
x765
x612
x153
x75
x6
x6 |
|
import { type Cache, type callback, 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 _event>>(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 } 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)) {
continue
}
const _callback = arguments[2]._callback
let callback = function (event: Event) {
if (!element.hasAttribute(attribute.name)) {
const registered = cache.get(element)?.get(attribute)?.get(event.type)
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
}
if (_callback) {
_callback(event, { attribute, expression })
return
}
if (typeof (expression as string | callback) === "function") {
;(expression as unknown as callback)(event)
return
}
renderer.evaluate(element, `${expression || _event.default}`, { 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 = NaN
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 })
}
},
} as Directive<WeakMap<HTMLElement, WeakMap<Attr, Map<string, { target: EventTarget; listener: EventListener }>>>, typeof typings> & { typings: typeof typings; execute: NonNullable<Directive["execute"]> }
export default _event
|