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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183 |
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x4
x38
x38
x38
x4
x4
x60
x1
x1
x59
x60
x60
x60
x60
x60
x29
x59
x59
x59
x59
x6
x6
x59
x33
x33
x59
x5
x5
x3
x8
x3
x3
x3
x5
x59
x17
x16
x16
x17
x59
x29
x29
x60
x29
x30
x30
x30
x30
x12
x12
x2
x3
x2
x2
x3
x12
x10
x10
x12
x11
x11
x1
x1
x1
x30
x2
x1
x1
x2
x30
x4
x30
x30
x19
x29
x29
x60
x29
x29
x2
x2
x29
x1
x1
x29
x29
x59
x60
x4
x4
x4
x90
x90
x90
x11
x11
x11
x4
x4
x11
x11
x90
x79
x79
x90
x90
x90
x4
x99
x100
x4
x4
x100
x10
x10
x100
x8
x8
x100
x3
x3
x75
x99
x99
x99 |
I
|
import { type Arg, type Arrayable, type Cache, type Callback, type Directive, type InferAttrTypings, type Nullable, Phase } from "@mizu/internal/engine"
import { equal } from "@std/assert"
import { _event } from "@mizu/event"
export type * from "@mizu/internal/engine"
export const typings = {
modifiers: {
event: { type: String, default: "input", enforce: true },
name: { type: Boolean },
value: { type: Boolean },
throttle: { type: Date, default: 250 },
debounce: { type: Date, default: 250 },
keys: { type: String },
nullish: { type: Boolean },
boolean: { type: Boolean },
number: { type: Boolean },
string: { type: Boolean },
},
} as const
export const _model = {
name: /^::(?<value>)$/,
prefix: "::",
phase: Phase.ATTRIBUTE_MODEL_VALUE,
typings,
default: "value",
init(renderer) {
if (!renderer.cache(this.name)) {
renderer.cache<Cache<typeof _model>>(this.name, new WeakMap())
}
},
async execute(renderer, element, { attributes: [attribute], cache, ...options }) {
if (!renderer.isHtmlElement(element)) {
return
}
const parsed = renderer.parseAttribute(attribute, this.typings, { prefix: this.prefix, modifiers: true })
const cached = cache.get(element)?.get(element) || cache.set(element, new WeakMap([[element, { model: { read: null, sync: null }, event: null, init: false }]])).get(element)!.get(element)!
const input = element as HTMLInputElement
const type = input.tagName === "INPUT" ? input.getAttribute("type") : input.tagName === "SELECT" ? input.tagName : null
const defaults = this.default
if (!cached.model.sync) {
cached.model.sync = async function () {
const model = await renderer.evaluate(input, attribute.value || defaults, options)
const value = parse(read(input), parsed.modifiers)
switch (type) {
case "radio":
input.checked = equal(model, value)
break
case "checkbox":
input.checked = Array.isArray(model) ? model.includes(value) : false
break
case "SELECT": {
const select = element as HTMLSelectElement
if (select.multiple) {
Array.from(select.options).forEach((option) => {
option.selected = Array.isArray(model) ? model.includes(parse(option.value, parsed.modifiers)) : false
})
break
}
}
default:
if (!equal(model, value)) {
input.value = `${model ?? ""}`
}
break
}
}
}
if (!cached.model.read) {
cached.model.read = async function () {
const model = await renderer.evaluate(input, attribute.value || defaults, options)
let value = parse(read(input), parsed.modifiers)
switch (type) {
case "checkbox": {
const array = Array.isArray(model) ? model : []
if (!input.checked) {
for (let i = array.length - 1; i >= 0; i--) {
if (array[i] === value) {
array.splice(i, 1)
}
}
} else if (!array.includes(value)) {
array.push(value)
}
if (array === model) {
return
}
value = array
break
}
case "radio":
if ((equal(model, value)) && (!input.checked)) {
value = undefined as unknown as typeof value
}
break
case "number":
value = parse(Number(value) as unknown as Arg<typeof parse>, parsed.modifiers)
}
await renderer.evaluate(input, `${attribute.value || defaults}=${renderer.internal("value")}`, { ...options, state: { ...options.state, [renderer.internal("value")]: value } })
input.dispatchEvent(new renderer.window.Event("::", { bubbles: true }))
}
}
if (!cached.init) {
cached.init = true
if (parsed.modifiers.name && (!input.hasAttribute("name"))) {
input.setAttribute("name", attribute.value || defaults)
}
if (parsed.modifiers.value && (input.getAttribute("value"))) {
await renderer.evaluate(input, `${attribute.value || defaults}??=${renderer.internal("value")}`, { ...options, state: { ...options.state, [renderer.internal("value")]: parse(read(input), parsed.modifiers) } })
}
await _event.execute.call(this, renderer, element, { ...arguments[2], attributes: [attribute], _event: parsed.modifiers.event, _callback: cached.model.read })
}
await cached.model.sync()
},
} as const satisfies Directive<{
Name: RegExp
Cache: WeakMap<HTMLElement, WeakMap<HTMLElement, { model: Record<"read" | "sync", Nullable<Callback>>; event: Nullable<string>; init: boolean }>>
Typings: typeof typings
Default: true
}>
export default [_model]
function read(input: HTMLElement) {
let value = null as Nullable<Arrayable<string>>
switch (input.tagName) {
case "SELECT": {
const select = input as HTMLSelectElement
value = Array.from(select.selectedOptions).map((option) => option.value)
if (!select.multiple) {
value = (value as string[])[0]
}
break
}
default:
value = (input as HTMLInputElement).value
break
}
return value
}
function parse(value: ReturnType<typeof read>, modifiers: InferAttrTypings<typeof typings>["modifiers"]) {
const parsed = [value].flat().map((value) => {
if ((modifiers.nullish) && (!value)) {
return null
}
if (modifiers.number) {
return Number(value)
}
if (modifiers.boolean) {
return Boolean(value) && !(value.length && /^(?:[Ff]alse|FALSE|[Nn]|[Nn]o|NO|[Oo]ff|OFF)$/.test(value))
}
if (modifiers.string) {
return String(value)
}
return value
})
return Array.isArray(value) ? parsed : parsed[0]
}
|