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