@jsweb/ui 1.2.0 → 1.2.1

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 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
- }