web-mojo 2.1.46

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.
Files changed (91) hide show
  1. package/LICENSE +198 -0
  2. package/README.md +510 -0
  3. package/dist/admin.cjs.js +2 -0
  4. package/dist/admin.cjs.js.map +1 -0
  5. package/dist/admin.css +621 -0
  6. package/dist/admin.es.js +7973 -0
  7. package/dist/admin.es.js.map +1 -0
  8. package/dist/auth.cjs.js +2 -0
  9. package/dist/auth.cjs.js.map +1 -0
  10. package/dist/auth.css +804 -0
  11. package/dist/auth.es.js +2168 -0
  12. package/dist/auth.es.js.map +1 -0
  13. package/dist/charts.cjs.js +2 -0
  14. package/dist/charts.cjs.js.map +1 -0
  15. package/dist/charts.css +1002 -0
  16. package/dist/charts.es.js +16 -0
  17. package/dist/charts.es.js.map +1 -0
  18. package/dist/chunks/ContextMenu-BrHqj0fn.js +80 -0
  19. package/dist/chunks/ContextMenu-BrHqj0fn.js.map +1 -0
  20. package/dist/chunks/ContextMenu-gEcpSz56.js +2 -0
  21. package/dist/chunks/ContextMenu-gEcpSz56.js.map +1 -0
  22. package/dist/chunks/DataView-DPryYpEW.js +2 -0
  23. package/dist/chunks/DataView-DPryYpEW.js.map +1 -0
  24. package/dist/chunks/DataView-DjZQrpba.js +843 -0
  25. package/dist/chunks/DataView-DjZQrpba.js.map +1 -0
  26. package/dist/chunks/Dialog-BsRx4eg3.js +2 -0
  27. package/dist/chunks/Dialog-BsRx4eg3.js.map +1 -0
  28. package/dist/chunks/Dialog-DSlctbon.js +1377 -0
  29. package/dist/chunks/Dialog-DSlctbon.js.map +1 -0
  30. package/dist/chunks/FilePreviewView-BmFHzK5K.js +5868 -0
  31. package/dist/chunks/FilePreviewView-BmFHzK5K.js.map +1 -0
  32. package/dist/chunks/FilePreviewView-DcdRl_ta.js +2 -0
  33. package/dist/chunks/FilePreviewView-DcdRl_ta.js.map +1 -0
  34. package/dist/chunks/FormView-CmBuwKGD.js +2 -0
  35. package/dist/chunks/FormView-CmBuwKGD.js.map +1 -0
  36. package/dist/chunks/FormView-DqUBMPJ9.js +5054 -0
  37. package/dist/chunks/FormView-DqUBMPJ9.js.map +1 -0
  38. package/dist/chunks/MetricsChart-CM4CI6eA.js +2095 -0
  39. package/dist/chunks/MetricsChart-CM4CI6eA.js.map +1 -0
  40. package/dist/chunks/MetricsChart-CPidSMaN.js +2 -0
  41. package/dist/chunks/MetricsChart-CPidSMaN.js.map +1 -0
  42. package/dist/chunks/PDFViewer-BNQlnS83.js +2 -0
  43. package/dist/chunks/PDFViewer-BNQlnS83.js.map +1 -0
  44. package/dist/chunks/PDFViewer-Dyo-Oeyd.js +946 -0
  45. package/dist/chunks/PDFViewer-Dyo-Oeyd.js.map +1 -0
  46. package/dist/chunks/Page-B524zSQs.js +351 -0
  47. package/dist/chunks/Page-B524zSQs.js.map +1 -0
  48. package/dist/chunks/Page-BFgj0pAA.js +2 -0
  49. package/dist/chunks/Page-BFgj0pAA.js.map +1 -0
  50. package/dist/chunks/TokenManager-BXNva8Jk.js +287 -0
  51. package/dist/chunks/TokenManager-BXNva8Jk.js.map +1 -0
  52. package/dist/chunks/TokenManager-Bzn4guFm.js +2 -0
  53. package/dist/chunks/TokenManager-Bzn4guFm.js.map +1 -0
  54. package/dist/chunks/TopNav-D3I3_25f.js +371 -0
  55. package/dist/chunks/TopNav-D3I3_25f.js.map +1 -0
  56. package/dist/chunks/TopNav-MDjL4kV0.js +2 -0
  57. package/dist/chunks/TopNav-MDjL4kV0.js.map +1 -0
  58. package/dist/chunks/User-BalfYTEF.js +3 -0
  59. package/dist/chunks/User-BalfYTEF.js.map +1 -0
  60. package/dist/chunks/User-DwIT-CTQ.js +1937 -0
  61. package/dist/chunks/User-DwIT-CTQ.js.map +1 -0
  62. package/dist/chunks/WebApp-B6mgbNn2.js +4767 -0
  63. package/dist/chunks/WebApp-B6mgbNn2.js.map +1 -0
  64. package/dist/chunks/WebApp-DqDowtkl.js +2 -0
  65. package/dist/chunks/WebApp-DqDowtkl.js.map +1 -0
  66. package/dist/chunks/WebSocketClient-D6i85jl2.js +2 -0
  67. package/dist/chunks/WebSocketClient-D6i85jl2.js.map +1 -0
  68. package/dist/chunks/WebSocketClient-Dvl3AYx1.js +297 -0
  69. package/dist/chunks/WebSocketClient-Dvl3AYx1.js.map +1 -0
  70. package/dist/core.css +1181 -0
  71. package/dist/css/web-mojo.css +17 -0
  72. package/dist/css-manifest.json +6 -0
  73. package/dist/docit.cjs.js +2 -0
  74. package/dist/docit.cjs.js.map +1 -0
  75. package/dist/docit.es.js +959 -0
  76. package/dist/docit.es.js.map +1 -0
  77. package/dist/index.cjs.js +2 -0
  78. package/dist/index.cjs.js.map +1 -0
  79. package/dist/index.es.js +2681 -0
  80. package/dist/index.es.js.map +1 -0
  81. package/dist/lightbox.cjs.js +2 -0
  82. package/dist/lightbox.cjs.js.map +1 -0
  83. package/dist/lightbox.css +606 -0
  84. package/dist/lightbox.es.js +3737 -0
  85. package/dist/lightbox.es.js.map +1 -0
  86. package/dist/loader.es.js +115 -0
  87. package/dist/loader.umd.js +85 -0
  88. package/dist/portal.css +2446 -0
  89. package/dist/table.css +639 -0
  90. package/dist/toast.css +181 -0
  91. package/package.json +179 -0
@@ -0,0 +1,4767 @@
1
+ const VERSION = "2.1.45";
2
+ const VERSION_MAJOR = 2;
3
+ const VERSION_MINOR = 1;
4
+ const VERSION_REVISION = 45;
5
+ const BUILD_TIME = "2025-09-06T19:46:44.491Z";
6
+ const VERSION_INFO = {
7
+ full: VERSION,
8
+ major: VERSION_MAJOR,
9
+ minor: VERSION_MINOR,
10
+ revision: VERSION_REVISION,
11
+ buildTime: BUILD_TIME,
12
+ toString() {
13
+ return this.full;
14
+ },
15
+ compare(other) {
16
+ const parseVer = (v) => v.split(".").map(Number);
17
+ const [a1, a2, a3] = parseVer(this.full);
18
+ const [b1, b2, b3] = parseVer(other);
19
+ if (a1 !== b1) return a1 - b1;
20
+ if (a2 !== b2) return a2 - b2;
21
+ return a3 - b3;
22
+ }
23
+ };
24
+ if (typeof window !== "undefined") {
25
+ window.MOJO = window.MOJO || {};
26
+ window.MOJO.VERSION = VERSION;
27
+ window.MOJO.VERSION_INFO = VERSION_INFO;
28
+ window.MOJO.version = VERSION;
29
+ }
30
+ const objectToString = Object.prototype.toString;
31
+ const isArray = Array.isArray || function(obj) {
32
+ return objectToString.call(obj) === "[object Array]";
33
+ };
34
+ const isFunction = function(obj) {
35
+ return typeof obj === "function";
36
+ };
37
+ const isObject = function(obj) {
38
+ return obj != null && typeof obj === "object";
39
+ };
40
+ const escapeHtml = function(string) {
41
+ const entityMap = {
42
+ "&": "&",
43
+ "<": "&lt;",
44
+ ">": "&gt;",
45
+ '"': "&quot;",
46
+ "'": "&#39;",
47
+ "/": "&#x2F;",
48
+ "`": "&#x60;",
49
+ "=": "&#x3D;"
50
+ };
51
+ return String(string).replace(/[&<>"'`=\/]/g, function(s) {
52
+ return entityMap[s];
53
+ });
54
+ };
55
+ class Scanner {
56
+ constructor(string) {
57
+ this.string = string;
58
+ this.tail = string;
59
+ this.pos = 0;
60
+ }
61
+ eos() {
62
+ return this.tail === "";
63
+ }
64
+ scan(re) {
65
+ const match = this.tail.match(re);
66
+ if (!match || match.index !== 0) {
67
+ return "";
68
+ }
69
+ const string = match[0];
70
+ this.tail = this.tail.substring(string.length);
71
+ this.pos += string.length;
72
+ return string;
73
+ }
74
+ scanUntil(re) {
75
+ const index = this.tail.search(re);
76
+ let match;
77
+ switch (index) {
78
+ case -1:
79
+ match = this.tail;
80
+ this.tail = "";
81
+ break;
82
+ case 0:
83
+ match = "";
84
+ break;
85
+ default:
86
+ match = this.tail.substring(0, index);
87
+ this.tail = this.tail.substring(index);
88
+ }
89
+ this.pos += match.length;
90
+ return match;
91
+ }
92
+ }
93
+ class Context {
94
+ constructor(view, parentContext) {
95
+ this.view = view;
96
+ this.cache = { ".": this.view };
97
+ this.parent = parentContext;
98
+ if (!this.view?._cacheId) {
99
+ if (this.view && typeof this.view === "object") {
100
+ this.view._cacheId = Math.random().toString(36).substring(2);
101
+ }
102
+ }
103
+ }
104
+ push(view) {
105
+ return new Context(view, this);
106
+ }
107
+ lookup(name) {
108
+ if (this.renderCache && this.view?._cacheId) {
109
+ const cacheKey = `${this.view._cacheId}:${name}`;
110
+ if (this.renderCache.has(cacheKey)) {
111
+ return this.renderCache.get(cacheKey);
112
+ }
113
+ }
114
+ if (name === ".") {
115
+ return this.view;
116
+ }
117
+ if (name && name.startsWith(".")) {
118
+ let actualName = name.substring(1);
119
+ let shouldIterate = false;
120
+ if (actualName.endsWith("|iter")) {
121
+ actualName = actualName.substring(0, actualName.length - 5);
122
+ shouldIterate = true;
123
+ }
124
+ if (this.view && typeof this.view === "object") {
125
+ let value2;
126
+ if (typeof this.view.getContextValue === "function") {
127
+ try {
128
+ value2 = this.view.getContextValue(actualName);
129
+ if (value2 !== void 0) {
130
+ if (isFunction(value2)) {
131
+ value2 = value2.call(this.view);
132
+ }
133
+ }
134
+ } catch (e) {
135
+ value2 = void 0;
136
+ }
137
+ }
138
+ if (value2 === void 0 && actualName in this.view) {
139
+ value2 = this.view[actualName];
140
+ if (isFunction(value2)) {
141
+ value2 = value2.call(this.view);
142
+ }
143
+ }
144
+ if (isArray(value2)) {
145
+ if (shouldIterate) {
146
+ return value2;
147
+ } else {
148
+ return value2.length > 0;
149
+ }
150
+ }
151
+ if (isObject(value2)) {
152
+ if (shouldIterate) {
153
+ return Object.entries(value2).map(([key, val]) => ({
154
+ key,
155
+ value: val
156
+ }));
157
+ } else {
158
+ return Object.keys(value2).length > 0;
159
+ }
160
+ }
161
+ return value2;
162
+ }
163
+ return void 0;
164
+ }
165
+ const cache = this.cache;
166
+ let value;
167
+ if (cache.hasOwnProperty(name)) {
168
+ value = cache[name];
169
+ } else {
170
+ let context = this;
171
+ let intermediateValue;
172
+ let names;
173
+ let index;
174
+ let lookupHit = false;
175
+ while (context) {
176
+ if (context.view && typeof context.view.getContextValue === "function") {
177
+ try {
178
+ intermediateValue = context.view.getContextValue(name);
179
+ if (intermediateValue !== void 0) {
180
+ lookupHit = true;
181
+ }
182
+ } catch (e) {
183
+ lookupHit = false;
184
+ }
185
+ }
186
+ if (!lookupHit) {
187
+ if (name.indexOf(".") > 0) {
188
+ intermediateValue = context.view;
189
+ names = name.split(".");
190
+ index = 0;
191
+ while (intermediateValue != null && index < names.length) {
192
+ if (intermediateValue && typeof intermediateValue.getContextValue === "function" && index < names.length) {
193
+ try {
194
+ const remainingPath = names.slice(index).join(".");
195
+ intermediateValue = intermediateValue.getContextValue(remainingPath);
196
+ index = names.length;
197
+ if (intermediateValue !== void 0) {
198
+ lookupHit = true;
199
+ }
200
+ } catch (e) {
201
+ if (index === names.length - 1) {
202
+ lookupHit = hasProperty(intermediateValue, names[index]) || primitiveHasOwnProperty(intermediateValue, names[index]);
203
+ }
204
+ intermediateValue = intermediateValue[names[index++]];
205
+ }
206
+ } else {
207
+ if (index === names.length - 1) {
208
+ lookupHit = hasProperty(intermediateValue, names[index]) || primitiveHasOwnProperty(intermediateValue, names[index]);
209
+ }
210
+ intermediateValue = intermediateValue[names[index++]];
211
+ }
212
+ }
213
+ } else {
214
+ intermediateValue = context.view[name];
215
+ lookupHit = hasProperty(context.view, name);
216
+ }
217
+ }
218
+ if (lookupHit) {
219
+ value = intermediateValue;
220
+ break;
221
+ }
222
+ context = context.parent;
223
+ }
224
+ cache[name] = value;
225
+ }
226
+ if (isFunction(value)) value = value.call(this.view);
227
+ if (this.renderCache && this.view?._cacheId) {
228
+ const cacheKey = `${this.view._cacheId}:${name}`;
229
+ this.renderCache.set(cacheKey, value);
230
+ }
231
+ return value;
232
+ }
233
+ }
234
+ function hasProperty(obj, propName) {
235
+ return obj != null && typeof obj === "object" && propName in obj;
236
+ }
237
+ function primitiveHasOwnProperty(primitive, propName) {
238
+ return primitive != null && typeof primitive !== "object" && primitive.hasOwnProperty && primitive.hasOwnProperty(propName);
239
+ }
240
+ class Writer {
241
+ constructor() {
242
+ this.templateCache = /* @__PURE__ */ new Map();
243
+ }
244
+ clearCache() {
245
+ this.templateCache.clear();
246
+ }
247
+ parse(template, tags) {
248
+ tags = tags || ["{{", "}}"];
249
+ const cacheKey = template + ":" + tags.join(":");
250
+ let tokens = this.templateCache.get(cacheKey);
251
+ if (tokens == null) {
252
+ tokens = this.parseTemplate(template, tags);
253
+ this.templateCache.set(cacheKey, tokens);
254
+ }
255
+ return tokens;
256
+ }
257
+ parseTemplate(template, tags) {
258
+ if (!template) return [];
259
+ const openingTag = tags[0];
260
+ const closingTag = tags[1];
261
+ const scanner = new Scanner(template);
262
+ const tokens = [];
263
+ let start, type, value, chr, token;
264
+ const openingTagRe = new RegExp(escapeRegExp(openingTag) + "\\s*");
265
+ const closingTagRe = new RegExp("\\s*" + escapeRegExp(closingTag));
266
+ const closingCurlyRe = new RegExp("\\s*" + escapeRegExp("}" + closingTag));
267
+ while (!scanner.eos()) {
268
+ start = scanner.pos;
269
+ value = scanner.scanUntil(openingTagRe);
270
+ if (value) {
271
+ for (let i = 0; i < value.length; ++i) {
272
+ chr = value.charAt(i);
273
+ if (chr === "\n") {
274
+ tokens.push(["text", chr]);
275
+ } else {
276
+ tokens.push(["text", chr]);
277
+ }
278
+ }
279
+ }
280
+ if (!scanner.scan(openingTagRe)) break;
281
+ type = scanner.scan(/[#^\/>{&=!]/);
282
+ if (!type) type = "name";
283
+ scanner.scan(/\s*/);
284
+ if (type === "=") {
285
+ value = scanner.scanUntil(/\s*=/);
286
+ scanner.scan(/\s*=/);
287
+ scanner.scanUntil(closingTagRe);
288
+ } else if (type === "{") {
289
+ value = scanner.scanUntil(closingCurlyRe);
290
+ scanner.scan(closingCurlyRe);
291
+ type = "&";
292
+ } else {
293
+ value = scanner.scanUntil(closingTagRe);
294
+ }
295
+ scanner.scan(closingTagRe);
296
+ if (type === "#" || type === "^") {
297
+ token = [type, value, start, scanner.pos];
298
+ tokens.push(token);
299
+ } else if (type === "/") {
300
+ let openSection;
301
+ for (let i = tokens.length - 1; i >= 0; --i) {
302
+ if (tokens[i][0] === "#" || tokens[i][0] === "^") {
303
+ if (tokens[i][1] === value) {
304
+ openSection = tokens[i];
305
+ break;
306
+ }
307
+ }
308
+ }
309
+ if (openSection) {
310
+ if (openSection.length === 4) {
311
+ openSection.push(scanner.pos);
312
+ }
313
+ }
314
+ tokens.push([type, value, start, scanner.pos]);
315
+ } else {
316
+ tokens.push([type, value, start, scanner.pos]);
317
+ }
318
+ }
319
+ return this.nestSections(this.squashTokens(tokens));
320
+ }
321
+ squashTokens(tokens) {
322
+ const squashedTokens = [];
323
+ let token, lastToken;
324
+ for (let i = 0; i < tokens.length; ++i) {
325
+ token = tokens[i];
326
+ if (token) {
327
+ if (token[0] === "text" && lastToken && lastToken[0] === "text") {
328
+ lastToken[1] += token[1];
329
+ lastToken[3] = token[3];
330
+ } else {
331
+ squashedTokens.push(token);
332
+ lastToken = token;
333
+ }
334
+ }
335
+ }
336
+ return squashedTokens;
337
+ }
338
+ nestSections(tokens) {
339
+ const nestedTokens = [];
340
+ let collector = nestedTokens;
341
+ const sections = [];
342
+ for (let i = 0; i < tokens.length; ++i) {
343
+ const token = tokens[i];
344
+ switch (token[0]) {
345
+ case "#":
346
+ case "^":
347
+ const sectionToken = [
348
+ token[0],
349
+ // type ('#' or '^')
350
+ token[1],
351
+ // section name
352
+ token[2],
353
+ // start position
354
+ token[3],
355
+ // end position after opening tag
356
+ [],
357
+ // children array
358
+ token[4] || null
359
+ // closing position (if set during parsing)
360
+ ];
361
+ collector.push(sectionToken);
362
+ sections.push(sectionToken);
363
+ collector = sectionToken[4];
364
+ break;
365
+ case "/":
366
+ const section = sections.pop();
367
+ if (section) {
368
+ section[5] = token[2];
369
+ collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
370
+ }
371
+ break;
372
+ default:
373
+ collector.push(token);
374
+ }
375
+ }
376
+ return nestedTokens;
377
+ }
378
+ render(template, view, partials, config) {
379
+ const tags = this.getConfigTags(config) || ["{{", "}}"];
380
+ const tokens = this.parse(template, tags);
381
+ const renderCache = /* @__PURE__ */ new Map();
382
+ return this.renderTokens(tokens, new Context(view), partials, template, config, renderCache);
383
+ }
384
+ renderTokens(tokens, context, partials, originalTemplate, config, renderCache) {
385
+ if (renderCache && !context.renderCache) {
386
+ context.renderCache = renderCache;
387
+ }
388
+ let buffer = "";
389
+ for (let i = 0; i < tokens.length; ++i) {
390
+ const token = tokens[i];
391
+ let value;
392
+ switch (token[0]) {
393
+ case "#":
394
+ value = context.lookup(token[1]);
395
+ if (!value) continue;
396
+ const childTokens = token[4];
397
+ if (!childTokens || !isArray(childTokens)) {
398
+ console.warn(`MUSTACHE WARNING - Section ${token[1]} has no child tokens:`, token);
399
+ continue;
400
+ }
401
+ if (isArray(value)) {
402
+ for (let j = 0; j < value.length; ++j) {
403
+ const itemContext = context.push(value[j]);
404
+ if (context.renderCache) {
405
+ itemContext.renderCache = context.renderCache;
406
+ }
407
+ const itemResult = this.renderTokens(childTokens, itemContext, partials, originalTemplate, config, renderCache);
408
+ buffer += itemResult;
409
+ }
410
+ } else if (typeof value === "object" || typeof value === "string" || typeof value === "number") {
411
+ const pushedContext = context.push(value);
412
+ if (context.renderCache) {
413
+ pushedContext.renderCache = context.renderCache;
414
+ }
415
+ buffer += this.renderTokens(childTokens, pushedContext, partials, originalTemplate, config, renderCache);
416
+ } else if (isFunction(value)) {
417
+ const text = originalTemplate == null ? null : originalTemplate.slice(token[3], token[5]);
418
+ value = value.call(context.view, text, (template) => this.render(template, context.view, partials, config));
419
+ if (value != null) buffer += value;
420
+ } else if (value) {
421
+ buffer += this.renderTokens(childTokens, context, partials, originalTemplate, config, renderCache);
422
+ }
423
+ break;
424
+ case "^":
425
+ value = context.lookup(token[1]);
426
+ if (!value || isArray(value) && value.length === 0) {
427
+ const childTokens2 = token[4];
428
+ if (childTokens2 && isArray(childTokens2)) {
429
+ buffer += this.renderTokens(childTokens2, context, partials, originalTemplate, config, renderCache);
430
+ }
431
+ }
432
+ break;
433
+ case ">":
434
+ if (!partials) continue;
435
+ value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
436
+ if (value != null) {
437
+ buffer += this.render(value, context.view, partials, config);
438
+ }
439
+ break;
440
+ case "&":
441
+ value = context.lookup(token[1]);
442
+ if (value != null) buffer += value;
443
+ break;
444
+ case "name":
445
+ value = context.lookup(token[1]);
446
+ if (value != null) buffer += escapeHtml(value);
447
+ break;
448
+ case "text":
449
+ buffer += token[1];
450
+ break;
451
+ }
452
+ }
453
+ return buffer;
454
+ }
455
+ getConfigTags(config) {
456
+ if (isObject(config) && isArray(config.tags)) {
457
+ return config.tags;
458
+ }
459
+ return null;
460
+ }
461
+ }
462
+ function escapeRegExp(string) {
463
+ return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
464
+ }
465
+ const defaultWriter = new Writer();
466
+ const Mustache = {
467
+ name: "MOJO Mustache",
468
+ version: "1.0.0",
469
+ tags: ["{{", "}}"],
470
+ Scanner,
471
+ Context,
472
+ Writer,
473
+ escape: escapeHtml,
474
+ clearCache() {
475
+ return defaultWriter.clearCache();
476
+ },
477
+ parse(template, tags) {
478
+ return defaultWriter.parse(template, tags);
479
+ },
480
+ render(template, view, partials, config) {
481
+ if (typeof template !== "string") {
482
+ throw new TypeError('Invalid template! Template should be a "string"');
483
+ }
484
+ return defaultWriter.render(template, view, partials, config);
485
+ }
486
+ };
487
+ const GENERIC_AVATAR_SVG = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2NlZDRkYSI+PHBhdGggZD0iTTEyIDEyYzIuMjEgMCA0LTEuNzkgNC00cy0xLjc5LTQtNC00LTQgMS43OS00IDQgMS43OSA0IDQgNHptMCAyYy0yLjY3IDAtOCAxLjM0LTggNHYyaDE2di0yYzAtMi42Ni01LjMzLTQtOC00eiIvPjwvc3ZnPg==";
488
+ class DataFormatter {
489
+ constructor() {
490
+ this.formatters = /* @__PURE__ */ new Map();
491
+ this.registerBuiltInFormatters();
492
+ }
493
+ escapeHtml(str) {
494
+ if (str === null || str === void 0) {
495
+ return "";
496
+ }
497
+ const map = {
498
+ "&": "&amp;",
499
+ "<": "&lt;",
500
+ ">": "&gt;",
501
+ '"': "&quot;",
502
+ "'": "&#039;"
503
+ };
504
+ return String(str).replace(/[&<>"']/g, (m) => map[m]);
505
+ }
506
+ /**
507
+ * Register all built-in formatters
508
+ */
509
+ registerBuiltInFormatters() {
510
+ this.register("date", this.date.bind(this));
511
+ this.register("time", this.time.bind(this));
512
+ this.register("datetime", this.datetime.bind(this));
513
+ this.register("relative", this.relative.bind(this));
514
+ this.register("fromNow", this.relative.bind(this));
515
+ this.register("iso", this.iso.bind(this));
516
+ this.register("epoch", (v) => {
517
+ if (v === null || v === void 0 || v === "") return v;
518
+ const num = parseFloat(v);
519
+ if (isNaN(num)) return v;
520
+ return num * 1e3;
521
+ });
522
+ this.register("number", this.number.bind(this));
523
+ this.register("currency", this.currency.bind(this));
524
+ this.register("percent", this.percent.bind(this));
525
+ this.register("filesize", this.filesize.bind(this));
526
+ this.register("ordinal", this.ordinal.bind(this));
527
+ this.register("compact", this.compact.bind(this));
528
+ this.register("uppercase", (v) => String(v).toUpperCase());
529
+ this.register("lowercase", (v) => String(v).toLowerCase());
530
+ this.register("capitalize", this.capitalize.bind(this));
531
+ this.register("truncate", this.truncate.bind(this));
532
+ this.register("truncate_middle", this.truncate_middle.bind(this));
533
+ this.register("slug", this.slug.bind(this));
534
+ this.register("initials", this.initials.bind(this));
535
+ this.register("mask", this.mask.bind(this));
536
+ this.register("email", this.email.bind(this));
537
+ this.register("phone", this.phone.bind(this));
538
+ this.register("url", this.url.bind(this));
539
+ this.register("badge", this.badge.bind(this));
540
+ this.register("status", this.status.bind(this));
541
+ this.register("boolean", this.boolean.bind(this));
542
+ this.register("bool", this.boolean.bind(this));
543
+ this.register("yesno", (v) => this.boolean(v, "Yes", "No"));
544
+ this.register("yesnoicon", this.yesnoicon.bind(this));
545
+ this.register("icon", this.icon.bind(this));
546
+ this.register("avatar", this.avatar.bind(this));
547
+ this.register("image", this.image.bind(this));
548
+ this.register("default", this.default.bind(this));
549
+ this.register("json", this.json.bind(this));
550
+ this.register("raw", (v) => v);
551
+ this.register("custom", (v, fn) => typeof fn === "function" ? fn(v) : v);
552
+ this.register("iter", this.iter.bind(this));
553
+ this.register("keys", (v) => {
554
+ if (v && typeof v === "object" && !Array.isArray(v)) {
555
+ return Object.keys(v);
556
+ }
557
+ return null;
558
+ });
559
+ this.register("values", (v) => {
560
+ if (v && typeof v === "object" && !Array.isArray(v)) {
561
+ return Object.values(v);
562
+ }
563
+ return null;
564
+ });
565
+ this.register("plural", this.plural.bind(this));
566
+ this.register("list", this.formatList.bind(this));
567
+ this.register("duration", this.duration.bind(this));
568
+ this.register("hash", this.hash.bind(this));
569
+ this.register("stripHtml", this.stripHtml.bind(this));
570
+ this.register("highlight", this.highlight.bind(this));
571
+ this.register("pre", (v) => `<pre class="bg-light p-2 rounded border">${this.escapeHtml(String(v))}</pre>`);
572
+ }
573
+ /**
574
+ * Register a custom formatter
575
+ * @param {string} name - Formatter name
576
+ * @param {Function} formatter - Formatter function
577
+ * @returns {DataFormatter} This instance for chaining
578
+ */
579
+ register(name, formatter) {
580
+ if (typeof formatter !== "function") {
581
+ throw new Error(`Formatter must be a function, got ${typeof formatter}`);
582
+ }
583
+ this.formatters.set(name.toLowerCase(), formatter);
584
+ return this;
585
+ }
586
+ /**
587
+ * Apply a formatter
588
+ * @param {string} name - Formatter name
589
+ * @param {*} value - Value to format
590
+ * @param {...*} args - Additional arguments
591
+ * @returns {*} Formatted value
592
+ */
593
+ apply(name, value, ...args) {
594
+ try {
595
+ const formatter = this.formatters.get(name.toLowerCase());
596
+ if (!formatter) {
597
+ console.warn(`Formatter '${name}' not found`);
598
+ return value;
599
+ }
600
+ return formatter(value, ...args);
601
+ } catch (error) {
602
+ console.error(`Error in formatter '${name}':`, error);
603
+ return value;
604
+ }
605
+ }
606
+ /**
607
+ * Process pipe string
608
+ * @param {*} value - Value to format
609
+ * @param {string} pipeString - Pipe string (e.g., "date('YYYY-MM-DD')|uppercase")
610
+ * @returns {*} Formatted value
611
+ */
612
+ pipe(value, pipeString) {
613
+ if (!pipeString) return value;
614
+ const pipes = this.parsePipeString(pipeString);
615
+ return pipes.reduce((currentValue, pipe) => {
616
+ return this.apply(pipe.name, currentValue, ...pipe.args);
617
+ }, value);
618
+ }
619
+ /**
620
+ * Parse pipe string into formatter calls
621
+ * @param {string} pipeString - Pipe string
622
+ * @returns {Array} Array of {name, args} objects
623
+ */
624
+ parsePipeString(pipeString) {
625
+ const pipes = [];
626
+ const tokens = pipeString.split("|").map((s) => s.trim());
627
+ for (const token of tokens) {
628
+ const parsed = this.parseFormatter(token);
629
+ if (parsed) {
630
+ pipes.push(parsed);
631
+ }
632
+ }
633
+ return pipes;
634
+ }
635
+ /**
636
+ * Parse individual formatter with arguments
637
+ * @param {string} token - Formatter token
638
+ * @returns {Object} {name, args} object
639
+ */
640
+ parseFormatter(token) {
641
+ const match = token.match(/^([a-zA-Z_]\w*)\s*(?:\((.*)\))?$/);
642
+ if (!match) return null;
643
+ const [, name, argsString] = match;
644
+ const args = argsString ? this.parseArguments(argsString) : [];
645
+ return { name, args };
646
+ }
647
+ /**
648
+ * Parse formatter arguments
649
+ * @param {string} argsString - Arguments string
650
+ * @returns {Array} Parsed arguments
651
+ */
652
+ parseArguments(argsString) {
653
+ const args = [];
654
+ let current = "";
655
+ let inQuotes = false;
656
+ let quoteChar = null;
657
+ let depth = 0;
658
+ for (let i = 0; i < argsString.length; i++) {
659
+ const char = argsString[i];
660
+ if (!inQuotes && (char === '"' || char === "'")) {
661
+ inQuotes = true;
662
+ quoteChar = char;
663
+ current += char;
664
+ } else if (inQuotes && char === quoteChar && argsString[i - 1] !== "\\") {
665
+ inQuotes = false;
666
+ quoteChar = null;
667
+ current += char;
668
+ } else if (!inQuotes && char === "{") {
669
+ depth++;
670
+ current += char;
671
+ } else if (!inQuotes && char === "}") {
672
+ depth--;
673
+ current += char;
674
+ } else if (!inQuotes && depth === 0 && char === ",") {
675
+ args.push(this.parseValue(current.trim()));
676
+ current = "";
677
+ } else {
678
+ current += char;
679
+ }
680
+ }
681
+ if (current.trim()) {
682
+ args.push(this.parseValue(current.trim()));
683
+ }
684
+ return args;
685
+ }
686
+ /**
687
+ * Parse a single value
688
+ * @param {string} value - Value string
689
+ * @returns {*} Parsed value
690
+ */
691
+ parseValue(value) {
692
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
693
+ return value.slice(1, -1);
694
+ }
695
+ if (value === "true") return true;
696
+ if (value === "false") return false;
697
+ if (value === "null") return null;
698
+ if (value === "undefined") return void 0;
699
+ if (!isNaN(value) && value !== "") {
700
+ return Number(value);
701
+ }
702
+ if (value.startsWith("{") && value.endsWith("}")) {
703
+ try {
704
+ return JSON.parse(value);
705
+ } catch (e) {
706
+ }
707
+ }
708
+ return value;
709
+ }
710
+ // ============= Date/Time Formatters =============
711
+ /**
712
+ * Format date
713
+ * @param {*} value - Date value
714
+ * @param {string} format - Date format pattern
715
+ * @returns {string} Formatted date
716
+ */
717
+ date(value, format = "MM/DD/YYYY") {
718
+ if (!value) return "";
719
+ value = this.normalizeEpoch(value);
720
+ let date;
721
+ if (value instanceof Date) {
722
+ date = value;
723
+ } else if (typeof value === "string") {
724
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
725
+ const [year, month, day] = value.split("-").map(Number);
726
+ date = new Date(year, month - 1, day);
727
+ } else {
728
+ date = new Date(value);
729
+ }
730
+ } else {
731
+ date = new Date(value);
732
+ }
733
+ if (isNaN(date.getTime())) return String(value);
734
+ const tokens = {
735
+ "YYYY": date.getFullYear(),
736
+ "YY": String(date.getFullYear()).slice(-2),
737
+ "MMMM": date.toLocaleDateString("en-US", { month: "long" }),
738
+ "MMM": date.toLocaleDateString("en-US", { month: "short" }),
739
+ "MM": String(date.getMonth() + 1).padStart(2, "0"),
740
+ "M": date.getMonth() + 1,
741
+ "dddd": date.toLocaleDateString("en-US", { weekday: "long" }),
742
+ "ddd": date.toLocaleDateString("en-US", { weekday: "short" }),
743
+ "DD": String(date.getDate()).padStart(2, "0"),
744
+ "D": date.getDate()
745
+ };
746
+ let result = format;
747
+ const tokenPattern = new RegExp(`(${Object.keys(tokens).join("|")})`, "g");
748
+ result = result.replace(tokenPattern, (match) => tokens[match] || match);
749
+ return result;
750
+ }
751
+ /**
752
+ * Format time
753
+ * @param {*} value - Time value
754
+ * @param {string} format - Time format pattern
755
+ * @returns {string} Formatted time
756
+ */
757
+ time(value, format = "HH:mm:ss") {
758
+ if (!value) return "";
759
+ value = this.normalizeEpoch(value);
760
+ const date = value instanceof Date ? value : new Date(value);
761
+ if (isNaN(date.getTime())) return String(value);
762
+ const hours = date.getHours();
763
+ const replacements = {
764
+ "HH": String(hours).padStart(2, "0"),
765
+ "H": hours,
766
+ "hh": String(hours % 12 || 12).padStart(2, "0"),
767
+ "h": hours % 12 || 12,
768
+ "mm": String(date.getMinutes()).padStart(2, "0"),
769
+ "m": date.getMinutes(),
770
+ "ss": String(date.getSeconds()).padStart(2, "0"),
771
+ "s": date.getSeconds(),
772
+ "A": hours >= 12 ? "PM" : "AM",
773
+ "a": hours >= 12 ? "pm" : "am"
774
+ };
775
+ let result = format;
776
+ const sortedKeys = Object.keys(replacements).sort((a, b) => b.length - a.length);
777
+ for (const key of sortedKeys) {
778
+ result = result.replace(new RegExp(key, "g"), replacements[key]);
779
+ }
780
+ return result;
781
+ }
782
+ /**
783
+ * Format date and time
784
+ * @param {*} value - DateTime value
785
+ * @param {string} dateFormat - Date format
786
+ * @param {string} timeFormat - Time format
787
+ * @returns {string} Formatted datetime
788
+ */
789
+ datetime(value, dateFormat = "MM/DD/YYYY", timeFormat = "HH:mm") {
790
+ value = this.normalizeEpoch(value);
791
+ const dateStr = this.date(value, dateFormat);
792
+ const timeStr = this.time(value, timeFormat);
793
+ return dateStr && timeStr ? `${dateStr} ${timeStr}` : "";
794
+ }
795
+ normalizeEpoch(value) {
796
+ if (typeof value !== "number") value = Number(value);
797
+ if (isNaN(value)) return "";
798
+ if (value < 1e11) {
799
+ return value * 1e3;
800
+ } else if (value > 1e12 && value < 1e13) {
801
+ return value;
802
+ } else {
803
+ throw new Error("Value doesn't look like epoch seconds or ms");
804
+ }
805
+ }
806
+ /**
807
+ * Format relative time
808
+ * @param {*} value - Date value
809
+ * @param {boolean} short - Use short format
810
+ * @returns {string} Relative time string
811
+ */
812
+ relative(value, short = false) {
813
+ if (!value) return "";
814
+ value = this.normalizeEpoch(value);
815
+ const date = value instanceof Date ? value : new Date(value);
816
+ if (isNaN(date.getTime())) return String(value);
817
+ const now2 = /* @__PURE__ */ new Date();
818
+ const diffMs = now2 - date;
819
+ const diffSecs = Math.floor(diffMs / 1e3);
820
+ const diffMins = Math.floor(diffSecs / 60);
821
+ const diffHours = Math.floor(diffMins / 60);
822
+ const diffDays = Math.floor(diffHours / 24);
823
+ if (short) {
824
+ if (diffDays > 365) return Math.floor(diffDays / 365) + "y";
825
+ if (diffDays > 30) return Math.floor(diffDays / 30) + "mo";
826
+ if (diffDays > 7) return Math.floor(diffDays / 7) + "w";
827
+ if (diffDays > 0) return diffDays + "d";
828
+ if (diffHours > 0) return diffHours + "h";
829
+ if (diffMins > 0) return diffMins + "m";
830
+ return "now";
831
+ }
832
+ if (diffDays > 365) {
833
+ const years = Math.floor(diffDays / 365);
834
+ return years + " year" + (years > 1 ? "s" : "") + " ago";
835
+ }
836
+ if (diffDays > 30) {
837
+ const months = Math.floor(diffDays / 30);
838
+ return months + " month" + (months > 1 ? "s" : "") + " ago";
839
+ }
840
+ if (diffDays > 7) {
841
+ const weeks = Math.floor(diffDays / 7);
842
+ return weeks + " week" + (weeks > 1 ? "s" : "") + " ago";
843
+ }
844
+ if (diffDays === 1) return "yesterday";
845
+ if (diffDays > 0) return diffDays + " days ago";
846
+ if (diffHours > 0) return diffHours + " hour" + (diffHours > 1 ? "s" : "") + " ago";
847
+ if (diffMins > 0) return diffMins + " minute" + (diffMins > 1 ? "s" : "") + " ago";
848
+ if (diffSecs > 30) return diffSecs + " seconds ago";
849
+ return "just now";
850
+ }
851
+ /**
852
+ * Format ISO date
853
+ * @param {*} value - Date value
854
+ * @param {boolean} dateOnly - Return date only
855
+ * @returns {string} ISO date string
856
+ */
857
+ iso(value, dateOnly = false) {
858
+ if (!value) return "";
859
+ value = this.normalizeEpoch(value);
860
+ const date = value instanceof Date ? value : new Date(value);
861
+ if (isNaN(date.getTime())) return String(value);
862
+ if (dateOnly) {
863
+ return date.toISOString().split("T")[0];
864
+ }
865
+ return date.toISOString();
866
+ }
867
+ // ============= Number Formatters =============
868
+ /**
869
+ * Format number
870
+ * @param {*} value - Number value
871
+ * @param {number} decimals - Decimal places
872
+ * @param {string} locale - Locale string
873
+ * @returns {string} Formatted number
874
+ */
875
+ number(value, decimals = 2, locale = "en-US") {
876
+ const num = parseFloat(value);
877
+ if (isNaN(num)) return String(value);
878
+ return num.toLocaleString(locale, {
879
+ minimumFractionDigits: decimals,
880
+ maximumFractionDigits: decimals
881
+ });
882
+ }
883
+ /**
884
+ * Format currency
885
+ * @param {*} value - Number value
886
+ * @param {string} symbol - Currency symbol
887
+ * @param {number} decimals - Decimal places
888
+ * @returns {string} Formatted currency
889
+ */
890
+ currency(value, symbol = "$", decimals = 2) {
891
+ const num = parseFloat(value);
892
+ if (isNaN(num)) return String(value);
893
+ const formatted = this.number(num, decimals);
894
+ return symbol + formatted;
895
+ }
896
+ /**
897
+ * Format percentage
898
+ * @param {*} value - Number value
899
+ * @param {number} decimals - Decimal places
900
+ * @param {boolean} multiply - Multiply by 100
901
+ * @returns {string} Formatted percentage
902
+ */
903
+ percent(value, decimals = 0, multiply = true) {
904
+ const num = parseFloat(value);
905
+ if (isNaN(num)) return String(value);
906
+ const percent = multiply ? num * 100 : num;
907
+ return this.number(percent, decimals) + "%";
908
+ }
909
+ /**
910
+ * Format file size
911
+ * @param {*} value - Size in bytes
912
+ * @param {boolean} binary - Use binary units (1024)
913
+ * @param {number} decimals - Decimal places
914
+ * @returns {string} Formatted file size
915
+ */
916
+ filesize(value, binary = false, decimals = 1) {
917
+ const bytes = parseInt(value);
918
+ if (isNaN(bytes)) return String(value);
919
+ const units = binary ? ["B", "KiB", "MiB", "GiB", "TiB"] : ["B", "KB", "MB", "GB", "TB"];
920
+ const divisor = binary ? 1024 : 1e3;
921
+ let size = bytes;
922
+ let unitIndex = 0;
923
+ while (size >= divisor && unitIndex < units.length - 1) {
924
+ size /= divisor;
925
+ unitIndex++;
926
+ }
927
+ const decimalPlaces = unitIndex === 0 ? 0 : decimals;
928
+ return `${size.toFixed(decimalPlaces)} ${units[unitIndex]}`;
929
+ }
930
+ /**
931
+ * Format ordinal number
932
+ * @param {*} value - Number value
933
+ * @param {boolean} suffixOnly - Return suffix only
934
+ * @returns {string} Ordinal number
935
+ */
936
+ ordinal(value, suffixOnly = false) {
937
+ const num = parseInt(value);
938
+ if (isNaN(num)) return String(value);
939
+ const j = num % 10;
940
+ const k = num % 100;
941
+ let suffix = "th";
942
+ if (j === 1 && k !== 11) suffix = "st";
943
+ else if (j === 2 && k !== 12) suffix = "nd";
944
+ else if (j === 3 && k !== 13) suffix = "rd";
945
+ return suffixOnly ? suffix : num + suffix;
946
+ }
947
+ /**
948
+ * Format compact number
949
+ * @param {*} value - Number value
950
+ * @param {number} decimals - Decimal places
951
+ * @returns {string} Compact number
952
+ */
953
+ compact(value, decimals = 1) {
954
+ const num = parseFloat(value);
955
+ if (isNaN(num)) return String(value);
956
+ const abs = Math.abs(num);
957
+ const sign = num < 0 ? "-" : "";
958
+ if (abs >= 1e9) {
959
+ return sign + (abs / 1e9).toFixed(decimals) + "B";
960
+ }
961
+ if (abs >= 1e6) {
962
+ return sign + (abs / 1e6).toFixed(decimals) + "M";
963
+ }
964
+ if (abs >= 1e3) {
965
+ return sign + (abs / 1e3).toFixed(decimals) + "K";
966
+ }
967
+ return String(num);
968
+ }
969
+ // ============= String Formatters =============
970
+ /**
971
+ * Capitalize string
972
+ * @param {*} value - String value
973
+ * @param {boolean} all - Capitalize all words (default: true). If false, only capitalizes first letter
974
+ * @returns {string} Capitalized string
975
+ */
976
+ capitalize(value, all = true) {
977
+ const str = String(value);
978
+ if (!str) return "";
979
+ if (all) {
980
+ return str.replace(/\b\w/g, (c) => c.toUpperCase());
981
+ }
982
+ return str.charAt(0).toUpperCase() + str.slice(1);
983
+ }
984
+ /**
985
+ * Truncate string
986
+ * @param {*} value - String value
987
+ * @param {number} length - Max length
988
+ * @param {string} suffix - Suffix to append
989
+ * @returns {string} Truncated string
990
+ */
991
+ truncate(value, length = 50, suffix = "...") {
992
+ const str = String(value);
993
+ if (str.length <= length) return str;
994
+ return str.substring(0, length) + suffix;
995
+ }
996
+ /**
997
+ * Truncate string in the middle
998
+ * @param {*} value - String value
999
+ * @param {number} size - The total number of characters to keep (half for the start, half for the end).
1000
+ * @param {string} replace - The character(s) to use for the middle part.
1001
+ * @returns {string} Truncated string
1002
+ */
1003
+ truncate_middle(value, size = 8, replace = "***") {
1004
+ const str = String(value);
1005
+ if (str.length <= size) {
1006
+ return str;
1007
+ }
1008
+ const halfSize = Math.floor(size / 2);
1009
+ const front = str.substring(0, halfSize);
1010
+ const back = str.substring(str.length - halfSize);
1011
+ return `${front}${replace}${back}`;
1012
+ }
1013
+ /**
1014
+ * Create slug from string
1015
+ * @param {*} value - String value
1016
+ * @param {string} separator - Word separator
1017
+ * @returns {string} Slug
1018
+ */
1019
+ slug(value, separator = "-") {
1020
+ const str = String(value);
1021
+ return str.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, separator).replace(new RegExp(`${separator}+`, "g"), separator).replace(new RegExp(`^${separator}|${separator}$`, "g"), "");
1022
+ }
1023
+ /**
1024
+ * Get initials from string
1025
+ * @param {*} value - String value
1026
+ * @param {number} count - Number of initials
1027
+ * @returns {string} Initials
1028
+ */
1029
+ initials(value, count = 2) {
1030
+ const str = String(value);
1031
+ const words = str.split(/\s+/).filter((w) => w.length > 0);
1032
+ return words.slice(0, count).map((word) => word.charAt(0).toUpperCase()).join("");
1033
+ }
1034
+ /**
1035
+ * Mask string
1036
+ * @param {*} value - String value
1037
+ * @param {string} char - Mask character
1038
+ * @param {number} showLast - Number of chars to show at end
1039
+ * @returns {string} Masked string
1040
+ */
1041
+ mask(value, char = "*", showLast = 4) {
1042
+ const str = String(value);
1043
+ if (str.length <= showLast) return str;
1044
+ const masked = char.repeat(Math.max(0, str.length - showLast));
1045
+ const visible = str.slice(-showLast);
1046
+ return masked + visible;
1047
+ }
1048
+ // ============= HTML/Web Formatters =============
1049
+ /**
1050
+ * Format email
1051
+ * @param {*} value - Email value
1052
+ * @param {Object} options - Options
1053
+ * @returns {string} Formatted email
1054
+ */
1055
+ email(value, options = {}) {
1056
+ const email = String(value).trim();
1057
+ if (!email) return "";
1058
+ if (options.link === false) {
1059
+ return email;
1060
+ }
1061
+ const subject = options.subject ? `?subject=${encodeURIComponent(options.subject)}` : "";
1062
+ const body = options.body ? `&body=${encodeURIComponent(options.body)}` : "";
1063
+ const className = options.class ? ` class="${options.class}"` : "";
1064
+ return `<a href="mailto:${email}${subject}${body}"${className}>${email}</a>`;
1065
+ }
1066
+ /**
1067
+ * Format phone number
1068
+ * @param {*} value - Phone value
1069
+ * @param {string} format - Format type
1070
+ * @param {boolean} link - Create tel link
1071
+ * @returns {string} Formatted phone
1072
+ */
1073
+ phone(value, format = "US", link = true) {
1074
+ let phone = String(value).replace(/\D/g, "");
1075
+ let formatted = phone;
1076
+ if (format === "US") {
1077
+ if (phone.length === 10) {
1078
+ formatted = `(${phone.slice(0, 3)}) ${phone.slice(3, 6)}-${phone.slice(6)}`;
1079
+ } else if (phone.length === 11 && phone[0] === "1") {
1080
+ formatted = `+1 (${phone.slice(1, 4)}) ${phone.slice(4, 7)}-${phone.slice(7)}`;
1081
+ }
1082
+ }
1083
+ if (!link) {
1084
+ return formatted;
1085
+ }
1086
+ return `<a href="tel:${phone}">${formatted}</a>`;
1087
+ }
1088
+ /**
1089
+ * Format URL
1090
+ * @param {*} value - URL value
1091
+ * @param {string} text - Link text
1092
+ * @param {boolean} newWindow - Open in new window
1093
+ * @returns {string} Formatted URL
1094
+ */
1095
+ url(value, text = null, newWindow = true) {
1096
+ let url = String(value).trim();
1097
+ if (!url) return "";
1098
+ if (!/^https?:\/\//.test(url)) {
1099
+ url = "https://" + url;
1100
+ }
1101
+ const linkText = text || url;
1102
+ const target = newWindow ? ' target="_blank"' : "";
1103
+ const rel = newWindow ? ' rel="noopener noreferrer"' : "";
1104
+ return `<a href="${url}"${target}${rel}>${linkText}</a>`;
1105
+ }
1106
+ /**
1107
+ * Format as badge
1108
+ * @param {*} value - Badge text
1109
+ * @param {string} type - Badge type
1110
+ * @returns {string} Badge HTML
1111
+ */
1112
+ badge(value, type = "auto") {
1113
+ if (Array.isArray(value)) {
1114
+ return value.map((item) => this.badge(item, type)).join(" ");
1115
+ }
1116
+ const text = String(value);
1117
+ const badgeType = type === "auto" ? this.inferBadgeType(text) : type;
1118
+ const className = badgeType ? `bg-${badgeType}` : "bg-secondary";
1119
+ return `<span class="badge ${className}">${text}</span>`;
1120
+ }
1121
+ /**
1122
+ * Infer badge type from text
1123
+ * @param {string} text - Badge text
1124
+ * @returns {string} Badge type
1125
+ */
1126
+ inferBadgeType(text) {
1127
+ const lowered = text.toLowerCase();
1128
+ if (["active", "success", "complete", "completed", "approved", "done", "true", "on", "yes"].includes(lowered)) return "success";
1129
+ if (["error", "failed", "rejected", "deleted", "cancelled", "false", "off", "no"].includes(lowered)) return "danger";
1130
+ if (["warning", "pending", "review", "processing", "uploading"].includes(lowered)) return "warning";
1131
+ if (["info", "new", "draft"].includes(lowered)) return "info";
1132
+ if (["inactive", "disabled", "archived", "suspended"].includes(lowered)) return "secondary";
1133
+ return "secondary";
1134
+ }
1135
+ /**
1136
+ * Format status
1137
+ * @param {*} value - Status value
1138
+ * @param {Object} icons - Icon mapping
1139
+ * @param {Object} colors - Color mapping
1140
+ * @returns {string} Status HTML
1141
+ */
1142
+ status(value, icons = {}, colors = {}) {
1143
+ const status = String(value).toLowerCase();
1144
+ const defaultIcons = {
1145
+ "active": "✓",
1146
+ "inactive": "✗",
1147
+ "pending": "⏳",
1148
+ "success": "✓",
1149
+ "error": "✗",
1150
+ "warning": "⚠"
1151
+ };
1152
+ const defaultColors = {
1153
+ "active": "success",
1154
+ "inactive": "secondary",
1155
+ "pending": "warning",
1156
+ "success": "success",
1157
+ "error": "danger",
1158
+ "warning": "warning"
1159
+ };
1160
+ const icon = icons[status] || defaultIcons[status] || "";
1161
+ const color = colors[status] || defaultColors[status] || "secondary";
1162
+ return `<span class="text-${color}">${icon}${icon ? " " : ""}${value}</span>`;
1163
+ }
1164
+ /**
1165
+ * Format boolean
1166
+ * @param {*} value - Boolean value
1167
+ * @param {string} trueText - Text for true
1168
+ * @param {string} falseText - Text for false
1169
+ * @returns {string} Boolean text
1170
+ */
1171
+ boolean(value, trueText = "True", falseText = "False") {
1172
+ return value ? trueText : falseText;
1173
+ }
1174
+ /**
1175
+ * Format icon
1176
+ * @param {*} value - Icon key
1177
+ * @param {Object} mapping - Icon mapping
1178
+ * @returns {string} Icon HTML
1179
+ */
1180
+ icon(value, mapping = {}) {
1181
+ const key = String(value).toLowerCase();
1182
+ const icon = mapping[key] || "";
1183
+ return icon ? `<i class="${icon}"></i>` : "";
1184
+ }
1185
+ /**
1186
+ * Format boolean as a yes/no icon
1187
+ * @param {*} value - Boolean value
1188
+ * @returns {string} Icon HTML
1189
+ */
1190
+ yesnoicon(value) {
1191
+ if (value) {
1192
+ return '<i class="bi bi-check-circle-fill text-success"></i>';
1193
+ }
1194
+ return '<i class="bi bi-x-circle-fill text-danger"></i>';
1195
+ }
1196
+ /**
1197
+ * Format value as Bootstrap 5 image with optional rendition support
1198
+ * @param {string|object} value - URL string or file object with renditions
1199
+ * @param {string} rendition - Desired rendition (thumbnail, thumbnail_sm, etc.)
1200
+ * @param {string} classes - Additional CSS classes
1201
+ * @param {string} alt - Alt text for the image
1202
+ * @returns {string} Bootstrap image HTML
1203
+ */
1204
+ image(value, rendition = "thumbnail", classes = "img-fluid", alt = "") {
1205
+ const url = this._extractImageUrl(value, rendition);
1206
+ if (!url) return "";
1207
+ return `<img src="${url}" class="${classes}" alt="${alt}" />`;
1208
+ }
1209
+ /**
1210
+ * Format value as Bootstrap 5 avatar (circular image)
1211
+ * @param {string|object} value - URL string or file object with renditions
1212
+ * @param {string} size - Avatar size (xs, sm, md, lg, xl)
1213
+ * @param {string} classes - Additional CSS classes
1214
+ * @param {string} alt - Alt text for the avatar
1215
+ * @returns {string} Bootstrap avatar HTML
1216
+ */
1217
+ avatar(value, size = "md", classes = "rounded-circle", alt = "") {
1218
+ const url = this._extractImageUrl(value, "square_sm") || GENERIC_AVATAR_SVG;
1219
+ const sizeClasses = {
1220
+ "xs": "width: 1.5rem; height: 1.5rem;",
1221
+ "sm": "width: 2rem; height: 2rem;",
1222
+ "md": "width: 3rem; height: 3rem;",
1223
+ "lg": "width: 4rem; height: 4rem;",
1224
+ "xl": "width: 5rem; height: 5rem;"
1225
+ };
1226
+ const sizeStyle = sizeClasses[size] || sizeClasses["md"];
1227
+ const baseClasses = "object-fit-cover";
1228
+ const allClasses = `${baseClasses} ${classes}`.trim();
1229
+ return `<img src="${url}" class="${allClasses}" style="${sizeStyle}" alt="${alt}" />`;
1230
+ }
1231
+ /**
1232
+ * Helper method to extract image URL from string or file object
1233
+ * @param {string|object} value - URL string or file object with renditions
1234
+ * @param {string} preferredRendition - Preferred rendition name
1235
+ * @returns {string|null} Image URL or null if not found
1236
+ */
1237
+ _extractImageUrl(value, preferredRendition = "thumbnail") {
1238
+ if (!value) return null;
1239
+ if (typeof value === "string") {
1240
+ return value;
1241
+ }
1242
+ if (typeof value === "object") {
1243
+ if (value.attributes) value = value.attributes;
1244
+ if (preferredRendition === "thumbnail" && value.thumbnail && typeof value.thumbnail === "string") {
1245
+ return value.thumbnail;
1246
+ }
1247
+ if (value.renditions && typeof value.renditions === "object") {
1248
+ const rendition = value.renditions[preferredRendition];
1249
+ if (rendition && rendition.url) {
1250
+ return rendition.url;
1251
+ }
1252
+ const availableRenditions = Object.values(value.renditions);
1253
+ if (availableRenditions.length > 0 && availableRenditions[0].url) {
1254
+ return availableRenditions[0].url;
1255
+ }
1256
+ }
1257
+ if (value.url) {
1258
+ return value.url;
1259
+ }
1260
+ }
1261
+ return null;
1262
+ }
1263
+ // ============= Utility Formatters =============
1264
+ /**
1265
+ * Apply default value
1266
+ * @param {*} value - Value
1267
+ * @param {*} defaultValue - Default value
1268
+ * @returns {*} Value or default
1269
+ */
1270
+ default(value, defaultValue = "") {
1271
+ return value === null || value === void 0 || value === "" ? defaultValue : value;
1272
+ }
1273
+ /**
1274
+ * Format as JSON
1275
+ * @param {*} value - Value to stringify
1276
+ * @param {number} indent - Indentation
1277
+ * @returns {string} JSON string
1278
+ */
1279
+ /**
1280
+ * Format pluralization based on count
1281
+ * @param {number} count - The count value
1282
+ * @param {string} singular - Singular form of the word
1283
+ * @param {string|null} plural - Plural form (defaults to singular + 's')
1284
+ * @param {boolean} includeCount - Whether to include the count in output
1285
+ * @returns {string} Formatted plural string
1286
+ */
1287
+ plural(count, singular, plural = null, includeCount = true) {
1288
+ if (count === null || count === void 0 || singular === null || singular === void 0) {
1289
+ return includeCount ? `${count} ${singular}` : singular || "";
1290
+ }
1291
+ const num = parseInt(count);
1292
+ if (isNaN(num)) {
1293
+ return includeCount ? `${count} ${singular}` : singular || "";
1294
+ }
1295
+ const word = Math.abs(num) === 1 ? singular : plural || singular + "s";
1296
+ return includeCount ? `${num} ${word}` : word;
1297
+ }
1298
+ /**
1299
+ * Format array as a human-readable list
1300
+ * @param {Array} array - Array to format
1301
+ * @param {Object} options - Formatting options
1302
+ * @returns {string} Formatted list string
1303
+ */
1304
+ formatList(array, options = {}) {
1305
+ if (!Array.isArray(array)) {
1306
+ return String(array);
1307
+ }
1308
+ const { conjunction = "and", limit = null, moreText = "others" } = options;
1309
+ if (array.length === 0) return "";
1310
+ if (array.length === 1) return String(array[0]);
1311
+ let items = array.slice();
1312
+ let hasMore = false;
1313
+ if (limit && array.length > limit) {
1314
+ items = array.slice(0, limit);
1315
+ hasMore = true;
1316
+ }
1317
+ if (hasMore) {
1318
+ const remaining = array.length - limit;
1319
+ return `${items.join(", ")}, ${conjunction} ${remaining} ${moreText}`;
1320
+ }
1321
+ if (items.length === 2) {
1322
+ return `${items[0]} ${conjunction} ${items[1]}`;
1323
+ }
1324
+ return `${items.slice(0, -1).join(", ")}, ${conjunction} ${items[items.length - 1]}`;
1325
+ }
1326
+ /**
1327
+ * Format duration in milliseconds to human-readable format
1328
+ * @param {number} milliseconds - Duration in milliseconds
1329
+ * @param {Object} options - Formatting options
1330
+ * @returns {string} Formatted duration string
1331
+ */
1332
+ duration(milliseconds, options = {}) {
1333
+ const { short = false, precision = 2 } = options;
1334
+ if (milliseconds === null || milliseconds === void 0) return "";
1335
+ const ms = parseInt(milliseconds);
1336
+ if (isNaN(ms)) return String(milliseconds);
1337
+ const units = [
1338
+ { name: "day", short: "d", value: 864e5 },
1339
+ { name: "hour", short: "h", value: 36e5 },
1340
+ { name: "minute", short: "m", value: 6e4 },
1341
+ { name: "second", short: "s", value: 1e3 }
1342
+ ];
1343
+ if (ms === 0) return short ? "0s" : "0 seconds";
1344
+ const absMs = Math.abs(ms);
1345
+ const sign = ms < 0 ? "-" : "";
1346
+ const parts = [];
1347
+ let remaining = absMs;
1348
+ for (const unit of units) {
1349
+ if (remaining >= unit.value) {
1350
+ const count = Math.floor(remaining / unit.value);
1351
+ remaining = remaining % unit.value;
1352
+ const unitName = short ? unit.short : count === 1 ? unit.name : unit.name + "s";
1353
+ parts.push(short ? `${count}${unitName}` : `${count} ${unitName}`);
1354
+ if (parts.length >= precision) break;
1355
+ }
1356
+ }
1357
+ if (parts.length === 0) {
1358
+ return short ? `${Math.round(absMs)}ms` : `${Math.round(absMs)} milliseconds`;
1359
+ }
1360
+ return sign + (short ? parts.join("") : parts.join(" "));
1361
+ }
1362
+ /**
1363
+ * Format long strings/IDs with truncation
1364
+ * @param {string} value - Value to format
1365
+ * @param {number} length - Maximum length before truncation
1366
+ * @param {string} prefix - Prefix to add
1367
+ * @param {string} suffix - Suffix for truncated strings
1368
+ * @returns {string} Formatted hash string
1369
+ */
1370
+ hash(value, length = 8, prefix = "", suffix = "...") {
1371
+ if (value === null || value === void 0) return "";
1372
+ const str = String(value);
1373
+ if (str.length <= length) return prefix + str;
1374
+ return prefix + str.substring(0, length) + suffix;
1375
+ }
1376
+ /**
1377
+ * Strip HTML tags from text
1378
+ * @param {string} html - HTML string to strip
1379
+ * @returns {string} Plain text without HTML tags
1380
+ */
1381
+ stripHtml(html) {
1382
+ if (html === null || html === void 0) return "";
1383
+ return String(html).replace(/<[^>]*>/g, "");
1384
+ }
1385
+ /**
1386
+ * Highlight search terms in text
1387
+ * @param {string} text - Text to search in
1388
+ * @param {string} searchTerm - Term to highlight
1389
+ * @param {string} className - CSS class for highlighting
1390
+ * @returns {string} Text with highlighted terms
1391
+ */
1392
+ highlight(text, searchTerm, className = "highlight") {
1393
+ if (text === null || text === void 0 || !searchTerm) {
1394
+ return String(text || "");
1395
+ }
1396
+ const escapedTerm = String(searchTerm).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1397
+ const regex = new RegExp(`(${escapedTerm})`, "gi");
1398
+ return String(text).replace(regex, `<mark class="${className}">$1</mark>`);
1399
+ }
1400
+ json(value, indent = 2) {
1401
+ try {
1402
+ return JSON.stringify(value, null, indent);
1403
+ } catch (e) {
1404
+ return String(value);
1405
+ }
1406
+ }
1407
+ /**
1408
+ * Check if formatter exists
1409
+ * @param {string} name - Formatter name
1410
+ * @returns {boolean} True if exists
1411
+ */
1412
+ has(name) {
1413
+ return this.formatters.has(name.toLowerCase());
1414
+ }
1415
+ /**
1416
+ * Remove a formatter
1417
+ * @param {string} name - Formatter name
1418
+ * @returns {boolean} True if removed
1419
+ */
1420
+ unregister(name) {
1421
+ return this.formatters.delete(name.toLowerCase());
1422
+ }
1423
+ /**
1424
+ * Get all formatter names
1425
+ * @returns {Array} Formatter names
1426
+ */
1427
+ listFormatters() {
1428
+ return Array.from(this.formatters.keys()).sort();
1429
+ }
1430
+ iter(v) {
1431
+ if (v === null || v === void 0) {
1432
+ return [];
1433
+ }
1434
+ if (Array.isArray(v)) {
1435
+ return v;
1436
+ }
1437
+ if (typeof v === "object") {
1438
+ return Object.entries(v).map(([key, value]) => ({
1439
+ key,
1440
+ value
1441
+ }));
1442
+ }
1443
+ return [{ key: "0", value: v }];
1444
+ }
1445
+ }
1446
+ const dataFormatter = new DataFormatter();
1447
+ window.dataFormatter = dataFormatter;
1448
+ class MOJOUtils {
1449
+ /**
1450
+ * Get data from context with support for:
1451
+ * - Dot notation (e.g., "user.name")
1452
+ * - Pipe formatting (e.g., "name|uppercase")
1453
+ * - Combined (e.g., "user.name|uppercase|truncate(10)")
1454
+ *
1455
+ * @param {object} context - The data context to search in
1456
+ * @param {string} key - The key path with optional pipes
1457
+ * @returns {*} The value, possibly formatted
1458
+ */
1459
+ static getContextData(context, key) {
1460
+ if (!key || context == null) {
1461
+ return void 0;
1462
+ }
1463
+ let field = key;
1464
+ let pipes = "";
1465
+ let parenDepth = 0;
1466
+ let pipeIndex = -1;
1467
+ for (let i = 0; i < key.length; i++) {
1468
+ const char = key[i];
1469
+ if (char === "(") parenDepth++;
1470
+ else if (char === ")") parenDepth--;
1471
+ else if (char === "|" && parenDepth === 0) {
1472
+ pipeIndex = i;
1473
+ break;
1474
+ }
1475
+ }
1476
+ if (pipeIndex > -1) {
1477
+ field = key.substring(0, pipeIndex).trim();
1478
+ pipes = key.substring(pipeIndex + 1).trim();
1479
+ }
1480
+ const value = this.getNestedValue(context, field);
1481
+ if (pipes) {
1482
+ return dataFormatter.pipe(value, pipes);
1483
+ }
1484
+ return value;
1485
+ }
1486
+ /**
1487
+ * Get nested value from object using dot notation
1488
+ * IMPORTANT: Never calls get() on the top-level context to avoid recursion
1489
+ * But DOES call get() on nested objects if they have that method
1490
+ *
1491
+ * @param {object} context - The object to search in
1492
+ * @param {string} path - Dot notation path
1493
+ * @returns {*} The value at the path
1494
+ */
1495
+ static getNestedValue(context, path) {
1496
+ if (!path || context == null) {
1497
+ return void 0;
1498
+ }
1499
+ if (!path.includes(".")) {
1500
+ if (path in context) {
1501
+ const value = context[path];
1502
+ if (typeof value === "function") {
1503
+ return value.call(context);
1504
+ }
1505
+ return value;
1506
+ }
1507
+ return void 0;
1508
+ }
1509
+ const keys = path.split(".");
1510
+ let current = context;
1511
+ for (let i = 0; i < keys.length; i++) {
1512
+ const key = keys[i];
1513
+ if (current == null) {
1514
+ return void 0;
1515
+ }
1516
+ if (i === 0) {
1517
+ if (current.hasOwnProperty(key)) {
1518
+ const value = current[key];
1519
+ if (typeof value === "function") {
1520
+ current = value.call(context);
1521
+ } else {
1522
+ current = value;
1523
+ }
1524
+ } else {
1525
+ return void 0;
1526
+ }
1527
+ } else {
1528
+ if (current && typeof current.getContextValue === "function") {
1529
+ const remainingPath = keys.slice(i).join(".");
1530
+ return current.getContextValue(remainingPath);
1531
+ }
1532
+ if (Array.isArray(current) && !isNaN(key)) {
1533
+ current = current[parseInt(key)];
1534
+ } else if (current.hasOwnProperty(key)) {
1535
+ current = current[key];
1536
+ } else if (typeof current[key] === "function") {
1537
+ current = current[key].call(current);
1538
+ } else {
1539
+ return void 0;
1540
+ }
1541
+ }
1542
+ }
1543
+ return current;
1544
+ }
1545
+ /**
1546
+ * Check if a value is null or undefined
1547
+ * @param {*} value - Value to check
1548
+ * @returns {boolean} True if null or undefined
1549
+ */
1550
+ static isNullOrUndefined(value) {
1551
+ return value === null || value === void 0;
1552
+ }
1553
+ /**
1554
+ * Deep clone an object
1555
+ * @param {*} obj - Object to clone
1556
+ * @returns {*} Cloned object
1557
+ */
1558
+ static deepClone(obj) {
1559
+ if (obj === null || typeof obj !== "object") return obj;
1560
+ if (obj instanceof Date) return new Date(obj.getTime());
1561
+ if (obj instanceof Array) return obj.map((item) => this.deepClone(item));
1562
+ if (obj instanceof Object) {
1563
+ const clonedObj = {};
1564
+ for (const key in obj) {
1565
+ if (obj.hasOwnProperty(key)) {
1566
+ clonedObj[key] = this.deepClone(obj[key]);
1567
+ }
1568
+ }
1569
+ return clonedObj;
1570
+ }
1571
+ }
1572
+ /**
1573
+ * Merge objects deeply
1574
+ * @param {object} target - Target object
1575
+ * @param {...object} sources - Source objects to merge
1576
+ * @returns {object} Merged object
1577
+ */
1578
+ static deepMerge(target, ...sources) {
1579
+ if (!sources.length) return target;
1580
+ const source = sources.shift();
1581
+ if (this.isObject(target) && this.isObject(source)) {
1582
+ for (const key in source) {
1583
+ if (this.isObject(source[key])) {
1584
+ if (!target[key]) Object.assign(target, { [key]: {} });
1585
+ this.deepMerge(target[key], source[key]);
1586
+ } else {
1587
+ Object.assign(target, { [key]: source[key] });
1588
+ }
1589
+ }
1590
+ }
1591
+ return this.deepMerge(target, ...sources);
1592
+ }
1593
+ /**
1594
+ * Check if value is a plain object
1595
+ * @param {*} item - Value to check
1596
+ * @returns {boolean} True if plain object
1597
+ */
1598
+ static isObject(item) {
1599
+ return item && typeof item === "object" && !Array.isArray(item);
1600
+ }
1601
+ /**
1602
+ * Debounce function calls
1603
+ * @param {function} func - Function to debounce
1604
+ * @param {number} wait - Wait time in milliseconds
1605
+ * @returns {function} Debounced function
1606
+ */
1607
+ static debounce(func, wait) {
1608
+ let timeout;
1609
+ return function executedFunction(...args) {
1610
+ const later = () => {
1611
+ clearTimeout(timeout);
1612
+ func(...args);
1613
+ };
1614
+ clearTimeout(timeout);
1615
+ timeout = setTimeout(later, wait);
1616
+ };
1617
+ }
1618
+ /**
1619
+ * Throttle function calls
1620
+ * @param {function} func - Function to throttle
1621
+ * @param {number} limit - Time limit in milliseconds
1622
+ * @returns {function} Throttled function
1623
+ */
1624
+ static throttle(func, limit) {
1625
+ let inThrottle;
1626
+ return function(...args) {
1627
+ if (!inThrottle) {
1628
+ func.apply(this, args);
1629
+ inThrottle = true;
1630
+ setTimeout(() => inThrottle = false, limit);
1631
+ }
1632
+ };
1633
+ }
1634
+ /**
1635
+ * Generate a unique ID
1636
+ * @param {string} prefix - Optional prefix for the ID
1637
+ * @returns {string} Unique ID
1638
+ */
1639
+ static generateId(prefix = "") {
1640
+ const timestamp = Date.now().toString(36);
1641
+ const randomStr = Math.random().toString(36).substr(2, 9);
1642
+ return prefix ? `${prefix}_${timestamp}_${randomStr}` : `${timestamp}_${randomStr}`;
1643
+ }
1644
+ /**
1645
+ * Escape HTML special characters
1646
+ * @param {string} str - String to escape
1647
+ * @returns {string} Escaped string
1648
+ */
1649
+ static escapeHtml(str) {
1650
+ const entityMap = {
1651
+ "&": "&amp;",
1652
+ "<": "&lt;",
1653
+ ">": "&gt;",
1654
+ '"': "&quot;",
1655
+ "'": "&#39;",
1656
+ "/": "&#x2F;",
1657
+ "`": "&#x60;",
1658
+ "=": "&#x3D;"
1659
+ };
1660
+ return String(str).replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
1661
+ }
1662
+ /**
1663
+ * Check password strength and provide detailed feedback
1664
+ * @param {string} password - Password to check
1665
+ * @returns {object} Password strength analysis
1666
+ */
1667
+ static checkPasswordStrength(password) {
1668
+ if (!password || typeof password !== "string") {
1669
+ return {
1670
+ score: 0,
1671
+ strength: "invalid",
1672
+ feedback: ["Password must be a non-empty string"],
1673
+ details: {
1674
+ length: 0,
1675
+ hasLowercase: false,
1676
+ hasUppercase: false,
1677
+ hasNumbers: false,
1678
+ hasSpecialChars: false,
1679
+ hasCommonPatterns: false,
1680
+ isCommonPassword: false
1681
+ }
1682
+ };
1683
+ }
1684
+ const feedback = [];
1685
+ const details = {
1686
+ length: password.length,
1687
+ hasLowercase: /[a-z]/.test(password),
1688
+ hasUppercase: /[A-Z]/.test(password),
1689
+ hasNumbers: /[0-9]/.test(password),
1690
+ hasSpecialChars: /[^a-zA-Z0-9]/.test(password),
1691
+ hasCommonPatterns: false,
1692
+ isCommonPassword: false
1693
+ };
1694
+ let score = 0;
1695
+ if (password.length < 6) {
1696
+ feedback.push("Password should be at least 6 characters long");
1697
+ } else if (password.length < 8) {
1698
+ score += 1;
1699
+ feedback.push("Consider using at least 8 characters for better security");
1700
+ } else if (password.length < 12) {
1701
+ score += 2;
1702
+ } else {
1703
+ score += 3;
1704
+ }
1705
+ if (details.hasLowercase) score += 1;
1706
+ else feedback.push("Include lowercase letters");
1707
+ if (details.hasUppercase) score += 1;
1708
+ else feedback.push("Include uppercase letters");
1709
+ if (details.hasNumbers) score += 1;
1710
+ else feedback.push("Include numbers");
1711
+ if (details.hasSpecialChars) score += 2;
1712
+ else feedback.push("Include special characters (!@#$%^&* etc.)");
1713
+ const commonPatterns = [
1714
+ /123/,
1715
+ // Sequential numbers
1716
+ /abc/i,
1717
+ // Sequential letters
1718
+ /qwerty/i,
1719
+ // Keyboard patterns
1720
+ /asdf/i,
1721
+ // Keyboard patterns
1722
+ /(.)\1{2,}/,
1723
+ // Repeated characters (aaa, 111)
1724
+ /password/i,
1725
+ // Contains "password"
1726
+ /admin/i,
1727
+ // Contains "admin"
1728
+ /user/i,
1729
+ // Contains "user"
1730
+ /login/i
1731
+ // Contains "login"
1732
+ ];
1733
+ for (const pattern of commonPatterns) {
1734
+ if (pattern.test(password)) {
1735
+ details.hasCommonPatterns = true;
1736
+ score -= 1;
1737
+ feedback.push("Avoid common patterns and dictionary words");
1738
+ break;
1739
+ }
1740
+ }
1741
+ const commonPasswords = [
1742
+ "123456",
1743
+ "password",
1744
+ "123456789",
1745
+ "12345678",
1746
+ "12345",
1747
+ "1234567",
1748
+ "1234567890",
1749
+ "qwerty",
1750
+ "abc123",
1751
+ "111111",
1752
+ "123123",
1753
+ "admin",
1754
+ "letmein",
1755
+ "welcome",
1756
+ "monkey",
1757
+ "password123",
1758
+ "123qwe",
1759
+ "qwerty123",
1760
+ "000000",
1761
+ "dragon",
1762
+ "sunshine",
1763
+ "princess",
1764
+ "azerty",
1765
+ "1234",
1766
+ "iloveyou",
1767
+ "trustno1",
1768
+ "superman",
1769
+ "shadow",
1770
+ "master",
1771
+ "jennifer"
1772
+ ];
1773
+ if (commonPasswords.includes(password.toLowerCase())) {
1774
+ details.isCommonPassword = true;
1775
+ score = Math.max(0, score - 3);
1776
+ feedback.push("This password is too common and easily guessed");
1777
+ }
1778
+ let strength;
1779
+ if (score < 2) {
1780
+ strength = "very-weak";
1781
+ } else if (score < 4) {
1782
+ strength = "weak";
1783
+ } else if (score < 6) {
1784
+ strength = "fair";
1785
+ } else if (score < 8) {
1786
+ strength = "good";
1787
+ } else {
1788
+ strength = "strong";
1789
+ }
1790
+ if (score >= 7 && feedback.length === 0) {
1791
+ feedback.push("Strong password! Consider using a password manager.");
1792
+ } else if (score >= 5 && feedback.length <= 1) {
1793
+ feedback.push("Good password strength. Consider adding more variety.");
1794
+ }
1795
+ return {
1796
+ score: Math.max(0, score),
1797
+ strength,
1798
+ feedback,
1799
+ details
1800
+ };
1801
+ }
1802
+ /**
1803
+ * Generate a secure password with customizable options
1804
+ * @param {object} options - Password generation options
1805
+ * @param {number} options.length - Password length (default: 12)
1806
+ * @param {boolean} options.includeLowercase - Include lowercase letters (default: true)
1807
+ * @param {boolean} options.includeUppercase - Include uppercase letters (default: true)
1808
+ * @param {boolean} options.includeNumbers - Include numbers (default: true)
1809
+ * @param {boolean} options.includeSpecialChars - Include special characters (default: true)
1810
+ * @param {string} options.customChars - Custom character set to use
1811
+ * @param {boolean} options.excludeAmbiguous - Exclude ambiguous characters like 0, O, l, I (default: false)
1812
+ * @returns {string} Generated password
1813
+ */
1814
+ static generatePassword(options = {}) {
1815
+ const defaults = {
1816
+ length: 12,
1817
+ includeLowercase: true,
1818
+ includeUppercase: true,
1819
+ includeNumbers: true,
1820
+ includeSpecialChars: true,
1821
+ customChars: "",
1822
+ excludeAmbiguous: false
1823
+ };
1824
+ const config = { ...defaults, ...options };
1825
+ if (config.length < 4) {
1826
+ throw new Error("Password length must be at least 4 characters");
1827
+ }
1828
+ let lowercase = "abcdefghijklmnopqrstuvwxyz";
1829
+ let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1830
+ let numbers = "0123456789";
1831
+ let specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
1832
+ if (config.excludeAmbiguous) {
1833
+ lowercase = lowercase.replace(/[il]/g, "");
1834
+ uppercase = uppercase.replace(/[IOL]/g, "");
1835
+ numbers = numbers.replace(/[01]/g, "");
1836
+ specialChars = specialChars.replace(/[|]/g, "");
1837
+ }
1838
+ let charPool = "";
1839
+ const requiredChars = [];
1840
+ if (config.customChars) {
1841
+ charPool = config.customChars;
1842
+ } else {
1843
+ if (config.includeLowercase) {
1844
+ charPool += lowercase;
1845
+ requiredChars.push(lowercase[Math.floor(Math.random() * lowercase.length)]);
1846
+ }
1847
+ if (config.includeUppercase) {
1848
+ charPool += uppercase;
1849
+ requiredChars.push(uppercase[Math.floor(Math.random() * uppercase.length)]);
1850
+ }
1851
+ if (config.includeNumbers) {
1852
+ charPool += numbers;
1853
+ requiredChars.push(numbers[Math.floor(Math.random() * numbers.length)]);
1854
+ }
1855
+ if (config.includeSpecialChars) {
1856
+ charPool += specialChars;
1857
+ requiredChars.push(specialChars[Math.floor(Math.random() * specialChars.length)]);
1858
+ }
1859
+ }
1860
+ if (!charPool) {
1861
+ throw new Error("No character types selected for password generation");
1862
+ }
1863
+ let password = "";
1864
+ for (const char of requiredChars) {
1865
+ password += char;
1866
+ }
1867
+ for (let i = password.length; i < config.length; i++) {
1868
+ password += charPool[Math.floor(Math.random() * charPool.length)];
1869
+ }
1870
+ return password.split("").sort(() => Math.random() - 0.5).join("");
1871
+ }
1872
+ /**
1873
+ * Parse query string into object
1874
+ * @param {string} queryString - Query string to parse
1875
+ * @returns {object} Parsed query parameters
1876
+ */
1877
+ static parseQueryString(queryString) {
1878
+ const params = {};
1879
+ const searchParams = new URLSearchParams(queryString);
1880
+ for (const [key, value] of searchParams.entries()) {
1881
+ params[key] = value;
1882
+ }
1883
+ return params;
1884
+ }
1885
+ /**
1886
+ * Convert object to query string
1887
+ * @param {object} params - Parameters object
1888
+ * @returns {string} Query string
1889
+ */
1890
+ static toQueryString(params) {
1891
+ return new URLSearchParams(params).toString();
1892
+ }
1893
+ /**
1894
+ * Wrap data objects to provide get() method support
1895
+ * This ensures pipe formatting works in all contexts
1896
+ * @param {*} data - Data to wrap
1897
+ * @param {object} rootContext - Optional root context for nested access
1898
+ * @returns {*} Wrapped data if object/array, otherwise original
1899
+ */
1900
+ static wrapData(data, rootContext = null, depth = 3) {
1901
+ if (!data || typeof data !== "object") {
1902
+ return data;
1903
+ }
1904
+ if (data instanceof Date || data instanceof RegExp || data instanceof Error) {
1905
+ return data;
1906
+ }
1907
+ if (depth <= 0) {
1908
+ return data;
1909
+ }
1910
+ if (typeof data.getContextValue === "function") {
1911
+ return data;
1912
+ }
1913
+ if (Array.isArray(data)) {
1914
+ return data.map((item) => {
1915
+ if (item && typeof item === "object" && !item.getContextValue) {
1916
+ return new DataWrapper(item, rootContext);
1917
+ }
1918
+ return item;
1919
+ });
1920
+ }
1921
+ return new DataWrapper(data, rootContext);
1922
+ }
1923
+ }
1924
+ class DataWrapper {
1925
+ constructor(data, rootContext = null) {
1926
+ Object.defineProperty(this, "_data", {
1927
+ value: data,
1928
+ writable: false,
1929
+ enumerable: false,
1930
+ configurable: false
1931
+ });
1932
+ Object.defineProperty(this, "_rootContext", {
1933
+ value: rootContext,
1934
+ writable: false,
1935
+ enumerable: false,
1936
+ configurable: false
1937
+ });
1938
+ if (data && typeof data === "object") {
1939
+ for (const key in data) {
1940
+ if (data.hasOwnProperty(key)) {
1941
+ const value = data[key];
1942
+ this[key] = MOJOUtils.wrapData(value, rootContext);
1943
+ }
1944
+ }
1945
+ }
1946
+ }
1947
+ /**
1948
+ * Get value with pipe support
1949
+ * @param {string} key - Key with optional pipes
1950
+ * @returns {*} Value, possibly formatted
1951
+ */
1952
+ getContextValue(key) {
1953
+ let field = key;
1954
+ let pipes = "";
1955
+ let parenDepth = 0;
1956
+ let pipeIndex = -1;
1957
+ for (let i = 0; i < key.length; i++) {
1958
+ const char = key[i];
1959
+ if (char === "(") parenDepth++;
1960
+ else if (char === ")") parenDepth--;
1961
+ else if (char === "|" && parenDepth === 0) {
1962
+ pipeIndex = i;
1963
+ break;
1964
+ }
1965
+ }
1966
+ if (pipeIndex > -1) {
1967
+ field = key.substring(0, pipeIndex).trim();
1968
+ pipes = key.substring(pipeIndex + 1).trim();
1969
+ }
1970
+ let value;
1971
+ if (this._data && this._data.hasOwnProperty(field)) {
1972
+ if (field in this && field !== "_data" && field !== "_rootContext") {
1973
+ value = this[field];
1974
+ } else {
1975
+ value = MOJOUtils.getNestedValue(this._data, field);
1976
+ }
1977
+ } else {
1978
+ value = void 0;
1979
+ }
1980
+ if (pipes && value !== void 0) {
1981
+ return dataFormatter.pipe(value, pipes);
1982
+ }
1983
+ return value;
1984
+ }
1985
+ /**
1986
+ * Check if wrapper has a property
1987
+ * @param {string} key - Property key
1988
+ * @returns {boolean} True if property exists
1989
+ */
1990
+ has(key) {
1991
+ return this._data && this._data.hasOwnProperty(key);
1992
+ }
1993
+ /**
1994
+ * Get the raw wrapped data
1995
+ * @returns {object} The original data object
1996
+ */
1997
+ toJSON() {
1998
+ return this._data;
1999
+ }
2000
+ }
2001
+ MOJOUtils.DataWrapper = DataWrapper;
2002
+ if (typeof window !== "undefined") {
2003
+ window.utils = MOJOUtils;
2004
+ }
2005
+ class EventDelegate {
2006
+ constructor(view) {
2007
+ this.view = view;
2008
+ this.domListeners = [];
2009
+ this.debounceTimers = /* @__PURE__ */ new Map();
2010
+ }
2011
+ bind(rootEl) {
2012
+ this.unbind();
2013
+ if (!rootEl) return;
2014
+ const onClick = async (event) => {
2015
+ const actionEl = event.target.closest("[data-action]");
2016
+ if (actionEl && this.shouldHandle(actionEl, event)) {
2017
+ const action = actionEl.getAttribute("data-action");
2018
+ const handled = await this.dispatch(action, event, actionEl);
2019
+ if (handled) {
2020
+ event.preventDefault();
2021
+ event.stopPropagation();
2022
+ event.handledByChild = true;
2023
+ return;
2024
+ }
2025
+ }
2026
+ const navEl = event.target.closest("a[href], [data-page]");
2027
+ if (navEl && !navEl.hasAttribute("data-action") && this.shouldHandle(navEl, event)) {
2028
+ if (event.ctrlKey || event.metaKey || event.shiftKey || event.button === 1) return;
2029
+ if (navEl.tagName === "A") {
2030
+ const href = navEl.getAttribute("href");
2031
+ if (href && href !== "#" && !href.startsWith("#") && (this.view.isExternalLink(href) || navEl.hasAttribute("data-external"))) return;
2032
+ }
2033
+ event.preventDefault();
2034
+ event.stopPropagation();
2035
+ event.handledByChild = true;
2036
+ if (navEl.hasAttribute("data-page")) await this.view.handlePageNavigation(navEl);
2037
+ else await this.view.handleHrefNavigation(navEl);
2038
+ }
2039
+ };
2040
+ const onChange = (event) => {
2041
+ const el = event.target.closest("[data-change-action]");
2042
+ if (!el || !this.shouldHandle(el, event)) return;
2043
+ const action = el.getAttribute("data-change-action");
2044
+ this.dispatchChange(action, event, el).then((handled) => {
2045
+ if (handled) {
2046
+ event.stopPropagation();
2047
+ event.handledByChild = true;
2048
+ }
2049
+ });
2050
+ };
2051
+ const onInput = (event) => {
2052
+ const el = event.target.closest("[data-change-action]");
2053
+ if (!el || !this.shouldHandle(el, event)) return;
2054
+ const liveSearch = event.target.matches('[data-filter="live-search"]');
2055
+ if (!liveSearch) return;
2056
+ const action = el.getAttribute("data-change-action");
2057
+ const debounceMs = parseInt(el.getAttribute("data-filter-debounce")) || 300;
2058
+ const timerId = `${action}-${el.getAttribute("data-container") || "default"}`;
2059
+ if (this.debounceTimers.has(timerId)) {
2060
+ clearTimeout(this.debounceTimers.get(timerId));
2061
+ }
2062
+ const timer = setTimeout(() => {
2063
+ this.debounceTimers.delete(timerId);
2064
+ this.dispatchChange(action, event, el).then((handled) => {
2065
+ if (handled) {
2066
+ event.stopPropagation();
2067
+ event.handledByChild = true;
2068
+ }
2069
+ });
2070
+ }, debounceMs);
2071
+ this.debounceTimers.set(timerId, timer);
2072
+ };
2073
+ const onEnterKey = (event) => {
2074
+ if (event.key !== "Enter" || !event.target.matches('[data-filter="search"]')) return;
2075
+ const el = event.target.closest("[data-change-action]");
2076
+ if (!el || !this.shouldHandle(el, event)) return;
2077
+ event.preventDefault();
2078
+ const action = el.getAttribute("data-change-action");
2079
+ this.dispatch(action, event, el).then((handled) => {
2080
+ if (handled) {
2081
+ event.stopPropagation();
2082
+ event.handledByChild = true;
2083
+ }
2084
+ });
2085
+ };
2086
+ const onSubmit = (event) => {
2087
+ const form = event.target.closest("form[data-action]");
2088
+ if (!form || !this.shouldHandle(form, event)) return;
2089
+ event.preventDefault();
2090
+ const action = form.getAttribute("data-action");
2091
+ this.dispatch(action, event, form);
2092
+ };
2093
+ rootEl.addEventListener("click", onClick);
2094
+ rootEl.addEventListener("change", onChange);
2095
+ rootEl.addEventListener("input", onInput);
2096
+ rootEl.addEventListener("keydown", onEnterKey);
2097
+ rootEl.addEventListener("submit", onSubmit);
2098
+ this.domListeners.push(
2099
+ { el: rootEl, type: "click", fn: onClick },
2100
+ { el: rootEl, type: "change", fn: onChange },
2101
+ { el: rootEl, type: "input", fn: onInput },
2102
+ { el: rootEl, type: "keydown", fn: onEnterKey },
2103
+ { el: rootEl, type: "submit", fn: onSubmit }
2104
+ );
2105
+ }
2106
+ unbind() {
2107
+ for (const { el, type, fn } of this.domListeners) el.removeEventListener(type, fn);
2108
+ this.domListeners = [];
2109
+ for (const timer of this.debounceTimers.values()) {
2110
+ clearTimeout(timer);
2111
+ }
2112
+ this.debounceTimers.clear();
2113
+ }
2114
+ hideDropdown(element) {
2115
+ const dropdownMenu = element.closest(".dropdown-menu");
2116
+ const dropdown = dropdownMenu.closest(".dropdown");
2117
+ const dropdownBtn = dropdown.querySelector('[data-bs-toggle="dropdown"]');
2118
+ if (dropdownBtn && window.bootstrap?.Dropdown) {
2119
+ const dropdownInstance = window.bootstrap.Dropdown.getInstance(dropdownBtn);
2120
+ dropdownInstance?.hide();
2121
+ }
2122
+ }
2123
+ async dispatch(action, event, el) {
2124
+ const v = this.view;
2125
+ const cap = (s) => s.includes("-") ? s.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("") : s[0].toUpperCase() + s.slice(1);
2126
+ const specific = `handleAction${cap(action)}`;
2127
+ if (typeof v[specific] === "function") {
2128
+ try {
2129
+ event.preventDefault();
2130
+ await v[specific](event, el);
2131
+ return true;
2132
+ } catch (e) {
2133
+ console.error(`Error in action ${action}:`, e);
2134
+ v.handleActionError(action, e, event, el);
2135
+ return true;
2136
+ }
2137
+ }
2138
+ const generic = `onAction${cap(action)}`;
2139
+ if (typeof v[generic] === "function") {
2140
+ try {
2141
+ if (await v[generic](event, el)) {
2142
+ const isInDropdown = !!el.closest(".dropdown-menu");
2143
+ if (isInDropdown) this.hideDropdown(el);
2144
+ event.preventDefault();
2145
+ event.stopPropagation();
2146
+ return true;
2147
+ }
2148
+ return false;
2149
+ } catch (e) {
2150
+ console.error(`Error in action ${action}:`, e);
2151
+ v.handleActionError(action, e, event, el);
2152
+ return true;
2153
+ }
2154
+ }
2155
+ const passThru = `onPassThruAction${cap(action)}`;
2156
+ if (typeof v[passThru] === "function") {
2157
+ try {
2158
+ await v[passThru](event, el);
2159
+ return false;
2160
+ } catch (e) {
2161
+ console.error(`Error in action ${action}:`, e);
2162
+ v.handleActionError(action, e, event, el);
2163
+ return true;
2164
+ }
2165
+ }
2166
+ if (typeof v.onActionDefault === "function") {
2167
+ try {
2168
+ return await v.onActionDefault(action, event, el);
2169
+ } catch (e) {
2170
+ console.error(`Error in default action handler for ${action}:`, e);
2171
+ v.handleActionError(action, e, event, el);
2172
+ return true;
2173
+ }
2174
+ }
2175
+ v.emit?.(`action:${action}`, { action, event, element: el });
2176
+ return false;
2177
+ }
2178
+ async dispatchChange(action, event, el) {
2179
+ const v = this.view;
2180
+ const cap = (s) => s.includes("-") ? s.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("") : s[0].toUpperCase() + s.slice(1);
2181
+ const changeHandler = `onChange${cap(action)}`;
2182
+ if (typeof v[changeHandler] === "function") {
2183
+ try {
2184
+ await v[changeHandler](event, el);
2185
+ return true;
2186
+ } catch (e) {
2187
+ console.error(`Error in onChange ${action}:`, e);
2188
+ v.handleActionError?.(action, e, event, el);
2189
+ return true;
2190
+ }
2191
+ }
2192
+ return await this.dispatch(action, event, el);
2193
+ }
2194
+ shouldHandle(el, event) {
2195
+ if (this.owns(el)) return true;
2196
+ if (this.contains(el) && !event.handledByChild) return true;
2197
+ return false;
2198
+ }
2199
+ owns(el) {
2200
+ const root = this.view.element;
2201
+ if (!root || !root.contains(el)) return false;
2202
+ for (const child of Object.values(this.view.children)) {
2203
+ if (child.element && child.element.contains(el)) return false;
2204
+ }
2205
+ return true;
2206
+ }
2207
+ contains(el) {
2208
+ return !!this.view.element && this.view.element.contains(el);
2209
+ }
2210
+ }
2211
+ const EventEmitter = {
2212
+ /**
2213
+ * Add an event listener
2214
+ * @param {string} event - Event name to listen for
2215
+ * @param {Function} callback - Function to call when event is emitted
2216
+ * @param {Object} [context] - Context to bind the callback to (optional)
2217
+ * @returns {Object} This instance for method chaining
2218
+ *
2219
+ * @example
2220
+ * // With context binding
2221
+ * model.on('change', this.handleChange, this);
2222
+ *
2223
+ * // Without context (traditional)
2224
+ * model.on('change', (data) => console.log(data));
2225
+ */
2226
+ on(event, callback, context) {
2227
+ if (!this._listeners) this._listeners = {};
2228
+ if (!this._listeners[event]) this._listeners[event] = [];
2229
+ const listener = {
2230
+ callback,
2231
+ context,
2232
+ fn: context ? callback.bind(context) : callback
2233
+ };
2234
+ this._listeners[event].push(listener);
2235
+ return this;
2236
+ },
2237
+ /**
2238
+ * Remove an event listener
2239
+ * @param {string} event - Event name
2240
+ * @param {Function} [callback] - Specific callback to remove. If omitted, removes all listeners for the event
2241
+ * @param {Object} [context] - Context that was used when adding the listener
2242
+ * @returns {Object} This instance for method chaining
2243
+ *
2244
+ * @example
2245
+ * // Remove specific listener with context
2246
+ * model.off('change', this.handleChange, this);
2247
+ *
2248
+ * // Remove specific callback (any context)
2249
+ * model.off('change', myHandler);
2250
+ *
2251
+ * // Remove all listeners for an event
2252
+ * model.off('change');
2253
+ */
2254
+ off(event, callback, context) {
2255
+ if (!this._listeners || !this._listeners[event]) return this;
2256
+ if (!callback) {
2257
+ delete this._listeners[event];
2258
+ } else {
2259
+ this._listeners[event] = this._listeners[event].filter((listener) => {
2260
+ if (listener.callback !== callback) return true;
2261
+ if (arguments.length === 3 && listener.context !== context) return true;
2262
+ return false;
2263
+ });
2264
+ if (this._listeners[event].length === 0) {
2265
+ delete this._listeners[event];
2266
+ }
2267
+ }
2268
+ return this;
2269
+ },
2270
+ /**
2271
+ * Add a one-time event listener that automatically removes itself after being called
2272
+ * @param {string} event - Event name to listen for
2273
+ * @param {Function} callback - Function to call when event is emitted (called only once)
2274
+ * @param {Object} [context] - Context to bind the callback to (optional)
2275
+ * @returns {Object} This instance for method chaining
2276
+ *
2277
+ * @example
2278
+ * model.once('ready', this.handleReady, this);
2279
+ */
2280
+ once(event, callback, context) {
2281
+ const onceWrapper = (...args) => {
2282
+ this.off(event, onceWrapper);
2283
+ const fn = context ? callback.bind(context) : callback;
2284
+ fn.apply(context || this, args);
2285
+ };
2286
+ this.on(event, onceWrapper);
2287
+ return this;
2288
+ },
2289
+ /**
2290
+ * Emit an event to all registered listeners
2291
+ * @param {string} event - Event name to emit
2292
+ * @param {...*} args - Arguments to pass to event listeners
2293
+ * @returns {Object} This instance for method chaining
2294
+ *
2295
+ * @example
2296
+ * // Emit with single argument
2297
+ * model.emit('change', { field: 'name', value: 'John' });
2298
+ *
2299
+ * // Emit with multiple arguments
2300
+ * model.emit('update', oldValue, newValue, timestamp);
2301
+ *
2302
+ * // Emit without arguments
2303
+ * model.emit('ready');
2304
+ */
2305
+ emit(event, ...args) {
2306
+ if (!this._listeners || !this._listeners[event]) return this;
2307
+ const listeners = this._listeners[event].slice();
2308
+ for (const listener of listeners) {
2309
+ try {
2310
+ listener.fn.apply(listener.context || this, args);
2311
+ } catch (error) {
2312
+ if (console && console.error) {
2313
+ console.error(`Error in ${event} event handler:`, error);
2314
+ }
2315
+ }
2316
+ }
2317
+ return this;
2318
+ }
2319
+ };
2320
+ if (typeof window !== "undefined") {
2321
+ window.Mustache = Mustache;
2322
+ }
2323
+ class View {
2324
+ // ---------------------------------------------
2325
+ // Construction & defaults
2326
+ // ---------------------------------------------
2327
+ constructor(opts = {}) {
2328
+ this.tagName = opts.tagName ?? "div";
2329
+ this.className = opts.className ?? "mojo-view";
2330
+ this.style = opts.style ?? null;
2331
+ this.id = opts.id ?? View._genId();
2332
+ this.containerId = opts.containerId ?? null;
2333
+ this.container = opts.container ?? null;
2334
+ if (typeof this.container === "string") {
2335
+ this.containerId = this.container;
2336
+ this.container = null;
2337
+ }
2338
+ this.parent = opts.parent ?? null;
2339
+ this.children = opts.children ?? {};
2340
+ this.template = opts.template || opts.templateUrl || "";
2341
+ this.data = opts.data ?? {};
2342
+ this.isRendering = false;
2343
+ this.lastRenderTime = 0;
2344
+ this.mounted = false;
2345
+ this.debug = opts.debug ?? false;
2346
+ this.app = opts.app ?? null;
2347
+ this.cacheTemplate = opts.cacheTemplate ?? true;
2348
+ this.options = { ...opts };
2349
+ this.element = this._ensureElement();
2350
+ this.events = new EventDelegate(this);
2351
+ if (opts.model) this.setModel(opts.model);
2352
+ }
2353
+ // ---------------------------------------------
2354
+ // Lifecycle hooks (overridable)
2355
+ // ---------------------------------------------
2356
+ async onInit() {
2357
+ }
2358
+ async onInitView() {
2359
+ if (this.initialized) return;
2360
+ this.initialized = true;
2361
+ await this.onInit();
2362
+ }
2363
+ async onBeforeRender() {
2364
+ }
2365
+ async onAfterRender() {
2366
+ }
2367
+ async onBeforeMount() {
2368
+ }
2369
+ async onAfterMount() {
2370
+ }
2371
+ async onBeforeUnmount() {
2372
+ }
2373
+ async onAfterUnmount() {
2374
+ }
2375
+ async onBeforeDestroy() {
2376
+ }
2377
+ async onAfterDestroy() {
2378
+ }
2379
+ // ---------------------------------------------
2380
+ // Public API
2381
+ // ---------------------------------------------
2382
+ setModel(model = {}) {
2383
+ let isDiff = model !== this.model;
2384
+ if (!isDiff) return this;
2385
+ if (this.model && this.model.off) {
2386
+ this.model.off("change", this._onModelChange, this);
2387
+ }
2388
+ this.model = model;
2389
+ if (this.model && this.model.on) {
2390
+ this.model.on("change", this._onModelChange, this);
2391
+ }
2392
+ for (const id in this.children) {
2393
+ const child = this.children[id];
2394
+ if (child && typeof child.setModel === "function") {
2395
+ child.setModel(model);
2396
+ }
2397
+ }
2398
+ if (isDiff) {
2399
+ this._onModelChange();
2400
+ }
2401
+ return this;
2402
+ }
2403
+ _onModelChange() {
2404
+ if (this.isMounted()) {
2405
+ this.render();
2406
+ }
2407
+ }
2408
+ setTemplate(tpl) {
2409
+ this.template = tpl ?? "";
2410
+ return this;
2411
+ }
2412
+ addChild(childView) {
2413
+ try {
2414
+ if (!childView || typeof childView !== "object") return this;
2415
+ childView.parent = this;
2416
+ if (this.getApp()) childView.app = this.app;
2417
+ this.children[childView.id] = childView;
2418
+ } catch (e) {
2419
+ View._warn("addChild error", e);
2420
+ }
2421
+ return childView;
2422
+ }
2423
+ removeChild(idOrView) {
2424
+ try {
2425
+ const id = typeof idOrView === "string" ? idOrView : idOrView && idOrView.id;
2426
+ if (!id) return this;
2427
+ const child = this.children[id];
2428
+ if (child) {
2429
+ Promise.resolve(child.destroy()).catch((err) => View._warn("removeChild destroy error", err));
2430
+ delete this.children[id];
2431
+ }
2432
+ } catch (e) {
2433
+ View._warn("removeChild error", e);
2434
+ }
2435
+ return this;
2436
+ }
2437
+ getChild(id) {
2438
+ return this.children[id];
2439
+ }
2440
+ async updateData(newData, rerender = false) {
2441
+ Object.assign(this.data, newData);
2442
+ if (rerender && this.isMounted()) {
2443
+ await this.render();
2444
+ }
2445
+ return this;
2446
+ }
2447
+ toggleClass(className, force) {
2448
+ if (force === void 0) {
2449
+ force = !this.element.classList.contains(className);
2450
+ }
2451
+ this.element.classList.toggle(className, force);
2452
+ return this;
2453
+ }
2454
+ addClass(className) {
2455
+ this.element.classList.add(className);
2456
+ return this;
2457
+ }
2458
+ setClass(className) {
2459
+ this.element.className = className;
2460
+ return this;
2461
+ }
2462
+ removeClass(className) {
2463
+ this.element.classList.remove(className);
2464
+ return this;
2465
+ }
2466
+ canRender() {
2467
+ if (this.options.renderCooldown > 0 && now - this.lastRenderTime < this.options.renderCooldown) {
2468
+ View._warn(`View ${this.id}: Render called too quickly, cooldown active`);
2469
+ return false;
2470
+ }
2471
+ if (this.options.noAppend) {
2472
+ if (this.parent) {
2473
+ if (!this.parent.contains(this.containerId || this.container)) {
2474
+ return false;
2475
+ } else if (this.containerId && !document.getElementById(this.containerId)) {
2476
+ return false;
2477
+ } else if (this.container && !document.contains(this.container)) {
2478
+ return false;
2479
+ }
2480
+ }
2481
+ }
2482
+ return true;
2483
+ }
2484
+ // ---------------------------------------------
2485
+ // Render flow
2486
+ // ---------------------------------------------
2487
+ async render(allowMount = true, container = null) {
2488
+ const now2 = Date.now();
2489
+ if (!this.canRender()) {
2490
+ return this;
2491
+ }
2492
+ this.isRendering = true;
2493
+ this.lastRenderTime = now2;
2494
+ try {
2495
+ if (!this.initialized) await this.onInitView();
2496
+ this.events.unbind();
2497
+ await this.onBeforeRender();
2498
+ if (this.getViewData) {
2499
+ this.data = await this.getViewData();
2500
+ }
2501
+ const html = await this.renderTemplate();
2502
+ this.element.innerHTML = html;
2503
+ if (allowMount && !this.isMounted()) {
2504
+ await this.mount(container);
2505
+ }
2506
+ await this._renderChildren();
2507
+ await this.onAfterRender();
2508
+ this.events.bind(this.element);
2509
+ } catch (e) {
2510
+ View._warn(`Render error in ${this.id}`, e);
2511
+ } finally {
2512
+ this.isRendering = false;
2513
+ }
2514
+ return this;
2515
+ }
2516
+ async _renderChildren() {
2517
+ for (const id in this.children) {
2518
+ const child = this.children[id];
2519
+ if (!child) continue;
2520
+ child.parent = this;
2521
+ await Promise.resolve(child.render()).catch((err) => View._warn(`Child render error (${id})`, err));
2522
+ }
2523
+ }
2524
+ async _unmountChildren() {
2525
+ for (const id in this.children) {
2526
+ const child = this.children[id];
2527
+ if (!child) continue;
2528
+ child.unbindEvents();
2529
+ }
2530
+ }
2531
+ isMounted() {
2532
+ return this.element?.isConnected;
2533
+ }
2534
+ getChildElementById(id, root = null) {
2535
+ const cleanId = id.startsWith("#") ? id.substring(1) : id;
2536
+ if (root) {
2537
+ return root.querySelector(`#${cleanId}`);
2538
+ }
2539
+ return this.element.querySelector(`#${cleanId}`);
2540
+ }
2541
+ getChildElement(id) {
2542
+ if (id.startsWith("#")) {
2543
+ return this.getChildElementById(id);
2544
+ }
2545
+ let el = this.element?.querySelector(`[data-container="${id}"]`);
2546
+ if (!el) {
2547
+ return this.getChildElementById(id);
2548
+ }
2549
+ return el;
2550
+ }
2551
+ getContainer() {
2552
+ if (this.replaceById) {
2553
+ if (this.parent) {
2554
+ return this.parent.getChildElementById(this.id);
2555
+ }
2556
+ return null;
2557
+ }
2558
+ if (!this.containerId) return null;
2559
+ if (this.parent) {
2560
+ return this.parent.getChildElement(this.containerId);
2561
+ }
2562
+ return this.getChildElementById(this.containerId, document.body);
2563
+ }
2564
+ async mount(container = null) {
2565
+ await this.onBeforeMount();
2566
+ if (!container) {
2567
+ container = this.getContainer();
2568
+ }
2569
+ if (this.containerId && !container) {
2570
+ console.error(`Container not found for ${this.containerId}`);
2571
+ return;
2572
+ }
2573
+ if (container && this.replaceById) {
2574
+ container.replaceWith(this.element);
2575
+ } else if (container) {
2576
+ container.replaceChildren(this.element);
2577
+ } else if (!this.containerId && this.parent) {
2578
+ this.parent.element.appendChild(this.element);
2579
+ } else if (!this.containerId && !this.parent) {
2580
+ document.body.appendChild(this.element);
2581
+ } else {
2582
+ console.error(`Container not found for ${this.containerId}`);
2583
+ }
2584
+ await this.onAfterMount();
2585
+ this.mounted = true;
2586
+ }
2587
+ async unmount() {
2588
+ if (!this.element || !this.element.parentNode) return;
2589
+ await this.onBeforeUnmount();
2590
+ await this._unmountChildren();
2591
+ if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
2592
+ this.events.unbind();
2593
+ await this.onAfterUnmount();
2594
+ this.mounted = false;
2595
+ }
2596
+ // FIX #1: make destroy async (it already awaited hooks)
2597
+ async destroy() {
2598
+ try {
2599
+ this.events.unbind();
2600
+ for (const id in this.children) {
2601
+ const ch = this.children[id];
2602
+ if (ch) {
2603
+ await Promise.resolve(ch.destroy()).catch((err) => View._warn(`Child destroy error (${id})`, err));
2604
+ }
2605
+ }
2606
+ this.mounted = false;
2607
+ if (this.element && this.element.parentNode) {
2608
+ await this.onBeforeDestroy();
2609
+ if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
2610
+ await this.onAfterDestroy();
2611
+ }
2612
+ } catch (e) {
2613
+ View._warn(`Destroy error in ${this.id}`, e);
2614
+ }
2615
+ }
2616
+ // ---------------------------------------------
2617
+ // DOM helpers
2618
+ // ---------------------------------------------
2619
+ _ensureElement() {
2620
+ try {
2621
+ if (this.element && this.element.tagName?.toLowerCase() === this.tagName) {
2622
+ this._syncAttrs();
2623
+ return this.element;
2624
+ }
2625
+ const el = document.createElement(this.tagName);
2626
+ this.element = el;
2627
+ this.el = el;
2628
+ this._syncAttrs();
2629
+ return el;
2630
+ } catch (e) {
2631
+ View._warn("ensureElement error", e);
2632
+ const el = document.createElement("div");
2633
+ el.id = this.id || View._genId();
2634
+ return el;
2635
+ }
2636
+ }
2637
+ _syncAttrs() {
2638
+ try {
2639
+ if (!this.element) return;
2640
+ if (this.id) this.element.id = this.id;
2641
+ this.element.className = this.className || "";
2642
+ if (this.style == null) {
2643
+ this.element.removeAttribute("style");
2644
+ } else {
2645
+ this.element.style.cssText = String(this.style);
2646
+ }
2647
+ } catch (e) {
2648
+ View._warn("_syncAttrs error", e);
2649
+ }
2650
+ }
2651
+ bindEvents() {
2652
+ this.events.bind(this.element);
2653
+ }
2654
+ unbindEvents() {
2655
+ this.events.unbind();
2656
+ }
2657
+ // ---------------------------------------------
2658
+ // Template helpers
2659
+ // ---------------------------------------------
2660
+ async renderTemplate() {
2661
+ const templateContent = await this.getTemplate();
2662
+ if (!templateContent) return "";
2663
+ const partials = this.getPartials();
2664
+ return Mustache.render(templateContent, this, partials);
2665
+ }
2666
+ renderTemplateString(template, context, partials) {
2667
+ return Mustache.render(template, context, partials);
2668
+ }
2669
+ getPartials() {
2670
+ return {};
2671
+ }
2672
+ async getTemplate() {
2673
+ if (this._templateCache && this.cacheTemplate) {
2674
+ return this._templateCache;
2675
+ }
2676
+ const template = this.template || this.templateUrl;
2677
+ if (!template) {
2678
+ throw new Error("Template not found");
2679
+ }
2680
+ let templateContent = "";
2681
+ if (typeof template === "string") {
2682
+ if (template.includes("<") || template.includes("{")) {
2683
+ templateContent = template;
2684
+ } else {
2685
+ try {
2686
+ let templatePath = template;
2687
+ if (!this.app) this.app = this.getApp();
2688
+ if (this.app && this.app.basePath) {
2689
+ if (!templatePath.startsWith("/") && !templatePath.startsWith("http://") && !templatePath.startsWith("https://")) {
2690
+ const base = this.app.basePath.endsWith("/") ? this.app.basePath.slice(0, -1) : this.app.basePath;
2691
+ templatePath = `${base}/${templatePath}`;
2692
+ }
2693
+ }
2694
+ const response = await fetch(templatePath);
2695
+ if (!response.ok) {
2696
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
2697
+ }
2698
+ templateContent = await response.text();
2699
+ } catch (error) {
2700
+ View._warn(`Failed to load template from ${template}: ${error}`);
2701
+ this.showError?.(`Failed to load template from ${template}: ${error.message}`);
2702
+ }
2703
+ }
2704
+ } else if (typeof template === "function") {
2705
+ templateContent = await this.template(this.data, this.state);
2706
+ }
2707
+ if (this.cacheTemplate && templateContent) {
2708
+ this._templateCache = templateContent;
2709
+ }
2710
+ return templateContent;
2711
+ }
2712
+ getContextValue(path) {
2713
+ const value = MOJOUtils.getContextData(this, path);
2714
+ if (path && path.startsWith("data.") && value && typeof value === "object") {
2715
+ return MOJOUtils.wrapData(value, this);
2716
+ }
2717
+ if (path && path.startsWith("model.") && path !== "model" && value && typeof value === "object" && typeof value.getContextValue !== "function") {
2718
+ return MOJOUtils.wrapData(value, null);
2719
+ }
2720
+ return value;
2721
+ }
2722
+ // 9) Navigation Helpers ------------------------------------------------------
2723
+ async handlePageNavigation(element) {
2724
+ const pageName = element.getAttribute("data-page");
2725
+ const paramsAttr = element.getAttribute("data-params");
2726
+ let params = {};
2727
+ if (paramsAttr) {
2728
+ try {
2729
+ params = JSON.parse(paramsAttr);
2730
+ } catch (error) {
2731
+ console.warn("Invalid JSON in data-params:", paramsAttr);
2732
+ }
2733
+ }
2734
+ const app = this.getApp();
2735
+ if (app) {
2736
+ app.showPage(pageName, params);
2737
+ return;
2738
+ }
2739
+ const router = this.findRouter();
2740
+ if (router && typeof router.navigateToPage === "function") {
2741
+ await router.navigateToPage(pageName, params);
2742
+ } else {
2743
+ console.error(`No router found for page navigation to '${pageName}'`);
2744
+ }
2745
+ }
2746
+ async handleHrefNavigation(element) {
2747
+ const href = element.getAttribute("href");
2748
+ if (this.isExternalLink(href) || element.hasAttribute("data-external")) return;
2749
+ const router = this.findRouter();
2750
+ if (router) {
2751
+ if (router.options && router.options.mode === "param" && href.startsWith("?")) {
2752
+ const fullPath = "/" + href;
2753
+ await router.navigate(fullPath);
2754
+ return;
2755
+ }
2756
+ if (router.options && router.options.mode === "hash" && href.startsWith("#")) {
2757
+ await router.navigate(href);
2758
+ return;
2759
+ }
2760
+ const routePath = this.hrefToRoutePath(href);
2761
+ await router.navigate(routePath);
2762
+ } else {
2763
+ console.warn("No router found for navigation, using default behavior");
2764
+ window.location.href = href;
2765
+ }
2766
+ }
2767
+ isExternalLink(href) {
2768
+ if (!href) return true;
2769
+ if (href.startsWith("/") && this.getApp()) {
2770
+ if (href.startsWith(this.findRouter().basePath)) return false;
2771
+ return true;
2772
+ }
2773
+ return href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//");
2774
+ }
2775
+ hrefToRoutePath(href) {
2776
+ if (href.startsWith("/")) {
2777
+ const router = this.findRouter();
2778
+ if (router && router.options && router.options.base) {
2779
+ const base = router.options.base;
2780
+ if (href.startsWith(base)) return href.substring(base.length) || "/";
2781
+ }
2782
+ return href;
2783
+ }
2784
+ return href.startsWith("./") ? href.substring(2) : href;
2785
+ }
2786
+ findRouter() {
2787
+ this.getApp();
2788
+ return this.app?.router || null;
2789
+ }
2790
+ getApp() {
2791
+ if (this.app) return this.app;
2792
+ const apps = [
2793
+ window.__app__,
2794
+ window.matchUUID ? window[window.matchUUID]() : window[window.matchUUID],
2795
+ window.MOJO?.app,
2796
+ window.APP,
2797
+ window.app,
2798
+ window.WebApp
2799
+ ];
2800
+ this.app = apps.find((app) => app && typeof app.showPage === "function") || null;
2801
+ return this.app;
2802
+ }
2803
+ handleActionError(action, err, evt, el) {
2804
+ this.showError(`Action '${action}' failed: ${err}`, evt, el);
2805
+ }
2806
+ // ---------------------------------------------
2807
+ // Utilities
2808
+ // ---------------------------------------------
2809
+ /**
2810
+ * Escape HTML characters
2811
+ * @param {string} str - String to escape
2812
+ * @returns {string} Escaped string
2813
+ */
2814
+ escapeHtml(str) {
2815
+ if (typeof str !== "string") return str;
2816
+ const div = document.createElement("div");
2817
+ div.textContent = str;
2818
+ return div.innerHTML;
2819
+ }
2820
+ contains(el) {
2821
+ if (typeof el === "string") {
2822
+ if (!this.element) return false;
2823
+ el = document.getElementById(el);
2824
+ }
2825
+ if (!el) return false;
2826
+ return this.element.contains(el);
2827
+ }
2828
+ /**
2829
+ * Show error message
2830
+ * @param {string} message - Error message
2831
+ */
2832
+ async showError(message) {
2833
+ console.error(`View ${this.id} error:`, message);
2834
+ const app = this.getApp ? this.getApp() : this.app || null;
2835
+ if (app && typeof app.showError === "function") {
2836
+ await app.showError(message);
2837
+ return;
2838
+ }
2839
+ alert(`Error: ${message}`);
2840
+ }
2841
+ /**
2842
+ * Show success message
2843
+ * @param {string} message - Success message
2844
+ */
2845
+ async showSuccess(message) {
2846
+ if (this.debug) {
2847
+ console.log(`View ${this.id} success:`, message);
2848
+ }
2849
+ const app = this.getApp ? this.getApp() : this.app || null;
2850
+ if (app && typeof app.showSuccess === "function") {
2851
+ await app.showSuccess(message);
2852
+ return;
2853
+ }
2854
+ alert(`Success: ${message}`);
2855
+ }
2856
+ /**
2857
+ * Show info message
2858
+ * @param {string} message - Info message
2859
+ */
2860
+ async showInfo(message) {
2861
+ console.info(`View ${this.id} info:`, message);
2862
+ const app = this.getApp ? this.getApp() : this.app || null;
2863
+ if (app && typeof app.showInfo === "function") {
2864
+ await app.showInfo(message);
2865
+ return;
2866
+ }
2867
+ alert(`Info: ${message}`);
2868
+ }
2869
+ /**
2870
+ * Show warning message
2871
+ * @param {string} message - Warning message
2872
+ */
2873
+ async showWarning(message) {
2874
+ console.warn(`View ${this.id} warning:`, message);
2875
+ const app = this.getApp ? this.getApp() : this.app || null;
2876
+ if (app && typeof app.showWarning === "function") {
2877
+ await app.showWarning(message);
2878
+ return;
2879
+ }
2880
+ alert(`Warning: ${message}`);
2881
+ }
2882
+ static _genId() {
2883
+ return `view_${Math.random().toString(36).substr(2, 9)}`;
2884
+ }
2885
+ static _warn(msg, err) {
2886
+ try {
2887
+ if (err) console.warn(`[View] ${msg}:`, err);
2888
+ else console.warn(`[View] ${msg}`);
2889
+ } catch {
2890
+ }
2891
+ }
2892
+ }
2893
+ Object.assign(View.prototype, EventEmitter);
2894
+ class Router {
2895
+ constructor(options = {}) {
2896
+ this.defaultRoute = options.defaultRoute || "home";
2897
+ this.routes = [];
2898
+ this.currentRoute = null;
2899
+ this.eventEmitter = options.eventEmitter || null;
2900
+ this.boundHandlePopState = this.handlePopState.bind(this);
2901
+ }
2902
+ start() {
2903
+ window.addEventListener("popstate", this.boundHandlePopState);
2904
+ this.handleCurrentLocation();
2905
+ }
2906
+ stop() {
2907
+ window.removeEventListener("popstate", this.boundHandlePopState);
2908
+ }
2909
+ addRoute(pattern, pageName) {
2910
+ this.routes.push({
2911
+ pattern: this.normalizePattern(pattern),
2912
+ regex: this.patternToRegex(pattern),
2913
+ pageName,
2914
+ paramNames: this.extractParamNames(pattern)
2915
+ });
2916
+ }
2917
+ async navigate(path, options = {}) {
2918
+ const { replace = false, state = null, trigger = true } = options;
2919
+ const { pageName, queryParams } = this.parseInput(path);
2920
+ if (trigger) {
2921
+ await this.handleRouteChange(pageName, queryParams);
2922
+ }
2923
+ }
2924
+ // Browser navigation
2925
+ back() {
2926
+ window.history.back();
2927
+ }
2928
+ forward() {
2929
+ window.history.forward();
2930
+ }
2931
+ // Get current route info
2932
+ getCurrentRoute() {
2933
+ return this.currentRoute;
2934
+ }
2935
+ getCurrentPath() {
2936
+ const { pageName, queryParams } = this.parseCurrentUrl();
2937
+ return this.buildPublicUrl(pageName, queryParams);
2938
+ }
2939
+ // Private methods
2940
+ handlePopState(_event) {
2941
+ this.handleCurrentLocation();
2942
+ }
2943
+ async handleCurrentLocation() {
2944
+ const { pageName, queryParams } = this.parseCurrentUrl();
2945
+ await this.handleRouteChange(pageName, queryParams);
2946
+ }
2947
+ async handleRouteChange(pageName, queryParams) {
2948
+ const routePath = "/" + pageName;
2949
+ const route = this.matchRoute(routePath);
2950
+ const publicUrl = this.buildPublicUrl(pageName, queryParams);
2951
+ if (!route) {
2952
+ console.log("No route matched for page:", pageName);
2953
+ if (this.eventEmitter) {
2954
+ this.eventEmitter.emit("route:notfound", { path: publicUrl });
2955
+ }
2956
+ return null;
2957
+ }
2958
+ this.currentRoute = route;
2959
+ if (this.eventEmitter) {
2960
+ this.eventEmitter.emit("route:changed", {
2961
+ path: publicUrl,
2962
+ pageName: route.pageName,
2963
+ params: route.params,
2964
+ query: queryParams,
2965
+ route
2966
+ });
2967
+ }
2968
+ return route;
2969
+ }
2970
+ matchRoute(path) {
2971
+ for (const route of this.routes) {
2972
+ const match = path.match(route.regex);
2973
+ if (match) {
2974
+ const params = {};
2975
+ route.paramNames.forEach((name, index) => {
2976
+ params[name] = match[index + 1];
2977
+ });
2978
+ return {
2979
+ ...route,
2980
+ params,
2981
+ path
2982
+ };
2983
+ }
2984
+ }
2985
+ return null;
2986
+ }
2987
+ // Parse any input format and extract page name + query params
2988
+ parseInput(input) {
2989
+ let pageName = this.defaultRoute;
2990
+ let queryParams = {};
2991
+ if (!input) {
2992
+ return { pageName, queryParams };
2993
+ }
2994
+ try {
2995
+ if (input.includes("?")) {
2996
+ const [pathPart, queryPart] = input.split("?", 2);
2997
+ const urlParams = new URLSearchParams(queryPart);
2998
+ if (urlParams.has("page")) {
2999
+ pageName = urlParams.get("page") || this.defaultRoute;
3000
+ for (const [key, value] of urlParams) {
3001
+ if (key !== "page") {
3002
+ queryParams[key] = value;
3003
+ }
3004
+ }
3005
+ } else {
3006
+ if (pathPart.startsWith("/")) {
3007
+ pageName = pathPart.substring(1) || this.defaultRoute;
3008
+ } else {
3009
+ pageName = pathPart || this.defaultRoute;
3010
+ }
3011
+ for (const [key, value] of urlParams) {
3012
+ queryParams[key] = value;
3013
+ }
3014
+ }
3015
+ } else if (input.startsWith("/")) {
3016
+ pageName = input.substring(1) || this.defaultRoute;
3017
+ } else {
3018
+ pageName = input;
3019
+ }
3020
+ } catch (error) {
3021
+ console.warn("Failed to parse input:", input, error);
3022
+ pageName = this.defaultRoute;
3023
+ queryParams = {};
3024
+ }
3025
+ return { pageName, queryParams };
3026
+ }
3027
+ // Parse current browser URL
3028
+ parseCurrentUrl() {
3029
+ const urlParams = new URLSearchParams(window.location.search);
3030
+ const pageName = urlParams.get("page") || this.defaultRoute;
3031
+ const queryParams = {};
3032
+ for (const [key, value] of urlParams) {
3033
+ if (key !== "page") {
3034
+ queryParams[key] = value;
3035
+ }
3036
+ }
3037
+ return { pageName, queryParams };
3038
+ }
3039
+ // Build public URL format: ?page=admin&group=123
3040
+ buildPublicUrl(pageName, queryParams = {}) {
3041
+ const urlParams = new URLSearchParams();
3042
+ urlParams.set("page", pageName);
3043
+ Object.entries(queryParams).forEach(([key, value]) => {
3044
+ if (value !== null && value !== void 0 && value !== "") {
3045
+ urlParams.set(key, String(value));
3046
+ }
3047
+ });
3048
+ return "?" + urlParams.toString();
3049
+ }
3050
+ // Update browser URL
3051
+ updateBrowserUrl(pageName, queryParams, replace, state) {
3052
+ const currentUrl = new URL(window.location.origin + window.location.pathname);
3053
+ currentUrl.searchParams.set("page", pageName);
3054
+ Object.entries(queryParams).forEach(([key, value]) => {
3055
+ if (value !== null && value !== void 0 && value !== "") {
3056
+ currentUrl.searchParams.set(key, String(value));
3057
+ }
3058
+ });
3059
+ const url = currentUrl.toString();
3060
+ if (replace) {
3061
+ window.history.replaceState(state, "", url);
3062
+ } else {
3063
+ window.history.pushState(state, "", url);
3064
+ }
3065
+ }
3066
+ // Route pattern utilities
3067
+ patternToRegex(pattern) {
3068
+ let regexPattern = pattern.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&").replace(/\/:([^/?]+)\?/g, "(?:/([^/]+))?").replace(/:([^/]+)/g, "([^/]+)");
3069
+ return new RegExp(`^${regexPattern}$`);
3070
+ }
3071
+ extractParamNames(pattern) {
3072
+ const matches = pattern.match(/:([^/?]+)\??/g) || [];
3073
+ return matches.map((match) => match.replace(/[:?]/g, ""));
3074
+ }
3075
+ normalizePattern(pattern) {
3076
+ return pattern.startsWith("/") ? pattern : `/${pattern}`;
3077
+ }
3078
+ /**
3079
+ * Update URL parameters without triggering navigation
3080
+ * @param {object} params - Parameters to update in URL
3081
+ * @param {object} options - Options like { replace: true }
3082
+ */
3083
+ updateUrl(params = {}, options = {}) {
3084
+ const { replace = false } = options;
3085
+ const { pageName } = this.parseCurrentUrl();
3086
+ this.updateBrowserUrl(pageName, params, replace);
3087
+ }
3088
+ /**
3089
+ * Build URL for given page and query parameters
3090
+ */
3091
+ buildUrl(page, query = {}) {
3092
+ return this.buildPublicUrl(page, query);
3093
+ }
3094
+ /**
3095
+ * Check if two routes match (ignoring query parameters)
3096
+ * @param {string} route1 - First route in any format ("/admin", "?page=admin", "admin")
3097
+ * @param {string} route2 - Second route in any format
3098
+ * @returns {boolean} - True if routes point to the same page
3099
+ */
3100
+ doRoutesMatch(route1, route2) {
3101
+ if (!route1 || !route2) return false;
3102
+ const { pageName: pageName1 } = this.parseInput(route1);
3103
+ const { pageName: pageName2 } = this.parseInput(route2);
3104
+ return pageName1 === pageName2;
3105
+ }
3106
+ }
3107
+ class Rest {
3108
+ constructor() {
3109
+ this.config = {
3110
+ baseURL: "",
3111
+ timeout: 3e4,
3112
+ headers: {
3113
+ "Content-Type": "application/json",
3114
+ "Accept": "application/json"
3115
+ },
3116
+ trackDevice: true,
3117
+ // New setting to control DUID tracking
3118
+ duidHeader: "X-Mojo-UID",
3119
+ // Header name for the DUID
3120
+ duidTransport: "header"
3121
+ // How to send the DUID: 'payload' or 'header'
3122
+ };
3123
+ this.interceptors = {
3124
+ request: [],
3125
+ response: []
3126
+ };
3127
+ this.duid = null;
3128
+ if (this.config.trackDevice) {
3129
+ this._initializeDuid();
3130
+ }
3131
+ }
3132
+ /**
3133
+ * Initialize or generate the Device Unique ID (DUID)
3134
+ * @private
3135
+ */
3136
+ _initializeDuid() {
3137
+ const storageKey = "mojo_device_uid";
3138
+ try {
3139
+ let storedDuid = localStorage.getItem(storageKey);
3140
+ if (storedDuid) {
3141
+ this.duid = storedDuid;
3142
+ } else {
3143
+ this.duid = this._generateDuid();
3144
+ localStorage.setItem(storageKey, this.duid);
3145
+ }
3146
+ } catch (e) {
3147
+ console.error("Could not access localStorage to get/set DUID.", e);
3148
+ this.duid = this._generateDuid();
3149
+ }
3150
+ }
3151
+ /**
3152
+ * Generate a new DUID (UUID v4)
3153
+ * @private
3154
+ * @returns {string} A new UUID
3155
+ */
3156
+ _generateDuid() {
3157
+ if (crypto && crypto.randomUUID) {
3158
+ return crypto.randomUUID();
3159
+ }
3160
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
3161
+ const r = Math.random() * 16 | 0, v = c === "x" ? r : r & 3 | 8;
3162
+ return v.toString(16);
3163
+ });
3164
+ }
3165
+ /**
3166
+ * Configure the REST client
3167
+ * @param {object} config - Configuration object
3168
+ */
3169
+ configure(config) {
3170
+ if (config.baseUrl) config.baseURL = config.baseUrl;
3171
+ const oldTrackDevice = this.config.trackDevice;
3172
+ this.config = {
3173
+ ...this.config,
3174
+ ...config,
3175
+ headers: {
3176
+ ...this.config.headers,
3177
+ ...config.headers
3178
+ }
3179
+ };
3180
+ if (this.config.trackDevice && !oldTrackDevice) {
3181
+ this._initializeDuid();
3182
+ }
3183
+ }
3184
+ /**
3185
+ * Add request or response interceptor
3186
+ * @param {string} type - 'request' or 'response'
3187
+ * @param {function} interceptor - Interceptor function
3188
+ */
3189
+ addInterceptor(type, interceptor) {
3190
+ if (this.interceptors[type]) {
3191
+ this.interceptors[type].push(interceptor);
3192
+ }
3193
+ }
3194
+ /**
3195
+ * Build complete URL
3196
+ * @param {string} url - Endpoint URL
3197
+ * @returns {string} Complete URL
3198
+ */
3199
+ buildUrl(url) {
3200
+ if (url.startsWith("http://") || url.startsWith("https://")) {
3201
+ return url;
3202
+ }
3203
+ const baseURL = this.config.baseURL.endsWith("/") ? this.config.baseURL.slice(0, -1) : this.config.baseURL;
3204
+ const endpoint = url.startsWith("/") ? url : `/${url}`;
3205
+ return `${baseURL}${endpoint}`;
3206
+ }
3207
+ /**
3208
+ * Categorize error into common reason codes
3209
+ * @param {Error} error - The error object
3210
+ * @param {number} status - HTTP status code (if available)
3211
+ * @returns {object} Object with reason code and user-friendly message
3212
+ */
3213
+ categorizeError(error, status = 0) {
3214
+ if (error.name === "TypeError" && error.message.includes("fetch")) {
3215
+ return {
3216
+ reason: "not_reachable",
3217
+ message: "Service is not reachable - please check your connection"
3218
+ };
3219
+ }
3220
+ if (error.name === "AbortError") {
3221
+ return {
3222
+ reason: "cancelled",
3223
+ message: "Request was cancelled"
3224
+ };
3225
+ }
3226
+ if (error.name === "TimeoutError" || error.message.includes("timeout")) {
3227
+ return {
3228
+ reason: "timed_out",
3229
+ message: "Request timed out - please try again"
3230
+ };
3231
+ }
3232
+ if (status >= 400) {
3233
+ if (status === 400) {
3234
+ return {
3235
+ reason: "bad_request",
3236
+ message: "Invalid request data"
3237
+ };
3238
+ }
3239
+ if (status === 401) {
3240
+ return {
3241
+ reason: "unauthorized",
3242
+ message: "Authentication required"
3243
+ };
3244
+ }
3245
+ if (status === 403) {
3246
+ return {
3247
+ reason: "forbidden",
3248
+ message: "Access denied"
3249
+ };
3250
+ }
3251
+ if (status === 404) {
3252
+ return {
3253
+ reason: "not_found",
3254
+ message: "Resource not found"
3255
+ };
3256
+ }
3257
+ if (status === 409) {
3258
+ return {
3259
+ reason: "conflict",
3260
+ message: "Resource conflict"
3261
+ };
3262
+ }
3263
+ if (status === 422) {
3264
+ return {
3265
+ reason: "validation_error",
3266
+ message: "Validation failed"
3267
+ };
3268
+ }
3269
+ if (status === 429) {
3270
+ return {
3271
+ reason: "rate_limited",
3272
+ message: "Too many requests - please wait"
3273
+ };
3274
+ }
3275
+ if (status >= 500) {
3276
+ return {
3277
+ reason: "server_error",
3278
+ message: "Server error - please try again later"
3279
+ };
3280
+ }
3281
+ if (status >= 400) {
3282
+ return {
3283
+ reason: "client_error",
3284
+ message: "Request error"
3285
+ };
3286
+ }
3287
+ }
3288
+ if (error.message.includes("CORS")) {
3289
+ return {
3290
+ reason: "cors_error",
3291
+ message: "Cross-origin request blocked"
3292
+ };
3293
+ }
3294
+ if (error.message.includes("DNS") || error.message.includes("ENOTFOUND")) {
3295
+ return {
3296
+ reason: "dns_error",
3297
+ message: "Unable to resolve server address"
3298
+ };
3299
+ }
3300
+ return {
3301
+ reason: "unknown_error",
3302
+ message: `Network error: ${error.message}`
3303
+ };
3304
+ }
3305
+ /**
3306
+ * Build query string from parameters
3307
+ * @param {object} params - Query parameters
3308
+ * @returns {string} Query string
3309
+ */
3310
+ buildQueryString(params = {}) {
3311
+ const searchParams = new URLSearchParams();
3312
+ Object.entries(params).forEach(([key, value]) => {
3313
+ if (value !== null && value !== void 0) {
3314
+ if (Array.isArray(value)) {
3315
+ value.forEach((v) => searchParams.append(`${key}[]`, v));
3316
+ } else {
3317
+ searchParams.append(key, value);
3318
+ }
3319
+ }
3320
+ });
3321
+ const queryString = searchParams.toString();
3322
+ return queryString ? `?${queryString}` : "";
3323
+ }
3324
+ /**
3325
+ * Process request through interceptors
3326
+ * @param {object} request - Request configuration
3327
+ * @returns {object} Processed request configuration
3328
+ */
3329
+ async processRequestInterceptors(request) {
3330
+ let processedRequest = { ...request };
3331
+ for (const interceptor of this.interceptors.request) {
3332
+ try {
3333
+ processedRequest = await interceptor(processedRequest);
3334
+ } catch (error) {
3335
+ console.error("Request interceptor error:", error);
3336
+ throw error;
3337
+ }
3338
+ }
3339
+ return processedRequest;
3340
+ }
3341
+ /**
3342
+ * Process response through interceptors
3343
+ * @param {Response} response - Fetch response object
3344
+ * @param {object} request - Original request configuration
3345
+ * @returns {object} Processed response data
3346
+ */
3347
+ async processResponseInterceptors(response, request) {
3348
+ let responseData = {
3349
+ success: response.ok,
3350
+ status: response.status,
3351
+ statusText: response.statusText,
3352
+ headers: Object.fromEntries(response.headers.entries()),
3353
+ data: null,
3354
+ errors: null,
3355
+ message: null,
3356
+ reason: null
3357
+ };
3358
+ try {
3359
+ const contentType = response.headers.get("content-type");
3360
+ if (contentType && contentType.includes("application/json")) {
3361
+ const jsonData = await response.json();
3362
+ responseData.data = jsonData;
3363
+ if (!response.ok) {
3364
+ const errorInfo = this.categorizeError(new Error("HTTP Error"), response.status);
3365
+ responseData.errors = jsonData.errors || {};
3366
+ responseData.message = jsonData.message || errorInfo.message;
3367
+ responseData.reason = errorInfo.reason;
3368
+ }
3369
+ } else {
3370
+ responseData.data = await response.text();
3371
+ if (!response.ok) {
3372
+ const errorInfo = this.categorizeError(new Error("HTTP Error"), response.status);
3373
+ responseData.message = errorInfo.message;
3374
+ responseData.reason = errorInfo.reason;
3375
+ }
3376
+ }
3377
+ } catch (error) {
3378
+ responseData.errors = { parse: "Failed to parse response" };
3379
+ responseData.message = "Invalid response format";
3380
+ }
3381
+ for (const interceptor of this.interceptors.response) {
3382
+ try {
3383
+ responseData = await interceptor(responseData, request);
3384
+ } catch (error) {
3385
+ console.error("Response interceptor error:", error);
3386
+ }
3387
+ }
3388
+ return responseData;
3389
+ }
3390
+ /**
3391
+ * Make HTTP request
3392
+ * @param {string} method - HTTP method
3393
+ * @param {string} url - Request URL
3394
+ * @param {object} data - Request body data
3395
+ * @param {object} params - Query parameters
3396
+ * @param {object} options - Additional request options
3397
+ * @returns {Promise} Promise that resolves with response data
3398
+ */
3399
+ async request(method, url, data = null, params = {}, options = {}) {
3400
+ let request = {
3401
+ method: method.toUpperCase(),
3402
+ url: this.buildUrl(url) + this.buildQueryString(params),
3403
+ headers: {
3404
+ ...this.config.headers,
3405
+ ...options.headers
3406
+ },
3407
+ data,
3408
+ options: {
3409
+ timeout: this.config.timeout,
3410
+ ...options
3411
+ }
3412
+ };
3413
+ request = await this.processRequestInterceptors(request);
3414
+ if (this.config.trackDevice && this.duid) {
3415
+ if (this.config.duidTransport === "header") {
3416
+ request.headers[this.config.duidHeader] = this.duid;
3417
+ } else {
3418
+ if (request.method === "GET") {
3419
+ const url2 = new URL(request.url);
3420
+ url2.searchParams.append("duid", this.duid);
3421
+ request.url = url2.toString();
3422
+ } else if (request.data && typeof request.data === "object" && !(request.data instanceof FormData)) {
3423
+ request.data.duid = this.duid;
3424
+ }
3425
+ }
3426
+ }
3427
+ const fetchOptions = {
3428
+ method: request.method,
3429
+ headers: request.headers
3430
+ };
3431
+ const signals = [];
3432
+ if (request.options.timeout) {
3433
+ signals.push(AbortSignal.timeout(request.options.timeout));
3434
+ }
3435
+ if (request.options.signal) {
3436
+ signals.push(request.options.signal);
3437
+ }
3438
+ if (signals.length > 1) {
3439
+ fetchOptions.signal = AbortSignal.any ? AbortSignal.any(signals) : signals[0];
3440
+ } else if (signals.length === 1) {
3441
+ fetchOptions.signal = signals[0];
3442
+ }
3443
+ if (request.data && ["POST", "PUT", "PATCH"].includes(request.method)) {
3444
+ if (request.data instanceof FormData) {
3445
+ fetchOptions.body = request.data;
3446
+ delete fetchOptions.headers["Content-Type"];
3447
+ } else if (typeof request.data === "object") {
3448
+ fetchOptions.body = JSON.stringify(request.data);
3449
+ } else {
3450
+ fetchOptions.body = request.data;
3451
+ }
3452
+ }
3453
+ try {
3454
+ const response = await fetch(request.url, fetchOptions);
3455
+ const responseData = await this.processResponseInterceptors(response, request);
3456
+ return responseData;
3457
+ } catch (error) {
3458
+ if (error.name === "AbortError") {
3459
+ throw error;
3460
+ }
3461
+ const errorInfo = this.categorizeError(error);
3462
+ const errorResponse = {
3463
+ success: false,
3464
+ status: 0,
3465
+ statusText: "Network Error",
3466
+ headers: {},
3467
+ data: null,
3468
+ errors: { network: error.message },
3469
+ message: errorInfo.message,
3470
+ reason: errorInfo.reason
3471
+ };
3472
+ const mockResponse = {
3473
+ ok: false,
3474
+ status: 0,
3475
+ statusText: "Network Error",
3476
+ headers: new Headers(),
3477
+ json: async () => ({}),
3478
+ text: async () => ""
3479
+ };
3480
+ await this.processResponseInterceptors(mockResponse, request);
3481
+ return errorResponse;
3482
+ }
3483
+ }
3484
+ /**
3485
+ * GET request
3486
+ * @param {string} url - Request URL
3487
+ * @param {object} params - Query parameters
3488
+ * @param {object} options - Request options
3489
+ * @returns {Promise} Promise that resolves with response data
3490
+ */
3491
+ async GET(url, params = {}, options = {}) {
3492
+ return this.request("GET", url, null, params, options);
3493
+ }
3494
+ /**
3495
+ * POST request
3496
+ * @param {string} url - Request URL
3497
+ * @param {object} data - Request body data
3498
+ * @param {object} params - Query parameters
3499
+ * @param {object} options - Request options
3500
+ * @returns {Promise} Promise that resolves with response data
3501
+ */
3502
+ async POST(url, data = {}, params = {}, options = {}) {
3503
+ return this.request("POST", url, data, params, options);
3504
+ }
3505
+ /**
3506
+ * PUT request
3507
+ * @param {string} url - Request URL
3508
+ * @param {object} data - Request body data
3509
+ * @param {object} params - Query parameters
3510
+ * @param {object} options - Request options
3511
+ * @returns {Promise} Promise that resolves with response data
3512
+ */
3513
+ async PUT(url, data = {}, params = {}, options = {}) {
3514
+ return this.request("PUT", url, data, params, options);
3515
+ }
3516
+ /**
3517
+ * PATCH request
3518
+ * @param {string} url - Request URL
3519
+ * @param {object} data - Request body data
3520
+ * @param {object} params - Query parameters
3521
+ * @param {object} options - Request options
3522
+ * @returns {Promise} Promise that resolves with response data
3523
+ */
3524
+ async PATCH(url, data = {}, params = {}, options = {}) {
3525
+ return this.request("PATCH", url, data, params, options);
3526
+ }
3527
+ /**
3528
+ * DELETE request
3529
+ * @param {string} url - Request URL
3530
+ * @param {object} params - Query parameters
3531
+ * @param {object} options - Request options
3532
+ * @returns {Promise} Promise that resolves with response data
3533
+ */
3534
+ async DELETE(url, params = {}, options = {}) {
3535
+ return this.request("DELETE", url, null, params, options);
3536
+ }
3537
+ /**
3538
+ * Upload file with raw PUT request (compatible with legacy backend)
3539
+ * @param {string} url - Upload URL
3540
+ * @param {File} file - Single file to upload
3541
+ * @param {object} options - Request options
3542
+ * @param {function} options.onProgress - Progress callback function(event)
3543
+ * @returns {Promise} Promise that resolves with response data
3544
+ */
3545
+ async upload(url, file, options = {}) {
3546
+ return new Promise((resolve, reject) => {
3547
+ if (!(file instanceof File)) {
3548
+ reject(new Error("Only single File objects are supported for legacy backend compatibility"));
3549
+ return;
3550
+ }
3551
+ const xhr = new XMLHttpRequest();
3552
+ if (options.onProgress && typeof options.onProgress === "function") {
3553
+ xhr.upload.onprogress = options.onProgress;
3554
+ }
3555
+ xhr.onload = function() {
3556
+ if (xhr.status >= 200 && xhr.status < 300) {
3557
+ resolve({
3558
+ data: xhr.response,
3559
+ status: xhr.status,
3560
+ statusText: xhr.statusText,
3561
+ xhr
3562
+ });
3563
+ } else {
3564
+ reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
3565
+ }
3566
+ };
3567
+ xhr.onerror = function() {
3568
+ reject(new Error("Upload failed: Network error"));
3569
+ };
3570
+ xhr.ontimeout = function() {
3571
+ reject(new Error("Upload failed: Timeout"));
3572
+ };
3573
+ xhr.open("PUT", url);
3574
+ xhr.setRequestHeader("Content-Type", file.type);
3575
+ if (options.timeout) {
3576
+ xhr.timeout = options.timeout;
3577
+ }
3578
+ xhr.send(file);
3579
+ });
3580
+ }
3581
+ /**
3582
+ * Upload multiple files with multipart/form-data (for modern backends)
3583
+ * @param {string} url - Upload URL
3584
+ * @param {File|FileList|FormData} files - File(s) to upload
3585
+ * @param {object} additionalData - Additional form fields
3586
+ * @param {object} options - Request options
3587
+ * @returns {Promise} Promise that resolves with response data
3588
+ */
3589
+ async uploadMultipart(url, files, additionalData = {}, options = {}) {
3590
+ const formData = new FormData();
3591
+ if (files instanceof FileList) {
3592
+ Array.from(files).forEach((file, index) => {
3593
+ formData.append(`file_${index}`, file);
3594
+ });
3595
+ } else if (files instanceof File) {
3596
+ formData.append("file", files);
3597
+ } else if (files instanceof FormData) {
3598
+ return this.POST(url, files, {}, options);
3599
+ }
3600
+ Object.entries(additionalData).forEach(([key, value]) => {
3601
+ formData.append(key, value);
3602
+ });
3603
+ return this.POST(url, formData, {}, options);
3604
+ }
3605
+ /**
3606
+ * Set authentication token
3607
+ * @param {string} token - JWT or API token
3608
+ * @param {string} type - Token type ('Bearer', 'Token', etc.)
3609
+ */
3610
+ setAuthToken(token, type = "Bearer") {
3611
+ if (token) {
3612
+ this.config.headers["Authorization"] = `${type} ${token}`;
3613
+ } else {
3614
+ delete this.config.headers["Authorization"];
3615
+ }
3616
+ }
3617
+ /**
3618
+ * Clear authentication
3619
+ */
3620
+ clearAuth() {
3621
+ delete this.config.headers["Authorization"];
3622
+ }
3623
+ /**
3624
+ * Check if an error is retryable (network issues that might resolve)
3625
+ * @param {object} response - Response object with reason field
3626
+ * @returns {boolean} True if error can be retried
3627
+ */
3628
+ isRetryableError(response) {
3629
+ const retryableReasons = [
3630
+ "not_reachable",
3631
+ "timed_out",
3632
+ "server_error",
3633
+ "dns_error"
3634
+ ];
3635
+ return retryableReasons.includes(response.reason);
3636
+ }
3637
+ /**
3638
+ * Check if error requires authentication
3639
+ * @param {object} response - Response object with reason field
3640
+ * @returns {boolean} True if authentication is required
3641
+ */
3642
+ requiresAuth(response) {
3643
+ return response.reason === "unauthorized";
3644
+ }
3645
+ /**
3646
+ * Check if error is network-related
3647
+ * @param {object} response - Response object with reason field
3648
+ * @returns {boolean} True if it's a network error
3649
+ */
3650
+ isNetworkError(response) {
3651
+ const networkReasons = [
3652
+ "not_reachable",
3653
+ "timed_out",
3654
+ "cancelled",
3655
+ "cors_error",
3656
+ "dns_error"
3657
+ ];
3658
+ return networkReasons.includes(response.reason);
3659
+ }
3660
+ /**
3661
+ * Get user-friendly error message based on reason
3662
+ * @param {object} response - Response object with reason field
3663
+ * @returns {string} User-friendly error message
3664
+ */
3665
+ getUserMessage(response) {
3666
+ if (response.message) {
3667
+ return response.message;
3668
+ }
3669
+ const messages = {
3670
+ "not_reachable": "Unable to connect to the server. Please check your internet connection.",
3671
+ "timed_out": "The request took too long. Please try again.",
3672
+ "cancelled": "The request was cancelled.",
3673
+ "unauthorized": "Please log in to continue.",
3674
+ "forbidden": "You don't have permission to perform this action.",
3675
+ "not_found": "The requested resource was not found.",
3676
+ "validation_error": "Please check your input and try again.",
3677
+ "rate_limited": "Too many requests. Please wait a moment before trying again.",
3678
+ "server_error": "Server error. Please try again later.",
3679
+ "cors_error": "Access blocked by security policy.",
3680
+ "dns_error": "Unable to reach the server.",
3681
+ "unknown_error": "An unexpected error occurred."
3682
+ };
3683
+ return messages[response.reason] || "An error occurred. Please try again.";
3684
+ }
3685
+ }
3686
+ const rest = new Rest();
3687
+ class EventBus {
3688
+ constructor() {
3689
+ this.listeners = {};
3690
+ this.onceListeners = {};
3691
+ this.maxListeners = 100;
3692
+ this.debugMode = false;
3693
+ this.eventStats = {};
3694
+ }
3695
+ /**
3696
+ * Add event listener
3697
+ * @param {string|Array<string>} event - Event name or array of event names
3698
+ * @param {function} callback - Event callback
3699
+ * @returns {EventBus} This instance for chaining
3700
+ */
3701
+ on(event, callback) {
3702
+ if (typeof callback !== "function") {
3703
+ throw new Error("Callback must be a function");
3704
+ }
3705
+ if (Array.isArray(event)) {
3706
+ event.forEach((eventName) => this.on(eventName, callback));
3707
+ return this;
3708
+ }
3709
+ if (!this.listeners[event]) {
3710
+ this.listeners[event] = [];
3711
+ }
3712
+ if (this.listeners[event].length >= this.maxListeners) {
3713
+ console.warn(`Max listeners (${this.maxListeners}) exceeded for event: ${event}`);
3714
+ }
3715
+ this.listeners[event].push(callback);
3716
+ return this;
3717
+ }
3718
+ /**
3719
+ * Add one-time event listener
3720
+ * @param {string|Array<string>} event - Event name or array of event names
3721
+ * @param {function} callback - Event callback
3722
+ * @returns {EventBus} This instance for chaining
3723
+ */
3724
+ once(event, callback) {
3725
+ if (typeof callback !== "function") {
3726
+ throw new Error("Callback must be a function");
3727
+ }
3728
+ if (Array.isArray(event)) {
3729
+ event.forEach((eventName) => this.once(eventName, callback));
3730
+ return this;
3731
+ }
3732
+ if (!this.onceListeners[event]) {
3733
+ this.onceListeners[event] = [];
3734
+ }
3735
+ this.onceListeners[event].push(callback);
3736
+ return this;
3737
+ }
3738
+ /**
3739
+ * Remove event listener
3740
+ * @param {string|Array<string>} event - Event name or array of event names
3741
+ * @param {function} callback - Event callback to remove
3742
+ * @returns {EventBus} This instance for chaining
3743
+ */
3744
+ off(event, callback) {
3745
+ if (Array.isArray(event)) {
3746
+ event.forEach((eventName) => this.off(eventName, callback));
3747
+ return this;
3748
+ }
3749
+ if (!callback) {
3750
+ delete this.listeners[event];
3751
+ delete this.onceListeners[event];
3752
+ return this;
3753
+ }
3754
+ if (this.listeners[event]) {
3755
+ const index = this.listeners[event].indexOf(callback);
3756
+ if (index !== -1) {
3757
+ this.listeners[event].splice(index, 1);
3758
+ if (this.listeners[event].length === 0) {
3759
+ delete this.listeners[event];
3760
+ }
3761
+ }
3762
+ }
3763
+ if (this.onceListeners[event]) {
3764
+ const index = this.onceListeners[event].indexOf(callback);
3765
+ if (index !== -1) {
3766
+ this.onceListeners[event].splice(index, 1);
3767
+ if (this.onceListeners[event].length === 0) {
3768
+ delete this.onceListeners[event];
3769
+ }
3770
+ }
3771
+ }
3772
+ return this;
3773
+ }
3774
+ /**
3775
+ * Emit event to all listeners
3776
+ * @param {string} event - Event name
3777
+ * @param {*} data - Event data
3778
+ * @returns {EventBus} This instance for chaining
3779
+ */
3780
+ emit(event, data) {
3781
+ this.updateEventStats(event);
3782
+ if (this.debugMode) {
3783
+ console.log(`[EventBus] Emitting: ${event}`, data);
3784
+ }
3785
+ const listeners = [];
3786
+ if (this.listeners[event]) {
3787
+ listeners.push(...this.listeners[event]);
3788
+ }
3789
+ if (this.listeners["*"]) {
3790
+ listeners.push(...this.listeners["*"]);
3791
+ }
3792
+ if (this.onceListeners[event]) {
3793
+ listeners.push(...this.onceListeners[event]);
3794
+ delete this.onceListeners[event];
3795
+ }
3796
+ if (this.onceListeners["*"]) {
3797
+ listeners.push(...this.onceListeners["*"]);
3798
+ delete this.onceListeners["*"];
3799
+ }
3800
+ if (this.debugMode && listeners.length > 0) {
3801
+ console.log(`[EventBus] ${listeners.length} listener(s) for '${event}'`);
3802
+ }
3803
+ listeners.forEach((callback) => {
3804
+ try {
3805
+ callback(data, event);
3806
+ } catch (error) {
3807
+ console.error(`Error in event listener for '${event}':`, error);
3808
+ this.emitError(error, event, callback);
3809
+ }
3810
+ });
3811
+ return this;
3812
+ }
3813
+ /**
3814
+ * Emit event asynchronously
3815
+ * @param {string} event - Event name
3816
+ * @param {*} data - Event data
3817
+ * @returns {Promise<EventBus>} Promise that resolves with this instance
3818
+ */
3819
+ async emitAsync(event, data) {
3820
+ const listeners = [];
3821
+ if (this.listeners[event]) {
3822
+ listeners.push(...this.listeners[event]);
3823
+ }
3824
+ if (this.listeners["*"]) {
3825
+ listeners.push(...this.listeners["*"]);
3826
+ }
3827
+ if (this.onceListeners[event]) {
3828
+ listeners.push(...this.onceListeners[event]);
3829
+ delete this.onceListeners[event];
3830
+ }
3831
+ if (this.onceListeners["*"]) {
3832
+ listeners.push(...this.onceListeners["*"]);
3833
+ delete this.onceListeners["*"];
3834
+ }
3835
+ const promises = listeners.map((callback) => {
3836
+ return new Promise((resolve) => {
3837
+ try {
3838
+ const result = callback(data, event);
3839
+ resolve(result);
3840
+ } catch (error) {
3841
+ console.error(`Error in async event listener for '${event}':`, error);
3842
+ this.emitError(error, event, callback);
3843
+ resolve();
3844
+ }
3845
+ });
3846
+ });
3847
+ await Promise.all(promises);
3848
+ return this;
3849
+ }
3850
+ /**
3851
+ * Remove all listeners for all events
3852
+ * @returns {EventBus} This instance for chaining
3853
+ */
3854
+ removeAllListeners() {
3855
+ this.listeners = {};
3856
+ this.onceListeners = {};
3857
+ return this;
3858
+ }
3859
+ /**
3860
+ * Get listener count for an event
3861
+ * @param {string} event - Event name
3862
+ * @returns {number} Number of listeners
3863
+ */
3864
+ listenerCount(event) {
3865
+ const regularCount = this.listeners[event] ? this.listeners[event].length : 0;
3866
+ const onceCount = this.onceListeners[event] ? this.onceListeners[event].length : 0;
3867
+ return regularCount + onceCount;
3868
+ }
3869
+ /**
3870
+ * Get all event names that have listeners
3871
+ * @returns {Array<string>} Array of event names
3872
+ */
3873
+ eventNames() {
3874
+ const regularEvents = Object.keys(this.listeners);
3875
+ const onceEvents = Object.keys(this.onceListeners);
3876
+ return [.../* @__PURE__ */ new Set([...regularEvents, ...onceEvents])];
3877
+ }
3878
+ /**
3879
+ * Set maximum number of listeners per event
3880
+ * @param {number} max - Maximum listeners
3881
+ * @returns {EventBus} This instance for chaining
3882
+ */
3883
+ setMaxListeners(max) {
3884
+ if (typeof max !== "number" || max < 0) {
3885
+ throw new Error("Max listeners must be a non-negative number");
3886
+ }
3887
+ this.maxListeners = max;
3888
+ return this;
3889
+ }
3890
+ /**
3891
+ * Create a namespaced event bus
3892
+ * @param {string} namespace - Namespace prefix
3893
+ * @returns {object} Namespaced event bus methods
3894
+ */
3895
+ namespace(namespace) {
3896
+ const prefixEvent = (event) => `${namespace}:${event}`;
3897
+ return {
3898
+ on: (event, callback) => this.on(prefixEvent(event), callback),
3899
+ once: (event, callback) => this.once(prefixEvent(event), callback),
3900
+ off: (event, callback) => this.off(prefixEvent(event), callback),
3901
+ emit: (event, data) => this.emit(prefixEvent(event), data),
3902
+ emitAsync: (event, data) => this.emitAsync(prefixEvent(event), data)
3903
+ };
3904
+ }
3905
+ /**
3906
+ * Add middleware to intercept events
3907
+ * @param {function} middleware - Middleware function
3908
+ * @returns {EventBus} This instance for chaining
3909
+ */
3910
+ use(middleware) {
3911
+ if (typeof middleware !== "function") {
3912
+ throw new Error("Middleware must be a function");
3913
+ }
3914
+ const originalEmit = this.emit;
3915
+ this.emit = (event, data) => {
3916
+ try {
3917
+ const result = middleware(event, data);
3918
+ if (result === false) {
3919
+ return this;
3920
+ }
3921
+ const finalData = result !== void 0 ? result : data;
3922
+ return originalEmit.call(this, event, finalData);
3923
+ } catch (error) {
3924
+ console.error("Error in event middleware:", error);
3925
+ return originalEmit.call(this, event, data);
3926
+ }
3927
+ };
3928
+ return this;
3929
+ }
3930
+ /**
3931
+ * Emit error event for listener errors
3932
+ * @private
3933
+ * @param {Error} error - The error that occurred
3934
+ * @param {string} originalEvent - The event that caused the error
3935
+ * @param {function} callback - The callback that threw the error
3936
+ */
3937
+ emitError(error, originalEvent, callback) {
3938
+ if (originalEvent === "error") {
3939
+ return;
3940
+ }
3941
+ setTimeout(() => {
3942
+ this.emit("error", {
3943
+ error,
3944
+ originalEvent,
3945
+ callback: callback.toString()
3946
+ });
3947
+ }, 0);
3948
+ }
3949
+ /**
3950
+ * Create a promise that resolves when an event is emitted
3951
+ * @param {string} event - Event name
3952
+ * @param {number} timeout - Optional timeout in milliseconds
3953
+ * @returns {Promise} Promise that resolves with event data
3954
+ */
3955
+ waitFor(event, timeout = null) {
3956
+ return new Promise((resolve, reject) => {
3957
+ let timeoutId = null;
3958
+ const cleanup = () => {
3959
+ if (timeoutId) {
3960
+ clearTimeout(timeoutId);
3961
+ }
3962
+ };
3963
+ const listener = (data) => {
3964
+ cleanup();
3965
+ resolve(data);
3966
+ };
3967
+ this.once(event, listener);
3968
+ if (timeout) {
3969
+ timeoutId = setTimeout(() => {
3970
+ this.off(event, listener);
3971
+ reject(new Error(`Timeout waiting for event: ${event}`));
3972
+ }, timeout);
3973
+ }
3974
+ });
3975
+ }
3976
+ /**
3977
+ * Enable/disable debug mode for detailed event logging
3978
+ * @param {boolean} enable - Whether to enable debug mode
3979
+ * @returns {EventBus} This instance for chaining
3980
+ */
3981
+ debug(enable = true) {
3982
+ this.debugMode = enable;
3983
+ if (enable) {
3984
+ console.log("[EventBus] Debug mode enabled");
3985
+ } else {
3986
+ console.log("[EventBus] Debug mode disabled");
3987
+ }
3988
+ return this;
3989
+ }
3990
+ /**
3991
+ * Get statistics about the event bus
3992
+ * @returns {object} Statistics object
3993
+ */
3994
+ getStats() {
3995
+ const eventNames = this.eventNames();
3996
+ const stats = {
3997
+ totalEvents: eventNames.length,
3998
+ totalListeners: 0,
3999
+ events: {},
4000
+ emissions: { ...this.eventStats }
4001
+ };
4002
+ eventNames.forEach((event) => {
4003
+ const count = this.listenerCount(event);
4004
+ stats.events[event] = count;
4005
+ stats.totalListeners += count;
4006
+ });
4007
+ return stats;
4008
+ }
4009
+ /**
4010
+ * Update event emission statistics
4011
+ * @private
4012
+ * @param {string} event - Event name
4013
+ */
4014
+ updateEventStats(event) {
4015
+ if (!this.eventStats[event]) {
4016
+ this.eventStats[event] = {
4017
+ count: 0,
4018
+ firstEmission: Date.now(),
4019
+ lastEmission: null
4020
+ };
4021
+ }
4022
+ this.eventStats[event].count++;
4023
+ this.eventStats[event].lastEmission = Date.now();
4024
+ }
4025
+ /**
4026
+ * Get detailed statistics for a specific event
4027
+ * @param {string} event - Event name
4028
+ * @returns {Object} Event statistics
4029
+ */
4030
+ getEventStats(event) {
4031
+ const stats = this.eventStats[event];
4032
+ if (!stats) {
4033
+ return null;
4034
+ }
4035
+ return {
4036
+ ...stats,
4037
+ listenerCount: this.listenerCount(event),
4038
+ avgEmissionsPerMinute: this.calculateEmissionRate(stats)
4039
+ };
4040
+ }
4041
+ /**
4042
+ * Calculate emission rate for an event
4043
+ * @private
4044
+ * @param {Object} stats - Event statistics
4045
+ * @returns {number} Emissions per minute
4046
+ */
4047
+ calculateEmissionRate(stats) {
4048
+ if (!stats.firstEmission || !stats.lastEmission) {
4049
+ return 0;
4050
+ }
4051
+ const durationMs = stats.lastEmission - stats.firstEmission;
4052
+ if (durationMs === 0) {
4053
+ return 0;
4054
+ }
4055
+ const durationMinutes = durationMs / (1e3 * 60);
4056
+ return Math.round(stats.count / durationMinutes * 100) / 100;
4057
+ }
4058
+ /**
4059
+ * Reset event statistics
4060
+ * @returns {EventBus} This instance for chaining
4061
+ */
4062
+ resetStats() {
4063
+ this.eventStats = {};
4064
+ return this;
4065
+ }
4066
+ /**
4067
+ * Get a summary of the most active events
4068
+ * @param {number} limit - Number of events to return (default: 10)
4069
+ * @returns {Array} Array of event statistics sorted by emission count
4070
+ */
4071
+ getTopEvents(limit = 10) {
4072
+ return Object.entries(this.eventStats).map(([event, stats]) => ({
4073
+ event,
4074
+ count: stats.count,
4075
+ rate: this.calculateEmissionRate(stats),
4076
+ listeners: this.listenerCount(event)
4077
+ })).sort((a, b) => b.count - a.count).slice(0, limit);
4078
+ }
4079
+ /**
4080
+ * Log comprehensive debug information
4081
+ * @returns {EventBus} This instance for chaining
4082
+ */
4083
+ debugInfo() {
4084
+ console.group("[EventBus] Debug Information");
4085
+ console.log("Debug Mode:", this.debugMode);
4086
+ console.log("Max Listeners:", this.maxListeners);
4087
+ const stats = this.getStats();
4088
+ console.log("Total Events:", stats.totalEvents);
4089
+ console.log("Total Listeners:", stats.totalListeners);
4090
+ if (Object.keys(this.eventStats).length > 0) {
4091
+ console.log("Top Events:", this.getTopEvents(5));
4092
+ }
4093
+ console.groupEnd();
4094
+ return this;
4095
+ }
4096
+ }
4097
+ class WebApp {
4098
+ constructor(config = {}) {
4099
+ this.initPluginRegistry();
4100
+ this.name = config.name || "MOJO App";
4101
+ this.version = config.version || "1.0.0";
4102
+ this.debug = config.debug || false;
4103
+ this.container = config.container || "#app";
4104
+ this.layoutType = config.layout || "portal";
4105
+ this.layoutConfig = config.layoutConfig || {};
4106
+ if (config.sidebar) {
4107
+ this.layoutConfig.sidebarConfig = config.sidebar;
4108
+ }
4109
+ if (config.topbar) {
4110
+ this.layoutConfig.topbarConfig = config.topbar;
4111
+ }
4112
+ this.layout = null;
4113
+ this.layoutConfig.containerId = this.container || this.containerId || "#app";
4114
+ this.pageContainer = config.pageContainer || "#page-container";
4115
+ this.basePath = config.basePath || "";
4116
+ this.routerMode = config.routerMode || config.router?.mode || "param";
4117
+ this.basePath = config.basePath || config.router?.base || "";
4118
+ this.defaultRoute = config.defaultRoute || "home";
4119
+ this.session = config.session || {};
4120
+ this.router = null;
4121
+ this.navigation = config.navigation || {};
4122
+ this.state = {
4123
+ currentPage: null,
4124
+ previousPage: null,
4125
+ loading: false
4126
+ };
4127
+ this.events = new EventBus();
4128
+ this.rest = rest;
4129
+ if (config.api) {
4130
+ this.rest.configure(config.api);
4131
+ }
4132
+ this.router = new Router({
4133
+ mode: this.routerMode === "param" ? "params" : this.routerMode,
4134
+ basePath: this.basePath,
4135
+ defaultRoute: this.defaultRoute,
4136
+ eventEmitter: this.events
4137
+ });
4138
+ this.events.on("route:changed", async (routeInfo) => {
4139
+ const { pageName, params, query } = routeInfo;
4140
+ await this.showPage(pageName, query, params, { fromRouter: true });
4141
+ });
4142
+ if (typeof window !== "undefined") {
4143
+ window.MOJO = window.MOJO || {};
4144
+ window.MOJO.router = this.router;
4145
+ }
4146
+ this.pageCache = /* @__PURE__ */ new Map();
4147
+ this.pageClasses = /* @__PURE__ */ new Map();
4148
+ this.componentClasses = /* @__PURE__ */ new Map();
4149
+ this.modelClasses = /* @__PURE__ */ new Map();
4150
+ this.currentPage = null;
4151
+ this.isStarted = false;
4152
+ if (window.matchUUID) {
4153
+ window[window.matchUUID] = this;
4154
+ } else if (window.MOJO) {
4155
+ window.MOJO.app = this;
4156
+ } else {
4157
+ window.__app__ = this;
4158
+ }
4159
+ console.log(`WebApp initialized: ${this.name} v${this.version}`);
4160
+ }
4161
+ /**
4162
+ * Start the application
4163
+ */
4164
+ async start() {
4165
+ if (this.isStarted) {
4166
+ console.warn("WebApp already started");
4167
+ return;
4168
+ }
4169
+ try {
4170
+ console.log(`Starting ${this.name}...`);
4171
+ this.setupPageContainer();
4172
+ this.validateDefaultRoute();
4173
+ console.log("Setting up router...");
4174
+ await this.setupRouter();
4175
+ this.isStarted = true;
4176
+ this.events.emit("app:ready", { app: this });
4177
+ console.log(`✅ ${this.name} started successfully`);
4178
+ } catch (error) {
4179
+ console.error(`Failed to start ${this.name}:`, error);
4180
+ this.showError("Failed to start application");
4181
+ throw error;
4182
+ }
4183
+ }
4184
+ /**
4185
+ * Setup router with configuration
4186
+ */
4187
+ async setupRouter() {
4188
+ if (!this.router) {
4189
+ console.error("Router not initialized");
4190
+ return;
4191
+ }
4192
+ this.events.on("route:notfound", async (info) => {
4193
+ console.warn(`Route not found: ${info.path}`);
4194
+ this._show404(info.path);
4195
+ });
4196
+ this.router.start();
4197
+ console.log(`Router started in ${this.routerMode} mode`);
4198
+ }
4199
+ /**
4200
+ * Setup layout based on configuration
4201
+ */
4202
+ setupPageContainer() {
4203
+ const container = typeof this.container === "string" ? document.querySelector(this.container) : this.container;
4204
+ if (container && !container.querySelector("#page-container")) {
4205
+ container.innerHTML = '<div id="page-container"></div>';
4206
+ }
4207
+ this.pageContainer = "#page-container";
4208
+ }
4209
+ /**
4210
+ * Register a page with the app
4211
+ */
4212
+ registerPage(pageName, PageClass, options = {}) {
4213
+ if (typeof pageName !== "string" || !pageName) {
4214
+ console.error("registerPage: pageName must be a non-empty string");
4215
+ return this;
4216
+ }
4217
+ if (typeof PageClass !== "function") {
4218
+ console.error("registerPage: PageClass must be a constructor function");
4219
+ return this;
4220
+ }
4221
+ if (!options.containerId) options.containerId = this.pageContainer;
4222
+ this.pageClasses.set(pageName, {
4223
+ PageClass,
4224
+ constructorOptions: options
4225
+ });
4226
+ if (this.router) {
4227
+ let route = options.route || `/${pageName}`;
4228
+ if (!route.startsWith("/")) {
4229
+ route = `/${route}`;
4230
+ }
4231
+ options.route = route;
4232
+ this.router.addRoute(route, pageName);
4233
+ console.log(`📝 Registered route: "${route}" -> ${pageName}`);
4234
+ }
4235
+ return this;
4236
+ }
4237
+ /**
4238
+ * Get page instance (cached)
4239
+ */
4240
+ getPage(pageName) {
4241
+ return this.pageCache.get(pageName);
4242
+ }
4243
+ /**
4244
+ * Get or create page instance (with caching)
4245
+ */
4246
+ getOrCreatePage(pageName) {
4247
+ if (this.pageCache.has(pageName)) {
4248
+ return this.pageCache.get(pageName);
4249
+ }
4250
+ const pageInfo = this.pageClasses.get(pageName);
4251
+ if (!pageInfo) {
4252
+ console.error(`Page not registered: ${pageName}`);
4253
+ return null;
4254
+ }
4255
+ const { PageClass, constructorOptions } = pageInfo;
4256
+ try {
4257
+ const pageOptions = {
4258
+ pageName,
4259
+ ...constructorOptions,
4260
+ app: this
4261
+ };
4262
+ const page = new PageClass(pageOptions);
4263
+ if (constructorOptions.route) {
4264
+ page.route = constructorOptions.route;
4265
+ }
4266
+ this.pageCache.set(pageName, page);
4267
+ console.log(`Created page: ${pageName} with route: ${page.route}`);
4268
+ return page;
4269
+ } catch (error) {
4270
+ console.error(`Failed to create page ${pageName}:`, error);
4271
+ return null;
4272
+ }
4273
+ }
4274
+ /**
4275
+ * Show a page
4276
+ */
4277
+ /**
4278
+ * SIMPLIFIED UNIFIED PAGE SHOWING METHOD
4279
+ * @param {string|object} page - Page name or page instance
4280
+ * @param {object} query - URL query parameters (URL-safe)
4281
+ * @param {object} params - Any data to pass to page (can include objects)
4282
+ * @param {object} options - Options { fromRouter, replace, force }
4283
+ */
4284
+ async showPage(page, query = {}, params = {}, options = {}) {
4285
+ const { fromRouter = false, replace = false, force = false } = options;
4286
+ try {
4287
+ let pageInstance, pageName;
4288
+ if (typeof page === "string") {
4289
+ pageName = page;
4290
+ pageInstance = this.getOrCreatePage(page);
4291
+ } else if (page && typeof page === "object") {
4292
+ pageInstance = page;
4293
+ pageName = page.pageName;
4294
+ }
4295
+ const oldPage = this.currentPage;
4296
+ if (!pageInstance) {
4297
+ this._show404(pageName, params, query, fromRouter);
4298
+ return;
4299
+ }
4300
+ if (!pageInstance.canEnter()) {
4301
+ this._showDeniedPage(pageInstance, params, query, fromRouter);
4302
+ return;
4303
+ }
4304
+ if (oldPage && oldPage !== pageInstance) {
4305
+ await this._exitOldPage(oldPage);
4306
+ }
4307
+ await pageInstance.onParams(params, query);
4308
+ if (oldPage !== pageInstance) {
4309
+ await pageInstance.onEnter();
4310
+ }
4311
+ pageInstance.syncUrl();
4312
+ await pageInstance.render();
4313
+ this.currentPage = pageInstance;
4314
+ this.events.emit("page:show", {
4315
+ page: pageInstance,
4316
+ pageName: pageInstance.pageName,
4317
+ params,
4318
+ query,
4319
+ fromRouter
4320
+ });
4321
+ console.log(`✅ Showing page: ${pageInstance.pageName}`, { query, params });
4322
+ } catch (error) {
4323
+ console.error("Error in showPage:", error);
4324
+ this.showError(`Failed to load page: ${error.message}`);
4325
+ if (page !== "error") {
4326
+ await this.showPage("error", {}, { error, originalPage: page }, { fromRouter });
4327
+ }
4328
+ }
4329
+ }
4330
+ async _show404(pageName, params, query, fromRouter) {
4331
+ const notFoundPage = this.getOrCreatePage("404");
4332
+ if (!notFoundPage) return;
4333
+ if (notFoundPage.setInfo) {
4334
+ notFoundPage.setInfo(pageName);
4335
+ }
4336
+ await this._exitOldPage(this.currentPage);
4337
+ await notFoundPage.render();
4338
+ this.currentPage = notFoundPage;
4339
+ this.events.emit("page:404", {
4340
+ page: null,
4341
+ pageName,
4342
+ params,
4343
+ query,
4344
+ fromRouter
4345
+ });
4346
+ }
4347
+ async _showDeniedPage(pageInstance, params, query, fromRouter) {
4348
+ const deniedPage = this.getOrCreatePage("denied");
4349
+ if (deniedPage.setDeniedPage) {
4350
+ deniedPage.setDeniedPage(pageInstance);
4351
+ }
4352
+ await this._exitOldPage(this.currentPage);
4353
+ await deniedPage.render();
4354
+ this.currentPage = deniedPage;
4355
+ this.events.emit("page:denied", {
4356
+ page: pageInstance,
4357
+ pageName: pageInstance.pageName,
4358
+ params,
4359
+ query,
4360
+ fromRouter
4361
+ });
4362
+ }
4363
+ async _exitOldPage(oldPage) {
4364
+ if (!oldPage) return;
4365
+ try {
4366
+ await oldPage.onExit();
4367
+ await oldPage.unmount();
4368
+ this.events.emit("page:hide", { page: oldPage });
4369
+ } catch (error) {
4370
+ console.error(`Error exiting page ${oldPage.pageName}:`, error);
4371
+ }
4372
+ }
4373
+ /**
4374
+ * SIMPLIFIED NAVIGATION - delegates to router
4375
+ * @param {string} route - Route like "/users" or "/users/123"
4376
+ * @param {object} query - Query string params { filter: 'active' }
4377
+ * @param {object} options - Navigation options { replace, force }
4378
+ */
4379
+ async navigate(route, query = {}, options = {}) {
4380
+ if (!this.router) {
4381
+ console.error("Router not initialized");
4382
+ return;
4383
+ }
4384
+ console.log("🧭 WebApp.navigate:", { route, query, options });
4385
+ let fullPath = route;
4386
+ if (Object.keys(query).length > 0) {
4387
+ const queryString = new URLSearchParams(query).toString();
4388
+ fullPath += (route.includes("?") ? "&" : "?") + queryString;
4389
+ }
4390
+ return await this.router.navigate(fullPath, options);
4391
+ }
4392
+ /**
4393
+ * Navigate to the default route
4394
+ */
4395
+ async navigateToDefault(options = {}) {
4396
+ return await this.showPage(this.defaultRoute, {}, {}, options);
4397
+ }
4398
+ /**
4399
+ * Navigate back
4400
+ */
4401
+ back() {
4402
+ if (this.router) {
4403
+ this.router.back();
4404
+ } else {
4405
+ console.warn("Router not initialized");
4406
+ }
4407
+ }
4408
+ /**
4409
+ * Navigate forward
4410
+ */
4411
+ forward() {
4412
+ if (this.router) {
4413
+ this.router.forward();
4414
+ } else {
4415
+ console.warn("Router not initialized");
4416
+ }
4417
+ }
4418
+ /**
4419
+ * Get current page
4420
+ */
4421
+ getCurrentPage() {
4422
+ return this.currentPage;
4423
+ }
4424
+ /**
4425
+ * Get page container element
4426
+ */
4427
+ getPageContainer() {
4428
+ if (this.layout && this.layout.getPageContainer) {
4429
+ return this.layout.getPageContainer();
4430
+ }
4431
+ const container = typeof this.pageContainer === "string" ? document.querySelector(this.pageContainer) : this.pageContainer;
4432
+ return container;
4433
+ }
4434
+ /**
4435
+ * Show error notification
4436
+ */
4437
+ async showError(message) {
4438
+ try {
4439
+ const Dialog = (await import("./Dialog-DSlctbon.js")).default;
4440
+ await Dialog.alert(message, "Error", { size: "md", class: "text-danger" });
4441
+ } catch (e) {
4442
+ this.events.emit("notification", { message, type: "error" });
4443
+ if (typeof window !== "undefined" && window?.console) {
4444
+ console.error("[WebApp] showError fallback:", e);
4445
+ }
4446
+ if (typeof window !== "undefined") {
4447
+ alert(`Error: ${message}`);
4448
+ }
4449
+ }
4450
+ }
4451
+ /**
4452
+ * Show success notification
4453
+ */
4454
+ async showSuccess(message) {
4455
+ try {
4456
+ const Dialog = (await import("./Dialog-DSlctbon.js")).default;
4457
+ await Dialog.alert(message, "Success", { size: "md", class: "text-success" });
4458
+ } catch (e) {
4459
+ this.events.emit("notification", { message, type: "success" });
4460
+ if (typeof window !== "undefined" && window?.console) {
4461
+ console.warn("[WebApp] showSuccess fallback:", e);
4462
+ }
4463
+ if (typeof window !== "undefined") {
4464
+ alert(`Success: ${message}`);
4465
+ }
4466
+ }
4467
+ }
4468
+ /**
4469
+ * Show info notification
4470
+ */
4471
+ async showInfo(message) {
4472
+ try {
4473
+ const Dialog = (await import("./Dialog-DSlctbon.js")).default;
4474
+ await Dialog.alert(message, "Information", { size: "md", class: "text-info" });
4475
+ } catch (e) {
4476
+ this.events.emit("notification", { message, type: "info" });
4477
+ if (typeof window !== "undefined" && window?.console) {
4478
+ console.info("[WebApp] showInfo fallback:", e);
4479
+ }
4480
+ if (typeof window !== "undefined") {
4481
+ alert(`Info: ${message}`);
4482
+ }
4483
+ }
4484
+ }
4485
+ /**
4486
+ * Show warning notification
4487
+ */
4488
+ async showWarning(message) {
4489
+ try {
4490
+ const Dialog = (await import("./Dialog-DSlctbon.js")).default;
4491
+ await Dialog.alert(message, "Warning", { size: "md", class: "text-warning" });
4492
+ } catch (e) {
4493
+ this.events.emit("notification", { message, type: "warning" });
4494
+ if (typeof window !== "undefined" && window?.console) {
4495
+ console.warn("[WebApp] showWarning fallback:", e);
4496
+ }
4497
+ if (typeof window !== "undefined") {
4498
+ alert(`Warning: ${message}`);
4499
+ }
4500
+ }
4501
+ }
4502
+ /**
4503
+ * Show generic notification
4504
+ */
4505
+ showNotification(message, type = "info") {
4506
+ this.events.emit("notification", { message, type });
4507
+ }
4508
+ /**
4509
+ * Show loading indicator
4510
+ */
4511
+ async showLoading(opts = {}) {
4512
+ if (typeof opts === "string") {
4513
+ opts = { message: opts };
4514
+ }
4515
+ try {
4516
+ const Dialog = (await import("./Dialog-DSlctbon.js")).default;
4517
+ Dialog.showBusy(opts);
4518
+ } catch (e) {
4519
+ if (typeof window !== "undefined" && window?.console) {
4520
+ console.warn("[WebApp] showLoading fallback:", e, opts);
4521
+ }
4522
+ this.events.emit("notification", { message: opts.message || "Loading...", type: "info" });
4523
+ }
4524
+ }
4525
+ /**
4526
+ * Hide loading indicator
4527
+ */
4528
+ async hideLoading() {
4529
+ try {
4530
+ const Dialog = (await import("./Dialog-DSlctbon.js")).default;
4531
+ Dialog.hideBusy();
4532
+ } catch (e) {
4533
+ if (typeof window !== "undefined" && window?.console) {
4534
+ console.warn("[WebApp] hideLoading fallback:", e);
4535
+ }
4536
+ }
4537
+ }
4538
+ async showModelForm(options = {}) {
4539
+ try {
4540
+ const Dialog = (await import("./Dialog-DSlctbon.js")).default;
4541
+ return await Dialog.showModelForm(options);
4542
+ } catch (e) {
4543
+ if (typeof window !== "undefined" && window?.console) {
4544
+ console.error("[WebApp] showModelForm failed:", e);
4545
+ }
4546
+ throw e;
4547
+ }
4548
+ }
4549
+ async showForm(options = {}) {
4550
+ try {
4551
+ const Dialog = (await import("./Dialog-DSlctbon.js")).default;
4552
+ return await Dialog.showForm(options);
4553
+ } catch (e) {
4554
+ if (typeof window !== "undefined" && window?.console) {
4555
+ console.error("[WebApp] showForm failed:", e);
4556
+ }
4557
+ throw e;
4558
+ }
4559
+ }
4560
+ /**
4561
+ * Setup global error handling
4562
+ */
4563
+ setupErrorHandling() {
4564
+ window.addEventListener("error", (event) => {
4565
+ console.error("Global error:", event.error);
4566
+ if (this.debug) {
4567
+ this.showError(`Error: ${event.error?.message || "Unknown error"}`);
4568
+ }
4569
+ });
4570
+ window.addEventListener("unhandledrejection", (event) => {
4571
+ console.error("Unhandled promise rejection:", event.reason);
4572
+ if (this.debug) {
4573
+ this.showError(`Promise rejected: ${event.reason?.message || "Unknown error"}`);
4574
+ }
4575
+ });
4576
+ }
4577
+ /**
4578
+ * Escape HTML to prevent XSS
4579
+ */
4580
+ escapeHtml(unsafe) {
4581
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
4582
+ }
4583
+ /**
4584
+ * Get application state
4585
+ */
4586
+ getState(key) {
4587
+ return key ? this.state[key] : this.state;
4588
+ }
4589
+ /**
4590
+ * Set application state
4591
+ */
4592
+ setState(updates) {
4593
+ const oldState = { ...this.state };
4594
+ Object.assign(this.state, updates);
4595
+ this.events.emit("state:changed", {
4596
+ oldState,
4597
+ newState: this.state,
4598
+ updates
4599
+ });
4600
+ }
4601
+ /**
4602
+ * Register component class
4603
+ */
4604
+ registerComponent(name, ComponentClass) {
4605
+ this.componentClasses.set(name, ComponentClass);
4606
+ }
4607
+ /**
4608
+ * Get component class
4609
+ */
4610
+ getComponent(name) {
4611
+ return this.componentClasses.get(name);
4612
+ }
4613
+ /**
4614
+ * Register model class
4615
+ */
4616
+ registerModel(name, ModelClass) {
4617
+ this.modelClasses.set(name, ModelClass);
4618
+ }
4619
+ /**
4620
+ * Get model class
4621
+ */
4622
+ getModel(name) {
4623
+ return this.modelClasses.get(name);
4624
+ }
4625
+ /**
4626
+ * Setup REST configuration
4627
+ */
4628
+ setupRest() {
4629
+ this.rest = rest;
4630
+ rest.configure(this.api);
4631
+ }
4632
+ /**
4633
+ * Destroy the application
4634
+ */
4635
+ async destroy() {
4636
+ console.log("Destroying WebApp...");
4637
+ if (this.router) {
4638
+ this.router.stop();
4639
+ }
4640
+ const pages = Array.from(this.pageCache.values());
4641
+ await Promise.allSettled(
4642
+ pages.map(async (page) => {
4643
+ try {
4644
+ if (page.destroy) {
4645
+ await page.destroy();
4646
+ }
4647
+ } catch (error) {
4648
+ console.error("Error destroying page:", error);
4649
+ }
4650
+ })
4651
+ );
4652
+ if (this.layout && this.layout.destroy) {
4653
+ try {
4654
+ await this.layout.destroy();
4655
+ } catch (error) {
4656
+ console.error("Error destroying layout:", error);
4657
+ }
4658
+ }
4659
+ this.pageCache.clear();
4660
+ this.pageClasses.clear();
4661
+ this.componentClasses.clear();
4662
+ this.modelClasses.clear();
4663
+ if (typeof window !== "undefined" && window.MOJO) {
4664
+ delete window.MOJO.router;
4665
+ }
4666
+ this.isStarted = false;
4667
+ console.log(`✨ ${this.name} destroyed`);
4668
+ }
4669
+ /**
4670
+ * Build page path with params and query
4671
+ */
4672
+ buildPagePath(page, params, query) {
4673
+ let path = page.route || `/${page.pageName.toLowerCase()}`;
4674
+ Object.keys(params).forEach((key) => {
4675
+ if (typeof params[key] === "string" || typeof params[key] === "number") {
4676
+ path = path.replace(`:${key}`, params[key]);
4677
+ }
4678
+ });
4679
+ if (query && Object.keys(query).length > 0) {
4680
+ const queryString = new URLSearchParams(query).toString();
4681
+ path += (path.includes("?") ? "&" : "?") + queryString;
4682
+ }
4683
+ return path;
4684
+ }
4685
+ /**
4686
+ * Static factory method
4687
+ */
4688
+ /**
4689
+ * Validate that default route has a registered page
4690
+ */
4691
+ validateDefaultRoute() {
4692
+ if (!this.pageClasses.has(this.defaultRoute)) {
4693
+ console.warn(`⚠️ Default route '${this.defaultRoute}' is not registered!`);
4694
+ console.warn(` Please register a page: app.registerPage('${this.defaultRoute}', YourPageClass);`);
4695
+ console.warn(` Or change default route: new WebApp({ defaultRoute: 'your-page' });`);
4696
+ } else {
4697
+ console.log(`✅ Default route '${this.defaultRoute}' is registered`);
4698
+ }
4699
+ }
4700
+ /**
4701
+ * Find any registered page to use as emergency fallback
4702
+ */
4703
+ findFallbackPage() {
4704
+ const systemPages = ["404", "error", "denied"];
4705
+ for (const [pageName] of this.pageClasses.entries()) {
4706
+ if (!systemPages.includes(pageName)) {
4707
+ return pageName;
4708
+ }
4709
+ }
4710
+ return null;
4711
+ }
4712
+ static create(config = {}) {
4713
+ return new WebApp(config);
4714
+ }
4715
+ /**
4716
+ * Initialize global plugin registry for extensions
4717
+ * Allows extensions to register components that core can use optionally
4718
+ */
4719
+ initPluginRegistry() {
4720
+ if (typeof window !== "undefined") {
4721
+ if (!window.MOJO) {
4722
+ window.MOJO = {};
4723
+ }
4724
+ if (!window.MOJO.plugins) {
4725
+ window.MOJO.plugins = {};
4726
+ }
4727
+ window.MOJO.app = this;
4728
+ }
4729
+ }
4730
+ /**
4731
+ * Register a plugin component
4732
+ * @param {string} name - Plugin component name (e.g., 'ImageCropView')
4733
+ * @param {*} component - The component/class to register
4734
+ */
4735
+ static registerPlugin(name, component) {
4736
+ if (typeof window !== "undefined") {
4737
+ if (!window.MOJO) {
4738
+ window.MOJO = {};
4739
+ }
4740
+ if (!window.MOJO.plugins) {
4741
+ window.MOJO.plugins = {};
4742
+ }
4743
+ window.MOJO.plugins[name] = component;
4744
+ console.debug(`MOJO Plugin registered: ${name}`);
4745
+ }
4746
+ }
4747
+ }
4748
+ export {
4749
+ BUILD_TIME as B,
4750
+ DataWrapper as D,
4751
+ EventDelegate as E,
4752
+ Mustache as M,
4753
+ Router as R,
4754
+ View as V,
4755
+ WebApp as W,
4756
+ VERSION_INFO as a,
4757
+ VERSION as b,
4758
+ VERSION_MAJOR as c,
4759
+ dataFormatter as d,
4760
+ VERSION_MINOR as e,
4761
+ VERSION_REVISION as f,
4762
+ EventBus as g,
4763
+ MOJOUtils as h,
4764
+ EventEmitter as i,
4765
+ rest as r
4766
+ };
4767
+ //# sourceMappingURL=WebApp-B6mgbNn2.js.map