web-mojo 2.2.57 → 2.2.59

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