@jsweb/ui 1.2.0 → 1.2.3
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.es.js +2 -0
- package/index.es.js.map +1 -0
- package/index.umd.js +2 -0
- package/index.umd.js.map +1 -0
- package/package.json +8 -23
- package/src/evaluator.d.ts +2 -0
- package/{dist/src → src}/parser.d.ts +1 -1
- package/.github/workflows/npm-publish.yml +0 -21
- package/.prettierignore +0 -3
- package/.prettierrc +0 -7
- package/HISTORY.md +0 -36
- package/PROJECT.md +0 -90
- package/dist/src/evaluator.d.ts +0 -2
- package/dist/ui.es.js +0 -2
- package/dist/ui.es.js.map +0 -1
- package/dist/ui.umd.js +0 -2
- package/dist/ui.umd.js.map +0 -1
- package/index.html +0 -162
- package/src/evaluator.ts +0 -33
- package/src/index.ts +0 -10
- package/src/parser.ts +0 -429
- package/src/reactivity.ts +0 -175
- package/tsconfig.json +0 -23
- package/vite.config.ts +0 -16
- /package/{dist/index.d.ts → index.d.ts} +0 -0
- /package/{dist/src → src}/index.d.ts +0 -0
- /package/{dist/src → src}/reactivity.d.ts +0 -0
- /package/{dist/vite.config.d.ts → vite.config.d.ts} +0 -0
package/index.html
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
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/src/evaluator.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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 (error) {
|
|
9
|
-
console.error(`[jsweb/ui] Error evaluating expression: ${expression}`, error)
|
|
10
|
-
return undefined
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function evaluateEvent(
|
|
15
|
-
expression: string,
|
|
16
|
-
context: Record<string, any> = {},
|
|
17
|
-
$event: Event,
|
|
18
|
-
) {
|
|
19
|
-
try {
|
|
20
|
-
const exp = expression.trim()
|
|
21
|
-
const isIdentifier = /^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(exp)
|
|
22
|
-
const code = `${exp} instanceof Function ? ${exp}.call(this, $event) : ${exp}`
|
|
23
|
-
const result = isIdentifier ? code : exp
|
|
24
|
-
|
|
25
|
-
const fn = new Function('$event', `with(this) { ${result} }`)
|
|
26
|
-
fn.call(context, $event)
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error(
|
|
29
|
-
`[jsweb/ui] Error evaluating event expression: ${expression}`,
|
|
30
|
-
error,
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,429 +0,0 @@
|
|
|
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) {
|
|
18
|
-
cleanupTree(child)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function createContext(
|
|
23
|
-
scope: any,
|
|
24
|
-
context: Context | null = null,
|
|
25
|
-
): Context {
|
|
26
|
-
const reactiveScope = scope._isReactive ? scope : reactive(scope)
|
|
27
|
-
|
|
28
|
-
return new Proxy(reactiveScope, {
|
|
29
|
-
get(target, prop) {
|
|
30
|
-
if (prop === '_isContext') return true
|
|
31
|
-
if (prop in target) return Reflect.get(target, prop, target)
|
|
32
|
-
if (context && prop in context) {
|
|
33
|
-
return Reflect.get(context, prop, context)
|
|
34
|
-
}
|
|
35
|
-
return Reflect.get(target, prop, target)
|
|
36
|
-
},
|
|
37
|
-
set(target, prop, value) {
|
|
38
|
-
if (prop in target) return Reflect.set(target, prop, value, target)
|
|
39
|
-
if (context && prop in context) {
|
|
40
|
-
return Reflect.set(context, prop, value, context)
|
|
41
|
-
}
|
|
42
|
-
return Reflect.set(target, prop, value, target)
|
|
43
|
-
},
|
|
44
|
-
has(target, prop) {
|
|
45
|
-
if (prop in target) return true
|
|
46
|
-
if (context && prop in context) return true
|
|
47
|
-
return false
|
|
48
|
-
},
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function parseNode(node: Node, context: Context) {
|
|
53
|
-
if (node.nodeType !== Node.ELEMENT_NODE) return
|
|
54
|
-
|
|
55
|
-
const el = node as HTMLElement
|
|
56
|
-
const scope = processScope(el, context)
|
|
57
|
-
|
|
58
|
-
const forAttrs = ['ui:for', ':for']
|
|
59
|
-
const forDirective = getDirectiveValue(el, forAttrs)
|
|
60
|
-
if (forDirective) {
|
|
61
|
-
removeDirectiveAttributes(el, forAttrs)
|
|
62
|
-
processFor(el, forDirective, scope)
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const ifAttrs = ['ui:if', ':if']
|
|
67
|
-
const ifDirective = getDirectiveValue(el, ifAttrs)
|
|
68
|
-
if (ifDirective) {
|
|
69
|
-
removeDirectiveAttributes(el, ifAttrs)
|
|
70
|
-
processIf(el, ifDirective, scope)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
processAttributes(el, scope)
|
|
74
|
-
|
|
75
|
-
const children = Array.from(el.childNodes)
|
|
76
|
-
for (const child of children) {
|
|
77
|
-
parseNode(child, scope)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function createScope(
|
|
82
|
-
selectorOrElement: string | HTMLElement,
|
|
83
|
-
context: Context = {},
|
|
84
|
-
) {
|
|
85
|
-
const el =
|
|
86
|
-
typeof selectorOrElement === 'string'
|
|
87
|
-
? document.querySelector(selectorOrElement)
|
|
88
|
-
: selectorOrElement
|
|
89
|
-
|
|
90
|
-
if (el) {
|
|
91
|
-
if (!context.$emit) {
|
|
92
|
-
context.$emit = (eventName: string, detail?: any) => {
|
|
93
|
-
el.dispatchEvent(
|
|
94
|
-
new CustomEvent(eventName, { detail, bubbles: true, composed: true }),
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
parseNode(el, context)
|
|
99
|
-
} else {
|
|
100
|
-
console.warn('[jsweb/ui] Element not found:', selectorOrElement)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function bindEffect(node: Node, fn: () => void) {
|
|
105
|
-
const e = effect(fn)
|
|
106
|
-
const bNode = node as BoundNode
|
|
107
|
-
bNode._effects ??= []
|
|
108
|
-
bNode._effects.push(e.stop)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function getDirectiveValue(el: HTMLElement, names: string[]) {
|
|
112
|
-
for (const name of names) {
|
|
113
|
-
const value = el.getAttribute(name)
|
|
114
|
-
if (value !== null) return value
|
|
115
|
-
}
|
|
116
|
-
return null
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function removeDirectiveAttributes(el: HTMLElement, names: string[]) {
|
|
120
|
-
for (const name of names) {
|
|
121
|
-
el.removeAttribute(name)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function processScope(el: HTMLElement, context: Context) {
|
|
126
|
-
const attrs = ['ui:scope', ':scope']
|
|
127
|
-
const directive = getDirectiveValue(el, attrs)
|
|
128
|
-
if (!directive) return context
|
|
129
|
-
|
|
130
|
-
const scope = evaluate(directive, context) || {}
|
|
131
|
-
removeDirectiveAttributes(el, attrs)
|
|
132
|
-
|
|
133
|
-
if (!scope.$emit) {
|
|
134
|
-
scope.$emit = (event: string, detail?: any) => {
|
|
135
|
-
el.dispatchEvent(
|
|
136
|
-
new CustomEvent(event, { detail, bubbles: true, composed: true }),
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return createContext(scope, context)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function processFor(el: HTMLElement, expr: string, context: Context) {
|
|
145
|
-
const parent = el.parentNode
|
|
146
|
-
if (!parent) return
|
|
147
|
-
|
|
148
|
-
const match = /^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(expr)
|
|
149
|
-
if (!match) {
|
|
150
|
-
return console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)
|
|
151
|
-
}
|
|
152
|
-
const [, itemName, listName] = match
|
|
153
|
-
|
|
154
|
-
const keyAttr = ['ui:key', ':key']
|
|
155
|
-
const keyDirective = getDirectiveValue(el, keyAttr)
|
|
156
|
-
removeDirectiveAttributes(el, keyAttr)
|
|
157
|
-
|
|
158
|
-
const uuid = crypto.randomUUID()
|
|
159
|
-
const comment = document.createComment(` ui:for ${uuid} `)
|
|
160
|
-
el.replaceWith(comment)
|
|
161
|
-
|
|
162
|
-
interface RenderedNode {
|
|
163
|
-
key: any
|
|
164
|
-
el: HTMLElement
|
|
165
|
-
scope: any
|
|
166
|
-
}
|
|
167
|
-
let renderedNodes: RenderedNode[] = []
|
|
168
|
-
|
|
169
|
-
bindEffect(comment, () => {
|
|
170
|
-
const list = evaluate(listName, context)
|
|
171
|
-
|
|
172
|
-
if (!Array.isArray(list)) {
|
|
173
|
-
renderedNodes.forEach((node) => {
|
|
174
|
-
node.el.remove()
|
|
175
|
-
cleanupTree(node.el)
|
|
176
|
-
})
|
|
177
|
-
renderedNodes = []
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const newNodes: RenderedNode[] = []
|
|
182
|
-
const oldNodesByKey = new Map<any, RenderedNode>()
|
|
183
|
-
renderedNodes.forEach((node) => oldNodesByKey.set(node.key, node))
|
|
184
|
-
|
|
185
|
-
list.forEach((item, index) => {
|
|
186
|
-
const scope = { [itemName]: item, $index: index }
|
|
187
|
-
let key: any = index
|
|
188
|
-
|
|
189
|
-
if (keyDirective) {
|
|
190
|
-
const tempContext = createContext(scope, context)
|
|
191
|
-
key = evaluate(keyDirective, tempContext)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
let node = oldNodesByKey.get(key)
|
|
195
|
-
if (node) {
|
|
196
|
-
// Reuse node
|
|
197
|
-
node.scope[itemName] = item
|
|
198
|
-
node.scope.$index = index
|
|
199
|
-
oldNodesByKey.delete(key)
|
|
200
|
-
} else {
|
|
201
|
-
// Create new node
|
|
202
|
-
const clone = el.cloneNode(true) as HTMLElement
|
|
203
|
-
const reactiveScope = reactive(scope)
|
|
204
|
-
const localContext = createContext(reactiveScope, context)
|
|
205
|
-
parseNode(clone, localContext)
|
|
206
|
-
node = { key, el: clone, scope: reactiveScope }
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
newNodes.push(node)
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
// Remove un-reused nodes
|
|
213
|
-
oldNodesByKey.forEach((node) => {
|
|
214
|
-
node.el.remove()
|
|
215
|
-
cleanupTree(node.el)
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
// Reorder and insert new DOM nodes
|
|
219
|
-
let currentAnchor = comment.nextSibling
|
|
220
|
-
newNodes.forEach((node) => {
|
|
221
|
-
if (currentAnchor === node.el) {
|
|
222
|
-
currentAnchor = currentAnchor.nextSibling
|
|
223
|
-
} else {
|
|
224
|
-
comment.parentNode?.insertBefore(node.el, currentAnchor)
|
|
225
|
-
}
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
renderedNodes = newNodes
|
|
229
|
-
})
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function processIf(el: HTMLElement, expr: string, context: Context) {
|
|
233
|
-
const parent = el.parentNode
|
|
234
|
-
if (!parent) return
|
|
235
|
-
|
|
236
|
-
const uuid = crypto.randomUUID()
|
|
237
|
-
const comment = document.createComment(` ui:if ${uuid} `)
|
|
238
|
-
el.before(comment)
|
|
239
|
-
|
|
240
|
-
bindEffect(comment, () => {
|
|
241
|
-
const val = evaluate(expr, context)
|
|
242
|
-
if (val) {
|
|
243
|
-
if (!el.parentNode) {
|
|
244
|
-
comment.parentNode?.insertBefore(el, comment.nextSibling)
|
|
245
|
-
}
|
|
246
|
-
} else if (el.parentNode) {
|
|
247
|
-
el.remove()
|
|
248
|
-
}
|
|
249
|
-
})
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function processAttributes(el: HTMLElement, context: Context) {
|
|
253
|
-
const attrs = Array.from(el.attributes)
|
|
254
|
-
|
|
255
|
-
for (const attr of attrs) {
|
|
256
|
-
const { name, value } = attr
|
|
257
|
-
const isText = ['ui:text', ':text'].includes(name)
|
|
258
|
-
const isTwoWayBind = ['ui:bind', ':bind'].includes(name)
|
|
259
|
-
const isAttrBind = name.startsWith('ui:') || name.startsWith(':')
|
|
260
|
-
const isEvent = name.startsWith('ui@') || name.startsWith('@')
|
|
261
|
-
|
|
262
|
-
if (isText) {
|
|
263
|
-
processTextBinding(el, value, context)
|
|
264
|
-
el.removeAttribute(name)
|
|
265
|
-
} else if (isTwoWayBind) {
|
|
266
|
-
processTwoWayBinding(el, value, context)
|
|
267
|
-
el.removeAttribute(name)
|
|
268
|
-
} else if (isAttrBind) {
|
|
269
|
-
const bound = name.split(':').pop()!
|
|
270
|
-
if (bound === 'class') {
|
|
271
|
-
processClassBinding(el, value, context)
|
|
272
|
-
} else if (bound === 'style') {
|
|
273
|
-
processStyleBinding(el, value, context)
|
|
274
|
-
} else {
|
|
275
|
-
processAttrBinding(el, bound, value, context)
|
|
276
|
-
}
|
|
277
|
-
el.removeAttribute(name)
|
|
278
|
-
} else if (isEvent) {
|
|
279
|
-
processEventBinding(el, name, value, context)
|
|
280
|
-
el.removeAttribute(name)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function processTextBinding(el: HTMLElement, expr: string, context: Context) {
|
|
286
|
-
bindEffect(el, () => {
|
|
287
|
-
const val = evaluate(expr, context)
|
|
288
|
-
el.textContent = val !== undefined && val !== null ? String(val) : ''
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {
|
|
293
|
-
const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'
|
|
294
|
-
const isRadio = el instanceof HTMLInputElement && el.type === 'radio'
|
|
295
|
-
|
|
296
|
-
// 1. Reactive state to DOM
|
|
297
|
-
bindEffect(el, () => {
|
|
298
|
-
const val = evaluate(expr, context)
|
|
299
|
-
if (isCheckbox) {
|
|
300
|
-
el.checked = !!val
|
|
301
|
-
} else if (isRadio) {
|
|
302
|
-
el.checked = el.value === String(val)
|
|
303
|
-
} else {
|
|
304
|
-
const target = el as
|
|
305
|
-
| HTMLInputElement
|
|
306
|
-
| HTMLSelectElement
|
|
307
|
-
| HTMLTextAreaElement
|
|
308
|
-
target.value = val == null ? '' : String(val)
|
|
309
|
-
}
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
// 2. DOM to Reactive state
|
|
313
|
-
const isChange = isCheckbox || isRadio || el instanceof HTMLSelectElement
|
|
314
|
-
const eventName = isChange ? 'change' : 'input'
|
|
315
|
-
el.addEventListener(eventName, ($event) => {
|
|
316
|
-
if (isCheckbox) {
|
|
317
|
-
evaluateEvent(`${expr} = $event.target.checked`, context, $event)
|
|
318
|
-
} else {
|
|
319
|
-
evaluateEvent(`${expr} = $event.target.value`, context, $event)
|
|
320
|
-
}
|
|
321
|
-
})
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function processAttrBinding(
|
|
325
|
-
el: HTMLElement,
|
|
326
|
-
attr: string,
|
|
327
|
-
expr: string,
|
|
328
|
-
context: Context,
|
|
329
|
-
) {
|
|
330
|
-
bindEffect(el, () => {
|
|
331
|
-
const val = evaluate(expr, context)
|
|
332
|
-
if (val === null || val === undefined || val === false) {
|
|
333
|
-
el.removeAttribute(attr)
|
|
334
|
-
} else if (val === true) {
|
|
335
|
-
el.setAttribute(attr, '')
|
|
336
|
-
} else {
|
|
337
|
-
el.setAttribute(attr, String(val))
|
|
338
|
-
}
|
|
339
|
-
})
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function processClassBinding(el: HTMLElement, expr: string, context: Context) {
|
|
343
|
-
let oldClasses = new Set<string>()
|
|
344
|
-
|
|
345
|
-
bindEffect(el, () => {
|
|
346
|
-
const val = evaluate(expr, context)
|
|
347
|
-
const newClasses = new Set<string>()
|
|
348
|
-
const addClass = (c: string) => c && newClasses.add(c)
|
|
349
|
-
const addClasses = (c: string) => c.split(/\s+/).forEach(addClass)
|
|
350
|
-
|
|
351
|
-
if (typeof val === 'string') addClasses(val)
|
|
352
|
-
else if (Array.isArray(val)) {
|
|
353
|
-
val.flat().forEach((c: any) => {
|
|
354
|
-
if (typeof c === 'string') addClasses(c)
|
|
355
|
-
})
|
|
356
|
-
} else if (typeof val === 'object' && val !== null) {
|
|
357
|
-
Object.entries(val).forEach(([c, condition]: [string, any]) => {
|
|
358
|
-
if (condition) addClasses(c)
|
|
359
|
-
})
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
oldClasses.forEach((c) => {
|
|
363
|
-
if (!newClasses.has(c)) el.classList.remove(c)
|
|
364
|
-
})
|
|
365
|
-
newClasses.forEach((c) => {
|
|
366
|
-
if (!oldClasses.has(c)) el.classList.add(c)
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
oldClasses = newClasses
|
|
370
|
-
})
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function processStyleBinding(el: HTMLElement, expr: string, context: Context) {
|
|
374
|
-
let oldStyles: Record<string, any> = {}
|
|
375
|
-
|
|
376
|
-
bindEffect(el, () => {
|
|
377
|
-
const val = evaluate(expr, context)
|
|
378
|
-
const newStyles = typeof val === 'object' && val !== null ? val : {}
|
|
379
|
-
|
|
380
|
-
for (const key in oldStyles) {
|
|
381
|
-
if (!(key in newStyles)) {
|
|
382
|
-
;(el.style as any)[key] = ''
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
for (const key in newStyles) {
|
|
387
|
-
if (oldStyles[key] !== newStyles[key]) {
|
|
388
|
-
;(el.style as any)[key] = newStyles[key]
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
oldStyles = { ...newStyles }
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function processEventBinding(
|
|
397
|
-
el: HTMLElement,
|
|
398
|
-
evt: string,
|
|
399
|
-
expr: string,
|
|
400
|
-
context: Context,
|
|
401
|
-
) {
|
|
402
|
-
const refs = evt.split('@').pop()!
|
|
403
|
-
const [name, ...modifiers] = refs.split('.')
|
|
404
|
-
|
|
405
|
-
const isOutside = modifiers.includes('outside')
|
|
406
|
-
const target = isOutside ? document : el
|
|
407
|
-
|
|
408
|
-
const handler: EventListener = ($event: Event) => {
|
|
409
|
-
if (!el.isConnected) return
|
|
410
|
-
|
|
411
|
-
const isTargetNode = $event.target instanceof Node
|
|
412
|
-
|
|
413
|
-
if (isOutside && isTargetNode && el.contains($event.target)) return
|
|
414
|
-
if (modifiers.includes('self') && $event.target !== el) return
|
|
415
|
-
|
|
416
|
-
if (modifiers.includes('prevent')) $event.preventDefault()
|
|
417
|
-
if (modifiers.includes('stop')) $event.stopPropagation()
|
|
418
|
-
|
|
419
|
-
evaluateEvent(expr, context, $event)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
target.addEventListener(name, handler)
|
|
423
|
-
|
|
424
|
-
if (isOutside) {
|
|
425
|
-
const bNode = el as BoundNode
|
|
426
|
-
bNode._effects ??= []
|
|
427
|
-
bNode._effects.push(() => target.removeEventListener(name, handler))
|
|
428
|
-
}
|
|
429
|
-
}
|