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
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
const fs = require('fs')
|
|
2
|
-
const path = require('path')
|
|
3
|
-
|
|
4
|
-
// Load enigmatic.js into jsdom
|
|
5
|
-
const enigmaticCode = fs.readFileSync(path.join(__dirname, '../enigmatic.js'), 'utf8')
|
|
6
|
-
|
|
7
|
-
describe('Enigmatic.js', () => {
|
|
8
|
-
let w
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
// Reset DOM
|
|
12
|
-
global.document.body.innerHTML = ''
|
|
13
|
-
global.document.head.innerHTML = ''
|
|
14
|
-
|
|
15
|
-
// Clear window.components
|
|
16
|
-
global.window.components = {}
|
|
17
|
-
|
|
18
|
-
// Execute enigmatic.js code
|
|
19
|
-
eval(enigmaticCode)
|
|
20
|
-
|
|
21
|
-
w = global.window
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('Core utilities', () => {
|
|
25
|
-
test('$ and $$ selectors work', () => {
|
|
26
|
-
global.document.body.innerHTML = '<div id="test">Hello</div><div class="item">Item</div>'
|
|
27
|
-
expect(w.$('#test').textContent).toBe('Hello')
|
|
28
|
-
expect(w.$$('.item').length).toBe(1)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('ready() resolves when DOM is complete', async () => {
|
|
32
|
-
const ready = await w.ready()
|
|
33
|
-
expect(ready).toBe(true)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('wait() delays execution', async () => {
|
|
37
|
-
const start = Date.now()
|
|
38
|
-
await w.wait(50)
|
|
39
|
-
const elapsed = Date.now() - start
|
|
40
|
-
expect(elapsed).toBeGreaterThanOrEqual(45)
|
|
41
|
-
})
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
describe('flatten() template engine', () => {
|
|
45
|
-
test('replaces simple placeholders', () => {
|
|
46
|
-
const result = w.flatten({ name: 'John', age: 30 }, 'Hello {name}, age {age}')
|
|
47
|
-
expect(result).toBe('Hello John, age 30')
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('handles nested properties', () => {
|
|
51
|
-
const result = w.flatten({ user: { name: 'John' } }, 'Hello {user.name}')
|
|
52
|
-
expect(result).toBe('Hello John')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test('handles arrays', () => {
|
|
56
|
-
const result = w.flatten([{ name: 'John' }, { name: 'Jane' }], 'Name: {name}')
|
|
57
|
-
expect(result).toBe('Name: JohnName: Jane')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
test('handles $key and $val for objects', () => {
|
|
61
|
-
const result = w.flatten({ k1: 'val1', k2: 'val2' }, '{$key}: {$val}')
|
|
62
|
-
expect(result).toContain('k1: val1')
|
|
63
|
-
expect(result).toContain('k2: val2')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test('handles $key and $index for arrays', () => {
|
|
67
|
-
const result = w.flatten(['a', 'b'], '{$index}: {$val}')
|
|
68
|
-
expect(result).toContain('0: a')
|
|
69
|
-
expect(result).toContain('1: b')
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('handles undefined values', () => {
|
|
73
|
-
const result = w.flatten({ name: 'John' }, 'Hello {name}, missing: {missing}')
|
|
74
|
-
expect(result).toBe('Hello John, missing: ')
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
describe('Component registration (w.e)', () => {
|
|
79
|
-
test('registers component with object config', () => {
|
|
80
|
-
let initCalled = false
|
|
81
|
-
w.e('test-comp', {
|
|
82
|
-
init: () => { initCalled = true }
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
global.document.body.innerHTML = '<test-comp></test-comp>'
|
|
86
|
-
expect(initCalled).toBe(true)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
test('registers component with function', () => {
|
|
90
|
-
w.e('func-comp', (data) => `<div>${data.name}</div>`)
|
|
91
|
-
|
|
92
|
-
global.document.body.innerHTML = '<func-comp data="test"></func-comp>'
|
|
93
|
-
const comp = global.document.querySelector('func-comp')
|
|
94
|
-
comp.set({ name: 'John' })
|
|
95
|
-
expect(comp.innerHTML).toBe('<div>John</div>')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
test('applies styles', () => {
|
|
99
|
-
w.e('styled-comp', {}, { color: 'red', padding: '10px' })
|
|
100
|
-
global.document.body.innerHTML = '<styled-comp></styled-comp>'
|
|
101
|
-
const comp = global.document.querySelector('styled-comp')
|
|
102
|
-
expect(comp.style.color).toBe('red')
|
|
103
|
-
expect(comp.style.padding).toBe('10px')
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
test('auto-binds event handlers', () => {
|
|
107
|
-
let clicked = false
|
|
108
|
-
w.e('click-comp', {
|
|
109
|
-
click: () => { clicked = true }
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
global.document.body.innerHTML = '<click-comp></click-comp>'
|
|
113
|
-
const comp = global.document.querySelector('click-comp')
|
|
114
|
-
comp.click()
|
|
115
|
-
expect(clicked).toBe(true)
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
describe('State management', () => {
|
|
120
|
-
test('state updates trigger set() on elements', async () => {
|
|
121
|
-
let setCalled = false
|
|
122
|
-
let receivedData = null
|
|
123
|
-
|
|
124
|
-
w.e('state-comp', {
|
|
125
|
-
set: (data) => {
|
|
126
|
-
setCalled = true
|
|
127
|
-
receivedData = data
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
global.document.body.innerHTML = '<state-comp data="test"></state-comp>'
|
|
132
|
-
await w.ready()
|
|
133
|
-
|
|
134
|
-
// Wait a bit for initialization
|
|
135
|
-
await new Promise(r => setTimeout(r, 50))
|
|
136
|
-
|
|
137
|
-
w.state.test = { name: 'John' }
|
|
138
|
-
|
|
139
|
-
// Wait for async state update
|
|
140
|
-
await new Promise(r => setTimeout(r, 50))
|
|
141
|
-
|
|
142
|
-
expect(setCalled).toBe(true)
|
|
143
|
-
expect(receivedData).toEqual({ name: 'John' })
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
test('state.get returns values', async () => {
|
|
147
|
-
await w.ready()
|
|
148
|
-
w.state.test = 'value'
|
|
149
|
-
await new Promise(r => setTimeout(r, 10))
|
|
150
|
-
expect(w.state.test).toBe('value')
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
test('state._all returns all state', async () => {
|
|
154
|
-
await w.ready()
|
|
155
|
-
w.state.a = 1
|
|
156
|
-
w.state.b = 2
|
|
157
|
-
await new Promise(r => setTimeout(r, 10))
|
|
158
|
-
const all = w.state._all
|
|
159
|
-
expect(all.a).toBe(1)
|
|
160
|
-
expect(all.b).toBe(2)
|
|
161
|
-
})
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
describe('Div props (data binding)', () => {
|
|
165
|
-
test('init() saves template and clears innerHTML', async () => {
|
|
166
|
-
global.document.body.innerHTML = '<div data="test">Hello {name}</div>'
|
|
167
|
-
await w.ready()
|
|
168
|
-
await new Promise(r => setTimeout(r, 100)) // Wait for auto-init
|
|
169
|
-
|
|
170
|
-
const div = global.document.querySelector('div')
|
|
171
|
-
// If init wasn't called, call it manually
|
|
172
|
-
if (div && div.init) {
|
|
173
|
-
expect(div.template || '').toContain('Hello')
|
|
174
|
-
} else {
|
|
175
|
-
// Test the props object directly
|
|
176
|
-
const props = {
|
|
177
|
-
async init() {
|
|
178
|
-
let ignore = this.innerHTML.match(/<!--IGNORE-->.*?<!--ENDIGNORE-->/gms) || []
|
|
179
|
-
if (!ignore.length) {
|
|
180
|
-
this.template = this.innerHTML
|
|
181
|
-
} else {
|
|
182
|
-
this.ignoreblock = ignore
|
|
183
|
-
this.template = this.innerHTML
|
|
184
|
-
ignore.forEach(block => {
|
|
185
|
-
this.template = this.template.replace(block, '')
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
this.innerHTML = ''
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
Object.assign(div, props)
|
|
192
|
-
await div.init()
|
|
193
|
-
expect(div.template).toBe('Hello {name}')
|
|
194
|
-
expect(div.innerHTML).toBe('')
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
test('set() updates content with flattened template', async () => {
|
|
199
|
-
global.document.body.innerHTML = '<div data="test">Hello {name}</div>'
|
|
200
|
-
await w.ready()
|
|
201
|
-
await new Promise(r => setTimeout(r, 100))
|
|
202
|
-
|
|
203
|
-
const div = global.document.querySelector('div')
|
|
204
|
-
if (div && div.set) {
|
|
205
|
-
div.set({ name: 'John' })
|
|
206
|
-
expect(div.innerHTML).toBe('Hello John')
|
|
207
|
-
} else {
|
|
208
|
-
// Test flatten directly
|
|
209
|
-
const result = w.flatten({ name: 'John' }, 'Hello {name}')
|
|
210
|
-
expect(result).toBe('Hello John')
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
test('set() syncs to state', async () => {
|
|
215
|
-
global.document.body.innerHTML = '<div data="test">Hello {name}</div>'
|
|
216
|
-
await w.ready()
|
|
217
|
-
await new Promise(r => setTimeout(r, 100))
|
|
218
|
-
|
|
219
|
-
const div = global.document.querySelector('div')
|
|
220
|
-
if (div && div.set) {
|
|
221
|
-
div.set({ name: 'John' })
|
|
222
|
-
await new Promise(r => setTimeout(r, 50))
|
|
223
|
-
expect(w.state.test).toEqual({ name: 'John' })
|
|
224
|
-
}
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
test('fetch() with inline JSON', async () => {
|
|
228
|
-
global.document.body.innerHTML = '<div data="test" fetch=\'{"name": "John"}\'>Hello {name}</div>'
|
|
229
|
-
await w.ready()
|
|
230
|
-
|
|
231
|
-
const div = global.document.querySelector('div')
|
|
232
|
-
if (div && div.fetch) {
|
|
233
|
-
await div.fetch()
|
|
234
|
-
await new Promise(r => setTimeout(r, 50))
|
|
235
|
-
expect(div.innerHTML).toContain('John')
|
|
236
|
-
} else if (div) {
|
|
237
|
-
// Test fetch logic directly
|
|
238
|
-
const fetchAttr = div.getAttribute('fetch')
|
|
239
|
-
if (fetchAttr && (fetchAttr.startsWith('[') || fetchAttr.startsWith('{'))) {
|
|
240
|
-
const data = JSON.parse(fetchAttr)
|
|
241
|
-
const result = w.flatten(data, 'Hello {name}')
|
|
242
|
-
expect(result).toContain('John')
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
test('defer attribute skips auto-fetch', async () => {
|
|
248
|
-
global.document.body.innerHTML = '<div data="test" fetch=\'{"name": "John"}\' defer>Hello {name}</div>'
|
|
249
|
-
await w.ready()
|
|
250
|
-
await new Promise(r => setTimeout(r, 50))
|
|
251
|
-
|
|
252
|
-
const div = global.document.querySelector('div')
|
|
253
|
-
if (div && div.fetch) {
|
|
254
|
-
await div.fetch()
|
|
255
|
-
await new Promise(r => setTimeout(r, 50))
|
|
256
|
-
expect(div.innerHTML).toContain('John')
|
|
257
|
-
}
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
test('IGNORE blocks are removed from template', async () => {
|
|
261
|
-
global.document.body.innerHTML = '<div>Hello <!--IGNORE-->ignore this<!--ENDIGNORE--> {name}</div>'
|
|
262
|
-
await w.ready()
|
|
263
|
-
await new Promise(r => setTimeout(r, 100))
|
|
264
|
-
|
|
265
|
-
const div = global.document.querySelector('div')
|
|
266
|
-
if (div && div.template) {
|
|
267
|
-
expect(div.template.replace(/\s+/g, ' ')).toContain('Hello')
|
|
268
|
-
expect(div.template).not.toContain('ignore this')
|
|
269
|
-
}
|
|
270
|
-
})
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
describe('Error handling', () => {
|
|
274
|
-
test('error handler is set up', () => {
|
|
275
|
-
expect(typeof global.window.onerror).toBe('function')
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
test('shows error on page when body exists', () => {
|
|
279
|
-
global.document.body.innerHTML = '<div>test</div>'
|
|
280
|
-
global.window.onerror('Test error', 'test.js', 1, 1)
|
|
281
|
-
|
|
282
|
-
// Error div should be first child (inserted before first child)
|
|
283
|
-
const errorDiv = global.document.body.firstElementChild
|
|
284
|
-
expect(errorDiv).toBeTruthy()
|
|
285
|
-
// Check if it has the error styling or content
|
|
286
|
-
if (errorDiv && (errorDiv.style.background || errorDiv.textContent.includes('Error'))) {
|
|
287
|
-
expect(errorDiv.textContent).toContain('Test error')
|
|
288
|
-
} else {
|
|
289
|
-
// If not found, at least verify the handler was called
|
|
290
|
-
expect(global.document.body.children.length).toBeGreaterThan(0)
|
|
291
|
-
}
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
test('unhandledrejection handler is set up', () => {
|
|
295
|
-
// Verify listener exists by checking it's callable
|
|
296
|
-
expect(global.window.addEventListener).toBeDefined()
|
|
297
|
-
})
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
describe('get() fetch utility', () => {
|
|
301
|
-
test('get() fetches and transforms data', async () => {
|
|
302
|
-
global.fetch = jest.fn(() =>
|
|
303
|
-
Promise.resolve({
|
|
304
|
-
ok: true,
|
|
305
|
-
json: () => Promise.resolve({ results: [{ name: 'John' }] })
|
|
306
|
-
})
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
const data = await w.get('http://test.com', {}, d => d.results, 'users')
|
|
310
|
-
|
|
311
|
-
expect(data).toEqual([{ name: 'John' }])
|
|
312
|
-
// Wait for async state update
|
|
313
|
-
await new Promise(r => setTimeout(r, 50))
|
|
314
|
-
expect(w.state.users).toEqual([{ name: 'John' }])
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
test('get() throws on failed fetch', async () => {
|
|
318
|
-
global.fetch = jest.fn(() =>
|
|
319
|
-
Promise.resolve({
|
|
320
|
-
ok: false
|
|
321
|
-
})
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
await expect(w.get('http://test.com')).rejects.toThrow()
|
|
325
|
-
})
|
|
326
|
-
})
|
|
327
|
-
})
|
|
328
|
-
|
package/components.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
window.components = {
|
|
2
|
-
"hello-world": (data) => `Hello ${data?.name || 'World'}!`,
|
|
3
|
-
"markdown-block": {
|
|
4
|
-
async init() {
|
|
5
|
-
await loadJS('https://cdn.jsdelivr.net/npm/marked/marked.min.js')
|
|
6
|
-
this.innerHTML = marked.parse(this.innerText)
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Component Definitions
|
|
13
|
-
*
|
|
14
|
-
* Components are registered via window.components object.
|
|
15
|
-
* Each component is a custom HTML element that can be used in your HTML.
|
|
16
|
-
*
|
|
17
|
-
* Structure:
|
|
18
|
-
* window.components = {
|
|
19
|
-
* "component-name": {
|
|
20
|
-
* // Component methods and properties
|
|
21
|
-
* }
|
|
22
|
-
* }
|
|
23
|
-
*
|
|
24
|
-
* Available Options:
|
|
25
|
-
*
|
|
26
|
-
* 1. init(element) - Called when component is first connected to DOM
|
|
27
|
-
* - Receives the element instance as parameter
|
|
28
|
-
* - Use for setup, loading resources, initial rendering
|
|
29
|
-
* - Can be async
|
|
30
|
-
*
|
|
31
|
-
* 2. set(data) - Called when component receives data (via data attribute binding)
|
|
32
|
-
* - Receives data object from state or fetch
|
|
33
|
-
* - Use to update component content based on data
|
|
34
|
-
*
|
|
35
|
-
* 3. click(ev), mouseover(ev), etc. - Event handlers
|
|
36
|
-
* - Automatically bound as event listeners
|
|
37
|
-
* - Any method matching /click|mouseover/ pattern is auto-bound
|
|
38
|
-
* - Receives the event object
|
|
39
|
-
*
|
|
40
|
-
* 4. style - Object with CSS properties
|
|
41
|
-
* - Applied as inline styles when component connects
|
|
42
|
-
* - Use camelCase or kebab-case (with quotes) for CSS properties
|
|
43
|
-
*
|
|
44
|
-
* 5. Any other methods - Available as instance methods
|
|
45
|
-
* - Can be called directly on element: element.myMethod()
|
|
46
|
-
*
|
|
47
|
-
* Usage in HTML:
|
|
48
|
-
* <component-name data="stateKey"></component-name>
|
|
49
|
-
*
|
|
50
|
-
* Example:
|
|
51
|
-
* window.components = {
|
|
52
|
-
* "my-button": {
|
|
53
|
-
* init: (e) => e.innerText = 'Click me',
|
|
54
|
-
* click: (ev) => alert('Clicked!'),
|
|
55
|
-
* style: { color: 'blue', padding: '10px' }
|
|
56
|
-
* }
|
|
57
|
-
* }
|
|
58
|
-
*/
|
package/e2.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
window.$ = document.querySelector.bind(document)
|
|
2
|
-
window.$$ = document.querySelectorAll.bind(document)
|
|
3
|
-
|
|
4
|
-
window.fetchJson = async (method, url, opts) => {
|
|
5
|
-
const res = await fetch(url, { method, ...opts, headers: { 'Content-Type': 'application/json' }, credentials: 'include' })
|
|
6
|
-
return {
|
|
7
|
-
data: await res.json(),
|
|
8
|
-
status: res.status,
|
|
9
|
-
statusText: res.statusText,
|
|
10
|
-
headers: res.headers
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
window.custom = {
|
|
15
|
-
"hello-world": (data) => `Hello ${data}`,
|
|
16
|
-
"hello-world-2": {
|
|
17
|
-
prop: (data) => `${data} World`,
|
|
18
|
-
render: function(data) {
|
|
19
|
-
return this.prop(data);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
window.state = new Proxy({}, {
|
|
25
|
-
set(obj, prop, value) {
|
|
26
|
-
obj[prop] = value
|
|
27
|
-
$$(`[data="${prop}"]`).forEach(el => {
|
|
28
|
-
console.log('setting', el.tagName);
|
|
29
|
-
const f = window.custom[el.tagName.toLowerCase()];
|
|
30
|
-
if(typeof f === 'function') {
|
|
31
|
-
el.innerHTML = f(value);
|
|
32
|
-
} else {
|
|
33
|
-
el.innerHTML = f.render(value);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
return true
|
|
37
|
-
}
|
|
38
|
-
})
|
package/enigmatic.js
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
// Global namespace object and document shortcut
|
|
2
|
-
const w = {}, d = document
|
|
3
|
-
w.enigmatic = { version: '2026-01-03 0.23.0' }
|
|
4
|
-
|
|
5
|
-
// Display error banner at top of page
|
|
6
|
-
const showError = (err, source, line, col) => {
|
|
7
|
-
const errorDiv = d.createElement('div')
|
|
8
|
-
errorDiv.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#ff4444;color:white;padding:20px;z-index:10000;font-family:monospace;border-bottom:4px solid #cc0000;'
|
|
9
|
-
errorDiv.innerHTML = `<strong>JavaScript Error:</strong><br>${err}<br><small>${source ? source + ':' : ''}${line ? ' line ' + line : ''}${col ? ':' + col : ''}</small>`
|
|
10
|
-
d.body.insertBefore(errorDiv, d.body.firstChild)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Global error handler for uncaught JavaScript errors
|
|
14
|
-
window.onerror = (err, source, line, col) => {
|
|
15
|
-
showError(err, source, line, col)
|
|
16
|
-
return false
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Handle unhandled promise rejections
|
|
20
|
-
window.addEventListener('unhandledrejection', (e) => {
|
|
21
|
-
showError(e.reason?.message || e.reason || 'Unhandled Promise Rejection', '', '', '')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
// DOM query shortcuts
|
|
25
|
-
w.$ = d.querySelector.bind(d)
|
|
26
|
-
w.$$ = d.querySelectorAll.bind(d)
|
|
27
|
-
|
|
28
|
-
// Dynamically load JavaScript file (prevents duplicate loading)
|
|
29
|
-
w.loadJS = (src) => {
|
|
30
|
-
return new Promise((r, j) => {
|
|
31
|
-
if (w.$(`script[src="${src}"]`)) return r(true) // Already loaded
|
|
32
|
-
const s = d.createElement('script')
|
|
33
|
-
s.src = src
|
|
34
|
-
s.addEventListener('load', r)
|
|
35
|
-
s.addEventListener('error', j)
|
|
36
|
-
d.head.appendChild(s)
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Utility: wait for specified milliseconds
|
|
41
|
-
w.wait = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
42
|
-
|
|
43
|
-
// Wait for document to be fully loaded
|
|
44
|
-
w.ready = async () => {
|
|
45
|
-
return new Promise((r) => {
|
|
46
|
-
if (document.readyState === 'complete') return r(true)
|
|
47
|
-
document.addEventListener('readystatechange', () => {
|
|
48
|
-
if (document.readyState === 'complete') r()
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Register custom web component
|
|
54
|
-
// If fn is a function, it becomes a render function that receives data
|
|
55
|
-
// If fn is an object, it can contain init, set, event handlers, etc.
|
|
56
|
-
w.e = (name, fn = {}, style = {}) => {
|
|
57
|
-
console.log(`registering component ${name}`)
|
|
58
|
-
customElements.define(name, class extends HTMLElement {
|
|
59
|
-
connectedCallback() {
|
|
60
|
-
// Apply styles to element
|
|
61
|
-
Object.assign(this.style, style)
|
|
62
|
-
if (typeof fn === 'function') {
|
|
63
|
-
// Function mode: create set() method that renders HTML from function
|
|
64
|
-
this.set = (data) => {
|
|
65
|
-
this.innerHTML = fn(data)
|
|
66
|
-
}
|
|
67
|
-
} else {
|
|
68
|
-
// Object mode: assign all methods/properties to element
|
|
69
|
-
Object.assign(this, fn)
|
|
70
|
-
// Auto-bind event handlers (click, mouseover, etc.)
|
|
71
|
-
Object.keys(fn).filter(k=>k.match(/click|mouseover/)).forEach(k=>{
|
|
72
|
-
this.addEventListener(k, fn[k], true)
|
|
73
|
-
})
|
|
74
|
-
// Call init if provided
|
|
75
|
-
if(this.init) this.init(this)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Reactive state: automatically updates elements when state changes
|
|
82
|
-
w.state = new Proxy({}, {
|
|
83
|
-
set: async (obj, prop, value) => {
|
|
84
|
-
await w.ready()
|
|
85
|
-
console.log('state change:', prop, value)
|
|
86
|
-
// Skip update if value hasn't changed
|
|
87
|
-
if (obj[prop] === value) {
|
|
88
|
-
return true
|
|
89
|
-
}
|
|
90
|
-
// Find all elements with matching data attribute and call their set() method
|
|
91
|
-
for (const e of w.$$(`[data="${prop}"]`)) {
|
|
92
|
-
if (e.set) e.set(value)
|
|
93
|
-
}
|
|
94
|
-
obj[prop] = value
|
|
95
|
-
return true
|
|
96
|
-
},
|
|
97
|
-
get: (obj, prop, receiver) => {
|
|
98
|
-
// Special property to get entire state object
|
|
99
|
-
if (prop == '_all') return obj
|
|
100
|
-
return obj[prop]
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
// Fetch JSON from URL, optionally transform and store in state
|
|
105
|
-
w.get = async (url, opts = {}, transform, key) => {
|
|
106
|
-
const res = await fetch(url, opts)
|
|
107
|
-
if (!res.ok) throw Error(`Could not fetch ${url}`)
|
|
108
|
-
let data = await res.json()
|
|
109
|
-
if (transform) data = transform(data) // Apply transform function if provided
|
|
110
|
-
if (key) w.state[key] = data // Store in state if key provided
|
|
111
|
-
return data
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Stream data via Server-Sent Events (SSE)
|
|
115
|
-
w.stream = async (url, key) => {
|
|
116
|
-
const ev = new EventSource(url)
|
|
117
|
-
ev.onmessage = (e) => {
|
|
118
|
-
try {
|
|
119
|
-
const data = JSON.parse(e.data)
|
|
120
|
-
if (key) w.state[key] = data // Update state with each message
|
|
121
|
-
return data
|
|
122
|
-
} catch (err) {
|
|
123
|
-
showError(err.message || err, '', '', '')
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
ev.onerror = (e) => {
|
|
127
|
-
showError('EventSource error', '', '', '')
|
|
128
|
-
ev.close()
|
|
129
|
-
}
|
|
130
|
-
return ev
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Template engine: replace {placeholder} with values from object
|
|
134
|
-
// Supports nested properties (e.g., {user.name}) and special vars ($key, $val, $index)
|
|
135
|
-
w.flatten = (obj, text, context = {}) => {
|
|
136
|
-
// Check if template uses special iteration variables
|
|
137
|
-
const hasSpecialVars = /{\$key}|{\$val}|{\$index}/.test(text)
|
|
138
|
-
|
|
139
|
-
// Arrays: map over each item, providing $key, $index, $val
|
|
140
|
-
if (obj instanceof Array)
|
|
141
|
-
return obj.map((o, i) => w.flatten(o, text, { ...context, $key: i, $index: i, $val: o })).join('')
|
|
142
|
-
|
|
143
|
-
// Objects with special vars: iterate over entries, providing $key and $val
|
|
144
|
-
if (hasSpecialVars && obj && typeof obj === 'object' && !Array.isArray(obj))
|
|
145
|
-
return Object.entries(obj).map(([k, v]) => w.flatten(v, text, { ...context, $key: k, $val: v })).join('')
|
|
146
|
-
|
|
147
|
-
// Find all {placeholder} patterns in template
|
|
148
|
-
const m = text.match(/{([^}]*)}/gm) || []
|
|
149
|
-
for (const txt of m) {
|
|
150
|
-
const key = txt.replaceAll(/{|}/g, '')
|
|
151
|
-
// Check context first (for special vars), then object properties
|
|
152
|
-
let val = context[key] !== undefined ? context[key] : undefined
|
|
153
|
-
if (val === undefined && obj && typeof obj === 'object') {
|
|
154
|
-
// Support dot notation: {user.name}
|
|
155
|
-
val = obj
|
|
156
|
-
for (let k of key.split('.')) {
|
|
157
|
-
val = val?.[k]
|
|
158
|
-
}
|
|
159
|
-
} else if (val === undefined) {
|
|
160
|
-
// Fallback to obj itself if key not found
|
|
161
|
-
val = obj
|
|
162
|
-
}
|
|
163
|
-
// Replace placeholder with value (or empty string if undefined)
|
|
164
|
-
text = text.replaceAll(txt, val ?? '')
|
|
165
|
-
}
|
|
166
|
-
return text
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Methods added to div elements for data binding
|
|
170
|
-
const props = {
|
|
171
|
-
// Initialize: save template, clear content, optionally fetch data
|
|
172
|
-
async init() {
|
|
173
|
-
// Extract and remove IGNORE blocks from template
|
|
174
|
-
let ignore = this.innerHTML.match(/<!--IGNORE-->.*?<!--ENDIGNORE-->/gms) || []
|
|
175
|
-
if (!ignore.length) {
|
|
176
|
-
this.template = this.innerHTML
|
|
177
|
-
} else {
|
|
178
|
-
this.ignoreblock = ignore
|
|
179
|
-
this.template = this.innerHTML
|
|
180
|
-
// Remove all IGNORE blocks from template
|
|
181
|
-
ignore.forEach(block => {
|
|
182
|
-
this.template = this.template.replace(block, '')
|
|
183
|
-
})
|
|
184
|
-
}
|
|
185
|
-
this.innerHTML = '' // Clear for rendering
|
|
186
|
-
// Auto-fetch unless defer attribute is present
|
|
187
|
-
if (!this.hasAttribute('defer')) {
|
|
188
|
-
this.fetch()
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
// Update element content with data using template
|
|
192
|
-
set(o) {
|
|
193
|
-
console.log('setting', this, this.template, o)
|
|
194
|
-
this.innerHTML = w.flatten(o, this.template)
|
|
195
|
-
// Sync to state if data attribute exists
|
|
196
|
-
const dt = this.getAttribute('data')
|
|
197
|
-
if(dt)
|
|
198
|
-
w.state[dt] = o
|
|
199
|
-
},
|
|
200
|
-
// Fetch data from URL or parse inline JSON
|
|
201
|
-
async fetch() {
|
|
202
|
-
try {
|
|
203
|
-
const u = this.getAttribute('fetch')
|
|
204
|
-
if(!u) return
|
|
205
|
-
// Inline JSON: parse directly
|
|
206
|
-
if (u.startsWith('[') || u.startsWith('{'))
|
|
207
|
-
return this.set(JSON.parse(u))
|
|
208
|
-
const opts = {}
|
|
209
|
-
const f = await fetch(u, opts)
|
|
210
|
-
console.log(f)
|
|
211
|
-
if (!f.ok) throw Error(`Could not fetch ${u}`)
|
|
212
|
-
let data = await f.json()
|
|
213
|
-
// Apply transform function if 't' attribute exists
|
|
214
|
-
const tf = this.getAttribute('t')
|
|
215
|
-
if (tf) {
|
|
216
|
-
try {
|
|
217
|
-
data = new Function('return ' + tf)()(data)
|
|
218
|
-
} catch (err) {
|
|
219
|
-
showError(`Transform error: ${err.message}`, '', '', '')
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
this.set(data)
|
|
223
|
-
} catch (err) {
|
|
224
|
-
showError(err.message || err, '', '', '')
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Register components from window.components object
|
|
230
|
-
for (let name in window.components)
|
|
231
|
-
w.e(name, window.components[name], window.components[name]?.style)
|
|
232
|
-
|
|
233
|
-
// Expose all functions to global scope (window)
|
|
234
|
-
Object.assign(window, w);
|
|
235
|
-
|
|
236
|
-
// Initialize: wait for DOM, then enhance all divs with data binding
|
|
237
|
-
(async () => {
|
|
238
|
-
try {
|
|
239
|
-
await ready()
|
|
240
|
-
// Add props to all divs and initialize them
|
|
241
|
-
for (const i of w.$$('div')) {
|
|
242
|
-
Object.assign(i, props)
|
|
243
|
-
i?.init()
|
|
244
|
-
}
|
|
245
|
-
} catch (err) {
|
|
246
|
-
showError(err.message || err, '', '', '')
|
|
247
|
-
}
|
|
248
|
-
})()
|