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