@liedekef/ftable 1.0.0
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 +21 -0
- package/README.md +77 -0
- package/ftable.esm.js +4749 -0
- package/ftable.js +4749 -0
- package/ftable.min.js +54 -0
- package/ftable.umd.js +4750 -0
- package/localization/ftable.ar.js +35 -0
- package/localization/ftable.bd.js +33 -0
- package/localization/ftable.ca.js +33 -0
- package/localization/ftable.cz.js +33 -0
- package/localization/ftable.de.js +33 -0
- package/localization/ftable.el.js +33 -0
- package/localization/ftable.es.js +33 -0
- package/localization/ftable.fa.js +33 -0
- package/localization/ftable.fr.js +33 -0
- package/localization/ftable.hr.js +33 -0
- package/localization/ftable.hu.js +33 -0
- package/localization/ftable.id.js +34 -0
- package/localization/ftable.it.js +33 -0
- package/localization/ftable.kr.js +33 -0
- package/localization/ftable.lt.js +33 -0
- package/localization/ftable.nl.js +33 -0
- package/localization/ftable.no.js +33 -0
- package/localization/ftable.pl.js +33 -0
- package/localization/ftable.pt-BR.js +33 -0
- package/localization/ftable.pt-PT.js +32 -0
- package/localization/ftable.ro.js +33 -0
- package/localization/ftable.ru.js +34 -0
- package/localization/ftable.se.js +33 -0
- package/localization/ftable.ta.js +33 -0
- package/localization/ftable.tr.js +33 -0
- package/localization/ftable.ua.js +33 -0
- package/localization/ftable.vi.js +33 -0
- package/localization/ftable.zh-CN.js +33 -0
- package/package.json +44 -0
- package/themes/basic/clone.png +0 -0
- package/themes/basic/close.png +0 -0
- package/themes/basic/column-asc.png +0 -0
- package/themes/basic/column-desc.png +0 -0
- package/themes/basic/column-sortable.png +0 -0
- package/themes/basic/delete.png +0 -0
- package/themes/basic/edit.png +0 -0
- package/themes/basic/ftable_basic.css +358 -0
- package/themes/basic/ftable_basic.less +89 -0
- package/themes/basic/ftable_basic.min.css +1 -0
- package/themes/ftable_theme_base.less +594 -0
- package/themes/lightcolor/add.png +0 -0
- package/themes/lightcolor/bg-thead.png +0 -0
- package/themes/lightcolor/blue/ftable.css +597 -0
- package/themes/lightcolor/blue/ftable.less +88 -0
- package/themes/lightcolor/blue/ftable.min.css +1 -0
- package/themes/lightcolor/blue/loading.gif +0 -0
- package/themes/lightcolor/clone.png +0 -0
- package/themes/lightcolor/close.png +0 -0
- package/themes/lightcolor/column-asc.png +0 -0
- package/themes/lightcolor/column-desc.png +0 -0
- package/themes/lightcolor/column-sortable.png +0 -0
- package/themes/lightcolor/delete.png +0 -0
- package/themes/lightcolor/edit.png +0 -0
- package/themes/lightcolor/ftable_lightcolor_base.less +337 -0
- package/themes/lightcolor/gray/ftable.css +597 -0
- package/themes/lightcolor/gray/ftable.less +88 -0
- package/themes/lightcolor/gray/ftable.min.css +1 -0
- package/themes/lightcolor/gray/loading.gif +0 -0
- package/themes/lightcolor/green/ftable.css +597 -0
- package/themes/lightcolor/green/ftable.less +88 -0
- package/themes/lightcolor/green/ftable.min.css +1 -0
- package/themes/lightcolor/green/loading.gif +0 -0
- package/themes/lightcolor/orange/ftable.css +597 -0
- package/themes/lightcolor/orange/ftable.less +88 -0
- package/themes/lightcolor/orange/ftable.min.css +1 -0
- package/themes/lightcolor/orange/loading.gif +0 -0
- package/themes/lightcolor/red/ftable.css +597 -0
- package/themes/lightcolor/red/ftable.less +88 -0
- package/themes/lightcolor/red/ftable.min.css +1 -0
- package/themes/lightcolor/red/loading.gif +0 -0
- package/themes/metro/add.png +0 -0
- package/themes/metro/blue/ftable.css +574 -0
- package/themes/metro/blue/ftable.less +9 -0
- package/themes/metro/blue/ftable.min.css +1 -0
- package/themes/metro/blue/loading.gif +0 -0
- package/themes/metro/brown/ftable.css +574 -0
- package/themes/metro/brown/ftable.less +9 -0
- package/themes/metro/brown/ftable.min.css +1 -0
- package/themes/metro/brown/loading.gif +0 -0
- package/themes/metro/clone.png +0 -0
- package/themes/metro/close.png +0 -0
- package/themes/metro/column-asc.png +0 -0
- package/themes/metro/column-desc.png +0 -0
- package/themes/metro/column-sortable.png +0 -0
- package/themes/metro/crimson/ftable.css +574 -0
- package/themes/metro/crimson/ftable.less +9 -0
- package/themes/metro/crimson/ftable.min.css +1 -0
- package/themes/metro/crimson/loading.gif +0 -0
- package/themes/metro/darkgray/ftable.css +574 -0
- package/themes/metro/darkgray/ftable.less +9 -0
- package/themes/metro/darkgray/ftable.min.css +1 -0
- package/themes/metro/darkgray/loading.gif +0 -0
- package/themes/metro/darkorange/ftable.css +574 -0
- package/themes/metro/darkorange/ftable.less +9 -0
- package/themes/metro/darkorange/ftable.min.css +1 -0
- package/themes/metro/darkorange/loading.gif +0 -0
- package/themes/metro/delete.png +0 -0
- package/themes/metro/edit.png +0 -0
- package/themes/metro/ftable_metro_base.less +450 -0
- package/themes/metro/green/ftable.css +574 -0
- package/themes/metro/green/ftable.less +9 -0
- package/themes/metro/green/ftable.min.css +1 -0
- package/themes/metro/green/loading.gif +0 -0
- package/themes/metro/lightgray/ftable.css +574 -0
- package/themes/metro/lightgray/ftable.less +9 -0
- package/themes/metro/lightgray/ftable.min.css +1 -0
- package/themes/metro/lightgray/loading.gif +0 -0
- package/themes/metro/pink/ftable.css +574 -0
- package/themes/metro/pink/ftable.less +9 -0
- package/themes/metro/pink/ftable.min.css +1 -0
- package/themes/metro/pink/loading.gif +0 -0
- package/themes/metro/purple/ftable.css +574 -0
- package/themes/metro/purple/ftable.less +9 -0
- package/themes/metro/purple/ftable.min.css +1 -0
- package/themes/metro/purple/loading.gif +0 -0
- package/themes/metro/red/ftable.css +574 -0
- package/themes/metro/red/ftable.less +9 -0
- package/themes/metro/red/ftable.min.css +1 -0
- package/themes/metro/red/loading.gif +0 -0
package/ftable.esm.js
ADDED
|
@@ -0,0 +1,4749 @@
|
|
|
1
|
+
// Modern fTable - Vanilla JS Refactor
|
|
2
|
+
|
|
3
|
+
const JTABLE_DEFAULT_MESSAGES = {
|
|
4
|
+
serverCommunicationError: 'An error occurred while communicating to the server.',
|
|
5
|
+
loadingMessage: 'Loading records...',
|
|
6
|
+
noDataAvailable: 'No data available!',
|
|
7
|
+
addNewRecord: 'Add new record',
|
|
8
|
+
editRecord: 'Edit record',
|
|
9
|
+
areYouSure: 'Are you sure?',
|
|
10
|
+
deleteConfirmation: 'This record will be deleted. Are you sure?',
|
|
11
|
+
save: 'Save',
|
|
12
|
+
saving: 'Saving',
|
|
13
|
+
cancel: 'Cancel',
|
|
14
|
+
deleteText: 'Delete',
|
|
15
|
+
deleting: 'Deleting',
|
|
16
|
+
error: 'Error',
|
|
17
|
+
close: 'Close',
|
|
18
|
+
cannotLoadOptionsFor: 'Cannot load options for field {0}!',
|
|
19
|
+
pagingInfo: 'Showing {0}-{1} of {2}',
|
|
20
|
+
canNotDeletedRecords: 'Can not delete {0} of {1} records!',
|
|
21
|
+
deleteProgress: 'Deleting {0} of {1} records, processing...',
|
|
22
|
+
pageSizeChangeLabel: 'Row count',
|
|
23
|
+
gotoPageLabel: 'Go to page',
|
|
24
|
+
sortingInfoPrefix: 'Sorting applied: ',
|
|
25
|
+
sortingInfoSuffix: '', // optional
|
|
26
|
+
ascending: 'Ascending',
|
|
27
|
+
descending: 'Descending',
|
|
28
|
+
sortingInfoNone: 'No sorting applied',
|
|
29
|
+
resetSorting: 'Reset sorting',
|
|
30
|
+
csvExport: 'CSV',
|
|
31
|
+
printTable: '🖨️ Print',
|
|
32
|
+
cloneRecord: 'Clone Record',
|
|
33
|
+
resetTable: 'Reset table',
|
|
34
|
+
resetTableConfirm: 'This will reset all columns, pagesize, sorting to their defaults. Do you want to continue?',
|
|
35
|
+
resetSearch: 'Reset'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class FTableOptionsCache {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.cache = new Map();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
generateKey(url, params) {
|
|
44
|
+
const sortedParams = Object.keys(params || {})
|
|
45
|
+
.sort()
|
|
46
|
+
.map(key => `${key}=${params[key]}`)
|
|
47
|
+
.join('&');
|
|
48
|
+
return `${url}?${sortedParams}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get(url, params) {
|
|
52
|
+
const key = this.generateKey(url, params);
|
|
53
|
+
return this.cache.get(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
set(url, params, data) {
|
|
57
|
+
const key = this.generateKey(url, params);
|
|
58
|
+
this.cache.set(key, data);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clear(url = null, params = null) {
|
|
62
|
+
if (url) {
|
|
63
|
+
if (params) {
|
|
64
|
+
const key = this.generateKey(url, params);
|
|
65
|
+
this.cache.delete(key);
|
|
66
|
+
} else {
|
|
67
|
+
// Clear all entries that start with this URL
|
|
68
|
+
const urlPrefix = url.split('?')[0];
|
|
69
|
+
for (const [key] of this.cache) {
|
|
70
|
+
if (key.startsWith(urlPrefix)) {
|
|
71
|
+
this.cache.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
this.cache.clear();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
size() {
|
|
81
|
+
return this.cache.size;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class FTableEventEmitter {
|
|
86
|
+
constructor() {
|
|
87
|
+
this.events = {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
on(event, callback) {
|
|
91
|
+
if (!this.events[event]) {
|
|
92
|
+
this.events[event] = [];
|
|
93
|
+
}
|
|
94
|
+
this.events[event].push(callback);
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
once(event, callback) {
|
|
99
|
+
// Create a wrapper that removes itself after first call
|
|
100
|
+
const wrapper = (...args) => {
|
|
101
|
+
this.off(event, wrapper);
|
|
102
|
+
callback.apply(this, args);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Store reference to wrapper so it can be removed
|
|
106
|
+
wrapper.fn = callback; // for off() to match
|
|
107
|
+
|
|
108
|
+
this.on(event, wrapper);
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
emit(event, data = {}) {
|
|
113
|
+
if (this.events[event]) {
|
|
114
|
+
this.events[event].forEach(callback => callback(data));
|
|
115
|
+
}
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
off(event, callback) {
|
|
120
|
+
if (this.events[event]) {
|
|
121
|
+
this.events[event] = this.events[event].filter(cb => cb !== callback);
|
|
122
|
+
}
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class FTableLogger {
|
|
128
|
+
static LOG_LEVELS = {
|
|
129
|
+
DEBUG: 0,
|
|
130
|
+
INFO: 1,
|
|
131
|
+
WARN: 2,
|
|
132
|
+
ERROR: 3,
|
|
133
|
+
NONE: 4
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
constructor(level = FTableLogger.LOG_LEVELS.WARN) {
|
|
137
|
+
this.level = level;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
log(level, message) {
|
|
141
|
+
if (!window.console || level < this.level) return;
|
|
142
|
+
|
|
143
|
+
const levelName = Object.keys(FTableLogger.LOG_LEVELS)
|
|
144
|
+
.find(key => FTableLogger.LOG_LEVELS[key] === level);
|
|
145
|
+
console.log(`fTable ${levelName}: ${message}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
debug(message) { this.log(FTableLogger.LOG_LEVELS.DEBUG, message); }
|
|
149
|
+
info(message) { this.log(FTableLogger.LOG_LEVELS.INFO, message); }
|
|
150
|
+
warn(message) { this.log(FTableLogger.LOG_LEVELS.WARN, message); }
|
|
151
|
+
error(message) { this.log(FTableLogger.LOG_LEVELS.ERROR, message); }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class FTableDOMHelper {
|
|
155
|
+
static create(tag, options = {}) {
|
|
156
|
+
const element = document.createElement(tag);
|
|
157
|
+
|
|
158
|
+
if (options.className) {
|
|
159
|
+
element.className = options.className;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (options.attributes) {
|
|
163
|
+
Object.entries(options.attributes).forEach(([key, value]) => {
|
|
164
|
+
element.setAttribute(key, value);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (options.text) {
|
|
169
|
+
element.textContent = options.text;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (options.html) {
|
|
173
|
+
element.innerHTML = options.html;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (options.parent) {
|
|
177
|
+
options.parent.appendChild(element);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return element;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static find(selector, parent = document) {
|
|
184
|
+
return parent.querySelector(selector);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static findAll(selector, parent = document) {
|
|
188
|
+
return Array.from(parent.querySelectorAll(selector));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static addClass(element, className) {
|
|
192
|
+
element.classList.add(...className.split(' '));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static removeClass(element, className) {
|
|
196
|
+
element.classList.remove(...className.split(' '));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static toggleClass(element, className) {
|
|
200
|
+
element.classList.toggle(className);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static show(element) {
|
|
204
|
+
element.style.display = '';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static hide(element) {
|
|
208
|
+
element.style.display = 'none';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
static escapeHtml(text) {
|
|
212
|
+
if (!text) return text;
|
|
213
|
+
const map = {
|
|
214
|
+
'&': '&',
|
|
215
|
+
'<': '<',
|
|
216
|
+
'>': '>',
|
|
217
|
+
'"': '"',
|
|
218
|
+
"'": '''
|
|
219
|
+
};
|
|
220
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
class FTableHttpClient {
|
|
225
|
+
static async request(url, options = {}) {
|
|
226
|
+
const defaults = {
|
|
227
|
+
method: 'GET',
|
|
228
|
+
headers: {}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const config = { ...defaults, ...options };
|
|
232
|
+
|
|
233
|
+
// Merge headers properly
|
|
234
|
+
if (options.headers) {
|
|
235
|
+
config.headers = { ...defaults.headers, ...options.headers };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const response = await fetch(url, config);
|
|
240
|
+
|
|
241
|
+
if (response.status === 401) {
|
|
242
|
+
throw new Error('Unauthorized');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Try to parse as JSON, fallback to text
|
|
250
|
+
const contentType = response.headers.get('content-type');
|
|
251
|
+
if (contentType && contentType.includes('application/json')) {
|
|
252
|
+
return await response.json();
|
|
253
|
+
} else {
|
|
254
|
+
const text = await response.text();
|
|
255
|
+
try {
|
|
256
|
+
return JSON.parse(text);
|
|
257
|
+
} catch {
|
|
258
|
+
return { Result: 'OK', Message: text };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
static async get(url, params = {}) {
|
|
267
|
+
// Handle relative URLs by using the current page's base
|
|
268
|
+
let fullUrl = new URL(url, window.location.href);
|
|
269
|
+
|
|
270
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
271
|
+
if (value === null || value === undefined) {
|
|
272
|
+
return; // Skip null or undefined values
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (Array.isArray(value)) {
|
|
276
|
+
// Append each item in the array with the same key
|
|
277
|
+
// This generates query strings like `key=val1&key=val2&key=val3`
|
|
278
|
+
value.forEach(item => {
|
|
279
|
+
if (item !== null && item !== undefined) { // Ensure array items are also not null/undefined
|
|
280
|
+
fullUrl.searchParams.append(key, item);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
// Append single values normally
|
|
285
|
+
fullUrl.searchParams.append(key, value);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return this.request(fullUrl.toString(), {
|
|
291
|
+
method: 'GET',
|
|
292
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded'}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
static async post(url, data = {}) {
|
|
297
|
+
// Handle relative URLs
|
|
298
|
+
let fullUrl = new URL(url, window.location.href);
|
|
299
|
+
|
|
300
|
+
let formData = new FormData();
|
|
301
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
302
|
+
if (value === null || value === undefined) {
|
|
303
|
+
return; // Skip null or undefined values
|
|
304
|
+
}
|
|
305
|
+
if (Array.isArray(value)) {
|
|
306
|
+
// Append each item in the array with the same key
|
|
307
|
+
// This generates query strings like `key=val1&key=val2&key=val3`
|
|
308
|
+
value.forEach(item => {
|
|
309
|
+
if (item !== null && item !== undefined) { // Ensure array items are also not null/undefined
|
|
310
|
+
formData.append(`${key}[]`, item);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
// Append single values normally
|
|
315
|
+
formData.append(key, value);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return this.request(fullUrl.toString(), {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
body: formData
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
class FTableUserPreferences {
|
|
327
|
+
constructor(prefix, method = 'localStorage') {
|
|
328
|
+
this.prefix = prefix;
|
|
329
|
+
this.method = method;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
set(key, value) {
|
|
333
|
+
const fullKey = `${this.prefix}${key}`;
|
|
334
|
+
|
|
335
|
+
if (this.method === 'localStorage') {
|
|
336
|
+
localStorage.setItem(fullKey, value);
|
|
337
|
+
} else {
|
|
338
|
+
// Cookie fallback
|
|
339
|
+
const expireDate = new Date();
|
|
340
|
+
expireDate.setDate(expireDate.getDate() + 30);
|
|
341
|
+
document.cookie = `${fullKey}=${value}; expires=${expireDate.toUTCString()}; path=/`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
get(key) {
|
|
346
|
+
const fullKey = `${this.prefix}${key}`;
|
|
347
|
+
|
|
348
|
+
if (this.method === 'localStorage') {
|
|
349
|
+
return localStorage.getItem(fullKey);
|
|
350
|
+
} else {
|
|
351
|
+
// Cookie fallback
|
|
352
|
+
const name = fullKey + "=";
|
|
353
|
+
const decodedCookie = decodeURIComponent(document.cookie);
|
|
354
|
+
const ca = decodedCookie.split(';');
|
|
355
|
+
for (let c of ca) {
|
|
356
|
+
while (c.charAt(0) === ' ') {
|
|
357
|
+
c = c.substring(1);
|
|
358
|
+
}
|
|
359
|
+
if (c.indexOf(name) === 0) {
|
|
360
|
+
return c.substring(name.length, c.length);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
remove(key) {
|
|
368
|
+
const fullKey = `${this.prefix}${key}`;
|
|
369
|
+
|
|
370
|
+
if (this.method === 'localStorage') {
|
|
371
|
+
localStorage.removeItem(fullKey);
|
|
372
|
+
} else {
|
|
373
|
+
document.cookie = `${fullKey}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
generatePrefix(tableId, fieldNames) {
|
|
378
|
+
const simpleHash = (value) => {
|
|
379
|
+
let hash = 0;
|
|
380
|
+
if (value.length === 0) return hash;
|
|
381
|
+
|
|
382
|
+
for (let i = 0; i < value.length; i++) {
|
|
383
|
+
const ch = value.charCodeAt(i);
|
|
384
|
+
hash = ((hash << 5) - hash) + ch;
|
|
385
|
+
hash = hash & hash;
|
|
386
|
+
}
|
|
387
|
+
return hash;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
let strToHash = tableId ? `${tableId}#` : '';
|
|
391
|
+
strToHash += fieldNames.join('$') + '#c' + fieldNames.length;
|
|
392
|
+
return `ftable#${simpleHash(strToHash)}`;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
class JtableModal {
|
|
397
|
+
constructor(options = {}) {
|
|
398
|
+
this.options = {
|
|
399
|
+
title: 'Modal',
|
|
400
|
+
content: '',
|
|
401
|
+
buttons: [],
|
|
402
|
+
className: 'ftable-modal',
|
|
403
|
+
parent: document.body,
|
|
404
|
+
...options
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
this.overlay = null;
|
|
408
|
+
this.modal = null;
|
|
409
|
+
this.isOpen = false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
create() {
|
|
413
|
+
// Create overlay
|
|
414
|
+
this.overlay = FTableDOMHelper.create('div', {
|
|
415
|
+
className: 'ftable-modal-overlay',
|
|
416
|
+
parent: this.options.parent
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Create modal
|
|
420
|
+
this.modal = FTableDOMHelper.create('div', {
|
|
421
|
+
className: `ftable-modal ${this.options.className}`,
|
|
422
|
+
parent: this.overlay
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Header
|
|
426
|
+
const header = FTableDOMHelper.create('h2', {
|
|
427
|
+
className: 'ftable-modal-header',
|
|
428
|
+
text: this.options.title,
|
|
429
|
+
parent: this.modal
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Close button
|
|
433
|
+
const closeBtn = FTableDOMHelper.create('span', {
|
|
434
|
+
className: 'ftable-modal-close',
|
|
435
|
+
html: '×',
|
|
436
|
+
parent: this.modal
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
closeBtn.addEventListener('click', () => this.close());
|
|
440
|
+
|
|
441
|
+
// Body
|
|
442
|
+
const body = FTableDOMHelper.create('div', {
|
|
443
|
+
className: 'ftable-modal-body',
|
|
444
|
+
parent: this.modal
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (typeof this.options.content === 'string') {
|
|
448
|
+
body.innerHTML = this.options.content;
|
|
449
|
+
} else {
|
|
450
|
+
body.appendChild(this.options.content);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Footer with buttons
|
|
454
|
+
if (this.options.buttons.length > 0) {
|
|
455
|
+
const footer = FTableDOMHelper.create('div', {
|
|
456
|
+
className: 'ftable-modal-footer',
|
|
457
|
+
parent: this.modal
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
this.options.buttons.forEach(button => {
|
|
461
|
+
const btn = FTableDOMHelper.create('button', {
|
|
462
|
+
className: `ftable-dialog-button ${button.className || ''}`,
|
|
463
|
+
html: `<span>${button.text}</span>`,
|
|
464
|
+
parent: footer
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (button.onClick) {
|
|
468
|
+
btn.addEventListener('click', button.onClick);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Close on overlay click
|
|
474
|
+
this.overlay.addEventListener('click', (e) => {
|
|
475
|
+
if (e.target === this.overlay) {
|
|
476
|
+
this.close();
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
this.hide();
|
|
481
|
+
return this;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
show() {
|
|
485
|
+
if (!this.modal) this.create();
|
|
486
|
+
this.overlay.style.display = 'flex';
|
|
487
|
+
this.isOpen = true;
|
|
488
|
+
return this;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
hide() {
|
|
492
|
+
if (this.overlay) {
|
|
493
|
+
this.overlay.style.display = 'none';
|
|
494
|
+
}
|
|
495
|
+
this.isOpen = false;
|
|
496
|
+
return this;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
close() {
|
|
500
|
+
this.hide();
|
|
501
|
+
if (this.options.onClose) {
|
|
502
|
+
this.options.onClose();
|
|
503
|
+
}
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
destroy() {
|
|
508
|
+
if (this.overlay) {
|
|
509
|
+
this.overlay.remove();
|
|
510
|
+
}
|
|
511
|
+
this.isOpen = false;
|
|
512
|
+
return this;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
setContent(content) {
|
|
516
|
+
this.options.content = content;
|
|
517
|
+
|
|
518
|
+
const body = this.modal.querySelector('.ftable-modal-body');
|
|
519
|
+
if (!body) return;
|
|
520
|
+
|
|
521
|
+
// Clear old content
|
|
522
|
+
body.innerHTML = '';
|
|
523
|
+
if (typeof content === 'string') {
|
|
524
|
+
body.innerHTML = content;
|
|
525
|
+
} else {
|
|
526
|
+
body.appendChild(content);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
class FTableFormBuilder {
|
|
532
|
+
constructor(options) {
|
|
533
|
+
this.options = options;
|
|
534
|
+
this.dependencies = new Map(); // Track field dependencies
|
|
535
|
+
this.optionsCache = new FTableOptionsCache();
|
|
536
|
+
this.originalFieldOptions = new Map(); // Store original field.options
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Store original field options before any resolution
|
|
540
|
+
storeOriginalFieldOptions() {
|
|
541
|
+
if (this.originalFieldOptions.size > 0) return; // Already stored
|
|
542
|
+
|
|
543
|
+
Object.entries(this.options.fields).forEach(([fieldName, field]) => {
|
|
544
|
+
if (field.options && (typeof field.options === 'function' || typeof field.options === 'string')) {
|
|
545
|
+
this.originalFieldOptions.set(fieldName, field.options);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
shouldIncludeField(field, formType) {
|
|
551
|
+
if (formType === 'create') {
|
|
552
|
+
return field.create !== false && !(field.key === true && field.create !== true);
|
|
553
|
+
} else if (formType === 'edit') {
|
|
554
|
+
return field.edit !== false;
|
|
555
|
+
}
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
createFieldContainer(fieldName, field, record, formType) {
|
|
560
|
+
const container = FTableDOMHelper.create('div', {
|
|
561
|
+
className: 'ftable-input-field-container',
|
|
562
|
+
attributes: {
|
|
563
|
+
id: `ftable-input-field-container-div-${fieldName}`,
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Label
|
|
568
|
+
const label = FTableDOMHelper.create('div', {
|
|
569
|
+
className: 'ftable-input-label',
|
|
570
|
+
text: field.inputTitle || field.title,
|
|
571
|
+
parent: container
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Input
|
|
575
|
+
const inputContainer = this.createInput(fieldName, field, record[fieldName], formType);
|
|
576
|
+
container.appendChild(inputContainer);
|
|
577
|
+
|
|
578
|
+
return container;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/*async resolveAllFieldOptions(fieldValues) {
|
|
582
|
+
// Store original options before first resolution
|
|
583
|
+
this.storeOriginalFieldOptions();
|
|
584
|
+
|
|
585
|
+
const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
|
|
586
|
+
// Use original options if we have them, otherwise use current field.options
|
|
587
|
+
const originalOptions = this.originalFieldOptions.get(fieldName) || field.options;
|
|
588
|
+
|
|
589
|
+
if (originalOptions && (typeof originalOptions === 'function' || typeof originalOptions === 'string')) {
|
|
590
|
+
try {
|
|
591
|
+
// Pass fieldValues as dependedValues for dependency resolution
|
|
592
|
+
const params = { dependedValues: fieldValues };
|
|
593
|
+
|
|
594
|
+
// Resolve using original options, not the possibly already-resolved ones
|
|
595
|
+
const tempField = { ...field, options: originalOptions };
|
|
596
|
+
const resolved = await this.resolveOptions(tempField, params);
|
|
597
|
+
field.options = resolved; // Replace with resolved data
|
|
598
|
+
} catch (err) {
|
|
599
|
+
console.error(`Failed to resolve options for ${fieldName}:`, err);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
await Promise.all(promises);
|
|
604
|
+
}*/
|
|
605
|
+
|
|
606
|
+
async resolveNonDependantFieldOptions(fieldValues) {
|
|
607
|
+
// Store original options before first resolution
|
|
608
|
+
this.storeOriginalFieldOptions();
|
|
609
|
+
|
|
610
|
+
const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
|
|
611
|
+
// Use original options if we have them, otherwise use current field.options
|
|
612
|
+
if (field.dependsOn) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const originalOptions = this.originalFieldOptions.get(fieldName) || field.options;
|
|
616
|
+
|
|
617
|
+
if (originalOptions && (typeof originalOptions === 'function' || typeof originalOptions === 'string')) {
|
|
618
|
+
try {
|
|
619
|
+
// Pass fieldValues as dependedValues for dependency resolution
|
|
620
|
+
const params = { dependedValues: fieldValues };
|
|
621
|
+
|
|
622
|
+
// Resolve using original options, not the possibly already-resolved ones
|
|
623
|
+
const tempField = { ...field, options: originalOptions };
|
|
624
|
+
const resolved = await this.resolveOptions(tempField, params);
|
|
625
|
+
field.options = resolved; // Replace with resolved data
|
|
626
|
+
} catch (err) {
|
|
627
|
+
console.error(`Failed to resolve options for ${fieldName}:`, err);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
await Promise.all(promises);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async createForm(formType = 'create', record = {}) {
|
|
635
|
+
|
|
636
|
+
this.currentFormRecord = record;
|
|
637
|
+
|
|
638
|
+
// Pre-resolve all options for fields depending on nothing, the others are handled down the road when dependancies are calculated
|
|
639
|
+
//await this.resolveAllFieldOptions(record);
|
|
640
|
+
await this.resolveNonDependantFieldOptions(record);
|
|
641
|
+
|
|
642
|
+
const form = FTableDOMHelper.create('form', {
|
|
643
|
+
className: `ftable-dialog-form ftable-${formType}-form`
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Build dependency map first
|
|
647
|
+
this.buildDependencyMap();
|
|
648
|
+
|
|
649
|
+
Object.entries(this.options.fields).forEach(([fieldName, field]) => {
|
|
650
|
+
if (this.shouldIncludeField(field, formType)) {
|
|
651
|
+
const fieldContainer = this.createFieldContainer(fieldName, field, record, formType);
|
|
652
|
+
form.appendChild(fieldContainer);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
/*if (this.options.formCreated) {
|
|
657
|
+
this.options.formCreated(form, formType, record);
|
|
658
|
+
}*/
|
|
659
|
+
|
|
660
|
+
// Set up dependency listeners after all fields are created
|
|
661
|
+
this.setupDependencyListeners(form);
|
|
662
|
+
|
|
663
|
+
return form;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
buildDependencyMap() {
|
|
667
|
+
this.dependencies.clear();
|
|
668
|
+
|
|
669
|
+
Object.entries(this.options.fields).forEach(([fieldName, field]) => {
|
|
670
|
+
if (field.dependsOn) {
|
|
671
|
+
// Normalize dependsOn to array
|
|
672
|
+
let dependsOnFields;
|
|
673
|
+
if (typeof field.dependsOn === 'string') {
|
|
674
|
+
// Handle CSV: 'field1, field2' → ['field1', 'field2']
|
|
675
|
+
dependsOnFields = field.dependsOn
|
|
676
|
+
.split(',')
|
|
677
|
+
.map(name => name.trim())
|
|
678
|
+
.filter(name => name);
|
|
679
|
+
} else {
|
|
680
|
+
return; // Invalid type
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Register this field as dependent on each master
|
|
684
|
+
dependsOnFields.forEach(dependsOnField => {
|
|
685
|
+
if (!this.dependencies.has(dependsOnField)) {
|
|
686
|
+
this.dependencies.set(dependsOnField, []);
|
|
687
|
+
}
|
|
688
|
+
this.dependencies.get(dependsOnField).push(fieldName);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
setupDependencyListeners(form) {
|
|
695
|
+
// Collect all master fields (any field that is depended on)
|
|
696
|
+
const masterFieldNames = Array.from(this.dependencies.keys());
|
|
697
|
+
|
|
698
|
+
masterFieldNames.forEach(masterFieldName => {
|
|
699
|
+
const masterInput = form.querySelector(`[name="${masterFieldName}"]`);
|
|
700
|
+
if (!masterInput) return;
|
|
701
|
+
|
|
702
|
+
// Listen for changes
|
|
703
|
+
masterInput.addEventListener('change', () => {
|
|
704
|
+
// Re-evaluate dependent fields (they’ll check their own dependsOn)
|
|
705
|
+
this.handleDependencyChange(form, masterFieldName);
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Trigger initial update
|
|
710
|
+
this.handleDependencyChange(form);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async resolveOptions(field, params = {}) {
|
|
714
|
+
if (!field.options) return [];
|
|
715
|
+
|
|
716
|
+
// Case 1: Direct options (array or object)
|
|
717
|
+
if (Array.isArray(field.options) || typeof field.options === 'object') {
|
|
718
|
+
return field.options;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
let result;
|
|
722
|
+
// Create a mutable flag for cache clearing
|
|
723
|
+
let noCache = false;
|
|
724
|
+
|
|
725
|
+
// Enhance params with clearCache() method
|
|
726
|
+
const enhancedParams = {
|
|
727
|
+
...params,
|
|
728
|
+
clearCache: () => { noCache = true; }
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
if (typeof field.options === 'function') {
|
|
732
|
+
result = await field.options(enhancedParams);
|
|
733
|
+
//result = await field.options(params); // Can return string or { url, noCache }
|
|
734
|
+
} else if (typeof field.options === 'string') {
|
|
735
|
+
result = field.options;
|
|
736
|
+
} else {
|
|
737
|
+
return [];
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// --- Handle result ---
|
|
741
|
+
const isObjectResult = result && typeof result === 'object' && result.url;
|
|
742
|
+
const url = isObjectResult ? result.url : result;
|
|
743
|
+
noCache = isObjectResult && result.noCache !== undefined ? result.noCache : noCache;
|
|
744
|
+
|
|
745
|
+
if (typeof url !== 'string') return [];
|
|
746
|
+
|
|
747
|
+
// Only use cache if noCache is NOT set
|
|
748
|
+
if (!noCache) {
|
|
749
|
+
const cached = this.optionsCache.get(url, {});
|
|
750
|
+
if (cached) return cached;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
const response = await FTableHttpClient.get(url);
|
|
755
|
+
const options = response.Options || response.options || response || [];
|
|
756
|
+
|
|
757
|
+
// Only cache if noCache is false
|
|
758
|
+
if (!noCache) {
|
|
759
|
+
this.optionsCache.set(url, {}, options);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return options;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
console.error(`Failed to load options from ${url}:`, error);
|
|
765
|
+
return [];
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
clearOptionsCache(url = null, params = null) {
|
|
770
|
+
this.optionsCache.clear(url, params);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async handleDependencyChange(form, changedFieldname='') {
|
|
774
|
+
// Build dependedValues: { field1: value1, field2: value2 }
|
|
775
|
+
const dependedValues = {};
|
|
776
|
+
|
|
777
|
+
// Get all field values from the form
|
|
778
|
+
for (const [fieldName, field] of Object.entries(this.options.fields)) {
|
|
779
|
+
const input = form.querySelector(`[name="${fieldName}"]`);
|
|
780
|
+
if (input) {
|
|
781
|
+
if (input.type === 'checkbox') {
|
|
782
|
+
dependedValues[fieldName] = input.checked ? '1' : '0';
|
|
783
|
+
} else {
|
|
784
|
+
dependedValues[fieldName] = input.value;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Determine form context
|
|
790
|
+
const formType = form.classList.contains('ftable-create-form') ? 'create' : 'edit';
|
|
791
|
+
const record = this.currentFormRecord || {};
|
|
792
|
+
|
|
793
|
+
// Prepare base params for options function
|
|
794
|
+
const baseParams = {
|
|
795
|
+
record,
|
|
796
|
+
source: formType,
|
|
797
|
+
form, // DOM form element
|
|
798
|
+
dependedValues
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// Update each dependent field
|
|
802
|
+
for (const [fieldName, field] of Object.entries(this.options.fields)) {
|
|
803
|
+
if (!field.dependsOn) continue;
|
|
804
|
+
if (changedFieldname !== '') {
|
|
805
|
+
let dependsOnFields = field.dependsOn
|
|
806
|
+
.split(',')
|
|
807
|
+
.map(name => name.trim())
|
|
808
|
+
.filter(name => name);
|
|
809
|
+
if (!dependsOnFields.includes(changedFieldname)) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const input = form.querySelector(`[name="${fieldName}"]`);
|
|
815
|
+
if (!input || !this.shouldIncludeField(field, formType)) continue;
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
// Clear current options
|
|
819
|
+
if (input.tagName === 'SELECT') {
|
|
820
|
+
input.innerHTML = '<option value="">Loading...</option>';
|
|
821
|
+
} else if (input.tagName === 'INPUT' && input.list) {
|
|
822
|
+
const datalist = document.getElementById(input.list.id);
|
|
823
|
+
if (datalist) datalist.innerHTML = '';
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Build params with full context
|
|
827
|
+
const params = {
|
|
828
|
+
...baseParams,
|
|
829
|
+
// Specific for this field
|
|
830
|
+
dependsOnField: field.dependsOn,
|
|
831
|
+
dependsOnValue: dependedValues[field.dependsOn]
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// Use original options for dependent fields, not the resolved ones
|
|
835
|
+
const originalOptions = this.originalFieldOptions.get(fieldName) || field.options;
|
|
836
|
+
const tempField = { ...field, options: originalOptions };
|
|
837
|
+
|
|
838
|
+
// Resolve options with full context using original options
|
|
839
|
+
const newOptions = await this.resolveOptions(tempField, params);
|
|
840
|
+
|
|
841
|
+
// Populate
|
|
842
|
+
if (input.tagName === 'SELECT') {
|
|
843
|
+
this.populateSelectOptions(input, newOptions, '');
|
|
844
|
+
} else if (input.tagName === 'INPUT' && input.list) {
|
|
845
|
+
this.populateDatalistOptions(input.list, newOptions);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
} catch (error) {
|
|
849
|
+
console.error(`Error loading options for ${fieldName}:`, error);
|
|
850
|
+
if (input.tagName === 'SELECT') {
|
|
851
|
+
input.innerHTML = '<option value="">Error</option>';
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
parseInputAttributes(inputAttributes) {
|
|
858
|
+
if (typeof inputAttributes === 'string') {
|
|
859
|
+
const parsed = {};
|
|
860
|
+
const regex = /(\w+)(?:=("[^"]*"|'[^']*'|\S+))?/g;
|
|
861
|
+
let match;
|
|
862
|
+
while ((match = regex.exec(inputAttributes)) !== null) {
|
|
863
|
+
const key = match[1];
|
|
864
|
+
const value = match[2] ? match[2].replace(/^["']|["']$/g, '') : '';
|
|
865
|
+
parsed[key] = value === '' ? 'true' : value;
|
|
866
|
+
}
|
|
867
|
+
return parsed;
|
|
868
|
+
}
|
|
869
|
+
return inputAttributes || {};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
createInput(fieldName, field, value, formType) {
|
|
873
|
+
const container = FTableDOMHelper.create('div', {
|
|
874
|
+
className: `ftable-input ftable-${field.type || 'text'}-input`
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
let input;
|
|
878
|
+
|
|
879
|
+
if (value == null || value == undefined ) {
|
|
880
|
+
value = field.defaultValue;
|
|
881
|
+
}
|
|
882
|
+
// Auto-detect select type if options are provided
|
|
883
|
+
if (!field.type && field.options) {
|
|
884
|
+
field.type = 'select';
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Create the input based on type
|
|
888
|
+
switch (field.type) {
|
|
889
|
+
case 'hidden':
|
|
890
|
+
input = this.createHiddenInput(fieldName, field, value);
|
|
891
|
+
break;
|
|
892
|
+
case 'textarea':
|
|
893
|
+
input = this.createTextarea(fieldName, field, value);
|
|
894
|
+
break;
|
|
895
|
+
case 'select':
|
|
896
|
+
input = this.createSelect(fieldName, field, value);
|
|
897
|
+
break;
|
|
898
|
+
case 'checkbox':
|
|
899
|
+
input = this.createCheckbox(fieldName, field, value);
|
|
900
|
+
break;
|
|
901
|
+
case 'radio':
|
|
902
|
+
input = this.createRadioGroup(fieldName, field, value);
|
|
903
|
+
break;
|
|
904
|
+
case 'datalist':
|
|
905
|
+
input = this.createDatalistInput(fieldName, field, value);
|
|
906
|
+
break;
|
|
907
|
+
case 'file':
|
|
908
|
+
input = this.createFileInput(fieldName, field, value);
|
|
909
|
+
break;
|
|
910
|
+
default:
|
|
911
|
+
input = this.createTypedInput(fieldName, field, value);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Allow field.input function to customize or replace the input
|
|
915
|
+
if (typeof field.input === 'function') {
|
|
916
|
+
const data = {
|
|
917
|
+
field: field,
|
|
918
|
+
record: this.currentFormRecord,
|
|
919
|
+
inputField: input,
|
|
920
|
+
formType: formType
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const result = field.input(data);
|
|
924
|
+
|
|
925
|
+
// If result is a string, set as innerHTML
|
|
926
|
+
if (typeof result === 'string') {
|
|
927
|
+
container.innerHTML = result;
|
|
928
|
+
}
|
|
929
|
+
// If result is a DOM node, append it
|
|
930
|
+
else if (result instanceof Node) {
|
|
931
|
+
container.appendChild(result);
|
|
932
|
+
}
|
|
933
|
+
// Otherwise, fallback to default
|
|
934
|
+
else {
|
|
935
|
+
container.appendChild(input);
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
// No custom input function — just add the default input
|
|
939
|
+
container.appendChild(input);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Add explanation if provided
|
|
943
|
+
if (field.explain) {
|
|
944
|
+
const explain = FTableDOMHelper.create('div', {
|
|
945
|
+
className: 'ftable-field-explain',
|
|
946
|
+
html: `<small>${field.explain}</small>`,
|
|
947
|
+
parent: container
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return container;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
createTypedInput(fieldName, field, value) {
|
|
955
|
+
const inputType = field.type || 'text';
|
|
956
|
+
const attributes = {
|
|
957
|
+
type: inputType,
|
|
958
|
+
id: `Edit-${fieldName}`,
|
|
959
|
+
placeholder: field.placeholder || '',
|
|
960
|
+
value: value || '',
|
|
961
|
+
class: field.inputClass || ''
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// extra check for name and multiple
|
|
965
|
+
let name = fieldName;
|
|
966
|
+
// Apply inputAttributes from field definition
|
|
967
|
+
if (field.inputAttributes) {
|
|
968
|
+
let hasMultiple = false;
|
|
969
|
+
|
|
970
|
+
const parsed = this.parseInputAttributes(field.inputAttributes);
|
|
971
|
+
Object.assign(attributes, parsed);
|
|
972
|
+
|
|
973
|
+
hasMultiple = parsed.multiple !== undefined && parsed.multiple !== false;
|
|
974
|
+
|
|
975
|
+
if (hasMultiple) {
|
|
976
|
+
name = `${fieldName}[]`;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
attributes.name = name;
|
|
980
|
+
|
|
981
|
+
// Handle required attribute
|
|
982
|
+
if (field.required) {
|
|
983
|
+
attributes.required = 'required';
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Handle readonly attribute
|
|
987
|
+
if (field.readonly) {
|
|
988
|
+
attributes.readonly = 'readonly';
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Handle disabled attribute
|
|
992
|
+
if (field.disabled) {
|
|
993
|
+
attributes.disabled = 'disabled';
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const input = FTableDOMHelper.create('input', { attributes });
|
|
997
|
+
|
|
998
|
+
// Prevent form submit on Enter, trigger change instead
|
|
999
|
+
input.addEventListener('keypress', (e) => {
|
|
1000
|
+
const keyPressed = e.keyCode || e.which;
|
|
1001
|
+
if (keyPressed === 13) { // Enter key
|
|
1002
|
+
e.preventDefault();
|
|
1003
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
return input;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
createDatalistInput(fieldName, field, value) {
|
|
1012
|
+
const input = FTableDOMHelper.create('input', {
|
|
1013
|
+
attributes: {
|
|
1014
|
+
type: 'text',
|
|
1015
|
+
name: fieldName,
|
|
1016
|
+
id: `Edit-${fieldName}`,
|
|
1017
|
+
placeholder: field.placeholder || '',
|
|
1018
|
+
value: value || '',
|
|
1019
|
+
class: field.inputClass || '',
|
|
1020
|
+
list: `${fieldName}-datalist`
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// Create the datalist element
|
|
1025
|
+
const datalist = FTableDOMHelper.create('datalist', {
|
|
1026
|
+
attributes: {
|
|
1027
|
+
id: `${fieldName}-datalist`
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// Populate datalist options
|
|
1032
|
+
if (field.options) {
|
|
1033
|
+
this.populateDatalistOptions(datalist, field.options);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Append datalist to the document body or form
|
|
1037
|
+
document.body.appendChild(datalist);
|
|
1038
|
+
|
|
1039
|
+
// Store reference for cleanup
|
|
1040
|
+
input.datalistElement = datalist;
|
|
1041
|
+
|
|
1042
|
+
return input;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
populateDatalistOptions(datalist, options) {
|
|
1046
|
+
datalist.innerHTML = ''; // Clear existing options
|
|
1047
|
+
|
|
1048
|
+
if (Array.isArray(options)) {
|
|
1049
|
+
options.forEach(option => {
|
|
1050
|
+
FTableDOMHelper.create('option', {
|
|
1051
|
+
attributes: {
|
|
1052
|
+
value: option.Value || option.value || option
|
|
1053
|
+
},
|
|
1054
|
+
text: option.DisplayText || option.text || option,
|
|
1055
|
+
parent: datalist
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
} else if (typeof options === 'object') {
|
|
1059
|
+
Object.entries(options).forEach(([key, text]) => {
|
|
1060
|
+
FTableDOMHelper.create('option', {
|
|
1061
|
+
attributes: { value: key },
|
|
1062
|
+
text: text,
|
|
1063
|
+
parent: datalist
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
createHiddenInput(fieldName, field, value) {
|
|
1070
|
+
const attributes = {
|
|
1071
|
+
type: 'hidden',
|
|
1072
|
+
name: fieldName,
|
|
1073
|
+
id: `Edit-${fieldName}`,
|
|
1074
|
+
value: value || ''
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// Apply inputAttributes
|
|
1078
|
+
if (field.inputAttributes) {
|
|
1079
|
+
const parsed = this.parseInputAttributes(field.inputAttributes);
|
|
1080
|
+
Object.assign(attributes, parsed);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return FTableDOMHelper.create('input', { attributes });
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
createTextarea(fieldName, field, value) {
|
|
1087
|
+
const attributes = {
|
|
1088
|
+
name: fieldName,
|
|
1089
|
+
id: `Edit-${fieldName}`,
|
|
1090
|
+
class: field.inputClass || '',
|
|
1091
|
+
placeholder: field.placeholder || ''
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
// Apply inputAttributes
|
|
1095
|
+
if (field.inputAttributes) {
|
|
1096
|
+
const parsed = this.parseInputAttributes(field.inputAttributes);
|
|
1097
|
+
Object.assign(attributes, parsed);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const textarea = FTableDOMHelper.create('textarea', { attributes });
|
|
1101
|
+
textarea.value = value || '';
|
|
1102
|
+
return textarea;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
createSelect(fieldName, field, value) {
|
|
1106
|
+
const attributes = {
|
|
1107
|
+
name: fieldName,
|
|
1108
|
+
id: `Edit-${fieldName}`,
|
|
1109
|
+
class: field.inputClass || ''
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// Apply inputAttributes
|
|
1113
|
+
if (field.inputAttributes) {
|
|
1114
|
+
const parsed = this.parseInputAttributes(field.inputAttributes);
|
|
1115
|
+
Object.assign(attributes, parsed);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const select = FTableDOMHelper.create('select', { attributes });
|
|
1119
|
+
|
|
1120
|
+
if (field.options) {
|
|
1121
|
+
//const options = this.resolveOptions(field);
|
|
1122
|
+
this.populateSelectOptions(select, field.options, value);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return select;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
createRadioGroup(fieldName, field, value) {
|
|
1129
|
+
const wrapper = FTableDOMHelper.create('div', {
|
|
1130
|
+
className: 'ftable-radio-group'
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
if (field.options) {
|
|
1134
|
+
const options = Array.isArray(field.options) ? field.options :
|
|
1135
|
+
typeof field.options === 'object' ? Object.entries(field.options).map(([k, v]) => ({Value: k, DisplayText: v})) : [];
|
|
1136
|
+
|
|
1137
|
+
options.forEach((option, index) => {
|
|
1138
|
+
const radioWrapper = FTableDOMHelper.create('div', {
|
|
1139
|
+
className: 'ftable-radio-wrapper',
|
|
1140
|
+
parent: wrapper
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
const radioId = `${fieldName}_${index}`;
|
|
1144
|
+
const radioAttributes = {
|
|
1145
|
+
type: 'radio',
|
|
1146
|
+
name: fieldName,
|
|
1147
|
+
id: radioId,
|
|
1148
|
+
value: option.Value || option.value || option,
|
|
1149
|
+
class: field.inputClass || ''
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
if (field.required && index === 0) radioAttributes.required = 'required';
|
|
1153
|
+
if (field.disabled) radioAttributes.disabled = 'disabled';
|
|
1154
|
+
|
|
1155
|
+
// Apply inputAttributes
|
|
1156
|
+
if (field.inputAttributes) {
|
|
1157
|
+
const parsed = this.parseInputAttributes(field.inputAttributes);
|
|
1158
|
+
Object.assign(attributes, parsed);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const radio = FTableDOMHelper.create('input', {
|
|
1162
|
+
attributes: radioAttributes,
|
|
1163
|
+
parent: radioWrapper
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
if (radioAttributes.value === value) {
|
|
1167
|
+
radio.checked = true;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const label = FTableDOMHelper.create('label', {
|
|
1171
|
+
attributes: { for: radioId },
|
|
1172
|
+
text: option.DisplayText || option.text || option,
|
|
1173
|
+
parent: radioWrapper
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return wrapper;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
createCheckbox(fieldName, field, value) {
|
|
1182
|
+
function getCheckboxText(field, value) {
|
|
1183
|
+
if (value == undefined ) {
|
|
1184
|
+
value = 0;
|
|
1185
|
+
}
|
|
1186
|
+
if (field.values && field.values[value] !== undefined) {
|
|
1187
|
+
return field.values[value];
|
|
1188
|
+
}
|
|
1189
|
+
return value ? 'Yes' : 'No';
|
|
1190
|
+
}
|
|
1191
|
+
const wrapper = FTableDOMHelper.create('div', {
|
|
1192
|
+
className: 'ftable-checkbox-wrapper'
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
const isChecked = [1, '1', true, 'true'].includes(value);
|
|
1196
|
+
|
|
1197
|
+
const displayValue = getCheckboxText(field, value); // Uses field.values if defined
|
|
1198
|
+
|
|
1199
|
+
const checkbox = FTableDOMHelper.create('input', {
|
|
1200
|
+
attributes: {
|
|
1201
|
+
type: 'checkbox',
|
|
1202
|
+
name: fieldName,
|
|
1203
|
+
id: `Edit-${fieldName}`,
|
|
1204
|
+
class: field.inputClass || '',
|
|
1205
|
+
value: '1'
|
|
1206
|
+
},
|
|
1207
|
+
parent: wrapper
|
|
1208
|
+
});
|
|
1209
|
+
checkbox.checked = isChecked;
|
|
1210
|
+
|
|
1211
|
+
const label = FTableDOMHelper.create('label', {
|
|
1212
|
+
attributes: { for: `Edit-${fieldName}` },
|
|
1213
|
+
parent: wrapper
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
// Add the static formText (e.g., "Is Active?")
|
|
1217
|
+
if (field.formText) {
|
|
1218
|
+
FTableDOMHelper.create('span', {
|
|
1219
|
+
text: field.formText,
|
|
1220
|
+
parent: label
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Only add dynamic value span if field.values is defined
|
|
1225
|
+
if (field.values) {
|
|
1226
|
+
const valueSpan = FTableDOMHelper.create('span', {
|
|
1227
|
+
className: 'ftable-checkbox-dynamic-value',
|
|
1228
|
+
text: ` ${displayValue}`,
|
|
1229
|
+
parent: label
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
// Update on change
|
|
1233
|
+
checkbox.addEventListener('change', () => {
|
|
1234
|
+
const newValue = checkbox.checked ? '1' : '0';
|
|
1235
|
+
const newText = getCheckboxText(field, newValue);
|
|
1236
|
+
valueSpan.textContent = ` ${newText}`;
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
return wrapper;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
createDateInput(fieldName, field, value) {
|
|
1244
|
+
return FTableDOMHelper.create('input', {
|
|
1245
|
+
attributes: {
|
|
1246
|
+
type: 'date',
|
|
1247
|
+
name: fieldName,
|
|
1248
|
+
id: `Edit-${fieldName}`,
|
|
1249
|
+
class: field.inputClass || '',
|
|
1250
|
+
value: value || ''
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
populateSelectOptions(select, options, selectedValue) {
|
|
1256
|
+
select.innerHTML = ''; // Clear existing options
|
|
1257
|
+
|
|
1258
|
+
if (Array.isArray(options)) {
|
|
1259
|
+
options.forEach(option => {
|
|
1260
|
+
const value = option.Value !== undefined ? option.Value :
|
|
1261
|
+
option.value !== undefined ? option.value :
|
|
1262
|
+
option; // fallback for string
|
|
1263
|
+
const optionElement = FTableDOMHelper.create('option', {
|
|
1264
|
+
attributes: { value: value },
|
|
1265
|
+
text: option.DisplayText || option.text || option,
|
|
1266
|
+
parent: select
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
if (option.Data && typeof option.Data === 'object') {
|
|
1270
|
+
Object.entries(option.Data).forEach(([key, dataValue]) => {
|
|
1271
|
+
optionElement.setAttribute(`data-${key}`, dataValue);
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (optionElement.value == selectedValue) {
|
|
1276
|
+
optionElement.selected = true;
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
} else if (typeof options === 'object') {
|
|
1280
|
+
Object.entries(options).forEach(([key, text]) => {
|
|
1281
|
+
const optionElement = FTableDOMHelper.create('option', {
|
|
1282
|
+
attributes: { value: key },
|
|
1283
|
+
text: text,
|
|
1284
|
+
parent: select
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
if (key == selectedValue) {
|
|
1288
|
+
optionElement.selected = true;
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
createFileInput(fieldName, field, value) {
|
|
1295
|
+
const attributes = {
|
|
1296
|
+
type: 'file',
|
|
1297
|
+
id: `Edit-${fieldName}`,
|
|
1298
|
+
class: field.inputClass || ''
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
// extra check for name and multiple
|
|
1302
|
+
let name = fieldName;
|
|
1303
|
+
// Apply inputAttributes from field definition
|
|
1304
|
+
if (field.inputAttributes) {
|
|
1305
|
+
let hasMultiple = false;
|
|
1306
|
+
|
|
1307
|
+
const parsed = this.parseInputAttributes(field.inputAttributes);
|
|
1308
|
+
Object.assign(attributes, parsed);
|
|
1309
|
+
hasMultiple = parsed.multiple !== undefined && parsed.multiple !== false;
|
|
1310
|
+
|
|
1311
|
+
if (hasMultiple) {
|
|
1312
|
+
name = `${fieldName}[]`;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
attributes.name = name;
|
|
1316
|
+
|
|
1317
|
+
return FTableDOMHelper.create('input', { attributes });
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Enhanced FTable class with search functionality
|
|
1323
|
+
class FTable extends FTableEventEmitter {
|
|
1324
|
+
constructor(element, options = {}) {
|
|
1325
|
+
super();
|
|
1326
|
+
|
|
1327
|
+
this.element = typeof element === 'string' ?
|
|
1328
|
+
document.querySelector(element) : element;
|
|
1329
|
+
|
|
1330
|
+
// Prevent double initialization
|
|
1331
|
+
if (element.ftableInstance) {
|
|
1332
|
+
//console.warn('FTable is already initialized on this element. Using that.');
|
|
1333
|
+
return element.ftableInstance;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
this.options = this.mergeOptions(options);
|
|
1337
|
+
this.logger = new FTableLogger(this.options.logLevel);
|
|
1338
|
+
this.userPrefs = new FTableUserPreferences('', this.options.saveFTableUserPreferencesMethod);
|
|
1339
|
+
this.formBuilder = new FTableFormBuilder(this.options, this);
|
|
1340
|
+
|
|
1341
|
+
this.state = {
|
|
1342
|
+
records: [],
|
|
1343
|
+
totalRecordCount: 0,
|
|
1344
|
+
currentPage: 1,
|
|
1345
|
+
isLoading: false,
|
|
1346
|
+
selectedRecords: new Set(),
|
|
1347
|
+
sorting: [],
|
|
1348
|
+
searchQueries: {}, // Stores current search terms per field
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
this.elements = {};
|
|
1352
|
+
this.modals = {};
|
|
1353
|
+
this.searchTimeout = null; // For debouncing
|
|
1354
|
+
this._recalculatedOnce = false;
|
|
1355
|
+
|
|
1356
|
+
// store it on the DOM too, so people can access it
|
|
1357
|
+
this.element.ftableInstance = this;
|
|
1358
|
+
|
|
1359
|
+
this.init();
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
mergeOptions(options) {
|
|
1363
|
+
const defaults = {
|
|
1364
|
+
tableId: undefined,
|
|
1365
|
+
logLevel: FTableLogger.LOG_LEVELS.WARN,
|
|
1366
|
+
actions: {},
|
|
1367
|
+
fields: {},
|
|
1368
|
+
animationsEnabled: true,
|
|
1369
|
+
loadingAnimationDelay: 1000,
|
|
1370
|
+
defaultDateFormat: 'yyyy-mm-dd',
|
|
1371
|
+
saveFTableUserPreferences: true,
|
|
1372
|
+
saveFTableUserPreferencesMethod: 'localStorage',
|
|
1373
|
+
defaultSorting: '',
|
|
1374
|
+
|
|
1375
|
+
// Paging
|
|
1376
|
+
paging: false,
|
|
1377
|
+
pageSize: 10,
|
|
1378
|
+
gotoPageArea: 'combobox',
|
|
1379
|
+
|
|
1380
|
+
// Sorting
|
|
1381
|
+
sorting: false,
|
|
1382
|
+
multiSorting: false,
|
|
1383
|
+
|
|
1384
|
+
// Selection
|
|
1385
|
+
selecting: false,
|
|
1386
|
+
multiselect: false,
|
|
1387
|
+
|
|
1388
|
+
// Toolbar search
|
|
1389
|
+
toolbarsearch: false, // Enable/disable toolbar search row
|
|
1390
|
+
toolbarreset: true, // Show reset button
|
|
1391
|
+
searchDebounceMs: 300, // Debounce time for search input
|
|
1392
|
+
|
|
1393
|
+
// Caching
|
|
1394
|
+
listCache: 30000, // or listCache: 30000 (duration in ms)
|
|
1395
|
+
|
|
1396
|
+
// Messages
|
|
1397
|
+
messages: { ...JTABLE_DEFAULT_MESSAGES } // Safe copy
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
return this.deepMerge(defaults, options);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
deepMerge(target, source) {
|
|
1404
|
+
const result = { ...target };
|
|
1405
|
+
|
|
1406
|
+
for (const key in source) {
|
|
1407
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
1408
|
+
result[key] = this.deepMerge(result[key] || {}, source[key]);
|
|
1409
|
+
} else {
|
|
1410
|
+
result[key] = source[key];
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
return result;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// Public
|
|
1418
|
+
static setMessages(customMessages) {
|
|
1419
|
+
Object.assign(JTABLE_DEFAULT_MESSAGES, customMessages);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
init() {
|
|
1423
|
+
this.processFieldDefinitions();
|
|
1424
|
+
this.createMainStructure();
|
|
1425
|
+
this.setupFTableUserPreferences();
|
|
1426
|
+
|
|
1427
|
+
this.createTable();
|
|
1428
|
+
this.createModals();
|
|
1429
|
+
|
|
1430
|
+
// Create paging UI if enabled
|
|
1431
|
+
if (this.options.paging) {
|
|
1432
|
+
this.createPagingUI();
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Start resolving in background
|
|
1436
|
+
this.resolveAsyncFieldOptions().then(() => {
|
|
1437
|
+
// re-render dynamic options rows — no server call
|
|
1438
|
+
setTimeout(() => {
|
|
1439
|
+
this.refreshDisplayValues();
|
|
1440
|
+
}, 0);
|
|
1441
|
+
}).catch(console.error);
|
|
1442
|
+
|
|
1443
|
+
this.bindEvents();
|
|
1444
|
+
|
|
1445
|
+
this.renderSortingInfo();
|
|
1446
|
+
|
|
1447
|
+
// Add essential CSS if not already present
|
|
1448
|
+
//this.addEssentialCSS();
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
parseDefaultSorting(sortStr) {
|
|
1452
|
+
const result = [];
|
|
1453
|
+
if (!sortStr || typeof sortStr !== 'string') return result;
|
|
1454
|
+
|
|
1455
|
+
sortStr.split(',').forEach(part => {
|
|
1456
|
+
const trimmed = part.trim();
|
|
1457
|
+
if (!trimmed) return;
|
|
1458
|
+
|
|
1459
|
+
const descIndex = trimmed.toUpperCase().indexOf(' DESC');
|
|
1460
|
+
const ascIndex = trimmed.toUpperCase().indexOf(' ASC');
|
|
1461
|
+
|
|
1462
|
+
let direction = 'ASC';
|
|
1463
|
+
let fieldName = trimmed;
|
|
1464
|
+
|
|
1465
|
+
if (descIndex > 0) {
|
|
1466
|
+
fieldName = trimmed.slice(0, descIndex).trim();
|
|
1467
|
+
direction = 'DESC';
|
|
1468
|
+
} else if (ascIndex > 0) {
|
|
1469
|
+
fieldName = trimmed.slice(0, ascIndex).trim();
|
|
1470
|
+
direction = 'ASC';
|
|
1471
|
+
} else {
|
|
1472
|
+
fieldName = trimmed.trim();
|
|
1473
|
+
direction = 'ASC';
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
const field = this.options.fields[fieldName];
|
|
1477
|
+
if (field && field.sorting !== false) {
|
|
1478
|
+
result.push({ fieldName, direction });
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
return result;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
addEssentialCSS() {
|
|
1486
|
+
// Check if our CSS is already added
|
|
1487
|
+
if (document.querySelector('#ftable-essential-css')) return;
|
|
1488
|
+
|
|
1489
|
+
const css = `
|
|
1490
|
+
.ftable-row-animation {
|
|
1491
|
+
transition: background-color 0.3s ease;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
.ftable-row-added {
|
|
1495
|
+
background-color: #d4edda !important;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
.ftable-row-edited {
|
|
1499
|
+
background-color: #d1ecf1 !important;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.ftable-row-deleted {
|
|
1503
|
+
opacity: 0;
|
|
1504
|
+
transform: translateY(-10px);
|
|
1505
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
.ftable-toolbarsearch {
|
|
1509
|
+
width: 90%;
|
|
1510
|
+
}
|
|
1511
|
+
`;
|
|
1512
|
+
|
|
1513
|
+
const style = document.createElement('style');
|
|
1514
|
+
style.id = 'ftable-essential-css';
|
|
1515
|
+
style.textContent = css;
|
|
1516
|
+
document.head.appendChild(style);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
createPagingUI() {
|
|
1520
|
+
this.elements.bottomPanel = FTableDOMHelper.create('div', {
|
|
1521
|
+
className: 'ftable-bottom-panel',
|
|
1522
|
+
parent: this.elements.mainContainer
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
this.elements.leftArea = FTableDOMHelper.create('div', {
|
|
1526
|
+
className: 'ftable-left-area',
|
|
1527
|
+
parent: this.elements.bottomPanel
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
this.elements.rightArea = FTableDOMHelper.create('div', {
|
|
1531
|
+
className: 'ftable-right-area',
|
|
1532
|
+
parent: this.elements.bottomPanel
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
// Page list area
|
|
1536
|
+
this.elements.pagingListArea = FTableDOMHelper.create('div', {
|
|
1537
|
+
className: 'ftable-page-list',
|
|
1538
|
+
parent: this.elements.leftArea
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
// Page Goto area
|
|
1542
|
+
this.elements.pagingGotoArea = FTableDOMHelper.create('div', {
|
|
1543
|
+
className: 'ftable-page-goto',
|
|
1544
|
+
parent: this.elements.leftArea
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
// Page info area
|
|
1548
|
+
this.elements.pageInfoSpan = FTableDOMHelper.create('div', {
|
|
1549
|
+
className: 'ftable-page-info',
|
|
1550
|
+
parent: this.elements.rightArea
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
// Page size selector if enabled
|
|
1554
|
+
if (this.options.pageSizeChangeArea !== false) {
|
|
1555
|
+
this.createPageSizeSelector();
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
createPageSizeSelector() {
|
|
1561
|
+
const container = FTableDOMHelper.create('span', {
|
|
1562
|
+
className: 'ftable-page-size-change',
|
|
1563
|
+
parent: this.elements.leftArea
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
FTableDOMHelper.create('span', {
|
|
1567
|
+
text: this.options.messages.pageSizeChangeLabel,
|
|
1568
|
+
parent: container
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
const select = FTableDOMHelper.create('select', {
|
|
1572
|
+
className: 'ftable-page-size-select',
|
|
1573
|
+
parent: container
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
const pageSizes = this.options.pageSizes || [10, 25, 50, 100];
|
|
1577
|
+
pageSizes.forEach(size => {
|
|
1578
|
+
const option = FTableDOMHelper.create('option', {
|
|
1579
|
+
attributes: { value: size },
|
|
1580
|
+
text: size.toString(),
|
|
1581
|
+
parent: select
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
if (size === this.state.pageSize) {
|
|
1585
|
+
option.selected = true;
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
select.addEventListener('change', (e) => {
|
|
1590
|
+
this.changePageSize(parseInt(e.target.value));
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
processFieldDefinitions() {
|
|
1595
|
+
this.fieldList = Object.keys(this.options.fields);
|
|
1596
|
+
|
|
1597
|
+
// Set default values for each field
|
|
1598
|
+
this.fieldList.forEach(fieldName => {
|
|
1599
|
+
const field = this.options.fields[fieldName];
|
|
1600
|
+
const isKeyField = field.key === true;
|
|
1601
|
+
|
|
1602
|
+
if (isKeyField) {
|
|
1603
|
+
if (field.create === undefined || !field.create) {
|
|
1604
|
+
field.create = true;
|
|
1605
|
+
field.type = 'hidden';
|
|
1606
|
+
}
|
|
1607
|
+
if (field.edit === undefined || !field.edit) {
|
|
1608
|
+
field.edit = true;
|
|
1609
|
+
field.type = 'hidden';
|
|
1610
|
+
}
|
|
1611
|
+
if (!field.hasOwnProperty('visibility')) {
|
|
1612
|
+
field.visibility = 'hidden';
|
|
1613
|
+
}
|
|
1614
|
+
} else {
|
|
1615
|
+
if (field.create === undefined) {
|
|
1616
|
+
field.create = true;
|
|
1617
|
+
}
|
|
1618
|
+
if (field.edit === undefined) {
|
|
1619
|
+
field.edit = true;
|
|
1620
|
+
}
|
|
1621
|
+
if (field.list === undefined) {
|
|
1622
|
+
field.list = true;
|
|
1623
|
+
}
|
|
1624
|
+
if (!field.hasOwnProperty('visibility')) {
|
|
1625
|
+
field.visibility = 'visible';
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
// Build column list (visible, listable, non-hidden) fields
|
|
1631
|
+
this.columnList = this.fieldList.filter(name => {
|
|
1632
|
+
const field = this.options.fields[name];
|
|
1633
|
+
return field.list !== false && field.type !== 'hidden';
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
// Find key field
|
|
1637
|
+
this.keyField = this.fieldList.find(name => this.options.fields[name].key === true);
|
|
1638
|
+
if (!this.keyField) {
|
|
1639
|
+
this.logger.warn('No key field defined');
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
async resolveAsyncFieldOptions() {
|
|
1644
|
+
// Store original field options before any resolution
|
|
1645
|
+
this.formBuilder.storeOriginalFieldOptions();
|
|
1646
|
+
|
|
1647
|
+
for (const fieldName of this.columnList) {
|
|
1648
|
+
const field = this.options.fields[fieldName];
|
|
1649
|
+
|
|
1650
|
+
// Use original options if available
|
|
1651
|
+
const originalOptions = this.formBuilder.originalFieldOptions.get(fieldName) || field.options;
|
|
1652
|
+
|
|
1653
|
+
if (originalOptions &&
|
|
1654
|
+
(typeof originalOptions === 'function' || typeof originalOptions === 'string') &&
|
|
1655
|
+
!Array.isArray(originalOptions) &&
|
|
1656
|
+
!(typeof originalOptions === 'object' && !Array.isArray(originalOptions) && Object.keys(originalOptions).length > 0)
|
|
1657
|
+
) {
|
|
1658
|
+
try {
|
|
1659
|
+
// Create temp field with original options for resolution
|
|
1660
|
+
const tempField = { ...field, options: originalOptions };
|
|
1661
|
+
const resolved = await this.formBuilder.resolveOptions(tempField);
|
|
1662
|
+
field.options = resolved;
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
console.error(`Failed to resolve options for ${fieldName}:`, err);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
refreshDisplayValues() {
|
|
1671
|
+
const rows = this.elements.tableBody.querySelectorAll('.ftable-data-row');
|
|
1672
|
+
if (rows.length === 0) return;
|
|
1673
|
+
|
|
1674
|
+
rows.forEach(row => {
|
|
1675
|
+
this.columnList.forEach(fieldName => {
|
|
1676
|
+
const field = this.options.fields[fieldName];
|
|
1677
|
+
if (!field.options) return;
|
|
1678
|
+
|
|
1679
|
+
// Check if options are now resolved (was a function/string before)
|
|
1680
|
+
if (typeof field.options === 'function' || typeof field.options === 'string') {
|
|
1681
|
+
return; // Still unresolved
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const cell = row.querySelector(`td[data-field-name="${fieldName}"]`);
|
|
1685
|
+
if (!cell) return;
|
|
1686
|
+
|
|
1687
|
+
const value = this.getDisplayText(row.recordData, fieldName);
|
|
1688
|
+
cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
|
|
1689
|
+
});
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
createMainStructure() {
|
|
1694
|
+
this.elements.mainContainer = FTableDOMHelper.create('div', {
|
|
1695
|
+
className: 'ftable-main-container',
|
|
1696
|
+
parent: this.element
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
// Title
|
|
1700
|
+
if (this.options.title) {
|
|
1701
|
+
this.elements.titleDiv = FTableDOMHelper.create('div', {
|
|
1702
|
+
className: 'ftable-title',
|
|
1703
|
+
parent: this.elements.mainContainer
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
FTableDOMHelper.create('div', {
|
|
1707
|
+
className: 'ftable-title-text',
|
|
1708
|
+
text: this.options.title,
|
|
1709
|
+
parent: this.elements.titleDiv
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Toolbar
|
|
1714
|
+
this.elements.toolbarDiv = FTableDOMHelper.create('div', {
|
|
1715
|
+
className: 'ftable-toolbar',
|
|
1716
|
+
parent: this.elements.titleDiv || this.elements.mainContainer
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
// Table container
|
|
1720
|
+
this.elements.tableDiv = FTableDOMHelper.create('div', {
|
|
1721
|
+
className: 'ftable-table-div',
|
|
1722
|
+
parent: this.elements.mainContainer
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
createTable() {
|
|
1727
|
+
this.elements.table = FTableDOMHelper.create('table', {
|
|
1728
|
+
className: 'ftable',
|
|
1729
|
+
parent: this.elements.tableDiv
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
if (this.options.tableId) {
|
|
1733
|
+
this.elements.table.id = this.options.tableId;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
this.createTableHeader();
|
|
1737
|
+
this.createTableBody();
|
|
1738
|
+
this.addNoDataRow();
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
createTableHeader() {
|
|
1742
|
+
const thead = FTableDOMHelper.create('thead', {
|
|
1743
|
+
parent: this.elements.table
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
const headerRow = FTableDOMHelper.create('tr', {
|
|
1747
|
+
parent: thead
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
// Add selecting column if enabled
|
|
1751
|
+
if (this.options.selecting) {
|
|
1752
|
+
const selectHeader = FTableDOMHelper.create('th', {
|
|
1753
|
+
className: 'ftable-column-header ftable-column-header-select',
|
|
1754
|
+
parent: headerRow
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
if (this.options.multiselect) {
|
|
1758
|
+
const selectAllCheckbox = FTableDOMHelper.create('input', {
|
|
1759
|
+
attributes: { type: 'checkbox' },
|
|
1760
|
+
parent: selectHeader
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
selectAllCheckbox.addEventListener('change', () => {
|
|
1764
|
+
this.toggleSelectAll(selectAllCheckbox.checked);
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Add data columns
|
|
1770
|
+
this.columnList.forEach(fieldName => {
|
|
1771
|
+
const field = this.options.fields[fieldName];
|
|
1772
|
+
const th = FTableDOMHelper.create('th', {
|
|
1773
|
+
className: 'ftable-column-header',
|
|
1774
|
+
attributes: { 'data-field-name': fieldName },
|
|
1775
|
+
parent: headerRow
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
// Set width if specified
|
|
1779
|
+
if (field.width) {
|
|
1780
|
+
th.style.width = field.width;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
const container = FTableDOMHelper.create('div', {
|
|
1784
|
+
className: 'ftable-column-header-container',
|
|
1785
|
+
parent: th
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
FTableDOMHelper.create('span', {
|
|
1789
|
+
className: 'ftable-column-header-text',
|
|
1790
|
+
text: field.title || fieldName,
|
|
1791
|
+
parent: container
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
// Make sortable if enabled
|
|
1795
|
+
if (this.options.sorting && field.sorting !== false) {
|
|
1796
|
+
FTableDOMHelper.addClass(th, 'ftable-column-header-sortable');
|
|
1797
|
+
th.addEventListener('click', () => this.sortByColumn(fieldName));
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// Add resize handler if column resizing is enabled
|
|
1801
|
+
if (this.options.columnResizable !== false && field.columnResizable !== false) {
|
|
1802
|
+
this.makeColumnResizable(th, container);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// Hide column if needed
|
|
1806
|
+
if (field.visibility === 'hidden') {
|
|
1807
|
+
FTableDOMHelper.hide(th);
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
// Add action columns
|
|
1812
|
+
if (this.options.actions.updateAction) {
|
|
1813
|
+
FTableDOMHelper.create('th', {
|
|
1814
|
+
className: 'ftable-command-column-header ftable-column-header-edit',
|
|
1815
|
+
parent: headerRow
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
if (this.options.actions.cloneAction) {
|
|
1820
|
+
FTableDOMHelper.create('th', {
|
|
1821
|
+
className: 'ftable-command-column-header ftable-column-header-clone',
|
|
1822
|
+
parent: headerRow
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
if (this.options.actions.deleteAction) {
|
|
1827
|
+
FTableDOMHelper.create('th', {
|
|
1828
|
+
className: 'ftable-command-column-header ftable-column-header-delete',
|
|
1829
|
+
parent: headerRow
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
if (this.options.toolbarsearch) {
|
|
1834
|
+
// Handle async search row without blocking by using catch
|
|
1835
|
+
this.createSearchHeaderRow(thead).catch(err => {
|
|
1836
|
+
console.error('Failed to create search header row:', err);
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
async createSearchHeaderRow(theadParent) {
|
|
1842
|
+
const searchRow = FTableDOMHelper.create('tr', {
|
|
1843
|
+
className: 'ftable-toolbarsearch-row',
|
|
1844
|
+
parent: theadParent
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
// Add empty cell for selecting column if enabled
|
|
1848
|
+
if (this.options.selecting) {
|
|
1849
|
+
FTableDOMHelper.create('th', { parent: searchRow });
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// Add search input cells for data columns
|
|
1853
|
+
for (const fieldName of this.columnList) {
|
|
1854
|
+
const field = this.options.fields[fieldName];
|
|
1855
|
+
const isSearchable = field.searchable !== false;
|
|
1856
|
+
|
|
1857
|
+
const th = FTableDOMHelper.create('th', {
|
|
1858
|
+
className: 'ftable-toolbarsearch-column-header',
|
|
1859
|
+
parent: searchRow
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
if (isSearchable) {
|
|
1863
|
+
const container = FTableDOMHelper.create('div', {
|
|
1864
|
+
className: 'ftable-column-header-container',
|
|
1865
|
+
parent: th
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
let input;
|
|
1869
|
+
|
|
1870
|
+
// Auto-detect select type if options are provided
|
|
1871
|
+
if (!field.type && field.options) {
|
|
1872
|
+
field.type = 'select';
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
switch (field.type) {
|
|
1876
|
+
case 'date':
|
|
1877
|
+
input = FTableDOMHelper.create('input', {
|
|
1878
|
+
attributes: {
|
|
1879
|
+
type: 'date',
|
|
1880
|
+
'data-field-name': fieldName,
|
|
1881
|
+
class: 'ftable-toolbarsearch'
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
break;
|
|
1885
|
+
|
|
1886
|
+
case 'checkbox':
|
|
1887
|
+
if (field.values) {
|
|
1888
|
+
input = await this.createSelectForSearch(fieldName, field, true);
|
|
1889
|
+
} else {
|
|
1890
|
+
input = FTableDOMHelper.create('input', {
|
|
1891
|
+
attributes: {
|
|
1892
|
+
type: 'text',
|
|
1893
|
+
'data-field-name': fieldName,
|
|
1894
|
+
class: 'ftable-toolbarsearch',
|
|
1895
|
+
placeholder: 'Search...'
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
break;
|
|
1900
|
+
|
|
1901
|
+
case 'select':
|
|
1902
|
+
if (field.options) {
|
|
1903
|
+
input = await this.createSelectForSearch(fieldName, field, false);
|
|
1904
|
+
} else {
|
|
1905
|
+
input = FTableDOMHelper.create('input', {
|
|
1906
|
+
attributes: {
|
|
1907
|
+
type: 'text',
|
|
1908
|
+
'data-field-name': fieldName,
|
|
1909
|
+
class: 'ftable-toolbarsearch',
|
|
1910
|
+
placeholder: 'Search...'
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
break;
|
|
1915
|
+
|
|
1916
|
+
default:
|
|
1917
|
+
input = FTableDOMHelper.create('input', {
|
|
1918
|
+
attributes: {
|
|
1919
|
+
type: 'text',
|
|
1920
|
+
'data-field-name': fieldName,
|
|
1921
|
+
class: 'ftable-toolbarsearch',
|
|
1922
|
+
placeholder: 'Search...'
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
if (input) {
|
|
1928
|
+
container.appendChild(input);
|
|
1929
|
+
|
|
1930
|
+
if (input.tagName === 'SELECT') {
|
|
1931
|
+
input.addEventListener('change', (e) => {
|
|
1932
|
+
this.handleSearchInputChange(e);
|
|
1933
|
+
});
|
|
1934
|
+
} else {
|
|
1935
|
+
input.addEventListener('input', (e) => {
|
|
1936
|
+
this.handleSearchInputChange(e);
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// Hide search cell if column is hidden
|
|
1943
|
+
if (field.visibility === 'hidden') {
|
|
1944
|
+
FTableDOMHelper.hide(th);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
if (this.options.toolbarsearch && this.options.toolbarreset) {
|
|
1949
|
+
// Add reset button cell
|
|
1950
|
+
const resetTh = FTableDOMHelper.create('th', {
|
|
1951
|
+
className: 'ftable-toolbarsearch-reset',
|
|
1952
|
+
parent: searchRow
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
const actionCount = (this.options.actions.updateAction ? 1 : 0) +
|
|
1956
|
+
(this.options.actions.deleteAction ? 1 : 0);
|
|
1957
|
+
|
|
1958
|
+
if (actionCount > 0) {
|
|
1959
|
+
resetTh.colSpan = actionCount;
|
|
1960
|
+
} else {
|
|
1961
|
+
FTableDOMHelper.addClass(resetTh, 'ftable-command-column-header');
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
const resetButton = FTableDOMHelper.create('button', {
|
|
1965
|
+
className: 'ftable-toolbarsearch-reset-button',
|
|
1966
|
+
text: this.options.messages.resetSearch,
|
|
1967
|
+
parent: resetTh
|
|
1968
|
+
});
|
|
1969
|
+
resetButton.addEventListener('click', () => this.resetSearch());
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
async createSelectForSearch(fieldName, field, isCheckboxValues) {
|
|
1974
|
+
const select = FTableDOMHelper.create('select', {
|
|
1975
|
+
attributes: {
|
|
1976
|
+
'data-field-name': fieldName,
|
|
1977
|
+
class: 'ftable-toolbarsearch'
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
let optionsSource;
|
|
1982
|
+
if (isCheckboxValues && field.values) {
|
|
1983
|
+
optionsSource = Object.entries(field.values).map(([value, displayText]) => ({
|
|
1984
|
+
Value: value,
|
|
1985
|
+
DisplayText: displayText
|
|
1986
|
+
}));
|
|
1987
|
+
} else if (field.options) {
|
|
1988
|
+
optionsSource = await this.formBuilder.resolveOptions(field);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// Add empty option only if first option is not already empty
|
|
1992
|
+
const hasEmptyFirst = optionsSource?.length > 0 &&
|
|
1993
|
+
(optionsSource[0].Value === '' ||
|
|
1994
|
+
optionsSource[0].value === '' ||
|
|
1995
|
+
optionsSource[0] === '' ||
|
|
1996
|
+
(optionsSource[0].DisplayText === '' && optionsSource[0].Value == null));
|
|
1997
|
+
|
|
1998
|
+
if (!hasEmptyFirst) {
|
|
1999
|
+
FTableDOMHelper.create('option', {
|
|
2000
|
+
attributes: { value: '' },
|
|
2001
|
+
text: '',
|
|
2002
|
+
parent: select
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
if (optionsSource && Array.isArray(optionsSource)) {
|
|
2007
|
+
optionsSource.forEach(option => {
|
|
2008
|
+
const optionElement = FTableDOMHelper.create('option', {
|
|
2009
|
+
attributes: {
|
|
2010
|
+
value: option.Value !== undefined ? option.Value : option.value !== undefined ? option.value : option
|
|
2011
|
+
},
|
|
2012
|
+
text: option.DisplayText || option.text || option,
|
|
2013
|
+
parent: select
|
|
2014
|
+
});
|
|
2015
|
+
});
|
|
2016
|
+
} else if (optionsSource && typeof optionsSource === 'object') {
|
|
2017
|
+
Object.entries(optionsSource).forEach(([key, text]) => {
|
|
2018
|
+
FTableDOMHelper.create('option', {
|
|
2019
|
+
attributes: { value: key },
|
|
2020
|
+
text: text,
|
|
2021
|
+
parent: select
|
|
2022
|
+
});
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
return select;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
handleSearchInputChange(event) {
|
|
2030
|
+
const input = event.target;
|
|
2031
|
+
const fieldName = input.getAttribute('data-field-name');
|
|
2032
|
+
const value = input.value.trim();
|
|
2033
|
+
|
|
2034
|
+
// Update internal search state
|
|
2035
|
+
if (value) {
|
|
2036
|
+
this.state.searchQueries[fieldName] = value;
|
|
2037
|
+
} else {
|
|
2038
|
+
delete this.state.searchQueries[fieldName];
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Debounce the load call
|
|
2042
|
+
clearTimeout(this.searchTimeout);
|
|
2043
|
+
this.searchTimeout = setTimeout(() => {
|
|
2044
|
+
this.load();
|
|
2045
|
+
}, this.options.searchDebounceMs);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
resetSearch() {
|
|
2049
|
+
// Clear internal search state
|
|
2050
|
+
this.state.searchQueries = {};
|
|
2051
|
+
|
|
2052
|
+
// Clear input values in the search row
|
|
2053
|
+
const searchInputs = this.elements.table.querySelectorAll('.ftable-toolbarsearch');
|
|
2054
|
+
searchInputs.forEach(input => {
|
|
2055
|
+
if (input.tagName === 'SELECT') {
|
|
2056
|
+
input.selectedIndex = 0; // Select the first (empty) option
|
|
2057
|
+
} else {
|
|
2058
|
+
input.value = '';
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
// Reload data without search parameters
|
|
2063
|
+
this.load();
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
makeColumnResizable(th, container) {
|
|
2067
|
+
// Create resize bar if it doesn't exist
|
|
2068
|
+
if (!this.elements.resizeBar) {
|
|
2069
|
+
this.elements.resizeBar = FTableDOMHelper.create('div', {
|
|
2070
|
+
className: 'ftable-column-resize-bar',
|
|
2071
|
+
parent: this.elements.mainContainer
|
|
2072
|
+
});
|
|
2073
|
+
FTableDOMHelper.hide(this.elements.resizeBar);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const resizeHandler = FTableDOMHelper.create('div', {
|
|
2077
|
+
className: 'ftable-column-resize-handler',
|
|
2078
|
+
parent: container
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
let isResizing = false;
|
|
2082
|
+
let startX = 0;
|
|
2083
|
+
let startWidth = 0;
|
|
2084
|
+
|
|
2085
|
+
resizeHandler.addEventListener('mousedown', (e) => {
|
|
2086
|
+
e.preventDefault();
|
|
2087
|
+
e.stopPropagation();
|
|
2088
|
+
|
|
2089
|
+
isResizing = true;
|
|
2090
|
+
startX = e.clientX;
|
|
2091
|
+
startWidth = th.offsetWidth;
|
|
2092
|
+
|
|
2093
|
+
// Show resize bar
|
|
2094
|
+
const rect = th.getBoundingClientRect();
|
|
2095
|
+
const containerRect = this.elements.mainContainer.getBoundingClientRect();
|
|
2096
|
+
|
|
2097
|
+
this.elements.resizeBar.style.left = (rect.right - containerRect.left) + 'px';
|
|
2098
|
+
this.elements.resizeBar.style.top = (rect.top - containerRect.top) + 'px';
|
|
2099
|
+
this.elements.resizeBar.style.height = this.elements.table.offsetHeight + 'px';
|
|
2100
|
+
FTableDOMHelper.show(this.elements.resizeBar);
|
|
2101
|
+
|
|
2102
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
2103
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
const handleMouseMove = (e) => {
|
|
2107
|
+
if (!isResizing) return;
|
|
2108
|
+
|
|
2109
|
+
const diff = e.clientX - startX;
|
|
2110
|
+
const newWidth = Math.max(50, startWidth + diff); // Minimum 50px width
|
|
2111
|
+
|
|
2112
|
+
// Update resize bar position
|
|
2113
|
+
const containerRect = this.elements.mainContainer.getBoundingClientRect();
|
|
2114
|
+
this.elements.resizeBar.style.left = (e.clientX - containerRect.left) + 'px';
|
|
2115
|
+
};
|
|
2116
|
+
|
|
2117
|
+
const handleMouseUp = (e) => {
|
|
2118
|
+
if (!isResizing) return;
|
|
2119
|
+
|
|
2120
|
+
isResizing = false;
|
|
2121
|
+
const diff = e.clientX - startX;
|
|
2122
|
+
const newWidth = Math.max(50, startWidth + diff);
|
|
2123
|
+
|
|
2124
|
+
// Apply new width
|
|
2125
|
+
th.style.width = newWidth + 'px';
|
|
2126
|
+
|
|
2127
|
+
// Hide resize bar
|
|
2128
|
+
FTableDOMHelper.hide(this.elements.resizeBar);
|
|
2129
|
+
|
|
2130
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
2131
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
2132
|
+
|
|
2133
|
+
// Save column width preference if enabled
|
|
2134
|
+
if (this.options.saveFTableUserPreferences) {
|
|
2135
|
+
this.saveColumnSettings();
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
saveColumnSettings() {
|
|
2141
|
+
if (!this.options.saveFTableUserPreferences) return;
|
|
2142
|
+
|
|
2143
|
+
const settings = {};
|
|
2144
|
+
this.columnList.forEach(fieldName => {
|
|
2145
|
+
const th = this.elements.table.querySelector(`[data-field-name="${fieldName}"]`);
|
|
2146
|
+
if (th) {
|
|
2147
|
+
const field = this.options.fields[fieldName];
|
|
2148
|
+
settings[fieldName] = {
|
|
2149
|
+
width: th.style.width || field.width || 'auto',
|
|
2150
|
+
visibility: field.visibility || 'visible'
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
this.userPrefs.set('column-settings', JSON.stringify(settings));
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
saveState() {
|
|
2159
|
+
if (!this.options.saveFTableUserPreferences) return;
|
|
2160
|
+
|
|
2161
|
+
const state = {
|
|
2162
|
+
sorting: this.state.sorting,
|
|
2163
|
+
pageSize: this.state.pageSize
|
|
2164
|
+
};
|
|
2165
|
+
|
|
2166
|
+
this.userPrefs.set('table-state', JSON.stringify(state));
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
loadColumnSettings() {
|
|
2170
|
+
if (!this.options.saveFTableUserPreferences) return;
|
|
2171
|
+
|
|
2172
|
+
const settingsJson = this.userPrefs.get('column-settings');
|
|
2173
|
+
if (!settingsJson) return;
|
|
2174
|
+
|
|
2175
|
+
try {
|
|
2176
|
+
const settings = JSON.parse(settingsJson);
|
|
2177
|
+
Object.entries(settings).forEach(([fieldName, config]) => {
|
|
2178
|
+
const field = this.options.fields[fieldName];
|
|
2179
|
+
if (field) {
|
|
2180
|
+
if (config.width) field.width = config.width;
|
|
2181
|
+
if (config.visibility) field.visibility = config.visibility;
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
} catch (error) {
|
|
2185
|
+
this.logger.warn('Failed to load column settings:', error);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
loadState() {
|
|
2190
|
+
if (!this.options.saveFTableUserPreferences) return;
|
|
2191
|
+
|
|
2192
|
+
const stateJson = this.userPrefs.get('table-state');
|
|
2193
|
+
if (!stateJson) return;
|
|
2194
|
+
|
|
2195
|
+
try {
|
|
2196
|
+
const state = JSON.parse(stateJson);
|
|
2197
|
+
if (Array.isArray(state.sorting)) {
|
|
2198
|
+
this.state.sorting = state.sorting;
|
|
2199
|
+
}
|
|
2200
|
+
if (state.pageSize) {
|
|
2201
|
+
this.state.pageSize = state.pageSize;
|
|
2202
|
+
}
|
|
2203
|
+
} catch (error) {
|
|
2204
|
+
this.logger.warn('Failed to load table state:', error);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
createTableBody() {
|
|
2209
|
+
this.elements.tableBody = FTableDOMHelper.create('tbody', {
|
|
2210
|
+
parent: this.elements.table
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
addNoDataRow() {
|
|
2215
|
+
if (this.elements.tableBody.querySelector('.ftable-no-data-row')) return;
|
|
2216
|
+
|
|
2217
|
+
const row = FTableDOMHelper.create('tr', {
|
|
2218
|
+
className: 'ftable-no-data-row',
|
|
2219
|
+
parent: this.elements.tableBody
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
const colCount = this.elements.table.querySelector('thead tr').children.length;
|
|
2223
|
+
FTableDOMHelper.create('td', {
|
|
2224
|
+
attributes: { colspan: colCount },
|
|
2225
|
+
text: this.options.messages.noDataAvailable,
|
|
2226
|
+
parent: row
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
removeNoDataRow() {
|
|
2231
|
+
const noDataRow = this.elements.tableBody.querySelector('.ftable-no-data-row');
|
|
2232
|
+
if (noDataRow) noDataRow.remove();
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
createModals() {
|
|
2236
|
+
// Create modals for different operations
|
|
2237
|
+
if (this.options.actions.createAction) {
|
|
2238
|
+
this.createAddRecordModal();
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
if (this.options.actions.updateAction) {
|
|
2242
|
+
this.createEditRecordModal();
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
if (this.options.actions.deleteAction) {
|
|
2246
|
+
this.createDeleteConfirmModal();
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
this.createErrorModal();
|
|
2250
|
+
this.createInfoModal();
|
|
2251
|
+
this.createLoadingModal();
|
|
2252
|
+
|
|
2253
|
+
// Initialize them (create DOM) once
|
|
2254
|
+
Object.values(this.modals).forEach(modal => modal.create());
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
createAddRecordModal() {
|
|
2258
|
+
this.modals.addRecord = new JtableModal({
|
|
2259
|
+
parent: this.elements.mainContainer,
|
|
2260
|
+
title: this.options.messages.addNewRecord,
|
|
2261
|
+
className: 'ftable-add-modal',
|
|
2262
|
+
buttons: [
|
|
2263
|
+
{
|
|
2264
|
+
text: this.options.messages.cancel,
|
|
2265
|
+
className: 'ftable-dialog-cancelbutton',
|
|
2266
|
+
onClick: () => {
|
|
2267
|
+
this.modals.addRecord.close();
|
|
2268
|
+
this.emit('formClosed', { form: this.currentForm, formType: 'create', record: null });
|
|
2269
|
+
/*if (this.options.formClosed) {
|
|
2270
|
+
this.options.formClosed(this.currentForm, 'create', null);
|
|
2271
|
+
}*/
|
|
2272
|
+
}
|
|
2273
|
+
},
|
|
2274
|
+
{
|
|
2275
|
+
text: this.options.messages.save,
|
|
2276
|
+
className: 'ftable-dialog-savebutton',
|
|
2277
|
+
onClick: () => this.saveNewRecord()
|
|
2278
|
+
}
|
|
2279
|
+
]
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
createEditRecordModal() {
|
|
2284
|
+
this.modals.editRecord = new JtableModal({
|
|
2285
|
+
parent: this.elements.mainContainer,
|
|
2286
|
+
title: this.options.messages.editRecord,
|
|
2287
|
+
className: 'ftable-edit-modal',
|
|
2288
|
+
buttons: [
|
|
2289
|
+
{
|
|
2290
|
+
text: this.options.messages.cancel,
|
|
2291
|
+
className: 'ftable-dialog-cancelbutton',
|
|
2292
|
+
onClick: () => {
|
|
2293
|
+
this.emit('formClosed', { form: this.currentForm, formType: 'edit', record: null });
|
|
2294
|
+
this.modals.editRecord.close();
|
|
2295
|
+
}
|
|
2296
|
+
},
|
|
2297
|
+
{
|
|
2298
|
+
text: this.options.messages.save,
|
|
2299
|
+
className: 'ftable-dialog-savebutton',
|
|
2300
|
+
onClick: () => this.saveEditedRecord()
|
|
2301
|
+
}
|
|
2302
|
+
]
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
createDeleteConfirmModal() {
|
|
2307
|
+
this.modals.deleteConfirm = new JtableModal({
|
|
2308
|
+
parent: this.elements.mainContainer,
|
|
2309
|
+
title: this.options.messages.areYouSure,
|
|
2310
|
+
className: 'ftable-delete-modal',
|
|
2311
|
+
buttons: [
|
|
2312
|
+
{
|
|
2313
|
+
text: this.options.messages.cancel,
|
|
2314
|
+
className: 'ftable-dialog-cancelbutton',
|
|
2315
|
+
onClick: () => this.modals.deleteConfirm.close()
|
|
2316
|
+
},
|
|
2317
|
+
{
|
|
2318
|
+
text: this.options.messages.deleteText,
|
|
2319
|
+
className: 'ftable-dialog-deletebutton',
|
|
2320
|
+
onClick: () => this.confirmDelete()
|
|
2321
|
+
}
|
|
2322
|
+
]
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
createErrorModal() {
|
|
2327
|
+
this.modals.error = new JtableModal({
|
|
2328
|
+
parent: this.elements.mainContainer,
|
|
2329
|
+
title: this.options.messages.error,
|
|
2330
|
+
className: 'ftable-error-modal',
|
|
2331
|
+
buttons: [
|
|
2332
|
+
{
|
|
2333
|
+
text: this.options.messages.close,
|
|
2334
|
+
className: 'ftable-dialog-closebutton',
|
|
2335
|
+
onClick: () => this.modals.error.close()
|
|
2336
|
+
}
|
|
2337
|
+
]
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
createInfoModal() {
|
|
2342
|
+
this.modals.info = new JtableModal({
|
|
2343
|
+
parent: this.elements.mainContainer,
|
|
2344
|
+
title: this.options.messages.error,
|
|
2345
|
+
className: 'ftable-info-modal',
|
|
2346
|
+
buttons: [
|
|
2347
|
+
{
|
|
2348
|
+
text: this.options.messages.close,
|
|
2349
|
+
className: 'ftable-dialog-closebutton',
|
|
2350
|
+
onClick: () => this.modals.info.close()
|
|
2351
|
+
}
|
|
2352
|
+
]
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
createLoadingModal() {
|
|
2357
|
+
this.modals.loading = new JtableModal({
|
|
2358
|
+
parent: this.elements.mainContainer,
|
|
2359
|
+
title: '',
|
|
2360
|
+
className: 'ftable-loading-modal',
|
|
2361
|
+
content: `<div class="ftable-loading-message">${this.options.messages.loadingMessage}</div>`
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
bindEvents() {
|
|
2366
|
+
// Subscribe all event handlers from options
|
|
2367
|
+
this.subscribeOptionEvents();
|
|
2368
|
+
|
|
2369
|
+
// Add toolbar buttons
|
|
2370
|
+
this.createToolbarButtons();
|
|
2371
|
+
this.createCustomToolbarItems();
|
|
2372
|
+
|
|
2373
|
+
// Handle window unload
|
|
2374
|
+
this.handlePageUnload();
|
|
2375
|
+
|
|
2376
|
+
// Keyboard shortcuts
|
|
2377
|
+
this.bindKeyboardEvents();
|
|
2378
|
+
|
|
2379
|
+
// Column selection context menu
|
|
2380
|
+
if (this.options.columnSelectable !== false) {
|
|
2381
|
+
this.createColumnSelectionMenu();
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
subscribeOptionEvents() {
|
|
2386
|
+
const events = [
|
|
2387
|
+
//'closeRequested', NOT EMITTED
|
|
2388
|
+
'formCreated',
|
|
2389
|
+
// 'formSubmitting', NOT EMITTED
|
|
2390
|
+
'formClosed',
|
|
2391
|
+
//'loadingRecords', NOT EMITTED
|
|
2392
|
+
'recordsLoaded', // { records: data.Records, serverResponse: data }
|
|
2393
|
+
// 'rowInserted', NOT EMITTED
|
|
2394
|
+
// 'rowsRemoved', NOT EMITTED
|
|
2395
|
+
'recordAdded', // { record: result.Record }
|
|
2396
|
+
// 'rowUpdated', NOT EMITTED
|
|
2397
|
+
'recordUpdated', // { record: result.Record || formData }
|
|
2398
|
+
'recordDeleted', // { record: this.currentDeletingRow.recordData }
|
|
2399
|
+
'selectionChanged', // { selectedRows: this.getSelectedRows() }
|
|
2400
|
+
//'bulkDelete', // NOT USEFULL { results: results, successful: successfulDeletes.length, failed: failed }
|
|
2401
|
+
//'columnVisibilityChanged', // NOT USEFULL { field: field }
|
|
2402
|
+
];
|
|
2403
|
+
|
|
2404
|
+
events.forEach(event => {
|
|
2405
|
+
if (typeof this.options[event] === 'function') {
|
|
2406
|
+
this.on(event, this.options[event]);
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
createColumnSelectionMenu() {
|
|
2412
|
+
// Create column selection overlay and menu
|
|
2413
|
+
this.elements.columnSelectionOverlay = null;
|
|
2414
|
+
this.elements.columnSelectionMenu = null;
|
|
2415
|
+
|
|
2416
|
+
// Bind right-click event to table header
|
|
2417
|
+
const thead = this.elements.table.querySelector('thead');
|
|
2418
|
+
thead.addEventListener('contextmenu', (e) => {
|
|
2419
|
+
e.preventDefault();
|
|
2420
|
+
this.showColumnSelectionMenu(e);
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
showColumnSelectionMenu(e) {
|
|
2425
|
+
// Remove existing menu if any
|
|
2426
|
+
this.hideColumnSelectionMenu();
|
|
2427
|
+
|
|
2428
|
+
// Create overlay to capture clicks outside menu
|
|
2429
|
+
this.elements.columnSelectionOverlay = FTableDOMHelper.create('div', {
|
|
2430
|
+
className: 'ftable-contextmenu-overlay',
|
|
2431
|
+
parent: this.elements.mainContainer
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
// Create the menu
|
|
2435
|
+
this.elements.columnSelectionMenu = FTableDOMHelper.create('div', {
|
|
2436
|
+
className: 'ftable-column-selection-container',
|
|
2437
|
+
parent: this.elements.columnSelectionOverlay
|
|
2438
|
+
});
|
|
2439
|
+
|
|
2440
|
+
// Populate menu with column options
|
|
2441
|
+
this.populateColumnSelectionMenu();
|
|
2442
|
+
|
|
2443
|
+
// Position the menu
|
|
2444
|
+
this.positionColumnSelectionMenu(e);
|
|
2445
|
+
|
|
2446
|
+
// Add event listeners
|
|
2447
|
+
this.elements.columnSelectionOverlay.addEventListener('click', (event) => {
|
|
2448
|
+
if (event.target === this.elements.columnSelectionOverlay) {
|
|
2449
|
+
this.hideColumnSelectionMenu();
|
|
2450
|
+
}
|
|
2451
|
+
});
|
|
2452
|
+
|
|
2453
|
+
this.elements.columnSelectionOverlay.addEventListener('contextmenu', (event) => {
|
|
2454
|
+
event.preventDefault();
|
|
2455
|
+
this.hideColumnSelectionMenu();
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
populateColumnSelectionMenu() {
|
|
2460
|
+
const menuList = FTableDOMHelper.create('ul', {
|
|
2461
|
+
className: 'ftable-column-select-list',
|
|
2462
|
+
parent: this.elements.columnSelectionMenu
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2465
|
+
this.columnList.forEach(fieldName => {
|
|
2466
|
+
const field = this.options.fields[fieldName];
|
|
2467
|
+
const isVisible = field.visibility !== 'hidden';
|
|
2468
|
+
const isFixed = field.visibility === 'fixed';
|
|
2469
|
+
const isSorted = this.isFieldSorted(fieldName);
|
|
2470
|
+
|
|
2471
|
+
const listItem = FTableDOMHelper.create('li', {
|
|
2472
|
+
className: 'ftable-column-select-item',
|
|
2473
|
+
parent: menuList
|
|
2474
|
+
});
|
|
2475
|
+
|
|
2476
|
+
const label = FTableDOMHelper.create('label', {
|
|
2477
|
+
className: 'ftable-column-select-label',
|
|
2478
|
+
parent: listItem
|
|
2479
|
+
});
|
|
2480
|
+
|
|
2481
|
+
const checkbox = FTableDOMHelper.create('input', {
|
|
2482
|
+
attributes: {
|
|
2483
|
+
type: 'checkbox',
|
|
2484
|
+
id: `column-${fieldName}`
|
|
2485
|
+
},
|
|
2486
|
+
parent: label
|
|
2487
|
+
});
|
|
2488
|
+
|
|
2489
|
+
checkbox.checked = isVisible;
|
|
2490
|
+
|
|
2491
|
+
// Disable checkbox if column is fixed or currently sorted
|
|
2492
|
+
if (isFixed || (isSorted && isVisible)) {
|
|
2493
|
+
checkbox.disabled = true;
|
|
2494
|
+
listItem.style.opacity = '0.6';
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
const labelText = FTableDOMHelper.create('span', {
|
|
2498
|
+
text: field.title || fieldName,
|
|
2499
|
+
parent: label
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
// Add sorted indicator
|
|
2503
|
+
if (isSorted) {
|
|
2504
|
+
const sortIndicator = FTableDOMHelper.create('span', {
|
|
2505
|
+
className: 'ftable-sort-indicator',
|
|
2506
|
+
text: ' (sorted)',
|
|
2507
|
+
parent: labelText
|
|
2508
|
+
});
|
|
2509
|
+
sortIndicator.style.fontSize = '0.8em';
|
|
2510
|
+
sortIndicator.style.color = '#666';
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// Handle checkbox change
|
|
2514
|
+
if (!checkbox.disabled) {
|
|
2515
|
+
checkbox.addEventListener('change', () => {
|
|
2516
|
+
this.setColumnVisibility(fieldName, checkbox.checked);
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
positionColumnSelectionMenu(e) {
|
|
2523
|
+
const containerRect = this.elements.mainContainer.getBoundingClientRect();
|
|
2524
|
+
const menuWidth = 200; // Approximate menu width
|
|
2525
|
+
const menuHeight = this.columnList.length * 30 + 20; // Approximate height
|
|
2526
|
+
|
|
2527
|
+
let left = e.clientX - containerRect.left;
|
|
2528
|
+
let top = e.clientY - containerRect.top;
|
|
2529
|
+
|
|
2530
|
+
// Adjust position to keep menu within container bounds
|
|
2531
|
+
if (left + menuWidth > containerRect.width) {
|
|
2532
|
+
left = Math.max(0, containerRect.width - menuWidth);
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
if (top + menuHeight > containerRect.height) {
|
|
2536
|
+
top = Math.max(0, containerRect.height - menuHeight);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
this.elements.columnSelectionMenu.style.left = left + 'px';
|
|
2540
|
+
this.elements.columnSelectionMenu.style.top = top + 'px';
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
hideColumnSelectionMenu() {
|
|
2544
|
+
if (this.elements.columnSelectionOverlay) {
|
|
2545
|
+
this.elements.columnSelectionOverlay.remove();
|
|
2546
|
+
this.elements.columnSelectionOverlay = null;
|
|
2547
|
+
this.elements.columnSelectionMenu = null;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
isFieldSorted(fieldName) {
|
|
2552
|
+
return this.state.sorting.some(sort => sort.fieldName === fieldName);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
createToolbarButtons() {
|
|
2556
|
+
// CSV Export Button
|
|
2557
|
+
if (this.options.csvExport) {
|
|
2558
|
+
this.addToolbarButton({
|
|
2559
|
+
text: this.options.messages.csvExport,
|
|
2560
|
+
className: 'ftable-toolbar-item-csv',
|
|
2561
|
+
onClick: () => {
|
|
2562
|
+
const filename = this.options.title
|
|
2563
|
+
? `${this.options.title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}.csv`
|
|
2564
|
+
: 'table-export.csv';
|
|
2565
|
+
this.exportToCSV(filename);
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
// Print Button
|
|
2570
|
+
if (this.options.printTable) {
|
|
2571
|
+
this.addToolbarButton({
|
|
2572
|
+
text: this.options.messages.printTable,
|
|
2573
|
+
className: 'ftable-toolbar-item-print',
|
|
2574
|
+
onClick: () => {
|
|
2575
|
+
this.printTable();
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
if (this.options.actions.createAction) {
|
|
2581
|
+
this.addToolbarButton({
|
|
2582
|
+
text: this.options.messages.addNewRecord,
|
|
2583
|
+
className: 'ftable-toolbar-item-add-record',
|
|
2584
|
+
addIconSpan: true,
|
|
2585
|
+
onClick: () => this.showAddRecordForm()
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
addToolbarButton(options) {
|
|
2591
|
+
const button = FTableDOMHelper.create('span', {
|
|
2592
|
+
className: `ftable-toolbar-item ${options.className || ''}`,
|
|
2593
|
+
parent: this.elements.toolbarDiv
|
|
2594
|
+
});
|
|
2595
|
+
if (options.addIconSpan) {
|
|
2596
|
+
// just the span, the rest is CSS here
|
|
2597
|
+
const buttonText = FTableDOMHelper.create('span', {
|
|
2598
|
+
className: `ftable-toolbar-item-icon ${options.className || ''}`,
|
|
2599
|
+
parent: button
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
const buttonText = FTableDOMHelper.create('span', {
|
|
2603
|
+
className: `ftable-toolbar-item-text ${options.className || ''}`,
|
|
2604
|
+
text: options.text,
|
|
2605
|
+
parent: button
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
if (options.onClick) {
|
|
2609
|
+
button.addEventListener('click', options.onClick);
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
return button;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
createCustomToolbarItems() {
|
|
2616
|
+
if (!this.options.toolbar || !this.options.toolbar.items) return;
|
|
2617
|
+
|
|
2618
|
+
this.options.toolbar.items.forEach(item => {
|
|
2619
|
+
const button = FTableDOMHelper.create('button', {
|
|
2620
|
+
className: `ftable-toolbar-item ftable-toolbar-item-custom ${item.buttonClass || ''}`,
|
|
2621
|
+
parent: this.elements.toolbarDiv
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
// Add icon if provided
|
|
2625
|
+
if (item.icon) {
|
|
2626
|
+
const img = FTableDOMHelper.create('img', {
|
|
2627
|
+
attributes: {
|
|
2628
|
+
src: item.icon,
|
|
2629
|
+
alt: '',
|
|
2630
|
+
width: 16,
|
|
2631
|
+
height: 16,
|
|
2632
|
+
style: 'margin-right: 6px; vertical-align: middle;'
|
|
2633
|
+
},
|
|
2634
|
+
parent: button
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// Add text
|
|
2639
|
+
if (item.text) {
|
|
2640
|
+
FTableDOMHelper.create('span', {
|
|
2641
|
+
text: item.text,
|
|
2642
|
+
parent: button
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// Attach click handler
|
|
2647
|
+
if (typeof item.click === 'function') {
|
|
2648
|
+
button.addEventListener('click', (e) => {
|
|
2649
|
+
e.preventDefault();
|
|
2650
|
+
e.stopPropagation();
|
|
2651
|
+
item.click();
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
handlePageUnload() {
|
|
2658
|
+
let unloadingPage = false;
|
|
2659
|
+
|
|
2660
|
+
window.addEventListener('beforeunload', () => {
|
|
2661
|
+
unloadingPage = true;
|
|
2662
|
+
});
|
|
2663
|
+
|
|
2664
|
+
window.addEventListener('unload', () => {
|
|
2665
|
+
unloadingPage = false;
|
|
2666
|
+
});
|
|
2667
|
+
|
|
2668
|
+
this.unloadingPage = () => unloadingPage;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
bindKeyboardEvents() {
|
|
2672
|
+
if (this.options.selecting) {
|
|
2673
|
+
this.shiftKeyDown = false;
|
|
2674
|
+
|
|
2675
|
+
document.addEventListener('keydown', (e) => {
|
|
2676
|
+
if (e.key === 'Shift') this.shiftKeyDown = true;
|
|
2677
|
+
});
|
|
2678
|
+
|
|
2679
|
+
document.addEventListener('keyup', (e) => {
|
|
2680
|
+
if (e.key === 'Shift') this.shiftKeyDown = false;
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
setupFTableUserPreferences() {
|
|
2686
|
+
if (this.options.saveFTableUserPreferences) {
|
|
2687
|
+
const prefix = this.userPrefs.generatePrefix(
|
|
2688
|
+
this.options.tableId || '',
|
|
2689
|
+
this.fieldList
|
|
2690
|
+
);
|
|
2691
|
+
this.userPrefs = new FTableUserPreferences(prefix, this.options.saveFTableUserPreferencesMethod);
|
|
2692
|
+
|
|
2693
|
+
// Load saved column settings
|
|
2694
|
+
this.loadState();
|
|
2695
|
+
this.loadColumnSettings();
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
async load(queryParams = {}) {
|
|
2700
|
+
if (this.state.isLoading) return;
|
|
2701
|
+
|
|
2702
|
+
this.state.isLoading = true;
|
|
2703
|
+
this.showLoadingIndicator();
|
|
2704
|
+
|
|
2705
|
+
|
|
2706
|
+
try {
|
|
2707
|
+
const params = {
|
|
2708
|
+
...queryParams,
|
|
2709
|
+
...this.buildLoadParams()
|
|
2710
|
+
};
|
|
2711
|
+
|
|
2712
|
+
const data = await this.performLoad(params);
|
|
2713
|
+
this.processLoadedData(data);
|
|
2714
|
+
this.emit('recordsLoaded', { records: data.Records, serverResponse: data });
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
this.showError(this.options.messages.serverCommunicationError);
|
|
2717
|
+
this.logger.error(`Load failed: ${error.message}`);
|
|
2718
|
+
} finally {
|
|
2719
|
+
this.state.isLoading = false;
|
|
2720
|
+
this.hideLoadingIndicator();
|
|
2721
|
+
}
|
|
2722
|
+
// Update sorting display
|
|
2723
|
+
this.renderSortingInfo();
|
|
2724
|
+
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
buildLoadParams() {
|
|
2728
|
+
const params = {};
|
|
2729
|
+
|
|
2730
|
+
if (this.options.paging) {
|
|
2731
|
+
if (!this.state.pageSize) {
|
|
2732
|
+
this.state.pageSize = this.options.pageSize;
|
|
2733
|
+
}
|
|
2734
|
+
params.jtStartIndex = (this.state.currentPage - 1) * this.state.pageSize;
|
|
2735
|
+
params.jtPageSize = this.state.pageSize;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
if (this.options.sorting) {
|
|
2739
|
+
if (this.state.sorting.length > 0) {
|
|
2740
|
+
params.jtSorting = this.state.sorting
|
|
2741
|
+
.map(sort => `${sort.fieldName} ${sort.direction}`)
|
|
2742
|
+
.join(', ');
|
|
2743
|
+
} else if (this.options.defaultSorting) {
|
|
2744
|
+
params.jtSorting = this.parseDefaultSorting(this.options.defaultSorting)
|
|
2745
|
+
.map(sort => `${sort.fieldName} ${sort.direction}`)
|
|
2746
|
+
.join(', ');
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
if (this.options.toolbarsearch && Object.keys(this.state.searchQueries).length > 0) {
|
|
2750
|
+
const queries = [];
|
|
2751
|
+
const searchFields = [];
|
|
2752
|
+
|
|
2753
|
+
Object.entries(this.state.searchQueries).forEach(([fieldName, query]) => {
|
|
2754
|
+
if (query !== '') { // Double check it's not empty
|
|
2755
|
+
queries.push(query);
|
|
2756
|
+
searchFields.push(fieldName);
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
|
|
2760
|
+
if (queries.length > 0) {
|
|
2761
|
+
params['q[]'] = queries;
|
|
2762
|
+
params['opt[]'] = searchFields;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// support listQueryParams
|
|
2767
|
+
if (typeof this.options.listQueryParams === 'function') {
|
|
2768
|
+
const customParams = this.options.listQueryParams();
|
|
2769
|
+
Object.assign(params, customParams);
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
return params;
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
isCacheExpired(cacheEntry, cacheDuration) {
|
|
2776
|
+
if (!cacheEntry || !cacheEntry.timestamp) return true;
|
|
2777
|
+
const age = Date.now() - cacheEntry.timestamp;
|
|
2778
|
+
return age > cacheDuration;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
async performLoad(params) {
|
|
2782
|
+
const listAction = this.options.actions.listAction;
|
|
2783
|
+
|
|
2784
|
+
// Check if caching is enabled
|
|
2785
|
+
if (this.options.listCache && typeof listAction === 'string') {
|
|
2786
|
+
// Try to get from cache
|
|
2787
|
+
const cached = this.formBuilder.optionsCache.get(listAction, params);
|
|
2788
|
+
if (cached && !this.isCacheExpired(cached, this.options.listCache)) {
|
|
2789
|
+
return cached.data;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
let data;
|
|
2794
|
+
if (typeof listAction === 'function') {
|
|
2795
|
+
data = await listAction(params);
|
|
2796
|
+
} else if (typeof listAction === 'string') {
|
|
2797
|
+
data = await FTableHttpClient.get(listAction, params);
|
|
2798
|
+
} else {
|
|
2799
|
+
throw new Error('No valid listAction provided');
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// Validate response
|
|
2803
|
+
if (!data || data.Result !== 'OK') {
|
|
2804
|
+
throw new Error(data?.Message || 'Invalid response from server');
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// Cache if enabled
|
|
2808
|
+
if (this.options.listCache && typeof listAction === 'string') {
|
|
2809
|
+
this.formBuilder.optionsCache.set(listAction, params, {
|
|
2810
|
+
data: data,
|
|
2811
|
+
timestamp: Date.now()
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
return data;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
processLoadedData(data) {
|
|
2819
|
+
if (data.Result !== 'OK') {
|
|
2820
|
+
this.showError(data.Message || 'Unknown error occurred');
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
this.state.records = data.Records || [];
|
|
2825
|
+
this.state.totalRecordCount = data.TotalRecordCount || this.state.records.length;
|
|
2826
|
+
|
|
2827
|
+
this.renderTableData();
|
|
2828
|
+
this.updatePagingInfo();
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
renderTableData() {
|
|
2832
|
+
// Clear existing data rows
|
|
2833
|
+
const dataRows = this.elements.tableBody.querySelectorAll('.ftable-data-row');
|
|
2834
|
+
dataRows.forEach(row => row.remove());
|
|
2835
|
+
|
|
2836
|
+
if (this.state.records.length === 0) {
|
|
2837
|
+
this.addNoDataRow();
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
this.removeNoDataRow();
|
|
2842
|
+
|
|
2843
|
+
// Add new rows
|
|
2844
|
+
this.state.records.forEach(record => {
|
|
2845
|
+
const row = this.createTableRow(record);
|
|
2846
|
+
this.elements.tableBody.appendChild(row);
|
|
2847
|
+
});
|
|
2848
|
+
|
|
2849
|
+
this.refreshRowStyles();
|
|
2850
|
+
this.refreshDisplayValues(); // for options that uses functions/url's
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
createTableRow(record) {
|
|
2854
|
+
const row = FTableDOMHelper.create('tr', {
|
|
2855
|
+
className: 'ftable-data-row',
|
|
2856
|
+
attributes: { 'data-record-key': this.getKeyValue(record) }
|
|
2857
|
+
});
|
|
2858
|
+
|
|
2859
|
+
// Store record data
|
|
2860
|
+
row.recordData = record;
|
|
2861
|
+
|
|
2862
|
+
// Add selecting checkbox if enabled
|
|
2863
|
+
if (this.options.selecting) {
|
|
2864
|
+
this.addSelectingCell(row);
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// Add data cells
|
|
2868
|
+
this.columnList.forEach(fieldName => {
|
|
2869
|
+
this.addDataCell(row, record, fieldName);
|
|
2870
|
+
});
|
|
2871
|
+
|
|
2872
|
+
// Add action cells
|
|
2873
|
+
let action_count = 0;
|
|
2874
|
+
if (this.options.actions.updateAction) {
|
|
2875
|
+
this.addEditCell(row);
|
|
2876
|
+
action_count++;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
if (this.options.actions.cloneAction) {
|
|
2880
|
+
this.addCloneCell(row);
|
|
2881
|
+
action_count++;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
if (this.options.actions.deleteAction) {
|
|
2885
|
+
this.addDeleteCell(row);
|
|
2886
|
+
action_count++;
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// Make row selectable if enabled
|
|
2890
|
+
if (this.options.selecting) {
|
|
2891
|
+
this.makeRowSelectable(row);
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
return row;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
addSelectingCell(row) {
|
|
2898
|
+
const cell = FTableDOMHelper.create('td', {
|
|
2899
|
+
className: 'ftable-command-column ftable-selecting-column',
|
|
2900
|
+
parent: row
|
|
2901
|
+
});
|
|
2902
|
+
|
|
2903
|
+
const checkbox = FTableDOMHelper.create('input', {
|
|
2904
|
+
attributes: { type: 'checkbox' },
|
|
2905
|
+
parent: cell
|
|
2906
|
+
});
|
|
2907
|
+
|
|
2908
|
+
checkbox.addEventListener('change', () => {
|
|
2909
|
+
this.toggleRowSelection(row);
|
|
2910
|
+
});
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
addDataCell(row, record, fieldName) {
|
|
2914
|
+
const field = this.options.fields[fieldName];
|
|
2915
|
+
const value = this.getDisplayText(record, fieldName);
|
|
2916
|
+
|
|
2917
|
+
const cell = FTableDOMHelper.create('td', {
|
|
2918
|
+
className: `${field.listClass || ''} ${field.listClassEntry || ''}`,
|
|
2919
|
+
html: field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value,
|
|
2920
|
+
attributes: { 'data-field-name': fieldName },
|
|
2921
|
+
parent: row
|
|
2922
|
+
});
|
|
2923
|
+
if (field.visibility === 'fixed') {
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
if (field.visibility === 'hidden') {
|
|
2927
|
+
FTableDOMHelper.hide(cell);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
addEditCell(row) {
|
|
2932
|
+
const cell = FTableDOMHelper.create('td', {
|
|
2933
|
+
className: 'ftable-command-column',
|
|
2934
|
+
parent: row
|
|
2935
|
+
});
|
|
2936
|
+
|
|
2937
|
+
const button = FTableDOMHelper.create('button', {
|
|
2938
|
+
className: 'ftable-command-button ftable-edit-command-button',
|
|
2939
|
+
attributes: { title: this.options.messages.editRecord },
|
|
2940
|
+
html: `<span>${this.options.messages.editRecord}</span>`,
|
|
2941
|
+
parent: cell
|
|
2942
|
+
});
|
|
2943
|
+
|
|
2944
|
+
button.addEventListener('click', (e) => {
|
|
2945
|
+
e.preventDefault();
|
|
2946
|
+
e.stopPropagation();
|
|
2947
|
+
this.editRecord(row);
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
addCloneCell(row) {
|
|
2952
|
+
const cell = FTableDOMHelper.create('td', {
|
|
2953
|
+
className: 'ftable-command-column',
|
|
2954
|
+
parent: row
|
|
2955
|
+
});
|
|
2956
|
+
const button = FTableDOMHelper.create('button', {
|
|
2957
|
+
className: 'ftable-command-button ftable-clone-command-button',
|
|
2958
|
+
attributes: { title: this.options.messages.cloneRecord || 'Clone' },
|
|
2959
|
+
html: `<span>${this.options.messages.cloneRecord || 'Clone'}</span>`,
|
|
2960
|
+
parent: cell
|
|
2961
|
+
});
|
|
2962
|
+
button.addEventListener('click', (e) => {
|
|
2963
|
+
e.preventDefault();
|
|
2964
|
+
e.stopPropagation();
|
|
2965
|
+
this.cloneRecord(row);
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
addDeleteCell(row) {
|
|
2970
|
+
const cell = FTableDOMHelper.create('td', {
|
|
2971
|
+
className: 'ftable-command-column',
|
|
2972
|
+
parent: row
|
|
2973
|
+
});
|
|
2974
|
+
|
|
2975
|
+
const button = FTableDOMHelper.create('button', {
|
|
2976
|
+
className: 'ftable-command-button ftable-delete-command-button',
|
|
2977
|
+
attributes: { title: this.options.messages.deleteText },
|
|
2978
|
+
html: `<span>${this.options.messages.deleteText}</span>`,
|
|
2979
|
+
parent: cell
|
|
2980
|
+
});
|
|
2981
|
+
|
|
2982
|
+
button.addEventListener('click', (e) => {
|
|
2983
|
+
e.preventDefault();
|
|
2984
|
+
e.stopPropagation();
|
|
2985
|
+
this.deleteRecord(row);
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
getDisplayText(record, fieldName) {
|
|
2990
|
+
const field = this.options.fields[fieldName];
|
|
2991
|
+
const value = record[fieldName];
|
|
2992
|
+
|
|
2993
|
+
if (field.display && typeof field.display === 'function') {
|
|
2994
|
+
return field.display({ record });
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
if (field.type === 'date' && value) {
|
|
2998
|
+
return this.formatDate(value, field.dateFormat);
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
if (field.type === 'checkbox') {
|
|
3002
|
+
return this.getCheckboxText(fieldName, value);
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
if (field.options) {
|
|
3006
|
+
const option = this.findOptionByValue(field.options, value);
|
|
3007
|
+
return option ? option.DisplayText || option.text || option : value;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
return value || '';
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
formatDate(dateValue, format) {
|
|
3014
|
+
if (!dateValue) return '';
|
|
3015
|
+
|
|
3016
|
+
const date = new Date(dateValue);
|
|
3017
|
+
if (isNaN(date.getTime())) return dateValue;
|
|
3018
|
+
|
|
3019
|
+
// Simple date formatting - could be enhanced with a proper date library
|
|
3020
|
+
const year = date.getFullYear();
|
|
3021
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
3022
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
3023
|
+
|
|
3024
|
+
return `${year}-${month}-${day}`;
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
getCheckboxText(fieldName, value) {
|
|
3028
|
+
const field = this.options.fields[fieldName];
|
|
3029
|
+
if (field.values && field.values[value]) {
|
|
3030
|
+
return field.values[value];
|
|
3031
|
+
}
|
|
3032
|
+
return value ? 'Yes' : 'No';
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
findOptionByValue(options, value) {
|
|
3036
|
+
if (Array.isArray(options)) {
|
|
3037
|
+
return options.find(opt =>
|
|
3038
|
+
(opt.Value || opt.value) === value || opt === value
|
|
3039
|
+
);
|
|
3040
|
+
}
|
|
3041
|
+
return null;
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
refreshRowStyles() {
|
|
3045
|
+
const rows = this.elements.tableBody.querySelectorAll('.ftable-data-row');
|
|
3046
|
+
rows.forEach((row, index) => {
|
|
3047
|
+
if (index % 2 === 0) {
|
|
3048
|
+
FTableDOMHelper.addClass(row, 'ftable-row-even');
|
|
3049
|
+
} else {
|
|
3050
|
+
FTableDOMHelper.removeClass(row, 'ftable-row-even');
|
|
3051
|
+
}
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
getKeyValue(record) {
|
|
3056
|
+
return this.keyField ? record[this.keyField] : null;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// CRUD Operations
|
|
3060
|
+
async showAddRecordForm() {
|
|
3061
|
+
const form = await this.formBuilder.createForm('create');
|
|
3062
|
+
this.modals.addRecord.setContent(form);
|
|
3063
|
+
this.modals.addRecord.show();
|
|
3064
|
+
this.currentForm = form;
|
|
3065
|
+
this.emit('formCreated', { form: form, formType: 'create', record: null });
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
async saveNewRecord() {
|
|
3069
|
+
if (!this.currentForm) return;
|
|
3070
|
+
|
|
3071
|
+
// Check validity
|
|
3072
|
+
if (!this.currentForm.checkValidity()) {
|
|
3073
|
+
// Triggers browser to show native validation messages
|
|
3074
|
+
this.currentForm.reportValidity();
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
const formData = this.getFormData(this.currentForm);
|
|
3079
|
+
|
|
3080
|
+
try {
|
|
3081
|
+
const result = await this.performCreate(formData);
|
|
3082
|
+
|
|
3083
|
+
if (result.Result === 'OK') {
|
|
3084
|
+
this.clearListCache();
|
|
3085
|
+
this.modals.addRecord.close();
|
|
3086
|
+
|
|
3087
|
+
// Call formClosed
|
|
3088
|
+
// this.emit('formClosed', { form: this.currentForm, formType: 'create', record: null });
|
|
3089
|
+
if (this.options.formClosed) {
|
|
3090
|
+
this.options.formClosed(this.currentForm, 'create', null);
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
if (result.Message) {
|
|
3094
|
+
this.showInfo(result.Message);
|
|
3095
|
+
}
|
|
3096
|
+
await this.load(); // Reload to show new record
|
|
3097
|
+
this.emit('recordAdded', { record: result.Record });
|
|
3098
|
+
} else {
|
|
3099
|
+
this.showError(result.Message || 'Create failed');
|
|
3100
|
+
}
|
|
3101
|
+
} catch (error) {
|
|
3102
|
+
this.showError(this.options.messages.serverCommunicationError);
|
|
3103
|
+
this.logger.error(`Create failed: ${error.message}`);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
async editRecord(row) {
|
|
3108
|
+
const record = row.recordData;
|
|
3109
|
+
const form = await this.formBuilder.createForm('edit', record);
|
|
3110
|
+
|
|
3111
|
+
this.modals.editRecord.setContent(form);
|
|
3112
|
+
this.modals.editRecord.show();
|
|
3113
|
+
this.currentForm = form;
|
|
3114
|
+
this.currentEditingRow = row;
|
|
3115
|
+
this.emit('formCreated', { form: form, formType: 'edit', record: record });
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
async saveEditedRecord() {
|
|
3119
|
+
if (!this.currentForm || !this.currentEditingRow) return;
|
|
3120
|
+
|
|
3121
|
+
// Check validity
|
|
3122
|
+
if (!this.currentForm.checkValidity()) {
|
|
3123
|
+
// Triggers browser to show native validation messages
|
|
3124
|
+
this.currentForm.reportValidity();
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
const formData = this.getFormData(this.currentForm);
|
|
3129
|
+
|
|
3130
|
+
try {
|
|
3131
|
+
const result = await this.performUpdate(formData);
|
|
3132
|
+
|
|
3133
|
+
if (result.Result === 'OK') {
|
|
3134
|
+
this.clearListCache();
|
|
3135
|
+
this.modals.editRecord.close();
|
|
3136
|
+
|
|
3137
|
+
// Call formClosed
|
|
3138
|
+
// this.emit('formClosed', { form: this.currentForm, formType: 'edit', record: result.Record || formData });
|
|
3139
|
+
if (this.options.formClosed) {
|
|
3140
|
+
this.options.formClosed(this.currentForm, 'edit', this.currentEditingRow.recordData);
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
// Update the row with new data
|
|
3144
|
+
this.updateRowData(this.currentEditingRow, result.Record || formData);
|
|
3145
|
+
if (result.Message) {
|
|
3146
|
+
this.showInfo(result.Message);
|
|
3147
|
+
}
|
|
3148
|
+
this.emit('recordUpdated', { record: result.Record || formData });
|
|
3149
|
+
} else {
|
|
3150
|
+
this.showError(result.Message || 'Update failed');
|
|
3151
|
+
}
|
|
3152
|
+
} catch (error) {
|
|
3153
|
+
this.showError(this.options.messages.serverCommunicationError);
|
|
3154
|
+
this.logger.error(`Update failed: ${error.message}`);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
async cloneRecord(row) {
|
|
3159
|
+
const record = { ...row.recordData };
|
|
3160
|
+
|
|
3161
|
+
// Clear key field to allow creation
|
|
3162
|
+
if (this.keyField) {
|
|
3163
|
+
record[this.keyField] = '';
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
const form = await this.formBuilder.createForm('create', record);
|
|
3167
|
+
this.modals.addRecord.options.content = form;
|
|
3168
|
+
this.modals.addRecord.setContent(form);
|
|
3169
|
+
this.modals.addRecord.show();
|
|
3170
|
+
|
|
3171
|
+
this.currentForm = form;
|
|
3172
|
+
this.emit('formCreated', { form: form, formType: 'create', record: record });
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
async deleteRows(keys) {
|
|
3176
|
+
if (!keys.length) return;
|
|
3177
|
+
const confirmMsg = this.options.messages.areYouSure;
|
|
3178
|
+
if (!confirm(confirmMsg)) {
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
const results = [];
|
|
3182
|
+
for (const key of keys) {
|
|
3183
|
+
try {
|
|
3184
|
+
const result = await this.performDelete(key);
|
|
3185
|
+
results.push({ key: key, success: result.Result === 'OK', result: result });
|
|
3186
|
+
} catch (error) {
|
|
3187
|
+
results.push({ key: key, success: false, error: error.message });
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// Remove successful deletions from table
|
|
3192
|
+
const successfulDeletes = results.filter(r => r.success);
|
|
3193
|
+
successfulDeletes.forEach(({ key }) => {
|
|
3194
|
+
const row = this.getRowByKey(key);
|
|
3195
|
+
if (row) this.removeRowFromTable(row);
|
|
3196
|
+
});
|
|
3197
|
+
|
|
3198
|
+
// Show summary
|
|
3199
|
+
const failed = results.filter(r => !r.success).length;
|
|
3200
|
+
if (failed > 0) {
|
|
3201
|
+
this.showError(`${failed} of ${results.length} records could not be deleted`);
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
// Refresh UI state
|
|
3205
|
+
this.refreshRowStyles();
|
|
3206
|
+
this.updatePagingInfo();
|
|
3207
|
+
|
|
3208
|
+
// this.emit('bulkDelete', { results: results, successful: successfulDeletes.length, failed: failed });
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
deleteRecord(row) {
|
|
3212
|
+
const record = row.recordData;
|
|
3213
|
+
let deleteConfirmMessage = this.options.messages.deleteConfirmation; // Default
|
|
3214
|
+
|
|
3215
|
+
// If deleteConfirmation is a function, call it
|
|
3216
|
+
if (typeof this.options.deleteConfirmation === 'function') {
|
|
3217
|
+
const data = {
|
|
3218
|
+
row: row,
|
|
3219
|
+
record: record,
|
|
3220
|
+
deleteConfirmMessage: deleteConfirmMessage,
|
|
3221
|
+
cancel: false,
|
|
3222
|
+
cancelMessage: this.options.messages.cancel
|
|
3223
|
+
};
|
|
3224
|
+
this.options.deleteConfirmation(data);
|
|
3225
|
+
|
|
3226
|
+
// Respect cancellation
|
|
3227
|
+
if (data.cancel) {
|
|
3228
|
+
if (data.cancelMessage) {
|
|
3229
|
+
this.showError(data.cancelMessage);
|
|
3230
|
+
}
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
// Use updated message
|
|
3235
|
+
deleteConfirmMessage = data.deleteConfirmMessage;
|
|
3236
|
+
}
|
|
3237
|
+
this.modals.deleteConfirm.setContent(`<p>${deleteConfirmMessage}</p>`);
|
|
3238
|
+
this.modals.deleteConfirm.show();
|
|
3239
|
+
this.currentDeletingRow = row;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
async confirmDelete() {
|
|
3243
|
+
if (!this.currentDeletingRow) return;
|
|
3244
|
+
|
|
3245
|
+
const keyValue = this.getKeyValue(this.currentDeletingRow.recordData);
|
|
3246
|
+
|
|
3247
|
+
try {
|
|
3248
|
+
const result = await this.performDelete(keyValue);
|
|
3249
|
+
|
|
3250
|
+
if (result.Result === 'OK') {
|
|
3251
|
+
this.clearListCache();
|
|
3252
|
+
this.modals.deleteConfirm.close();
|
|
3253
|
+
this.removeRowFromTable(this.currentDeletingRow);
|
|
3254
|
+
if (result.Message) {
|
|
3255
|
+
this.showInfo(result.Message);
|
|
3256
|
+
}
|
|
3257
|
+
this.emit('recordDeleted', { record: this.currentDeletingRow.recordData });
|
|
3258
|
+
} else {
|
|
3259
|
+
this.showError(result.Message || 'Delete failed');
|
|
3260
|
+
}
|
|
3261
|
+
} catch (error) {
|
|
3262
|
+
this.showError(this.options.messages.serverCommunicationError);
|
|
3263
|
+
this.logger.error(`Delete failed: ${error.message}`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
async performCreate(data) {
|
|
3268
|
+
const createAction = this.options.actions.createAction;
|
|
3269
|
+
|
|
3270
|
+
if (typeof createAction === 'function') {
|
|
3271
|
+
return await createAction(data);
|
|
3272
|
+
} else if (typeof createAction === 'string') {
|
|
3273
|
+
return await FTableHttpClient.post(createAction, data);
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
throw new Error('No valid createAction provided');
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
async performUpdate(data) {
|
|
3280
|
+
const updateAction = this.options.actions.updateAction;
|
|
3281
|
+
|
|
3282
|
+
if (typeof updateAction === 'function') {
|
|
3283
|
+
return await updateAction(data);
|
|
3284
|
+
} else if (typeof updateAction === 'string') {
|
|
3285
|
+
return await FTableHttpClient.post(updateAction, data);
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
throw new Error('No valid updateAction provided');
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
async performDelete(keyValue) {
|
|
3292
|
+
const deleteAction = this.options.actions.deleteAction;
|
|
3293
|
+
const data = { [this.keyField]: keyValue };
|
|
3294
|
+
|
|
3295
|
+
if (typeof deleteAction === 'function') {
|
|
3296
|
+
return await deleteAction(data);
|
|
3297
|
+
} else if (typeof deleteAction === 'string') {
|
|
3298
|
+
return await FTableHttpClient.post(deleteAction, data);
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
throw new Error('No valid deleteAction provided');
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
getFormData(form) {
|
|
3305
|
+
const formData = new FormData(form);
|
|
3306
|
+
const data = {};
|
|
3307
|
+
|
|
3308
|
+
for (const [key, value] of formData.entries()) {
|
|
3309
|
+
if (key.endsWith('[]')) {
|
|
3310
|
+
const baseKey = key.slice(0, -2); // Remove '[]'
|
|
3311
|
+
if (!data[baseKey]) {
|
|
3312
|
+
data[baseKey] = [];
|
|
3313
|
+
}
|
|
3314
|
+
data[baseKey].push(value);
|
|
3315
|
+
} else {
|
|
3316
|
+
// For regular fields, if a key already exists, convert it to array
|
|
3317
|
+
if (data.hasOwnProperty(key)) {
|
|
3318
|
+
if (Array.isArray(data[key])) {
|
|
3319
|
+
data[key].push(value);
|
|
3320
|
+
} else {
|
|
3321
|
+
data[key] = [data[key], value];
|
|
3322
|
+
}
|
|
3323
|
+
} else {
|
|
3324
|
+
data[key] = value;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
return data;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
updateRowData(row, newData) {
|
|
3333
|
+
row.recordData = { ...row.recordData, ...newData };
|
|
3334
|
+
//Object.assign(row.recordData, newData);
|
|
3335
|
+
|
|
3336
|
+
// Update only the fields that were changed
|
|
3337
|
+
Object.keys(newData).forEach(fieldName => {
|
|
3338
|
+
const field = this.options.fields[fieldName];
|
|
3339
|
+
if (!field) return;
|
|
3340
|
+
|
|
3341
|
+
// Find the cell for this field
|
|
3342
|
+
const cell = row.querySelector(`td[data-field-name="${fieldName}"]`);
|
|
3343
|
+
if (!cell) return;
|
|
3344
|
+
|
|
3345
|
+
// Get display text
|
|
3346
|
+
const value = this.getDisplayText(row.recordData, fieldName);
|
|
3347
|
+
cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
|
|
3348
|
+
cell.className = `${field.listClass || ''} ${field.listClassEntry || ''}`.trim();
|
|
3349
|
+
});
|
|
3350
|
+
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
removeRowFromTable(row) {
|
|
3354
|
+
row.remove();
|
|
3355
|
+
|
|
3356
|
+
// Check if we need to show no data row
|
|
3357
|
+
const remainingRows = this.elements.tableBody.querySelectorAll('.ftable-data-row');
|
|
3358
|
+
if (remainingRows.length === 0) {
|
|
3359
|
+
this.addNoDataRow();
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
this.refreshRowStyles();
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
// Selection Methods
|
|
3366
|
+
makeRowSelectable(row) {
|
|
3367
|
+
if (this.options.selectOnRowClick !== false) {
|
|
3368
|
+
row.addEventListener('click', (e) => {
|
|
3369
|
+
if (!e.target.classList.contains('norowselectonclick')) {
|
|
3370
|
+
this.toggleRowSelection(row);
|
|
3371
|
+
}
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
toggleRowSelection(row) {
|
|
3377
|
+
const isSelected = row.classList.contains('ftable-row-selected');
|
|
3378
|
+
|
|
3379
|
+
if (!this.options.multiselect) {
|
|
3380
|
+
// Clear all other selections
|
|
3381
|
+
this.clearAllSelections();
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
if (isSelected) {
|
|
3385
|
+
this.deselectRow(row);
|
|
3386
|
+
} else {
|
|
3387
|
+
this.selectRow(row);
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
this.emit('selectionChanged', { selectedRows: this.getSelectedRows() });
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
selectRow(row) {
|
|
3394
|
+
FTableDOMHelper.addClass(row, 'ftable-row-selected');
|
|
3395
|
+
const checkbox = row.querySelector('input[type="checkbox"]');
|
|
3396
|
+
if (checkbox) checkbox.checked = true;
|
|
3397
|
+
|
|
3398
|
+
const keyValue = this.getKeyValue(row.recordData);
|
|
3399
|
+
if (keyValue) this.state.selectedRecords.add(keyValue);
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
deselectRow(row) {
|
|
3403
|
+
FTableDOMHelper.removeClass(row, 'ftable-row-selected');
|
|
3404
|
+
const checkbox = row.querySelector('input[type="checkbox"]');
|
|
3405
|
+
if (checkbox) checkbox.checked = false;
|
|
3406
|
+
|
|
3407
|
+
const keyValue = this.getKeyValue(row.recordData);
|
|
3408
|
+
if (keyValue) this.state.selectedRecords.delete(keyValue);
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
recalcColumnWidths() {
|
|
3412
|
+
this.columnList.forEach(fieldName => {
|
|
3413
|
+
const field = this.options.fields[fieldName];
|
|
3414
|
+
const th = this.elements.table.querySelector(`[data-field-name="${fieldName}"]`);
|
|
3415
|
+
if (th && field.width) {
|
|
3416
|
+
th.style.width = field.width;
|
|
3417
|
+
}
|
|
3418
|
+
});
|
|
3419
|
+
// Trigger reflow
|
|
3420
|
+
this.elements.table.offsetHeight;
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
recalcColumnWidthsOnce() {
|
|
3424
|
+
if (!this._recalculatedOnce) {
|
|
3425
|
+
this.recalcColumnWidths();
|
|
3426
|
+
this._recalculatedOnce = true;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
clearAllSelections() {
|
|
3431
|
+
const selectedRows = this.elements.tableBody.querySelectorAll('.ftable-row-selected');
|
|
3432
|
+
selectedRows.forEach(row => this.deselectRow(row));
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
toggleSelectAll(selectAll) {
|
|
3436
|
+
const rows = this.elements.tableBody.querySelectorAll('.ftable-data-row');
|
|
3437
|
+
rows.forEach(row => {
|
|
3438
|
+
if (selectAll) {
|
|
3439
|
+
this.selectRow(row);
|
|
3440
|
+
} else {
|
|
3441
|
+
this.deselectRow(row);
|
|
3442
|
+
}
|
|
3443
|
+
});
|
|
3444
|
+
|
|
3445
|
+
this.emit('selectionChanged', { selectedRows: this.getSelectedRows() });
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
getSelectedRows() {
|
|
3449
|
+
return Array.from(this.elements.tableBody.querySelectorAll('.ftable-row-selected'));
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
// Sorting Methods
|
|
3453
|
+
sortByColumn(fieldName) {
|
|
3454
|
+
const existingSortIndex = this.state.sorting.findIndex(s => s.fieldName === fieldName);
|
|
3455
|
+
|
|
3456
|
+
if (!this.options.multiSorting) {
|
|
3457
|
+
this.state.sorting = [];
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
if (existingSortIndex >= 0) {
|
|
3461
|
+
const currentSort = this.state.sorting[existingSortIndex];
|
|
3462
|
+
if (currentSort.direction === 'ASC') {
|
|
3463
|
+
currentSort.direction = 'DESC';
|
|
3464
|
+
} else {
|
|
3465
|
+
this.state.sorting.splice(existingSortIndex, 1);
|
|
3466
|
+
}
|
|
3467
|
+
} else {
|
|
3468
|
+
this.state.sorting.push({ fieldName, direction: 'ASC' });
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
this.updateSortingHeaders();
|
|
3472
|
+
this.load(); // Reload with new sorting
|
|
3473
|
+
this.saveState();
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
updateSortingHeaders() {
|
|
3477
|
+
// Clear all sorting classes
|
|
3478
|
+
const headers = this.elements.table.querySelectorAll('.ftable-column-header-sortable');
|
|
3479
|
+
headers.forEach(header => {
|
|
3480
|
+
FTableDOMHelper.removeClass(header, 'ftable-column-header-sorted-asc ftable-column-header-sorted-desc');
|
|
3481
|
+
});
|
|
3482
|
+
|
|
3483
|
+
// Apply current sorting classes
|
|
3484
|
+
this.state.sorting.forEach(sort => {
|
|
3485
|
+
const header = this.elements.table.querySelector(`[data-field-name="${sort.fieldName}"]`);
|
|
3486
|
+
if (header) {
|
|
3487
|
+
FTableDOMHelper.addClass(header, `ftable-column-header-sorted-${sort.direction.toLowerCase()}`);
|
|
3488
|
+
}
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
// Paging Methods
|
|
3493
|
+
updatePagingInfo() {
|
|
3494
|
+
if (!this.options.paging || !this.elements.pageInfoSpan) return;
|
|
3495
|
+
|
|
3496
|
+
if (this.state.totalRecordCount <= 0) {
|
|
3497
|
+
this.elements.pageInfoSpan.textContent = '';
|
|
3498
|
+
this.elements.pagingListArea.innerHTML = '';
|
|
3499
|
+
return;
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// Update page info
|
|
3503
|
+
const startRecord = (this.state.currentPage - 1) * this.state.pageSize + 1;
|
|
3504
|
+
const endRecord = Math.min(this.state.currentPage * this.state.pageSize, this.state.totalRecordCount);
|
|
3505
|
+
|
|
3506
|
+
const pagingInfoMsg = this.options.messages.pagingInfo || 'Showing {0}-{1} of {2}';
|
|
3507
|
+
// Format with placeholders
|
|
3508
|
+
this.elements.pageInfoSpan.textContent = pagingInfoMsg
|
|
3509
|
+
.replace(/\{0\}/g, startRecord)
|
|
3510
|
+
.replace(/\{1\}/g, endRecord)
|
|
3511
|
+
.replace(/\{2\}/g, this.state.totalRecordCount);
|
|
3512
|
+
|
|
3513
|
+
// Update page navigation
|
|
3514
|
+
this.createPageListNavigation();
|
|
3515
|
+
this.createPageGotoNavigation();
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
createPageListNavigation() {
|
|
3519
|
+
if (!this.elements.pagingListArea) return;
|
|
3520
|
+
|
|
3521
|
+
this.elements.pagingListArea.innerHTML = '';
|
|
3522
|
+
|
|
3523
|
+
const totalPages = Math.ceil(this.state.totalRecordCount / this.state.pageSize);
|
|
3524
|
+
if (totalPages <= 1) return;
|
|
3525
|
+
|
|
3526
|
+
// First and Previous buttons
|
|
3527
|
+
this.createPageButton('«', 1, this.state.currentPage === 1, 'ftable-page-number-first');
|
|
3528
|
+
this.createPageButton('‹', this.state.currentPage - 1, this.state.currentPage === 1, 'ftable-page-number-previous');
|
|
3529
|
+
|
|
3530
|
+
// Page numbers
|
|
3531
|
+
const pageNumbers = this.calculatePageNumbers(totalPages);
|
|
3532
|
+
let lastNumber = 0;
|
|
3533
|
+
|
|
3534
|
+
pageNumbers.forEach(pageNum => {
|
|
3535
|
+
if (pageNum - lastNumber > 1) {
|
|
3536
|
+
FTableDOMHelper.create('span', {
|
|
3537
|
+
className: 'ftable-page-number-space',
|
|
3538
|
+
text: '...',
|
|
3539
|
+
parent: this.elements.pagingListArea
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
this.createPageButton(
|
|
3544
|
+
pageNum.toString(),
|
|
3545
|
+
pageNum,
|
|
3546
|
+
false,
|
|
3547
|
+
pageNum === this.state.currentPage ? 'ftable-page-number ftable-page-number-active' : 'ftable-page-number'
|
|
3548
|
+
);
|
|
3549
|
+
|
|
3550
|
+
lastNumber = pageNum;
|
|
3551
|
+
});
|
|
3552
|
+
|
|
3553
|
+
// Next and Last buttons
|
|
3554
|
+
this.createPageButton('›', this.state.currentPage + 1, this.state.currentPage >= totalPages, 'ftable-page-number-next');
|
|
3555
|
+
this.createPageButton('»', totalPages, this.state.currentPage >= totalPages, 'ftable-page-number-last');
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
createPageGotoNavigation() {
|
|
3559
|
+
if (!this.options.paging || this.options.gotoPageArea === 'none') {
|
|
3560
|
+
this.elements.pagingGotoArea.style.display = 'none';
|
|
3561
|
+
this.elements.pagingGotoArea.innerHTML = '';
|
|
3562
|
+
return;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
const totalPages = Math.ceil(this.state.totalRecordCount / this.state.pageSize);
|
|
3566
|
+
if (totalPages <= 1) {
|
|
3567
|
+
this.elements.pagingGotoArea.style.display = 'none';
|
|
3568
|
+
this.elements.pagingGotoArea.innerHTML = '';
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
this.elements.pagingGotoArea.style.display = 'inline-block';
|
|
3573
|
+
this.elements.pagingGotoArea.innerHTML = ''; // Clear
|
|
3574
|
+
|
|
3575
|
+
// Label
|
|
3576
|
+
const label = FTableDOMHelper.create('span', {
|
|
3577
|
+
text: this.options.messages.gotoPageLabel + ': ',
|
|
3578
|
+
parent: this.elements.pagingGotoArea
|
|
3579
|
+
});
|
|
3580
|
+
|
|
3581
|
+
const gotoPageInputId = `ftable-goto-page-${this.options.tableId || 'default'}`;
|
|
3582
|
+
|
|
3583
|
+
if (this.options.gotoPageArea === 'combobox') {
|
|
3584
|
+
// --- COMBOBOX (dropdown) ---
|
|
3585
|
+
this.elements.gotoPageSelect = FTableDOMHelper.create('select', {
|
|
3586
|
+
id: gotoPageInputId,
|
|
3587
|
+
className: 'ftable-page-goto-select',
|
|
3588
|
+
parent: this.elements.pagingGotoArea
|
|
3589
|
+
});
|
|
3590
|
+
|
|
3591
|
+
for (let i = 1; i <= totalPages; i++) {
|
|
3592
|
+
FTableDOMHelper.create('option', {
|
|
3593
|
+
attributes: { value: i },
|
|
3594
|
+
text: i,
|
|
3595
|
+
parent: this.elements.gotoPageSelect
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
this.elements.gotoPageSelect.value = this.state.currentPage;
|
|
3600
|
+
|
|
3601
|
+
this.elements.gotoPageSelect.addEventListener('change', (e) => {
|
|
3602
|
+
const page = parseInt(e.target.value);
|
|
3603
|
+
if (page >= 1 && page <= totalPages) {
|
|
3604
|
+
this.changePage(page);
|
|
3605
|
+
}
|
|
3606
|
+
});
|
|
3607
|
+
|
|
3608
|
+
} else if (this.options.gotoPageArea === 'textbox') {
|
|
3609
|
+
// --- TEXTBOX (number input) ---
|
|
3610
|
+
this.elements.gotoPageInput = FTableDOMHelper.create('input', {
|
|
3611
|
+
attributes: {
|
|
3612
|
+
type: 'number',
|
|
3613
|
+
id: gotoPageInputId,
|
|
3614
|
+
min: '1',
|
|
3615
|
+
max: totalPages,
|
|
3616
|
+
value: this.state.currentPage,
|
|
3617
|
+
className: 'ftable-page-goto-input',
|
|
3618
|
+
style: 'width: 65px; margin-left: 4px;',
|
|
3619
|
+
},
|
|
3620
|
+
parent: this.elements.pagingGotoArea
|
|
3621
|
+
});
|
|
3622
|
+
|
|
3623
|
+
// Handle change (user types, uses spinner, or presses Enter)
|
|
3624
|
+
this.elements.gotoPageInput.addEventListener('change', (e) => {
|
|
3625
|
+
const page = parseInt(e.target.value);
|
|
3626
|
+
if (page >= 1 && page <= totalPages) {
|
|
3627
|
+
this.changePage(page);
|
|
3628
|
+
} else {
|
|
3629
|
+
e.target.value = this.state.currentPage; // Revert if invalid
|
|
3630
|
+
}
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
createPageButton(text, pageNumber, disabled, className) {
|
|
3636
|
+
const button = FTableDOMHelper.create('span', {
|
|
3637
|
+
className: className + (disabled ? ' ftable-page-number-disabled' : ''),
|
|
3638
|
+
html: text,
|
|
3639
|
+
parent: this.elements.pagingListArea
|
|
3640
|
+
});
|
|
3641
|
+
|
|
3642
|
+
if (!disabled) {
|
|
3643
|
+
button.style.cursor = 'pointer';
|
|
3644
|
+
button.addEventListener('click', (e) => {
|
|
3645
|
+
e.preventDefault();
|
|
3646
|
+
this.changePage(pageNumber);
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
calculatePageNumbers(totalPages) {
|
|
3652
|
+
if (totalPages <= 7) {
|
|
3653
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
const current = this.state.currentPage;
|
|
3657
|
+
const pages = new Set([1, 2, totalPages - 1, totalPages]);
|
|
3658
|
+
|
|
3659
|
+
// Add current page and neighbors
|
|
3660
|
+
for (let i = Math.max(1, current - 1); i <= Math.min(totalPages, current + 1); i++) {
|
|
3661
|
+
pages.add(i);
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
return Array.from(pages).sort((a, b) => a - b);
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
changePage(pageNumber) {
|
|
3668
|
+
const totalPages = Math.ceil(this.state.totalRecordCount / this.state.pageSize);
|
|
3669
|
+
pageNumber = Math.max(1, Math.min(pageNumber, totalPages));
|
|
3670
|
+
|
|
3671
|
+
if (pageNumber === this.state.currentPage) return;
|
|
3672
|
+
|
|
3673
|
+
this.state.currentPage = pageNumber;
|
|
3674
|
+
this.load();
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
changePageSize(newSize) {
|
|
3678
|
+
this.state.pageSize = newSize;
|
|
3679
|
+
this.state.currentPage = 1; // Reset to first page
|
|
3680
|
+
this.load();
|
|
3681
|
+
this.saveState();
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
// Utility Methods
|
|
3685
|
+
showLoadingIndicator() {
|
|
3686
|
+
if (this.options.loadingAnimationDelay === 0) {
|
|
3687
|
+
if (this.modals.loading) {
|
|
3688
|
+
this.modals.loading.show();
|
|
3689
|
+
}
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
this.loadingTimeout = setTimeout(() => {
|
|
3694
|
+
if (this.modals.loading) {
|
|
3695
|
+
this.modals.loading.show();
|
|
3696
|
+
}
|
|
3697
|
+
this.loadingShownAt = Date.now(); // Track when shown
|
|
3698
|
+
}, this.options.loadingAnimationDelay || 500);
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
hideLoadingIndicator() {
|
|
3702
|
+
if (this.loadingTimeout) {
|
|
3703
|
+
clearTimeout(this.loadingTimeout);
|
|
3704
|
+
this.loadingTimeout = null;
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
const minDisplayTime = 200;
|
|
3708
|
+
const timeShown = this.loadingShownAt ? (Date.now() - this.loadingShownAt) : 0;
|
|
3709
|
+
|
|
3710
|
+
if (this.modals.loading) {
|
|
3711
|
+
if (timeShown < minDisplayTime) {
|
|
3712
|
+
setTimeout(() => {
|
|
3713
|
+
this.modals.loading.hide();
|
|
3714
|
+
}, minDisplayTime - timeShown);
|
|
3715
|
+
} else {
|
|
3716
|
+
this.modals.loading.hide();
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
this.loadingShownAt = null;
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
showError(message) {
|
|
3724
|
+
if (this.modals.error) {
|
|
3725
|
+
this.modals.error.setContent(`<p>${message}</p>`);
|
|
3726
|
+
this.modals.error.show();
|
|
3727
|
+
} else {
|
|
3728
|
+
alert(message); // Fallback
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
showInfo(message) {
|
|
3733
|
+
if (this.modals.info) {
|
|
3734
|
+
this.modals.info.setContent(`<p>${message}</p>`);
|
|
3735
|
+
this.modals.info.show();
|
|
3736
|
+
} else {
|
|
3737
|
+
alert(message); // Fallback
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
// Public API Methods
|
|
3742
|
+
reload(hard = false) {
|
|
3743
|
+
if (hard) {
|
|
3744
|
+
// Clear list cache
|
|
3745
|
+
this.clearListCache();
|
|
3746
|
+
}
|
|
3747
|
+
return this.load();
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
clearListCache() {
|
|
3751
|
+
if (this.options.actions.listAction && typeof this.options.actions.listAction === 'string') {
|
|
3752
|
+
this.formBuilder.optionsCache.clear(this.options.actions.listAction);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
getRowByKey(key) {
|
|
3757
|
+
return this.elements.tableBody.querySelector(`[data-record-key="${key}"]`);
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
destroy() {
|
|
3761
|
+
// Remove the instance reference from the DOM
|
|
3762
|
+
if (this.element && this.element.ftableInstance) {
|
|
3763
|
+
this.element.ftableInstance = null;
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
// Clean up modals
|
|
3767
|
+
Object.values(this.modals).forEach(modal => modal.destroy());
|
|
3768
|
+
|
|
3769
|
+
// Remove main container
|
|
3770
|
+
if (this.elements.mainContainer) {
|
|
3771
|
+
this.elements.mainContainer.remove();
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
// Clean timeouts and listeners
|
|
3775
|
+
this.searchTimeout && clearTimeout(this.searchTimeout);
|
|
3776
|
+
this.loadingTimeout && clearTimeout(this.loadingTimeout);
|
|
3777
|
+
window.removeEventListener('resize', this.handleResize);
|
|
3778
|
+
|
|
3779
|
+
// Clear state
|
|
3780
|
+
this.options = null;
|
|
3781
|
+
this.state = null;
|
|
3782
|
+
this.elements = null;
|
|
3783
|
+
this.formBuilder = null;
|
|
3784
|
+
this.modals = null;
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
// Chainable method for setting options
|
|
3788
|
+
setOption(key, value) {
|
|
3789
|
+
this.options[key] = value;
|
|
3790
|
+
return this;
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
// Method to get current state
|
|
3794
|
+
getState() {
|
|
3795
|
+
return { ...this.state };
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
// Advanced filtering and search
|
|
3799
|
+
addFilter(fieldName, value, operator = 'equals') {
|
|
3800
|
+
if (!this.state.filters) {
|
|
3801
|
+
this.state.filters = [];
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
// Remove existing filter for this field
|
|
3805
|
+
this.state.filters = this.state.filters.filter(f => f.fieldName !== fieldName);
|
|
3806
|
+
|
|
3807
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
3808
|
+
this.state.filters.push({ fieldName, value, operator });
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
return this;
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
clearFilters() {
|
|
3815
|
+
this.state.filters = [];
|
|
3816
|
+
return this;
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
// CSV Export functionality
|
|
3820
|
+
exportToCSV(filename = 'table-data.csv') {
|
|
3821
|
+
const headers = this.columnList.map(fieldName => {
|
|
3822
|
+
const field = this.options.fields[fieldName];
|
|
3823
|
+
return field.title || fieldName;
|
|
3824
|
+
});
|
|
3825
|
+
|
|
3826
|
+
const rows = this.state.records.map(record => {
|
|
3827
|
+
return this.columnList.map(fieldName => {
|
|
3828
|
+
const value = this.getDisplayText(record, fieldName);
|
|
3829
|
+
// Escape CSV values
|
|
3830
|
+
return `"${String(value).replace(/"/g, '""')}"`;
|
|
3831
|
+
});
|
|
3832
|
+
});
|
|
3833
|
+
|
|
3834
|
+
const csvContent = [
|
|
3835
|
+
headers.map(h => `"${h}"`).join(','),
|
|
3836
|
+
...rows.map(row => row.join(','))
|
|
3837
|
+
].join('\n');
|
|
3838
|
+
|
|
3839
|
+
// Create and trigger download
|
|
3840
|
+
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
3841
|
+
const link = document.createElement('a');
|
|
3842
|
+
link.href = URL.createObjectURL(blob);
|
|
3843
|
+
link.download = filename;
|
|
3844
|
+
link.click();
|
|
3845
|
+
link.remove();
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
// Print functionality
|
|
3849
|
+
printTable() {
|
|
3850
|
+
const printWindow = window.open('', '_blank', 'width=800,height=600');
|
|
3851
|
+
const tableHtml = this.elements.table.outerHTML;
|
|
3852
|
+
|
|
3853
|
+
const printContent = `
|
|
3854
|
+
<!DOCTYPE html>
|
|
3855
|
+
<html>
|
|
3856
|
+
<head>
|
|
3857
|
+
<title>${this.options.title || 'Table Data'}</title>
|
|
3858
|
+
<style>
|
|
3859
|
+
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
3860
|
+
table { width: 100%; border-collapse: collapse; }
|
|
3861
|
+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
3862
|
+
th { background-color: #f2f2f2; font-weight: bold; }
|
|
3863
|
+
.ftable-command-column { display: none !important; }
|
|
3864
|
+
.ftable-command-column-header { display: none !important; }
|
|
3865
|
+
.ftable-selecting-column { display: none !important; }
|
|
3866
|
+
.ftable-column-header-select { display: none !important; }
|
|
3867
|
+
@media print {
|
|
3868
|
+
body { margin: 0; }
|
|
3869
|
+
table { font-size: 12px; }
|
|
3870
|
+
}
|
|
3871
|
+
</style>
|
|
3872
|
+
</head>
|
|
3873
|
+
<body>
|
|
3874
|
+
<h1>${this.options.title || 'Table Data'}</h1>
|
|
3875
|
+
${tableHtml}
|
|
3876
|
+
<script>
|
|
3877
|
+
window.onload = function() {
|
|
3878
|
+
window.print();
|
|
3879
|
+
setTimeout(() => window.close(), 100);
|
|
3880
|
+
};
|
|
3881
|
+
</script>
|
|
3882
|
+
</body>
|
|
3883
|
+
</html>
|
|
3884
|
+
`;
|
|
3885
|
+
|
|
3886
|
+
printWindow.document.write(printContent);
|
|
3887
|
+
printWindow.document.close();
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
// Bulk operations
|
|
3891
|
+
async bulkDelete(confirmMessage = 'Delete selected records?') {
|
|
3892
|
+
const selectedRows = this.getSelectedRows();
|
|
3893
|
+
if (selectedRows.length === 0) {
|
|
3894
|
+
this.showError('No records selected');
|
|
3895
|
+
return;
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
if (!confirm(confirmMessage)) return;
|
|
3899
|
+
|
|
3900
|
+
const keyValues = selectedRows.map(row => this.getKeyValue(row.recordData));
|
|
3901
|
+
const results = [];
|
|
3902
|
+
|
|
3903
|
+
for (const keyValue of keyValues) {
|
|
3904
|
+
try {
|
|
3905
|
+
const result = await this.performDelete(keyValue);
|
|
3906
|
+
results.push({ key: keyValue, success: result.Result === 'OK', result });
|
|
3907
|
+
} catch (error) {
|
|
3908
|
+
results.push({ key: keyValue, success: false, error: error.message });
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
// Remove successful deletions from table
|
|
3913
|
+
const successfulDeletes = results.filter(r => r.success);
|
|
3914
|
+
successfulDeletes.forEach(({ key }) => {
|
|
3915
|
+
const row = this.getRowByKey(key);
|
|
3916
|
+
if (row) this.removeRowFromTable(row);
|
|
3917
|
+
});
|
|
3918
|
+
|
|
3919
|
+
// Show summary
|
|
3920
|
+
const failed = results.filter(r => !r.success).length;
|
|
3921
|
+
if (failed > 0) {
|
|
3922
|
+
this.showError(`${failed} of ${results.length} records could not be deleted`);
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
// this.emit('bulkDelete', { results: results, successful: successfulDeletes.length, failed: failed });
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
// Column management
|
|
3929
|
+
showColumn(fieldName) {
|
|
3930
|
+
this.setColumnVisibility(fieldName, true);
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
hideColumn(fieldName) {
|
|
3934
|
+
this.setColumnVisibility(fieldName, false);
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
setColumnVisibility(fieldName, visible) {
|
|
3938
|
+
const field = this.options.fields[fieldName];
|
|
3939
|
+
if (!field) return;
|
|
3940
|
+
|
|
3941
|
+
// Don't allow hiding sorted columns
|
|
3942
|
+
if (!visible && this.isFieldSorted(fieldName)) {
|
|
3943
|
+
this.showError(`Cannot hide column "${field.title || fieldName}" because it is currently sorted`);
|
|
3944
|
+
return;
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
// Don't allow changing fixed columns
|
|
3948
|
+
if (field.visibility === 'fixed') {
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
field.visibility = visible ? 'visible' : 'hidden';
|
|
3953
|
+
|
|
3954
|
+
// Update existing table
|
|
3955
|
+
const columnIndex = this.columnList.indexOf(fieldName);
|
|
3956
|
+
if (columnIndex >= 0) {
|
|
3957
|
+
// Calculate actual column index (accounting for selecting column)
|
|
3958
|
+
let actualIndex = columnIndex + 1; // CSS nth-child is 1-based
|
|
3959
|
+
if (this.options.selecting) {
|
|
3960
|
+
actualIndex += 1; // Account for selecting column
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
const selector = `th:nth-child(${actualIndex}), td:nth-child(${actualIndex})`;
|
|
3964
|
+
const cells = this.elements.table.querySelectorAll(selector);
|
|
3965
|
+
|
|
3966
|
+
cells.forEach(cell => {
|
|
3967
|
+
if (visible) {
|
|
3968
|
+
FTableDOMHelper.show(cell);
|
|
3969
|
+
} else {
|
|
3970
|
+
FTableDOMHelper.hide(cell);
|
|
3971
|
+
}
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
// Save column settings
|
|
3976
|
+
if (this.options.saveFTableUserPreferences) {
|
|
3977
|
+
this.saveColumnSettings();
|
|
3978
|
+
this.saveState(); // sorting might affect state
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
// Hide the column selection menu
|
|
3982
|
+
//this.hideColumnSelectionMenu();
|
|
3983
|
+
|
|
3984
|
+
// Emit event
|
|
3985
|
+
// this.emit('columnVisibilityChanged', { field: field });
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
// Responsive helpers
|
|
3989
|
+
makeResponsive() {
|
|
3990
|
+
// Add responsive classes and behavior
|
|
3991
|
+
FTableDOMHelper.addClass(this.elements.mainContainer, 'ftable-responsive');
|
|
3992
|
+
|
|
3993
|
+
// Handle window resize
|
|
3994
|
+
const handleResize = () => {
|
|
3995
|
+
const containerWidth = this.elements.mainContainer.offsetWidth;
|
|
3996
|
+
|
|
3997
|
+
if (containerWidth < 768) {
|
|
3998
|
+
FTableDOMHelper.addClass(this.elements.table, 'ftable-mobile');
|
|
3999
|
+
this.handleMobileView();
|
|
4000
|
+
} else {
|
|
4001
|
+
FTableDOMHelper.removeClass(this.elements.table, 'ftable-mobile');
|
|
4002
|
+
this.handleDesktopView();
|
|
4003
|
+
}
|
|
4004
|
+
};
|
|
4005
|
+
|
|
4006
|
+
window.addEventListener('resize', handleResize);
|
|
4007
|
+
handleResize(); // Initial call
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
handleMobileView() {
|
|
4011
|
+
// In mobile view, could stack cells vertically or hide less important columns
|
|
4012
|
+
// This is a simplified version
|
|
4013
|
+
const lessPriorityColumns = this.columnList.filter(fieldName => {
|
|
4014
|
+
const field = this.options.fields[fieldName];
|
|
4015
|
+
return field.priority === 'low' || field.mobileHidden === true;
|
|
4016
|
+
});
|
|
4017
|
+
|
|
4018
|
+
lessPriorityColumns.forEach(fieldName => {
|
|
4019
|
+
this.setColumnVisibility(fieldName, false);
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
handleDesktopView() {
|
|
4024
|
+
// Restore all columns in desktop view
|
|
4025
|
+
this.columnList.forEach(fieldName => {
|
|
4026
|
+
const field = this.options.fields[fieldName];
|
|
4027
|
+
if (field.visibility !== 'hidden') {
|
|
4028
|
+
this.setColumnVisibility(fieldName, true);
|
|
4029
|
+
}
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
// Data validation
|
|
4034
|
+
validateRecord(record, operation = 'create') {
|
|
4035
|
+
const errors = [];
|
|
4036
|
+
|
|
4037
|
+
Object.entries(this.options.fields).forEach(([fieldName, field]) => {
|
|
4038
|
+
const value = record[fieldName];
|
|
4039
|
+
|
|
4040
|
+
// Required field validation
|
|
4041
|
+
if (field.required && (!value || value.toString().trim() === '')) {
|
|
4042
|
+
errors.push(`${field.title || fieldName} is required`);
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
// Type validation
|
|
4046
|
+
if (value && field.validate && typeof field.validate === 'function') {
|
|
4047
|
+
const validationResult = field.validate(value, record);
|
|
4048
|
+
if (validationResult !== true) {
|
|
4049
|
+
errors.push(validationResult || `${field.title || fieldName} is invalid`);
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
// Built-in type validations
|
|
4054
|
+
if (value) {
|
|
4055
|
+
switch (field.type) {
|
|
4056
|
+
case 'email':
|
|
4057
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4058
|
+
if (!emailRegex.test(value)) {
|
|
4059
|
+
errors.push(`${field.title || fieldName} must be a valid email`);
|
|
4060
|
+
}
|
|
4061
|
+
break;
|
|
4062
|
+
case 'number':
|
|
4063
|
+
if (isNaN(value)) {
|
|
4064
|
+
errors.push(`${field.title || fieldName} must be a number`);
|
|
4065
|
+
}
|
|
4066
|
+
break;
|
|
4067
|
+
case 'date':
|
|
4068
|
+
if (isNaN(new Date(value).getTime())) {
|
|
4069
|
+
errors.push(`${field.title || fieldName} must be a valid date`);
|
|
4070
|
+
}
|
|
4071
|
+
break;
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
});
|
|
4075
|
+
|
|
4076
|
+
return {
|
|
4077
|
+
isValid: errors.length === 0,
|
|
4078
|
+
errors
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
// Advanced search functionality
|
|
4083
|
+
enableSearch(options = {}) {
|
|
4084
|
+
const searchOptions = {
|
|
4085
|
+
placeholder: 'Search...',
|
|
4086
|
+
debounceMs: 300,
|
|
4087
|
+
searchFields: this.columnList,
|
|
4088
|
+
...options
|
|
4089
|
+
};
|
|
4090
|
+
|
|
4091
|
+
const searchContainer = FTableDOMHelper.create('div', {
|
|
4092
|
+
className: 'ftable-search-container',
|
|
4093
|
+
parent: this.elements.toolbarDiv
|
|
4094
|
+
});
|
|
4095
|
+
|
|
4096
|
+
const searchInput = FTableDOMHelper.create('input', {
|
|
4097
|
+
attributes: {
|
|
4098
|
+
type: 'text',
|
|
4099
|
+
placeholder: searchOptions.placeholder,
|
|
4100
|
+
class: 'ftable-search-input'
|
|
4101
|
+
},
|
|
4102
|
+
parent: searchContainer
|
|
4103
|
+
});
|
|
4104
|
+
|
|
4105
|
+
// Debounced search
|
|
4106
|
+
let searchTimeout;
|
|
4107
|
+
searchInput.addEventListener('input', (e) => {
|
|
4108
|
+
clearTimeout(searchTimeout);
|
|
4109
|
+
searchTimeout = setTimeout(() => {
|
|
4110
|
+
this.performSearch(e.target.value, searchOptions.searchFields);
|
|
4111
|
+
}, searchOptions.debounceMs);
|
|
4112
|
+
});
|
|
4113
|
+
|
|
4114
|
+
return searchInput;
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
async performSearch(query, searchFields) {
|
|
4118
|
+
if (!query.trim()) {
|
|
4119
|
+
return this.load(); // Clear search
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
const searchParams = {
|
|
4123
|
+
search: query,
|
|
4124
|
+
searchFields: searchFields.join(',')
|
|
4125
|
+
};
|
|
4126
|
+
|
|
4127
|
+
return this.load(searchParams);
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
// Keyboard shortcuts
|
|
4131
|
+
enableKeyboardShortcuts() {
|
|
4132
|
+
document.addEventListener('keydown', (e) => {
|
|
4133
|
+
// Only handle shortcuts when table has focus or is active
|
|
4134
|
+
if (!this.elements.mainContainer.contains(document.activeElement)) return;
|
|
4135
|
+
|
|
4136
|
+
switch (e.key) {
|
|
4137
|
+
case 'n':
|
|
4138
|
+
if (e.ctrlKey && this.options.actions.createAction) {
|
|
4139
|
+
e.preventDefault();
|
|
4140
|
+
this.showAddRecordForm();
|
|
4141
|
+
}
|
|
4142
|
+
break;
|
|
4143
|
+
case 'r':
|
|
4144
|
+
if (e.ctrlKey) {
|
|
4145
|
+
e.preventDefault();
|
|
4146
|
+
this.reload();
|
|
4147
|
+
}
|
|
4148
|
+
break;
|
|
4149
|
+
case 'Delete':
|
|
4150
|
+
if (this.options.actions.deleteAction) {
|
|
4151
|
+
const selectedRows = this.getSelectedRows();
|
|
4152
|
+
if (selectedRows.length > 0) {
|
|
4153
|
+
e.preventDefault();
|
|
4154
|
+
this.bulkDelete();
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
break;
|
|
4158
|
+
case 'a':
|
|
4159
|
+
if (e.ctrlKey && this.options.selecting && this.options.multiselect) {
|
|
4160
|
+
e.preventDefault();
|
|
4161
|
+
this.toggleSelectAll(true);
|
|
4162
|
+
}
|
|
4163
|
+
break;
|
|
4164
|
+
case 'Escape':
|
|
4165
|
+
// Close any open modals
|
|
4166
|
+
Object.values(this.modals).forEach(modal => {
|
|
4167
|
+
if (modal.isOpen) modal.close();
|
|
4168
|
+
});
|
|
4169
|
+
break;
|
|
4170
|
+
}
|
|
4171
|
+
});
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
// Performance optimization for large datasets
|
|
4175
|
+
enableVirtualScrolling(options = {}) {
|
|
4176
|
+
const virtualOptions = {
|
|
4177
|
+
rowHeight: 40,
|
|
4178
|
+
overscan: 5,
|
|
4179
|
+
...options
|
|
4180
|
+
};
|
|
4181
|
+
|
|
4182
|
+
// This would implement virtual scrolling for performance with large datasets
|
|
4183
|
+
// Simplified version - full implementation would be more complex
|
|
4184
|
+
this.virtualScrolling = {
|
|
4185
|
+
enabled: true,
|
|
4186
|
+
...virtualOptions,
|
|
4187
|
+
visibleRange: { start: 0, end: 0 },
|
|
4188
|
+
scrollContainer: null
|
|
4189
|
+
};
|
|
4190
|
+
|
|
4191
|
+
// Replace table body with virtual scroll container
|
|
4192
|
+
// Implementation would calculate visible rows and only render those
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
// Real-time updates via WebSocket
|
|
4196
|
+
enableRealTimeUpdates(websocketUrl) {
|
|
4197
|
+
if (!websocketUrl) return;
|
|
4198
|
+
|
|
4199
|
+
this.websocket = new WebSocket(websocketUrl);
|
|
4200
|
+
|
|
4201
|
+
this.websocket.onmessage = (event) => {
|
|
4202
|
+
try {
|
|
4203
|
+
const data = JSON.parse(event.data);
|
|
4204
|
+
this.handleRealTimeUpdate(data);
|
|
4205
|
+
} catch (error) {
|
|
4206
|
+
this.logger.error('Failed to parse WebSocket message', error);
|
|
4207
|
+
}
|
|
4208
|
+
};
|
|
4209
|
+
|
|
4210
|
+
this.websocket.onerror = (error) => {
|
|
4211
|
+
this.logger.error('WebSocket error', error);
|
|
4212
|
+
};
|
|
4213
|
+
|
|
4214
|
+
this.websocket.onclose = () => {
|
|
4215
|
+
this.logger.info('WebSocket connection closed');
|
|
4216
|
+
// Attempt to reconnect after delay
|
|
4217
|
+
setTimeout(() => {
|
|
4218
|
+
if (this.websocket.readyState === WebSocket.CLOSED) {
|
|
4219
|
+
this.enableRealTimeUpdates(websocketUrl);
|
|
4220
|
+
}
|
|
4221
|
+
}, 5000);
|
|
4222
|
+
};
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
handleRealTimeUpdate(data) {
|
|
4226
|
+
switch (data.type) {
|
|
4227
|
+
case 'record_added':
|
|
4228
|
+
this.addRecordToTable(data.record);
|
|
4229
|
+
break;
|
|
4230
|
+
case 'record_updated':
|
|
4231
|
+
this.updateRecordInTable(data.record);
|
|
4232
|
+
break;
|
|
4233
|
+
case 'record_deleted':
|
|
4234
|
+
this.removeRecordFromTable(data.recordKey);
|
|
4235
|
+
break;
|
|
4236
|
+
case 'refresh':
|
|
4237
|
+
this.reload();
|
|
4238
|
+
break;
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
addRecordToTable(record) {
|
|
4243
|
+
const row = this.createTableRow(record);
|
|
4244
|
+
|
|
4245
|
+
// Add to beginning or end based on sorting
|
|
4246
|
+
if (this.state.sorting.length > 0) {
|
|
4247
|
+
// Would need to calculate correct position based on sort
|
|
4248
|
+
this.elements.tableBody.appendChild(row);
|
|
4249
|
+
} else {
|
|
4250
|
+
this.elements.tableBody.appendChild(row);
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
this.state.records.push(record);
|
|
4254
|
+
this.removeNoDataRow();
|
|
4255
|
+
this.refreshRowStyles();
|
|
4256
|
+
|
|
4257
|
+
// Show animation
|
|
4258
|
+
if (this.options.animationsEnabled) {
|
|
4259
|
+
this.showRowAnimation(row, 'added');
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
updateRecordInTable(record) {
|
|
4264
|
+
const keyValue = this.getKeyValue(record);
|
|
4265
|
+
const existingRow = this.getRowByKey(keyValue);
|
|
4266
|
+
|
|
4267
|
+
if (existingRow) {
|
|
4268
|
+
this.updateRowData(existingRow, record);
|
|
4269
|
+
|
|
4270
|
+
if (this.options.animationsEnabled) {
|
|
4271
|
+
this.showRowAnimation(existingRow, 'updated');
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
removeRecordFromTable(keyValue) {
|
|
4277
|
+
const row = this.getRowByKey(keyValue);
|
|
4278
|
+
if (row) {
|
|
4279
|
+
this.removeRowFromTable(row);
|
|
4280
|
+
|
|
4281
|
+
// Remove from state
|
|
4282
|
+
this.state.records = this.state.records.filter(r =>
|
|
4283
|
+
this.getKeyValue(r) !== keyValue
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
showRowAnimation(row, type) {
|
|
4289
|
+
const animationClass = `ftable-row-${type}`;
|
|
4290
|
+
FTableDOMHelper.addClass(row, animationClass);
|
|
4291
|
+
|
|
4292
|
+
setTimeout(() => {
|
|
4293
|
+
FTableDOMHelper.removeClass(row, animationClass);
|
|
4294
|
+
}, 2000);
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
// Plugin system for extensions
|
|
4298
|
+
use(plugin, options = {}) {
|
|
4299
|
+
if (typeof plugin === 'function') {
|
|
4300
|
+
plugin(this, options);
|
|
4301
|
+
} else if (plugin && typeof plugin.install === 'function') {
|
|
4302
|
+
plugin.install(this, options);
|
|
4303
|
+
}
|
|
4304
|
+
return this;
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
// Event delegation for dynamic content
|
|
4308
|
+
delegate(selector, event, handler) {
|
|
4309
|
+
this.elements.mainContainer.addEventListener(event, (e) => {
|
|
4310
|
+
const target = e.target.closest(selector);
|
|
4311
|
+
if (target) {
|
|
4312
|
+
handler.call(target, e);
|
|
4313
|
+
}
|
|
4314
|
+
});
|
|
4315
|
+
return this;
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
async editRecordViaAjax(recordId, url, params = {}) {
|
|
4319
|
+
try {
|
|
4320
|
+
// Get the actual key field name (e.g., 'asset_id', 'user_id', etc.)
|
|
4321
|
+
const keyFieldName = this.keyField;
|
|
4322
|
+
if (!keyFieldName) {
|
|
4323
|
+
throw new Error('No key field defined in fTable options');
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
// Build parameters using the correct key field name
|
|
4327
|
+
const fullParams = {
|
|
4328
|
+
[keyFieldName]: recordId,
|
|
4329
|
+
...params
|
|
4330
|
+
};
|
|
4331
|
+
|
|
4332
|
+
const response = await FTableHttpClient.get(url, fullParams);
|
|
4333
|
+
|
|
4334
|
+
if (!response || !response.Record) {
|
|
4335
|
+
throw new Error('Invalid response or missing Record');
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
const record = response.Record;
|
|
4339
|
+
|
|
4340
|
+
// Find or create a row
|
|
4341
|
+
const row = this.getRowByKey(recordId) || FTableDOMHelper.create('tr', {
|
|
4342
|
+
className: 'ftable-data-row',
|
|
4343
|
+
attributes: { 'data-record-key': recordId }
|
|
4344
|
+
});
|
|
4345
|
+
|
|
4346
|
+
row.recordData = { ...record };
|
|
4347
|
+
|
|
4348
|
+
// Open the edit form
|
|
4349
|
+
await this.editRecord(row);
|
|
4350
|
+
} catch (error) {
|
|
4351
|
+
this.showError('Failed to load record for editing.');
|
|
4352
|
+
this.logger.error(`editRecordViaAjax failed: ${error.message}`);
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
openChildTable(parentRow, childOptions, onInit) {
|
|
4357
|
+
// Prevent multiple child tables
|
|
4358
|
+
this.closeChildTable(parentRow);
|
|
4359
|
+
|
|
4360
|
+
// Create container for child table
|
|
4361
|
+
const childContainer = FTableDOMHelper.create('tr', {
|
|
4362
|
+
className: 'ftable-child-row'
|
|
4363
|
+
});
|
|
4364
|
+
|
|
4365
|
+
const cell = FTableDOMHelper.create('td', {
|
|
4366
|
+
attributes: { colspan: this.getVisibleColumnCount() },
|
|
4367
|
+
parent: childContainer
|
|
4368
|
+
});
|
|
4369
|
+
|
|
4370
|
+
// Create the child table wrapper
|
|
4371
|
+
const childWrapper = FTableDOMHelper.create('div', {
|
|
4372
|
+
className: 'ftable-child-table-container',
|
|
4373
|
+
parent: cell
|
|
4374
|
+
});
|
|
4375
|
+
|
|
4376
|
+
// Insert after parent row
|
|
4377
|
+
parentRow.parentNode.insertBefore(childContainer, parentRow.nextSibling);
|
|
4378
|
+
|
|
4379
|
+
// Store reference
|
|
4380
|
+
parentRow.childRow = childContainer;
|
|
4381
|
+
childContainer.parentRow = parentRow;
|
|
4382
|
+
|
|
4383
|
+
// Initialize child table
|
|
4384
|
+
const childTable = new FTable(childWrapper, {
|
|
4385
|
+
...childOptions,
|
|
4386
|
+
// Inherit some parent settings
|
|
4387
|
+
paging: childOptions.paging !== undefined ? childOptions.paging : true,
|
|
4388
|
+
pageSize: childOptions.pageSize || 10,
|
|
4389
|
+
sorting: childOptions.sorting !== undefined ? childOptions.sorting : true,
|
|
4390
|
+
selecting: false,
|
|
4391
|
+
toolbarsearch: true,
|
|
4392
|
+
messages: {
|
|
4393
|
+
...this.options.messages,
|
|
4394
|
+
...childOptions.messages
|
|
4395
|
+
}
|
|
4396
|
+
});
|
|
4397
|
+
|
|
4398
|
+
// Hook into close events
|
|
4399
|
+
const originalClose = childTable.close;
|
|
4400
|
+
childTable.close = () => {
|
|
4401
|
+
this.closeChildTable(parentRow);
|
|
4402
|
+
};
|
|
4403
|
+
|
|
4404
|
+
// Init and load
|
|
4405
|
+
childTable.init();
|
|
4406
|
+
if (onInit) onInit(childTable);
|
|
4407
|
+
|
|
4408
|
+
// Store reference
|
|
4409
|
+
parentRow.childTable = childTable;
|
|
4410
|
+
|
|
4411
|
+
return childTable;
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
closeChildTable(parentRow) {
|
|
4415
|
+
if (parentRow.childRow) {
|
|
4416
|
+
if (parentRow.childTable && typeof parentRow.childTable.destroy === 'function') {
|
|
4417
|
+
parentRow.childTable.destroy();
|
|
4418
|
+
}
|
|
4419
|
+
parentRow.childRow.remove();
|
|
4420
|
+
parentRow.childRow = null;
|
|
4421
|
+
parentRow.childTable = null;
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
renderSortingInfo() {
|
|
4426
|
+
if (!this.options.sortingInfoSelector || !this.options.sorting) return;
|
|
4427
|
+
|
|
4428
|
+
const container = document.querySelector(this.options.sortingInfoSelector);
|
|
4429
|
+
if (!container) return;
|
|
4430
|
+
|
|
4431
|
+
// Clear existing content
|
|
4432
|
+
container.innerHTML = '';
|
|
4433
|
+
|
|
4434
|
+
const messages = this.options.messages || {};
|
|
4435
|
+
|
|
4436
|
+
// Get prefix/suffix if defined
|
|
4437
|
+
const prefix = messages.sortingInfoPrefix ? `<span class="ftable-sorting-prefix">${messages.sortingInfoPrefix}</span> ` : '';
|
|
4438
|
+
const suffix = messages.sortingInfoSuffix ? ` <span class="ftable-sorting-suffix">${messages.sortingInfoSuffix}</span>` : '';
|
|
4439
|
+
|
|
4440
|
+
if (this.state.sorting.length === 0) {
|
|
4441
|
+
container.innerHTML = messages.sortingInfoNone || '';
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
// Build sorted fields list with translated directions
|
|
4446
|
+
const sortedItems = this.state.sorting.map(s => {
|
|
4447
|
+
const field = this.options.fields[s.fieldName];
|
|
4448
|
+
const title = field?.title || s.fieldName;
|
|
4449
|
+
|
|
4450
|
+
// Translate direction
|
|
4451
|
+
const directionText = s.direction === 'ASC'
|
|
4452
|
+
? (messages.ascending || 'ascending')
|
|
4453
|
+
: (messages.descending || 'descending');
|
|
4454
|
+
|
|
4455
|
+
return `${title} (${directionText})`;
|
|
4456
|
+
}).join(', ');
|
|
4457
|
+
|
|
4458
|
+
// Combine with prefix and suffix
|
|
4459
|
+
container.innerHTML = `${prefix}${sortedItems}${suffix}`;
|
|
4460
|
+
|
|
4461
|
+
// Add reset sorting button
|
|
4462
|
+
if (this.state.sorting.length > 0) {
|
|
4463
|
+
const resetSortBtn = document.createElement('button');
|
|
4464
|
+
resetSortBtn.textContent = messages.resetSorting || 'Reset Sorting';
|
|
4465
|
+
resetSortBtn.style.marginLeft = '10px';
|
|
4466
|
+
resetSortBtn.classList.add('ftable-sorting-reset-btn');
|
|
4467
|
+
resetSortBtn.addEventListener('click', (e) => {
|
|
4468
|
+
e.preventDefault();
|
|
4469
|
+
this.state.sorting = [];
|
|
4470
|
+
this.updateSortingHeaders();
|
|
4471
|
+
this.load();
|
|
4472
|
+
this.saveState();
|
|
4473
|
+
});
|
|
4474
|
+
container.appendChild(resetSortBtn);
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
// Add reset table button if enabled
|
|
4478
|
+
if (this.options.tableReset) {
|
|
4479
|
+
const resetTableBtn = document.createElement('button');
|
|
4480
|
+
resetTableBtn.textContent = messages.resetTable || 'Reset Table';
|
|
4481
|
+
resetTableBtn.style.marginLeft = '10px';
|
|
4482
|
+
resetTableBtn.classList.add('ftable-table-reset-btn');
|
|
4483
|
+
resetTableBtn.addEventListener('click', (e) => {
|
|
4484
|
+
e.preventDefault();
|
|
4485
|
+
const confirmMsg = messages.resetTableConfirm;
|
|
4486
|
+
if (confirm(confirmMsg)) {
|
|
4487
|
+
this.userPrefs.remove('column-settings');
|
|
4488
|
+
this.userPrefs.remove('table-state');
|
|
4489
|
+
|
|
4490
|
+
// Clear any in-memory state that might affect rendering
|
|
4491
|
+
this.state.sorting = [];
|
|
4492
|
+
this.state.pageSize = this.options.pageSize;
|
|
4493
|
+
|
|
4494
|
+
// Reset field visibility to default
|
|
4495
|
+
this.columnList.forEach(fieldName => {
|
|
4496
|
+
const field = this.options.fields[fieldName];
|
|
4497
|
+
// Reset to default: hidden only if explicitly set
|
|
4498
|
+
field.visibility = field.visibility === 'fixed' ? 'fixed' : 'visible';
|
|
4499
|
+
});
|
|
4500
|
+
location.reload();
|
|
4501
|
+
}
|
|
4502
|
+
});
|
|
4503
|
+
container.appendChild(resetTableBtn);
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
/**
|
|
4508
|
+
* Waits for a specific option value to be available in a select field.
|
|
4509
|
+
* Useful for pre-filling forms with async-loaded options.
|
|
4510
|
+
*
|
|
4511
|
+
* @param {string} fieldName - The name of the field
|
|
4512
|
+
* @param {string|number} value - The option value to wait for
|
|
4513
|
+
* @param {Function} callback - Called when the option is available
|
|
4514
|
+
* @param {Object} [options] - Optional settings
|
|
4515
|
+
* @param {HTMLElement} [options.form] - Form to search in (default: current form)
|
|
4516
|
+
* @param {number} [options.timeout=5000] - Max wait time in ms
|
|
4517
|
+
*/
|
|
4518
|
+
/*
|
|
4519
|
+
_waitForFieldReady(fieldName, callback, options = {}) {
|
|
4520
|
+
const { form = this.currentForm, timeout = 5000 } = options;
|
|
4521
|
+
if (!form) {
|
|
4522
|
+
console.warn(`FTable: No form available for waitForFieldReady('${fieldName}')`);
|
|
4523
|
+
return;
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
const select = form.querySelector(`[name="${fieldName}"]`);
|
|
4527
|
+
if (!select || select.tagName !== 'SELECT') {
|
|
4528
|
+
console.warn(`FTable: Field '${fieldName}' not found or not a <select>`);
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
|
|
4532
|
+
// If already has options, call immediately
|
|
4533
|
+
if (select.options.length > 1) {
|
|
4534
|
+
callback();
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
// Otherwise, wait for first option to be added
|
|
4539
|
+
const observer = new MutationObserver(() => {
|
|
4540
|
+
if (select.options.length > 1) {
|
|
4541
|
+
observer.disconnect();
|
|
4542
|
+
callback();
|
|
4543
|
+
}
|
|
4544
|
+
});
|
|
4545
|
+
observer.observe(select, { childList: true });
|
|
4546
|
+
|
|
4547
|
+
// Optional: timeout fallback
|
|
4548
|
+
if (timeout > 0) {
|
|
4549
|
+
setTimeout(() => {
|
|
4550
|
+
if (observer) {
|
|
4551
|
+
observer.disconnect();
|
|
4552
|
+
if (select.options.length > 1) {
|
|
4553
|
+
callback();
|
|
4554
|
+
} else {
|
|
4555
|
+
console.warn(`FTable: Timeout waiting for field '${fieldName}' to load options`);
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
}, timeout);
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
async waitForFieldReady(fieldName, options = {}) {
|
|
4563
|
+
return new Promise((resolve) => {
|
|
4564
|
+
this._waitForFieldReady(fieldName, resolve, options);
|
|
4565
|
+
});
|
|
4566
|
+
}
|
|
4567
|
+
*/
|
|
4568
|
+
static _waitForFieldReady(fieldName, form, callback, timeout = 5000) {
|
|
4569
|
+
if (!form) {
|
|
4570
|
+
console.warn(`FTable: No form provided for waitForFieldReady('${fieldName}')`);
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
const select = form.querySelector(`[name="${fieldName}"]`);
|
|
4575
|
+
if (!select || select.tagName !== 'SELECT') {
|
|
4576
|
+
console.warn(`FTable: Field '${fieldName}' not found or not a <select>`);
|
|
4577
|
+
return;
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
// Wait for more than just placeholder/loading option
|
|
4581
|
+
if (select.options.length > 1) {
|
|
4582
|
+
callback();
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
// Observe when real options are added
|
|
4587
|
+
const observer = new MutationObserver(() => {
|
|
4588
|
+
if (select.options.length > 1) {
|
|
4589
|
+
observer.disconnect();
|
|
4590
|
+
callback();
|
|
4591
|
+
}
|
|
4592
|
+
});
|
|
4593
|
+
|
|
4594
|
+
observer.observe(select, { childList: true });
|
|
4595
|
+
|
|
4596
|
+
// Timeout fallback
|
|
4597
|
+
if (timeout > 0) {
|
|
4598
|
+
setTimeout(() => {
|
|
4599
|
+
observer.disconnect();
|
|
4600
|
+
if (select.options.length > 1) {
|
|
4601
|
+
callback();
|
|
4602
|
+
} else {
|
|
4603
|
+
console.warn(`FTable: Timeout waiting for field '${fieldName}' to load options`);
|
|
4604
|
+
}
|
|
4605
|
+
}, timeout);
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
static async waitForFieldReady(fieldName, form, timeout = 5000) {
|
|
4609
|
+
return new Promise((resolve) => {
|
|
4610
|
+
FTable._waitForFieldReady(fieldName, form, resolve, timeout);
|
|
4611
|
+
});
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
// Export for use
|
|
4616
|
+
//window.FTable = FTable;
|
|
4617
|
+
|
|
4618
|
+
// Usage example:
|
|
4619
|
+
/*
|
|
4620
|
+
const table = new FTable('#myTable', {
|
|
4621
|
+
title: 'My Data Table',
|
|
4622
|
+
paging: true,
|
|
4623
|
+
pageSize: 25,
|
|
4624
|
+
sorting: true,
|
|
4625
|
+
selecting: true,
|
|
4626
|
+
actions: {
|
|
4627
|
+
listAction: '/api/users',
|
|
4628
|
+
createAction: '/api/users',
|
|
4629
|
+
updateAction: '/api/users',
|
|
4630
|
+
deleteAction: '/api/users'
|
|
4631
|
+
},
|
|
4632
|
+
fields: {
|
|
4633
|
+
id: { key: true, list: false },
|
|
4634
|
+
name: {
|
|
4635
|
+
title: 'Name',
|
|
4636
|
+
type: 'text',
|
|
4637
|
+
inputAttributes: "maxlength=100 required"
|
|
4638
|
+
},
|
|
4639
|
+
email: {
|
|
4640
|
+
title: 'Email',
|
|
4641
|
+
type: 'email',
|
|
4642
|
+
width: '40%',
|
|
4643
|
+
inputAttributes: {
|
|
4644
|
+
pattern: '[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$'
|
|
4645
|
+
}
|
|
4646
|
+
},
|
|
4647
|
+
created: { title: 'Created', type: 'date', width: '30%' }
|
|
4648
|
+
},
|
|
4649
|
+
toolbarsearch: true,
|
|
4650
|
+
childTable: {
|
|
4651
|
+
title: 'Child Records',
|
|
4652
|
+
actions: {
|
|
4653
|
+
listAction: '/api/users/{id}/orders', // {id} will be replaced with parent record id
|
|
4654
|
+
createAction: '/api/users/{id}/orders',
|
|
4655
|
+
updateAction: '/api/orders',
|
|
4656
|
+
deleteAction: '/api/orders'
|
|
4657
|
+
},
|
|
4658
|
+
fields: {
|
|
4659
|
+
orderId: { key: true, list: false },
|
|
4660
|
+
orderDate: { title: 'Date', type: 'date' },
|
|
4661
|
+
amount: { title: 'Amount', type: 'number' }
|
|
4662
|
+
}
|
|
4663
|
+
},
|
|
4664
|
+
childTableColumnsVisible: true,
|
|
4665
|
+
|
|
4666
|
+
});
|
|
4667
|
+
|
|
4668
|
+
// Or dynamic child table
|
|
4669
|
+
childTable: async function(parentRecord) {
|
|
4670
|
+
return {
|
|
4671
|
+
title: `Orders for ${parentRecord.name}`,
|
|
4672
|
+
actions: {
|
|
4673
|
+
listAction: `/api/users/${parentRecord.id}/orders`
|
|
4674
|
+
},
|
|
4675
|
+
fields: {
|
|
4676
|
+
// Dynamic fields based on parent
|
|
4677
|
+
}
|
|
4678
|
+
};
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
// function for select options
|
|
4682
|
+
fields: {
|
|
4683
|
+
assignee: {
|
|
4684
|
+
title: 'Assigned To',
|
|
4685
|
+
type: 'select',
|
|
4686
|
+
options: async function(params) {
|
|
4687
|
+
// params contains dependsOnValue, dependsOnField, etc.
|
|
4688
|
+
const department = params.dependsOnValue;
|
|
4689
|
+
const response = await fetch(`/api/users?department=${department}`);
|
|
4690
|
+
return response.json();
|
|
4691
|
+
},
|
|
4692
|
+
dependsOn: 'department'
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
// child table:
|
|
4697
|
+
phoneNumbers: {
|
|
4698
|
+
title: 'Phones',
|
|
4699
|
+
display: (data) => {
|
|
4700
|
+
const img = document.createElement('img');
|
|
4701
|
+
img.className = 'child-opener-image';
|
|
4702
|
+
img.src = '/Content/images/Misc/phone.png';
|
|
4703
|
+
img.title = 'Edit phone numbers';
|
|
4704
|
+
img.style.cursor = 'pointer';
|
|
4705
|
+
|
|
4706
|
+
parentRow = img.closest('tr');
|
|
4707
|
+
img.addEventListener('click', () => {
|
|
4708
|
+
e.stopPropagation();
|
|
4709
|
+
if (parentRow.childRow) {
|
|
4710
|
+
myTable.closeChildTable(parentRow);
|
|
4711
|
+
} else {
|
|
4712
|
+
myTable.openChildTable( parentRow, {
|
|
4713
|
+
title: `${data.record.Name} - Phone numbers`,
|
|
4714
|
+
actions: {
|
|
4715
|
+
listAction: `/PagingPerson/PhoneList?PersonId=${data.record.PersonId}`,
|
|
4716
|
+
deleteAction: '/PagingPerson/DeletePhone',
|
|
4717
|
+
updateAction: '/PagingPerson/UpdatePhone',
|
|
4718
|
+
createAction: `/PagingPerson/CreatePhone?PersonId=${data.record.PersonId}`
|
|
4719
|
+
},
|
|
4720
|
+
fields: {
|
|
4721
|
+
PhoneId: { key: true },
|
|
4722
|
+
Number: { title: 'Number', type: 'text' },
|
|
4723
|
+
Type: { title: 'Type', options: { 0: 'Home', 1: 'Work', 2: 'Mobile' } }
|
|
4724
|
+
}
|
|
4725
|
+
}, (childTable) => {
|
|
4726
|
+
console.log('Child table created');
|
|
4727
|
+
};
|
|
4728
|
+
}
|
|
4729
|
+
});
|
|
4730
|
+
img.addEventListener('click', (e) => {
|
|
4731
|
+
e.stopPropagation();
|
|
4732
|
+
if (parentRow.childRow) {
|
|
4733
|
+
myTable.closeChildTable(parentRow);
|
|
4734
|
+
} else {
|
|
4735
|
+
myTable.openChildTable(parentRow, childOptions);
|
|
4736
|
+
}
|
|
4737
|
+
});
|
|
4738
|
+
|
|
4739
|
+
return img;
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
// Clear specific options cache
|
|
4744
|
+
table.clearOptionsCache('/api/countries');
|
|
4745
|
+
|
|
4746
|
+
table.load();
|
|
4747
|
+
*/
|
|
4748
|
+
|
|
4749
|
+
window.FTable = FTable;
|