enigmatic 0.27.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLIENT_JS_DOCS.md +322 -0
- package/__tests__/e2.test.js +270 -52
- package/__tests__/jest.config.js +7 -0
- package/{jest.setup.js → __tests__/jest.setup.js} +0 -1
- package/bun-server.js +130 -0
- package/package.json +12 -12
- package/public/client.js +127 -0
- package/public/custom.js +29 -0
- package/public/index.html +45 -0
- package/README.md +0 -218
- package/__tests__/enigmatic.test.js +0 -328
- package/components.js +0 -58
- package/e2.js +0 -38
- package/enigmatic.js +0 -248
- package/index.html +0 -34
- package/jest.config.js +0 -7
- /package/{enigmatic.css → public/client.css} +0 -0
- /package/{theme.css → public/theme.css} +0 -0
package/bun-server.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { S3Client } from "bun";
|
|
2
|
+
|
|
3
|
+
const dbPath = "db.json";
|
|
4
|
+
const db = await Bun.file(dbPath).json().catch(() => ({}));
|
|
5
|
+
const s3 = new S3Client({
|
|
6
|
+
accessKeyId: Bun.env.CLOUDFLARE_ACCESS_KEY_ID,
|
|
7
|
+
secretAccessKey: Bun.env.CLOUDFLARE_SECRET_ACCESS_KEY,
|
|
8
|
+
bucket: Bun.env.CLOUDFLARE_BUCKET_NAME,
|
|
9
|
+
endpoint: Bun.env.CLOUDFLARE_PUBLIC_URL
|
|
10
|
+
});
|
|
11
|
+
const json = (data, status = 200, extraHeaders = {}) => new Response(JSON.stringify(data), {
|
|
12
|
+
status,
|
|
13
|
+
headers: {
|
|
14
|
+
"Access-Control-Allow-Origin": "*",
|
|
15
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PURGE, PROPFIND, DOWNLOAD, OPTIONS",
|
|
16
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, Cookie, X-HTTP-Method-Override",
|
|
17
|
+
"Access-Control-Allow-Credentials": "true",
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
...extraHeaders
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const writeDb = () => Bun.write(dbPath, JSON.stringify(db, null, 2));
|
|
23
|
+
const redirect = (url, cookie = null) => new Response(null, {
|
|
24
|
+
status: 302,
|
|
25
|
+
headers: { Location: url, ...(cookie && { "Set-Cookie": cookie }) }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
async fetch(req) {
|
|
30
|
+
console.log('req.method', req.method);
|
|
31
|
+
const url = new URL(req.url);
|
|
32
|
+
const key = url.pathname.slice(1);
|
|
33
|
+
const cb = `${url.origin}/callback`;
|
|
34
|
+
const token = req.headers.get("Cookie")?.match(/token=([^;]+)/)?.[1];
|
|
35
|
+
const user = token ? db[`session:${token}`] : null;
|
|
36
|
+
|
|
37
|
+
if (req.method === "OPTIONS") return json(null, 204);
|
|
38
|
+
|
|
39
|
+
if (url.pathname === '/login') {
|
|
40
|
+
return Response.redirect(`https://${Bun.env.AUTH0_DOMAIN}/authorize?${new URLSearchParams({
|
|
41
|
+
response_type: "code",
|
|
42
|
+
client_id: Bun.env.AUTH0_CLIENT_ID,
|
|
43
|
+
redirect_uri: cb,
|
|
44
|
+
scope: "openid email profile"
|
|
45
|
+
})}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (url.pathname === '/callback') {
|
|
49
|
+
const code = url.searchParams.get("code");
|
|
50
|
+
if (!code) return json({ error: 'No code' }, 400);
|
|
51
|
+
const tRes = await fetch(`https://${Bun.env.AUTH0_DOMAIN}/oauth/token`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { "content-type": "application/json" },
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
grant_type: "authorization_code",
|
|
56
|
+
client_id: Bun.env.AUTH0_CLIENT_ID,
|
|
57
|
+
client_secret: Bun.env.AUTH0_CLIENT_SECRET,
|
|
58
|
+
code,
|
|
59
|
+
redirect_uri: cb
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
if (!tRes.ok) return json({ error: 'Auth error' }, 401);
|
|
63
|
+
const tokens = await tRes.json();
|
|
64
|
+
const userInfo = await (await fetch(`https://${Bun.env.AUTH0_DOMAIN}/userinfo`, {
|
|
65
|
+
headers: { Authorization: `Bearer ${tokens.access_token}` }
|
|
66
|
+
})).json();
|
|
67
|
+
const session = crypto.randomUUID();
|
|
68
|
+
db[`session:${session}`] = {
|
|
69
|
+
...userInfo,
|
|
70
|
+
login_time: new Date().toISOString(),
|
|
71
|
+
access_token_expires_at: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000).toISOString() : null
|
|
72
|
+
};
|
|
73
|
+
await writeDb();
|
|
74
|
+
return redirect(url.origin, `token=${session}; HttpOnly; Path=/; Secure; SameSite=Lax; Max-Age=86400`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!token || !user) return json({ error: 'Unauthorized' }, 401);
|
|
78
|
+
|
|
79
|
+
if (url.pathname === '/logout') {
|
|
80
|
+
delete db[`session:${token}`];
|
|
81
|
+
await writeDb();
|
|
82
|
+
return redirect(url.origin, "token=; Max-Age=0; Path=/");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const files = { '/': 'index.html', '/client.js': 'client.js', '/custom.js': 'custom.js' };
|
|
86
|
+
if (req.method === 'GET' && files[url.pathname]) return new Response(Bun.file(`./public/${files[url.pathname]}`));
|
|
87
|
+
|
|
88
|
+
console.log(req.method);
|
|
89
|
+
switch (req.method) {
|
|
90
|
+
case 'GET': return json(db[key]);
|
|
91
|
+
case 'POST':
|
|
92
|
+
const value = await req.text();
|
|
93
|
+
db[key] = (() => { try { return JSON.parse(value); } catch { return value; } })();
|
|
94
|
+
await writeDb();
|
|
95
|
+
return json({ key, value });
|
|
96
|
+
case 'DELETE':
|
|
97
|
+
delete db[key];
|
|
98
|
+
await writeDb();
|
|
99
|
+
return json({ status: "Deleted" });
|
|
100
|
+
case 'PUT':
|
|
101
|
+
await s3.write(`${user.sub}/${key}`, req.body);
|
|
102
|
+
return json({ status: "Saved to R2" });
|
|
103
|
+
case 'PURGE':
|
|
104
|
+
await s3.delete(`${user.sub}/${key}`);
|
|
105
|
+
return json({ status: "Deleted from R2" });
|
|
106
|
+
case 'PROPFIND':
|
|
107
|
+
const list = await s3.list({ prefix: `${user.sub}/` });
|
|
108
|
+
const items = Array.isArray(list) ? list : (list?.contents || []);
|
|
109
|
+
return json(items.map(item => ({
|
|
110
|
+
name: item.key?.split('/').pop() || item.name || item.Key,
|
|
111
|
+
lastModified: item.lastModified || item.LastModified,
|
|
112
|
+
size: item.size || item.Size || 0
|
|
113
|
+
})));
|
|
114
|
+
case 'PATCH':
|
|
115
|
+
try {
|
|
116
|
+
const exists = await s3.exists(`${user.sub}/${key}`);
|
|
117
|
+
if (!exists) return json({ error: 'File not found' }, 404);
|
|
118
|
+
const file = await s3.file(`${user.sub}/${key}`);
|
|
119
|
+
if (!file) return json({ error: 'File not found' }, 404);
|
|
120
|
+
return new Response(file.stream(), { headers: file.headers });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.error('Download error:', err);
|
|
123
|
+
return json({ error: 'File not found', details: err.message }, 404);
|
|
124
|
+
}
|
|
125
|
+
default: return json({ error: 'Method not allowed' }, 405);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
port: 3000,
|
|
129
|
+
tls: { cert: Bun.file("cert.pem"), key: Bun.file("key.pem") }
|
|
130
|
+
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
"name": "enigmatic",
|
|
3
|
+
"version": "0.29.1",
|
|
4
|
+
"unpkg": "./public/client.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "bun --hot ./bun-server.js",
|
|
7
|
+
"test": "jest --config __tests__/jest.config.js"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"jest": "^29.0.0",
|
|
11
|
+
"jest-environment-jsdom": "^29.0.0",
|
|
12
|
+
"whatwg-fetch": "^3.6.20"
|
|
13
|
+
}
|
|
14
14
|
}
|
package/public/client.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
window.api_url = "https://localhost:3000"
|
|
2
|
+
window.$ = document.querySelector.bind(document)
|
|
3
|
+
window.$$ = document.querySelectorAll.bind(document)
|
|
4
|
+
window.$c = (selector) => $0.closest(selector);
|
|
5
|
+
|
|
6
|
+
// Initialize custom elements
|
|
7
|
+
window.initCustomElements = function() {
|
|
8
|
+
Object.keys(window.custom || {}).forEach(tagName => {
|
|
9
|
+
$$(tagName).forEach(async el => {
|
|
10
|
+
const f = window.custom[tagName];
|
|
11
|
+
if (typeof f === 'function') {
|
|
12
|
+
el.innerHTML = await f();
|
|
13
|
+
} else if (f && typeof f.render === 'function') {
|
|
14
|
+
el.innerHTML = f.render();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Make window.custom a Proxy that auto-initializes when properties are added
|
|
21
|
+
if (!window.custom) window.custom = {};
|
|
22
|
+
const customProxy = new Proxy(window.custom, {
|
|
23
|
+
set(target, prop, value) {
|
|
24
|
+
target[prop] = value;
|
|
25
|
+
if (typeof value === 'function' || (value && typeof value.render === 'function')) {
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
$$(prop).forEach(async el => {
|
|
28
|
+
if (typeof value === 'function') {
|
|
29
|
+
el.innerHTML = await value();
|
|
30
|
+
} else {
|
|
31
|
+
el.innerHTML = value.render();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}, 0);
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(window, 'custom', {
|
|
40
|
+
get: () => customProxy,
|
|
41
|
+
set: (val) => {
|
|
42
|
+
Object.assign(customProxy, val || {});
|
|
43
|
+
},
|
|
44
|
+
configurable: true
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
window.state = new Proxy({}, {
|
|
48
|
+
set(obj, prop, value) {
|
|
49
|
+
obj[prop] = value
|
|
50
|
+
$$(`[data="${prop}"]`).forEach(el => {
|
|
51
|
+
console.log('setting', el.tagName);
|
|
52
|
+
const f = window.custom?.[el.tagName.toLowerCase()];
|
|
53
|
+
if (!f) return;
|
|
54
|
+
if(typeof f === 'function') {
|
|
55
|
+
el.innerHTML = f(value);
|
|
56
|
+
} else if (f && typeof f.render === 'function') {
|
|
57
|
+
el.innerHTML = f.render(value);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
window.get = async function(key) {
|
|
64
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`)
|
|
65
|
+
return await res.json()
|
|
66
|
+
}
|
|
67
|
+
window.set = async function(key, value) {
|
|
68
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
|
|
69
|
+
method: 'POST', body: typeof value === 'string' ? value : JSON.stringify(value)
|
|
70
|
+
})
|
|
71
|
+
return await res.json()
|
|
72
|
+
}
|
|
73
|
+
window.delete = async function(key) {
|
|
74
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
|
|
75
|
+
method: 'DELETE'
|
|
76
|
+
})
|
|
77
|
+
return await res.json()
|
|
78
|
+
}
|
|
79
|
+
window.put = async function(key, body) {
|
|
80
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
|
|
81
|
+
method: 'PUT', body: body instanceof Blob ? body : typeof body === 'string' ? body : JSON.stringify(body)
|
|
82
|
+
})
|
|
83
|
+
return await res.json()
|
|
84
|
+
}
|
|
85
|
+
window.purge = async function(key) {
|
|
86
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
|
|
87
|
+
method: 'PURGE'
|
|
88
|
+
})
|
|
89
|
+
return await res.json()
|
|
90
|
+
}
|
|
91
|
+
window.list = async function() {
|
|
92
|
+
const res = await fetch(`${window.api_url}`, {
|
|
93
|
+
method: 'PROPFIND'
|
|
94
|
+
})
|
|
95
|
+
return await res.json()
|
|
96
|
+
}
|
|
97
|
+
window.download = async function(key) {
|
|
98
|
+
try {
|
|
99
|
+
console.log('Downloading with method DOWNLOAD:', key);
|
|
100
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, { method: 'PATCH' });
|
|
101
|
+
console.log('Response:', key, res.status, res.statusText);
|
|
102
|
+
if (!res.ok) throw new Error('Download failed');
|
|
103
|
+
const blob = await res.blob();
|
|
104
|
+
const url = URL.createObjectURL(blob);
|
|
105
|
+
const a = document.createElement('a');
|
|
106
|
+
a.href = url;
|
|
107
|
+
a.download = key;
|
|
108
|
+
a.click();
|
|
109
|
+
URL.revokeObjectURL(url);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error('Download error:', err);
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
window.login = function() {
|
|
116
|
+
window.location.href = `${window.api_url}/login`
|
|
117
|
+
}
|
|
118
|
+
window.logout = function() {
|
|
119
|
+
window.location.href = `${window.api_url}/logout`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Auto-initialize on load
|
|
123
|
+
if (document.readyState === 'loading') {
|
|
124
|
+
document.addEventListener('DOMContentLoaded', window.initCustomElements);
|
|
125
|
+
} else {
|
|
126
|
+
window.initCustomElements();
|
|
127
|
+
}
|
package/public/custom.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
window.custom = {
|
|
2
|
+
"hello-world": (data) => `Hello ${data}`,
|
|
3
|
+
"hello-world-2": {
|
|
4
|
+
prop: (data) => `${data} World`,
|
|
5
|
+
render: function(data) {
|
|
6
|
+
return this.prop(data);
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"file-widget": async () => {
|
|
10
|
+
const list = await window.list();
|
|
11
|
+
const style = `<style>.w-c{font:13px sans-serif;border:1px solid #ddd;border-radius:6px;overflow:hidden;max-width:320px}.w-i{display:flex;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #f0f0f0;align-items:center}.w-i:hover{background:#f9f9f9}.w-d{border:none;background:none;cursor:pointer;opacity:.5;transition:.2s}.w-d:hover{opacity:1}.w-u{display:block;padding:10px;background:#f5f5f5;text-align:center;cursor:pointer;color:#555;font-weight:600;transition:.2s}.w-u:hover{background:#eee}</style>`;
|
|
12
|
+
|
|
13
|
+
const items = list.map(item => `
|
|
14
|
+
<div class="w-i">
|
|
15
|
+
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:10px">${item.name}</span>
|
|
16
|
+
<button class="w-d" onclick="window.download('${item.name}')" title="Download">⬇️</button>
|
|
17
|
+
<button class="w-d" onclick="(async()=>{await window.purge('${item.name}');location.reload()})()" title="Delete">🗑️</button>
|
|
18
|
+
</div>`
|
|
19
|
+
).join('');
|
|
20
|
+
|
|
21
|
+
const upload = `
|
|
22
|
+
<label class="w-u">
|
|
23
|
+
📂 Upload
|
|
24
|
+
<input type="file" style="display:none" onchange="(async()=>{const f=this.files[0];if(f){await window.put(f.name,f);location.reload()}})()">
|
|
25
|
+
</label>`;
|
|
26
|
+
|
|
27
|
+
return style + `<div class="w-c">${items}${upload}</div>`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>API Test</title>
|
|
7
|
+
<script src="custom.js"></script>
|
|
8
|
+
<script src="client.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
body { font-family: sans-serif; max-width: 800px; margin: 20px auto; padding: 20px; }
|
|
11
|
+
.section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
|
|
12
|
+
input, button { margin: 5px; padding: 8px; }
|
|
13
|
+
input { width: 200px; }
|
|
14
|
+
button { cursor: pointer; }
|
|
15
|
+
#result { margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 3px; white-space: pre-wrap; }
|
|
16
|
+
.error { color: red; }
|
|
17
|
+
.success { color: green; }
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<h1>Server API Test</h1>
|
|
22
|
+
|
|
23
|
+
<div class="section">
|
|
24
|
+
<h2>KV Storage</h2>
|
|
25
|
+
<input type="text" id="kv-key" placeholder="Key" value="test-key">
|
|
26
|
+
<input type="text" id="kv-value" placeholder="Value" value="test-value">
|
|
27
|
+
<button onclick="window.set(document.querySelector('#kv-key').value, document.querySelector('#kv-value').value).then(r => window.$('#result').textContent = JSON.stringify(r)).catch(err => window.$('#result').textContent = 'Error: ' + err.message)">POST</button>
|
|
28
|
+
<button onclick="window.get(document.querySelector('#kv-key').value).then(r => window.$('#result').textContent = JSON.stringify(r)).catch(err => window.$('#result').textContent = 'Error: ' + err.message)">GET</button>
|
|
29
|
+
<button onclick="window.delete(document.querySelector('#kv-key').value).then(r => window.$('#result').textContent = JSON.stringify(r)).catch(err => window.$('#result').textContent = 'Error: ' + err.message)">DELETE</button>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="section">
|
|
33
|
+
<h2>R2 Storage</h2>
|
|
34
|
+
<file-widget></file-widget>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="section">
|
|
38
|
+
<h2>Auth</h2>
|
|
39
|
+
<button onclick="window.login()">Login</button>
|
|
40
|
+
<button onclick="window.logout()">Logout</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<pre id="result"></pre>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
package/README.md
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
**Enigmatic.js - Documentation**
|
|
2
|
-
|
|
3
|
-
*Note: This repository contains a single JavaScript file that exposes a small set of utility functions under the global `w` object.*
|
|
4
|
-
|
|
5
|
-
**1. Introduction**
|
|
6
|
-
|
|
7
|
-
Enigmatic.js provides helpers for loading resources, creating custom elements and managing a small reactive state. The library automatically activates once the script is loaded and offers the following features:
|
|
8
|
-
|
|
9
|
-
- Resource Loading: load JavaScript files dynamically
|
|
10
|
-
- Custom Elements: quickly define web components with built-in event wiring
|
|
11
|
-
- State Management: update DOM elements automatically when state values change
|
|
12
|
-
- Data Handling: fetch JSON on elements with a `fetch` attribute and receive server-sent events via streams
|
|
13
|
-
- Error Handling: automatic error display for JavaScript errors and promise rejections
|
|
14
|
-
- Template Engine: powerful templating with support for nested properties and special variables
|
|
15
|
-
|
|
16
|
-
**2. Helpers**
|
|
17
|
-
|
|
18
|
-
Several helper functions simplify common tasks:
|
|
19
|
-
|
|
20
|
-
- `w.$(selector)`: return the first element matching the selector
|
|
21
|
-
- `w.$$(selector)`: return all elements matching the selector
|
|
22
|
-
- `w.loadJS(src)`: dynamically load a JavaScript file (returns Promise)
|
|
23
|
-
- `w.wait(ms)`: resolve a Promise after `ms` milliseconds
|
|
24
|
-
- `w.ready()`: resolve when the DOM is ready
|
|
25
|
-
- `w.get(url, opts, transform, key)`: fetch JSON from URL, optionally transform and store in state
|
|
26
|
-
|
|
27
|
-
**3. Template Flattening**
|
|
28
|
-
|
|
29
|
-
`w.flatten(obj, text)` replaces placeholders like `{key}` within a template string using data from `obj`.
|
|
30
|
-
|
|
31
|
-
**Basic usage:**
|
|
32
|
-
```javascript
|
|
33
|
-
w.flatten({ name: 'John', age: 30 }, 'Hello {name}, age {age}')
|
|
34
|
-
// Returns: "Hello John, age 30"
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
**Nested properties:**
|
|
38
|
-
```javascript
|
|
39
|
-
w.flatten({ user: { name: 'John' } }, 'Hello {user.name}')
|
|
40
|
-
// Returns: "Hello John"
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**Arrays:**
|
|
44
|
-
```javascript
|
|
45
|
-
w.flatten([{ name: 'John' }, { name: 'Jane' }], 'Name: {name}')
|
|
46
|
-
// Returns: "Name: JohnName: Jane"
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**Special variables for iteration:**
|
|
50
|
-
- `{$key}` - current key/index
|
|
51
|
-
- `{$val}` - current value
|
|
52
|
-
- `{$index}` - array index (for arrays)
|
|
53
|
-
|
|
54
|
-
```javascript
|
|
55
|
-
// For objects
|
|
56
|
-
w.flatten({ k1: 'val1', k2: 'val2' }, '{$key}: {$val}')
|
|
57
|
-
// Returns: "k1: val1k2: val2"
|
|
58
|
-
|
|
59
|
-
// For arrays
|
|
60
|
-
w.flatten(['a', 'b'], '{$index}: {$val}')
|
|
61
|
-
// Returns: "0: a1: b"
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**4. Custom Elements**
|
|
65
|
-
|
|
66
|
-
Use `w.e(name, fn, style)` to register a custom element.
|
|
67
|
-
|
|
68
|
-
**With object configuration:**
|
|
69
|
-
```javascript
|
|
70
|
-
w.e('my-element', {
|
|
71
|
-
init: (e) => e.innerText = 'ready',
|
|
72
|
-
click: (ev) => console.log('clicked'),
|
|
73
|
-
set: (data) => { /* handle data updates */ }
|
|
74
|
-
}, {
|
|
75
|
-
color: 'red',
|
|
76
|
-
padding: '10px'
|
|
77
|
-
})
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
**With function (simplified):**
|
|
81
|
-
```javascript
|
|
82
|
-
w.e('my-element', (data) => `<div>${data.name}</div>`)
|
|
83
|
-
// Automatically creates a set() method that updates innerHTML
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
The `fn` parameter can be:
|
|
87
|
-
- An object with methods (`init`, `set`, event handlers like `click`, `mouseover`)
|
|
88
|
-
- A function that receives data and returns HTML string
|
|
89
|
-
|
|
90
|
-
Event handlers matching `/click|mouseover/` are automatically bound as event listeners.
|
|
91
|
-
|
|
92
|
-
**5. State and Reactivity**
|
|
93
|
-
|
|
94
|
-
- `w.state`: reactive storage backed by a `Proxy`. Updating a key automatically calls `set()` on any element with a matching `data` attribute.
|
|
95
|
-
|
|
96
|
-
```javascript
|
|
97
|
-
w.state.users = [{ name: 'John' }]
|
|
98
|
-
// All elements with data="users" will have their set() method called
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
- `w.state._all`: returns the entire state object
|
|
102
|
-
- `w.stream(url, key)`: subscribe to an EventSource and populate `w.state[key]` with incoming data
|
|
103
|
-
|
|
104
|
-
**6. Data Binding on Divs**
|
|
105
|
-
|
|
106
|
-
The library automatically enhances all `<div>` elements with data binding capabilities:
|
|
107
|
-
|
|
108
|
-
**Basic usage:**
|
|
109
|
-
```html
|
|
110
|
-
<div data="users" fetch="https://api.example.com/users">
|
|
111
|
-
<div>{name} - {email}</div>
|
|
112
|
-
</div>
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
**Attributes:**
|
|
116
|
-
- `data="key"` - binds to `w.state[key]`, updates when state changes
|
|
117
|
-
- `fetch="url"` - fetch JSON from URL and render with template
|
|
118
|
-
- `fetch='{"inline": "json"}'` - use inline JSON instead of URL
|
|
119
|
-
- `defer` - skip automatic fetch on init (call `element.fetch()` manually)
|
|
120
|
-
- `t="transform"` - transform function as string (e.g., `t="d=>d.results"`)
|
|
121
|
-
|
|
122
|
-
**IGNORE blocks:**
|
|
123
|
-
```html
|
|
124
|
-
<div>
|
|
125
|
-
Hello {name}
|
|
126
|
-
<!--IGNORE-->
|
|
127
|
-
This won't be in the template
|
|
128
|
-
<!--ENDIGNORE-->
|
|
129
|
-
</div>
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**7. Error Handling**
|
|
133
|
-
|
|
134
|
-
Enigmatic.js automatically displays JavaScript errors and unhandled promise rejections as a red banner at the top of the page, showing the error message and location.
|
|
135
|
-
|
|
136
|
-
**8. Component Registration via window.components**
|
|
137
|
-
|
|
138
|
-
You can also register components via `window.components`:
|
|
139
|
-
|
|
140
|
-
```javascript
|
|
141
|
-
window.components = {
|
|
142
|
-
"my-component": {
|
|
143
|
-
init: async () => {
|
|
144
|
-
// initialization
|
|
145
|
-
},
|
|
146
|
-
set: (data) => {
|
|
147
|
-
// handle data
|
|
148
|
-
},
|
|
149
|
-
click: (ev) => {
|
|
150
|
-
// click handler
|
|
151
|
-
},
|
|
152
|
-
style: { color: 'blue' } // optional styles
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
See `components.js` for examples.
|
|
158
|
-
|
|
159
|
-
**9. Usage Examples**
|
|
160
|
-
|
|
161
|
-
**Basic example:**
|
|
162
|
-
```html
|
|
163
|
-
<script src="enigmatic.js"></script>
|
|
164
|
-
<script>
|
|
165
|
-
e('custom-element', {
|
|
166
|
-
init: e => e.innerHTML = 'ready',
|
|
167
|
-
click: ev => console.log('clicked')
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
(async () => {
|
|
171
|
-
await ready();
|
|
172
|
-
w.state.example = { key1: 'value1', key2: 'value2' };
|
|
173
|
-
})();
|
|
174
|
-
</script>
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
**Data binding example:**
|
|
178
|
-
```html
|
|
179
|
-
<div data="users" fetch="https://api.example.com/users">
|
|
180
|
-
<div>{name} - {email}</div>
|
|
181
|
-
</div>
|
|
182
|
-
|
|
183
|
-
<script>
|
|
184
|
-
// State updates automatically update the div
|
|
185
|
-
w.state.users = [{ name: 'John', email: 'john@example.com' }];
|
|
186
|
-
</script>
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
**Component with function:**
|
|
190
|
-
```html
|
|
191
|
-
<script>
|
|
192
|
-
e('user-card', (data) => `
|
|
193
|
-
<div class="card">
|
|
194
|
-
<h3>${data.name}</h3>
|
|
195
|
-
<p>${data.email}</p>
|
|
196
|
-
</div>
|
|
197
|
-
`);
|
|
198
|
-
</script>
|
|
199
|
-
|
|
200
|
-
<user-card data="currentUser"></user-card>
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
**10. Testing**
|
|
204
|
-
|
|
205
|
-
The library includes comprehensive headless tests using Jest:
|
|
206
|
-
|
|
207
|
-
```bash
|
|
208
|
-
npm test # Run tests once
|
|
209
|
-
npm run test:watch # Run tests in watch mode
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
**11. Global Object**
|
|
213
|
-
|
|
214
|
-
All helper functions are attached to the global object `w` as well as directly on `window` for convenience.
|
|
215
|
-
|
|
216
|
-
**12. Support**
|
|
217
|
-
|
|
218
|
-
For bug reports, feature requests, or general inquiries, please visit the GitHub repository of Enigmatic.js.
|