@bookklik/senangstart-actions 0.1.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/README.md +58 -0
- package/dist/senangstart-actions.esm.js +992 -0
- package/dist/senangstart-actions.js +997 -0
- package/dist/senangstart-actions.min.js +9 -0
- package/package.json +46 -0
- package/src/evaluator.js +118 -0
- package/src/handlers/attributes.js +168 -0
- package/src/handlers/bind.js +46 -0
- package/src/handlers/directives.js +119 -0
- package/src/handlers/events.js +66 -0
- package/src/handlers/index.js +11 -0
- package/src/index.js +106 -0
- package/src/observer.js +42 -0
- package/src/reactive.js +210 -0
- package/src/walker.js +117 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
var SenangStart=function(){"use strict";const e=["push","pop","shift","unshift","splice","sort","reverse","fill","copyWithin"];let t=!1;const s=new Set;let r=null;function n(t,a,i){return new Proxy(t,{get(t,d){!r||"length"!==d&&isNaN(parseInt(d))||(i.has("__array__")||i.set("__array__",new Set),i.get("__array__").add(r));const l=t[d];return e.includes(d)&&"function"==typeof l?function(...e){const r=Array.prototype[d].apply(t,e);return i.has("__array__")&&i.get("__array__").forEach(e=>{s.add(e)}),c(a),r}:l&&"object"==typeof l?Array.isArray(l)?n(l,a,i):o(l,a,i):l},set:(e,t,r)=>(e[t]===r||(e[t]=r,i.has("__array__")&&i.get("__array__").forEach(e=>{s.add(e)}),c(a)),!0)})}function o(e,t,a){const i=new Proxy(e,{get(e,s){if("__subscribers"===s||"__isReactive"===s)return e[s];r&&(a.has(s)||a.set(s,new Set),a.get(s).add(r));const c=e[s];if("function"==typeof c)return c.bind(i);if(c&&"object"==typeof c){if(Array.isArray(c))return n(c,t,a);if(!c.__isReactive)return o(c,t,a)}return c},set:(e,r,n)=>(e[r]===n||(e[r]=n,a.has(r)&&a.get(r).forEach(e=>{s.add(e)}),c(t)),!0),deleteProperty:(e,r)=>(delete e[r],a.has(r)&&a.get(r).forEach(e=>{s.add(e)}),c(t),!0)});return i}function a(e,t){const s=new Map;let r;return r=Array.isArray(e)?n(e,t,s):o(e,t,s),r.__subscribers=s,r.__isReactive=!0,r}function i(e){r=e;try{e()}finally{r=null}}function c(e){t||(t=!0,queueMicrotask(()=>{t=!1;const r=[...s];s.clear(),r.forEach(e=>{try{i(e)}catch(e){console.error("[SenangStart] Effect error:",e)}}),e&&e()}))}function d(e,t,s){const{data:r,$refs:n,$store:o}=t,a={$data:r,$store:o,$el:s,$my:s,$refs:n,$dispatch:(e,t={})=>{s.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,cancelable:!0}))},$watch:(e,t)=>{const s=()=>{const s=r[e];t(s)};r.__subscribers&&(r.__subscribers.has(e)||r.__subscribers.set(e,new Set),r.__subscribers.get(e).add(s))},$nextTick:e=>{queueMicrotask(e)}},i=Object.keys("object"==typeof r&&null!==r?r:{}),c=Object.keys(a);try{const t=new Function(...i,...c,`with(this) { return (${e}); }`);return function(){const e=i.map(e=>r[e]),s=c.map(e=>a[e]);return t.call(r,...e,...s)}}catch(t){return console.error(`[SenangStart] Failed to parse expression: ${e}`,t),()=>{}}}function l(e,t,s){const{data:r,$refs:n,$store:o}=t,a={$data:r,$store:o,$el:s,$my:s,$refs:n,$dispatch:(e,t={})=>{s.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,cancelable:!0}))},$watch:(e,t)=>{r.__subscribers&&(r.__subscribers.has(e)||r.__subscribers.set(e,new Set),r.__subscribers.get(e).add(()=>t(r[e])))},$nextTick:e=>{queueMicrotask(e)}},i=Object.keys("object"==typeof r&&null!==r?r:{}),c=Object.keys(a);try{const t=new Function(...i,...c,`with(this) { ${e} }`);return function(){const e=i.map(e=>r[e]),s=c.map(e=>a[e]);return t.call(r,...e,...s)}}catch(t){return console.error(`[SenangStart] Failed to parse expression: ${e}`,t),()=>{}}}const u={"ss-text":(e,t,s)=>{i(()=>{const r=d(t,s,e);e.innerText=r()??""})},"ss-html":(e,t,s)=>{i(()=>{const r=d(t,s,e);e.innerHTML=r()??""})},"ss-show":(e,t,s)=>{const r=e.style.display||"";i(()=>{const n=!!d(t,s,e)();e.hasAttribute("ss-transition")?function(e,t,s){if(t){e.classList.add("ss-enter-from"),e.classList.add("ss-enter-active"),e.style.display=s,requestAnimationFrame(()=>{e.classList.remove("ss-enter-from"),e.classList.add("ss-enter-to")});const t=()=>{e.classList.remove("ss-enter-active","ss-enter-to"),e.removeEventListener("transitionend",t)};e.addEventListener("transitionend",t)}else{e.classList.add("ss-leave-from"),e.classList.add("ss-leave-active"),requestAnimationFrame(()=>{e.classList.remove("ss-leave-from"),e.classList.add("ss-leave-to")});const t=()=>{e.style.display="none",e.classList.remove("ss-leave-active","ss-leave-to"),e.removeEventListener("transitionend",t)};e.addEventListener("transitionend",t)}}(e,n,r):e.style.display=n?r:"none"})},"ss-model":(e,t,s)=>{const{data:r}=s,n="checkbox"===e.type,o="radio"===e.type;e.tagName;i(()=>{const r=d(t,s,e)();n?e.checked=!!r:o?e.checked=e.value===r:e.value=r??""});const a=n||o?"change":"input";e.addEventListener(a,()=>{let s;if(n)s=e.checked;else if(o){if(!e.checked)return;s=e.value}else s=e.value;r[t]=s})},"ss-ref":(e,t,s)=>{s.$refs[t]=e},"ss-init":(e,t,s)=>{l(t,s,e)()},"ss-effect":(e,t,s)=>{i(()=>{l(t,s,e)()})}};function f(e,t,s,r){const n=t.replace("ss-bind:","");i(()=>{const t=d(s,r,e)();"class"===n?"string"==typeof t?e.className=t:"object"==typeof t&&Object.entries(t).forEach(([t,s])=>{e.classList.toggle(t,!!s)}):"style"===n?"string"==typeof t?e.style.cssText=t:"object"==typeof t&&Object.assign(e.style,t):!1===t||null==t?e.removeAttribute(n):!0===t?e.setAttribute(n,""):e.setAttribute(n,t)})}function p(e,t,s,r){const n=t.replace("ss-on:","").split("."),o=n[0],a=n.slice(1),i=l(s,r,e),c=t=>{if(a.includes("prevent")&&t.preventDefault(),a.includes("stop")&&t.stopPropagation(),!a.includes("self")||t.target===e){if(a.includes("once")&&e.removeEventListener(o,c),t instanceof KeyboardEvent){const e=t.key.toLowerCase(),s=["enter","escape","tab","space","up","down","left","right"];if(a.some(e=>s.includes(e))){const t={enter:"enter",escape:"escape",tab:"tab",space:" ",up:"arrowup",down:"arrowdown",left:"arrowleft",right:"arrowright"};if(!a.some(s=>t[s]===e))return}}r.data.$event=t,i(),delete r.data.$event}};a.includes("window")?window.addEventListener(o,c):a.includes("document")?document.addEventListener(o,c):e.addEventListener(o,c)}let h=null;let y={},b={};function m(e,t=null){if(1!==e.nodeType)return;if(e.hasAttribute("ss-ignore"))return;let s=t;if(e.hasAttribute("ss-data")){const t=e.getAttribute("ss-data").trim();let r={};if(t)if(y[t])r=y[t]();else try{r=new Function(`return (${t})`)()}catch(e){console.error("[SenangStart] Failed to parse ss-data:",t,e)}s={data:a(r,()=>{}),$refs:{},$store:b},e.__ssScope=s}if(!s)return void Array.from(e.children).forEach(e=>m(e,null));if("TEMPLATE"===e.tagName&&e.hasAttribute("ss-for"))return void function(e,t,s){const r=t.match(/^\s*(?:\(([^,]+),\s*([^)]+)\)|([^\s]+))\s+in\s+(.+)$/);if(!r)return void console.error("[SenangStart] Invalid ss-for expression:",t);const n=r[1]||r[3],o=r[2]||"index",c=r[4],l=e.parentNode,u=document.createComment(`ss-for: ${t}`);l.insertBefore(u,e),e.remove();let f=[],p="";i(()=>{const t=d(c,s,e)()||[],r=JSON.stringify(t);r!==p&&(p=r,f.forEach(e=>e.remove()),f=[],t.forEach((t,r)=>{const i=e.content.cloneNode(!0),c=Array.from(i.childNodes).filter(e=>1===e.nodeType),d={data:a({...s.data,[n]:t,[o]:r},()=>{}),$refs:s.$refs,$store:s.$store,parentData:s.data};c.forEach(e=>{l.insertBefore(e,u),f.push(e),h&&h(e,d)})}))})}(e,e.getAttribute("ss-for"),s);if("TEMPLATE"===e.tagName&&e.hasAttribute("ss-if"))return void function(e,t,s){const r=e.parentNode,n=document.createComment(`ss-if: ${t}`);r.insertBefore(n,e),e.remove();let o=[];i(()=>{const a=!!d(t,s,e)();if(o.forEach(e=>e.remove()),o=[],a){const t=e.content.cloneNode(!0);Array.from(t.childNodes).filter(e=>1===e.nodeType).forEach(e=>{r.insertBefore(e,n),o.push(e),h&&h(e,s)})}})}(e,e.getAttribute("ss-if"),s);const r=Array.from(e.attributes);for(const t of r){const r=t.name,n=t.value;"ss-data"!==r&&"ss-describe"!==r&&(u[r]?u[r](e,n,s):r.startsWith("ss-bind:")?f(e,r,n,s):r.startsWith("ss-on:")&&p(e,r,n,s))}e.hasAttribute("ss-cloak")&&e.removeAttribute("ss-cloak"),Array.from(e.children).forEach(e=>m(e,s))}function v(){const e=new MutationObserver(e=>{for(const t of e)for(const e of t.addedNodes)if(1===e.nodeType){let t=e,s=null;for(;t.parentElement;)if(t=t.parentElement,t.__ssScope){s=t.__ssScope;break}m(e,s)}});return e.observe(document.body,{childList:!0,subtree:!0}),e}
|
|
2
|
+
/**
|
|
3
|
+
* SenangStart Actions v0.1.0
|
|
4
|
+
* Declarative UI framework for humans and AI agents
|
|
5
|
+
*
|
|
6
|
+
* @license MIT
|
|
7
|
+
* @author Bookklik
|
|
8
|
+
* @module senangstart-actions
|
|
9
|
+
*/h=m;const _=document.createElement("style");_.textContent="[ss-cloak] { display: none !important; }",document.head.appendChild(_);const g={},$={};y=g,b=$;const E={data(e,t){return"function"!=typeof t?(console.error("[SenangStart] data() requires a factory function"),this):(g[e]=t,this)},store(e,t){return"object"!=typeof t?(console.error("[SenangStart] store() requires an object"),this):($[e]=a(t,()=>{}),this)},init(e=document.body){return m(e,null),this},start(){return"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{this.init(),v()}):(this.init(),v()),this},version:"0.1.0"};return"undefined"!=typeof window&&(window.SenangStart=E),E.start(),E}();
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bookklik/senangstart-actions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Declarative UI framework for humans and AI agents",
|
|
5
|
+
"main": "dist/senangstart-actions.js",
|
|
6
|
+
"module": "dist/senangstart-actions.esm.js",
|
|
7
|
+
"unpkg": "dist/senangstart-actions.min.js",
|
|
8
|
+
"jsdelivr": "dist/senangstart-actions.min.js",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "rollup -c",
|
|
16
|
+
"dev": "npx -y serve .",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"test:coverage": "vitest run --coverage",
|
|
20
|
+
"docs:dev": "vitepress dev docs",
|
|
21
|
+
"docs:build": "vitepress build docs",
|
|
22
|
+
"docs:preview": "vitepress preview docs"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"javascript",
|
|
26
|
+
"framework",
|
|
27
|
+
"reactive",
|
|
28
|
+
"declarative",
|
|
29
|
+
"ai-friendly",
|
|
30
|
+
"lightweight"
|
|
31
|
+
],
|
|
32
|
+
"author": "a-hakim",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/bookklik-technologies/senangstart-actions"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
40
|
+
"@vitest/coverage-v8": "^2.1.0",
|
|
41
|
+
"jsdom": "^25.0.0",
|
|
42
|
+
"rollup": "^4.9.0",
|
|
43
|
+
"vitepress": "^1.5.0",
|
|
44
|
+
"vitest": "^2.1.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/evaluator.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart Actions - Expression Evaluator
|
|
3
|
+
* Safe evaluation of expressions within component scope
|
|
4
|
+
*
|
|
5
|
+
* @module evaluator
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a function to evaluate an expression within a component scope
|
|
10
|
+
*/
|
|
11
|
+
export function createEvaluator(expression, scope, element) {
|
|
12
|
+
const { data, $refs, $store } = scope;
|
|
13
|
+
|
|
14
|
+
// Magic properties
|
|
15
|
+
const magics = {
|
|
16
|
+
$data: data,
|
|
17
|
+
$store: $store,
|
|
18
|
+
$el: element,
|
|
19
|
+
$my: element,
|
|
20
|
+
$refs: $refs,
|
|
21
|
+
$dispatch: (name, detail = {}) => {
|
|
22
|
+
element.dispatchEvent(new CustomEvent(name, {
|
|
23
|
+
detail,
|
|
24
|
+
bubbles: true,
|
|
25
|
+
cancelable: true
|
|
26
|
+
}));
|
|
27
|
+
},
|
|
28
|
+
$watch: (prop, callback) => {
|
|
29
|
+
const watchEffect = () => {
|
|
30
|
+
const value = data[prop];
|
|
31
|
+
callback(value);
|
|
32
|
+
};
|
|
33
|
+
if (data.__subscribers) {
|
|
34
|
+
if (!data.__subscribers.has(prop)) {
|
|
35
|
+
data.__subscribers.set(prop, new Set());
|
|
36
|
+
}
|
|
37
|
+
data.__subscribers.get(prop).add(watchEffect);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
$nextTick: (fn) => {
|
|
41
|
+
queueMicrotask(fn);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Build the function with data properties spread as local variables
|
|
46
|
+
const dataKeys = Object.keys(typeof data === 'object' && data !== null ? data : {});
|
|
47
|
+
const magicKeys = Object.keys(magics);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const fn = new Function(
|
|
51
|
+
...dataKeys,
|
|
52
|
+
...magicKeys,
|
|
53
|
+
`with(this) { return (${expression}); }`
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return function() {
|
|
57
|
+
const dataValues = dataKeys.map(k => data[k]);
|
|
58
|
+
const magicValues = magicKeys.map(k => magics[k]);
|
|
59
|
+
return fn.call(data, ...dataValues, ...magicValues);
|
|
60
|
+
};
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(`[SenangStart] Failed to parse expression: ${expression}`, e);
|
|
63
|
+
return () => undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a function to execute a statement (not return value)
|
|
69
|
+
*/
|
|
70
|
+
export function createExecutor(expression, scope, element) {
|
|
71
|
+
const { data, $refs, $store } = scope;
|
|
72
|
+
|
|
73
|
+
const magics = {
|
|
74
|
+
$data: data,
|
|
75
|
+
$store: $store,
|
|
76
|
+
$el: element,
|
|
77
|
+
$my: element,
|
|
78
|
+
$refs: $refs,
|
|
79
|
+
$dispatch: (name, detail = {}) => {
|
|
80
|
+
element.dispatchEvent(new CustomEvent(name, {
|
|
81
|
+
detail,
|
|
82
|
+
bubbles: true,
|
|
83
|
+
cancelable: true
|
|
84
|
+
}));
|
|
85
|
+
},
|
|
86
|
+
$watch: (prop, callback) => {
|
|
87
|
+
if (data.__subscribers) {
|
|
88
|
+
if (!data.__subscribers.has(prop)) {
|
|
89
|
+
data.__subscribers.set(prop, new Set());
|
|
90
|
+
}
|
|
91
|
+
data.__subscribers.get(prop).add(() => callback(data[prop]));
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
$nextTick: (fn) => {
|
|
95
|
+
queueMicrotask(fn);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const dataKeys = Object.keys(typeof data === 'object' && data !== null ? data : {});
|
|
100
|
+
const magicKeys = Object.keys(magics);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const fn = new Function(
|
|
104
|
+
...dataKeys,
|
|
105
|
+
...magicKeys,
|
|
106
|
+
`with(this) { ${expression} }`
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return function() {
|
|
110
|
+
const dataValues = dataKeys.map(k => data[k]);
|
|
111
|
+
const magicValues = magicKeys.map(k => magics[k]);
|
|
112
|
+
return fn.call(data, ...dataValues, ...magicValues);
|
|
113
|
+
};
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error(`[SenangStart] Failed to parse expression: ${expression}`, e);
|
|
116
|
+
return () => {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart Actions - Attribute Handlers
|
|
3
|
+
* Handlers for basic ss-* attributes
|
|
4
|
+
*
|
|
5
|
+
* @module handlers/attributes
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createEvaluator, createExecutor } from '../evaluator.js';
|
|
9
|
+
import { runEffect } from '../reactive.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle ss-transition animations
|
|
13
|
+
*/
|
|
14
|
+
export function handleTransition(el, show, originalDisplay) {
|
|
15
|
+
if (show) {
|
|
16
|
+
// Enter transition
|
|
17
|
+
el.classList.add('ss-enter-from');
|
|
18
|
+
el.classList.add('ss-enter-active');
|
|
19
|
+
el.style.display = originalDisplay;
|
|
20
|
+
|
|
21
|
+
requestAnimationFrame(() => {
|
|
22
|
+
el.classList.remove('ss-enter-from');
|
|
23
|
+
el.classList.add('ss-enter-to');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const onEnd = () => {
|
|
27
|
+
el.classList.remove('ss-enter-active', 'ss-enter-to');
|
|
28
|
+
el.removeEventListener('transitionend', onEnd);
|
|
29
|
+
};
|
|
30
|
+
el.addEventListener('transitionend', onEnd);
|
|
31
|
+
} else {
|
|
32
|
+
// Leave transition
|
|
33
|
+
el.classList.add('ss-leave-from');
|
|
34
|
+
el.classList.add('ss-leave-active');
|
|
35
|
+
|
|
36
|
+
requestAnimationFrame(() => {
|
|
37
|
+
el.classList.remove('ss-leave-from');
|
|
38
|
+
el.classList.add('ss-leave-to');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const onEnd = () => {
|
|
42
|
+
el.style.display = 'none';
|
|
43
|
+
el.classList.remove('ss-leave-active', 'ss-leave-to');
|
|
44
|
+
el.removeEventListener('transitionend', onEnd);
|
|
45
|
+
};
|
|
46
|
+
el.addEventListener('transitionend', onEnd);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Attribute handlers map
|
|
52
|
+
*/
|
|
53
|
+
export const attributeHandlers = {
|
|
54
|
+
/**
|
|
55
|
+
* ss-text: Set element's innerText
|
|
56
|
+
*/
|
|
57
|
+
'ss-text': (el, expr, scope) => {
|
|
58
|
+
const update = () => {
|
|
59
|
+
const evaluator = createEvaluator(expr, scope, el);
|
|
60
|
+
el.innerText = evaluator() ?? '';
|
|
61
|
+
};
|
|
62
|
+
runEffect(update);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* ss-html: Set element's innerHTML
|
|
67
|
+
*/
|
|
68
|
+
'ss-html': (el, expr, scope) => {
|
|
69
|
+
const update = () => {
|
|
70
|
+
const evaluator = createEvaluator(expr, scope, el);
|
|
71
|
+
el.innerHTML = evaluator() ?? '';
|
|
72
|
+
};
|
|
73
|
+
runEffect(update);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* ss-show: Toggle visibility
|
|
78
|
+
*/
|
|
79
|
+
'ss-show': (el, expr, scope) => {
|
|
80
|
+
const originalDisplay = el.style.display || '';
|
|
81
|
+
|
|
82
|
+
const update = () => {
|
|
83
|
+
const evaluator = createEvaluator(expr, scope, el);
|
|
84
|
+
const show = !!evaluator();
|
|
85
|
+
|
|
86
|
+
if (el.hasAttribute('ss-transition')) {
|
|
87
|
+
handleTransition(el, show, originalDisplay);
|
|
88
|
+
} else {
|
|
89
|
+
el.style.display = show ? originalDisplay : 'none';
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
runEffect(update);
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* ss-model: Two-way binding for inputs
|
|
97
|
+
*/
|
|
98
|
+
'ss-model': (el, expr, scope) => {
|
|
99
|
+
const { data } = scope;
|
|
100
|
+
|
|
101
|
+
// Determine input type
|
|
102
|
+
const isCheckbox = el.type === 'checkbox';
|
|
103
|
+
const isRadio = el.type === 'radio';
|
|
104
|
+
const isSelect = el.tagName === 'SELECT';
|
|
105
|
+
|
|
106
|
+
// Set initial value
|
|
107
|
+
const setInitialValue = () => {
|
|
108
|
+
const evaluator = createEvaluator(expr, scope, el);
|
|
109
|
+
const value = evaluator();
|
|
110
|
+
|
|
111
|
+
if (isCheckbox) {
|
|
112
|
+
el.checked = !!value;
|
|
113
|
+
} else if (isRadio) {
|
|
114
|
+
el.checked = el.value === value;
|
|
115
|
+
} else if (isSelect) {
|
|
116
|
+
el.value = value ?? '';
|
|
117
|
+
} else {
|
|
118
|
+
el.value = value ?? '';
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
runEffect(setInitialValue);
|
|
123
|
+
|
|
124
|
+
// Listen for changes
|
|
125
|
+
const eventType = isCheckbox || isRadio ? 'change' : 'input';
|
|
126
|
+
el.addEventListener(eventType, () => {
|
|
127
|
+
let newValue;
|
|
128
|
+
|
|
129
|
+
if (isCheckbox) {
|
|
130
|
+
newValue = el.checked;
|
|
131
|
+
} else if (isRadio) {
|
|
132
|
+
if (el.checked) newValue = el.value;
|
|
133
|
+
else return; // Don't update if not checked
|
|
134
|
+
} else {
|
|
135
|
+
newValue = el.value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Set the value on the data object
|
|
139
|
+
data[expr] = newValue;
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* ss-ref: Register element reference
|
|
145
|
+
*/
|
|
146
|
+
'ss-ref': (el, name, scope) => {
|
|
147
|
+
scope.$refs[name] = el;
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* ss-init: Run initialization code
|
|
152
|
+
*/
|
|
153
|
+
'ss-init': (el, expr, scope) => {
|
|
154
|
+
const executor = createExecutor(expr, scope, el);
|
|
155
|
+
executor();
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* ss-effect: Run reactive effect
|
|
160
|
+
*/
|
|
161
|
+
'ss-effect': (el, expr, scope) => {
|
|
162
|
+
const update = () => {
|
|
163
|
+
const executor = createExecutor(expr, scope, el);
|
|
164
|
+
executor();
|
|
165
|
+
};
|
|
166
|
+
runEffect(update);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart Actions - Bind Handler
|
|
3
|
+
* Handler for ss-bind:[attr] dynamic attribute binding
|
|
4
|
+
*
|
|
5
|
+
* @module handlers/bind
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createEvaluator } from '../evaluator.js';
|
|
9
|
+
import { runEffect } from '../reactive.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle ss-bind:[attr] dynamically
|
|
13
|
+
*/
|
|
14
|
+
export function handleBind(el, attrName, expr, scope) {
|
|
15
|
+
const attr = attrName.replace('ss-bind:', '');
|
|
16
|
+
|
|
17
|
+
const update = () => {
|
|
18
|
+
const evaluator = createEvaluator(expr, scope, el);
|
|
19
|
+
const value = evaluator();
|
|
20
|
+
|
|
21
|
+
if (attr === 'class') {
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
el.className = value;
|
|
24
|
+
} else if (typeof value === 'object') {
|
|
25
|
+
// Object syntax: { 'class-name': condition }
|
|
26
|
+
Object.entries(value).forEach(([className, condition]) => {
|
|
27
|
+
el.classList.toggle(className, !!condition);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
} else if (attr === 'style') {
|
|
31
|
+
if (typeof value === 'string') {
|
|
32
|
+
el.style.cssText = value;
|
|
33
|
+
} else if (typeof value === 'object') {
|
|
34
|
+
Object.assign(el.style, value);
|
|
35
|
+
}
|
|
36
|
+
} else if (value === false || value === null || value === undefined) {
|
|
37
|
+
el.removeAttribute(attr);
|
|
38
|
+
} else if (value === true) {
|
|
39
|
+
el.setAttribute(attr, '');
|
|
40
|
+
} else {
|
|
41
|
+
el.setAttribute(attr, value);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
runEffect(update);
|
|
46
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart Actions - Directive Handlers
|
|
3
|
+
* Handlers for ss-for and ss-if template directives
|
|
4
|
+
*
|
|
5
|
+
* @module handlers/directives
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createEvaluator } from '../evaluator.js';
|
|
9
|
+
import { createReactive, runEffect } from '../reactive.js';
|
|
10
|
+
|
|
11
|
+
// Forward declaration - will be set by walker.js
|
|
12
|
+
let walkFn = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Set the walk function reference (to avoid circular imports)
|
|
16
|
+
*/
|
|
17
|
+
export function setWalkFunction(fn) {
|
|
18
|
+
walkFn = fn;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handle ss-for directive
|
|
23
|
+
*/
|
|
24
|
+
export function handleFor(templateEl, expr, scope) {
|
|
25
|
+
// Parse expression: "item in items" or "(item, index) in items"
|
|
26
|
+
const match = expr.match(/^\s*(?:\(([^,]+),\s*([^)]+)\)|([^\s]+))\s+in\s+(.+)$/);
|
|
27
|
+
if (!match) {
|
|
28
|
+
console.error('[SenangStart] Invalid ss-for expression:', expr);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const itemName = match[1] || match[3];
|
|
33
|
+
const indexName = match[2] || 'index';
|
|
34
|
+
const arrayExpr = match[4];
|
|
35
|
+
|
|
36
|
+
const parent = templateEl.parentNode;
|
|
37
|
+
const anchor = document.createComment(`ss-for: ${expr}`);
|
|
38
|
+
parent.insertBefore(anchor, templateEl);
|
|
39
|
+
templateEl.remove();
|
|
40
|
+
|
|
41
|
+
let currentNodes = [];
|
|
42
|
+
let lastItemsJSON = '';
|
|
43
|
+
|
|
44
|
+
const update = () => {
|
|
45
|
+
const evaluator = createEvaluator(arrayExpr, scope, templateEl);
|
|
46
|
+
const items = evaluator() || [];
|
|
47
|
+
|
|
48
|
+
// Check if items actually changed (shallow comparison)
|
|
49
|
+
const itemsJSON = JSON.stringify(items);
|
|
50
|
+
if (itemsJSON === lastItemsJSON) {
|
|
51
|
+
return; // No change, skip re-render
|
|
52
|
+
}
|
|
53
|
+
lastItemsJSON = itemsJSON;
|
|
54
|
+
|
|
55
|
+
// Remove old nodes
|
|
56
|
+
currentNodes.forEach(node => node.remove());
|
|
57
|
+
currentNodes = [];
|
|
58
|
+
|
|
59
|
+
// Create new nodes
|
|
60
|
+
items.forEach((item, index) => {
|
|
61
|
+
const clone = templateEl.content.cloneNode(true);
|
|
62
|
+
const nodes = Array.from(clone.childNodes).filter(n => n.nodeType === 1);
|
|
63
|
+
|
|
64
|
+
// Create child scope with item and index - use parent scope's data for non-item properties
|
|
65
|
+
const itemScope = {
|
|
66
|
+
data: createReactive({
|
|
67
|
+
...scope.data,
|
|
68
|
+
[itemName]: item,
|
|
69
|
+
[indexName]: index
|
|
70
|
+
}, () => {}),
|
|
71
|
+
$refs: scope.$refs,
|
|
72
|
+
$store: scope.$store,
|
|
73
|
+
parentData: scope.data // Keep reference to parent data
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
nodes.forEach(node => {
|
|
77
|
+
parent.insertBefore(node, anchor);
|
|
78
|
+
currentNodes.push(node);
|
|
79
|
+
if (walkFn) walkFn(node, itemScope);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
runEffect(update);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Handle ss-if directive
|
|
89
|
+
*/
|
|
90
|
+
export function handleIf(templateEl, expr, scope) {
|
|
91
|
+
const parent = templateEl.parentNode;
|
|
92
|
+
const anchor = document.createComment(`ss-if: ${expr}`);
|
|
93
|
+
parent.insertBefore(anchor, templateEl);
|
|
94
|
+
templateEl.remove();
|
|
95
|
+
|
|
96
|
+
let currentNodes = [];
|
|
97
|
+
|
|
98
|
+
const update = () => {
|
|
99
|
+
const evaluator = createEvaluator(expr, scope, templateEl);
|
|
100
|
+
const condition = !!evaluator();
|
|
101
|
+
|
|
102
|
+
// Remove old nodes
|
|
103
|
+
currentNodes.forEach(node => node.remove());
|
|
104
|
+
currentNodes = [];
|
|
105
|
+
|
|
106
|
+
if (condition) {
|
|
107
|
+
const clone = templateEl.content.cloneNode(true);
|
|
108
|
+
const nodes = Array.from(clone.childNodes).filter(n => n.nodeType === 1);
|
|
109
|
+
|
|
110
|
+
nodes.forEach(node => {
|
|
111
|
+
parent.insertBefore(node, anchor);
|
|
112
|
+
currentNodes.push(node);
|
|
113
|
+
if (walkFn) walkFn(node, scope);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
runEffect(update);
|
|
119
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart Actions - Event Handler
|
|
3
|
+
* Handler for ss-on:[event] with modifiers
|
|
4
|
+
*
|
|
5
|
+
* @module handlers/events
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createExecutor } from '../evaluator.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handle ss-on:[event] dynamically
|
|
12
|
+
*/
|
|
13
|
+
export function handleEvent(el, attrName, expr, scope) {
|
|
14
|
+
const parts = attrName.replace('ss-on:', '').split('.');
|
|
15
|
+
const eventName = parts[0];
|
|
16
|
+
const modifiers = parts.slice(1);
|
|
17
|
+
|
|
18
|
+
const executor = createExecutor(expr, scope, el);
|
|
19
|
+
|
|
20
|
+
const handler = (event) => {
|
|
21
|
+
// Handle modifiers
|
|
22
|
+
if (modifiers.includes('prevent')) event.preventDefault();
|
|
23
|
+
if (modifiers.includes('stop')) event.stopPropagation();
|
|
24
|
+
if (modifiers.includes('self') && event.target !== el) return;
|
|
25
|
+
if (modifiers.includes('once')) {
|
|
26
|
+
el.removeEventListener(eventName, handler);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// For keyboard events, check key modifiers
|
|
30
|
+
if (event instanceof KeyboardEvent) {
|
|
31
|
+
const key = event.key.toLowerCase();
|
|
32
|
+
const keyModifiers = ['enter', 'escape', 'tab', 'space', 'up', 'down', 'left', 'right'];
|
|
33
|
+
const hasKeyModifier = modifiers.some(m => keyModifiers.includes(m));
|
|
34
|
+
|
|
35
|
+
if (hasKeyModifier) {
|
|
36
|
+
const keyMap = {
|
|
37
|
+
'enter': 'enter',
|
|
38
|
+
'escape': 'escape',
|
|
39
|
+
'tab': 'tab',
|
|
40
|
+
'space': ' ',
|
|
41
|
+
'up': 'arrowup',
|
|
42
|
+
'down': 'arrowdown',
|
|
43
|
+
'left': 'arrowleft',
|
|
44
|
+
'right': 'arrowright'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const shouldFire = modifiers.some(m => keyMap[m] === key);
|
|
48
|
+
if (!shouldFire) return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Execute the expression with $event available
|
|
53
|
+
scope.data.$event = event;
|
|
54
|
+
executor();
|
|
55
|
+
delete scope.data.$event;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Special window/document events
|
|
59
|
+
if (modifiers.includes('window')) {
|
|
60
|
+
window.addEventListener(eventName, handler);
|
|
61
|
+
} else if (modifiers.includes('document')) {
|
|
62
|
+
document.addEventListener(eventName, handler);
|
|
63
|
+
} else {
|
|
64
|
+
el.addEventListener(eventName, handler);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart Actions - Handlers Index
|
|
3
|
+
* Re-exports all handler modules
|
|
4
|
+
*
|
|
5
|
+
* @module handlers
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { attributeHandlers, handleTransition } from './attributes.js';
|
|
9
|
+
export { handleBind } from './bind.js';
|
|
10
|
+
export { handleEvent } from './events.js';
|
|
11
|
+
export { handleFor, handleIf, setWalkFunction } from './directives.js';
|