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.
- package/LICENSE +198 -0
- package/README.md +510 -0
- package/dist/admin.cjs.js +2 -0
- package/dist/admin.cjs.js.map +1 -0
- package/dist/admin.css +621 -0
- package/dist/admin.es.js +7973 -0
- package/dist/admin.es.js.map +1 -0
- package/dist/auth.cjs.js +2 -0
- package/dist/auth.cjs.js.map +1 -0
- package/dist/auth.css +804 -0
- package/dist/auth.es.js +2168 -0
- package/dist/auth.es.js.map +1 -0
- package/dist/charts.cjs.js +2 -0
- package/dist/charts.cjs.js.map +1 -0
- package/dist/charts.css +1002 -0
- package/dist/charts.es.js +16 -0
- package/dist/charts.es.js.map +1 -0
- package/dist/chunks/ContextMenu-BrHqj0fn.js +80 -0
- package/dist/chunks/ContextMenu-BrHqj0fn.js.map +1 -0
- package/dist/chunks/ContextMenu-gEcpSz56.js +2 -0
- package/dist/chunks/ContextMenu-gEcpSz56.js.map +1 -0
- package/dist/chunks/DataView-DPryYpEW.js +2 -0
- package/dist/chunks/DataView-DPryYpEW.js.map +1 -0
- package/dist/chunks/DataView-DjZQrpba.js +843 -0
- package/dist/chunks/DataView-DjZQrpba.js.map +1 -0
- package/dist/chunks/Dialog-BsRx4eg3.js +2 -0
- package/dist/chunks/Dialog-BsRx4eg3.js.map +1 -0
- package/dist/chunks/Dialog-DSlctbon.js +1377 -0
- package/dist/chunks/Dialog-DSlctbon.js.map +1 -0
- package/dist/chunks/FilePreviewView-BmFHzK5K.js +5868 -0
- package/dist/chunks/FilePreviewView-BmFHzK5K.js.map +1 -0
- package/dist/chunks/FilePreviewView-DcdRl_ta.js +2 -0
- package/dist/chunks/FilePreviewView-DcdRl_ta.js.map +1 -0
- package/dist/chunks/FormView-CmBuwKGD.js +2 -0
- package/dist/chunks/FormView-CmBuwKGD.js.map +1 -0
- package/dist/chunks/FormView-DqUBMPJ9.js +5054 -0
- package/dist/chunks/FormView-DqUBMPJ9.js.map +1 -0
- package/dist/chunks/MetricsChart-CM4CI6eA.js +2095 -0
- package/dist/chunks/MetricsChart-CM4CI6eA.js.map +1 -0
- package/dist/chunks/MetricsChart-CPidSMaN.js +2 -0
- package/dist/chunks/MetricsChart-CPidSMaN.js.map +1 -0
- package/dist/chunks/PDFViewer-BNQlnS83.js +2 -0
- package/dist/chunks/PDFViewer-BNQlnS83.js.map +1 -0
- package/dist/chunks/PDFViewer-Dyo-Oeyd.js +946 -0
- package/dist/chunks/PDFViewer-Dyo-Oeyd.js.map +1 -0
- package/dist/chunks/Page-B524zSQs.js +351 -0
- package/dist/chunks/Page-B524zSQs.js.map +1 -0
- package/dist/chunks/Page-BFgj0pAA.js +2 -0
- package/dist/chunks/Page-BFgj0pAA.js.map +1 -0
- package/dist/chunks/TokenManager-BXNva8Jk.js +287 -0
- package/dist/chunks/TokenManager-BXNva8Jk.js.map +1 -0
- package/dist/chunks/TokenManager-Bzn4guFm.js +2 -0
- package/dist/chunks/TokenManager-Bzn4guFm.js.map +1 -0
- package/dist/chunks/TopNav-D3I3_25f.js +371 -0
- package/dist/chunks/TopNav-D3I3_25f.js.map +1 -0
- package/dist/chunks/TopNav-MDjL4kV0.js +2 -0
- package/dist/chunks/TopNav-MDjL4kV0.js.map +1 -0
- package/dist/chunks/User-BalfYTEF.js +3 -0
- package/dist/chunks/User-BalfYTEF.js.map +1 -0
- package/dist/chunks/User-DwIT-CTQ.js +1937 -0
- package/dist/chunks/User-DwIT-CTQ.js.map +1 -0
- package/dist/chunks/WebApp-B6mgbNn2.js +4767 -0
- package/dist/chunks/WebApp-B6mgbNn2.js.map +1 -0
- package/dist/chunks/WebApp-DqDowtkl.js +2 -0
- package/dist/chunks/WebApp-DqDowtkl.js.map +1 -0
- package/dist/chunks/WebSocketClient-D6i85jl2.js +2 -0
- package/dist/chunks/WebSocketClient-D6i85jl2.js.map +1 -0
- package/dist/chunks/WebSocketClient-Dvl3AYx1.js +297 -0
- package/dist/chunks/WebSocketClient-Dvl3AYx1.js.map +1 -0
- package/dist/core.css +1181 -0
- package/dist/css/web-mojo.css +17 -0
- package/dist/css-manifest.json +6 -0
- package/dist/docit.cjs.js +2 -0
- package/dist/docit.cjs.js.map +1 -0
- package/dist/docit.es.js +959 -0
- package/dist/docit.es.js.map +1 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.es.js +2681 -0
- package/dist/index.es.js.map +1 -0
- package/dist/lightbox.cjs.js +2 -0
- package/dist/lightbox.cjs.js.map +1 -0
- package/dist/lightbox.css +606 -0
- package/dist/lightbox.es.js +3737 -0
- package/dist/lightbox.es.js.map +1 -0
- package/dist/loader.es.js +115 -0
- package/dist/loader.umd.js +85 -0
- package/dist/portal.css +2446 -0
- package/dist/table.css +639 -0
- package/dist/toast.css +181 -0
- 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
|