@liedekef/ftable 1.3.5 → 1.3.7

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 (39) hide show
  1. package/ftable.esm.js +137 -377
  2. package/ftable.js +137 -377
  3. package/ftable.min.js +2 -2
  4. package/ftable.umd.js +137 -377
  5. package/localization/ftable.nl.js +4 -1
  6. package/package.json +1 -1
  7. package/themes/basic/ftable_basic.css +15 -6
  8. package/themes/basic/ftable_basic.min.css +1 -1
  9. package/themes/ftable_theme_base.less +17 -6
  10. package/themes/lightcolor/blue/ftable.css +15 -6
  11. package/themes/lightcolor/blue/ftable.min.css +1 -1
  12. package/themes/lightcolor/gray/ftable.css +15 -6
  13. package/themes/lightcolor/gray/ftable.min.css +1 -1
  14. package/themes/lightcolor/green/ftable.css +15 -6
  15. package/themes/lightcolor/green/ftable.min.css +1 -1
  16. package/themes/lightcolor/orange/ftable.css +15 -6
  17. package/themes/lightcolor/orange/ftable.min.css +1 -1
  18. package/themes/lightcolor/red/ftable.css +15 -6
  19. package/themes/lightcolor/red/ftable.min.css +1 -1
  20. package/themes/metro/blue/ftable.css +15 -6
  21. package/themes/metro/blue/ftable.min.css +1 -1
  22. package/themes/metro/brown/ftable.css +15 -6
  23. package/themes/metro/brown/ftable.min.css +1 -1
  24. package/themes/metro/crimson/ftable.css +15 -6
  25. package/themes/metro/crimson/ftable.min.css +1 -1
  26. package/themes/metro/darkgray/ftable.css +15 -6
  27. package/themes/metro/darkgray/ftable.min.css +1 -1
  28. package/themes/metro/darkorange/ftable.css +15 -6
  29. package/themes/metro/darkorange/ftable.min.css +1 -1
  30. package/themes/metro/green/ftable.css +15 -6
  31. package/themes/metro/green/ftable.min.css +1 -1
  32. package/themes/metro/lightgray/ftable.css +15 -6
  33. package/themes/metro/lightgray/ftable.min.css +1 -1
  34. package/themes/metro/pink/ftable.css +15 -6
  35. package/themes/metro/pink/ftable.min.css +1 -1
  36. package/themes/metro/purple/ftable.css +15 -6
  37. package/themes/metro/purple/ftable.min.css +1 -1
  38. package/themes/metro/red/ftable.css +15 -6
  39. package/themes/metro/red/ftable.min.css +1 -1
package/ftable.umd.js CHANGED
@@ -34,7 +34,8 @@
34
34
  printTable: '🖨️ Print',
35
35
  cloneRecord: 'Clone Record',
36
36
  resetTable: 'Reset table',
37
- resetTableConfirm: 'This will reset all columns, pagesize, sorting to their defaults. Do you want to continue?',
37
+ resetTableConfirm: 'This will reset column visibility, column widths and page size to their defaults. Do you want to continue?',
38
+ resetTableTooltip: 'Resets column visibility, column widths and page size to defaults. Sorting is not affected.',
38
39
  resetSearch: 'Reset'
39
40
  };
40
41
 
@@ -623,124 +624,46 @@ class FTableFormBuilder {
623
624
  this.options = options;
624
625
  this.dependencies = new Map(); // Track field dependencies
625
626
  this.optionsCache = new FTableOptionsCache();
626
- this.originalFieldOptions = new Map(); // Store original field.options
627
- this.resolvedFieldOptions = new Map(); // Store resolved options per context
628
-
629
- // Initialize with empty cache objects
630
- Object.keys(this.options.fields || {}).forEach(fieldName => {
631
- this.resolvedFieldOptions.set(fieldName, {});
632
- });
633
- Object.entries(this.options.fields).forEach(([fieldName, field]) => {
634
- this.originalFieldOptions.set(fieldName, {
635
- options: field.options,
636
- searchOptions: field.searchOptions
637
- });
638
- });
639
627
  }
640
628
 
641
- // Get options for specific context
629
+ // Get options for a field, respecting context ('search' prefers searchOptions over options).
630
+ // URL-level caching and concurrent-request deduplication is handled by FTableOptionsCache
631
+ // inside resolveOptions
642
632
  async getFieldOptions(fieldName, context = 'table', params = {}) {
643
633
  const field = this.options.fields[fieldName];
644
- const stored = this.originalFieldOptions.get(fieldName);
645
634
 
646
- // Determine which options source to use for this context
647
- let originalOptions;
648
- if (context === 'search') {
649
- // Prefer searchOptions; fall back to regular options
650
- originalOptions = stored?.searchOptions ?? stored?.options;
651
- } else {
652
- originalOptions = stored?.options;
653
- }
635
+ // For search context, prefer searchOptions and fall back to options
636
+ const optionsSource = (context === 'search')
637
+ ? (field.searchOptions ?? field.options)
638
+ : field.options;
654
639
 
655
- // If no options or already resolved for this context with same params, return cached
656
- if (!originalOptions) {
657
- return null;
658
- }
640
+ if (!optionsSource) return null;
659
641
 
660
- // Determine if we should skip caching for this specific context
661
- const shouldSkipCache = this.shouldForceRefreshForContext(field, context, params);
662
- const cacheKey = this.generateOptionsCacheKey(context, params);
663
- // Skip cache if configured or forceRefresh requested
664
- if (!shouldSkipCache && !params.forceRefresh) {
665
- const cached = this.resolvedFieldOptions.get(fieldName)[cacheKey];
666
- if (cached) return cached;
667
- }
642
+ const noCache = this.shouldSkipCache(field, context, params);
668
643
 
669
644
  try {
670
- // Create temp field with original options for resolution
671
- const tempField = { ...field, options: originalOptions };
672
- const resolved = await this.resolveOptions(tempField, {
673
- ...params
674
- }, context, shouldSkipCache);
675
-
676
- // we store the resolved option always
677
- this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
678
- return resolved;
645
+ return await this.resolveOptions(
646
+ { ...field, options: optionsSource },
647
+ params,
648
+ context,
649
+ noCache
650
+ );
679
651
  } catch (err) {
680
652
  console.error(`Failed to resolve options for ${fieldName} (${context}):`, err);
681
- return originalOptions;
682
- }
683
- }
684
-
685
- /**
686
- * Clear resolved options for specific field or all fields
687
- * @param {string|null} fieldName - Field name to clear, or null for all fields
688
- * @param {string|null} context - Context to clear ('table', 'create', 'edit'), or null for all contexts
689
- */
690
- clearResolvedOptions(fieldName = null, context = null) {
691
- if (fieldName) {
692
- // Clear specific field
693
- if (this.resolvedFieldOptions.has(fieldName)) {
694
- if (context) {
695
- // Clear specific context for specific field
696
- this.resolvedFieldOptions.get(fieldName)[context] = null;
697
- } else {
698
- // Clear all contexts for specific field
699
- this.resolvedFieldOptions.set(fieldName, { table: null, create: null, edit: null });
700
- }
701
- }
702
- } else {
703
- // Clear all fields
704
- if (context) {
705
- // Clear specific context for all fields
706
- this.resolvedFieldOptions.forEach((value, key) => {
707
- this.resolvedFieldOptions.get(key)[context] = null;
708
- });
709
- } else {
710
- // Clear everything
711
- this.resolvedFieldOptions.forEach((value, key) => {
712
- this.resolvedFieldOptions.set(key, { table: null, create: null, edit: null });
713
- });
714
- }
653
+ return optionsSource;
715
654
  }
716
655
  }
717
656
 
718
- // Helper method to determine caching behavior
719
- shouldForceRefreshForContext(field, context, params) {
720
- // Rename to reflect what it actually does now
657
+ // Determine whether to bypass the URL cache for this field/context
658
+ shouldSkipCache(field, context, params) {
659
+ if (params.forceRefresh) return true;
721
660
  if (!field.noCache) return false;
722
-
723
661
  if (typeof field.noCache === 'boolean') return field.noCache;
724
662
  if (typeof field.noCache === 'function') return field.noCache({ context, ...params });
725
663
  if (typeof field.noCache === 'object') return field.noCache[context] === true;
726
-
727
664
  return false;
728
665
  }
729
666
 
730
- generateOptionsCacheKey(context, params) {
731
- // Create a unique key based on context and dependency values
732
- const keyParts = [context];
733
-
734
- if (params.dependedValues) {
735
- // Include relevant dependency values in the cache key
736
- Object.keys(params.dependedValues).sort().forEach(key => {
737
- keyParts.push(`${key}=${params.dependedValues[key]}`);
738
- });
739
- }
740
-
741
- return keyParts.join('|');
742
- }
743
-
744
667
  shouldIncludeField(field, formType) {
745
668
  if (formType === 'create') {
746
669
  return field.create !== false && !(field.key === true && field.create !== true);
@@ -813,12 +736,6 @@ class FTableFormBuilder {
813
736
  return form;
814
737
  }
815
738
 
816
- shouldResolveOptions(options) {
817
- return options &&
818
- (typeof options === 'function' || typeof options === 'string') &&
819
- !Array.isArray(options) &&
820
- !(typeof options === 'object' && !Array.isArray(options) && Object.keys(options).length > 0);
821
- }
822
739
 
823
740
  buildDependencyMap() {
824
741
  this.dependencies.clear();
@@ -2102,7 +2019,8 @@ class FTable extends FTableEventEmitter {
2102
2019
  saveUserPreferences: true,
2103
2020
  saveUserPreferencesMethod: 'localStorage',
2104
2021
  defaultSorting: '',
2105
- tableReset: false,
2022
+ tableResetButton: false,
2023
+ sortingResetButton: false,
2106
2024
 
2107
2025
  // Paging
2108
2026
  paging: false,
@@ -2177,13 +2095,13 @@ class FTable extends FTableEventEmitter {
2177
2095
  }
2178
2096
 
2179
2097
  // Start resolving in background
2180
- this.resolveAsyncFieldOptions().then(() => {
2098
+ this.resolveAllFieldOptionsForTable().then(() => {
2181
2099
  // re-render dynamic options rows — no server call
2182
2100
  // this is needed so that once options are resolved, the table shows correct display values
2183
2101
  // why: load() can actually finish faster than option resolving (and calling refreshDisplayValues
2184
2102
  // there is then pointless, since the resolving hasn't finished yet),
2185
2103
  // so we need to do it when the options are actually resolved (here)
2186
- // We could call await this.resolveAsyncFieldOptions() during load, but that would slow down the loading ...
2104
+ // We could call await this.resolveAllFieldOptionsForTable() during load, but that would slow down the loading ...
2187
2105
  setTimeout(() => {
2188
2106
  this.refreshDisplayValues();
2189
2107
  }, 0);
@@ -2312,6 +2230,23 @@ class FTable extends FTableEventEmitter {
2312
2230
  this.createPageSizeSelector();
2313
2231
  }
2314
2232
 
2233
+ // Table reset button — resets column visibility/widths and pageSize (not sorting)
2234
+ if (this.options.tableResetButton) {
2235
+ const resetTableBtn = FTableDOMHelper.create('button', {
2236
+ className: 'ftable-toolbar-item ftable-table-reset-btn',
2237
+ textContent: this.options.messages.resetTable || 'Reset table',
2238
+ title: this.options.messages.resetTableTooltip || 'Resets column visibility, column widths and page size to defaults.',
2239
+ type: 'button',
2240
+ parent: this.elements.rightArea
2241
+ });
2242
+ resetTableBtn.addEventListener('click', (e) => {
2243
+ e.preventDefault();
2244
+ const msg = this.options.messages.resetTableConfirm;
2245
+ this.modals.resetTable.setContent(`<p>${msg}</p>`);
2246
+ this.modals.resetTable.show();
2247
+ });
2248
+ }
2249
+
2315
2250
  }
2316
2251
 
2317
2252
  createPageSizeSelector() {
@@ -2404,22 +2339,17 @@ class FTable extends FTableEventEmitter {
2404
2339
  }
2405
2340
  }
2406
2341
 
2407
- async resolveAsyncFieldOptions() {
2342
+ async resolveAllFieldOptionsForTable() {
2343
+ this.tableOptionsCache = new Map();
2344
+
2408
2345
  const promises = this.columnList.map(async (fieldName) => {
2409
2346
  const field = this.options.fields[fieldName];
2410
2347
  if (field.action) return; // Skip action columns
2411
- const originalOptions = this.formBuilder.originalFieldOptions.get(fieldName)?.options;
2412
-
2413
- if (this.formBuilder.shouldResolveOptions(originalOptions)) {
2414
- try {
2415
- // Check if already resolved to avoid duplicate work
2416
- const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
2417
- if (!this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey]) {
2418
- await this.formBuilder.getFieldOptions(fieldName, 'table');
2419
- }
2420
- } catch (err) {
2421
- console.error(`Failed to resolve table options for ${fieldName}:`, err);
2422
- }
2348
+ try {
2349
+ const resolved = await this.formBuilder.getFieldOptions(fieldName, 'table');
2350
+ if (resolved) this.tableOptionsCache.set(fieldName, resolved);
2351
+ } catch (err) {
2352
+ console.error(`Failed to resolve table options for ${fieldName}:`, err);
2423
2353
  }
2424
2354
  });
2425
2355
 
@@ -2438,9 +2368,7 @@ class FTable extends FTableEventEmitter {
2438
2368
  const cell = row.querySelector(`td[data-field-name="${fieldName}"]`);
2439
2369
  if (!cell) continue;
2440
2370
 
2441
- // Get table-specific options
2442
- const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
2443
- const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
2371
+ const resolvedOptions = this.tableOptionsCache?.get(fieldName);
2444
2372
  const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
2445
2373
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
2446
2374
  }
@@ -3288,6 +3216,10 @@ class FTable extends FTableEventEmitter {
3288
3216
  this.createInfoModal();
3289
3217
  this.createLoadingModal();
3290
3218
 
3219
+ if (this.options.tableResetButton) {
3220
+ this.createResetTableModal();
3221
+ }
3222
+
3291
3223
  // Initialize them (create DOM) once
3292
3224
  Object.values(this.modals).forEach(modal => modal.create());
3293
3225
  }
@@ -3373,6 +3305,34 @@ class FTable extends FTableEventEmitter {
3373
3305
  });
3374
3306
  }
3375
3307
 
3308
+ createResetTableModal() {
3309
+ this.modals.resetTable = new FtableModal({
3310
+ parent: this.elements.mainContainer,
3311
+ title: this.options.messages.resetTable || 'Reset table',
3312
+ className: 'ftable-reset-table-modal',
3313
+ closeOnOverlayClick: this.options.closeOnOverlayClick,
3314
+ buttons: [
3315
+ {
3316
+ text: this.options.messages.cancel,
3317
+ className: 'ftable-dialog-cancelbutton',
3318
+ onClick: () => this.modals.resetTable.close()
3319
+ },
3320
+ {
3321
+ text: this.options.messages.yes,
3322
+ className: 'ftable-dialog-savebutton',
3323
+ onClick: () => {
3324
+ this.userPrefs.remove('column-settings');
3325
+ // Preserve current sorting, only reset column settings and pageSize
3326
+ this.userPrefs.set('table-state', JSON.stringify({
3327
+ sorting: this.state.sorting
3328
+ }));
3329
+ location.reload();
3330
+ }
3331
+ }
3332
+ ]
3333
+ });
3334
+ }
3335
+
3376
3336
  createErrorModal() {
3377
3337
  this.modals.error = new FtableModal({
3378
3338
  parent: this.elements.mainContainer,
@@ -3656,6 +3616,21 @@ class FTable extends FTableEventEmitter {
3656
3616
  });
3657
3617
  }
3658
3618
 
3619
+ // Sorting reset button — visible only when sorting differs from default
3620
+ if (this.options.sorting && this.options.sortingResetButton) {
3621
+ this.elements.sortingResetBtn = this.addToolbarButton({
3622
+ text: this.options.messages.resetSorting || 'Reset sorting',
3623
+ className: 'ftable-toolbar-item-sorting-reset',
3624
+ onClick: () => {
3625
+ this.state.sorting = [];
3626
+ this.updateSortingHeaders();
3627
+ this.load();
3628
+ this.saveState();
3629
+ }
3630
+ });
3631
+ FTableDOMHelper.hide(this.elements.sortingResetBtn); // hidden by default
3632
+ }
3633
+
3659
3634
  if (this.options.actions.createAction) {
3660
3635
  this.addToolbarButton({
3661
3636
  text: this.options.messages.addNewRecord,
@@ -4026,8 +4001,7 @@ class FTable extends FTableEventEmitter {
4026
4001
 
4027
4002
  addDataCell(row, record, fieldName) {
4028
4003
  const field = this.options.fields[fieldName];
4029
- const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
4030
- const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
4004
+ const resolvedOptions = this.tableOptionsCache?.get(fieldName);
4031
4005
  const value = this.getDisplayText(record, fieldName, resolvedOptions);
4032
4006
 
4033
4007
  const cell = FTableDOMHelper.create('td', {
@@ -4530,8 +4504,7 @@ class FTable extends FTableEventEmitter {
4530
4504
  if (!cell) return;
4531
4505
 
4532
4506
  // Get display text
4533
- const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
4534
- const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
4507
+ const resolvedOptions = this.tableOptionsCache?.get(fieldName);
4535
4508
  const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
4536
4509
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
4537
4510
  cell.className = `${field.listClass || ''} ${field.listClassEntry || ''}`.trim();
@@ -4697,19 +4670,45 @@ class FTable extends FTableEventEmitter {
4697
4670
  }
4698
4671
 
4699
4672
  updateSortingHeaders() {
4700
- // Clear all sorting classes
4673
+ // Clear all sorting classes and remove any existing sort badges
4701
4674
  const headers = this.elements.table.querySelectorAll('.ftable-column-header-sortable');
4702
4675
  headers.forEach(header => {
4703
4676
  FTableDOMHelper.removeClass(header, 'ftable-column-header-sorted-asc ftable-column-header-sorted-desc');
4677
+ const existing = header.querySelector('.ftable-sort-badge');
4678
+ if (existing) existing.remove();
4704
4679
  });
4705
-
4706
- // Apply current sorting classes
4707
- this.state.sorting.forEach(sort => {
4680
+
4681
+ // Apply current sorting classes and sort number badges
4682
+ this.state.sorting.forEach((sort, index) => {
4708
4683
  const header = this.elements.table.querySelector(`[data-field-name="${sort.fieldName}"]`);
4709
- if (header) {
4710
- FTableDOMHelper.addClass(header, `ftable-column-header-sorted-${sort.direction.toLowerCase()}`);
4684
+ if (!header) return;
4685
+
4686
+ FTableDOMHelper.addClass(header, `ftable-column-header-sorted-${sort.direction.toLowerCase()}`);
4687
+
4688
+ // Sort number badge — only show when multisorting with more than 1 active sort
4689
+ if (this.options.multiSorting && this.state.sorting.length > 1) {
4690
+ const container = header.querySelector('.ftable-column-header-container');
4691
+ if (container) {
4692
+ FTableDOMHelper.create('span', {
4693
+ className: 'ftable-sort-badge',
4694
+ textContent: String(index + 1),
4695
+ parent: container
4696
+ });
4697
+ }
4711
4698
  }
4712
4699
  });
4700
+
4701
+ // Update visibility of the sorting reset toolbar button
4702
+ this._updateSortingResetButton();
4703
+ }
4704
+
4705
+ _updateSortingResetButton() {
4706
+ if (!this.elements.sortingResetBtn) return;
4707
+ if (this.state.sorting.length === 0) {
4708
+ FTableDOMHelper.hide(this.elements.sortingResetBtn);
4709
+ } else {
4710
+ FTableDOMHelper.show(this.elements.sortingResetBtn);
4711
+ }
4713
4712
  }
4714
4713
 
4715
4714
  // Paging Methods
@@ -5291,8 +5290,8 @@ class FTable extends FTableEventEmitter {
5291
5290
  // this.emit('columnVisibilityChanged', { field: field });
5292
5291
  }
5293
5292
 
5294
- // Responsive helpers
5295
5293
  /*
5294
+ // Responsive helpers
5296
5295
  makeResponsive() {
5297
5296
  // Add responsive classes and behavior
5298
5297
  FTableDOMHelper.addClass(this.elements.mainContainer, 'ftable-responsive');
@@ -5337,200 +5336,6 @@ class FTable extends FTableEventEmitter {
5337
5336
  });
5338
5337
  }
5339
5338
 
5340
- // Advanced search functionality
5341
- enableSearch(options = {}) {
5342
- const searchOptions = {
5343
- placeholder: 'Search...',
5344
- debounceMs: 300,
5345
- searchFields: this.columnList,
5346
- ...options
5347
- };
5348
-
5349
- const searchContainer = FTableDOMHelper.create('div', {
5350
- className: 'ftable-search-container',
5351
- parent: this.elements.toolbarDiv
5352
- });
5353
-
5354
- const searchInput = FTableDOMHelper.create('input', {
5355
- attributes: {
5356
- type: 'text',
5357
- placeholder: searchOptions.placeholder,
5358
- class: 'ftable-search-input'
5359
- },
5360
- parent: searchContainer
5361
- });
5362
-
5363
- // Debounced search
5364
- let searchTimeout;
5365
- searchInput.addEventListener('input', (e) => {
5366
- clearTimeout(searchTimeout);
5367
- searchTimeout = setTimeout(() => {
5368
- this.performSearch(e.target.value, searchOptions.searchFields);
5369
- }, searchOptions.debounceMs);
5370
- });
5371
-
5372
- return searchInput;
5373
- }
5374
-
5375
- async performSearch(query, searchFields) {
5376
- if (!query.trim()) {
5377
- return this.load(); // Clear search
5378
- }
5379
-
5380
- const searchParams = {
5381
- search: query,
5382
- searchFields: searchFields.join(',')
5383
- };
5384
-
5385
- return this.load(searchParams);
5386
- }
5387
-
5388
- // Keyboard shortcuts
5389
- enableKeyboardShortcuts() {
5390
- document.addEventListener('keydown', (e) => {
5391
- // Only handle shortcuts when table has focus or is active
5392
- if (!this.elements.mainContainer.contains(document.activeElement)) return;
5393
-
5394
- switch (e.key) {
5395
- case 'n':
5396
- if (e.ctrlKey && this.options.actions.createAction) {
5397
- e.preventDefault();
5398
- this.showAddRecordForm();
5399
- }
5400
- break;
5401
- case 'r':
5402
- if (e.ctrlKey) {
5403
- e.preventDefault();
5404
- this.reload();
5405
- }
5406
- break;
5407
- case 'Delete':
5408
- if (this.options.actions.deleteAction) {
5409
- const selectedRows = this.getSelectedRows();
5410
- if (selectedRows.length > 0) {
5411
- e.preventDefault();
5412
- this.bulkDelete();
5413
- }
5414
- }
5415
- break;
5416
- case 'a':
5417
- if (e.ctrlKey && this.options.selecting && this.options.multiselect) {
5418
- e.preventDefault();
5419
- this.toggleSelectAll(true);
5420
- }
5421
- break;
5422
- case 'Escape':
5423
- // Close any open modals
5424
- Object.values(this.modals).forEach(modal => {
5425
- if (modal.isOpen) modal.close();
5426
- });
5427
- break;
5428
- }
5429
- });
5430
- }
5431
-
5432
- // Real-time updates via WebSocket
5433
- enableRealTimeUpdates(websocketUrl) {
5434
- if (!websocketUrl) return;
5435
-
5436
- this.websocket = new WebSocket(websocketUrl);
5437
-
5438
- this.websocket.onmessage = (event) => {
5439
- try {
5440
- const data = JSON.parse(event.data);
5441
- this.handleRealTimeUpdate(data);
5442
- } catch (error) {
5443
- this.logger.error('Failed to parse WebSocket message', error);
5444
- }
5445
- };
5446
-
5447
- this.websocket.onerror = (error) => {
5448
- this.logger.error('WebSocket error', error);
5449
- };
5450
-
5451
- this.websocket.onclose = () => {
5452
- this.logger.info('WebSocket connection closed');
5453
- // Attempt to reconnect after delay
5454
- setTimeout(() => {
5455
- if (this.websocket.readyState === WebSocket.CLOSED) {
5456
- this.enableRealTimeUpdates(websocketUrl);
5457
- }
5458
- }, 5000);
5459
- };
5460
- }
5461
-
5462
- handleRealTimeUpdate(data) {
5463
- switch (data.type) {
5464
- case 'record_added':
5465
- this.addRecordToTable(data.record);
5466
- break;
5467
- case 'record_updated':
5468
- this.updateRecordInTable(data.record);
5469
- break;
5470
- case 'record_deleted':
5471
- this.removeRecordFromTable(data.recordKey);
5472
- break;
5473
- case 'refresh':
5474
- this.reload();
5475
- break;
5476
- }
5477
- }
5478
-
5479
- addRecordToTable(record) {
5480
- const row = this.createTableRow(record);
5481
-
5482
- // Add to beginning or end based on sorting
5483
- if (this.state.sorting.length > 0) {
5484
- // Would need to calculate correct position based on sort
5485
- this.elements.tableBody.appendChild(row);
5486
- } else {
5487
- this.elements.tableBody.appendChild(row);
5488
- }
5489
-
5490
- this.state.records.push(record);
5491
- this.removeNoDataRow();
5492
- this.refreshRowStyles();
5493
-
5494
- // Show animation
5495
- if (this.options.animationsEnabled) {
5496
- this.showRowAnimation(row, 'added');
5497
- }
5498
- }
5499
-
5500
- updateRecordInTable(record) {
5501
- const keyValue = this.getKeyValue(record);
5502
- const existingRow = this.getRowByKey(keyValue);
5503
-
5504
- if (existingRow) {
5505
- this.updateRowData(existingRow, record);
5506
-
5507
- if (this.options.animationsEnabled) {
5508
- this.showRowAnimation(existingRow, 'updated');
5509
- }
5510
- }
5511
- }
5512
-
5513
- removeRecordFromTable(keyValue) {
5514
- const row = this.getRowByKey(keyValue);
5515
- if (row) {
5516
- this.removeRowFromTable(row);
5517
-
5518
- // Remove from state
5519
- this.state.records = this.state.records.filter(r =>
5520
- this.getKeyValue(r) !== keyValue
5521
- );
5522
- }
5523
- }
5524
-
5525
- showRowAnimation(row, type) {
5526
- const animationClass = `ftable-row-${type}`;
5527
- FTableDOMHelper.addClass(row, animationClass);
5528
-
5529
- setTimeout(() => {
5530
- FTableDOMHelper.removeClass(row, animationClass);
5531
- }, 2000);
5532
- }
5533
-
5534
5339
  // Plugin system for extensions
5535
5340
  use(plugin, options = {}) {
5536
5341
  if (typeof plugin === 'function') {
@@ -5711,65 +5516,20 @@ class FTable extends FTableEventEmitter {
5711
5516
 
5712
5517
  const messages = this.options.messages || {};
5713
5518
 
5714
- // Get prefix/suffix if defined
5715
- const prefix = messages.sortingInfoPrefix ? `<span class="ftable-sorting-prefix">${messages.sortingInfoPrefix}</span> ` : '';
5716
- const suffix = messages.sortingInfoSuffix ? ` <span class="ftable-sorting-suffix">${messages.sortingInfoSuffix}</span>` : '';
5717
-
5718
5519
  if (this.state.sorting.length === 0) {
5719
5520
  container.innerHTML = messages.sortingInfoNone || '';
5720
5521
  return;
5721
5522
  }
5722
5523
 
5524
+ // Get prefix/suffix if defined
5525
+ const prefix = messages.sortingInfoPrefix ? `<span class="ftable-sorting-prefix">${messages.sortingInfoPrefix}</span> ` : '';
5526
+ const suffix = messages.sortingInfoSuffix ? ` <span class="ftable-sorting-suffix">${messages.sortingInfoSuffix}</span>` : '';
5527
+
5723
5528
  // Build sorted fields list with translated directions
5724
5529
  const sortingInfo = this.getSortingInfo();
5725
5530
 
5726
5531
  // Combine with prefix and suffix
5727
5532
  container.innerHTML = `${prefix}${sortingInfo}${suffix}`;
5728
-
5729
- // Add reset sorting button
5730
- if (this.state.sorting.length > 0) {
5731
- const resetSortBtn = document.createElement('button');
5732
- resetSortBtn.textContent = messages.resetSorting || 'Reset Sorting';
5733
- resetSortBtn.style.marginLeft = '10px';
5734
- resetSortBtn.classList.add('ftable-sorting-reset-btn');
5735
- resetSortBtn.addEventListener('click', (e) => {
5736
- e.preventDefault();
5737
- this.state.sorting = [];
5738
- this.updateSortingHeaders();
5739
- this.load();
5740
- this.saveState();
5741
- });
5742
- container.appendChild(resetSortBtn);
5743
- }
5744
-
5745
- // Add reset table button if enabled
5746
- if (this.options.tableReset) {
5747
- const resetTableBtn = document.createElement('button');
5748
- resetTableBtn.textContent = messages.resetTable || 'Reset Table';
5749
- resetTableBtn.style.marginLeft = '10px';
5750
- resetTableBtn.classList.add('ftable-table-reset-btn');
5751
- resetTableBtn.addEventListener('click', (e) => {
5752
- e.preventDefault();
5753
- const confirmMsg = messages.resetTableConfirm;
5754
- if (confirm(confirmMsg)) {
5755
- this.userPrefs.remove('column-settings');
5756
- this.userPrefs.remove('table-state');
5757
-
5758
- // Clear any in-memory state that might affect rendering
5759
- this.state.sorting = [];
5760
- this.state.pageSize = this.options.pageSize;
5761
-
5762
- // Reset field visibility to default
5763
- this.columnList.forEach(fieldName => {
5764
- const field = this.options.fields[fieldName];
5765
- // Reset to default: hidden only if explicitly set
5766
- field.visibility = field.visibility === 'fixed' ? 'fixed' : 'visible';
5767
- });
5768
- location.reload();
5769
- }
5770
- });
5771
- container.appendChild(resetTableBtn);
5772
- }
5773
5533
  }
5774
5534
 
5775
5535
  /**
@@ -28,8 +28,11 @@ FTable.setMessages({
28
28
  sortingInfoPrefix: 'Sortering toegepast: ',
29
29
  ascending: 'Oplopend',
30
30
  descending: 'Aflopend',
31
+ resetTableConfirm: 'Hiermee worden de kolomzichtbaarheid, kolombreedtes en paginagrootte teruggezet naar de standaardinstellingen. Wilt u doorgaan?',
31
32
  sortingInfoNone: 'Geen sortering toegepast',
32
33
  csvExport: 'CSV',
33
34
  printTable: '🖨️ Print',
34
- cloneRecord: 'Clone Record'
35
+ cloneRecord: 'Clone Record',
36
+ resetSorting: 'Reset sorteren',
37
+ resetTable: 'Reset tabel'
35
38
  });