@jsweb/ui 0.1.0 → 0.2.0
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/HISTORY.md +4 -4
- package/PROJECT.md +9 -7
- package/README.md +3 -1
- package/dist/src/parser.d.ts +2 -1
- package/dist/src/reactivity.d.ts +14 -1
- package/dist/ui.es.js +1 -1
- package/dist/ui.es.js.map +1 -1
- package/dist/ui.umd.js +1 -1
- package/dist/ui.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/parser.ts +221 -138
- package/src/reactivity.ts +81 -20
package/HISTORY.md
CHANGED
|
@@ -4,6 +4,7 @@ Este documento serve como contexto histórico de todas as funcionalidades implem
|
|
|
4
4
|
|
|
5
5
|
## 🧠 1. Core de Reatividade (`src/reactivity.ts`)
|
|
6
6
|
Construímos um motor baseado em **Signals** usando `Proxy` para interceptar leituras (`track`) e escritas (`trigger`).
|
|
7
|
+
- **Arquitetura `ReactiveEffect`:** A reatividade é gerenciada pela classe `ReactiveEffect`, permitindo controle refinado sobre o ciclo de vida dos efeitos (`stop`, `cleanup`) e garantindo limpeza eficiente de dependências para evitar vazamentos de memória. Também inclui micro-otimizações no rastreamento do Proxy para evitar buscas redundantes.
|
|
7
8
|
- **Deep Reactivity:** A reatividade funciona recursivamente em objetos profundamente aninhados.
|
|
8
9
|
- **Arrays e Mutabilidade:** Tratamento especial para arrays, onde a adição ou remoção de itens notifica dependências sobre a propriedade `length`, o que garante que loops reajam adequadamente a `.push`, `.pop`, etc.
|
|
9
10
|
- **Transparência:** O usuário final trabalha com dados mutáveis puros sem a necessidade de getters/setters explícitos (ex: `state.count++` em vez de `state.count.value++`).
|
|
@@ -21,7 +22,7 @@ Em vez de Virtual DOM, manipulamos o DOM real empacotando atualizações atravé
|
|
|
21
22
|
- **`:if`:** Renderização condicional. O framework usa "âncoras" (`Comment Nodes`) para substituir dinamicamente o elemento no DOM quando a condição é falsa e restaurá-lo na posição exata quando for verdadeira.
|
|
22
23
|
- **`:for` e Reconciliação:** Renderização de listas utilizando algoritmo de *diffing*. O motor rastreia chaves (`:key` ou fallback para índice) de cada elemento gerado e reutiliza os mesmos nós DOM (`RenderedNode`). Isso traz performance massiva e garante que atributos nativos do navegador (ex: foco de um input) não se percam em mudanças reativas do array.
|
|
23
24
|
- **Atributos Genéricos (`:attr`):** Transformação dinâmica de qualquer atributo. Valores booleanos injetam/removem o atributo (ex: `disabled`).
|
|
24
|
-
- **Eventos (`@event`):** Adição simples de ouvintes a qualquer evento DOM nativo.
|
|
25
|
+
- **Eventos (`@event` e Modificadores):** Adição simples de ouvintes a qualquer evento DOM nativo. Inclui suporte nativo a **modificadores encadeados** com sintaxe de ponto (ex: `@submit.prevent`, `@click.stop`, `@click.self`) para um controle declarativo do comportamento do evento.
|
|
25
26
|
- **Two-way Data Binding (`:bind`):** Suporte total a reatividade bidirecional (Tela <-> Estado) para `input[text]`, `input[checkbox]`, `input[radio]`, `<select>` e `<textarea>`. Sincroniza em tempo real tanto via evento `input` quanto `change`.
|
|
26
27
|
|
|
27
28
|
## 📦 4. Build e Bundling (`vite.config.ts`)
|
|
@@ -31,6 +32,5 @@ Em vez de Virtual DOM, manipulamos o DOM real empacotando atualizações atravé
|
|
|
31
32
|
|
|
32
33
|
## 🎯 Próximos Passos (Backlog Futuro Sugerido)
|
|
33
34
|
Para outros agentes, aqui estão os próximos passos lógicos de evolução deste framework:
|
|
34
|
-
1. **
|
|
35
|
-
2. **
|
|
36
|
-
3. **Eventos customizados:** Implementação de um `$emit` para comunicação de um escopo/componente interno para um mais externo.
|
|
35
|
+
1. **Sintaxe Especial para Classes e Estilos:** Suporte para dicionários lógicos no CSS como `:class="{ 'is-active': active }"`.
|
|
36
|
+
2. **Eventos customizados:** Implementação de um `$emit` para comunicação de um escopo/componente interno para um mais externo.
|
package/PROJECT.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
### A. Sistema de Reatividade
|
|
19
19
|
|
|
20
|
-
- **Mecanismo:** Proxy-based
|
|
20
|
+
- **Mecanismo:** Proxy-based em conjunto com a classe `ReactiveEffect`. O estado é interceptado para disparar "efeitos" com gerenciamento preciso de dependências, controle de ciclo de vida (`stop`, `cleanup`) e otimizado contra vazamento de memória.
|
|
21
21
|
- **Global State:** Deve ser possível exportar um objeto reativo de um arquivo e importá-lo em múltiplos componentes/contextos, tornando-o um estado compartilhado.
|
|
22
22
|
- **Global Effect:** Deve ser possível criar efeitos globais que reajam a mudanças em qualquer estado compartilhado.
|
|
23
23
|
- **Local State:** Deve ser possível criar estados locais que reajam a mudanças apenas dentro do escopo do componente.
|
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
|
|
31
31
|
### B. Avaliador de Expressões (The Evaluator)
|
|
32
32
|
|
|
33
|
-
- **Implementação:** Uso de `new Function()`.
|
|
34
|
-
- **Estratégia de
|
|
35
|
-
1.
|
|
36
|
-
2.
|
|
37
|
-
3.
|
|
33
|
+
- **Implementação:** Uso de `new Function()` com `with(this)`.
|
|
34
|
+
- **Estratégia de Execução:** Para avaliar expressões declaradas no HTML de forma encapsulada (sandboxed):
|
|
35
|
+
1. O motor encapsula o objeto/escopo em um Proxy de Contexto para resolução de dependências.
|
|
36
|
+
2. Constrói a função dinâmica: `new Function('with(this) { ... }')`.
|
|
37
|
+
3. Executa a função passando o escopo reativo atrelado ao `this`.
|
|
38
|
+
4. Para eventos, também expõe a variável nativa `$event`.
|
|
38
39
|
|
|
39
40
|
### C. Parser de Template
|
|
40
41
|
|
|
@@ -48,7 +49,8 @@
|
|
|
48
49
|
| `ui:scope` | Define o objeto de estado para o elemento e seus filhos. | `<div ui:scope="{ count: 0 }">` |
|
|
49
50
|
| `ui:text` | Sincroniza o `textContent` com uma variável. | `<span ui:text="count"></span>` |
|
|
50
51
|
| `:attr` | Shorthand para bind de atributos HTML nativos. | `<button :disabled="count > 10">` |
|
|
51
|
-
| `@event` | Shorthand para event listeners.
|
|
52
|
+
| `@event` | Shorthand para event listeners (com suporte a modificadores). | `<button @click.prevent="save">` |
|
|
53
|
+
| `:bind` | Two-way data binding para inputs, checkboxes, radios e selects. | `<input :bind="name">` |
|
|
52
54
|
| `ui:if` | Adiciona/Remove o elemento do DOM (via Comment Node placeholder). | `<div ui:if="count > 0">` |
|
|
53
55
|
| `ui:for` | Renderiza uma lista de elementos a partir de um array. | `<li ui:for="item in items">` |
|
|
54
56
|
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ O `@jsweb/ui` é um micro-framework frontend escrito em TypeScript, projetado pa
|
|
|
12
12
|
- **Module**: Pacote ESM com exports nomeados para suporte a Tree-Shaking.
|
|
13
13
|
- **Contexto Híbrido**: Suporta definição de estado via Objetos Literais (POJOs) ou Classes TypeScript.
|
|
14
14
|
- **Template Engine**: Baseado em atributos customizados no HTML (`ui:*` para diretivas e `ui@*` para eventos, com shorthands `@`, `:`).
|
|
15
|
+
- Suporte completo a **modificadores de eventos** encadeados (`.prevent`, `.stop`, `.self`, `.outside`).
|
|
15
16
|
|
|
16
17
|
## Instalação
|
|
17
18
|
|
|
@@ -36,7 +37,8 @@ O framework utiliza um sistema de atributos customizados para declaratividade no
|
|
|
36
37
|
| `ui:scope` | Define o objeto de estado para o elemento e seus filhos. | `<div ui:scope="{ count: 0 }">` |
|
|
37
38
|
| `ui:text` | Sincroniza o `textContent` com uma variável. | `<span ui:text="count"></span>` |
|
|
38
39
|
| `:attr` | Shorthand para bind de atributos HTML nativos (Binding Condicional). | `<button :disabled="count > 10">` |
|
|
39
|
-
| `@event` | Shorthand para event listeners.
|
|
40
|
+
| `@event` | Shorthand para event listeners (suporta modificadores encadeados). | `<button @click.prevent="save">` |
|
|
41
|
+
| `:bind` | Two-way data binding para inputs, checkboxes, radios e selects. | `<input :bind="name">` |
|
|
40
42
|
| `ui:if` | Adiciona/Remove o elemento do DOM (via Comment Node placeholder). | `<div ui:if="count > 0">` |
|
|
41
43
|
| `ui:for` | Renderiza uma lista de elementos a partir de um array. | `<li ui:for="item in items">` |
|
|
42
44
|
|
package/dist/src/parser.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type Context = Record<string, any>;
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function cleanupTree(node: Node): void;
|
|
3
|
+
export declare function createContext(scope: any, context?: Context | null): Context;
|
|
3
4
|
export declare function parseNode(node: Node, context: Context): void;
|
|
4
5
|
export declare function createComponent(selectorOrElement: string | HTMLElement, rootContext?: Context): void;
|
package/dist/src/reactivity.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare class ReactiveEffect {
|
|
2
|
+
fn: () => void;
|
|
3
|
+
active: boolean;
|
|
4
|
+
deps: Set<Set<ReactiveEffect>>;
|
|
5
|
+
constructor(fn: () => void);
|
|
6
|
+
run(): void;
|
|
7
|
+
stop(): void;
|
|
8
|
+
cleanup(): void;
|
|
9
|
+
effect(): {
|
|
10
|
+
run: () => void;
|
|
11
|
+
stop: () => void;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function effect(fn: () => void): any;
|
|
2
15
|
export declare function track(target: object, key: string | symbol): void;
|
|
3
16
|
export declare function trigger(target: object, key: string | symbol): void;
|
|
4
17
|
export declare function reactive<T extends object>(target: T): T;
|
package/dist/ui.es.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var e=null,t=/* @__PURE__ */new WeakMap;
|
|
1
|
+
var e=null,t=/* @__PURE__ */new WeakMap,n=/* @__PURE__ */new WeakMap,o=/* @__PURE__ */new WeakMap,r=class{active=!0;deps=/* @__PURE__ */new Set;constructor(e){this.fn=e}run(){if(!this.active)return this.fn();this.cleanup(),e=Symbol(),o.set(e,this);try{return this.fn()}finally{o.delete(e),e=null}}stop(){this.active&&(this.cleanup(),this.active=!1)}cleanup(){this.deps.forEach(e=>e.delete(this)),this.deps.clear()}effect(){return{run:()=>this.run(),stop:()=>this.stop()}}};function i(e){const t=new r(e);return t.run(),t.effect()}function s(n,r){if(e){let i=t.get(n);i||(i=/* @__PURE__ */new Map,t.set(n,i));let s=i.get(r);s||(s=/* @__PURE__ */new Set,i.set(r,s));const c=o.get(e);c&&(s.add(c),c.deps.add(s))}}function c(e,n){const o=t.get(e);if(!o)return;const r=o.get(n);r&&new Set(r).forEach(e=>e.run())}function u(e){if("object"!=typeof e||null===e)return e;if(Object.hasOwn(e,"_isReactive"))return e;const t=n.get(e);if(t)return t;const o=new Proxy(e,{get(e,t,n){if("_isReactive"===t)return!0;s(e,t);const o=Reflect.get(e,t,n);return"object"==typeof o&&null!==o?u(o):o},set(e,t,n,o){const r=Array.isArray(e),i=Reflect.get(e,t,o),s=r&&String(Number(t))===t?Number(t)<e.length:Object.hasOwn(e,t),u=Reflect.set(e,t,n,o);return s?i!==n&&c(e,t):(c(e,t),r&&"length"!==t&&c(e,"length")),u}});return n.set(e,o),o}function f(e,t={}){try{return new Function(`with(this) { return ${e} }`).call(t)}catch(n){return void console.error(`[jsweb/ui] Error evaluating expression: ${e}`,n)}}function a(e,t={},n){try{const o=e.trim(),r=/^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(o);new Function("$event",`with(this) { ${r?`${o} instanceof Function ? ${o}.call(this, $event) : ${o}`:o} }`).call(t,n)}catch(o){console.error(`[jsweb/ui] Error evaluating event expression: ${e}`,o)}}function l(e){const t=e;t._effects&&(t._effects.forEach(e=>e()),t._effects=[]);const n=Array.from(e.childNodes);for(const o of n)l(o)}function d(e,t=null){const n=e._isReactive?e:u(e);return new Proxy(n,{get:(e,n)=>"_isContext"===n||(n in e?Reflect.get(e,n,e):t&&n in t?Reflect.get(t,n,t):Reflect.get(e,n,e)),set:(e,n,o)=>n in e?Reflect.set(e,n,o,e):t&&n in t?Reflect.set(t,n,o,t):Reflect.set(e,n,o,e),has:(e,n)=>n in e||!(!t||!(n in t))})}function p(e,t){if(e.nodeType!==Node.ELEMENT_NODE)return;const n=e,o=function(e,t){const n=["ui:scope",":scope"],o=g(e,n);if(!o)return t;const r=f(o,t)||{};return m(e,n),d(r,t)}(n,t),r=g(n,["ui:for",":for"]);if(r)return m(n,["ui:for",":for"]),void function(e,t,n){if(!e.parentNode)return;const o=/^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(t);if(!o)return console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,r,i]=o,s=["ui:key",":key"],c=g(e,s);m(e,s);const a=crypto.randomUUID(),h=document.createComment(` ui:for ${a} `);e.replaceWith(h);let w=[];v(h,()=>{const t=f(i,n);if(!Array.isArray(t))return w.forEach(e=>{e.el.remove(),l(e.el)}),void(w=[]);const o=[],s=/* @__PURE__ */new Map;w.forEach(e=>s.set(e.key,e)),t.forEach((t,i)=>{const a={[r]:t,$index:i};let l=i;c&&(l=f(c,d(a,n)));let h=s.get(l);if(h)h.scope[r]=t,h.scope.$index=i,s.delete(l);else{const t=e.cloneNode(!0),o=u(a);p(t,d(o,n)),h={key:l,el:t,scope:o}}o.push(h)}),s.forEach(e=>{e.el.remove(),l(e.el)});let a=h.nextSibling;o.forEach(e=>{a===e.el?a=a.nextSibling:h.parentNode?.insertBefore(e.el,a)}),w=o})}(n,r,o);const i=g(n,["ui:if",":if"]);i&&(m(n,["ui:if",":if"]),function(e,t,n){if(!e.parentNode)return;const o=crypto.randomUUID(),r=document.createComment(` ui:if ${o} `);e.before(r),v(r,()=>{f(t,n)?e.parentNode||r.parentNode?.insertBefore(e,r.nextSibling):e.parentNode&&e.remove()})}(n,i,o)),function(e,t){const n=Array.from(e.attributes);for(const o of n){const{name:n,value:r}=o,i=["ui:text",":text"].includes(n),s=["ui:bind",":bind"].includes(n),c=n.startsWith("ui:")||n.startsWith(":"),u=n.startsWith("ui@")||n.startsWith("@");i?w(e,r,t):s?b(e,r,t):c?y(e,n.split(":").pop(),r,t):u&&E(e,n,r,t),e.removeAttribute(n)}}(n,o);const s=Array.from(n.childNodes);for(const c of s)p(c,o)}function h(e,t={}){const n="string"==typeof e?document.querySelector(e):e;n?p(n,t):console.warn("[jsweb/ui] Element not found:",e)}function v(e,t){const n=i(t),o=e;o._effects??=[],o._effects.push(n.stop)}function g(e,t){for(const n of t){const t=e.getAttribute(n);if(null!==t)return t}return null}function m(e,t){for(const n of t)e.removeAttribute(n)}function w(e,t,n){v(e,()=>{const o=f(t,n);e.textContent=null!=o?String(o):""})}function b(e,t,n){const o=e instanceof HTMLInputElement&&"checkbox"===e.type,r=e instanceof HTMLInputElement&&"radio"===e.type;v(e,()=>{const i=f(t,n);if(o)e.checked=!!i;else if(r)e.checked=e.value===String(i);else{e.value=null==i?"":String(i)}});const i=o||r||e instanceof HTMLSelectElement?"change":"input";e.addEventListener(i,e=>{a(o?`${t} = $event.target.checked`:`${t} = $event.target.value`,n,e)})}function y(e,t,n,o){v(e,()=>{const r=f(n,o);null==r||!1===r?e.removeAttribute(t):!0===r?e.setAttribute(t,""):e.setAttribute(t,String(r))})}function E(e,t,n,o){const[r,...i]=t.split("@").pop().split("."),s=i.includes("outside"),c=s?document:e,u=t=>{if(!e.isConnected)return;const r=t.target instanceof Node;s&&r&&e.contains(t.target)||i.includes("self")&&t.target!==e||(i.includes("prevent")&&t.preventDefault(),i.includes("stop")&&t.stopPropagation(),a(n,o,t))};if(c.addEventListener(r,u),s){const t=e;t._effects??=[],t._effects.push(()=>c.removeEventListener(r,u))}}if("undefined"!=typeof window){const e=window;e.jsweb=e.jsweb||{},e.jsweb.ui={createComponent:h,reactive:u,effect:i,track:s,trigger:c,evaluate:f,evaluateEvent:a,parseNode:p}}export{h as createComponent,i as effect,f as evaluate,a as evaluateEvent,p as parseNode,u as reactive,s as track,c as trigger};
|
|
2
2
|
//# sourceMappingURL=ui.es.js.map
|
package/dist/ui.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.es.js","names":[],"sources":["../src/reactivity.ts","../src/evaluator.ts","../src/parser.ts","../src/index.ts"],"sourcesContent":["let activeEffect: (() => void) | null = null\nconst targetMap = new WeakMap<object, Map<string | symbol, Set<() => void>>>()\n\nexport function effect(fn: () => void) {\n const effectFn = () => {\n // cleanup old deps could be added here\n activeEffect = effectFn\n fn()\n activeEffect = null\n }\n effectFn()\n}\n\nexport function track(target: object, key: string | symbol) {\n if (activeEffect) {\n let depsMap = targetMap.get(target)\n if (!depsMap) {\n depsMap = new Map()\n targetMap.set(target, depsMap)\n }\n let dep = depsMap.get(key)\n if (!dep) {\n dep = new Set()\n depsMap.set(key, dep)\n }\n dep.add(activeEffect)\n }\n}\n\nexport function trigger(target: object, key: string | symbol) {\n const depsMap = targetMap.get(target)\n if (!depsMap) return\n const dep = depsMap.get(key)\n if (dep) {\n dep.forEach((effectFn) => effectFn())\n }\n}\n\nexport function reactive<T extends object>(target: T): T {\n if (typeof target !== 'object' || target === null) return target\n if ((target as any).__isReactive) return target\n\n return new Proxy(target, {\n get(obj, key, receiver) {\n if (key === '__isReactive') return true\n track(obj, key)\n const res = Reflect.get(obj, key, receiver)\n // deep reactivity\n if (typeof res === 'object' && res !== null) {\n return reactive(res)\n }\n return res\n },\n set(obj, key, value, receiver) {\n const isArray = Array.isArray(obj)\n const oldValue = Reflect.get(obj, key, receiver)\n const hadKey = isArray && String(Number(key)) === key \n ? Number(key) < obj.length \n : Object.prototype.hasOwnProperty.call(obj, key)\n\n const result = Reflect.set(obj, key, value, receiver)\n\n if (!hadKey) {\n trigger(obj, key)\n if (isArray && key !== 'length') {\n trigger(obj, 'length')\n }\n } else if (oldValue !== value) {\n trigger(obj, key)\n }\n \n return result\n },\n })\n}\n","export function evaluate(\n expression: string,\n context: Record<string, any> = {},\n) {\n try {\n const fn = new Function(`with(this) { return ${expression} }`)\n return fn.call(context)\n } catch (error) {\n console.error(`[jsweb/ui] Error evaluating expression: ${expression}`, error)\n return undefined\n }\n}\n\nexport function evaluateEvent(\n expression: string,\n context: Record<string, any> = {},\n $event: Event,\n) {\n try {\n const exp = expression.trim()\n const isIdentifier = /^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(exp)\n const code = `${exp} instanceof Function ? ${exp}.call(this, $event) : ${exp}`\n const result = isIdentifier ? code : exp\n\n const fn = new Function('$event', `with(this) { ${result} }`)\n fn.call(context, $event)\n } catch (error) {\n console.error(\n `[jsweb/ui] Error evaluating event expression: ${expression}`,\n error,\n )\n }\n}\n","import { effect, reactive } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\n\nexport type Context = Record<string, any>\n\nexport function createContext(scopeData: any, parentContext: Context | null = null): Context {\n const reactiveScope = scopeData.__isReactive ? scopeData : reactive(scopeData)\n \n return new Proxy(reactiveScope, {\n get(target, prop) {\n if (prop === '__isContext') return true\n if (prop in target) return Reflect.get(target, prop, target)\n if (parentContext && prop in parentContext) {\n return Reflect.get(parentContext, prop, parentContext)\n }\n return Reflect.get(target, prop, target)\n },\n set(target, prop, value) {\n if (prop in target) return Reflect.set(target, prop, value, target)\n if (parentContext && prop in parentContext) {\n return Reflect.set(parentContext, prop, value, parentContext)\n }\n return Reflect.set(target, prop, value, target)\n },\n has(target, prop) {\n if (prop in target) return true\n if (parentContext && prop in parentContext) return true\n return false\n }\n })\n}\n\nexport function parseNode(node: Node, context: Context) {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const el = node as HTMLElement\n\n // 1. Check for scope\n let currentContext = context\n const scopeAttr = el.getAttribute('ui:scope') || el.getAttribute(':scope')\n if (scopeAttr) {\n const scopeData = evaluate(scopeAttr, context) || {}\n currentContext = createContext(scopeData, context)\n el.removeAttribute('ui:scope')\n el.removeAttribute(':scope')\n }\n\n // 2. Check for ui:for (must be processed before children and other directives on same element)\n const forAttr = el.getAttribute('ui:for') || el.getAttribute(':for')\n if (forAttr) {\n el.removeAttribute('ui:for')\n el.removeAttribute(':for')\n processFor(el, forAttr, currentContext)\n return // Stop processing this node further, processFor handles clones\n }\n\n // 3. Check for ui:if\n const ifAttr = el.getAttribute('ui:if') || el.getAttribute(':if')\n if (ifAttr) {\n el.removeAttribute('ui:if')\n el.removeAttribute(':if')\n processIf(el, ifAttr, currentContext)\n // We continue processing children because the element might be shown\n }\n\n // 4. Other directives\n const attrs = Array.from(el.attributes)\n for (const attr of attrs) {\n const { name, value } = attr\n const isText = ['ui:text', ':text'].includes(name)\n const isTwoWayBind = ['ui:bind', ':bind'].includes(name)\n const isAttrBind = name.startsWith('ui:') || name.startsWith(':') \n const isEvent = name.startsWith('ui@') || name.startsWith('@')\n\n if (isText) {\n el.removeAttribute(name)\n effect(() => {\n const val = evaluate(value, currentContext)\n el.textContent = val !== undefined && val !== null ? String(val) : ''\n })\n } else if (isTwoWayBind) {\n el.removeAttribute(name)\n processTwoWayBinding(el, value, currentContext)\n } else if (isAttrBind) {\n const boundAttr = name.split(':').pop()! \n el.removeAttribute(name)\n effect(() => {\n const val = evaluate(value, currentContext)\n if (val === null || val === undefined || val === false) {\n el.removeAttribute(boundAttr)\n } else if (val === true) {\n el.setAttribute(boundAttr, '')\n } else {\n el.setAttribute(boundAttr, String(val))\n }\n })\n } else if (isEvent) {\n const eventName = name.split('@').pop()!\n el.removeAttribute(name)\n el.addEventListener(eventName, ($event) => {\n evaluateEvent(value, currentContext, $event)\n })\n }\n }\n\n // Process children\n // Need to convert to array because childNodes might mutate if elements are added/removed\n const children = Array.from(el.childNodes)\n for (const child of children) {\n parseNode(child, currentContext)\n }\n }\n}\n\nfunction processIf(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:if ${uuid} `)\n parent.insertBefore(comment, el)\n\n effect(() => {\n const val = evaluate(expr, context)\n if (val) {\n if (!el.parentNode) {\n comment.parentNode?.insertBefore(el, comment.nextSibling)\n }\n } else {\n if (el.parentNode) {\n el.parentNode.removeChild(el)\n }\n }\n })\n}\n\nfunction processFor(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n \n const match = expr.match(/^\\s*(.+)\\s+(?:in|of)\\s+(.+)\\s*$/)\n if (!match) {\n console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)\n return\n }\n const [, itemName, listName] = match\n \n const keyExpr = el.getAttribute('ui:key') || el.getAttribute(':key')\n el.removeAttribute('ui:key')\n el.removeAttribute(':key')\n \n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:for ${uuid} `)\n parent.replaceChild(comment, el)\n \n type RenderedNode = {\n key: any\n el: HTMLElement\n scope: any\n }\n let renderedNodes: RenderedNode[] = []\n\n effect(() => {\n const list = evaluate(listName, context)\n\n if (!Array.isArray(list)) {\n renderedNodes.forEach(node => node.el.parentNode?.removeChild(node.el))\n renderedNodes = []\n return\n }\n\n const newNodes: RenderedNode[] = []\n const oldNodesByKey = new Map<any, RenderedNode>()\n renderedNodes.forEach(node => oldNodesByKey.set(node.key, node))\n\n list.forEach((item, index) => {\n const scope = { [itemName]: item, $index: index }\n let key: any = index\n\n if (keyExpr) {\n const tempContext = createContext(scope, context)\n key = evaluate(keyExpr, tempContext)\n }\n\n let node = oldNodesByKey.get(key)\n if (node) {\n // Reuse node\n node.scope[itemName] = item\n node.scope.$index = index\n oldNodesByKey.delete(key)\n } else {\n // Create new node\n const clone = el.cloneNode(true) as HTMLElement\n const reactiveScope = reactive(scope)\n const localContext = createContext(reactiveScope, context)\n parseNode(clone, localContext)\n node = { key, el: clone, scope: reactiveScope }\n }\n \n newNodes.push(node)\n })\n\n // Remove un-reused nodes\n oldNodesByKey.forEach(node => {\n node.el.parentNode?.removeChild(node.el)\n })\n\n // Reorder and insert new DOM nodes\n let currentAnchor = comment.nextSibling\n newNodes.forEach((node) => {\n if (currentAnchor === node.el) {\n currentAnchor = currentAnchor.nextSibling\n } else {\n comment.parentNode?.insertBefore(node.el, currentAnchor)\n }\n })\n\n renderedNodes = newNodes\n })\n}\n\nexport function createComponent(\n selectorOrElement: string | HTMLElement,\n rootContext: Context = {},\n) {\n const el =\n typeof selectorOrElement === 'string'\n ? document.querySelector(selectorOrElement)\n : selectorOrElement\n\n if (el) {\n parseNode(el, rootContext)\n } else {\n console.warn(`[jsweb/ui] Element not found: ${selectorOrElement}`)\n }\n}\n\nfunction processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {\n const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'\n const isRadio = el instanceof HTMLInputElement && el.type === 'radio'\n \n // 1. Reactive state to DOM\n effect(() => {\n const val = evaluate(expr, context)\n if (isCheckbox) {\n const target = el as HTMLInputElement\n target.checked = !!val\n } else if (isRadio) {\n const target = el as HTMLInputElement\n target.checked = target.value === String(val)\n } else {\n const target = el as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement\n target.value = val == null ? '' : String(val)\n }\n })\n\n // 2. DOM to Reactive state\n const isChange = isCheckbox || isRadio || el instanceof HTMLSelectElement\n const eventName = isChange ? 'change' : 'input'\n el.addEventListener(eventName, ($event) => {\n if (isCheckbox) {\n evaluateEvent(`${expr} = $event.target.checked`, context, $event)\n } else {\n evaluateEvent(`${expr} = $event.target.value`, context, $event)\n }\n })\n}\n","import { reactive, effect, track, trigger } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\nimport { createComponent, parseNode } from './parser'\n\nexport {\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n createComponent,\n parseNode,\n}\n\nif (typeof window !== 'undefined') {\n const w = window as any\n w.jsweb = w.jsweb || {}\n w.jsweb.ui = {\n createComponent,\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n parseNode,\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ui.es.js","names":[],"sources":["../src/reactivity.ts","../src/evaluator.ts","../src/parser.ts","../src/index.ts"],"sourcesContent":["let activeEffect: symbol | null = null\nconst targetMap = new WeakMap<\n object,\n Map<string | symbol, Set<ReactiveEffect>>\n>()\nconst proxyMap = new WeakMap<object, any>()\nconst effectMap = new WeakMap<symbol, ReactiveEffect>()\n\nexport class ReactiveEffect {\n active = true\n deps: Set<Set<ReactiveEffect>> = new Set()\n\n constructor(public fn: () => void) {}\n\n run() {\n if (!this.active) return this.fn()\n\n this.cleanup()\n\n activeEffect = Symbol()\n effectMap.set(activeEffect, this)\n\n try {\n return this.fn()\n } finally {\n effectMap.delete(activeEffect)\n activeEffect = null\n }\n }\n\n stop() {\n if (this.active) {\n this.cleanup()\n this.active = false\n }\n }\n\n cleanup() {\n this.deps.forEach((dep) => dep.delete(this))\n this.deps.clear()\n }\n\n effect() {\n return {\n run: () => this.run(),\n stop: () => this.stop(),\n }\n }\n}\n\nexport function effect(fn: () => void): any {\n const ref = new ReactiveEffect(fn)\n ref.run()\n return ref.effect()\n}\n\nexport function track(target: object, key: string | symbol) {\n if (activeEffect) {\n let depsMap = targetMap.get(target)\n if (!depsMap) {\n depsMap = new Map()\n targetMap.set(target, depsMap)\n }\n\n let dep = depsMap.get(key)\n if (!dep) {\n dep = new Set()\n depsMap.set(key, dep)\n }\n\n const active = effectMap.get(activeEffect)\n if (active) {\n dep.add(active)\n active.deps.add(dep)\n }\n }\n}\n\nexport function trigger(target: object, key: string | symbol) {\n const depsMap = targetMap.get(target)\n if (!depsMap) return\n\n const dep = depsMap.get(key)\n if (dep) {\n const effects = new Set(dep)\n effects.forEach((effect) => effect.run())\n }\n}\n\nexport function reactive<T extends object>(target: T): T {\n const notObject = typeof target !== 'object' || target === null\n if (notObject) return target\n\n const isReactive = Object.hasOwn(target, '_isReactive')\n if (isReactive) return target\n\n const existingProxy = proxyMap.get(target)\n if (existingProxy) return existingProxy\n\n const proxy = new Proxy(target, {\n get(obj, key, receiver) {\n if (key === '_isReactive') return true\n track(obj, key)\n const res = Reflect.get(obj, key, receiver)\n // deep reactivity\n if (typeof res === 'object' && res !== null) {\n return reactive(res)\n }\n return res\n },\n set(obj, key, value, receiver) {\n const isArray = Array.isArray(obj)\n const oldValue = Reflect.get(obj, key, receiver)\n const hadKey =\n isArray && String(Number(key)) === key\n ? Number(key) < obj.length\n : Object.hasOwn(obj, key)\n\n const result = Reflect.set(obj, key, value, receiver)\n\n if (!hadKey) {\n trigger(obj, key)\n if (isArray && key !== 'length') {\n trigger(obj, 'length')\n }\n } else if (oldValue !== value) {\n trigger(obj, key)\n }\n\n return result\n },\n })\n\n proxyMap.set(target, proxy)\n return proxy\n}\n","export function evaluate(\n expression: string,\n context: Record<string, any> = {},\n) {\n try {\n const fn = new Function(`with(this) { return ${expression} }`)\n return fn.call(context)\n } catch (error) {\n console.error(`[jsweb/ui] Error evaluating expression: ${expression}`, error)\n return undefined\n }\n}\n\nexport function evaluateEvent(\n expression: string,\n context: Record<string, any> = {},\n $event: Event,\n) {\n try {\n const exp = expression.trim()\n const isIdentifier = /^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(exp)\n const code = `${exp} instanceof Function ? ${exp}.call(this, $event) : ${exp}`\n const result = isIdentifier ? code : exp\n\n const fn = new Function('$event', `with(this) { ${result} }`)\n fn.call(context, $event)\n } catch (error) {\n console.error(\n `[jsweb/ui] Error evaluating event expression: ${expression}`,\n error,\n )\n }\n}\n","import { effect, reactive } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\n\nexport type Context = Record<string, any>\n\ninterface BoundNode extends Node {\n _effects?: Array<() => void>\n}\n\nexport function cleanupTree(node: Node) {\n const bNode = node as BoundNode\n if (bNode._effects) {\n bNode._effects.forEach((stop) => stop())\n bNode._effects = []\n }\n const children = Array.from(node.childNodes)\n for (const child of children) {\n cleanupTree(child)\n }\n}\n\nexport function createContext(\n scope: any,\n context: Context | null = null,\n): Context {\n const reactiveScope = scope._isReactive ? scope : reactive(scope)\n\n return new Proxy(reactiveScope, {\n get(target, prop) {\n if (prop === '_isContext') return true\n if (prop in target) return Reflect.get(target, prop, target)\n if (context && prop in context) {\n return Reflect.get(context, prop, context)\n }\n return Reflect.get(target, prop, target)\n },\n set(target, prop, value) {\n if (prop in target) return Reflect.set(target, prop, value, target)\n if (context && prop in context) {\n return Reflect.set(context, prop, value, context)\n }\n return Reflect.set(target, prop, value, target)\n },\n has(target, prop) {\n if (prop in target) return true\n if (context && prop in context) return true\n return false\n },\n })\n}\n\nexport function parseNode(node: Node, context: Context) {\n if (node.nodeType !== Node.ELEMENT_NODE) return\n\n const el = node as HTMLElement\n const currentContext = processScope(el, context)\n\n const forAttr = getDirectiveValue(el, ['ui:for', ':for'])\n if (forAttr) {\n removeDirectiveAttributes(el, ['ui:for', ':for'])\n processFor(el, forAttr, currentContext)\n return\n }\n\n const ifAttr = getDirectiveValue(el, ['ui:if', ':if'])\n if (ifAttr) {\n removeDirectiveAttributes(el, ['ui:if', ':if'])\n processIf(el, ifAttr, currentContext)\n }\n\n processAttributes(el, currentContext)\n\n const children = Array.from(el.childNodes)\n for (const child of children) {\n parseNode(child, currentContext)\n }\n}\n\nexport function createComponent(\n selectorOrElement: string | HTMLElement,\n rootContext: Context = {},\n) {\n const el =\n typeof selectorOrElement === 'string'\n ? document.querySelector(selectorOrElement)\n : selectorOrElement\n\n if (el) {\n parseNode(el, rootContext)\n } else {\n console.warn('[jsweb/ui] Element not found:', selectorOrElement)\n }\n}\n\nfunction bindEffect(node: Node, fn: () => void) {\n const e = effect(fn)\n const bNode = node as BoundNode\n bNode._effects ??= []\n bNode._effects.push(e.stop)\n}\n\nfunction getDirectiveValue(el: HTMLElement, names: string[]) {\n for (const name of names) {\n const value = el.getAttribute(name)\n if (value !== null) return value\n }\n return null\n}\n\nfunction removeDirectiveAttributes(el: HTMLElement, names: string[]) {\n for (const name of names) {\n el.removeAttribute(name)\n }\n}\n\nfunction processScope(el: HTMLElement, context: Context) {\n const attrs = ['ui:scope', ':scope']\n const directive = getDirectiveValue(el, attrs)\n if (!directive) return context\n\n const scope = evaluate(directive, context) || {}\n removeDirectiveAttributes(el, attrs)\n return createContext(scope, context)\n}\n\nfunction processFor(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const match = /^\\s*(.+)\\s+(?:in|of)\\s+(.+)\\s*$/.exec(expr)\n if (!match) {\n return console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)\n }\n const [, itemName, listName] = match\n\n const keyAttr = ['ui:key', ':key']\n const keyExpr = getDirectiveValue(el, keyAttr)\n removeDirectiveAttributes(el, keyAttr)\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:for ${uuid} `)\n el.replaceWith(comment)\n\n interface RenderedNode {\n key: any\n el: HTMLElement\n scope: any\n }\n let renderedNodes: RenderedNode[] = []\n\n bindEffect(comment, () => {\n const list = evaluate(listName, context)\n\n if (!Array.isArray(list)) {\n renderedNodes.forEach((node) => {\n node.el.remove()\n cleanupTree(node.el)\n })\n renderedNodes = []\n return\n }\n\n const newNodes: RenderedNode[] = []\n const oldNodesByKey = new Map<any, RenderedNode>()\n renderedNodes.forEach((node) => oldNodesByKey.set(node.key, node))\n\n list.forEach((item, index) => {\n const scope = { [itemName]: item, $index: index }\n let key: any = index\n\n if (keyExpr) {\n const tempContext = createContext(scope, context)\n key = evaluate(keyExpr, tempContext)\n }\n\n let node = oldNodesByKey.get(key)\n if (node) {\n // Reuse node\n node.scope[itemName] = item\n node.scope.$index = index\n oldNodesByKey.delete(key)\n } else {\n // Create new node\n const clone = el.cloneNode(true) as HTMLElement\n const reactiveScope = reactive(scope)\n const localContext = createContext(reactiveScope, context)\n parseNode(clone, localContext)\n node = { key, el: clone, scope: reactiveScope }\n }\n\n newNodes.push(node)\n })\n\n // Remove un-reused nodes\n oldNodesByKey.forEach((node) => {\n node.el.remove()\n cleanupTree(node.el)\n })\n\n // Reorder and insert new DOM nodes\n let currentAnchor = comment.nextSibling\n newNodes.forEach((node) => {\n if (currentAnchor === node.el) {\n currentAnchor = currentAnchor.nextSibling\n } else {\n comment.parentNode?.insertBefore(node.el, currentAnchor)\n }\n })\n\n renderedNodes = newNodes\n })\n}\n\nfunction processIf(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:if ${uuid} `)\n el.before(comment)\n\n bindEffect(comment, () => {\n const val = evaluate(expr, context)\n if (val) {\n if (!el.parentNode) {\n comment.parentNode?.insertBefore(el, comment.nextSibling)\n }\n } else if (el.parentNode) {\n el.remove()\n }\n })\n}\n\nfunction processAttributes(el: HTMLElement, context: Context) {\n const attrs = Array.from(el.attributes)\n\n for (const attr of attrs) {\n const { name, value } = attr\n const isText = ['ui:text', ':text'].includes(name)\n const isTwoWayBind = ['ui:bind', ':bind'].includes(name)\n const isAttrBind = name.startsWith('ui:') || name.startsWith(':')\n const isEvent = name.startsWith('ui@') || name.startsWith('@')\n\n if (isText) {\n processTextBinding(el, value, context)\n } else if (isTwoWayBind) {\n processTwoWayBinding(el, value, context)\n } else if (isAttrBind) {\n const bound = name.split(':').pop()!\n processAttrBinding(el, bound, value, context)\n } else if (isEvent) {\n processEventBinding(el, name, value, context)\n }\n\n el.removeAttribute(name)\n }\n}\n\nfunction processTextBinding(el: HTMLElement, expr: string, context: Context) {\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n el.textContent = val !== undefined && val !== null ? String(val) : ''\n })\n}\n\nfunction processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {\n const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'\n const isRadio = el instanceof HTMLInputElement && el.type === 'radio'\n\n // 1. Reactive state to DOM\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n if (isCheckbox) {\n el.checked = !!val\n } else if (isRadio) {\n el.checked = el.value === String(val)\n } else {\n const target = el as\n | HTMLInputElement\n | HTMLSelectElement\n | HTMLTextAreaElement\n target.value = val == null ? '' : String(val)\n }\n })\n\n // 2. DOM to Reactive state\n const isChange = isCheckbox || isRadio || el instanceof HTMLSelectElement\n const eventName = isChange ? 'change' : 'input'\n el.addEventListener(eventName, ($event) => {\n if (isCheckbox) {\n evaluateEvent(`${expr} = $event.target.checked`, context, $event)\n } else {\n evaluateEvent(`${expr} = $event.target.value`, context, $event)\n }\n })\n}\n\nfunction processAttrBinding(\n el: HTMLElement,\n attr: string,\n expr: string,\n context: Context,\n) {\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n if (val === null || val === undefined || val === false) {\n el.removeAttribute(attr)\n } else if (val === true) {\n el.setAttribute(attr, '')\n } else {\n el.setAttribute(attr, String(val))\n }\n })\n}\n\nfunction processEventBinding(\n el: HTMLElement,\n evt: string,\n expr: string,\n context: Context,\n) {\n const refs = evt.split('@').pop()!\n const [name, ...modifiers] = refs.split('.')\n\n const isOutside = modifiers.includes('outside')\n const target = isOutside ? document : el\n\n const handler: EventListener = ($event: Event) => {\n if (!el.isConnected) return\n\n const isTargetNode = $event.target instanceof Node\n\n if (isOutside && isTargetNode && el.contains($event.target)) return\n if (modifiers.includes('self') && $event.target !== el) return\n\n if (modifiers.includes('prevent')) $event.preventDefault()\n if (modifiers.includes('stop')) $event.stopPropagation()\n\n evaluateEvent(expr, context, $event)\n }\n\n target.addEventListener(name, handler)\n\n if (isOutside) {\n const bNode = el as BoundNode\n bNode._effects ??= []\n bNode._effects.push(() => target.removeEventListener(name, handler))\n }\n}\n","import { reactive, effect, track, trigger } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\nimport { createComponent, parseNode } from './parser'\n\nexport {\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n createComponent,\n parseNode,\n}\n\nif (typeof window !== 'undefined') {\n const w = window as any\n w.jsweb = w.jsweb || {}\n w.jsweb.ui = {\n createComponent,\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n parseNode,\n }\n}\n"],"mappings":"AAAA,IAAI,EAA8B,KAC5B,iBAAY,IAAI,QAIhB,iBAAW,IAAI,QACf,iBAAY,IAAI,QAET,EAAb,MACE,QAAS,EACT,oBAAiC,IAAI,IAErC,WAAA,CAAY,GAAO,KAAA,GAAA,EAEnB,GAAA,GACE,IAAK,KAAK,OAAQ,OAAO,KAAK,KAE9B,KAAK,UAEL,EAAe,SACf,EAAU,IAAI,EAAc,MAE5B,IACE,OAAO,KAAK,aAEZ,EAAU,OAAO,GACjB,EAAe,MAInB,IAAA,GACM,KAAK,SACP,KAAK,UACL,KAAK,QAAS,GAIlB,OAAA,GACE,KAAK,KAAK,QAAS,GAAQ,EAAI,OAAO,OACtC,KAAK,KAAK,QAGZ,MAAA,GACE,MAAO,CACL,IAAA,IAAW,KAAK,MAChB,KAAA,IAAY,KAAK,UAKvB,SAAgB,EAAO,GACrB,MAAM,EAAM,IAAI,EAAe,GAE/B,OADA,EAAI,MACG,EAAI,SAGb,SAAgB,EAAM,EAAgB,GACpC,GAAI,EAAc,CAChB,IAAI,EAAU,EAAU,IAAI,GACvB,IACH,iBAAU,IAAI,IACd,EAAU,IAAI,EAAQ,IAGxB,IAAI,EAAM,EAAQ,IAAI,GACjB,IACH,iBAAM,IAAI,IACV,EAAQ,IAAI,EAAK,IAGnB,MAAM,EAAS,EAAU,IAAI,GACzB,IACF,EAAI,IAAI,GACR,EAAO,KAAK,IAAI,KAKtB,SAAgB,EAAQ,EAAgB,GACtC,MAAM,EAAU,EAAU,IAAI,GAC9B,IAAK,EAAS,OAEd,MAAM,EAAM,EAAQ,IAAI,GACpB,GAEF,IADoB,IAAI,GAChB,QAAS,GAAW,EAAO,OAIvC,SAAgB,EAA2B,GAEzC,GADoC,iBAAX,GAAkC,OAAX,EACjC,OAAO,EAGtB,GADmB,OAAO,OAAO,EAAQ,eACzB,OAAO,EAEvB,MAAM,EAAgB,EAAS,IAAI,GACnC,GAAI,EAAe,OAAO,EAE1B,MAAM,EAAQ,IAAI,MAAM,EAAQ,CAC9B,GAAA,CAAI,EAAK,EAAK,GACZ,GAAY,gBAAR,EAAuB,OAAO,EAClC,EAAM,EAAK,GACX,MAAM,EAAM,QAAQ,IAAI,EAAK,EAAK,GAElC,MAAmB,iBAAR,GAA4B,OAAR,EACtB,EAAS,GAEX,GAET,GAAA,CAAI,EAAK,EAAK,EAAO,GACnB,MAAM,EAAU,MAAM,QAAQ,GACxB,EAAW,QAAQ,IAAI,EAAK,EAAK,GACjC,EACJ,GAAW,OAAO,OAAO,MAAU,EAC/B,OAAO,GAAO,EAAI,OAClB,OAAO,OAAO,EAAK,GAEnB,EAAS,QAAQ,IAAI,EAAK,EAAK,EAAO,GAW5C,OATK,EAKM,IAAa,GACtB,EAAQ,EAAK,IALb,EAAQ,EAAK,GACT,GAAmB,WAAR,GACb,EAAQ,EAAK,WAMV,KAKX,OADA,EAAS,IAAI,EAAQ,GACd,ECtIT,SAAgB,EACd,EACA,EAA+B,CAAA,GAE/B,IAEE,OAAO,IADQ,SAAS,uBAAuB,OACrC,KAAK,SACR,GAEP,YADA,QAAQ,MAAM,2CAA2C,IAAc,IAK3E,SAAgB,EACd,EACA,EAA+B,CAAA,EAC/B,GAEA,IACE,MAAM,EAAM,EAAW,OACjB,EAAe,8BAA8B,KAAK,GAKxD,IADe,SAAS,SAAU,gBAFnB,EADF,GAAG,2BAA6B,0BAA4B,IACpC,OAGlC,KAAK,EAAS,SACV,GACP,QAAQ,MACN,iDAAiD,IACjD,ICpBN,SAAgB,EAAY,GAC1B,MAAM,EAAQ,EACV,EAAM,WACR,EAAM,SAAS,QAAS,GAAS,KACjC,EAAM,SAAW,IAEnB,MAAM,EAAW,MAAM,KAAK,EAAK,YACjC,IAAK,MAAM,KAAS,EAClB,EAAY,GAIhB,SAAgB,EACd,EACA,EAA0B,MAE1B,MAAM,EAAgB,EAAM,YAAc,EAAQ,EAAS,GAE3D,OAAO,IAAI,MAAM,EAAe,CAC9B,IAAA,CAAI,EAAQ,IACG,eAAT,IACA,KAAQ,EAAe,QAAQ,IAAI,EAAQ,EAAM,GACjD,GAAW,KAAQ,EACd,QAAQ,IAAI,EAAS,EAAM,GAE7B,QAAQ,IAAI,EAAQ,EAAM,IAEnC,IAAA,CAAI,EAAQ,EAAM,IACZ,KAAQ,EAAe,QAAQ,IAAI,EAAQ,EAAM,EAAO,GACxD,GAAW,KAAQ,EACd,QAAQ,IAAI,EAAS,EAAM,EAAO,GAEpC,QAAQ,IAAI,EAAQ,EAAM,EAAO,GAE1C,IAAA,CAAI,EAAQ,IACN,KAAQ,MACR,KAAW,KAAQ,MAM7B,SAAgB,EAAU,EAAY,GACpC,GAAI,EAAK,WAAa,KAAK,aAAc,OAEzC,MAAM,EAAK,EACL,EA4DR,SAAsB,EAAiB,GACrC,MAAM,EAAQ,CAAC,WAAY,UACrB,EAAY,EAAkB,EAAI,GACxC,IAAK,EAAW,OAAO,EAEvB,MAAM,EAAQ,EAAS,EAAW,IAAY,CAAA,EAE9C,OADA,EAA0B,EAAI,GACvB,EAAc,EAAO,GAnEL,CAAa,EAAI,GAElC,EAAU,EAAkB,EAAI,CAAC,SAAU,SACjD,GAAI,EAGF,OAFA,EAA0B,EAAI,CAAC,SAAU,cAkE7C,SAAoB,EAAiB,EAAc,GAEjD,IADe,EAAG,WACL,OAEb,MAAM,EAAQ,kCAAkC,KAAK,GACrD,IAAK,EACH,OAAO,QAAQ,KAAK,yCAAyC,KAE/D,MAAM,CAAG,EAAU,GAAY,EAEzB,EAAU,CAAC,SAAU,QACrB,EAAU,EAAkB,EAAI,GACtC,EAA0B,EAAI,GAE9B,MAAM,EAAO,OAAO,aACd,EAAU,SAAS,cAAc,WAAW,MAClD,EAAG,YAAY,GAOf,IAAI,EAAgC,GAEpC,EAAW,EAAA,KACT,MAAM,EAAO,EAAS,EAAU,GAEhC,IAAK,MAAM,QAAQ,GAMjB,OALA,EAAc,QAAS,IACrB,EAAK,GAAG,SACR,EAAY,EAAK,WAEnB,EAAgB,IAIlB,MAAM,EAA2B,GAC3B,iBAAgB,IAAI,IAC1B,EAAc,QAAS,GAAS,EAAc,IAAI,EAAK,IAAK,IAE5D,EAAK,QAAA,CAAS,EAAM,KAClB,MAAM,EAAQ,EAAG,GAAW,EAAM,OAAQ,GAC1C,IAAI,EAAW,EAEX,IAEF,EAAM,EAAS,EADK,EAAc,EAAO,KAI3C,IAAI,EAAO,EAAc,IAAI,GAC7B,GAAI,EAEF,EAAK,MAAM,GAAY,EACvB,EAAK,MAAM,OAAS,EACpB,EAAc,OAAO,OAChB,CAEL,MAAM,EAAQ,EAAG,WAAU,GACrB,EAAgB,EAAS,GAE/B,EAAU,EADW,EAAc,EAAe,IAElD,EAAO,CAAE,MAAK,GAAI,EAAO,MAAO,GAGlC,EAAS,KAAK,KAIhB,EAAc,QAAS,IACrB,EAAK,GAAG,SACR,EAAY,EAAK,MAInB,IAAI,EAAgB,EAAQ,YAC5B,EAAS,QAAS,IACZ,IAAkB,EAAK,GACzB,EAAgB,EAAc,YAE9B,EAAQ,YAAY,aAAa,EAAK,GAAI,KAI9C,EAAgB,IArJhB,CAAW,EAAI,EAAS,GAI1B,MAAM,EAAS,EAAkB,EAAI,CAAC,QAAS,QAC3C,IACF,EAA0B,EAAI,CAAC,QAAS,QAmJ5C,SAAmB,EAAiB,EAAc,GAEhD,IADe,EAAG,WACL,OAEb,MAAM,EAAO,OAAO,aACd,EAAU,SAAS,cAAc,UAAU,MACjD,EAAG,OAAO,GAEV,EAAW,EAAA,KACG,EAAS,EAAM,GAEpB,EAAG,YACN,EAAQ,YAAY,aAAa,EAAI,EAAQ,aAEtC,EAAG,YACZ,EAAG,WAjKL,CAAU,EAAI,EAAQ,IAsK1B,SAA2B,EAAiB,GAC1C,MAAM,EAAQ,MAAM,KAAK,EAAG,YAE5B,IAAK,MAAM,KAAQ,EAAO,CACxB,MAAM,KAAE,EAAA,MAAM,GAAU,EAClB,EAAS,CAAC,UAAW,SAAS,SAAS,GACvC,EAAe,CAAC,UAAW,SAAS,SAAS,GAC7C,EAAa,EAAK,WAAW,QAAU,EAAK,WAAW,KACvD,EAAU,EAAK,WAAW,QAAU,EAAK,WAAW,KAEtD,EACF,EAAmB,EAAI,EAAO,GACrB,EACT,EAAqB,EAAI,EAAO,GACvB,EAET,EAAmB,EADL,EAAK,MAAM,KAAK,MACA,EAAO,GAC5B,GACT,EAAoB,EAAI,EAAM,EAAO,GAGvC,EAAG,gBAAgB,IAxLrB,CAAkB,EAAI,GAEtB,MAAM,EAAW,MAAM,KAAK,EAAG,YAC/B,IAAK,MAAM,KAAS,EAClB,EAAU,EAAO,GAIrB,SAAgB,EACd,EACA,EAAuB,CAAA,GAEvB,MAAM,EACyB,iBAAtB,EACH,SAAS,cAAc,GACvB,EAEF,EACF,EAAU,EAAI,GAEd,QAAQ,KAAK,gCAAiC,GAIlD,SAAS,EAAW,EAAY,GAC9B,MAAM,EAAI,EAAO,GACX,EAAQ,EACd,EAAM,WAAa,GACnB,EAAM,SAAS,KAAK,EAAE,MAGxB,SAAS,EAAkB,EAAiB,GAC1C,IAAK,MAAM,KAAQ,EAAO,CACxB,MAAM,EAAQ,EAAG,aAAa,GAC9B,GAAc,OAAV,EAAgB,OAAO,EAE7B,OAAO,KAGT,SAAS,EAA0B,EAAiB,GAClD,IAAK,MAAM,KAAQ,EACjB,EAAG,gBAAgB,GAmJvB,SAAS,EAAmB,EAAiB,EAAc,GACzD,EAAW,EAAA,KACT,MAAM,EAAM,EAAS,EAAM,GAC3B,EAAG,YAAc,QAAoC,OAAO,GAAO,KAIvE,SAAS,EAAqB,EAAiB,EAAc,GAC3D,MAAM,EAAa,aAAc,kBAAgC,aAAZ,EAAG,KAClD,EAAU,aAAc,kBAAgC,UAAZ,EAAG,KAGrD,EAAW,EAAA,KACT,MAAM,EAAM,EAAS,EAAM,GAC3B,GAAI,EACF,EAAG,UAAY,UACN,EACT,EAAG,QAAU,EAAG,QAAU,OAAO,OAC5B,CACU,EAIR,MAAe,MAAP,EAAc,GAAK,OAAO,MAM7C,MAAM,EADW,GAAc,GAAW,aAAc,kBAC3B,SAAW,QACxC,EAAG,iBAAiB,EAAY,IAE5B,EADE,EACY,GAAG,4BAEH,GAAG,0BAFgC,EAAS,KAOhE,SAAS,EACP,EACA,EACA,EACA,GAEA,EAAW,EAAA,KACT,MAAM,EAAM,EAAS,EAAM,GACvB,UAA6C,IAAR,EACvC,EAAG,gBAAgB,IACF,IAAR,EACT,EAAG,aAAa,EAAM,IAEtB,EAAG,aAAa,EAAM,OAAO,MAKnC,SAAS,EACP,EACA,EACA,EACA,GAGA,MAAO,KAAS,GADH,EAAI,MAAM,KAAK,MACM,MAAM,KAElC,EAAY,EAAU,SAAS,WAC/B,EAAS,EAAY,SAAW,EAEhC,EAA0B,IAC9B,IAAK,EAAG,YAAa,OAErB,MAAM,EAAe,EAAO,kBAAkB,KAE1C,GAAa,GAAgB,EAAG,SAAS,EAAO,SAChD,EAAU,SAAS,SAAW,EAAO,SAAW,IAEhD,EAAU,SAAS,YAAY,EAAO,iBACtC,EAAU,SAAS,SAAS,EAAO,kBAEvC,EAAc,EAAM,EAAS,KAK/B,GAFA,EAAO,iBAAiB,EAAM,GAE1B,EAAW,CACb,MAAM,EAAQ,EACd,EAAM,WAAa,GACnB,EAAM,SAAS,KAAA,IAAW,EAAO,oBAAoB,EAAM,KC3U/D,GAAsB,oBAAX,OAAwB,CACjC,MAAM,EAAI,OACV,EAAE,MAAQ,EAAE,OAAS,CAAA,EACrB,EAAE,MAAM,GAAK,CACX,kBACA,WACA,SACA,QACA,UACA,WACA,gBACA"}
|
package/dist/ui.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self)["@jsweb/ui"]={})}(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var t=null,n=new WeakMap;
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self)["@jsweb/ui"]={})}(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var t=null,n=new WeakMap,o=new WeakMap,r=new WeakMap,i=class{active=!0;deps=new Set;constructor(e){this.fn=e}run(){if(!this.active)return this.fn();this.cleanup(),t=Symbol(),r.set(t,this);try{return this.fn()}finally{r.delete(t),t=null}}stop(){this.active&&(this.cleanup(),this.active=!1)}cleanup(){this.deps.forEach(e=>e.delete(this)),this.deps.clear()}effect(){return{run:()=>this.run(),stop:()=>this.stop()}}};function s(e){const t=new i(e);return t.run(),t.effect()}function c(e,o){if(t){let i=n.get(e);i||(i=new Map,n.set(e,i));let s=i.get(o);s||(s=new Set,i.set(o,s));const c=r.get(t);c&&(s.add(c),c.deps.add(s))}}function f(e,t){const o=n.get(e);if(!o)return;const r=o.get(t);r&&new Set(r).forEach(e=>e.run())}function u(e){if("object"!=typeof e||null===e)return e;if(Object.hasOwn(e,"_isReactive"))return e;const t=o.get(e);if(t)return t;const n=new Proxy(e,{get(e,t,n){if("_isReactive"===t)return!0;c(e,t);const o=Reflect.get(e,t,n);return"object"==typeof o&&null!==o?u(o):o},set(e,t,n,o){const r=Array.isArray(e),i=Reflect.get(e,t,o),s=r&&String(Number(t))===t?Number(t)<e.length:Object.hasOwn(e,t),c=Reflect.set(e,t,n,o);return s?i!==n&&f(e,t):(f(e,t),r&&"length"!==t&&f(e,"length")),c}});return o.set(e,n),n}function a(e,t={}){try{return new Function(`with(this) { return ${e} }`).call(t)}catch(n){return void console.error(`[jsweb/ui] Error evaluating expression: ${e}`,n)}}function l(e,t={},n){try{const o=e.trim(),r=/^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(o);new Function("$event",`with(this) { ${r?`${o} instanceof Function ? ${o}.call(this, $event) : ${o}`:o} }`).call(t,n)}catch(o){console.error(`[jsweb/ui] Error evaluating event expression: ${e}`,o)}}function d(e){const t=e;t._effects&&(t._effects.forEach(e=>e()),t._effects=[]);const n=Array.from(e.childNodes);for(const o of n)d(o)}function p(e,t=null){const n=e._isReactive?e:u(e);return new Proxy(n,{get:(e,n)=>"_isContext"===n||(n in e?Reflect.get(e,n,e):t&&n in t?Reflect.get(t,n,t):Reflect.get(e,n,e)),set:(e,n,o)=>n in e?Reflect.set(e,n,o,e):t&&n in t?Reflect.set(t,n,o,t):Reflect.set(e,n,o,e),has:(e,n)=>n in e||!(!t||!(n in t))})}function h(e,t){if(e.nodeType!==Node.ELEMENT_NODE)return;const n=e,o=function(e,t){const n=["ui:scope",":scope"],o=b(e,n);if(!o)return t;const r=a(o,t)||{};return m(e,n),p(r,t)}(n,t),r=b(n,["ui:for",":for"]);if(r)return m(n,["ui:for",":for"]),void function(e,t,n){if(!e.parentNode)return;const o=/^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(t);if(!o)return console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,r,i]=o,s=["ui:key",":key"],c=b(e,s);m(e,s);const f=crypto.randomUUID(),l=document.createComment(` ui:for ${f} `);e.replaceWith(l);let g=[];v(l,()=>{const t=a(i,n);if(!Array.isArray(t))return g.forEach(e=>{e.el.remove(),d(e.el)}),void(g=[]);const o=[],s=new Map;g.forEach(e=>s.set(e.key,e)),t.forEach((t,i)=>{const f={[r]:t,$index:i};let l=i;c&&(l=a(c,p(f,n)));let d=s.get(l);if(d)d.scope[r]=t,d.scope.$index=i,s.delete(l);else{const t=e.cloneNode(!0),o=u(f);h(t,p(o,n)),d={key:l,el:t,scope:o}}o.push(d)}),s.forEach(e=>{e.el.remove(),d(e.el)});let f=l.nextSibling;o.forEach(e=>{f===e.el?f=f.nextSibling:l.parentNode?.insertBefore(e.el,f)}),g=o})}(n,r,o);const i=b(n,["ui:if",":if"]);i&&(m(n,["ui:if",":if"]),function(e,t,n){if(!e.parentNode)return;const o=crypto.randomUUID(),r=document.createComment(` ui:if ${o} `);e.before(r),v(r,()=>{a(t,n)?e.parentNode||r.parentNode?.insertBefore(e,r.nextSibling):e.parentNode&&e.remove()})}(n,i,o)),function(e,t){const n=Array.from(e.attributes);for(const o of n){const{name:n,value:r}=o,i=["ui:text",":text"].includes(n),s=["ui:bind",":bind"].includes(n),c=n.startsWith("ui:")||n.startsWith(":"),f=n.startsWith("ui@")||n.startsWith("@");i?y(e,r,t):s?w(e,r,t):c?E(e,n.split(":").pop(),r,t):f&&$(e,n,r,t),e.removeAttribute(n)}}(n,o);const s=Array.from(n.childNodes);for(const c of s)h(c,o)}function g(e,t={}){const n="string"==typeof e?document.querySelector(e):e;n?h(n,t):console.warn("[jsweb/ui] Element not found:",e)}function v(e,t){const n=s(t),o=e;o._effects??=[],o._effects.push(n.stop)}function b(e,t){for(const n of t){const t=e.getAttribute(n);if(null!==t)return t}return null}function m(e,t){for(const n of t)e.removeAttribute(n)}function y(e,t,n){v(e,()=>{const o=a(t,n);e.textContent=null!=o?String(o):""})}function w(e,t,n){const o=e instanceof HTMLInputElement&&"checkbox"===e.type,r=e instanceof HTMLInputElement&&"radio"===e.type;v(e,()=>{const i=a(t,n);if(o)e.checked=!!i;else if(r)e.checked=e.value===String(i);else{e.value=null==i?"":String(i)}});const i=o||r||e instanceof HTMLSelectElement?"change":"input";e.addEventListener(i,e=>{l(o?`${t} = $event.target.checked`:`${t} = $event.target.value`,n,e)})}function E(e,t,n,o){v(e,()=>{const r=a(n,o);null==r||!1===r?e.removeAttribute(t):!0===r?e.setAttribute(t,""):e.setAttribute(t,String(r))})}function $(e,t,n,o){const[r,...i]=t.split("@").pop().split("."),s=i.includes("outside"),c=s?document:e,f=t=>{if(!e.isConnected)return;const r=t.target instanceof Node;s&&r&&e.contains(t.target)||i.includes("self")&&t.target!==e||(i.includes("prevent")&&t.preventDefault(),i.includes("stop")&&t.stopPropagation(),l(n,o,t))};if(c.addEventListener(r,f),s){const t=e;t._effects??=[],t._effects.push(()=>c.removeEventListener(r,f))}}if("undefined"!=typeof window){const e=window;e.jsweb=e.jsweb||{},e.jsweb.ui={createComponent:g,reactive:u,effect:s,track:c,trigger:f,evaluate:a,evaluateEvent:l,parseNode:h}}e.createComponent=g,e.effect=s,e.evaluate=a,e.evaluateEvent=l,e.parseNode=h,e.reactive=u,e.track=c,e.trigger=f});
|
|
2
2
|
//# sourceMappingURL=ui.umd.js.map
|
package/dist/ui.umd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui.umd.js","names":[],"sources":["../src/reactivity.ts","../src/evaluator.ts","../src/parser.ts","../src/index.ts"],"sourcesContent":["let activeEffect: (() => void) | null = null\nconst targetMap = new WeakMap<object, Map<string | symbol, Set<() => void>>>()\n\nexport function effect(fn: () => void) {\n const effectFn = () => {\n // cleanup old deps could be added here\n activeEffect = effectFn\n fn()\n activeEffect = null\n }\n effectFn()\n}\n\nexport function track(target: object, key: string | symbol) {\n if (activeEffect) {\n let depsMap = targetMap.get(target)\n if (!depsMap) {\n depsMap = new Map()\n targetMap.set(target, depsMap)\n }\n let dep = depsMap.get(key)\n if (!dep) {\n dep = new Set()\n depsMap.set(key, dep)\n }\n dep.add(activeEffect)\n }\n}\n\nexport function trigger(target: object, key: string | symbol) {\n const depsMap = targetMap.get(target)\n if (!depsMap) return\n const dep = depsMap.get(key)\n if (dep) {\n dep.forEach((effectFn) => effectFn())\n }\n}\n\nexport function reactive<T extends object>(target: T): T {\n if (typeof target !== 'object' || target === null) return target\n if ((target as any).__isReactive) return target\n\n return new Proxy(target, {\n get(obj, key, receiver) {\n if (key === '__isReactive') return true\n track(obj, key)\n const res = Reflect.get(obj, key, receiver)\n // deep reactivity\n if (typeof res === 'object' && res !== null) {\n return reactive(res)\n }\n return res\n },\n set(obj, key, value, receiver) {\n const isArray = Array.isArray(obj)\n const oldValue = Reflect.get(obj, key, receiver)\n const hadKey = isArray && String(Number(key)) === key \n ? Number(key) < obj.length \n : Object.prototype.hasOwnProperty.call(obj, key)\n\n const result = Reflect.set(obj, key, value, receiver)\n\n if (!hadKey) {\n trigger(obj, key)\n if (isArray && key !== 'length') {\n trigger(obj, 'length')\n }\n } else if (oldValue !== value) {\n trigger(obj, key)\n }\n \n return result\n },\n })\n}\n","export function evaluate(\n expression: string,\n context: Record<string, any> = {},\n) {\n try {\n const fn = new Function(`with(this) { return ${expression} }`)\n return fn.call(context)\n } catch (error) {\n console.error(`[jsweb/ui] Error evaluating expression: ${expression}`, error)\n return undefined\n }\n}\n\nexport function evaluateEvent(\n expression: string,\n context: Record<string, any> = {},\n $event: Event,\n) {\n try {\n const exp = expression.trim()\n const isIdentifier = /^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(exp)\n const code = `${exp} instanceof Function ? ${exp}.call(this, $event) : ${exp}`\n const result = isIdentifier ? code : exp\n\n const fn = new Function('$event', `with(this) { ${result} }`)\n fn.call(context, $event)\n } catch (error) {\n console.error(\n `[jsweb/ui] Error evaluating event expression: ${expression}`,\n error,\n )\n }\n}\n","import { effect, reactive } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\n\nexport type Context = Record<string, any>\n\nexport function createContext(scopeData: any, parentContext: Context | null = null): Context {\n const reactiveScope = scopeData.__isReactive ? scopeData : reactive(scopeData)\n \n return new Proxy(reactiveScope, {\n get(target, prop) {\n if (prop === '__isContext') return true\n if (prop in target) return Reflect.get(target, prop, target)\n if (parentContext && prop in parentContext) {\n return Reflect.get(parentContext, prop, parentContext)\n }\n return Reflect.get(target, prop, target)\n },\n set(target, prop, value) {\n if (prop in target) return Reflect.set(target, prop, value, target)\n if (parentContext && prop in parentContext) {\n return Reflect.set(parentContext, prop, value, parentContext)\n }\n return Reflect.set(target, prop, value, target)\n },\n has(target, prop) {\n if (prop in target) return true\n if (parentContext && prop in parentContext) return true\n return false\n }\n })\n}\n\nexport function parseNode(node: Node, context: Context) {\n if (node.nodeType === Node.ELEMENT_NODE) {\n const el = node as HTMLElement\n\n // 1. Check for scope\n let currentContext = context\n const scopeAttr = el.getAttribute('ui:scope') || el.getAttribute(':scope')\n if (scopeAttr) {\n const scopeData = evaluate(scopeAttr, context) || {}\n currentContext = createContext(scopeData, context)\n el.removeAttribute('ui:scope')\n el.removeAttribute(':scope')\n }\n\n // 2. Check for ui:for (must be processed before children and other directives on same element)\n const forAttr = el.getAttribute('ui:for') || el.getAttribute(':for')\n if (forAttr) {\n el.removeAttribute('ui:for')\n el.removeAttribute(':for')\n processFor(el, forAttr, currentContext)\n return // Stop processing this node further, processFor handles clones\n }\n\n // 3. Check for ui:if\n const ifAttr = el.getAttribute('ui:if') || el.getAttribute(':if')\n if (ifAttr) {\n el.removeAttribute('ui:if')\n el.removeAttribute(':if')\n processIf(el, ifAttr, currentContext)\n // We continue processing children because the element might be shown\n }\n\n // 4. Other directives\n const attrs = Array.from(el.attributes)\n for (const attr of attrs) {\n const { name, value } = attr\n const isText = ['ui:text', ':text'].includes(name)\n const isTwoWayBind = ['ui:bind', ':bind'].includes(name)\n const isAttrBind = name.startsWith('ui:') || name.startsWith(':') \n const isEvent = name.startsWith('ui@') || name.startsWith('@')\n\n if (isText) {\n el.removeAttribute(name)\n effect(() => {\n const val = evaluate(value, currentContext)\n el.textContent = val !== undefined && val !== null ? String(val) : ''\n })\n } else if (isTwoWayBind) {\n el.removeAttribute(name)\n processTwoWayBinding(el, value, currentContext)\n } else if (isAttrBind) {\n const boundAttr = name.split(':').pop()! \n el.removeAttribute(name)\n effect(() => {\n const val = evaluate(value, currentContext)\n if (val === null || val === undefined || val === false) {\n el.removeAttribute(boundAttr)\n } else if (val === true) {\n el.setAttribute(boundAttr, '')\n } else {\n el.setAttribute(boundAttr, String(val))\n }\n })\n } else if (isEvent) {\n const eventName = name.split('@').pop()!\n el.removeAttribute(name)\n el.addEventListener(eventName, ($event) => {\n evaluateEvent(value, currentContext, $event)\n })\n }\n }\n\n // Process children\n // Need to convert to array because childNodes might mutate if elements are added/removed\n const children = Array.from(el.childNodes)\n for (const child of children) {\n parseNode(child, currentContext)\n }\n }\n}\n\nfunction processIf(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:if ${uuid} `)\n parent.insertBefore(comment, el)\n\n effect(() => {\n const val = evaluate(expr, context)\n if (val) {\n if (!el.parentNode) {\n comment.parentNode?.insertBefore(el, comment.nextSibling)\n }\n } else {\n if (el.parentNode) {\n el.parentNode.removeChild(el)\n }\n }\n })\n}\n\nfunction processFor(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n \n const match = expr.match(/^\\s*(.+)\\s+(?:in|of)\\s+(.+)\\s*$/)\n if (!match) {\n console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)\n return\n }\n const [, itemName, listName] = match\n \n const keyExpr = el.getAttribute('ui:key') || el.getAttribute(':key')\n el.removeAttribute('ui:key')\n el.removeAttribute(':key')\n \n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:for ${uuid} `)\n parent.replaceChild(comment, el)\n \n type RenderedNode = {\n key: any\n el: HTMLElement\n scope: any\n }\n let renderedNodes: RenderedNode[] = []\n\n effect(() => {\n const list = evaluate(listName, context)\n\n if (!Array.isArray(list)) {\n renderedNodes.forEach(node => node.el.parentNode?.removeChild(node.el))\n renderedNodes = []\n return\n }\n\n const newNodes: RenderedNode[] = []\n const oldNodesByKey = new Map<any, RenderedNode>()\n renderedNodes.forEach(node => oldNodesByKey.set(node.key, node))\n\n list.forEach((item, index) => {\n const scope = { [itemName]: item, $index: index }\n let key: any = index\n\n if (keyExpr) {\n const tempContext = createContext(scope, context)\n key = evaluate(keyExpr, tempContext)\n }\n\n let node = oldNodesByKey.get(key)\n if (node) {\n // Reuse node\n node.scope[itemName] = item\n node.scope.$index = index\n oldNodesByKey.delete(key)\n } else {\n // Create new node\n const clone = el.cloneNode(true) as HTMLElement\n const reactiveScope = reactive(scope)\n const localContext = createContext(reactiveScope, context)\n parseNode(clone, localContext)\n node = { key, el: clone, scope: reactiveScope }\n }\n \n newNodes.push(node)\n })\n\n // Remove un-reused nodes\n oldNodesByKey.forEach(node => {\n node.el.parentNode?.removeChild(node.el)\n })\n\n // Reorder and insert new DOM nodes\n let currentAnchor = comment.nextSibling\n newNodes.forEach((node) => {\n if (currentAnchor === node.el) {\n currentAnchor = currentAnchor.nextSibling\n } else {\n comment.parentNode?.insertBefore(node.el, currentAnchor)\n }\n })\n\n renderedNodes = newNodes\n })\n}\n\nexport function createComponent(\n selectorOrElement: string | HTMLElement,\n rootContext: Context = {},\n) {\n const el =\n typeof selectorOrElement === 'string'\n ? document.querySelector(selectorOrElement)\n : selectorOrElement\n\n if (el) {\n parseNode(el, rootContext)\n } else {\n console.warn(`[jsweb/ui] Element not found: ${selectorOrElement}`)\n }\n}\n\nfunction processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {\n const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'\n const isRadio = el instanceof HTMLInputElement && el.type === 'radio'\n \n // 1. Reactive state to DOM\n effect(() => {\n const val = evaluate(expr, context)\n if (isCheckbox) {\n const target = el as HTMLInputElement\n target.checked = !!val\n } else if (isRadio) {\n const target = el as HTMLInputElement\n target.checked = target.value === String(val)\n } else {\n const target = el as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement\n target.value = val == null ? '' : String(val)\n }\n })\n\n // 2. DOM to Reactive state\n const isChange = isCheckbox || isRadio || el instanceof HTMLSelectElement\n const eventName = isChange ? 'change' : 'input'\n el.addEventListener(eventName, ($event) => {\n if (isCheckbox) {\n evaluateEvent(`${expr} = $event.target.checked`, context, $event)\n } else {\n evaluateEvent(`${expr} = $event.target.value`, context, $event)\n }\n })\n}\n","import { reactive, effect, track, trigger } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\nimport { createComponent, parseNode } from './parser'\n\nexport {\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n createComponent,\n parseNode,\n}\n\nif (typeof window !== 'undefined') {\n const w = window as any\n w.jsweb = w.jsweb || {}\n w.jsweb.ui = {\n createComponent,\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n parseNode,\n }\n}\n"],"mappings":"mSAAA,IAAI,EAAoC,KAClC,EAAY,IAAI,QAEtB,SAAgB,EAAO,GACrB,MAAM,EAAA,KAEJ,EAAe,EACf,IACA,EAAe,MAEjB,IAGF,SAAgB,EAAM,EAAgB,GACpC,GAAI,EAAc,CAChB,IAAI,EAAU,EAAU,IAAI,GACvB,IACH,EAAU,IAAI,IACd,EAAU,IAAI,EAAQ,IAExB,IAAI,EAAM,EAAQ,IAAI,GACjB,IACH,EAAM,IAAI,IACV,EAAQ,IAAI,EAAK,IAEnB,EAAI,IAAI,IAIZ,SAAgB,EAAQ,EAAgB,GACtC,MAAM,EAAU,EAAU,IAAI,GAC9B,IAAK,EAAS,OACd,MAAM,EAAM,EAAQ,IAAI,GACpB,GACF,EAAI,QAAS,GAAa,KAI9B,SAAgB,EAA2B,GACzC,MAAsB,iBAAX,GAAkC,OAAX,GAC7B,EAAe,aADsC,EAGnD,IAAI,MAAM,EAAQ,CACvB,GAAA,CAAI,EAAK,EAAK,GACZ,GAAY,iBAAR,EAAwB,OAAO,EACnC,EAAM,EAAK,GACX,MAAM,EAAM,QAAQ,IAAI,EAAK,EAAK,GAElC,MAAmB,iBAAR,GAA4B,OAAR,EACtB,EAAS,GAEX,GAET,GAAA,CAAI,EAAK,EAAK,EAAO,GACnB,MAAM,EAAU,MAAM,QAAQ,GACxB,EAAW,QAAQ,IAAI,EAAK,EAAK,GACjC,EAAS,GAAW,OAAO,OAAO,MAAU,EAC9C,OAAO,GAAO,EAAI,OAClB,OAAO,UAAU,eAAe,KAAK,EAAK,GAExC,EAAS,QAAQ,IAAI,EAAK,EAAK,EAAO,GAW5C,OATK,EAKM,IAAa,GACtB,EAAQ,EAAK,IALb,EAAQ,EAAK,GACT,GAAmB,WAAR,GACb,EAAQ,EAAK,WAMV,KCvEb,SAAgB,EACd,EACA,EAA+B,CAAA,GAE/B,IAEE,OAAO,IADQ,SAAS,uBAAuB,OACrC,KAAK,SACR,GAEP,YADA,QAAQ,MAAM,2CAA2C,IAAc,IAK3E,SAAgB,EACd,EACA,EAA+B,CAAA,EAC/B,GAEA,IACE,MAAM,EAAM,EAAW,OACjB,EAAe,8BAA8B,KAAK,GAKxD,IADe,SAAS,SAAU,gBAFnB,EADF,GAAG,2BAA6B,0BAA4B,IACpC,OAGlC,KAAK,EAAS,SACV,GACP,QAAQ,MACN,iDAAiD,IACjD,ICxBN,SAAgB,EAAc,EAAgB,EAAgC,MAC5E,MAAM,EAAgB,EAAU,aAAe,EAAY,EAAS,GAEpE,OAAO,IAAI,MAAM,EAAe,CAC9B,IAAA,CAAI,EAAQ,IACG,gBAAT,IACA,KAAQ,EAAe,QAAQ,IAAI,EAAQ,EAAM,GACjD,GAAiB,KAAQ,EACpB,QAAQ,IAAI,EAAe,EAAM,GAEnC,QAAQ,IAAI,EAAQ,EAAM,IAEnC,IAAA,CAAI,EAAQ,EAAM,IACZ,KAAQ,EAAe,QAAQ,IAAI,EAAQ,EAAM,EAAO,GACxD,GAAiB,KAAQ,EACpB,QAAQ,IAAI,EAAe,EAAM,EAAO,GAE1C,QAAQ,IAAI,EAAQ,EAAM,EAAO,GAE1C,IAAA,CAAI,EAAQ,IACN,KAAQ,MACR,KAAiB,KAAQ,MAMnC,SAAgB,EAAU,EAAY,GACpC,GAAI,EAAK,WAAa,KAAK,aAAc,CACvC,MAAM,EAAK,EAGX,IAAI,EAAiB,EACrB,MAAM,EAAY,EAAG,aAAa,aAAe,EAAG,aAAa,UAC7D,IAEF,EAAiB,EADC,EAAS,EAAW,IAAY,CAAA,EACR,GAC1C,EAAG,gBAAgB,YACnB,EAAG,gBAAgB,WAIrB,MAAM,EAAU,EAAG,aAAa,WAAa,EAAG,aAAa,QAC7D,GAAI,EAIF,OAHA,EAAG,gBAAgB,UACnB,EAAG,gBAAgB,aAqFzB,SAAoB,EAAiB,EAAc,GACjD,MAAM,EAAS,EAAG,WAClB,IAAK,EAAQ,OAEb,MAAM,EAAQ,EAAK,MAAM,mCACzB,IAAK,EAEH,YADA,QAAQ,KAAK,yCAAyC,KAGxD,MAAM,CAAG,EAAU,GAAY,EAEzB,EAAU,EAAG,aAAa,WAAa,EAAG,aAAa,QAC7D,EAAG,gBAAgB,UACnB,EAAG,gBAAgB,QAEnB,MAAM,EAAO,OAAO,aACd,EAAU,SAAS,cAAc,WAAW,MAClD,EAAO,aAAa,EAAS,GAO7B,IAAI,EAAgC,GAEpC,EAAA,KACE,MAAM,EAAO,EAAS,EAAU,GAEhC,IAAK,MAAM,QAAQ,GAGjB,OAFA,EAAc,QAAQ,GAAQ,EAAK,GAAG,YAAY,YAAY,EAAK,UACnE,EAAgB,IAIlB,MAAM,EAA2B,GAC3B,EAAgB,IAAI,IAC1B,EAAc,QAAQ,GAAQ,EAAc,IAAI,EAAK,IAAK,IAE1D,EAAK,QAAA,CAAS,EAAM,KAClB,MAAM,EAAQ,EAAG,GAAW,EAAM,OAAQ,GAC1C,IAAI,EAAW,EAEX,IAEF,EAAM,EAAS,EADK,EAAc,EAAO,KAI3C,IAAI,EAAO,EAAc,IAAI,GAC7B,GAAI,EAEF,EAAK,MAAM,GAAY,EACvB,EAAK,MAAM,OAAS,EACpB,EAAc,OAAO,OAChB,CAEL,MAAM,EAAQ,EAAG,WAAU,GACrB,EAAgB,EAAS,GAE/B,EAAU,EADW,EAAc,EAAe,IAElD,EAAO,CAAE,MAAK,GAAI,EAAO,MAAO,GAGlC,EAAS,KAAK,KAIhB,EAAc,QAAQ,IACpB,EAAK,GAAG,YAAY,YAAY,EAAK,MAIvC,IAAI,EAAgB,EAAQ,YAC5B,EAAS,QAAS,IACZ,IAAkB,EAAK,GACzB,EAAgB,EAAc,YAE9B,EAAQ,YAAY,aAAa,EAAK,GAAI,KAI9C,EAAgB,IArKd,CAAW,EAAI,EAAS,GAK1B,MAAM,EAAS,EAAG,aAAa,UAAY,EAAG,aAAa,OACvD,IACF,EAAG,gBAAgB,SACnB,EAAG,gBAAgB,OAsDzB,SAAmB,EAAiB,EAAc,GAChD,MAAM,EAAS,EAAG,WAClB,IAAK,EAAQ,OAEb,MAAM,EAAO,OAAO,aACd,EAAU,SAAS,cAAc,UAAU,MACjD,EAAO,aAAa,EAAS,GAE7B,EAAA,KACc,EAAS,EAAM,GAEpB,EAAG,YACN,EAAQ,YAAY,aAAa,EAAI,EAAQ,aAG3C,EAAG,YACL,EAAG,WAAW,YAAY,KArE5B,CAAU,EAAI,EAAQ,IAKxB,MAAM,EAAQ,MAAM,KAAK,EAAG,YAC5B,IAAK,MAAM,KAAQ,EAAO,CACxB,MAAM,KAAE,EAAA,MAAM,GAAU,EAClB,EAAS,CAAC,UAAW,SAAS,SAAS,GACvC,EAAe,CAAC,UAAW,SAAS,SAAS,GAC7C,EAAa,EAAK,WAAW,QAAU,EAAK,WAAW,KACvD,EAAU,EAAK,WAAW,QAAU,EAAK,WAAW,KAE1D,GAAI,EACF,EAAG,gBAAgB,GACnB,EAAA,KACE,MAAM,EAAM,EAAS,EAAO,GAC5B,EAAG,YAAc,QAAoC,OAAO,GAAO,aAE5D,EACT,EAAG,gBAAgB,GACnB,EAAqB,EAAI,EAAO,WACvB,EAAY,CACrB,MAAM,EAAY,EAAK,MAAM,KAAK,MAClC,EAAG,gBAAgB,GACnB,EAAA,KACE,MAAM,EAAM,EAAS,EAAO,GACxB,UAA6C,IAAR,EACvC,EAAG,gBAAgB,IACF,IAAR,EACT,EAAG,aAAa,EAAW,IAE3B,EAAG,aAAa,EAAW,OAAO,cAG7B,EAAS,CAClB,MAAM,EAAY,EAAK,MAAM,KAAK,MAClC,EAAG,gBAAgB,GACnB,EAAG,iBAAiB,EAAY,IAC9B,EAAc,EAAO,EAAgB,MAO3C,MAAM,EAAW,MAAM,KAAK,EAAG,YAC/B,IAAK,MAAM,KAAS,EAClB,EAAU,EAAO,IAgHvB,SAAgB,EACd,EACA,EAAuB,CAAA,GAEvB,MAAM,EACyB,iBAAtB,EACH,SAAS,cAAc,GACvB,EAEF,EACF,EAAU,EAAI,GAEd,QAAQ,KAAK,iCAAiC,KAIlD,SAAS,EAAqB,EAAiB,EAAc,GAC3D,MAAM,EAAa,aAAc,kBAAgC,aAAZ,EAAG,KAClD,EAAU,aAAc,kBAAgC,UAAZ,EAAG,KAGrD,EAAA,KACE,MAAM,EAAM,EAAS,EAAM,GAC3B,GAAI,EAAY,CACC,EACR,UAAY,UACV,EAAS,CAClB,MAAM,EAAS,EACf,EAAO,QAAU,EAAO,QAAU,OAAO,OACpC,CACU,EACR,MAAe,MAAP,EAAc,GAAK,OAAO,MAM7C,MAAM,EADW,GAAc,GAAW,aAAc,kBAC3B,SAAW,QACxC,EAAG,iBAAiB,EAAY,IAE5B,EADE,EACY,GAAG,4BAEH,GAAG,0BAFgC,EAAS,KCrPhE,GAAsB,oBAAX,OAAwB,CACjC,MAAM,EAAI,OACV,EAAE,MAAQ,EAAE,OAAS,CAAA,EACrB,EAAE,MAAM,GAAK,CACX,kBACA,WACA,SACA,QACA,UACA,WACA,gBACA"}
|
|
1
|
+
{"version":3,"file":"ui.umd.js","names":[],"sources":["../src/reactivity.ts","../src/evaluator.ts","../src/parser.ts","../src/index.ts"],"sourcesContent":["let activeEffect: symbol | null = null\nconst targetMap = new WeakMap<\n object,\n Map<string | symbol, Set<ReactiveEffect>>\n>()\nconst proxyMap = new WeakMap<object, any>()\nconst effectMap = new WeakMap<symbol, ReactiveEffect>()\n\nexport class ReactiveEffect {\n active = true\n deps: Set<Set<ReactiveEffect>> = new Set()\n\n constructor(public fn: () => void) {}\n\n run() {\n if (!this.active) return this.fn()\n\n this.cleanup()\n\n activeEffect = Symbol()\n effectMap.set(activeEffect, this)\n\n try {\n return this.fn()\n } finally {\n effectMap.delete(activeEffect)\n activeEffect = null\n }\n }\n\n stop() {\n if (this.active) {\n this.cleanup()\n this.active = false\n }\n }\n\n cleanup() {\n this.deps.forEach((dep) => dep.delete(this))\n this.deps.clear()\n }\n\n effect() {\n return {\n run: () => this.run(),\n stop: () => this.stop(),\n }\n }\n}\n\nexport function effect(fn: () => void): any {\n const ref = new ReactiveEffect(fn)\n ref.run()\n return ref.effect()\n}\n\nexport function track(target: object, key: string | symbol) {\n if (activeEffect) {\n let depsMap = targetMap.get(target)\n if (!depsMap) {\n depsMap = new Map()\n targetMap.set(target, depsMap)\n }\n\n let dep = depsMap.get(key)\n if (!dep) {\n dep = new Set()\n depsMap.set(key, dep)\n }\n\n const active = effectMap.get(activeEffect)\n if (active) {\n dep.add(active)\n active.deps.add(dep)\n }\n }\n}\n\nexport function trigger(target: object, key: string | symbol) {\n const depsMap = targetMap.get(target)\n if (!depsMap) return\n\n const dep = depsMap.get(key)\n if (dep) {\n const effects = new Set(dep)\n effects.forEach((effect) => effect.run())\n }\n}\n\nexport function reactive<T extends object>(target: T): T {\n const notObject = typeof target !== 'object' || target === null\n if (notObject) return target\n\n const isReactive = Object.hasOwn(target, '_isReactive')\n if (isReactive) return target\n\n const existingProxy = proxyMap.get(target)\n if (existingProxy) return existingProxy\n\n const proxy = new Proxy(target, {\n get(obj, key, receiver) {\n if (key === '_isReactive') return true\n track(obj, key)\n const res = Reflect.get(obj, key, receiver)\n // deep reactivity\n if (typeof res === 'object' && res !== null) {\n return reactive(res)\n }\n return res\n },\n set(obj, key, value, receiver) {\n const isArray = Array.isArray(obj)\n const oldValue = Reflect.get(obj, key, receiver)\n const hadKey =\n isArray && String(Number(key)) === key\n ? Number(key) < obj.length\n : Object.hasOwn(obj, key)\n\n const result = Reflect.set(obj, key, value, receiver)\n\n if (!hadKey) {\n trigger(obj, key)\n if (isArray && key !== 'length') {\n trigger(obj, 'length')\n }\n } else if (oldValue !== value) {\n trigger(obj, key)\n }\n\n return result\n },\n })\n\n proxyMap.set(target, proxy)\n return proxy\n}\n","export function evaluate(\n expression: string,\n context: Record<string, any> = {},\n) {\n try {\n const fn = new Function(`with(this) { return ${expression} }`)\n return fn.call(context)\n } catch (error) {\n console.error(`[jsweb/ui] Error evaluating expression: ${expression}`, error)\n return undefined\n }\n}\n\nexport function evaluateEvent(\n expression: string,\n context: Record<string, any> = {},\n $event: Event,\n) {\n try {\n const exp = expression.trim()\n const isIdentifier = /^[a-zA-Z_$][0-9a-zA-Z_$.]*$/.test(exp)\n const code = `${exp} instanceof Function ? ${exp}.call(this, $event) : ${exp}`\n const result = isIdentifier ? code : exp\n\n const fn = new Function('$event', `with(this) { ${result} }`)\n fn.call(context, $event)\n } catch (error) {\n console.error(\n `[jsweb/ui] Error evaluating event expression: ${expression}`,\n error,\n )\n }\n}\n","import { effect, reactive } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\n\nexport type Context = Record<string, any>\n\ninterface BoundNode extends Node {\n _effects?: Array<() => void>\n}\n\nexport function cleanupTree(node: Node) {\n const bNode = node as BoundNode\n if (bNode._effects) {\n bNode._effects.forEach((stop) => stop())\n bNode._effects = []\n }\n const children = Array.from(node.childNodes)\n for (const child of children) {\n cleanupTree(child)\n }\n}\n\nexport function createContext(\n scope: any,\n context: Context | null = null,\n): Context {\n const reactiveScope = scope._isReactive ? scope : reactive(scope)\n\n return new Proxy(reactiveScope, {\n get(target, prop) {\n if (prop === '_isContext') return true\n if (prop in target) return Reflect.get(target, prop, target)\n if (context && prop in context) {\n return Reflect.get(context, prop, context)\n }\n return Reflect.get(target, prop, target)\n },\n set(target, prop, value) {\n if (prop in target) return Reflect.set(target, prop, value, target)\n if (context && prop in context) {\n return Reflect.set(context, prop, value, context)\n }\n return Reflect.set(target, prop, value, target)\n },\n has(target, prop) {\n if (prop in target) return true\n if (context && prop in context) return true\n return false\n },\n })\n}\n\nexport function parseNode(node: Node, context: Context) {\n if (node.nodeType !== Node.ELEMENT_NODE) return\n\n const el = node as HTMLElement\n const currentContext = processScope(el, context)\n\n const forAttr = getDirectiveValue(el, ['ui:for', ':for'])\n if (forAttr) {\n removeDirectiveAttributes(el, ['ui:for', ':for'])\n processFor(el, forAttr, currentContext)\n return\n }\n\n const ifAttr = getDirectiveValue(el, ['ui:if', ':if'])\n if (ifAttr) {\n removeDirectiveAttributes(el, ['ui:if', ':if'])\n processIf(el, ifAttr, currentContext)\n }\n\n processAttributes(el, currentContext)\n\n const children = Array.from(el.childNodes)\n for (const child of children) {\n parseNode(child, currentContext)\n }\n}\n\nexport function createComponent(\n selectorOrElement: string | HTMLElement,\n rootContext: Context = {},\n) {\n const el =\n typeof selectorOrElement === 'string'\n ? document.querySelector(selectorOrElement)\n : selectorOrElement\n\n if (el) {\n parseNode(el, rootContext)\n } else {\n console.warn('[jsweb/ui] Element not found:', selectorOrElement)\n }\n}\n\nfunction bindEffect(node: Node, fn: () => void) {\n const e = effect(fn)\n const bNode = node as BoundNode\n bNode._effects ??= []\n bNode._effects.push(e.stop)\n}\n\nfunction getDirectiveValue(el: HTMLElement, names: string[]) {\n for (const name of names) {\n const value = el.getAttribute(name)\n if (value !== null) return value\n }\n return null\n}\n\nfunction removeDirectiveAttributes(el: HTMLElement, names: string[]) {\n for (const name of names) {\n el.removeAttribute(name)\n }\n}\n\nfunction processScope(el: HTMLElement, context: Context) {\n const attrs = ['ui:scope', ':scope']\n const directive = getDirectiveValue(el, attrs)\n if (!directive) return context\n\n const scope = evaluate(directive, context) || {}\n removeDirectiveAttributes(el, attrs)\n return createContext(scope, context)\n}\n\nfunction processFor(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const match = /^\\s*(.+)\\s+(?:in|of)\\s+(.+)\\s*$/.exec(expr)\n if (!match) {\n return console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)\n }\n const [, itemName, listName] = match\n\n const keyAttr = ['ui:key', ':key']\n const keyExpr = getDirectiveValue(el, keyAttr)\n removeDirectiveAttributes(el, keyAttr)\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:for ${uuid} `)\n el.replaceWith(comment)\n\n interface RenderedNode {\n key: any\n el: HTMLElement\n scope: any\n }\n let renderedNodes: RenderedNode[] = []\n\n bindEffect(comment, () => {\n const list = evaluate(listName, context)\n\n if (!Array.isArray(list)) {\n renderedNodes.forEach((node) => {\n node.el.remove()\n cleanupTree(node.el)\n })\n renderedNodes = []\n return\n }\n\n const newNodes: RenderedNode[] = []\n const oldNodesByKey = new Map<any, RenderedNode>()\n renderedNodes.forEach((node) => oldNodesByKey.set(node.key, node))\n\n list.forEach((item, index) => {\n const scope = { [itemName]: item, $index: index }\n let key: any = index\n\n if (keyExpr) {\n const tempContext = createContext(scope, context)\n key = evaluate(keyExpr, tempContext)\n }\n\n let node = oldNodesByKey.get(key)\n if (node) {\n // Reuse node\n node.scope[itemName] = item\n node.scope.$index = index\n oldNodesByKey.delete(key)\n } else {\n // Create new node\n const clone = el.cloneNode(true) as HTMLElement\n const reactiveScope = reactive(scope)\n const localContext = createContext(reactiveScope, context)\n parseNode(clone, localContext)\n node = { key, el: clone, scope: reactiveScope }\n }\n\n newNodes.push(node)\n })\n\n // Remove un-reused nodes\n oldNodesByKey.forEach((node) => {\n node.el.remove()\n cleanupTree(node.el)\n })\n\n // Reorder and insert new DOM nodes\n let currentAnchor = comment.nextSibling\n newNodes.forEach((node) => {\n if (currentAnchor === node.el) {\n currentAnchor = currentAnchor.nextSibling\n } else {\n comment.parentNode?.insertBefore(node.el, currentAnchor)\n }\n })\n\n renderedNodes = newNodes\n })\n}\n\nfunction processIf(el: HTMLElement, expr: string, context: Context) {\n const parent = el.parentNode\n if (!parent) return\n\n const uuid = crypto.randomUUID()\n const comment = document.createComment(` ui:if ${uuid} `)\n el.before(comment)\n\n bindEffect(comment, () => {\n const val = evaluate(expr, context)\n if (val) {\n if (!el.parentNode) {\n comment.parentNode?.insertBefore(el, comment.nextSibling)\n }\n } else if (el.parentNode) {\n el.remove()\n }\n })\n}\n\nfunction processAttributes(el: HTMLElement, context: Context) {\n const attrs = Array.from(el.attributes)\n\n for (const attr of attrs) {\n const { name, value } = attr\n const isText = ['ui:text', ':text'].includes(name)\n const isTwoWayBind = ['ui:bind', ':bind'].includes(name)\n const isAttrBind = name.startsWith('ui:') || name.startsWith(':')\n const isEvent = name.startsWith('ui@') || name.startsWith('@')\n\n if (isText) {\n processTextBinding(el, value, context)\n } else if (isTwoWayBind) {\n processTwoWayBinding(el, value, context)\n } else if (isAttrBind) {\n const bound = name.split(':').pop()!\n processAttrBinding(el, bound, value, context)\n } else if (isEvent) {\n processEventBinding(el, name, value, context)\n }\n\n el.removeAttribute(name)\n }\n}\n\nfunction processTextBinding(el: HTMLElement, expr: string, context: Context) {\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n el.textContent = val !== undefined && val !== null ? String(val) : ''\n })\n}\n\nfunction processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {\n const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'\n const isRadio = el instanceof HTMLInputElement && el.type === 'radio'\n\n // 1. Reactive state to DOM\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n if (isCheckbox) {\n el.checked = !!val\n } else if (isRadio) {\n el.checked = el.value === String(val)\n } else {\n const target = el as\n | HTMLInputElement\n | HTMLSelectElement\n | HTMLTextAreaElement\n target.value = val == null ? '' : String(val)\n }\n })\n\n // 2. DOM to Reactive state\n const isChange = isCheckbox || isRadio || el instanceof HTMLSelectElement\n const eventName = isChange ? 'change' : 'input'\n el.addEventListener(eventName, ($event) => {\n if (isCheckbox) {\n evaluateEvent(`${expr} = $event.target.checked`, context, $event)\n } else {\n evaluateEvent(`${expr} = $event.target.value`, context, $event)\n }\n })\n}\n\nfunction processAttrBinding(\n el: HTMLElement,\n attr: string,\n expr: string,\n context: Context,\n) {\n bindEffect(el, () => {\n const val = evaluate(expr, context)\n if (val === null || val === undefined || val === false) {\n el.removeAttribute(attr)\n } else if (val === true) {\n el.setAttribute(attr, '')\n } else {\n el.setAttribute(attr, String(val))\n }\n })\n}\n\nfunction processEventBinding(\n el: HTMLElement,\n evt: string,\n expr: string,\n context: Context,\n) {\n const refs = evt.split('@').pop()!\n const [name, ...modifiers] = refs.split('.')\n\n const isOutside = modifiers.includes('outside')\n const target = isOutside ? document : el\n\n const handler: EventListener = ($event: Event) => {\n if (!el.isConnected) return\n\n const isTargetNode = $event.target instanceof Node\n\n if (isOutside && isTargetNode && el.contains($event.target)) return\n if (modifiers.includes('self') && $event.target !== el) return\n\n if (modifiers.includes('prevent')) $event.preventDefault()\n if (modifiers.includes('stop')) $event.stopPropagation()\n\n evaluateEvent(expr, context, $event)\n }\n\n target.addEventListener(name, handler)\n\n if (isOutside) {\n const bNode = el as BoundNode\n bNode._effects ??= []\n bNode._effects.push(() => target.removeEventListener(name, handler))\n }\n}\n","import { reactive, effect, track, trigger } from './reactivity'\nimport { evaluate, evaluateEvent } from './evaluator'\nimport { createComponent, parseNode } from './parser'\n\nexport {\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n createComponent,\n parseNode,\n}\n\nif (typeof window !== 'undefined') {\n const w = window as any\n w.jsweb = w.jsweb || {}\n w.jsweb.ui = {\n createComponent,\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n parseNode,\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/parser.ts
CHANGED
|
@@ -3,174 +3,166 @@ import { evaluate, evaluateEvent } from './evaluator'
|
|
|
3
3
|
|
|
4
4
|
export type Context = Record<string, any>
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
|
|
9
28
|
return new Proxy(reactiveScope, {
|
|
10
29
|
get(target, prop) {
|
|
11
|
-
if (prop === '
|
|
30
|
+
if (prop === '_isContext') return true
|
|
12
31
|
if (prop in target) return Reflect.get(target, prop, target)
|
|
13
|
-
if (
|
|
14
|
-
return Reflect.get(
|
|
32
|
+
if (context && prop in context) {
|
|
33
|
+
return Reflect.get(context, prop, context)
|
|
15
34
|
}
|
|
16
35
|
return Reflect.get(target, prop, target)
|
|
17
36
|
},
|
|
18
37
|
set(target, prop, value) {
|
|
19
38
|
if (prop in target) return Reflect.set(target, prop, value, target)
|
|
20
|
-
if (
|
|
21
|
-
return Reflect.set(
|
|
39
|
+
if (context && prop in context) {
|
|
40
|
+
return Reflect.set(context, prop, value, context)
|
|
22
41
|
}
|
|
23
42
|
return Reflect.set(target, prop, value, target)
|
|
24
43
|
},
|
|
25
44
|
has(target, prop) {
|
|
26
45
|
if (prop in target) return true
|
|
27
|
-
if (
|
|
46
|
+
if (context && prop in context) return true
|
|
28
47
|
return false
|
|
29
|
-
}
|
|
48
|
+
},
|
|
30
49
|
})
|
|
31
50
|
}
|
|
32
51
|
|
|
33
52
|
export function parseNode(node: Node, context: Context) {
|
|
34
|
-
if (node.nodeType
|
|
35
|
-
const el = node as HTMLElement
|
|
36
|
-
|
|
37
|
-
// 1. Check for scope
|
|
38
|
-
let currentContext = context
|
|
39
|
-
const scopeAttr = el.getAttribute('ui:scope') || el.getAttribute(':scope')
|
|
40
|
-
if (scopeAttr) {
|
|
41
|
-
const scopeData = evaluate(scopeAttr, context) || {}
|
|
42
|
-
currentContext = createContext(scopeData, context)
|
|
43
|
-
el.removeAttribute('ui:scope')
|
|
44
|
-
el.removeAttribute(':scope')
|
|
45
|
-
}
|
|
53
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (forAttr) {
|
|
50
|
-
el.removeAttribute('ui:for')
|
|
51
|
-
el.removeAttribute(':for')
|
|
52
|
-
processFor(el, forAttr, currentContext)
|
|
53
|
-
return // Stop processing this node further, processFor handles clones
|
|
54
|
-
}
|
|
55
|
+
const el = node as HTMLElement
|
|
56
|
+
const currentContext = processScope(el, context)
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// We continue processing children because the element might be shown
|
|
63
|
-
}
|
|
58
|
+
const forAttr = getDirectiveValue(el, ['ui:for', ':for'])
|
|
59
|
+
if (forAttr) {
|
|
60
|
+
removeDirectiveAttributes(el, ['ui:for', ':for'])
|
|
61
|
+
processFor(el, forAttr, currentContext)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const isTwoWayBind = ['ui:bind', ':bind'].includes(name)
|
|
71
|
-
const isAttrBind = name.startsWith('ui:') || name.startsWith(':')
|
|
72
|
-
const isEvent = name.startsWith('ui@') || name.startsWith('@')
|
|
73
|
-
|
|
74
|
-
if (isText) {
|
|
75
|
-
el.removeAttribute(name)
|
|
76
|
-
effect(() => {
|
|
77
|
-
const val = evaluate(value, currentContext)
|
|
78
|
-
el.textContent = val !== undefined && val !== null ? String(val) : ''
|
|
79
|
-
})
|
|
80
|
-
} else if (isTwoWayBind) {
|
|
81
|
-
el.removeAttribute(name)
|
|
82
|
-
processTwoWayBinding(el, value, currentContext)
|
|
83
|
-
} else if (isAttrBind) {
|
|
84
|
-
const boundAttr = name.split(':').pop()!
|
|
85
|
-
el.removeAttribute(name)
|
|
86
|
-
effect(() => {
|
|
87
|
-
const val = evaluate(value, currentContext)
|
|
88
|
-
if (val === null || val === undefined || val === false) {
|
|
89
|
-
el.removeAttribute(boundAttr)
|
|
90
|
-
} else if (val === true) {
|
|
91
|
-
el.setAttribute(boundAttr, '')
|
|
92
|
-
} else {
|
|
93
|
-
el.setAttribute(boundAttr, String(val))
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
} else if (isEvent) {
|
|
97
|
-
const eventName = name.split('@').pop()!
|
|
98
|
-
el.removeAttribute(name)
|
|
99
|
-
el.addEventListener(eventName, ($event) => {
|
|
100
|
-
evaluateEvent(value, currentContext, $event)
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
}
|
|
65
|
+
const ifAttr = getDirectiveValue(el, ['ui:if', ':if'])
|
|
66
|
+
if (ifAttr) {
|
|
67
|
+
removeDirectiveAttributes(el, ['ui:if', ':if'])
|
|
68
|
+
processIf(el, ifAttr, currentContext)
|
|
69
|
+
}
|
|
104
70
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
71
|
+
processAttributes(el, currentContext)
|
|
72
|
+
|
|
73
|
+
const children = Array.from(el.childNodes)
|
|
74
|
+
for (const child of children) {
|
|
75
|
+
parseNode(child, currentContext)
|
|
111
76
|
}
|
|
112
77
|
}
|
|
113
78
|
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
79
|
+
export function createComponent(
|
|
80
|
+
selectorOrElement: string | HTMLElement,
|
|
81
|
+
rootContext: Context = {},
|
|
82
|
+
) {
|
|
83
|
+
const el =
|
|
84
|
+
typeof selectorOrElement === 'string'
|
|
85
|
+
? document.querySelector(selectorOrElement)
|
|
86
|
+
: selectorOrElement
|
|
117
87
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
88
|
+
if (el) {
|
|
89
|
+
parseNode(el, rootContext)
|
|
90
|
+
} else {
|
|
91
|
+
console.warn('[jsweb/ui] Element not found:', selectorOrElement)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
121
94
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
95
|
+
function bindEffect(node: Node, fn: () => void) {
|
|
96
|
+
const e = effect(fn)
|
|
97
|
+
const bNode = node as BoundNode
|
|
98
|
+
bNode._effects ??= []
|
|
99
|
+
bNode._effects.push(e.stop)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getDirectiveValue(el: HTMLElement, names: string[]) {
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
const value = el.getAttribute(name)
|
|
105
|
+
if (value !== null) return value
|
|
106
|
+
}
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function removeDirectiveAttributes(el: HTMLElement, names: string[]) {
|
|
111
|
+
for (const name of names) {
|
|
112
|
+
el.removeAttribute(name)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function processScope(el: HTMLElement, context: Context) {
|
|
117
|
+
const attrs = ['ui:scope', ':scope']
|
|
118
|
+
const directive = getDirectiveValue(el, attrs)
|
|
119
|
+
if (!directive) return context
|
|
120
|
+
|
|
121
|
+
const scope = evaluate(directive, context) || {}
|
|
122
|
+
removeDirectiveAttributes(el, attrs)
|
|
123
|
+
return createContext(scope, context)
|
|
134
124
|
}
|
|
135
125
|
|
|
136
126
|
function processFor(el: HTMLElement, expr: string, context: Context) {
|
|
137
127
|
const parent = el.parentNode
|
|
138
128
|
if (!parent) return
|
|
139
|
-
|
|
140
|
-
const match =
|
|
129
|
+
|
|
130
|
+
const match = /^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(expr)
|
|
141
131
|
if (!match) {
|
|
142
|
-
console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)
|
|
143
|
-
return
|
|
132
|
+
return console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)
|
|
144
133
|
}
|
|
145
134
|
const [, itemName, listName] = match
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
el
|
|
149
|
-
el
|
|
150
|
-
|
|
135
|
+
|
|
136
|
+
const keyAttr = ['ui:key', ':key']
|
|
137
|
+
const keyExpr = getDirectiveValue(el, keyAttr)
|
|
138
|
+
removeDirectiveAttributes(el, keyAttr)
|
|
139
|
+
|
|
151
140
|
const uuid = crypto.randomUUID()
|
|
152
141
|
const comment = document.createComment(` ui:for ${uuid} `)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
142
|
+
el.replaceWith(comment)
|
|
143
|
+
|
|
144
|
+
interface RenderedNode {
|
|
156
145
|
key: any
|
|
157
146
|
el: HTMLElement
|
|
158
147
|
scope: any
|
|
159
148
|
}
|
|
160
149
|
let renderedNodes: RenderedNode[] = []
|
|
161
150
|
|
|
162
|
-
|
|
151
|
+
bindEffect(comment, () => {
|
|
163
152
|
const list = evaluate(listName, context)
|
|
164
153
|
|
|
165
154
|
if (!Array.isArray(list)) {
|
|
166
|
-
renderedNodes.forEach(node =>
|
|
155
|
+
renderedNodes.forEach((node) => {
|
|
156
|
+
node.el.remove()
|
|
157
|
+
cleanupTree(node.el)
|
|
158
|
+
})
|
|
167
159
|
renderedNodes = []
|
|
168
160
|
return
|
|
169
161
|
}
|
|
170
162
|
|
|
171
163
|
const newNodes: RenderedNode[] = []
|
|
172
164
|
const oldNodesByKey = new Map<any, RenderedNode>()
|
|
173
|
-
renderedNodes.forEach(node => oldNodesByKey.set(node.key, node))
|
|
165
|
+
renderedNodes.forEach((node) => oldNodesByKey.set(node.key, node))
|
|
174
166
|
|
|
175
167
|
list.forEach((item, index) => {
|
|
176
168
|
const scope = { [itemName]: item, $index: index }
|
|
@@ -195,13 +187,14 @@ function processFor(el: HTMLElement, expr: string, context: Context) {
|
|
|
195
187
|
parseNode(clone, localContext)
|
|
196
188
|
node = { key, el: clone, scope: reactiveScope }
|
|
197
189
|
}
|
|
198
|
-
|
|
190
|
+
|
|
199
191
|
newNodes.push(node)
|
|
200
192
|
})
|
|
201
193
|
|
|
202
194
|
// Remove un-reused nodes
|
|
203
|
-
oldNodesByKey.forEach(node => {
|
|
204
|
-
node.el.
|
|
195
|
+
oldNodesByKey.forEach((node) => {
|
|
196
|
+
node.el.remove()
|
|
197
|
+
cleanupTree(node.el)
|
|
205
198
|
})
|
|
206
199
|
|
|
207
200
|
// Reorder and insert new DOM nodes
|
|
@@ -218,37 +211,74 @@ function processFor(el: HTMLElement, expr: string, context: Context) {
|
|
|
218
211
|
})
|
|
219
212
|
}
|
|
220
213
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
) {
|
|
225
|
-
const el =
|
|
226
|
-
typeof selectorOrElement === 'string'
|
|
227
|
-
? document.querySelector(selectorOrElement)
|
|
228
|
-
: selectorOrElement
|
|
214
|
+
function processIf(el: HTMLElement, expr: string, context: Context) {
|
|
215
|
+
const parent = el.parentNode
|
|
216
|
+
if (!parent) return
|
|
229
217
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
218
|
+
const uuid = crypto.randomUUID()
|
|
219
|
+
const comment = document.createComment(` ui:if ${uuid} `)
|
|
220
|
+
el.before(comment)
|
|
221
|
+
|
|
222
|
+
bindEffect(comment, () => {
|
|
223
|
+
const val = evaluate(expr, context)
|
|
224
|
+
if (val) {
|
|
225
|
+
if (!el.parentNode) {
|
|
226
|
+
comment.parentNode?.insertBefore(el, comment.nextSibling)
|
|
227
|
+
}
|
|
228
|
+
} else if (el.parentNode) {
|
|
229
|
+
el.remove()
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function processAttributes(el: HTMLElement, context: Context) {
|
|
235
|
+
const attrs = Array.from(el.attributes)
|
|
236
|
+
|
|
237
|
+
for (const attr of attrs) {
|
|
238
|
+
const { name, value } = attr
|
|
239
|
+
const isText = ['ui:text', ':text'].includes(name)
|
|
240
|
+
const isTwoWayBind = ['ui:bind', ':bind'].includes(name)
|
|
241
|
+
const isAttrBind = name.startsWith('ui:') || name.startsWith(':')
|
|
242
|
+
const isEvent = name.startsWith('ui@') || name.startsWith('@')
|
|
243
|
+
|
|
244
|
+
if (isText) {
|
|
245
|
+
processTextBinding(el, value, context)
|
|
246
|
+
} else if (isTwoWayBind) {
|
|
247
|
+
processTwoWayBinding(el, value, context)
|
|
248
|
+
} else if (isAttrBind) {
|
|
249
|
+
const bound = name.split(':').pop()!
|
|
250
|
+
processAttrBinding(el, bound, value, context)
|
|
251
|
+
} else if (isEvent) {
|
|
252
|
+
processEventBinding(el, name, value, context)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
el.removeAttribute(name)
|
|
234
256
|
}
|
|
235
257
|
}
|
|
236
258
|
|
|
259
|
+
function processTextBinding(el: HTMLElement, expr: string, context: Context) {
|
|
260
|
+
bindEffect(el, () => {
|
|
261
|
+
const val = evaluate(expr, context)
|
|
262
|
+
el.textContent = val !== undefined && val !== null ? String(val) : ''
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
237
266
|
function processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {
|
|
238
267
|
const isCheckbox = el instanceof HTMLInputElement && el.type === 'checkbox'
|
|
239
268
|
const isRadio = el instanceof HTMLInputElement && el.type === 'radio'
|
|
240
|
-
|
|
269
|
+
|
|
241
270
|
// 1. Reactive state to DOM
|
|
242
|
-
|
|
271
|
+
bindEffect(el, () => {
|
|
243
272
|
const val = evaluate(expr, context)
|
|
244
273
|
if (isCheckbox) {
|
|
245
|
-
|
|
246
|
-
target.checked = !!val
|
|
274
|
+
el.checked = !!val
|
|
247
275
|
} else if (isRadio) {
|
|
248
|
-
|
|
249
|
-
target.checked = target.value === String(val)
|
|
276
|
+
el.checked = el.value === String(val)
|
|
250
277
|
} else {
|
|
251
|
-
const target = el as
|
|
278
|
+
const target = el as
|
|
279
|
+
| HTMLInputElement
|
|
280
|
+
| HTMLSelectElement
|
|
281
|
+
| HTMLTextAreaElement
|
|
252
282
|
target.value = val == null ? '' : String(val)
|
|
253
283
|
}
|
|
254
284
|
})
|
|
@@ -264,3 +294,56 @@ function processTwoWayBinding(el: HTMLElement, expr: string, context: Context) {
|
|
|
264
294
|
}
|
|
265
295
|
})
|
|
266
296
|
}
|
|
297
|
+
|
|
298
|
+
function processAttrBinding(
|
|
299
|
+
el: HTMLElement,
|
|
300
|
+
attr: string,
|
|
301
|
+
expr: string,
|
|
302
|
+
context: Context,
|
|
303
|
+
) {
|
|
304
|
+
bindEffect(el, () => {
|
|
305
|
+
const val = evaluate(expr, context)
|
|
306
|
+
if (val === null || val === undefined || val === false) {
|
|
307
|
+
el.removeAttribute(attr)
|
|
308
|
+
} else if (val === true) {
|
|
309
|
+
el.setAttribute(attr, '')
|
|
310
|
+
} else {
|
|
311
|
+
el.setAttribute(attr, String(val))
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function processEventBinding(
|
|
317
|
+
el: HTMLElement,
|
|
318
|
+
evt: string,
|
|
319
|
+
expr: string,
|
|
320
|
+
context: Context,
|
|
321
|
+
) {
|
|
322
|
+
const refs = evt.split('@').pop()!
|
|
323
|
+
const [name, ...modifiers] = refs.split('.')
|
|
324
|
+
|
|
325
|
+
const isOutside = modifiers.includes('outside')
|
|
326
|
+
const target = isOutside ? document : el
|
|
327
|
+
|
|
328
|
+
const handler: EventListener = ($event: Event) => {
|
|
329
|
+
if (!el.isConnected) return
|
|
330
|
+
|
|
331
|
+
const isTargetNode = $event.target instanceof Node
|
|
332
|
+
|
|
333
|
+
if (isOutside && isTargetNode && el.contains($event.target)) return
|
|
334
|
+
if (modifiers.includes('self') && $event.target !== el) return
|
|
335
|
+
|
|
336
|
+
if (modifiers.includes('prevent')) $event.preventDefault()
|
|
337
|
+
if (modifiers.includes('stop')) $event.stopPropagation()
|
|
338
|
+
|
|
339
|
+
evaluateEvent(expr, context, $event)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
target.addEventListener(name, handler)
|
|
343
|
+
|
|
344
|
+
if (isOutside) {
|
|
345
|
+
const bNode = el as BoundNode
|
|
346
|
+
bNode._effects ??= []
|
|
347
|
+
bNode._effects.push(() => target.removeEventListener(name, handler))
|
|
348
|
+
}
|
|
349
|
+
}
|
package/src/reactivity.ts
CHANGED
|
@@ -1,14 +1,57 @@
|
|
|
1
|
-
let activeEffect:
|
|
2
|
-
const targetMap = new WeakMap<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
let activeEffect: symbol | null = null
|
|
2
|
+
const targetMap = new WeakMap<
|
|
3
|
+
object,
|
|
4
|
+
Map<string | symbol, Set<ReactiveEffect>>
|
|
5
|
+
>()
|
|
6
|
+
const proxyMap = new WeakMap<object, any>()
|
|
7
|
+
const effectMap = new WeakMap<symbol, ReactiveEffect>()
|
|
8
|
+
|
|
9
|
+
export class ReactiveEffect {
|
|
10
|
+
active = true
|
|
11
|
+
deps: Set<Set<ReactiveEffect>> = new Set()
|
|
12
|
+
|
|
13
|
+
constructor(public fn: () => void) {}
|
|
14
|
+
|
|
15
|
+
run() {
|
|
16
|
+
if (!this.active) return this.fn()
|
|
17
|
+
|
|
18
|
+
this.cleanup()
|
|
19
|
+
|
|
20
|
+
activeEffect = Symbol()
|
|
21
|
+
effectMap.set(activeEffect, this)
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
return this.fn()
|
|
25
|
+
} finally {
|
|
26
|
+
effectMap.delete(activeEffect)
|
|
27
|
+
activeEffect = null
|
|
28
|
+
}
|
|
10
29
|
}
|
|
11
|
-
|
|
30
|
+
|
|
31
|
+
stop() {
|
|
32
|
+
if (this.active) {
|
|
33
|
+
this.cleanup()
|
|
34
|
+
this.active = false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
cleanup() {
|
|
39
|
+
this.deps.forEach((dep) => dep.delete(this))
|
|
40
|
+
this.deps.clear()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
effect() {
|
|
44
|
+
return {
|
|
45
|
+
run: () => this.run(),
|
|
46
|
+
stop: () => this.stop(),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function effect(fn: () => void): any {
|
|
52
|
+
const ref = new ReactiveEffect(fn)
|
|
53
|
+
ref.run()
|
|
54
|
+
return ref.effect()
|
|
12
55
|
}
|
|
13
56
|
|
|
14
57
|
export function track(target: object, key: string | symbol) {
|
|
@@ -18,31 +61,45 @@ export function track(target: object, key: string | symbol) {
|
|
|
18
61
|
depsMap = new Map()
|
|
19
62
|
targetMap.set(target, depsMap)
|
|
20
63
|
}
|
|
64
|
+
|
|
21
65
|
let dep = depsMap.get(key)
|
|
22
66
|
if (!dep) {
|
|
23
67
|
dep = new Set()
|
|
24
68
|
depsMap.set(key, dep)
|
|
25
69
|
}
|
|
26
|
-
|
|
70
|
+
|
|
71
|
+
const active = effectMap.get(activeEffect)
|
|
72
|
+
if (active) {
|
|
73
|
+
dep.add(active)
|
|
74
|
+
active.deps.add(dep)
|
|
75
|
+
}
|
|
27
76
|
}
|
|
28
77
|
}
|
|
29
78
|
|
|
30
79
|
export function trigger(target: object, key: string | symbol) {
|
|
31
80
|
const depsMap = targetMap.get(target)
|
|
32
81
|
if (!depsMap) return
|
|
82
|
+
|
|
33
83
|
const dep = depsMap.get(key)
|
|
34
84
|
if (dep) {
|
|
35
|
-
|
|
85
|
+
const effects = new Set(dep)
|
|
86
|
+
effects.forEach((effect) => effect.run())
|
|
36
87
|
}
|
|
37
88
|
}
|
|
38
89
|
|
|
39
90
|
export function reactive<T extends object>(target: T): T {
|
|
40
|
-
|
|
41
|
-
if (
|
|
91
|
+
const notObject = typeof target !== 'object' || target === null
|
|
92
|
+
if (notObject) return target
|
|
93
|
+
|
|
94
|
+
const isReactive = Object.hasOwn(target, '_isReactive')
|
|
95
|
+
if (isReactive) return target
|
|
42
96
|
|
|
43
|
-
|
|
97
|
+
const existingProxy = proxyMap.get(target)
|
|
98
|
+
if (existingProxy) return existingProxy
|
|
99
|
+
|
|
100
|
+
const proxy = new Proxy(target, {
|
|
44
101
|
get(obj, key, receiver) {
|
|
45
|
-
if (key === '
|
|
102
|
+
if (key === '_isReactive') return true
|
|
46
103
|
track(obj, key)
|
|
47
104
|
const res = Reflect.get(obj, key, receiver)
|
|
48
105
|
// deep reactivity
|
|
@@ -54,9 +111,10 @@ export function reactive<T extends object>(target: T): T {
|
|
|
54
111
|
set(obj, key, value, receiver) {
|
|
55
112
|
const isArray = Array.isArray(obj)
|
|
56
113
|
const oldValue = Reflect.get(obj, key, receiver)
|
|
57
|
-
const hadKey =
|
|
58
|
-
|
|
59
|
-
|
|
114
|
+
const hadKey =
|
|
115
|
+
isArray && String(Number(key)) === key
|
|
116
|
+
? Number(key) < obj.length
|
|
117
|
+
: Object.hasOwn(obj, key)
|
|
60
118
|
|
|
61
119
|
const result = Reflect.set(obj, key, value, receiver)
|
|
62
120
|
|
|
@@ -68,8 +126,11 @@ export function reactive<T extends object>(target: T): T {
|
|
|
68
126
|
} else if (oldValue !== value) {
|
|
69
127
|
trigger(obj, key)
|
|
70
128
|
}
|
|
71
|
-
|
|
129
|
+
|
|
72
130
|
return result
|
|
73
131
|
},
|
|
74
132
|
})
|
|
133
|
+
|
|
134
|
+
proxyMap.set(target, proxy)
|
|
135
|
+
return proxy
|
|
75
136
|
}
|