enigmatic 0.23.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/e2.js +29 -0
- package/enigmatic.js +51 -5
- package/package.json +1 -1
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,6 +1,8 @@
|
|
|
1
|
+
// Global namespace object and document shortcut
|
|
1
2
|
const w = {}, d = document
|
|
2
3
|
w.enigmatic = { version: '2026-01-03 0.23.0' }
|
|
3
4
|
|
|
5
|
+
// Display error banner at top of page
|
|
4
6
|
const showError = (err, source, line, col) => {
|
|
5
7
|
const errorDiv = d.createElement('div')
|
|
6
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;'
|
|
@@ -8,21 +10,25 @@ const showError = (err, source, line, col) => {
|
|
|
8
10
|
d.body.insertBefore(errorDiv, d.body.firstChild)
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
// Global error handler for uncaught JavaScript errors
|
|
11
14
|
window.onerror = (err, source, line, col) => {
|
|
12
15
|
showError(err, source, line, col)
|
|
13
16
|
return false
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
// Handle unhandled promise rejections
|
|
16
20
|
window.addEventListener('unhandledrejection', (e) => {
|
|
17
21
|
showError(e.reason?.message || e.reason || 'Unhandled Promise Rejection', '', '', '')
|
|
18
22
|
})
|
|
19
23
|
|
|
24
|
+
// DOM query shortcuts
|
|
20
25
|
w.$ = d.querySelector.bind(d)
|
|
21
26
|
w.$$ = d.querySelectorAll.bind(d)
|
|
22
27
|
|
|
28
|
+
// Dynamically load JavaScript file (prevents duplicate loading)
|
|
23
29
|
w.loadJS = (src) => {
|
|
24
30
|
return new Promise((r, j) => {
|
|
25
|
-
if (w.$(`script[src="${src}"]`)) return r(true)
|
|
31
|
+
if (w.$(`script[src="${src}"]`)) return r(true) // Already loaded
|
|
26
32
|
const s = d.createElement('script')
|
|
27
33
|
s.src = src
|
|
28
34
|
s.addEventListener('load', r)
|
|
@@ -31,8 +37,10 @@ w.loadJS = (src) => {
|
|
|
31
37
|
})
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
// Utility: wait for specified milliseconds
|
|
34
41
|
w.wait = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
35
42
|
|
|
43
|
+
// Wait for document to be fully loaded
|
|
36
44
|
w.ready = async () => {
|
|
37
45
|
return new Promise((r) => {
|
|
38
46
|
if (document.readyState === 'complete') return r(true)
|
|
@@ -42,33 +50,44 @@ w.ready = async () => {
|
|
|
42
50
|
})
|
|
43
51
|
}
|
|
44
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.
|
|
45
56
|
w.e = (name, fn = {}, style = {}) => {
|
|
46
57
|
console.log(`registering component ${name}`)
|
|
47
58
|
customElements.define(name, class extends HTMLElement {
|
|
48
59
|
connectedCallback() {
|
|
60
|
+
// Apply styles to element
|
|
49
61
|
Object.assign(this.style, style)
|
|
50
62
|
if (typeof fn === 'function') {
|
|
63
|
+
// Function mode: create set() method that renders HTML from function
|
|
51
64
|
this.set = (data) => {
|
|
52
65
|
this.innerHTML = fn(data)
|
|
53
66
|
}
|
|
54
67
|
} else {
|
|
68
|
+
// Object mode: assign all methods/properties to element
|
|
55
69
|
Object.assign(this, fn)
|
|
70
|
+
// Auto-bind event handlers (click, mouseover, etc.)
|
|
56
71
|
Object.keys(fn).filter(k=>k.match(/click|mouseover/)).forEach(k=>{
|
|
57
72
|
this.addEventListener(k, fn[k], true)
|
|
58
73
|
})
|
|
74
|
+
// Call init if provided
|
|
59
75
|
if(this.init) this.init(this)
|
|
60
76
|
}
|
|
61
77
|
}
|
|
62
78
|
})
|
|
63
79
|
}
|
|
64
80
|
|
|
81
|
+
// Reactive state: automatically updates elements when state changes
|
|
65
82
|
w.state = new Proxy({}, {
|
|
66
83
|
set: async (obj, prop, value) => {
|
|
67
84
|
await w.ready()
|
|
68
85
|
console.log('state change:', prop, value)
|
|
86
|
+
// Skip update if value hasn't changed
|
|
69
87
|
if (obj[prop] === value) {
|
|
70
88
|
return true
|
|
71
89
|
}
|
|
90
|
+
// Find all elements with matching data attribute and call their set() method
|
|
72
91
|
for (const e of w.$$(`[data="${prop}"]`)) {
|
|
73
92
|
if (e.set) e.set(value)
|
|
74
93
|
}
|
|
@@ -76,26 +95,29 @@ w.state = new Proxy({}, {
|
|
|
76
95
|
return true
|
|
77
96
|
},
|
|
78
97
|
get: (obj, prop, receiver) => {
|
|
98
|
+
// Special property to get entire state object
|
|
79
99
|
if (prop == '_all') return obj
|
|
80
100
|
return obj[prop]
|
|
81
101
|
}
|
|
82
102
|
})
|
|
83
103
|
|
|
104
|
+
// Fetch JSON from URL, optionally transform and store in state
|
|
84
105
|
w.get = async (url, opts = {}, transform, key) => {
|
|
85
106
|
const res = await fetch(url, opts)
|
|
86
107
|
if (!res.ok) throw Error(`Could not fetch ${url}`)
|
|
87
108
|
let data = await res.json()
|
|
88
|
-
if (transform) data = transform(data)
|
|
89
|
-
if (key) w.state[key] = data
|
|
109
|
+
if (transform) data = transform(data) // Apply transform function if provided
|
|
110
|
+
if (key) w.state[key] = data // Store in state if key provided
|
|
90
111
|
return data
|
|
91
112
|
}
|
|
92
113
|
|
|
114
|
+
// Stream data via Server-Sent Events (SSE)
|
|
93
115
|
w.stream = async (url, key) => {
|
|
94
116
|
const ev = new EventSource(url)
|
|
95
117
|
ev.onmessage = (e) => {
|
|
96
118
|
try {
|
|
97
119
|
const data = JSON.parse(e.data)
|
|
98
|
-
if (key) w.state[key] = data
|
|
120
|
+
if (key) w.state[key] = data // Update state with each message
|
|
99
121
|
return data
|
|
100
122
|
} catch (err) {
|
|
101
123
|
showError(err.message || err, '', '', '')
|
|
@@ -108,60 +130,79 @@ w.stream = async (url, key) => {
|
|
|
108
130
|
return ev
|
|
109
131
|
}
|
|
110
132
|
|
|
133
|
+
// Template engine: replace {placeholder} with values from object
|
|
134
|
+
// Supports nested properties (e.g., {user.name}) and special vars ($key, $val, $index)
|
|
111
135
|
w.flatten = (obj, text, context = {}) => {
|
|
136
|
+
// Check if template uses special iteration variables
|
|
112
137
|
const hasSpecialVars = /{\$key}|{\$val}|{\$index}/.test(text)
|
|
113
138
|
|
|
139
|
+
// Arrays: map over each item, providing $key, $index, $val
|
|
114
140
|
if (obj instanceof Array)
|
|
115
141
|
return obj.map((o, i) => w.flatten(o, text, { ...context, $key: i, $index: i, $val: o })).join('')
|
|
116
142
|
|
|
143
|
+
// Objects with special vars: iterate over entries, providing $key and $val
|
|
117
144
|
if (hasSpecialVars && obj && typeof obj === 'object' && !Array.isArray(obj))
|
|
118
145
|
return Object.entries(obj).map(([k, v]) => w.flatten(v, text, { ...context, $key: k, $val: v })).join('')
|
|
119
146
|
|
|
147
|
+
// Find all {placeholder} patterns in template
|
|
120
148
|
const m = text.match(/{([^}]*)}/gm) || []
|
|
121
149
|
for (const txt of m) {
|
|
122
150
|
const key = txt.replaceAll(/{|}/g, '')
|
|
151
|
+
// Check context first (for special vars), then object properties
|
|
123
152
|
let val = context[key] !== undefined ? context[key] : undefined
|
|
124
153
|
if (val === undefined && obj && typeof obj === 'object') {
|
|
154
|
+
// Support dot notation: {user.name}
|
|
125
155
|
val = obj
|
|
126
156
|
for (let k of key.split('.')) {
|
|
127
157
|
val = val?.[k]
|
|
128
158
|
}
|
|
129
159
|
} else if (val === undefined) {
|
|
160
|
+
// Fallback to obj itself if key not found
|
|
130
161
|
val = obj
|
|
131
162
|
}
|
|
163
|
+
// Replace placeholder with value (or empty string if undefined)
|
|
132
164
|
text = text.replaceAll(txt, val ?? '')
|
|
133
165
|
}
|
|
134
166
|
return text
|
|
135
167
|
}
|
|
136
168
|
|
|
169
|
+
// Methods added to div elements for data binding
|
|
137
170
|
const props = {
|
|
171
|
+
// Initialize: save template, clear content, optionally fetch data
|
|
138
172
|
async init() {
|
|
173
|
+
// Extract and remove IGNORE blocks from template
|
|
139
174
|
let ignore = this.innerHTML.match(/<!--IGNORE-->.*?<!--ENDIGNORE-->/gms) || []
|
|
140
175
|
if (!ignore.length) {
|
|
141
176
|
this.template = this.innerHTML
|
|
142
177
|
} else {
|
|
143
178
|
this.ignoreblock = ignore
|
|
144
179
|
this.template = this.innerHTML
|
|
180
|
+
// Remove all IGNORE blocks from template
|
|
145
181
|
ignore.forEach(block => {
|
|
146
182
|
this.template = this.template.replace(block, '')
|
|
147
183
|
})
|
|
148
184
|
}
|
|
149
|
-
this.innerHTML = ''
|
|
185
|
+
this.innerHTML = '' // Clear for rendering
|
|
186
|
+
// Auto-fetch unless defer attribute is present
|
|
150
187
|
if (!this.hasAttribute('defer')) {
|
|
151
188
|
this.fetch()
|
|
152
189
|
}
|
|
153
190
|
},
|
|
191
|
+
// Update element content with data using template
|
|
154
192
|
set(o) {
|
|
155
193
|
console.log('setting', this, this.template, o)
|
|
156
194
|
this.innerHTML = w.flatten(o, this.template)
|
|
195
|
+
// Sync to state if data attribute exists
|
|
157
196
|
const dt = this.getAttribute('data')
|
|
158
197
|
if(dt)
|
|
159
198
|
w.state[dt] = o
|
|
160
199
|
},
|
|
200
|
+
// Fetch data from URL or parse inline JSON
|
|
161
201
|
async fetch() {
|
|
162
202
|
try {
|
|
163
203
|
const u = this.getAttribute('fetch')
|
|
164
204
|
if(!u) return
|
|
205
|
+
// Inline JSON: parse directly
|
|
165
206
|
if (u.startsWith('[') || u.startsWith('{'))
|
|
166
207
|
return this.set(JSON.parse(u))
|
|
167
208
|
const opts = {}
|
|
@@ -169,6 +210,7 @@ const props = {
|
|
|
169
210
|
console.log(f)
|
|
170
211
|
if (!f.ok) throw Error(`Could not fetch ${u}`)
|
|
171
212
|
let data = await f.json()
|
|
213
|
+
// Apply transform function if 't' attribute exists
|
|
172
214
|
const tf = this.getAttribute('t')
|
|
173
215
|
if (tf) {
|
|
174
216
|
try {
|
|
@@ -184,14 +226,18 @@ const props = {
|
|
|
184
226
|
}
|
|
185
227
|
}
|
|
186
228
|
|
|
229
|
+
// Register components from window.components object
|
|
187
230
|
for (let name in window.components)
|
|
188
231
|
w.e(name, window.components[name], window.components[name]?.style)
|
|
189
232
|
|
|
233
|
+
// Expose all functions to global scope (window)
|
|
190
234
|
Object.assign(window, w);
|
|
191
235
|
|
|
236
|
+
// Initialize: wait for DOM, then enhance all divs with data binding
|
|
192
237
|
(async () => {
|
|
193
238
|
try {
|
|
194
239
|
await ready()
|
|
240
|
+
// Add props to all divs and initialize them
|
|
195
241
|
for (const i of w.$$('div')) {
|
|
196
242
|
Object.assign(i, props)
|
|
197
243
|
i?.init()
|