web-mojo 2.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +198 -0
  2. package/README.md +510 -0
  3. package/dist/admin.cjs.js +2 -0
  4. package/dist/admin.cjs.js.map +1 -0
  5. package/dist/admin.css +621 -0
  6. package/dist/admin.es.js +7973 -0
  7. package/dist/admin.es.js.map +1 -0
  8. package/dist/auth.cjs.js +2 -0
  9. package/dist/auth.cjs.js.map +1 -0
  10. package/dist/auth.css +804 -0
  11. package/dist/auth.es.js +2168 -0
  12. package/dist/auth.es.js.map +1 -0
  13. package/dist/charts.cjs.js +2 -0
  14. package/dist/charts.cjs.js.map +1 -0
  15. package/dist/charts.css +1002 -0
  16. package/dist/charts.es.js +16 -0
  17. package/dist/charts.es.js.map +1 -0
  18. package/dist/chunks/ContextMenu-BrHqj0fn.js +80 -0
  19. package/dist/chunks/ContextMenu-BrHqj0fn.js.map +1 -0
  20. package/dist/chunks/ContextMenu-gEcpSz56.js +2 -0
  21. package/dist/chunks/ContextMenu-gEcpSz56.js.map +1 -0
  22. package/dist/chunks/DataView-DPryYpEW.js +2 -0
  23. package/dist/chunks/DataView-DPryYpEW.js.map +1 -0
  24. package/dist/chunks/DataView-DjZQrpba.js +843 -0
  25. package/dist/chunks/DataView-DjZQrpba.js.map +1 -0
  26. package/dist/chunks/Dialog-BsRx4eg3.js +2 -0
  27. package/dist/chunks/Dialog-BsRx4eg3.js.map +1 -0
  28. package/dist/chunks/Dialog-DSlctbon.js +1377 -0
  29. package/dist/chunks/Dialog-DSlctbon.js.map +1 -0
  30. package/dist/chunks/FilePreviewView-BmFHzK5K.js +5868 -0
  31. package/dist/chunks/FilePreviewView-BmFHzK5K.js.map +1 -0
  32. package/dist/chunks/FilePreviewView-DcdRl_ta.js +2 -0
  33. package/dist/chunks/FilePreviewView-DcdRl_ta.js.map +1 -0
  34. package/dist/chunks/FormView-CmBuwKGD.js +2 -0
  35. package/dist/chunks/FormView-CmBuwKGD.js.map +1 -0
  36. package/dist/chunks/FormView-DqUBMPJ9.js +5054 -0
  37. package/dist/chunks/FormView-DqUBMPJ9.js.map +1 -0
  38. package/dist/chunks/MetricsChart-CM4CI6eA.js +2095 -0
  39. package/dist/chunks/MetricsChart-CM4CI6eA.js.map +1 -0
  40. package/dist/chunks/MetricsChart-CPidSMaN.js +2 -0
  41. package/dist/chunks/MetricsChart-CPidSMaN.js.map +1 -0
  42. package/dist/chunks/PDFViewer-BNQlnS83.js +2 -0
  43. package/dist/chunks/PDFViewer-BNQlnS83.js.map +1 -0
  44. package/dist/chunks/PDFViewer-Dyo-Oeyd.js +946 -0
  45. package/dist/chunks/PDFViewer-Dyo-Oeyd.js.map +1 -0
  46. package/dist/chunks/Page-B524zSQs.js +351 -0
  47. package/dist/chunks/Page-B524zSQs.js.map +1 -0
  48. package/dist/chunks/Page-BFgj0pAA.js +2 -0
  49. package/dist/chunks/Page-BFgj0pAA.js.map +1 -0
  50. package/dist/chunks/TokenManager-BXNva8Jk.js +287 -0
  51. package/dist/chunks/TokenManager-BXNva8Jk.js.map +1 -0
  52. package/dist/chunks/TokenManager-Bzn4guFm.js +2 -0
  53. package/dist/chunks/TokenManager-Bzn4guFm.js.map +1 -0
  54. package/dist/chunks/TopNav-D3I3_25f.js +371 -0
  55. package/dist/chunks/TopNav-D3I3_25f.js.map +1 -0
  56. package/dist/chunks/TopNav-MDjL4kV0.js +2 -0
  57. package/dist/chunks/TopNav-MDjL4kV0.js.map +1 -0
  58. package/dist/chunks/User-BalfYTEF.js +3 -0
  59. package/dist/chunks/User-BalfYTEF.js.map +1 -0
  60. package/dist/chunks/User-DwIT-CTQ.js +1937 -0
  61. package/dist/chunks/User-DwIT-CTQ.js.map +1 -0
  62. package/dist/chunks/WebApp-B6mgbNn2.js +4767 -0
  63. package/dist/chunks/WebApp-B6mgbNn2.js.map +1 -0
  64. package/dist/chunks/WebApp-DqDowtkl.js +2 -0
  65. package/dist/chunks/WebApp-DqDowtkl.js.map +1 -0
  66. package/dist/chunks/WebSocketClient-D6i85jl2.js +2 -0
  67. package/dist/chunks/WebSocketClient-D6i85jl2.js.map +1 -0
  68. package/dist/chunks/WebSocketClient-Dvl3AYx1.js +297 -0
  69. package/dist/chunks/WebSocketClient-Dvl3AYx1.js.map +1 -0
  70. package/dist/core.css +1181 -0
  71. package/dist/css/web-mojo.css +17 -0
  72. package/dist/css-manifest.json +6 -0
  73. package/dist/docit.cjs.js +2 -0
  74. package/dist/docit.cjs.js.map +1 -0
  75. package/dist/docit.es.js +959 -0
  76. package/dist/docit.es.js.map +1 -0
  77. package/dist/index.cjs.js +2 -0
  78. package/dist/index.cjs.js.map +1 -0
  79. package/dist/index.es.js +2681 -0
  80. package/dist/index.es.js.map +1 -0
  81. package/dist/lightbox.cjs.js +2 -0
  82. package/dist/lightbox.cjs.js.map +1 -0
  83. package/dist/lightbox.css +606 -0
  84. package/dist/lightbox.es.js +3737 -0
  85. package/dist/lightbox.es.js.map +1 -0
  86. package/dist/loader.es.js +115 -0
  87. package/dist/loader.umd.js +85 -0
  88. package/dist/portal.css +2446 -0
  89. package/dist/table.css +639 -0
  90. package/dist/toast.css +181 -0
  91. package/package.json +179 -0
@@ -0,0 +1,843 @@
1
+ import { V as View, d as dataFormatter } from "./WebApp-B6mgbNn2.js";
2
+ class DataView extends View {
3
+ constructor(options = {}) {
4
+ const {
5
+ data,
6
+ model,
7
+ fields,
8
+ columns,
9
+ responsive,
10
+ showEmptyValues,
11
+ emptyValueText,
12
+ ...viewOptions
13
+ } = options;
14
+ super({
15
+ tagName: "div",
16
+ className: "data-view",
17
+ ...viewOptions
18
+ });
19
+ this.data = data || {};
20
+ this.fields = fields || [];
21
+ this.model = model || null;
22
+ if (this.model) {
23
+ this.data = this.model;
24
+ }
25
+ this.dataViewOptions = {
26
+ columns: columns || 2,
27
+ responsive: responsive !== false,
28
+ // Default to true
29
+ showEmptyValues: showEmptyValues || false,
30
+ emptyValueText: emptyValueText || "—",
31
+ rowClass: "row g-3",
32
+ itemClass: "data-view-item",
33
+ labelClass: "data-view-label fw-semibold text-muted small text-uppercase",
34
+ valueClass: "data-view-value"
35
+ };
36
+ }
37
+ /**
38
+ * Lifecycle hook - prepare data and fields before rendering
39
+ */
40
+ async onBeforeRender() {
41
+ if (this.fields.length === 0 && this.getData()) {
42
+ this.generateFieldsFromData();
43
+ }
44
+ }
45
+ /**
46
+ * Override renderTemplate to generate HTML directly
47
+ * @returns {string} Complete HTML string
48
+ */
49
+ async renderTemplate() {
50
+ const items = this.buildItemsHTML();
51
+ return `
52
+ <div class="${this.dataViewOptions.rowClass}">
53
+ ${items}
54
+ </div>
55
+ `;
56
+ }
57
+ /**
58
+ * Auto-generate field definitions from data object with intelligent type inference
59
+ */
60
+ generateFieldsFromData() {
61
+ const dataObj = this.getData();
62
+ if (dataObj && typeof dataObj === "object") {
63
+ this.fields = Object.keys(dataObj).map((key) => {
64
+ const value = dataObj[key];
65
+ const fieldType = this.inferFieldType(value, key);
66
+ const formatter = this.inferFormatter(value, key, fieldType);
67
+ return {
68
+ name: key,
69
+ label: this.formatLabel(key),
70
+ type: fieldType,
71
+ format: formatter
72
+ };
73
+ });
74
+ }
75
+ }
76
+ /**
77
+ * Format field name into a readable label
78
+ * @param {string} name - Field name
79
+ * @returns {string} Formatted label
80
+ */
81
+ formatLabel(name) {
82
+ return name.replace(/([A-Z])/g, " $1").replace(/[_-]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()).trim();
83
+ }
84
+ /**
85
+ * Infer field type from value and key with improved intelligence
86
+ * @param {*} value - Field value
87
+ * @param {string} key - Field key
88
+ * @returns {string} Field type
89
+ */
90
+ inferFieldType(value, key = "") {
91
+ if (value === null || value === void 0) return "text";
92
+ const keyLower = key.toLowerCase();
93
+ const type = typeof value;
94
+ if (keyLower.includes("date") || keyLower.includes("time") || keyLower.includes("created") || keyLower.includes("updated") || keyLower.includes("modified") || keyLower.includes("last_login") || keyLower.includes("expires") || keyLower.includes("last_activity")) {
95
+ return "datetime";
96
+ }
97
+ if (keyLower.includes("email") || keyLower.includes("mail")) {
98
+ return "email";
99
+ }
100
+ if (keyLower.includes("url") || keyLower.includes("link") || keyLower.includes("website") || keyLower.includes("homepage")) {
101
+ return "url";
102
+ }
103
+ if (keyLower.includes("phone") || keyLower.includes("tel") || keyLower.includes("mobile") || keyLower.includes("cell")) {
104
+ return "phone";
105
+ }
106
+ if (keyLower.includes("price") || keyLower.includes("cost") || keyLower.includes("amount") || keyLower.includes("fee") || keyLower.includes("salary") || keyLower.includes("revenue")) {
107
+ return "currency";
108
+ }
109
+ if (keyLower.includes("size") || keyLower.includes("bytes")) {
110
+ return "filesize";
111
+ }
112
+ if (keyLower.includes("percent") || keyLower.includes("rate") || keyLower.includes("ratio") && type === "number") {
113
+ return "percent";
114
+ }
115
+ if (type === "boolean") return "boolean";
116
+ if (type === "number") return "number";
117
+ if (type === "object") {
118
+ if (Array.isArray(value)) return "array";
119
+ if (value && value.renditions) return "file";
120
+ if (this.shouldUseDataView(value, keyLower)) {
121
+ return "dataview";
122
+ }
123
+ return "object";
124
+ }
125
+ if (type === "string") {
126
+ if (value.includes("@") && value.includes(".")) return "email";
127
+ if (value.match(/^\d{4}-\d{2}-\d{2}/)) return "date";
128
+ if (value.match(/^https?:\/\//)) return "url";
129
+ if (value.match(/^\+?[\d\s\-\(\)]+$/)) return "phone";
130
+ }
131
+ return "text";
132
+ }
133
+ /**
134
+ * Infer appropriate formatter based on type and context
135
+ * @param {*} value - Field value
136
+ * @param {string} key - Field key
137
+ * @param {string} fieldType - Inferred field type
138
+ * @returns {string|null} Formatter pipe string
139
+ */
140
+ inferFormatter(value, key, fieldType) {
141
+ const keyLower = key.toLowerCase();
142
+ const formatters = [];
143
+ switch (fieldType) {
144
+ case "datetime":
145
+ if (keyLower.includes("time") && !keyLower.includes("date")) {
146
+ formatters.push("time");
147
+ } else if (keyLower.includes("relative") || keyLower.includes("ago") || keyLower.includes("last_")) {
148
+ formatters.push("relative");
149
+ } else if (keyLower.includes("created") || keyLower.includes("updated") || keyLower.includes("modified")) {
150
+ formatters.push('date("MMM D, YYYY")');
151
+ } else {
152
+ formatters.push('date("MMMM D, YYYY")');
153
+ }
154
+ break;
155
+ case "date":
156
+ if (keyLower.includes("birth") || keyLower.includes("dob")) {
157
+ formatters.push('date("MMMM D, YYYY")');
158
+ } else {
159
+ formatters.push('date("MMM D, YYYY")');
160
+ }
161
+ break;
162
+ case "email":
163
+ break;
164
+ case "url":
165
+ break;
166
+ case "phone":
167
+ formatters.push("phone");
168
+ break;
169
+ case "currency":
170
+ formatters.push("currency");
171
+ if (keyLower.includes("eur") || keyLower.includes("euro")) {
172
+ formatters[formatters.length - 1] = 'currency("EUR")';
173
+ } else if (keyLower.includes("gbp") || keyLower.includes("pound")) {
174
+ formatters[formatters.length - 1] = 'currency("GBP")';
175
+ }
176
+ break;
177
+ case "filesize":
178
+ formatters.push("filesize");
179
+ break;
180
+ case "percent":
181
+ formatters.push("percent");
182
+ break;
183
+ case "number":
184
+ if (typeof value === "number") {
185
+ if (keyLower.includes("count") || keyLower.includes("total") || keyLower.includes("followers") || keyLower.includes("views")) {
186
+ if (value >= 1e3) {
187
+ formatters.push("compact");
188
+ } else {
189
+ formatters.push("number");
190
+ }
191
+ } else if (keyLower.includes("score") || keyLower.includes("rating")) {
192
+ formatters.push("number");
193
+ if (value % 1 !== 0) {
194
+ formatters[formatters.length - 1] = "number(1)";
195
+ }
196
+ } else if (keyLower.includes("version") || keyLower.includes("id")) {
197
+ return null;
198
+ } else {
199
+ formatters.push("number");
200
+ }
201
+ }
202
+ break;
203
+ case "boolean":
204
+ break;
205
+ case "text":
206
+ if (typeof value === "string") {
207
+ if (keyLower.includes("description") || keyLower.includes("content") || keyLower.includes("body")) {
208
+ if (value.length > 200) {
209
+ formatters.push("truncate(200)");
210
+ } else if (value.length > 100) {
211
+ formatters.push("truncate(100)");
212
+ }
213
+ } else if (keyLower.includes("summary") || keyLower.includes("excerpt")) {
214
+ if (value.length > 150) {
215
+ formatters.push("truncate(150)");
216
+ }
217
+ } else if (keyLower.includes("name") || keyLower.includes("title") || keyLower.includes("label")) {
218
+ formatters.push("capitalize");
219
+ if (value.length > 50) {
220
+ formatters.unshift("truncate(50)");
221
+ }
222
+ } else if (keyLower.includes("slug") || keyLower.includes("handle") || keyLower.includes("username")) {
223
+ formatters.push("slug");
224
+ } else if (keyLower.includes("code") || keyLower.includes("token") || keyLower.includes("key")) {
225
+ if (value.length > 20) {
226
+ formatters.push("mask");
227
+ }
228
+ } else {
229
+ if (value.length > 100) {
230
+ formatters.push("truncate(100)");
231
+ }
232
+ }
233
+ }
234
+ break;
235
+ case "array":
236
+ case "object":
237
+ break;
238
+ case "dataview":
239
+ break;
240
+ default:
241
+ if (typeof value === "string" && value.length > 100) {
242
+ formatters.push("truncate(100)");
243
+ }
244
+ break;
245
+ }
246
+ return formatters.length > 0 ? formatters.join("|") : null;
247
+ }
248
+ /**
249
+ * Determine if an object should be displayed as nested DataView vs JSON
250
+ * @param {object} value - Object value to check
251
+ * @param {string} keyLower - Lowercase field key
252
+ * @returns {boolean} True if should use DataView
253
+ */
254
+ shouldUseDataView(value, keyLower) {
255
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
256
+ return false;
257
+ }
258
+ const dataViewPatterns = [
259
+ "permissions",
260
+ "perms",
261
+ "access",
262
+ "rights",
263
+ "settings",
264
+ "config",
265
+ "configuration",
266
+ "options",
267
+ "profile",
268
+ "info",
269
+ "details",
270
+ "data",
271
+ "metadata",
272
+ "meta",
273
+ "attributes",
274
+ "props",
275
+ "preferences",
276
+ "prefs",
277
+ "user_data",
278
+ "contact",
279
+ "address",
280
+ "location",
281
+ "stats",
282
+ "statistics",
283
+ "metrics",
284
+ "counts"
285
+ ];
286
+ if (window.utils && window.utils.isObject(value) && value.id) {
287
+ return true;
288
+ }
289
+ const matchesPattern = dataViewPatterns.some((pattern) => keyLower.includes(pattern));
290
+ if (matchesPattern) {
291
+ const keys = Object.keys(value);
292
+ if (keys.length >= 2 && keys.length <= 20) {
293
+ const hasComplexNesting = keys.some(
294
+ (k) => typeof value[k] === "object" && value[k] !== null && !Array.isArray(value[k]) && Object.keys(value[k]).length > 3
295
+ );
296
+ if (!hasComplexNesting) {
297
+ return true;
298
+ }
299
+ }
300
+ }
301
+ return false;
302
+ }
303
+ /**
304
+ * Get data object (handles both raw objects and Models)
305
+ * @returns {object} Data object
306
+ */
307
+ getData() {
308
+ if (this.model && this.model.attributes) {
309
+ return { ...this.model.attributes };
310
+ }
311
+ return this.data || {};
312
+ }
313
+ /**
314
+ * Get field value with formatting support
315
+ * @param {object} field - Field definition
316
+ * @returns {*} Field value (formatted if specified)
317
+ */
318
+ getFieldValue(field) {
319
+ let value;
320
+ if (this.model && typeof this.model.get === "function") {
321
+ value = this.model.get(field.name);
322
+ } else {
323
+ value = this.getData()[field.name];
324
+ }
325
+ if (field.format) {
326
+ value = dataFormatter.pipe(value, field.format);
327
+ }
328
+ if (value === null || value === void 0 || value === "") {
329
+ return this.dataViewOptions.showEmptyValues ? this.dataViewOptions.emptyValueText : null;
330
+ }
331
+ if (field.template) {
332
+ const modelData = this.model ? this.model : this.data;
333
+ return this.renderTemplateString(field.template, modelData);
334
+ }
335
+ return value;
336
+ }
337
+ /**
338
+ * Render a template string with data from a model or object.
339
+ * Replaces {{key}} and {{nested.key}} placeholders.
340
+ * @param {string} templateString - The template string.
341
+ * @param {object} data - The data object or model.
342
+ * @returns {string} The rendered string.
343
+ */
344
+ renderTemplateString(templateString, data) {
345
+ if (!templateString || !data) {
346
+ return "";
347
+ }
348
+ return templateString.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
349
+ const trimmedKey = key.trim();
350
+ let value;
351
+ const parts = trimmedKey.split("|");
352
+ const dataKey = parts[0];
353
+ const formatters = parts.slice(1).join("|");
354
+ if (this.model && typeof this.model.get === "function") {
355
+ value = this.model.get(dataKey);
356
+ } else {
357
+ value = dataKey.split(".").reduce((o, i) => o ? o[i] : void 0, data);
358
+ }
359
+ if (formatters) {
360
+ value = dataFormatter.pipe(value, formatters);
361
+ }
362
+ return value !== void 0 && value !== null ? value : "";
363
+ });
364
+ }
365
+ /**
366
+ * Generate column classes based on configuration
367
+ * @param {object} field - Field definition
368
+ * @returns {string} CSS classes
369
+ */
370
+ getColumnClasses(field) {
371
+ if (field.type === "array" || field.type === "object" || field.type === "dataview") {
372
+ return "col-12";
373
+ }
374
+ const colSize = field.columns || field.colSize || field.cols || Math.floor(12 / this.dataViewOptions.columns);
375
+ if (this.dataViewOptions.responsive) {
376
+ return `col-12 col-md-${colSize}`;
377
+ }
378
+ return `col-${colSize}`;
379
+ }
380
+ /**
381
+ * Build HTML for all data items
382
+ * @returns {string} Items HTML
383
+ */
384
+ buildItemsHTML() {
385
+ return this.fields.map((field) => this.buildItemHTML(field)).filter(Boolean).join("");
386
+ }
387
+ /**
388
+ * Build HTML for a single data item
389
+ * @param {object} field - Field definition
390
+ * @returns {string} Item HTML
391
+ */
392
+ buildItemHTML(field) {
393
+ const value = this.getFieldValue(field);
394
+ if (value === null && !this.dataViewOptions.showEmptyValues) {
395
+ return "";
396
+ }
397
+ const label = field.label || this.formatLabel(field.name);
398
+ const colClasses = this.getColumnClasses(field);
399
+ return `
400
+ <div class="${colClasses}">
401
+ <div class="${this.dataViewOptions.itemClass}" data-field="${field.name}">
402
+ ${this.buildLabelHTML(label, field)}
403
+ ${this.buildValueHTML(value, field)}
404
+ </div>
405
+ </div>
406
+ `;
407
+ }
408
+ /**
409
+ * Build label HTML
410
+ * @param {string} label - Label text
411
+ * @param {object} field - Field definition
412
+ * @returns {string} Label HTML
413
+ */
414
+ buildLabelHTML(label, field) {
415
+ const labelClass = field.labelClass || this.dataViewOptions.labelClass;
416
+ return `<div class="${labelClass}">${this.escapeHtml(label)}:</div>`;
417
+ }
418
+ /**
419
+ * Build value HTML with type-specific formatting
420
+ * @param {*} value - Field value
421
+ * @param {object} field - Field definition
422
+ * @returns {string} Value HTML
423
+ */
424
+ buildValueHTML(value, field) {
425
+ const valueClass = field.valueClass || this.dataViewOptions.valueClass;
426
+ const displayValue = this.formatDisplayValue(value, field);
427
+ return `<div class="${valueClass}">${displayValue}</div>`;
428
+ }
429
+ /**
430
+ * Format value for display with enhanced type handling
431
+ * @param {*} value - Formatted value from DataFormatter (or raw if no format)
432
+ * @param {object} field - Field definition
433
+ * @returns {string} Formatted display value with HTML markup
434
+ */
435
+ formatDisplayValue(value, field) {
436
+ if (value === null || value === void 0) {
437
+ return this.dataViewOptions.emptyValueText;
438
+ }
439
+ if (field.template) {
440
+ return String(value);
441
+ }
442
+ if (field.format) {
443
+ const htmlSafeFormatters = [
444
+ "badge",
445
+ "email",
446
+ "url",
447
+ "icon",
448
+ "status",
449
+ "image",
450
+ "avatar",
451
+ "phone",
452
+ "highlight",
453
+ "pre"
454
+ ];
455
+ const pipes = dataFormatter.parsePipeString(field.format);
456
+ const lastFormatter = pipes.length > 0 ? pipes[pipes.length - 1].name.toLowerCase() : null;
457
+ if (lastFormatter && htmlSafeFormatters.includes(lastFormatter)) {
458
+ return String(value);
459
+ }
460
+ return this.escapeHtml(String(value));
461
+ }
462
+ const rawValue = this.getData()[field.name];
463
+ switch (field.type) {
464
+ case "boolean":
465
+ return rawValue ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>';
466
+ case "email":
467
+ const emailStr = String(value);
468
+ return `<a href="mailto:${this.escapeHtml(emailStr)}" class="text-decoration-none">${this.escapeHtml(emailStr)}</a>`;
469
+ case "url":
470
+ const urlStr = String(value);
471
+ return `<a href="${this.escapeHtml(urlStr)}" target="_blank" rel="noopener" class="text-decoration-none">${this.escapeHtml(urlStr)} <i class="bi bi-box-arrow-up-right"></i></a>`;
472
+ case "array":
473
+ case "object":
474
+ return this.formatAsJson(rawValue);
475
+ case "dataview":
476
+ return this.formatAsDataView(rawValue, field);
477
+ case "phone":
478
+ const phoneStr = String(value);
479
+ const telHref = phoneStr.replace(/[^\d\+]/g, "");
480
+ return `<a href="tel:${telHref}" class="text-decoration-none">${this.escapeHtml(phoneStr)}</a>`;
481
+ default:
482
+ return this.escapeHtml(String(value));
483
+ }
484
+ }
485
+ /**
486
+ * Format object/array values as styled JSON
487
+ * @param {*} value - Object or array value
488
+ * @returns {string} Formatted JSON HTML
489
+ */
490
+ formatAsJson(value) {
491
+ try {
492
+ const jsonString = JSON.stringify(value, null, 2);
493
+ const escapedJson = this.escapeHtml(jsonString);
494
+ const lines = jsonString.split("\n").length;
495
+ const isLarge = lines > 10 || jsonString.length > 500;
496
+ const uniqueId = `json-${Math.random().toString(36).substr(2, 9)}`;
497
+ if (isLarge) {
498
+ const preview = JSON.stringify(value).substring(0, 100) + (JSON.stringify(value).length > 100 ? "..." : "");
499
+ const escapedPreview = this.escapeHtml(preview);
500
+ return `
501
+ <div class="json-container">
502
+ <div class="d-flex align-items-center justify-content-between mb-1">
503
+ <small class="text-muted">${Array.isArray(value) ? "Array" : "Object"} (${lines} lines)</small>
504
+ <div class="btn-group btn-group-sm" role="group">
505
+ <button type="button" class="btn btn-outline-secondary btn-sm json-toggle" data-bs-toggle="collapse" data-bs-target="#${uniqueId}" aria-expanded="false">
506
+ <i class="bi bi-eye"></i> Show
507
+ </button>
508
+ <button type="button" class="btn btn-outline-secondary btn-sm json-copy" data-json='${this.escapeHtml(jsonString)}' title="Copy JSON">
509
+ <i class="bi bi-clipboard"></i>
510
+ </button>
511
+ </div>
512
+ </div>
513
+ <div class="json-preview bg-light p-2 rounded small border" style="font-family: 'Courier New', monospace;">
514
+ <code class="text-muted">${escapedPreview}</code>
515
+ </div>
516
+ <div class="collapse mt-2" id="${uniqueId}">
517
+ <pre class="json-display p-3 rounded small mb-0" style="max-height: 400px; overflow-y: auto; white-space: pre-wrap; font-family: 'Courier New', monospace;"><code>${this.syntaxHighlightJson(escapedJson)}</code></pre>
518
+ </div>
519
+ </div>
520
+ `;
521
+ } else {
522
+ return `
523
+ <div class="json-container">
524
+ <div class="d-flex align-items-center justify-content-between mb-1">
525
+ <small class="text-muted">${Array.isArray(value) ? "Array" : "Object"}</small>
526
+ <button type="button" class="btn btn-outline-secondary btn-sm json-copy" data-json='${this.escapeHtml(jsonString)}' title="Copy JSON">
527
+ <i class="bi bi-clipboard"></i>
528
+ </button>
529
+ </div>
530
+ <pre class="json-display bg-light p-2 rounded small mb-0 border" style="white-space: pre-wrap; font-family: 'Courier New', monospace;"><code>${this.syntaxHighlightJson(escapedJson)}</code></pre>
531
+ </div>
532
+ `;
533
+ }
534
+ } catch (error) {
535
+ return `<span class="text-muted fst-italic">[Object: ${typeof value}] - Cannot display as JSON</span>`;
536
+ }
537
+ }
538
+ /**
539
+ * Apply basic syntax highlighting to JSON
540
+ * @param {string} json - Escaped JSON string
541
+ * @returns {string} JSON with basic syntax highlighting
542
+ */
543
+ syntaxHighlightJson(json) {
544
+ return json.replace(/("([^"\\]|\\.)*")\s*:/g, '<span style="color: #0969da;">$1</span>:').replace(/:\s*("([^"\\]|\\.)*")/g, ': <span style="color: #0a3069;">$1</span>').replace(/:\s*(true|false)/g, ': <span style="color: #8250df;">$1</span>').replace(/:\s*(null)/g, ': <span style="color: #656d76;">$1</span>').replace(/:\s*(-?\d+\.?\d*)/g, ': <span style="color: #0550ae;">$1</span>');
545
+ }
546
+ /**
547
+ * Bind events including JSON interaction handlers
548
+ */
549
+ bindEvents() {
550
+ super.bindEvents();
551
+ if (!this.element) return;
552
+ this.element.addEventListener("click", (e) => {
553
+ const fieldElement = e.target.closest("[data-field]");
554
+ if (fieldElement) {
555
+ const fieldName = fieldElement.dataset.field;
556
+ const field = this.fields.find((f) => f.name === fieldName);
557
+ this.emit("field:click", { field, fieldName, element: fieldElement, event: e });
558
+ }
559
+ if (e.target.closest(".json-copy")) {
560
+ e.preventDefault();
561
+ e.stopPropagation();
562
+ this.handleJsonCopy(e.target.closest(".json-copy"));
563
+ }
564
+ if (e.target.closest(".json-toggle")) {
565
+ this.handleJsonToggle(e.target.closest(".json-toggle"));
566
+ }
567
+ });
568
+ }
569
+ /**
570
+ * Handle copying JSON to clipboard
571
+ * @param {HTMLElement} button - Copy button element
572
+ */
573
+ handleJsonCopy(button) {
574
+ const jsonData = button.getAttribute("data-json");
575
+ if (!jsonData) return;
576
+ try {
577
+ if (navigator.clipboard && window.isSecureContext) {
578
+ navigator.clipboard.writeText(jsonData).then(() => {
579
+ this.showCopyFeedback(button);
580
+ });
581
+ } else {
582
+ const textarea = document.createElement("textarea");
583
+ textarea.value = jsonData;
584
+ document.body.appendChild(textarea);
585
+ textarea.select();
586
+ document.execCommand("copy");
587
+ document.body.removeChild(textarea);
588
+ this.showCopyFeedback(button);
589
+ }
590
+ } catch (error) {
591
+ console.warn("Failed to copy JSON:", error);
592
+ }
593
+ }
594
+ /**
595
+ * Handle JSON toggle button state
596
+ * @param {HTMLElement} button - Toggle button element
597
+ */
598
+ handleJsonToggle(button) {
599
+ const icon = button.querySelector("i");
600
+ const isExpanded = button.getAttribute("aria-expanded") === "true";
601
+ setTimeout(() => {
602
+ if (isExpanded) {
603
+ icon.className = "bi bi-eye-slash";
604
+ button.innerHTML = '<i class="bi bi-eye-slash"></i> Hide';
605
+ } else {
606
+ icon.className = "bi bi-eye";
607
+ button.innerHTML = '<i class="bi bi-eye"></i> Show';
608
+ }
609
+ }, 10);
610
+ }
611
+ /**
612
+ * Show visual feedback for successful copy
613
+ * @param {HTMLElement} button - Copy button element
614
+ */
615
+ showCopyFeedback(button) {
616
+ const originalIcon = button.querySelector("i").className;
617
+ const icon = button.querySelector("i");
618
+ icon.className = "bi bi-check text-success";
619
+ button.classList.add("btn-success");
620
+ button.classList.remove("btn-outline-secondary");
621
+ setTimeout(() => {
622
+ icon.className = originalIcon;
623
+ button.classList.remove("btn-success");
624
+ button.classList.add("btn-outline-secondary");
625
+ }, 1e3);
626
+ }
627
+ /**
628
+ * Format complex objects as nested DataView
629
+ * @param {object} value - Object value to display as DataView
630
+ * @param {object} field - Field definition
631
+ * @returns {string} Formatted DataView HTML
632
+ */
633
+ formatAsDataView(value, field) {
634
+ if (!value || typeof value !== "object") {
635
+ return `<span class="text-muted fst-italic">No data available</span>`;
636
+ }
637
+ try {
638
+ const nestedView = new this.constructor({
639
+ data: value,
640
+ columns: field.dataViewColumns || 2,
641
+ showEmptyValues: field.showEmptyValues ?? true,
642
+ emptyValueText: field.emptyValueText || "Not set",
643
+ // Pass any other dataView-specific options from field config
644
+ ...field.dataViewOptions || {}
645
+ });
646
+ nestedView.onInit();
647
+ nestedView.generateFieldsFromData();
648
+ const nestedHtml = nestedView.buildItemsHTML();
649
+ return `
650
+ <div class="nested-dataview border rounded p-3 bg-light">
651
+ <div class="${nestedView.dataViewOptions.rowClass}">
652
+ ${nestedHtml}
653
+ </div>
654
+ </div>
655
+ `;
656
+ } catch (error) {
657
+ console.error("Error creating nested DataView:", error);
658
+ return `<span class="text-danger">Error displaying nested data</span>`;
659
+ }
660
+ }
661
+ /**
662
+ * Update the data and re-render
663
+ * @param {object} newData - New data object
664
+ * @returns {Promise<DataView>} Promise resolving to this instance
665
+ */
666
+ async updateData(newData) {
667
+ this.data = newData;
668
+ if (this.model && typeof this.model.set === "function") {
669
+ this.model.set(newData);
670
+ }
671
+ if (this.fields.length > 0 && !this.options.fields) {
672
+ this.fields = [];
673
+ }
674
+ await this.render();
675
+ this.emit("data:updated", { data: newData });
676
+ return this;
677
+ }
678
+ /**
679
+ * Update field configuration and re-render
680
+ * @param {array} newFields - New field configuration
681
+ * @returns {Promise<DataView>} Promise resolving to this instance
682
+ */
683
+ async updateFields(newFields) {
684
+ this.fields = newFields;
685
+ await this.render();
686
+ this.emit("fields:updated", { fields: newFields });
687
+ return this;
688
+ }
689
+ /**
690
+ * Update configuration and re-render
691
+ * @param {object} newOptions - New configuration options
692
+ * @returns {Promise<DataView>} Promise resolving to this instance
693
+ */
694
+ async updateConfig(newOptions) {
695
+ this.dataViewOptions = { ...this.dataViewOptions, ...newOptions };
696
+ await this.render();
697
+ this.emit("config:updated", { options: this.dataViewOptions });
698
+ return this;
699
+ }
700
+ /**
701
+ * Refresh data from model if available
702
+ * @returns {Promise<DataView>} Promise resolving to this instance
703
+ */
704
+ async refresh() {
705
+ if (this.model && typeof this.model.fetch === "function") {
706
+ try {
707
+ await this.model.fetch();
708
+ this.emit("data:refreshed", { model: this.model });
709
+ } catch (error) {
710
+ this.emit("error", { error, message: "Failed to refresh data" });
711
+ throw error;
712
+ }
713
+ }
714
+ return this;
715
+ }
716
+ /**
717
+ * Get current data
718
+ * @returns {object} Current data object
719
+ */
720
+ getCurrentData() {
721
+ return this.getData();
722
+ }
723
+ /**
724
+ * Get field definition by name
725
+ * @param {string} name - Field name
726
+ * @returns {object|null} Field definition
727
+ */
728
+ getField(name) {
729
+ return this.fields.find((field) => field.name === name) || null;
730
+ }
731
+ /**
732
+ * Set custom format for a specific field
733
+ * @param {string} fieldName - Name of the field
734
+ * @param {string} format - Pipe format string (e.g., "currency|uppercase")
735
+ * @returns {DataView} This instance for chaining
736
+ */
737
+ setFieldFormat(fieldName, format) {
738
+ const field = this.getField(fieldName);
739
+ if (field) {
740
+ field.format = format;
741
+ } else {
742
+ this.fields.push({
743
+ name: fieldName,
744
+ label: this.formatLabel(fieldName),
745
+ type: this.inferFieldType(this.getData()[fieldName], fieldName),
746
+ format
747
+ });
748
+ }
749
+ return this;
750
+ }
751
+ /**
752
+ * Add additional formatter to existing field format pipe chain
753
+ * @param {string} fieldName - Name of the field
754
+ * @param {string} formatter - Formatter to add (e.g., "uppercase", "truncate(50)")
755
+ * @returns {DataView} This instance for chaining
756
+ */
757
+ addFormatPipe(fieldName, formatter) {
758
+ const field = this.getField(fieldName);
759
+ if (field) {
760
+ if (field.format) {
761
+ field.format += `|${formatter}`;
762
+ } else {
763
+ field.format = formatter;
764
+ }
765
+ }
766
+ return this;
767
+ }
768
+ /**
769
+ * Clear custom format for a field (revert to auto-inferred format)
770
+ * @param {string} fieldName - Name of the field
771
+ * @returns {DataView} This instance for chaining
772
+ */
773
+ clearFieldFormat(fieldName) {
774
+ const field = this.getField(fieldName);
775
+ if (field) {
776
+ const data = this.getData();
777
+ field.format = this.inferFormatter(data[fieldName], fieldName, field.type);
778
+ }
779
+ return this;
780
+ }
781
+ /**
782
+ * Get formatted value for a specific field without rendering
783
+ * @param {string} fieldName - Name of the field
784
+ * @param {*} value - Optional value to format (uses current data if not provided)
785
+ * @returns {*} Formatted value
786
+ */
787
+ getFormattedValue(fieldName, value = null) {
788
+ const field = this.getField(fieldName);
789
+ if (!field) return null;
790
+ const targetValue = value !== null ? value : this.getData()[fieldName];
791
+ if (field.format && targetValue != null) {
792
+ return dataFormatter.pipe(targetValue, field.format);
793
+ }
794
+ return targetValue;
795
+ }
796
+ /**
797
+ * Set multiple field formats at once
798
+ * @param {object} formats - Object mapping field names to format strings
799
+ * @returns {DataView} This instance for chaining
800
+ */
801
+ setFieldFormats(formats) {
802
+ Object.entries(formats).forEach(([fieldName, format]) => {
803
+ this.setFieldFormat(fieldName, format);
804
+ });
805
+ return this;
806
+ }
807
+ /**
808
+ * Get all current field formats as an object
809
+ * @returns {object} Object mapping field names to their current formats
810
+ */
811
+ getFieldFormats() {
812
+ const formats = {};
813
+ this.fields.forEach((field) => {
814
+ if (field.format) {
815
+ formats[field.name] = field.format;
816
+ }
817
+ });
818
+ return formats;
819
+ }
820
+ /**
821
+ * Set up model event listeners if model is provided
822
+ */
823
+ onInit() {
824
+ super.onInit();
825
+ if (this.model && typeof this.model.on === "function") {
826
+ this.model.on("change", () => {
827
+ this.render();
828
+ });
829
+ }
830
+ }
831
+ /**
832
+ * Static factory method
833
+ * @param {object} options - DataView options
834
+ * @returns {DataView} New DataView instance
835
+ */
836
+ static create(options = {}) {
837
+ return new DataView(options);
838
+ }
839
+ }
840
+ export {
841
+ DataView as default
842
+ };
843
+ //# sourceMappingURL=DataView-DjZQrpba.js.map