enigmatic 0.22.0 → 0.24.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 +181 -44
- package/__tests__/enigmatic.test.js +328 -0
- package/components.js +162 -1
- package/e2.js +29 -0
- package/enigmatic.js +181 -151
- package/index.html +31 -74
- package/jest.config.js +6 -0
- package/jest.setup.js +5 -0
- package/package.json +5 -4
- package/tests/test-error.html +0 -5
- package/tests/test-fetch.html +0 -26
- package/tests/test-flatten.html +0 -38
- package/tests/test-layout.html +0 -32
- package/tests/test-schema.html +0 -10
- package/tests/test.html +0 -44
package/components.js
CHANGED
|
@@ -1,8 +1,169 @@
|
|
|
1
1
|
window.components = {
|
|
2
|
+
"hello-world": (data) => `Hello ${data?.name || 'World'}!`,
|
|
2
3
|
"markdown-block": {
|
|
3
4
|
async init() {
|
|
4
5
|
await loadJS('https://cdn.jsdelivr.net/npm/marked/marked.min.js')
|
|
5
6
|
this.innerHTML = marked.parse(this.innerText)
|
|
6
7
|
}
|
|
8
|
+
},
|
|
9
|
+
"auto-complete": {
|
|
10
|
+
init() {
|
|
11
|
+
// Prevent re-initialization
|
|
12
|
+
if (this._initialized) return
|
|
13
|
+
this._initialized = true
|
|
14
|
+
|
|
15
|
+
// Ensure element is visible
|
|
16
|
+
this.style.display = 'block'
|
|
17
|
+
this.style.width = '100%'
|
|
18
|
+
this.style.minWidth = '200px'
|
|
19
|
+
this.style.margin = '10px 0'
|
|
20
|
+
this.style.padding = '0'
|
|
21
|
+
this.style.backgroundColor = 'transparent'
|
|
22
|
+
|
|
23
|
+
this.input = document.createElement('input')
|
|
24
|
+
this.input.type = 'text'
|
|
25
|
+
this.input.placeholder = this.getAttribute('placeholder') || 'Type to search...'
|
|
26
|
+
this.input.style.cssText = 'width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;font-size:14px;'
|
|
27
|
+
|
|
28
|
+
this.dropdown = document.createElement('div')
|
|
29
|
+
this.dropdown.style.cssText = 'position:absolute;top:100%;left:0;right:0;background:white;border:1px solid #ccc;border-top:none;max-height:200px;overflow-y:auto;z-index:1000;display:none;box-shadow:0 2px 4px rgba(0,0,0,0.1);'
|
|
30
|
+
|
|
31
|
+
this.container = document.createElement('div')
|
|
32
|
+
this.container.style.cssText = 'position:relative;width:100%;min-width:200px;'
|
|
33
|
+
this.container.appendChild(this.input)
|
|
34
|
+
this.container.appendChild(this.dropdown)
|
|
35
|
+
|
|
36
|
+
// Clear existing content
|
|
37
|
+
this.innerHTML = ''
|
|
38
|
+
this.appendChild(this.container)
|
|
39
|
+
|
|
40
|
+
this.items = []
|
|
41
|
+
this.filtered = []
|
|
42
|
+
this.selectedIndex = -1
|
|
43
|
+
|
|
44
|
+
this.input.addEventListener('input', () => this.filter())
|
|
45
|
+
this.input.addEventListener('keydown', (e) => this.handleKey(e))
|
|
46
|
+
this.input.addEventListener('focus', () => {
|
|
47
|
+
if (this.filtered.length > 0) this.dropdown.style.display = 'block'
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Use a unique handler per instance
|
|
51
|
+
this._clickHandler = (e) => {
|
|
52
|
+
if (!this.contains(e.target)) {
|
|
53
|
+
this.dropdown.style.display = 'none'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
document.addEventListener('click', this._clickHandler)
|
|
57
|
+
},
|
|
58
|
+
set(data) {
|
|
59
|
+
this.items = Array.isArray(data) ? data : (data?.items || [])
|
|
60
|
+
this.filter()
|
|
61
|
+
},
|
|
62
|
+
filter() {
|
|
63
|
+
const query = this.input.value.toLowerCase()
|
|
64
|
+
this.filtered = this.items.filter(item => {
|
|
65
|
+
const text = typeof item === 'string' ? item : (item.label || item.name || String(item))
|
|
66
|
+
return text.toLowerCase().includes(query)
|
|
67
|
+
})
|
|
68
|
+
this.render()
|
|
69
|
+
},
|
|
70
|
+
render() {
|
|
71
|
+
if (this.filtered.length === 0) {
|
|
72
|
+
this.dropdown.style.display = 'none'
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.dropdown.innerHTML = this.filtered.map((item, i) => {
|
|
77
|
+
const text = typeof item === 'string' ? item : (item.label || item.name || String(item))
|
|
78
|
+
const selected = i === this.selectedIndex ? 'background:#f0f0f0;' : ''
|
|
79
|
+
return `<div data-index="${i}" style="padding:8px;cursor:pointer;${selected}">${text}</div>`
|
|
80
|
+
}).join('')
|
|
81
|
+
|
|
82
|
+
this.dropdown.style.display = 'block'
|
|
83
|
+
|
|
84
|
+
this.dropdown.querySelectorAll('div').forEach((div, i) => {
|
|
85
|
+
div.addEventListener('click', () => this.select(i))
|
|
86
|
+
div.addEventListener('mouseenter', () => {
|
|
87
|
+
this.selectedIndex = i
|
|
88
|
+
this.render()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
},
|
|
92
|
+
select(index) {
|
|
93
|
+
const item = this.filtered[index]
|
|
94
|
+
const text = typeof item === 'string' ? item : (item.label || item.name || String(item))
|
|
95
|
+
this.input.value = text
|
|
96
|
+
this.dropdown.style.display = 'none'
|
|
97
|
+
this.selectedIndex = -1
|
|
98
|
+
|
|
99
|
+
const event = new CustomEvent('select', { detail: item })
|
|
100
|
+
this.dispatchEvent(event)
|
|
101
|
+
},
|
|
102
|
+
handleKey(e) {
|
|
103
|
+
if (e.key === 'ArrowDown') {
|
|
104
|
+
e.preventDefault()
|
|
105
|
+
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filtered.length - 1)
|
|
106
|
+
this.render()
|
|
107
|
+
} else if (e.key === 'ArrowUp') {
|
|
108
|
+
e.preventDefault()
|
|
109
|
+
this.selectedIndex = Math.max(this.selectedIndex - 1, -1)
|
|
110
|
+
this.render()
|
|
111
|
+
} else if (e.key === 'Enter' && this.selectedIndex >= 0) {
|
|
112
|
+
e.preventDefault()
|
|
113
|
+
this.select(this.selectedIndex)
|
|
114
|
+
} else if (e.key === 'Escape') {
|
|
115
|
+
this.dropdown.style.display = 'none'
|
|
116
|
+
this.selectedIndex = -1
|
|
117
|
+
}
|
|
118
|
+
}
|
|
7
119
|
}
|
|
8
|
-
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Component Definitions
|
|
124
|
+
*
|
|
125
|
+
* Components are registered via window.components object.
|
|
126
|
+
* Each component is a custom HTML element that can be used in your HTML.
|
|
127
|
+
*
|
|
128
|
+
* Structure:
|
|
129
|
+
* window.components = {
|
|
130
|
+
* "component-name": {
|
|
131
|
+
* // Component methods and properties
|
|
132
|
+
* }
|
|
133
|
+
* }
|
|
134
|
+
*
|
|
135
|
+
* Available Options:
|
|
136
|
+
*
|
|
137
|
+
* 1. init(element) - Called when component is first connected to DOM
|
|
138
|
+
* - Receives the element instance as parameter
|
|
139
|
+
* - Use for setup, loading resources, initial rendering
|
|
140
|
+
* - Can be async
|
|
141
|
+
*
|
|
142
|
+
* 2. set(data) - Called when component receives data (via data attribute binding)
|
|
143
|
+
* - Receives data object from state or fetch
|
|
144
|
+
* - Use to update component content based on data
|
|
145
|
+
*
|
|
146
|
+
* 3. click(ev), mouseover(ev), etc. - Event handlers
|
|
147
|
+
* - Automatically bound as event listeners
|
|
148
|
+
* - Any method matching /click|mouseover/ pattern is auto-bound
|
|
149
|
+
* - Receives the event object
|
|
150
|
+
*
|
|
151
|
+
* 4. style - Object with CSS properties
|
|
152
|
+
* - Applied as inline styles when component connects
|
|
153
|
+
* - Use camelCase or kebab-case (with quotes) for CSS properties
|
|
154
|
+
*
|
|
155
|
+
* 5. Any other methods - Available as instance methods
|
|
156
|
+
* - Can be called directly on element: element.myMethod()
|
|
157
|
+
*
|
|
158
|
+
* Usage in HTML:
|
|
159
|
+
* <component-name data="stateKey"></component-name>
|
|
160
|
+
*
|
|
161
|
+
* Example:
|
|
162
|
+
* window.components = {
|
|
163
|
+
* "my-button": {
|
|
164
|
+
* init: (e) => e.innerText = 'Click me',
|
|
165
|
+
* click: (ev) => alert('Clicked!'),
|
|
166
|
+
* style: { color: 'blue', padding: '10px' }
|
|
167
|
+
* }
|
|
168
|
+
* }
|
|
169
|
+
*/
|
package/e2.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
window.$ = document.querySelector.bind(document)
|
|
2
|
+
window.$$ = document.querySelectorAll.bind(document)
|
|
3
|
+
|
|
4
|
+
window.custom = {
|
|
5
|
+
"hello-world": (data) => `Hello ${data}`,
|
|
6
|
+
"hello-world-2": {
|
|
7
|
+
prop: (data) => `${data} World`,
|
|
8
|
+
render: function(data) {
|
|
9
|
+
return this.prop(data);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
window.state = new Proxy({}, {
|
|
15
|
+
set(obj, prop, value) {
|
|
16
|
+
obj[prop] = value
|
|
17
|
+
console.log('set', prop, value);
|
|
18
|
+
$$(`hello-world, hello-world-2`).forEach(el => {
|
|
19
|
+
console.log('setting', el.tagName);
|
|
20
|
+
const f = window.custom[el.tagName.toLowerCase()];
|
|
21
|
+
if(typeof f === 'function') {
|
|
22
|
+
el.innerHTML = f(value);
|
|
23
|
+
} else {
|
|
24
|
+
el.innerHTML = f.render(value);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
})
|
package/enigmatic.js
CHANGED
|
@@ -1,218 +1,248 @@
|
|
|
1
|
+
// Global namespace object and document shortcut
|
|
1
2
|
const w = {}, d = document
|
|
2
|
-
w.enigmatic = { version: '
|
|
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
|
+
}
|
|
3
12
|
|
|
4
|
-
|
|
5
|
-
|
|
13
|
+
// Global error handler for uncaught JavaScript errors
|
|
14
|
+
window.onerror = (err, source, line, col) => {
|
|
15
|
+
showError(err, source, line, col)
|
|
16
|
+
return false
|
|
6
17
|
}
|
|
7
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
|
|
8
25
|
w.$ = d.querySelector.bind(d)
|
|
9
26
|
w.$$ = d.querySelectorAll.bind(d)
|
|
27
|
+
|
|
28
|
+
// Dynamically load JavaScript file (prevents duplicate loading)
|
|
10
29
|
w.loadJS = (src) => {
|
|
11
30
|
return new Promise((r, j) => {
|
|
12
|
-
if (
|
|
31
|
+
if (w.$(`script[src="${src}"]`)) return r(true) // Already loaded
|
|
13
32
|
const s = d.createElement('script')
|
|
14
33
|
s.src = src
|
|
15
34
|
s.addEventListener('load', r)
|
|
35
|
+
s.addEventListener('error', j)
|
|
16
36
|
d.head.appendChild(s)
|
|
17
37
|
})
|
|
18
38
|
}
|
|
19
39
|
|
|
20
|
-
|
|
21
|
-
return new Promise((r, j) => {
|
|
22
|
-
const s = document.createElement('link')
|
|
23
|
-
s.rel = 'stylesheet'
|
|
24
|
-
s.href = src
|
|
25
|
-
s.addEventListener('load', r)
|
|
26
|
-
d.head.appendChild(s)
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
40
|
+
// Utility: wait for specified milliseconds
|
|
30
41
|
w.wait = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
31
42
|
|
|
43
|
+
// Wait for document to be fully loaded
|
|
32
44
|
w.ready = async () => {
|
|
33
45
|
return new Promise((r) => {
|
|
34
|
-
if (document.readyState === 'complete') r(true)
|
|
35
|
-
document.
|
|
46
|
+
if (document.readyState === 'complete') return r(true)
|
|
47
|
+
document.addEventListener('readystatechange', () => {
|
|
36
48
|
if (document.readyState === 'complete') r()
|
|
37
|
-
}
|
|
49
|
+
})
|
|
38
50
|
})
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for (let k in obj) {
|
|
45
|
-
template += text.replaceAll('{$key}', k).replaceAll('{$val}', obj[k])
|
|
46
|
-
}
|
|
47
|
-
return template
|
|
48
|
-
}
|
|
49
|
-
for (let k in obj) {
|
|
50
|
-
text = text.replaceAll(`{${k}}`, obj[k])
|
|
51
|
-
}
|
|
52
|
-
return text
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
w.flatten = (obj, text) => {
|
|
56
|
-
if (!(obj instanceof Array) && typeof Object.values(obj)[0] === 'string') {
|
|
57
|
-
return w.flattenMap(obj, text)
|
|
58
|
-
}
|
|
59
|
-
let htmls = ''
|
|
60
|
-
if (obj instanceof Array) obj = { ...obj }
|
|
61
|
-
for (let k in obj) {
|
|
62
|
-
let html = text.replaceAll('{$key}', k)
|
|
63
|
-
for (let j in obj[k]) {
|
|
64
|
-
const val = typeof obj[k] === 'object' ? obj[k][j] : obj[k]
|
|
65
|
-
html = html.replaceAll('{_key_}', j).replaceAll('{$val}', val)
|
|
66
|
-
html = html.replaceAll(`{${j}}`, val)
|
|
67
|
-
}
|
|
68
|
-
htmls += html
|
|
69
|
-
}
|
|
70
|
-
return htmls
|
|
71
|
-
}
|
|
72
|
-
|
|
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.
|
|
73
56
|
w.e = (name, fn = {}, style = {}) => {
|
|
57
|
+
console.log(`registering component ${name}`)
|
|
74
58
|
customElements.define(name, class extends HTMLElement {
|
|
75
59
|
connectedCallback() {
|
|
60
|
+
// Apply styles to element
|
|
76
61
|
Object.assign(this.style, style)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
if(this.init) this.init(this)
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
w.element = (
|
|
87
|
-
name,
|
|
88
|
-
{ onMount = x => x, beforeData = (x) => x, style, template = '', fn = {} }
|
|
89
|
-
) => {
|
|
90
|
-
customElements.define(
|
|
91
|
-
name,
|
|
92
|
-
class extends HTMLElement {
|
|
93
|
-
connectedCallback(props) {
|
|
94
|
-
onMount(this)
|
|
95
|
-
if (style) {
|
|
96
|
-
const s = document.createElement('style')
|
|
97
|
-
s.innerHTML = `${name} {${style}}`
|
|
98
|
-
d.body.appendChild(s)
|
|
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)
|
|
99
66
|
}
|
|
100
|
-
|
|
101
|
-
|
|
67
|
+
} else {
|
|
68
|
+
// Object mode: assign all methods/properties to element
|
|
102
69
|
Object.assign(this, fn)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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)
|
|
108
76
|
}
|
|
109
77
|
}
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (window.components) {
|
|
114
|
-
for (let name in window.components) w.e(name, window.components[name], window.components[name]?.style)
|
|
78
|
+
})
|
|
115
79
|
}
|
|
116
80
|
|
|
81
|
+
// Reactive state: automatically updates elements when state changes
|
|
117
82
|
w.state = new Proxy({}, {
|
|
118
83
|
set: async (obj, prop, value) => {
|
|
119
84
|
await w.ready()
|
|
120
85
|
console.log('state change:', prop, value)
|
|
121
|
-
|
|
86
|
+
// Skip update if value hasn't changed
|
|
87
|
+
if (obj[prop] === value) {
|
|
122
88
|
return true
|
|
123
89
|
}
|
|
124
|
-
|
|
90
|
+
// Find all elements with matching data attribute and call their set() method
|
|
91
|
+
for (const e of w.$$(`[data="${prop}"]`)) {
|
|
125
92
|
if (e.set) e.set(value)
|
|
126
93
|
}
|
|
127
94
|
obj[prop] = value
|
|
128
|
-
return
|
|
95
|
+
return true
|
|
129
96
|
},
|
|
130
97
|
get: (obj, prop, receiver) => {
|
|
98
|
+
// Special property to get entire state object
|
|
131
99
|
if (prop == '_all') return obj
|
|
132
100
|
return obj[prop]
|
|
133
101
|
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
w.get = async (url,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
let f = await fetch(url, options)
|
|
144
|
-
if (!f.ok) throw Error(`Could not fetch ${url}`)
|
|
145
|
-
data = await f.json()
|
|
146
|
-
}
|
|
147
|
-
if (transform) {
|
|
148
|
-
console.log('transforming ' + data)
|
|
149
|
-
data = transform(data)
|
|
150
|
-
}
|
|
151
|
-
if (key) w.state[key] = data
|
|
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
|
|
152
111
|
return data
|
|
153
112
|
}
|
|
154
113
|
|
|
114
|
+
// Stream data via Server-Sent Events (SSE)
|
|
155
115
|
w.stream = async (url, key) => {
|
|
156
116
|
const ev = new EventSource(url)
|
|
157
|
-
ev.onmessage = (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
}
|
|
161
125
|
}
|
|
126
|
+
ev.onerror = (e) => {
|
|
127
|
+
showError('EventSource error', '', '', '')
|
|
128
|
+
ev.close()
|
|
129
|
+
}
|
|
130
|
+
return ev
|
|
162
131
|
}
|
|
163
132
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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]
|
|
184
158
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (e.attr?.stream) {
|
|
189
|
-
e.stream = w.stream.bind(null, e.pr.stream, null, window[e.pr.transform], e.id)
|
|
159
|
+
} else if (val === undefined) {
|
|
160
|
+
// Fallback to obj itself if key not found
|
|
161
|
+
val = obj
|
|
190
162
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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}`, '', '', '')
|
|
199
220
|
}
|
|
200
221
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
if (e.attr.value) {
|
|
206
|
-
let o = e.attr.value
|
|
207
|
-
try { o = JSON.parse(o) } catch (e) { }
|
|
208
|
-
w.state[dta] = o
|
|
222
|
+
this.set(data)
|
|
223
|
+
} catch (err) {
|
|
224
|
+
showError(err.message || err, '', '', '')
|
|
209
225
|
}
|
|
210
226
|
}
|
|
211
|
-
})
|
|
212
227
|
}
|
|
213
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)
|
|
214
234
|
Object.assign(window, w);
|
|
215
235
|
|
|
236
|
+
// Initialize: wait for DOM, then enhance all divs with data binding
|
|
216
237
|
(async () => {
|
|
217
|
-
|
|
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
|
+
}
|
|
218
248
|
})()
|