zauberflote 1.0.1 → 1.0.2
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/package.json +1 -1
- package/src/ui.js +132 -5
package/package.json
CHANGED
package/src/ui.js
CHANGED
|
@@ -2,11 +2,77 @@ const ui = (() => {
|
|
|
2
2
|
const pendingApps = [];
|
|
3
3
|
let autoMountScheduled = false;
|
|
4
4
|
|
|
5
|
+
// Helper to wrap builders with Proxy for better error messages
|
|
6
|
+
function wrapBuilder(instance, className, chainHistory = []) {
|
|
7
|
+
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(instance))
|
|
8
|
+
.filter(m => m !== 'constructor' && typeof instance[m] === 'function');
|
|
9
|
+
|
|
10
|
+
return new Proxy(instance, {
|
|
11
|
+
get(target, prop) {
|
|
12
|
+
// Track chain for error messages
|
|
13
|
+
target._chainHistory = chainHistory;
|
|
14
|
+
|
|
15
|
+
if (prop in target) {
|
|
16
|
+
const value = target[prop];
|
|
17
|
+
if (typeof value === 'function') {
|
|
18
|
+
return function(...args) {
|
|
19
|
+
const result = value.apply(target, args);
|
|
20
|
+
// If result is a builder object, wrap it too
|
|
21
|
+
if (result && typeof result === 'object' && result.constructor &&
|
|
22
|
+
result.constructor.name.includes('Builder')) {
|
|
23
|
+
// Format arguments nicely for the chain
|
|
24
|
+
const formatArg = (a) => {
|
|
25
|
+
if (typeof a === 'string') {
|
|
26
|
+
const short = a.length > 20 ? a.slice(0, 20) + '...' : a;
|
|
27
|
+
return `"${short.replace(/\n/g, ' ')}"`;
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(a)) return `[${a.length} items]`;
|
|
30
|
+
if (typeof a === 'object' && a !== null) return '{...}';
|
|
31
|
+
return String(a);
|
|
32
|
+
};
|
|
33
|
+
const argStr = args.length > 0 ? `(${args.map(formatArg).join(', ')})` : '()';
|
|
34
|
+
const newChain = [...chainHistory, `.${prop}${argStr}`];
|
|
35
|
+
return wrapBuilder(result, result.constructor.name, newChain);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Property doesn't exist - provide helpful error
|
|
44
|
+
if (typeof prop === 'string' && !prop.startsWith('_')) {
|
|
45
|
+
const suggestion = methods.find(m => m.toLowerCase().includes(prop.toLowerCase()));
|
|
46
|
+
|
|
47
|
+
// Format chain nicely - show last 5 calls max
|
|
48
|
+
const recentChain = chainHistory.slice(-5);
|
|
49
|
+
const chainStr = recentChain.length > 0
|
|
50
|
+
? (chainHistory.length > 5 ? ' ...\n' : '') + recentChain.map(c => ` ${c}`).join('\n')
|
|
51
|
+
: ' (start of chain)';
|
|
52
|
+
|
|
53
|
+
let errorMsg = `\n[ui.js] ❌ ${className} has no method "${prop}"\n\n`;
|
|
54
|
+
errorMsg += `Available methods:\n ${methods.join(', ')}\n\n`;
|
|
55
|
+
errorMsg += `Chain (last ${Math.min(chainHistory.length, 5)} calls):\n${chainStr}\n .${prop}() ← ERROR HERE\n`;
|
|
56
|
+
if (suggestion) {
|
|
57
|
+
errorMsg += `\n💡 Did you mean: "${suggestion}"?\n`;
|
|
58
|
+
}
|
|
59
|
+
console.error(errorMsg);
|
|
60
|
+
|
|
61
|
+
// Return a function that throws to give a cleaner stack trace
|
|
62
|
+
return function() {
|
|
63
|
+
throw new Error(`[ui.js] ${className}.${prop} is not a function. See console for available methods.`);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
5
71
|
function app(title) {
|
|
6
72
|
const builder = new AppBuilder(title);
|
|
7
73
|
pendingApps.push(builder);
|
|
8
74
|
scheduleAutoMount();
|
|
9
|
-
return builder;
|
|
75
|
+
return wrapBuilder(builder, 'AppBuilder', [`ui.app("${title || 'App'}")`]);
|
|
10
76
|
}
|
|
11
77
|
|
|
12
78
|
class AppBuilder {
|
|
@@ -100,6 +166,7 @@ const ui = (() => {
|
|
|
100
166
|
this.jsonConfig = null;
|
|
101
167
|
this.textConfig = null;
|
|
102
168
|
this.markdownConfig = null;
|
|
169
|
+
this.htmlContent = null;
|
|
103
170
|
this.customRenderer = null;
|
|
104
171
|
this.renderHook = null;
|
|
105
172
|
this.autoConfig = null;
|
|
@@ -169,6 +236,11 @@ const ui = (() => {
|
|
|
169
236
|
this.storeMap = Object.assign({}, this.storeMap, map);
|
|
170
237
|
return this;
|
|
171
238
|
}
|
|
239
|
+
hidden() {
|
|
240
|
+
// Signature: hide section output (useful for store-only reads).
|
|
241
|
+
this.suppressOutput = true;
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
172
244
|
storeView(key, template) {
|
|
173
245
|
// Signature: chapter-3 JWT/CSRF token display.
|
|
174
246
|
this.storeViewConfig = { key, template: template || `{{${key}}}` };
|
|
@@ -189,6 +261,11 @@ const ui = (() => {
|
|
|
189
261
|
this.textConfig = { text: text || '', path };
|
|
190
262
|
return this;
|
|
191
263
|
}
|
|
264
|
+
html(content) {
|
|
265
|
+
// Signature: static HTML content block.
|
|
266
|
+
this.htmlContent = content || '';
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
192
269
|
markdown(text, path) {
|
|
193
270
|
// Signature: chapter-4/4-1-validation, chapter-0 UI language docs.
|
|
194
271
|
this.markdownConfig = { text: text || '', path };
|
|
@@ -292,6 +369,15 @@ const ui = (() => {
|
|
|
292
369
|
this.path = path;
|
|
293
370
|
return this;
|
|
294
371
|
}
|
|
372
|
+
delete(path) {
|
|
373
|
+
// Alias for del() - more intuitive name.
|
|
374
|
+
return this.del(path);
|
|
375
|
+
}
|
|
376
|
+
confirm(message) {
|
|
377
|
+
// Signature: show confirmation dialog before action.
|
|
378
|
+
this.confirmMessage = message || 'Are you sure?';
|
|
379
|
+
return this;
|
|
380
|
+
}
|
|
295
381
|
upload(path) {
|
|
296
382
|
// Signature: chapter-4/4-8-uploads.
|
|
297
383
|
this.method = 'UPLOAD';
|
|
@@ -564,6 +650,13 @@ const ui = (() => {
|
|
|
564
650
|
root.appendChild(markdownWrap);
|
|
565
651
|
}
|
|
566
652
|
|
|
653
|
+
// Static HTML content
|
|
654
|
+
if (section.htmlContent) {
|
|
655
|
+
const htmlWrap = document.createElement('div');
|
|
656
|
+
htmlWrap.innerHTML = section.htmlContent;
|
|
657
|
+
root.appendChild(htmlWrap);
|
|
658
|
+
}
|
|
659
|
+
|
|
567
660
|
let customWrap = null;
|
|
568
661
|
if (section.customRenderer) {
|
|
569
662
|
customWrap = document.createElement('div');
|
|
@@ -954,6 +1047,10 @@ const ui = (() => {
|
|
|
954
1047
|
btn.className = 'rounded border border-slate-300 px-3 py-2 text-sm';
|
|
955
1048
|
btn.textContent = action.label;
|
|
956
1049
|
btn.addEventListener('click', async () => {
|
|
1050
|
+
// Confirmation dialog if configured
|
|
1051
|
+
if (action.confirmMessage && !window.confirm(action.confirmMessage)) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
957
1054
|
const fields = action.fieldList.length > 0 ? action.fieldList : section.rowFields;
|
|
958
1055
|
const body = collectFields(fields, rowInputs);
|
|
959
1056
|
const path = renderTemplate(action.path, row, section.appStore);
|
|
@@ -991,6 +1088,10 @@ const ui = (() => {
|
|
|
991
1088
|
btn.className = 'rounded border border-slate-300 px-3 py-2 text-sm';
|
|
992
1089
|
btn.textContent = action.label;
|
|
993
1090
|
btn.addEventListener('click', async () => {
|
|
1091
|
+
// Confirmation dialog if configured
|
|
1092
|
+
if (action.confirmMessage && !window.confirm(action.confirmMessage)) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
994
1095
|
const body = Object.assign({}, action.bodyMap, collectFields(action.fieldList, fieldInputs, sectionInputs));
|
|
995
1096
|
applySetMap(appStore, action.setMap, body, sectionInputs);
|
|
996
1097
|
applyAdjustments(appStore, action.adjustments, sectionInputs);
|
|
@@ -1369,6 +1470,10 @@ const ui = (() => {
|
|
|
1369
1470
|
|
|
1370
1471
|
function getPath(obj, path) {
|
|
1371
1472
|
if (!obj || !path) return undefined;
|
|
1473
|
+
if (typeof path !== 'string') {
|
|
1474
|
+
console.error(`[ui.js] getPath expected a string path, got ${typeof path}:`, path);
|
|
1475
|
+
return undefined;
|
|
1476
|
+
}
|
|
1372
1477
|
return path.split('.').reduce((acc, part) => (acc ? acc[part] : undefined), obj);
|
|
1373
1478
|
}
|
|
1374
1479
|
|
|
@@ -1376,15 +1481,37 @@ const ui = (() => {
|
|
|
1376
1481
|
if (!bindings || bindings.length === 0) return;
|
|
1377
1482
|
bindings.forEach(({ field, input }) => {
|
|
1378
1483
|
if (!field.optionsFrom) return;
|
|
1379
|
-
|
|
1380
|
-
|
|
1484
|
+
|
|
1485
|
+
// Handle optionsFrom as object { store: "key", value: "id", label: "name" } or string "key"
|
|
1486
|
+
let storePath, valueKey, labelKey;
|
|
1487
|
+
if (typeof field.optionsFrom === 'string') {
|
|
1488
|
+
storePath = field.optionsFrom;
|
|
1489
|
+
valueKey = field.optionValue || 'id';
|
|
1490
|
+
labelKey = field.optionLabel || 'name';
|
|
1491
|
+
} else if (typeof field.optionsFrom === 'object') {
|
|
1492
|
+
storePath = field.optionsFrom.store;
|
|
1493
|
+
valueKey = field.optionsFrom.value || field.optionValue || 'id';
|
|
1494
|
+
labelKey = field.optionsFrom.label || field.optionLabel || 'name';
|
|
1495
|
+
} else {
|
|
1496
|
+
console.error(`[ui.js] optionsFrom must be a string or object, got:`, field.optionsFrom);
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const list = getPath(store, storePath);
|
|
1501
|
+
if (!Array.isArray(list)) {
|
|
1502
|
+
if (list !== undefined) {
|
|
1503
|
+
console.warn(`[ui.js] optionsFrom "${storePath}" is not an array:`, list);
|
|
1504
|
+
}
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1381
1508
|
const current = input.value;
|
|
1382
1509
|
input.innerHTML = '';
|
|
1383
1510
|
list.forEach((row) => {
|
|
1384
1511
|
const option = document.createElement('option');
|
|
1385
|
-
const value =
|
|
1512
|
+
const value = getPath(row, valueKey) ?? row.id ?? row.name ?? row.value ?? '';
|
|
1386
1513
|
option.value = value;
|
|
1387
|
-
const label =
|
|
1514
|
+
const label = getPath(row, labelKey) ?? String(value);
|
|
1388
1515
|
option.textContent = label;
|
|
1389
1516
|
input.appendChild(option);
|
|
1390
1517
|
});
|