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