enigmatic 0.26.0 → 0.29.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/CLIENT_JS_DOCS.md +322 -0
- package/__tests__/e2.test.js +231 -138
- package/__tests__/jest.config.js +7 -0
- package/{jest.setup.js → __tests__/jest.setup.js} +4 -0
- package/bun-server.js +130 -0
- package/package.json +12 -11
- package/public/client.js +98 -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 -169
- package/e2.js +0 -38
- package/enigmatic.js +0 -248
- package/index.html +0 -34
- package/jest.config.js +0 -6
- /package/{enigmatic.css → public/client.css} +0 -0
- /package/{theme.css → public/theme.css} +0 -0
package/public/client.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
window.state = new Proxy({}, {
|
|
6
|
+
set(obj, prop, value) {
|
|
7
|
+
obj[prop] = value
|
|
8
|
+
$$(`[data="${prop}"]`).forEach(el => {
|
|
9
|
+
console.log('setting', el.tagName);
|
|
10
|
+
const f = window.custom?.[el.tagName.toLowerCase()];
|
|
11
|
+
if (!f) return;
|
|
12
|
+
if(typeof f === 'function') {
|
|
13
|
+
el.innerHTML = f(value);
|
|
14
|
+
} else if (f && typeof f.render === 'function') {
|
|
15
|
+
el.innerHTML = f.render(value);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
window.get = async function(key) {
|
|
22
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`)
|
|
23
|
+
return await res.json()
|
|
24
|
+
}
|
|
25
|
+
window.set = async function(key, value) {
|
|
26
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
|
|
27
|
+
method: 'POST', body: typeof value === 'string' ? value : JSON.stringify(value)
|
|
28
|
+
})
|
|
29
|
+
return await res.json()
|
|
30
|
+
}
|
|
31
|
+
window.delete = async function(key) {
|
|
32
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
|
|
33
|
+
method: 'DELETE'
|
|
34
|
+
})
|
|
35
|
+
return await res.json()
|
|
36
|
+
}
|
|
37
|
+
window.put = async function(key, body) {
|
|
38
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
|
|
39
|
+
method: 'PUT', body: body instanceof Blob ? body : typeof body === 'string' ? body : JSON.stringify(body)
|
|
40
|
+
})
|
|
41
|
+
return await res.json()
|
|
42
|
+
}
|
|
43
|
+
window.purge = async function(key) {
|
|
44
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, {
|
|
45
|
+
method: 'PURGE'
|
|
46
|
+
})
|
|
47
|
+
return await res.json()
|
|
48
|
+
}
|
|
49
|
+
window.list = async function() {
|
|
50
|
+
const res = await fetch(`${window.api_url}`, {
|
|
51
|
+
method: 'PROPFIND'
|
|
52
|
+
})
|
|
53
|
+
return await res.json()
|
|
54
|
+
}
|
|
55
|
+
window.download = async function(key) {
|
|
56
|
+
try {
|
|
57
|
+
console.log('Downloading with method DOWNLOAD:', key);
|
|
58
|
+
const res = await fetch(`${window.api_url}/${encodeURIComponent(key)}`, { method: 'PATCH' });
|
|
59
|
+
console.log('Response:', key, res.status, res.statusText);
|
|
60
|
+
if (!res.ok) throw new Error('Download failed');
|
|
61
|
+
const blob = await res.blob();
|
|
62
|
+
const url = URL.createObjectURL(blob);
|
|
63
|
+
const a = document.createElement('a');
|
|
64
|
+
a.href = url;
|
|
65
|
+
a.download = key;
|
|
66
|
+
a.click();
|
|
67
|
+
URL.revokeObjectURL(url);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error('Download error:', err);
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
window.login = function() {
|
|
74
|
+
window.location.href = `${window.api_url}/login`
|
|
75
|
+
}
|
|
76
|
+
window.logout = function() {
|
|
77
|
+
window.location.href = `${window.api_url}/logout`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Initialize custom elements on page load
|
|
81
|
+
function initCustomElements() {
|
|
82
|
+
Object.keys(window.custom).forEach(tagName => {
|
|
83
|
+
$$(tagName).forEach(async el => {
|
|
84
|
+
const f = window.custom[tagName];
|
|
85
|
+
if (typeof f === 'function') {
|
|
86
|
+
el.innerHTML = await f();
|
|
87
|
+
} else if (f && typeof f.render === 'function') {
|
|
88
|
+
el.innerHTML = f.render();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (document.readyState === 'loading') {
|
|
95
|
+
document.addEventListener('DOMContentLoaded', initCustomElements);
|
|
96
|
+
} else {
|
|
97
|
+
initCustomElements();
|
|
98
|
+
}
|
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.
|