@jsweb/ui 1.2.4 → 1.2.6

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/index.html ADDED
@@ -0,0 +1,162 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>JS Web UI Test</title>
7
+ <style>
8
+ .box {
9
+ padding: 20px;
10
+ border-radius: 8px;
11
+ transition: all 0.3s ease;
12
+ margin-bottom: 10px;
13
+ color: white;
14
+ }
15
+ .active {
16
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
17
+ transform: scale(1.02);
18
+ }
19
+ </style>
20
+ <script type="module">
21
+ import { createScope, reactive } from '/src/index.ts'
22
+
23
+ const scope = reactive({
24
+ count: 0,
25
+ inc: 'Incremento',
26
+ dec: 'Decremento',
27
+ model: 'Exemplo',
28
+ items: ['A', 'B', 'C'],
29
+ active: false,
30
+ color: '#42b883',
31
+
32
+ get computedItems() {
33
+ return this.items.map((value, index) => {
34
+ return { value, index }
35
+ })
36
+ },
37
+
38
+ increment() {
39
+ this.count++
40
+ },
41
+ decrement() {
42
+ this.count--
43
+ },
44
+ zero() {
45
+ this.count = 0
46
+ },
47
+ addItem() {
48
+ const value = Date.now()
49
+ this.items.push(value)
50
+ },
51
+ removeItem() {
52
+ this.items.pop()
53
+ },
54
+ logEvent(e, ...args) {
55
+ console.log('Evento recebido:', e, args)
56
+ this.items.push(`Evento ${e.type}`)
57
+ },
58
+ toggleActive() {
59
+ this.active = !this.active
60
+ },
61
+ handleCustomEvent(e) {
62
+ alert(`Custom event received: ${e.detail.message}`)
63
+ this.items.push(`Custom Event: ${e.detail.message}`)
64
+ },
65
+ })
66
+
67
+ window.scope = scope
68
+
69
+ createScope('body', { scope })
70
+ </script>
71
+ </head>
72
+ <body>
73
+ <div :scope="scope">
74
+ <h1>JS Web UI</h1>
75
+ <p>Contador: <span :text="count"></span></p>
76
+ <button :text="inc" @click="increment">+</button>
77
+ <button :text="dec" @click="decrement">-</button>
78
+
79
+ <div style="margin-top: 20px">
80
+ <button @click="zero" :disabled="!count">Zerar</button>
81
+ </div>
82
+
83
+ <div
84
+ :if="count > 0"
85
+ style="margin-top: 20px; padding: 10px; border: 1px solid green"
86
+ >
87
+ O contador é maior que zero!
88
+ </div>
89
+
90
+ <div style="margin-top: 20px">
91
+ <h3>Lista:</h3>
92
+ <div style="margin-bottom: 10px">
93
+ <input type="text" :bind="model" placeholder="Digite algo..." />
94
+ <p>Você vai adicionar: <strong :text="model"></strong></p>
95
+ <button @click="addItem">Adicionar Item</button>
96
+ <button @click="removeItem">Remover Item</button>
97
+ <button @click="logEvent">Testar Evento (Sem Parênteses)</button>
98
+ <button @click="logEvent($event, 'A', 'B', 'C')">
99
+ Testar Evento (Com Parênteses)
100
+ </button>
101
+ </div>
102
+ <ul>
103
+ <li :for="item of computedItems">
104
+ <span :text="item.index"></span>
105
+ <input type="text" :value="item.value" />
106
+ </li>
107
+ </ul>
108
+ </div>
109
+
110
+ <hr />
111
+
112
+ <div style="margin-top: 20px">
113
+ <h3>Testes de :class e :style</h3>
114
+ <div
115
+ class="box"
116
+ :class="{ active }"
117
+ :style="{ backgroundColor: color, opacity: count ? 1 : 0.5 }"
118
+ >
119
+ Caixa de teste! Ativa: <strong :text="active"></strong>
120
+ </div>
121
+
122
+ <div style="margin-top: 10px">
123
+ <button @click="toggleActive">Alternar Classe 'active'</button>
124
+
125
+ <label style="margin-left: 10px">
126
+ Cor de Fundo:
127
+ <input type="color" :bind="color" />
128
+ </label>
129
+ </div>
130
+ </div>
131
+
132
+ <hr />
133
+
134
+ <div style="margin-top: 20px" @custom-event="handleCustomEvent">
135
+ <h3>Teste de $emit (Comunicação de Eventos)</h3>
136
+ <p>A div pai está escutando <code>@custom-event</code>.</p>
137
+
138
+ <!-- Escopo Filho Simulado -->
139
+ <div
140
+ :scope="{ component: 'Componente Interno' }"
141
+ style="
142
+ padding: 0 15px 15px 15px;
143
+ border: 2px dashed silver;
144
+ margin-top: 10px;
145
+ "
146
+ >
147
+ <p>Nome interno: <strong :text="component"></strong></p>
148
+ <button
149
+ @click="
150
+ $emit(
151
+ 'custom-event',
152
+ { message: `Mensagem enviada do ${component}` },
153
+ )
154
+ "
155
+ >
156
+ Disparar evento para o pai
157
+ </button>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </body>
162
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsweb/ui",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "JS Web Microframework",
5
5
  "keywords": [
6
6
  "js",
@@ -21,14 +21,30 @@
21
21
  "license": "MIT",
22
22
  "author": "Alex Bruno Cáceres <email@alexbruno.dev>",
23
23
  "type": "module",
24
- "main": "index.umd.js",
25
- "module": "index.es.js",
26
- "types": "index.d.ts",
24
+ "main": "dist/ui.umd.js",
25
+ "module": "dist/ui.es.js",
26
+ "types": "dist/index.d.ts",
27
27
  "exports": {
28
28
  ".": {
29
- "import": "./index.es.js",
30
- "require": "./index.umd.js",
31
- "types": "./index.d.ts"
29
+ "import": "./dist/ui.es.js",
30
+ "require": "./dist/ui.umd.js",
31
+ "types": "./dist/index.d.ts"
32
32
  }
33
+ },
34
+ "scripts": {
35
+ "dev": "vite",
36
+ "build": "tsc && vite build && node publish.js",
37
+ "preview": "vite preview",
38
+ "format": "prettier --write .",
39
+ "test": "echo 'test'",
40
+ "preversion": "npm run build",
41
+ "postversion": "git push && git push --tags"
42
+ },
43
+ "devDependencies": {
44
+ "prettier": "^3.8.3",
45
+ "terser": "^5.46.2",
46
+ "typescript": "^6.0.3",
47
+ "vite": "^8.0.10",
48
+ "vite-plugin-dts": "^5.0.0"
33
49
  }
34
- }
50
+ }
package/publish.js ADDED
@@ -0,0 +1,34 @@
1
+ import { resolve } from 'node:path'
2
+ import { copyFileSync, readFileSync, writeFileSync } from 'node:fs'
3
+
4
+ const root = process.cwd()
5
+ const source = resolve(root, 'package.json')
6
+ const target = resolve(root, 'dist/package.json')
7
+ const pkgInfo = JSON.parse(readFileSync(source, 'utf8'))
8
+
9
+ // 1. Remove campos que não são necessários no pacote publicado
10
+ delete pkgInfo.scripts
11
+ delete pkgInfo.devDependencies
12
+
13
+ // 2. Ajusta os caminhos dos arquivos, pois o root do pacote agora será a pasta dist/
14
+ pkgInfo.main = 'index.umd.js'
15
+ pkgInfo.module = 'index.es.js'
16
+ pkgInfo.types = 'index.d.ts'
17
+ pkgInfo.exports = {
18
+ '.': {
19
+ import: './index.es.js',
20
+ require: './index.umd.js',
21
+ types: './index.d.ts',
22
+ },
23
+ }
24
+
25
+ // 3. Salva o package.json modificado dentro da pasta dist/
26
+ writeFileSync(target, JSON.stringify(pkgInfo, null, 2))
27
+
28
+ // 4. Copia arquivos de metadados importantes para o NPM
29
+ copyFileSync(resolve(root, 'README.md'), resolve(root, 'dist/README.md'))
30
+ copyFileSync(resolve(root, 'LICENSE'), resolve(root, 'dist/LICENSE'))
31
+
32
+ console.log(
33
+ '✅ Arquivo package.json mínimo e metadados preparados na pasta dist/',
34
+ )
@@ -0,0 +1,29 @@
1
+ export function evaluate(
2
+ expression: string,
3
+ context: Record<string, any> = {},
4
+ ) {
5
+ try {
6
+ const fn = new Function(`with(this) { return ${expression} }`)
7
+ return fn.call(context)
8
+ } catch {
9
+ return undefined
10
+ }
11
+ }
12
+
13
+ export function evaluateEvent(
14
+ $event: Event,
15
+ expression: string,
16
+ context: Record<string, any> = {},
17
+ ) {
18
+ try {
19
+ const exp = expression.trim()
20
+ const isIdentifier = /^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(exp)
21
+ const code = `${exp} instanceof Function ? ${exp}.call(this, $event) : ${exp}`
22
+ const result = isIdentifier ? code : exp
23
+ const fn = new Function('$event', `with(this) { ${result} }`)
24
+
25
+ fn.call(context, $event)
26
+ } catch {
27
+ console.warn(`[jsweb/ui] Error evaluating event: ${expression}`)
28
+ }
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { reactive, watch } from './reactivity'
2
+ import { createScope } from './parser'
3
+
4
+ export { reactive, watch, createScope }
5
+
6
+ if (typeof window !== 'undefined') {
7
+ const w = window as any
8
+ w.jsweb = w.jsweb || {}
9
+ w.jsweb.ui = { createScope, reactive, watch }
10
+ }
package/src/parser.ts ADDED
@@ -0,0 +1,426 @@
1
+ import { effect, reactive } from './reactivity'
2
+ import { evaluate, evaluateEvent } from './evaluator'
3
+
4
+ export type Context = Record<string, any>
5
+
6
+ interface BoundNode extends Node {
7
+ _effects?: Array<() => void>
8
+ }
9
+
10
+ export function cleanupTree(node: Node) {
11
+ const bNode = node as BoundNode
12
+ if (bNode._effects) {
13
+ bNode._effects.forEach((stop) => stop())
14
+ bNode._effects = []
15
+ }
16
+ const children = Array.from(node.childNodes)
17
+ for (const child of children) cleanupTree(child)
18
+ }
19
+
20
+ export function createContext(scope: any, context: Context = {}): Context {
21
+ const reactiveScope = scope._isReactive ? scope : reactive(scope)
22
+
23
+ return new Proxy(reactiveScope, {
24
+ get(target, prop) {
25
+ if (prop === '_isContext') return true
26
+ if (prop in target) return Reflect.get(target, prop, target)
27
+ if (prop in context) {
28
+ return Reflect.get(context, prop, context)
29
+ }
30
+ return Reflect.get(target, prop, target)
31
+ },
32
+ set(target, prop, value) {
33
+ if (prop in target) return Reflect.set(target, prop, value, target)
34
+ if (prop in context) {
35
+ return Reflect.set(context, prop, value, context)
36
+ }
37
+ return Reflect.set(target, prop, value, target)
38
+ },
39
+ has(target, prop) {
40
+ if (prop in target) return true
41
+ if (prop in context) return true
42
+ return false
43
+ },
44
+ })
45
+ }
46
+
47
+ export function parseNode(node: Node, context: Context) {
48
+ if (node.nodeType !== Node.ELEMENT_NODE) return
49
+
50
+ const el = node as HTMLElement
51
+ const scope = processScope(el, context)
52
+ if (!scope) return
53
+
54
+ const forAttrs = ['ui:for', ':for']
55
+ const forDirective = getDirectiveValue(el, forAttrs)
56
+ if (forDirective) {
57
+ removeDirectiveAttributes(el, forAttrs)
58
+ processFor(el, forDirective, scope)
59
+ return
60
+ }
61
+
62
+ const ifAttrs = ['ui:if', ':if']
63
+ const ifDirective = getDirectiveValue(el, ifAttrs)
64
+ if (ifDirective) {
65
+ removeDirectiveAttributes(el, ifAttrs)
66
+ processIf(el, ifDirective, scope)
67
+ }
68
+
69
+ processAttributes(el, scope)
70
+
71
+ const children = Array.from(el.childNodes)
72
+ for (const child of children) parseNode(child, scope)
73
+ }
74
+
75
+ export function createScope(
76
+ selectorOrElement: string | HTMLElement,
77
+ context: Context = {},
78
+ ) {
79
+ const el =
80
+ typeof selectorOrElement === 'string'
81
+ ? document.querySelector(selectorOrElement)
82
+ : selectorOrElement
83
+
84
+ if (el) {
85
+ if (!context.$emit) {
86
+ context.$emit = (eventName: string, detail?: any) => {
87
+ el.dispatchEvent(
88
+ new CustomEvent(eventName, { detail, bubbles: true, composed: true }),
89
+ )
90
+ }
91
+ }
92
+
93
+ parseNode(el, context)
94
+ } else {
95
+ console.warn('[jsweb/ui] Element not found:', selectorOrElement)
96
+ }
97
+ }
98
+
99
+ function bindEffect(node: Node, fn: () => void) {
100
+ const e = effect(fn)
101
+ const bNode = node as BoundNode
102
+ bNode._effects ??= []
103
+ bNode._effects.push(e.stop)
104
+ }
105
+
106
+ function getDirectiveValue(el: HTMLElement, names: string[]) {
107
+ for (const name of names) {
108
+ const value = el.getAttribute(name)
109
+ if (value !== null) return value
110
+ }
111
+ return null
112
+ }
113
+
114
+ function removeDirectiveAttributes(el: HTMLElement, names: string[]) {
115
+ for (const name of names) {
116
+ el.removeAttribute(name)
117
+ }
118
+ }
119
+
120
+ function processScope(el: HTMLElement, context: Context) {
121
+ const attrs = ['ui:scope', ':scope']
122
+ const directive = getDirectiveValue(el, attrs)
123
+ if (!directive) return context
124
+
125
+ const scope = evaluate(directive, context)
126
+ if (!scope) return undefined
127
+
128
+ removeDirectiveAttributes(el, attrs)
129
+
130
+ if (!scope.$emit) {
131
+ scope.$emit = (event: string, detail?: any) => {
132
+ el.dispatchEvent(
133
+ new CustomEvent(event, { detail, bubbles: true, composed: true }),
134
+ )
135
+ }
136
+ }
137
+
138
+ return createContext(scope, context)
139
+ }
140
+
141
+ function processFor(el: HTMLElement, expr: string, context: Context) {
142
+ const parent = el.parentNode
143
+ if (!parent) return
144
+
145
+ const match = /^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(expr)
146
+ if (!match) {
147
+ return console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)
148
+ }
149
+ const [, itemName, listName] = match
150
+
151
+ const keyAttr = ['ui:key', ':key']
152
+ const keyDirective = getDirectiveValue(el, keyAttr)
153
+ removeDirectiveAttributes(el, keyAttr)
154
+
155
+ const uuid = crypto.randomUUID()
156
+ const comment = document.createComment(` ui:for ${uuid} `)
157
+ el.replaceWith(comment)
158
+
159
+ interface RenderedNode {
160
+ key: any
161
+ el: HTMLElement
162
+ scope: any
163
+ }
164
+ let renderedNodes: RenderedNode[] = []
165
+
166
+ bindEffect(comment, () => {
167
+ const list = evaluate(listName, context)
168
+
169
+ if (!Array.isArray(list)) {
170
+ renderedNodes.forEach((node) => {
171
+ node.el.remove()
172
+ cleanupTree(node.el)
173
+ })
174
+ renderedNodes = []
175
+ return
176
+ }
177
+
178
+ const newNodes: RenderedNode[] = []
179
+ const oldNodesByKey = new Map<any, RenderedNode>()
180
+ renderedNodes.forEach((node) => oldNodesByKey.set(node.key, node))
181
+
182
+ list.forEach((item, index) => {
183
+ const scope = { [itemName]: item, $index: index }
184
+ let key: any = index
185
+
186
+ if (keyDirective) {
187
+ const tempContext = createContext(scope, context)
188
+ key = evaluate(keyDirective, tempContext)
189
+ }
190
+
191
+ let node = oldNodesByKey.get(key)
192
+ if (node) {
193
+ // Reuse node
194
+ node.scope[itemName] = item
195
+ node.scope.$index = index
196
+ oldNodesByKey.delete(key)
197
+ } else {
198
+ // Create new node
199
+ const clone = el.cloneNode(true) as HTMLElement
200
+ const reactiveScope = reactive(scope)
201
+ const localContext = createContext(reactiveScope, context)
202
+ parseNode(clone, localContext)
203
+ node = { key, el: clone, scope: reactiveScope }
204
+ }
205
+
206
+ newNodes.push(node)
207
+ })
208
+
209
+ // Remove un-reused nodes
210
+ oldNodesByKey.forEach((node) => {
211
+ node.el.remove()
212
+ cleanupTree(node.el)
213
+ })
214
+
215
+ // Reorder and insert new DOM nodes
216
+ let currentAnchor = comment.nextSibling
217
+ newNodes.forEach((node) => {
218
+ if (currentAnchor === node.el) {
219
+ currentAnchor = currentAnchor.nextSibling
220
+ } else {
221
+ comment.parentNode?.insertBefore(node.el, currentAnchor)
222
+ }
223
+ })
224
+
225
+ renderedNodes = newNodes
226
+ })
227
+ }
228
+
229
+ function processIf(el: HTMLElement, expr: string, context: Context) {
230
+ const parent = el.parentNode
231
+ if (!parent) return
232
+
233
+ const uuid = crypto.randomUUID()
234
+ const comment = document.createComment(` ui:if ${uuid} `)
235
+ el.before(comment)
236
+
237
+ bindEffect(comment, () => {
238
+ const val = evaluate(expr, context)
239
+ if (val) {
240
+ if (!el.parentNode) {
241
+ comment.parentNode?.insertBefore(el, comment.nextSibling)
242
+ }
243
+ } else if (el.parentNode) {
244
+ el.remove()
245
+ }
246
+ })
247
+ }
248
+
249
+ function processAttributes(el: HTMLElement, context: Context) {
250
+ const attrs = Array.from(el.attributes)
251
+
252
+ for (const attr of attrs) {
253
+ const { name, value } = attr
254
+ const isText = ['ui:text', ':text'].includes(name)
255
+ const isTwoWayBind = ['ui:bind', ':bind'].includes(name)
256
+ const isClassBind = ['ui:class', ':class'].includes(name)
257
+ const isStyleBind = ['ui:style', ':style'].includes(name)
258
+ const isAttrBind = name.startsWith('ui:') || name.startsWith(':')
259
+ const isEvent = name.startsWith('ui@') || name.startsWith('@')
260
+
261
+ if (isText) {
262
+ processTextBinding(el, value, context)
263
+ el.removeAttribute(name)
264
+ } else if (isTwoWayBind) {
265
+ processTwoWayBinding(el, value, context)
266
+ el.removeAttribute(name)
267
+ } else if (isClassBind) {
268
+ processClassBinding(el, value, context)
269
+ el.removeAttribute(name)
270
+ } else if (isStyleBind) {
271
+ processStyleBinding(el, value, context)
272
+ el.removeAttribute(name)
273
+ } else if (isAttrBind) {
274
+ const bound = name.split(':').pop()!
275
+ processAttrBinding(el, bound, value, context)
276
+ el.removeAttribute(name)
277
+ } else if (isEvent) {
278
+ processEventBinding(el, name, value, context)
279
+ el.removeAttribute(name)
280
+ }
281
+ }
282
+ }
283
+
284
+ function processTextBinding(el: HTMLElement, expr: string, context: Context) {
285
+ bindEffect(el, () => {
286
+ const val = evaluate(expr, context)
287
+ el.textContent = val !== undefined && val !== null ? String(val) : ''
288
+ })
289
+ }
290
+
291
+ function processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {
292
+ const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'
293
+ const isRadio = el instanceof HTMLInputElement && el.type === 'radio'
294
+
295
+ // 1. Reactive state to DOM
296
+ bindEffect(el, () => {
297
+ const val = evaluate(expr, context)
298
+ if (isCheckbox) {
299
+ el.checked = !!val
300
+ } else if (isRadio) {
301
+ el.checked = el.value === String(val)
302
+ } else {
303
+ const target = el as
304
+ | HTMLInputElement
305
+ | HTMLSelectElement
306
+ | HTMLTextAreaElement
307
+ target.value = val == null ? '' : String(val)
308
+ }
309
+ })
310
+
311
+ // 2. DOM to Reactive state
312
+ const isChange = isCheckbox || isRadio || el instanceof HTMLSelectElement
313
+ const eventName = isChange ? 'change' : 'input'
314
+ el.addEventListener(eventName, ($event) => {
315
+ const target = isCheckbox ? 'checked' : 'value'
316
+ const value = `$event.target.${target}`
317
+ evaluateEvent($event, `${expr} = ${value}`, context)
318
+ })
319
+ }
320
+
321
+ function processAttrBinding(
322
+ el: HTMLElement,
323
+ attr: string,
324
+ expr: string,
325
+ context: Context,
326
+ ) {
327
+ bindEffect(el, () => {
328
+ const val = evaluate(expr, context)
329
+ if (val === null || val === undefined || val === false) {
330
+ el.removeAttribute(attr)
331
+ } else if (val === true) {
332
+ el.setAttribute(attr, '')
333
+ } else {
334
+ el.setAttribute(attr, String(val))
335
+ }
336
+ })
337
+ }
338
+
339
+ function processClassBinding(el: HTMLElement, expr: string, context: Context) {
340
+ let oldClasses = new Set<string>()
341
+
342
+ bindEffect(el, () => {
343
+ const val = evaluate(expr, context)
344
+ const newClasses = new Set<string>()
345
+ const addClass = (c: string) => c && newClasses.add(c)
346
+ const addClasses = (c: string) => c.split(/\s+/).forEach(addClass)
347
+
348
+ if (typeof val === 'string') addClasses(val)
349
+ else if (Array.isArray(val)) {
350
+ val.flat().forEach((c: any) => {
351
+ if (typeof c === 'string') addClasses(c)
352
+ })
353
+ } else if (typeof val === 'object' && val !== null) {
354
+ Object.entries(val).forEach(([c, condition]: [string, any]) => {
355
+ if (condition) addClasses(c)
356
+ })
357
+ }
358
+
359
+ oldClasses.forEach((c) => {
360
+ if (!newClasses.has(c)) el.classList.remove(c)
361
+ })
362
+ newClasses.forEach((c) => {
363
+ if (!oldClasses.has(c)) el.classList.add(c)
364
+ })
365
+
366
+ oldClasses = newClasses
367
+ })
368
+ }
369
+
370
+ function processStyleBinding(el: HTMLElement, expr: string, context: Context) {
371
+ let oldStyles: Record<string, any> = {}
372
+
373
+ bindEffect(el, () => {
374
+ const val = evaluate(expr, context)
375
+ const newStyles = typeof val === 'object' && val !== null ? val : {}
376
+
377
+ for (const key in oldStyles) {
378
+ if (!(key in newStyles)) {
379
+ ;(el.style as any)[key] = ''
380
+ }
381
+ }
382
+
383
+ for (const key in newStyles) {
384
+ if (oldStyles[key] !== newStyles[key]) {
385
+ ;(el.style as any)[key] = newStyles[key]
386
+ }
387
+ }
388
+
389
+ oldStyles = { ...newStyles }
390
+ })
391
+ }
392
+
393
+ function processEventBinding(
394
+ el: HTMLElement,
395
+ evt: string,
396
+ expr: string,
397
+ context: Context,
398
+ ) {
399
+ const refs = evt.split('@').pop()!
400
+ const [name, ...modifiers] = refs.split('.')
401
+
402
+ const isOutside = modifiers.includes('outside')
403
+ const target = isOutside ? document : el
404
+
405
+ const handler: EventListener = ($event: Event) => {
406
+ if (!el.isConnected) return
407
+
408
+ const isTargetNode = $event.target instanceof Node
409
+
410
+ if (isOutside && isTargetNode && el.contains($event.target)) return
411
+ if (modifiers.includes('self') && $event.target !== el) return
412
+
413
+ if (modifiers.includes('prevent')) $event.preventDefault()
414
+ if (modifiers.includes('stop')) $event.stopPropagation()
415
+
416
+ evaluateEvent($event, expr, context)
417
+ }
418
+
419
+ target.addEventListener(name, handler)
420
+
421
+ if (isOutside) {
422
+ const bNode = el as BoundNode
423
+ bNode._effects ??= []
424
+ bNode._effects.push(() => target.removeEventListener(name, handler))
425
+ }
426
+ }