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.
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.cjs.js.map +1 -1
- package/dist/admin.es.js +1 -10105
- package/dist/admin.es.js.map +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -588
- package/dist/auth.es.js.map +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -571
- package/dist/charts.es.js.map +1 -1
- package/dist/chunks/ChatView-D4A9rIX3.js +2 -0
- package/dist/chunks/ChatView-D4A9rIX3.js.map +1 -0
- package/dist/chunks/ChatView-nxaq8aIo.js +2 -0
- package/dist/chunks/ChatView-nxaq8aIo.js.map +1 -0
- package/dist/chunks/Collection-1sPoIFvQ.js +2 -0
- package/dist/chunks/{Collection-DaiL0uGl.js.map → Collection-1sPoIFvQ.js.map} +1 -1
- package/dist/chunks/{Collection-CxbNKOas.js → Collection-DSBRXpwK.js} +2 -2
- package/dist/chunks/{Collection-CxbNKOas.js.map → Collection-DSBRXpwK.js.map} +1 -1
- package/dist/chunks/{ContextMenu-ClwHEbbD.js → ContextMenu-BWy7WqF4.js} +2 -2
- package/dist/chunks/{ContextMenu-ClwHEbbD.js.map → ContextMenu-BWy7WqF4.js.map} +1 -1
- package/dist/chunks/ContextMenu-BvniQz-N.js +3 -0
- package/dist/chunks/{ContextMenu-sgvgSACY.js.map → ContextMenu-BvniQz-N.js.map} +1 -1
- package/dist/chunks/DataView--nUWtq6r.js +2 -0
- package/dist/chunks/{DataView-Dzo0jbs2.js.map → DataView--nUWtq6r.js.map} +1 -1
- package/dist/chunks/{DataView-1xh3GFeC.js → DataView-CK3Z0TJH.js} +2 -2
- package/dist/chunks/{DataView-1xh3GFeC.js.map → DataView-CK3Z0TJH.js.map} +1 -1
- package/dist/chunks/Dialog-BcgSR01Z.js +2 -0
- package/dist/chunks/{Dialog-DOGDalUq.js.map → Dialog-BcgSR01Z.js.map} +1 -1
- package/dist/chunks/{Dialog-CQlTDhZS.js → Dialog-DwCTFV6O.js} +2 -2
- package/dist/chunks/{Dialog-CQlTDhZS.js.map → Dialog-DwCTFV6O.js.map} +1 -1
- package/dist/chunks/FormPlugins-DvQ-G5J5.js +2 -0
- package/dist/chunks/{FormPlugins-DY6e88YT.js.map → FormPlugins-DvQ-G5J5.js.map} +1 -1
- package/dist/chunks/{FormView-DaKA4Sys.js → FormView-CRmEReTC.js} +3 -3
- package/dist/chunks/{FormView-DaKA4Sys.js.map → FormView-CRmEReTC.js.map} +1 -1
- package/dist/chunks/FormView-OLA7t-yv.js +3 -0
- package/dist/chunks/{FormView-Dz3mYasQ.js.map → FormView-OLA7t-yv.js.map} +1 -1
- package/dist/chunks/ListView-6JQ6tRXs.js +2 -0
- package/dist/chunks/{ListView-X5w5jf51.js.map → ListView-6JQ6tRXs.js.map} +1 -1
- package/dist/chunks/{ListView-CDzKIpd8.js → ListView-DVStKiMi.js} +2 -2
- package/dist/chunks/{ListView-CDzKIpd8.js.map → ListView-DVStKiMi.js.map} +1 -1
- package/dist/chunks/{MetricsCountryMapView-Dx2cw7ya.js → MetricsCountryMapView-CnAEbUw_.js} +2 -2
- package/dist/chunks/{MetricsCountryMapView-Dx2cw7ya.js.map → MetricsCountryMapView-CnAEbUw_.js.map} +1 -1
- package/dist/chunks/MetricsCountryMapView-J067qrrt.js +2 -0
- package/dist/chunks/{MetricsCountryMapView-B2xz6zUw.js.map → MetricsCountryMapView-J067qrrt.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-CBuso0OE.js → MetricsMiniChartWidget-BeD1slGs.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-CBuso0OE.js.map → MetricsMiniChartWidget-BeD1slGs.js.map} +1 -1
- package/dist/chunks/MetricsMiniChartWidget-x2gFjHOU.js +2 -0
- package/dist/chunks/{MetricsMiniChartWidget-DvKd7Qrk.js.map → MetricsMiniChartWidget-x2gFjHOU.js.map} +1 -1
- package/dist/chunks/PDFViewer-CsyKn-gh.js +2 -0
- package/dist/chunks/{PDFViewer-EJ9cOfPF.js.map → PDFViewer-CsyKn-gh.js.map} +1 -1
- package/dist/chunks/{PDFViewer-ofMGdSaj.js → PDFViewer-DSa4BZCm.js} +2 -2
- package/dist/chunks/{PDFViewer-ofMGdSaj.js.map → PDFViewer-DSa4BZCm.js.map} +1 -1
- package/dist/chunks/Rest-DHbszkuP.js +2 -0
- package/dist/chunks/Rest-DHbszkuP.js.map +1 -0
- package/dist/chunks/Rest-Ds9e8tN8.js +2 -0
- package/dist/chunks/Rest-Ds9e8tN8.js.map +1 -0
- package/dist/chunks/TokenManager-D6SjKgPZ.js +2 -0
- package/dist/chunks/{TokenManager-DoN9e6q6.js.map → TokenManager-D6SjKgPZ.js.map} +1 -1
- package/dist/chunks/{TokenManager-Gqvj7SDX.js → TokenManager-REbha1Le.js} +2 -2
- package/dist/chunks/{TokenManager-Gqvj7SDX.js.map → TokenManager-REbha1Le.js.map} +1 -1
- package/dist/chunks/WebApp-CULZpO_0.js +2 -0
- package/dist/chunks/{WebApp-6qvqmOts.js.map → WebApp-CULZpO_0.js.map} +1 -1
- package/dist/chunks/{WebApp-_dgpwtFw.js → WebApp-DovLtA60.js} +2 -2
- package/dist/chunks/{WebApp-_dgpwtFw.js.map → WebApp-DovLtA60.js.map} +1 -1
- package/dist/chunks/WebSocketClient-B-wc3mez.js +2 -0
- package/dist/chunks/{WebSocketClient-DG2olXpH.js.map → WebSocketClient-B-wc3mez.js.map} +1 -1
- package/dist/chunks/{WebSocketClient-MFkFlSue.js → WebSocketClient-BdZ9QYll.js} +2 -2
- package/dist/chunks/{WebSocketClient-MFkFlSue.js.map → WebSocketClient-BdZ9QYll.js.map} +1 -1
- package/dist/chunks/version-C3dnl1bg.js +2 -0
- package/dist/chunks/version-C3dnl1bg.js.map +1 -0
- package/dist/chunks/{version-BVADfTA5.js → version-ioN546cp.js} +2 -2
- package/dist/chunks/{version-BVADfTA5.js.map → version-ioN546cp.js.map} +1 -1
- package/dist/css/web-mojo.css +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.es.js +1 -957
- package/dist/docit.es.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -3252
- package/dist/index.es.js.map +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -3737
- package/dist/lightbox.es.js.map +1 -1
- package/dist/loader.umd.js +2 -2
- package/dist/map.cjs.js +1 -1
- package/dist/map.es.js +1 -1032
- package/dist/map.es.js.map +1 -1
- package/dist/mojo-auth.es.js +338 -0
- package/dist/mojo-auth.umd.js +1 -0
- package/dist/timeline.cjs.js +1 -1
- package/dist/timeline.es.js +1 -224
- package/dist/timeline.es.js.map +1 -1
- package/dist/web-mojo.lite.iife.js +14 -3
- package/dist/web-mojo.lite.iife.js.map +1 -1
- package/dist/web-mojo.lite.iife.min.js +6 -6
- package/dist/web-mojo.lite.iife.min.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunks/ChatView-9k6xBWXk.js +0 -7632
- package/dist/chunks/ChatView-9k6xBWXk.js.map +0 -1
- package/dist/chunks/ChatView-CdtuCDYm.js +0 -2
- package/dist/chunks/ChatView-CdtuCDYm.js.map +0 -1
- package/dist/chunks/Collection-DaiL0uGl.js +0 -1014
- package/dist/chunks/ContextMenu-sgvgSACY.js +0 -1535
- package/dist/chunks/DataView-Dzo0jbs2.js +0 -862
- package/dist/chunks/Dialog-DOGDalUq.js +0 -1579
- package/dist/chunks/FormPlugins-DY6e88YT.js +0 -124
- package/dist/chunks/FormView-Dz3mYasQ.js +0 -8636
- package/dist/chunks/ListView-X5w5jf51.js +0 -495
- package/dist/chunks/MetricsCountryMapView-B2xz6zUw.js +0 -1054
- package/dist/chunks/MetricsMiniChartWidget-DvKd7Qrk.js +0 -3283
- package/dist/chunks/PDFViewer-EJ9cOfPF.js +0 -946
- package/dist/chunks/Rest-CgSjfMaU.js +0 -2
- package/dist/chunks/Rest-CgSjfMaU.js.map +0 -1
- package/dist/chunks/Rest-W-sPfGh9.js +0 -4375
- package/dist/chunks/Rest-W-sPfGh9.js.map +0 -1
- package/dist/chunks/TokenManager-DoN9e6q6.js +0 -1423
- package/dist/chunks/WebApp-6qvqmOts.js +0 -1386
- package/dist/chunks/WebSocketClient-DG2olXpH.js +0 -209
- package/dist/chunks/version-OyPGnx30.js +0 -38
- 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
|
-
"<": "<",
|
|
14
|
-
">": ">",
|
|
15
|
-
'"': """,
|
|
16
|
-
"'": "'"
|
|
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
|
-
"&": "&",
|
|
1824
|
-
"<": "<",
|
|
1825
|
-
">": ">",
|
|
1826
|
-
'"': """,
|
|
1827
|
-
"'": "'",
|
|
1828
|
-
"/": "/",
|
|
1829
|
-
"`": "`",
|
|
1830
|
-
"=": "="
|
|
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
|
-
"&": "&",
|
|
2192
|
-
"<": "<",
|
|
2193
|
-
">": ">",
|
|
2194
|
-
'"': """,
|
|
2195
|
-
"'": "'",
|
|
2196
|
-
"/": "/",
|
|
2197
|
-
"`": "`",
|
|
2198
|
-
"=": "="
|
|
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
|