@jsweb/ui 0.1.1 → 1.0.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 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. **Modificadores de Evento:** Adicionar suporte a sufixos como `@click.prevent` e `@click.stop`.
35
- 2. **Sintaxe Especial para Classes e Estilos:** Suporte para dicionários lógicos no CSS como `:class="{ 'is-active': active }"`.
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. O estado deve ser interceptado para disparar "efeitos" (side-effects) que atualizam o DOM.
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 Injeção:** Para avaliar strings de atributos (ex: `ui:text="user.name"`), o motor deve:
35
- 1. Extrair `keys` e `values` do contexto atual.
36
- 2. Construir a função dinâmica: `new Function(...keys, 'return ' + expression)`.
37
- 3. Executar passando os valores: `fn(...values)`.
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. | `<button @click="count++">` |
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. | `<button @click="count++">` |
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
 
@@ -1,4 +1,4 @@
1
1
  import { reactive, effect, track, trigger } from './reactivity';
2
2
  import { evaluate, evaluateEvent } from './evaluator';
3
- import { createComponent, parseNode } from './parser';
4
- export { reactive, effect, track, trigger, evaluate, evaluateEvent, createComponent, parseNode, };
3
+ import { createScope, parseNode } from './parser';
4
+ export { reactive, effect, track, trigger, evaluate, evaluateEvent, createScope, parseNode, };
@@ -2,4 +2,4 @@ export type Context = Record<string, any>;
2
2
  export declare function cleanupTree(node: Node): void;
3
3
  export declare function createContext(scope: any, context?: Context | null): Context;
4
4
  export declare function parseNode(node: Node, context: Context): void;
5
- export declare function createComponent(selectorOrElement: string | HTMLElement, rootContext?: Context): void;
5
+ export declare function createScope(selectorOrElement: string | HTMLElement, context?: Context): void;
package/dist/ui.es.js CHANGED
@@ -1,2 +1,2 @@
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;if(n.has(e))return n.get(e);const t=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,t),t}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 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 a(e){const t=e;t.__effects&&(t.__effects.forEach(e=>e()),t.__effects=[]);const n=Array.from(e.childNodes);for(const o of n)a(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 d(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 b(e,n),p(r,t)}(n,t),r=g(n,["ui:for",":for"]);if(r)return b(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 void console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,r,i]=o,s=e.getAttribute("ui:key")||e.getAttribute(":key");e.removeAttribute("ui:key"),e.removeAttribute(":key");const c=crypto.randomUUID(),l=document.createComment(` ui:for ${c} `);e.replaceWith(l);let h=[];v(l,()=>{const t=f(i,n);if(!Array.isArray(t))return h.forEach(e=>{e.el.remove(),a(e.el)}),void(h=[]);const o=[],c=/* @__PURE__ */new Map;h.forEach(e=>c.set(e.key,e)),t.forEach((t,i)=>{const l={[r]:t,$index:i};let a=i;s&&(a=f(s,p(l,n)));let h=c.get(a);if(h)h.scope[r]=t,h.scope.$index=i,c.delete(a);else{const t=e.cloneNode(!0),o=u(l);d(t,p(o,n)),h={key:a,el:t,scope:o}}o.push(h)}),c.forEach(e=>{e.el.remove(),a(e.el)});let v=l.nextSibling;o.forEach(e=>{v===e.el?v=v.nextSibling:l.parentNode?.insertBefore(e.el,v)}),h=o})}(n,r,o);const i=g(n,["ui:if",":if"]);i&&(b(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?m(e,r,t):s?w(e,r,t):c?y(e,n.split(":").pop(),r,t):u&&$(e,n,r,t),e.removeAttribute(n)}}(n,o);const s=Array.from(n.childNodes);for(const c of s)d(c,o)}function h(e,t={}){const n="string"==typeof e?document.querySelector(e):e;n?d(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 b(e,t){for(const n of t)e.removeAttribute(n)}function m(e,t,n){v(e,()=>{const o=f(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=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=>{l(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,t,n,o){const[r,...i]=t.split("@").pop().split(".");e.addEventListener(r,t=>{i.includes("prevent")&&t.preventDefault(),i.includes("stop")&&t.stopPropagation(),i.includes("self")&&t.target!==e||l(n,o,t)})}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:l,parseNode:d}}export{h as createComponent,i as effect,f as evaluate,l as evaluateEvent,d as parseNode,u as reactive,s as track,c as trigger};
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 w(e,n),d(r,t)}(n,t),r=["ui:for",":for"],i=g(n,r);if(i)return w(n,r),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);w(e,s);const a=crypto.randomUUID(),h=document.createComment(` ui:for ${a} `);e.replaceWith(h);let m=[];v(h,()=>{const t=f(i,n);if(!Array.isArray(t))return m.forEach(e=>{e.el.remove(),l(e.el)}),void(m=[]);const o=[],s=/* @__PURE__ */new Map;m.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)}),m=o})}(n,i,o);const s=["ui:if",":if"],c=g(n,s);c&&(w(n,s),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,c,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?m(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 a=Array.from(n.childNodes);for(const u of a)p(u,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 w(e,t){for(const n of t)e.removeAttribute(n)}function m(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={createScope:h,reactive:u,effect:i,track:s,trigger:c,evaluate:f,evaluateEvent:a,parseNode:p}}export{h as createScope,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: 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 if (proxyMap.has(target)) return proxyMap.get(target)\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 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 el.replaceWith(comment)\n\n type 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 el.addEventListener(name, ($event) => {\n if (modifiers.includes('prevent')) $event.preventDefault()\n if (modifiers.includes('stop')) $event.stopPropagation()\n if (modifiers.includes('self') && $event.target !== el) return\n\n evaluateEvent(expr, context, $event)\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 scope = processScope(el, context)\n\n const forAttrs = ['ui:for', ':for']\n const forDirective = getDirectiveValue(el, forAttrs)\n if (forDirective) {\n removeDirectiveAttributes(el, forAttrs)\n processFor(el, forDirective, scope)\n return\n }\n\n const ifAttrs = ['ui:if', ':if']\n const ifDirective = getDirectiveValue(el, ifAttrs)\n if (ifDirective) {\n removeDirectiveAttributes(el, ifAttrs)\n processIf(el, ifDirective, scope)\n }\n\n processAttributes(el, scope)\n\n const children = Array.from(el.childNodes)\n for (const child of children) {\n parseNode(child, scope)\n }\n}\n\nexport function createScope(\n selectorOrElement: string | HTMLElement,\n context: Context = {},\n) {\n const el =\n typeof selectorOrElement === 'string'\n ? document.querySelector(selectorOrElement)\n : selectorOrElement\n\n if (el) {\n parseNode(el, context)\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 keyDirective = 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 (keyDirective) {\n const tempContext = createContext(scope, context)\n key = evaluate(keyDirective, 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 { createScope, parseNode } from './parser'\n\nexport {\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n createScope,\n parseNode,\n}\n\nif (typeof window !== 'undefined') {\n const w = window as any\n w.jsweb = w.jsweb || {}\n w.jsweb.ui = {\n createScope,\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,EA8DR,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,GArEd,CAAa,EAAI,GAEzB,EAAW,CAAC,SAAU,QACtB,EAAe,EAAkB,EAAI,GAC3C,GAAI,EAGF,OAFA,EAA0B,EAAI,QAmElC,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,EAAe,EAAkB,EAAI,GAC3C,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,IAtJhB,CAAW,EAAI,EAAc,GAI/B,MAAM,EAAU,CAAC,QAAS,OACpB,EAAc,EAAkB,EAAI,GACtC,IACF,EAA0B,EAAI,GAmJlC,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,EAAa,IAsK/B,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,EAAmB,CAAA,GAEnB,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,KC7U/D,GAAsB,oBAAX,OAAwB,CACjC,MAAM,EAAI,OACV,EAAE,MAAQ,EAAE,OAAS,CAAA,EACrB,EAAE,MAAM,GAAK,CACX,cACA,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,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 u(e,t){const o=n.get(e);if(!o)return;const r=o.get(t);r&&new Set(r).forEach(e=>e.run())}function f(e){if("object"!=typeof e||null===e)return e;if(Object.hasOwn(e,"_isReactive"))return e;if(o.has(e))return o.get(e);const t=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?f(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&&u(e,t):(u(e,t),r&&"length"!==t&&u(e,"length")),c}});return o.set(e,t),t}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:f(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 y(e,n),p(r,t)}(n,t),r=b(n,["ui:for",":for"]);if(r)return y(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 void console.warn(`[jsweb/ui] Invalid ui:for expression: ${t}`);const[,r,i]=o,s=e.getAttribute("ui:key")||e.getAttribute(":key");e.removeAttribute("ui:key"),e.removeAttribute(":key");const c=crypto.randomUUID(),u=document.createComment(` ui:for ${c} `);e.replaceWith(u);let l=[];g(u,()=>{const t=a(i,n);if(!Array.isArray(t))return l.forEach(e=>{e.el.remove(),d(e.el)}),void(l=[]);const o=[],c=new Map;l.forEach(e=>c.set(e.key,e)),t.forEach((t,i)=>{const u={[r]:t,$index:i};let l=i;s&&(l=a(s,p(u,n)));let d=c.get(l);if(d)d.scope[r]=t,d.scope.$index=i,c.delete(l);else{const t=e.cloneNode(!0),o=f(u);h(t,p(o,n)),d={key:l,el:t,scope:o}}o.push(d)}),c.forEach(e=>{e.el.remove(),d(e.el)});let v=u.nextSibling;o.forEach(e=>{v===e.el?v=v.nextSibling:u.parentNode?.insertBefore(e.el,v)}),l=o})}(n,r,o);const i=b(n,["ui:if",":if"]);i&&(y(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),g(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(":"),u=n.startsWith("ui@")||n.startsWith("@");i?m(e,r,t):s?w(e,r,t):c?E(e,n.split(":").pop(),r,t):u&&$(e,n,r,t),e.removeAttribute(n)}}(n,o);const s=Array.from(n.childNodes);for(const c of s)h(c,o)}function v(e,t={}){const n="string"==typeof e?document.querySelector(e):e;n?h(n,t):console.warn("[jsweb/ui] Element not found:",e)}function g(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 y(e,t){for(const n of t)e.removeAttribute(n)}function m(e,t,n){g(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;g(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){g(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(".");e.addEventListener(r,t=>{i.includes("prevent")&&t.preventDefault(),i.includes("stop")&&t.stopPropagation(),i.includes("self")&&t.target!==e||l(n,o,t)})}if("undefined"!=typeof window){const e=window;e.jsweb=e.jsweb||{},e.jsweb.ui={createComponent:v,reactive:f,effect:s,track:c,trigger:u,evaluate:a,evaluateEvent:l,parseNode:h}}e.createComponent=v,e.effect=s,e.evaluate=a,e.evaluateEvent=l,e.parseNode=h,e.reactive=f,e.track=c,e.trigger=u});
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 y(e,n),p(r,t)}(n,t),r=["ui:for",":for"],i=b(n,r);if(i)return y(n,r),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);y(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,i,o);const s=["ui:if",":if"],c=b(n,s);c&&(y(n,s),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,c,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?m(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 f=Array.from(n.childNodes);for(const u of f)h(u,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 y(e,t){for(const n of t)e.removeAttribute(n)}function m(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={createScope:g,reactive:u,effect:s,track:c,trigger:f,evaluate:a,evaluateEvent:l,parseNode:h}}e.createScope=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
@@ -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: 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 if (proxyMap.has(target)) return proxyMap.get(target)\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 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 el.replaceWith(comment)\n\n type 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 el.addEventListener(name, ($event) => {\n if (modifiers.includes('prevent')) $event.preventDefault()\n if (modifiers.includes('stop')) $event.stopPropagation()\n if (modifiers.includes('self') && $event.target !== el) return\n\n evaluateEvent(expr, context, $event)\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.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 scope = processScope(el, context)\n\n const forAttrs = ['ui:for', ':for']\n const forDirective = getDirectiveValue(el, forAttrs)\n if (forDirective) {\n removeDirectiveAttributes(el, forAttrs)\n processFor(el, forDirective, scope)\n return\n }\n\n const ifAttrs = ['ui:if', ':if']\n const ifDirective = getDirectiveValue(el, ifAttrs)\n if (ifDirective) {\n removeDirectiveAttributes(el, ifAttrs)\n processIf(el, ifDirective, scope)\n }\n\n processAttributes(el, scope)\n\n const children = Array.from(el.childNodes)\n for (const child of children) {\n parseNode(child, scope)\n }\n}\n\nexport function createScope(\n selectorOrElement: string | HTMLElement,\n context: Context = {},\n) {\n const el =\n typeof selectorOrElement === 'string'\n ? document.querySelector(selectorOrElement)\n : selectorOrElement\n\n if (el) {\n parseNode(el, context)\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 keyDirective = 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 (keyDirective) {\n const tempContext = createContext(scope, context)\n key = evaluate(keyDirective, 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 { createScope, parseNode } from './parser'\n\nexport {\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n createScope,\n parseNode,\n}\n\nif (typeof window !== 'undefined') {\n const w = window as any\n w.jsweb = w.jsweb || {}\n w.jsweb.ui = {\n createScope,\n reactive,\n effect,\n track,\n trigger,\n evaluate,\n evaluateEvent,\n parseNode,\n }\n}\n"],"mappings":"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"}
package/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>JS Web UI Test</title>
7
7
  <script type="module">
8
- import { createComponent, reactive } from '/src/index.ts'
8
+ import { createScope, reactive } from '/src/index.ts'
9
9
 
10
10
  const scope = reactive({
11
11
  count: 0,
@@ -44,7 +44,7 @@
44
44
 
45
45
  window.scope = scope
46
46
 
47
- createComponent('body', { scope })
47
+ createScope('body', { scope })
48
48
  </script>
49
49
  </head>
50
50
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsweb/ui",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "description": "JS Web Microframework",
5
5
  "keywords": [
6
6
  "js",
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { reactive, effect, track, trigger } from './reactivity'
2
2
  import { evaluate, evaluateEvent } from './evaluator'
3
- import { createComponent, parseNode } from './parser'
3
+ import { createScope, parseNode } from './parser'
4
4
 
5
5
  export {
6
6
  reactive,
@@ -9,7 +9,7 @@ export {
9
9
  trigger,
10
10
  evaluate,
11
11
  evaluateEvent,
12
- createComponent,
12
+ createScope,
13
13
  parseNode,
14
14
  }
15
15
 
@@ -17,7 +17,7 @@ if (typeof window !== 'undefined') {
17
17
  const w = window as any
18
18
  w.jsweb = w.jsweb || {}
19
19
  w.jsweb.ui = {
20
- createComponent,
20
+ createScope,
21
21
  reactive,
22
22
  effect,
23
23
  track,
package/src/parser.ts CHANGED
@@ -4,14 +4,14 @@ import { evaluate, evaluateEvent } from './evaluator'
4
4
  export type Context = Record<string, any>
5
5
 
6
6
  interface BoundNode extends Node {
7
- __effects?: Array<() => void>
7
+ _effects?: Array<() => void>
8
8
  }
9
9
 
10
10
  export function cleanupTree(node: Node) {
11
11
  const bNode = node as BoundNode
12
- if (bNode.__effects) {
13
- bNode.__effects.forEach((stop) => stop())
14
- bNode.__effects = []
12
+ if (bNode._effects) {
13
+ bNode._effects.forEach((stop) => stop())
14
+ bNode._effects = []
15
15
  }
16
16
  const children = Array.from(node.childNodes)
17
17
  for (const child of children) {
@@ -53,32 +53,34 @@ export function parseNode(node: Node, context: Context) {
53
53
  if (node.nodeType !== Node.ELEMENT_NODE) return
54
54
 
55
55
  const el = node as HTMLElement
56
- const currentContext = processScope(el, context)
56
+ const scope = processScope(el, context)
57
57
 
58
- const forAttr = getDirectiveValue(el, ['ui:for', ':for'])
59
- if (forAttr) {
60
- removeDirectiveAttributes(el, ['ui:for', ':for'])
61
- processFor(el, forAttr, currentContext)
58
+ const forAttrs = ['ui:for', ':for']
59
+ const forDirective = getDirectiveValue(el, forAttrs)
60
+ if (forDirective) {
61
+ removeDirectiveAttributes(el, forAttrs)
62
+ processFor(el, forDirective, scope)
62
63
  return
63
64
  }
64
65
 
65
- const ifAttr = getDirectiveValue(el, ['ui:if', ':if'])
66
- if (ifAttr) {
67
- removeDirectiveAttributes(el, ['ui:if', ':if'])
68
- processIf(el, ifAttr, currentContext)
66
+ const ifAttrs = ['ui:if', ':if']
67
+ const ifDirective = getDirectiveValue(el, ifAttrs)
68
+ if (ifDirective) {
69
+ removeDirectiveAttributes(el, ifAttrs)
70
+ processIf(el, ifDirective, scope)
69
71
  }
70
72
 
71
- processAttributes(el, currentContext)
73
+ processAttributes(el, scope)
72
74
 
73
75
  const children = Array.from(el.childNodes)
74
76
  for (const child of children) {
75
- parseNode(child, currentContext)
77
+ parseNode(child, scope)
76
78
  }
77
79
  }
78
80
 
79
- export function createComponent(
81
+ export function createScope(
80
82
  selectorOrElement: string | HTMLElement,
81
- rootContext: Context = {},
83
+ context: Context = {},
82
84
  ) {
83
85
  const el =
84
86
  typeof selectorOrElement === 'string'
@@ -86,7 +88,7 @@ export function createComponent(
86
88
  : selectorOrElement
87
89
 
88
90
  if (el) {
89
- parseNode(el, rootContext)
91
+ parseNode(el, context)
90
92
  } else {
91
93
  console.warn('[jsweb/ui] Element not found:', selectorOrElement)
92
94
  }
@@ -95,8 +97,8 @@ export function createComponent(
95
97
  function bindEffect(node: Node, fn: () => void) {
96
98
  const e = effect(fn)
97
99
  const bNode = node as BoundNode
98
- bNode.__effects ??= []
99
- bNode.__effects.push(e.stop)
100
+ bNode._effects ??= []
101
+ bNode._effects.push(e.stop)
100
102
  }
101
103
 
102
104
  function getDirectiveValue(el: HTMLElement, names: string[]) {
@@ -129,20 +131,19 @@ function processFor(el: HTMLElement, expr: string, context: Context) {
129
131
 
130
132
  const match = /^\s*(.+)\s+(?:in|of)\s+(.+)\s*$/.exec(expr)
131
133
  if (!match) {
132
- console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)
133
- return
134
+ return console.warn(`[jsweb/ui] Invalid ui:for expression: ${expr}`)
134
135
  }
135
136
  const [, itemName, listName] = match
136
137
 
137
- const keyExpr = el.getAttribute('ui:key') || el.getAttribute(':key')
138
- el.removeAttribute('ui:key')
139
- el.removeAttribute(':key')
138
+ const keyAttr = ['ui:key', ':key']
139
+ const keyDirective = getDirectiveValue(el, keyAttr)
140
+ removeDirectiveAttributes(el, keyAttr)
140
141
 
141
142
  const uuid = crypto.randomUUID()
142
143
  const comment = document.createComment(` ui:for ${uuid} `)
143
144
  el.replaceWith(comment)
144
145
 
145
- type RenderedNode = {
146
+ interface RenderedNode {
146
147
  key: any
147
148
  el: HTMLElement
148
149
  scope: any
@@ -169,9 +170,9 @@ function processFor(el: HTMLElement, expr: string, context: Context) {
169
170
  const scope = { [itemName]: item, $index: index }
170
171
  let key: any = index
171
172
 
172
- if (keyExpr) {
173
+ if (keyDirective) {
173
174
  const tempContext = createContext(scope, context)
174
- key = evaluate(keyExpr, tempContext)
175
+ key = evaluate(keyDirective, tempContext)
175
176
  }
176
177
 
177
178
  let node = oldNodesByKey.get(key)
@@ -323,11 +324,28 @@ function processEventBinding(
323
324
  const refs = evt.split('@').pop()!
324
325
  const [name, ...modifiers] = refs.split('.')
325
326
 
326
- el.addEventListener(name, ($event) => {
327
+ const isOutside = modifiers.includes('outside')
328
+ const target = isOutside ? document : el
329
+
330
+ const handler: EventListener = ($event: Event) => {
331
+ if (!el.isConnected) return
332
+
333
+ const isTargetNode = $event.target instanceof Node
334
+
335
+ if (isOutside && isTargetNode && el.contains($event.target)) return
336
+ if (modifiers.includes('self') && $event.target !== el) return
337
+
327
338
  if (modifiers.includes('prevent')) $event.preventDefault()
328
339
  if (modifiers.includes('stop')) $event.stopPropagation()
329
- if (modifiers.includes('self') && $event.target !== el) return
330
340
 
331
341
  evaluateEvent(expr, context, $event)
332
- })
342
+ }
343
+
344
+ target.addEventListener(name, handler)
345
+
346
+ if (isOutside) {
347
+ const bNode = el as BoundNode
348
+ bNode._effects ??= []
349
+ bNode._effects.push(() => target.removeEventListener(name, handler))
350
+ }
333
351
  }
package/src/reactivity.ts CHANGED
@@ -94,7 +94,8 @@ export function reactive<T extends object>(target: T): T {
94
94
  const isReactive = Object.hasOwn(target, '_isReactive')
95
95
  if (isReactive) return target
96
96
 
97
- if (proxyMap.has(target)) return proxyMap.get(target)
97
+ const existingProxy = proxyMap.get(target)
98
+ if (existingProxy) return existingProxy
98
99
 
99
100
  const proxy = new Proxy(target, {
100
101
  get(obj, key, receiver) {