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/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: '2023-07-29 0.21.2' }
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
- window.onerror = (err, l, n) => {
5
- document.write(`<h2 style="border:8px solid Tomato;">${[l, 'line: ' + n, err].join('<br>')}</h2>`)
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 ($(`script[src="${src}"]`)) return r(true)
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
- w.loadCSS = (src) => {
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.onreadystatechange = () => {
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
- w.flattenMap = (obj, text) => {
42
- let template = ''
43
- if (text.match(/\$key|\$val/i)) {
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
- Object.assign(this, fn)
78
- Object.keys(fn).filter(k=>k.match(/click/)).forEach(k=>{
79
- this.addEventListener(k, fn[k], true)
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
- this.template = template
101
- if (!this.template.match('{')) this.innerHTML = this.template
67
+ } else {
68
+ // Object mode: assign all methods/properties to element
102
69
  Object.assign(this, fn)
103
- }
104
- set(o) {
105
- o = beforeData(o)
106
- this.innerHTML = w.flatten(o, this.template)
107
- return o
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
- if (this[prop] === value) {
86
+ // Skip update if value hasn't changed
87
+ if (obj[prop] === value) {
122
88
  return true
123
89
  }
124
- for (const e of $$(`[data=${prop}]`)) {
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 value
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, options = {}, transform, key) => {
138
- console.log(`fetching ${url}`)
139
- let data
140
- if (url.startsWith('{') || url.startsWith('[')) {
141
- data = JSON.parse(url)
142
- } else {
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 = (ev) => {
158
- const data = JSON.parse(ev.data)
159
- if (key) w.state[key] = data
160
- return data
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
- w.start = async () => {
165
- await w.ready();
166
- [...$$('*')].map(e => {
167
- e.attr = {};
168
- [...e.attributes].map((a) => (e.attr[a.name] = a.value))
169
- if (e.attr.fetch) {
170
- e.fetch = async () => {
171
- let template = e.innerHTML
172
- let ignore = template.match(/<!--IGNORE-->.*>/gms) || ''
173
- if(ignore)
174
- template = template.replace(ignore, '')
175
- const obj = await w.get(e.attr.fetch, {}, null, e.attr.data)
176
- e.innerHTML = w.flatten(obj, template) + ignore
177
- let pos = 0
178
- for(c in e.children) {
179
- const ele = e.children[c]
180
- if(typeof ele === 'object' && 'set' in ele)
181
- e.children[c].set(obj[pos++])
182
- }
183
- return obj
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
- if (!e.hasAttribute('defer'))
186
- e.fetch()
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
- let dta = e.attr?.data
192
- if (dta) {
193
- console.log(`reactive ${e} ${dta}`)
194
- if (!e.set) {
195
- if (e.innerHTML) {
196
- e.template = e.innerHTML
197
- if (e.innerHTML.match('{') && !e.attr.preserve) {
198
- e.innerHTML = ''
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
- e.set = (o) => {
202
- e.innerHTML = w.flatten(o, e.template) || o
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
- await w.start()
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
  })()