@liedekef/ftable 1.3.6 → 1.3.8

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 +123 -254
  2. package/ftable.js +123 -254
  3. package/ftable.min.js +2 -2
  4. package/ftable.umd.js +123 -254
  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
 
@@ -1331,7 +1332,23 @@ class FTableFormBuilder {
1331
1332
  });
1332
1333
 
1333
1334
  if (field.options) {
1334
- this.populateSelectOptions(select, field.options, value);
1335
+ // If a placeholder is defined and the options don't already start with an empty
1336
+ // option, prepend one so that populateSelectOptions handles selected-state correctly.
1337
+ let options = field.options;
1338
+ if (field.placeholder !== undefined) {
1339
+ const firstValue = Array.isArray(options)
1340
+ ? (options[0]?.Value ?? options[0]?.value ?? options[0])
1341
+ : Object.keys(options)[0];
1342
+ if (firstValue !== '' && firstValue !== undefined) {
1343
+ const emptyOption = Array.isArray(options)
1344
+ ? { Value: '', DisplayText: field.placeholder }
1345
+ : { '': field.placeholder };
1346
+ options = Array.isArray(options)
1347
+ ? [emptyOption, ...options]
1348
+ : Object.fromEntries([['', field.placeholder], ...Object.entries(options)]);
1349
+ }
1350
+ }
1351
+ this.populateSelectOptions(select, options, value);
1335
1352
  }
1336
1353
 
1337
1354
  return select;
@@ -2017,7 +2034,8 @@ class FTable extends FTableEventEmitter {
2017
2034
  saveUserPreferences: true,
2018
2035
  saveUserPreferencesMethod: 'localStorage',
2019
2036
  defaultSorting: '',
2020
- tableReset: false,
2037
+ tableResetButton: false,
2038
+ sortingResetButton: false,
2021
2039
 
2022
2040
  // Paging
2023
2041
  paging: false,
@@ -2227,6 +2245,23 @@ class FTable extends FTableEventEmitter {
2227
2245
  this.createPageSizeSelector();
2228
2246
  }
2229
2247
 
2248
+ // Table reset button — resets column visibility/widths and pageSize (not sorting)
2249
+ if (this.options.tableResetButton) {
2250
+ const resetTableBtn = FTableDOMHelper.create('button', {
2251
+ className: 'ftable-toolbar-item ftable-table-reset-btn',
2252
+ textContent: this.options.messages.resetTable || 'Reset table',
2253
+ title: this.options.messages.resetTableTooltip || 'Resets column visibility, column widths and page size to defaults.',
2254
+ type: 'button',
2255
+ parent: this.elements.rightArea
2256
+ });
2257
+ resetTableBtn.addEventListener('click', (e) => {
2258
+ e.preventDefault();
2259
+ const msg = this.options.messages.resetTableConfirm;
2260
+ this.modals.resetTable.setContent(`<p>${msg}</p>`);
2261
+ this.modals.resetTable.show();
2262
+ });
2263
+ }
2264
+
2230
2265
  }
2231
2266
 
2232
2267
  createPageSizeSelector() {
@@ -2825,7 +2860,7 @@ class FTable extends FTableEventEmitter {
2825
2860
  if (!hasEmptyFirst) {
2826
2861
  FTableDOMHelper.create('option', {
2827
2862
  value: '',
2828
- innerHTML: '&nbsp;',
2863
+ innerHTML: field.searchPlaceholder || field.placeholder || '&nbsp;',
2829
2864
  parent: select
2830
2865
  });
2831
2866
  }
@@ -3196,6 +3231,10 @@ class FTable extends FTableEventEmitter {
3196
3231
  this.createInfoModal();
3197
3232
  this.createLoadingModal();
3198
3233
 
3234
+ if (this.options.tableResetButton) {
3235
+ this.createResetTableModal();
3236
+ }
3237
+
3199
3238
  // Initialize them (create DOM) once
3200
3239
  Object.values(this.modals).forEach(modal => modal.create());
3201
3240
  }
@@ -3281,6 +3320,34 @@ class FTable extends FTableEventEmitter {
3281
3320
  });
3282
3321
  }
3283
3322
 
3323
+ createResetTableModal() {
3324
+ this.modals.resetTable = new FtableModal({
3325
+ parent: this.elements.mainContainer,
3326
+ title: this.options.messages.resetTable || 'Reset table',
3327
+ className: 'ftable-reset-table-modal',
3328
+ closeOnOverlayClick: this.options.closeOnOverlayClick,
3329
+ buttons: [
3330
+ {
3331
+ text: this.options.messages.cancel,
3332
+ className: 'ftable-dialog-cancelbutton',
3333
+ onClick: () => this.modals.resetTable.close()
3334
+ },
3335
+ {
3336
+ text: this.options.messages.yes,
3337
+ className: 'ftable-dialog-savebutton',
3338
+ onClick: () => {
3339
+ this.userPrefs.remove('column-settings');
3340
+ // Preserve current sorting, only reset column settings and pageSize
3341
+ this.userPrefs.set('table-state', JSON.stringify({
3342
+ sorting: this.state.sorting
3343
+ }));
3344
+ location.reload();
3345
+ }
3346
+ }
3347
+ ]
3348
+ });
3349
+ }
3350
+
3284
3351
  createErrorModal() {
3285
3352
  this.modals.error = new FtableModal({
3286
3353
  parent: this.elements.mainContainer,
@@ -3564,6 +3631,21 @@ class FTable extends FTableEventEmitter {
3564
3631
  });
3565
3632
  }
3566
3633
 
3634
+ // Sorting reset button — visible only when sorting differs from default
3635
+ if (this.options.sorting && this.options.sortingResetButton) {
3636
+ this.elements.sortingResetBtn = this.addToolbarButton({
3637
+ text: this.options.messages.resetSorting || 'Reset sorting',
3638
+ className: 'ftable-toolbar-item-sorting-reset',
3639
+ onClick: () => {
3640
+ this.state.sorting = [];
3641
+ this.updateSortingHeaders();
3642
+ this.load();
3643
+ this.saveState();
3644
+ }
3645
+ });
3646
+ FTableDOMHelper.hide(this.elements.sortingResetBtn); // hidden by default
3647
+ }
3648
+
3567
3649
  if (this.options.actions.createAction) {
3568
3650
  this.addToolbarButton({
3569
3651
  text: this.options.messages.addNewRecord,
@@ -4603,19 +4685,45 @@ class FTable extends FTableEventEmitter {
4603
4685
  }
4604
4686
 
4605
4687
  updateSortingHeaders() {
4606
- // Clear all sorting classes
4688
+ // Clear all sorting classes and remove any existing sort badges
4607
4689
  const headers = this.elements.table.querySelectorAll('.ftable-column-header-sortable');
4608
4690
  headers.forEach(header => {
4609
4691
  FTableDOMHelper.removeClass(header, 'ftable-column-header-sorted-asc ftable-column-header-sorted-desc');
4692
+ const existing = header.querySelector('.ftable-sort-badge');
4693
+ if (existing) existing.remove();
4610
4694
  });
4611
-
4612
- // Apply current sorting classes
4613
- this.state.sorting.forEach(sort => {
4695
+
4696
+ // Apply current sorting classes and sort number badges
4697
+ this.state.sorting.forEach((sort, index) => {
4614
4698
  const header = this.elements.table.querySelector(`[data-field-name="${sort.fieldName}"]`);
4615
- if (header) {
4616
- FTableDOMHelper.addClass(header, `ftable-column-header-sorted-${sort.direction.toLowerCase()}`);
4699
+ if (!header) return;
4700
+
4701
+ FTableDOMHelper.addClass(header, `ftable-column-header-sorted-${sort.direction.toLowerCase()}`);
4702
+
4703
+ // Sort number badge — only show when multisorting with more than 1 active sort
4704
+ if (this.options.multiSorting && this.state.sorting.length > 1) {
4705
+ const container = header.querySelector('.ftable-column-header-container');
4706
+ if (container) {
4707
+ FTableDOMHelper.create('span', {
4708
+ className: 'ftable-sort-badge',
4709
+ textContent: String(index + 1),
4710
+ parent: container
4711
+ });
4712
+ }
4617
4713
  }
4618
4714
  });
4715
+
4716
+ // Update visibility of the sorting reset toolbar button
4717
+ this._updateSortingResetButton();
4718
+ }
4719
+
4720
+ _updateSortingResetButton() {
4721
+ if (!this.elements.sortingResetBtn) return;
4722
+ if (this.state.sorting.length === 0) {
4723
+ FTableDOMHelper.hide(this.elements.sortingResetBtn);
4724
+ } else {
4725
+ FTableDOMHelper.show(this.elements.sortingResetBtn);
4726
+ }
4619
4727
  }
4620
4728
 
4621
4729
  // Paging Methods
@@ -5197,8 +5305,8 @@ class FTable extends FTableEventEmitter {
5197
5305
  // this.emit('columnVisibilityChanged', { field: field });
5198
5306
  }
5199
5307
 
5200
- // Responsive helpers
5201
5308
  /*
5309
+ // Responsive helpers
5202
5310
  makeResponsive() {
5203
5311
  // Add responsive classes and behavior
5204
5312
  FTableDOMHelper.addClass(this.elements.mainContainer, 'ftable-responsive');
@@ -5243,200 +5351,6 @@ class FTable extends FTableEventEmitter {
5243
5351
  });
5244
5352
  }
5245
5353
 
5246
- // Advanced search functionality
5247
- enableSearch(options = {}) {
5248
- const searchOptions = {
5249
- placeholder: 'Search...',
5250
- debounceMs: 300,
5251
- searchFields: this.columnList,
5252
- ...options
5253
- };
5254
-
5255
- const searchContainer = FTableDOMHelper.create('div', {
5256
- className: 'ftable-search-container',
5257
- parent: this.elements.toolbarDiv
5258
- });
5259
-
5260
- const searchInput = FTableDOMHelper.create('input', {
5261
- attributes: {
5262
- type: 'text',
5263
- placeholder: searchOptions.placeholder,
5264
- class: 'ftable-search-input'
5265
- },
5266
- parent: searchContainer
5267
- });
5268
-
5269
- // Debounced search
5270
- let searchTimeout;
5271
- searchInput.addEventListener('input', (e) => {
5272
- clearTimeout(searchTimeout);
5273
- searchTimeout = setTimeout(() => {
5274
- this.performSearch(e.target.value, searchOptions.searchFields);
5275
- }, searchOptions.debounceMs);
5276
- });
5277
-
5278
- return searchInput;
5279
- }
5280
-
5281
- async performSearch(query, searchFields) {
5282
- if (!query.trim()) {
5283
- return this.load(); // Clear search
5284
- }
5285
-
5286
- const searchParams = {
5287
- search: query,
5288
- searchFields: searchFields.join(',')
5289
- };
5290
-
5291
- return this.load(searchParams);
5292
- }
5293
-
5294
- // Keyboard shortcuts
5295
- enableKeyboardShortcuts() {
5296
- document.addEventListener('keydown', (e) => {
5297
- // Only handle shortcuts when table has focus or is active
5298
- if (!this.elements.mainContainer.contains(document.activeElement)) return;
5299
-
5300
- switch (e.key) {
5301
- case 'n':
5302
- if (e.ctrlKey && this.options.actions.createAction) {
5303
- e.preventDefault();
5304
- this.showAddRecordForm();
5305
- }
5306
- break;
5307
- case 'r':
5308
- if (e.ctrlKey) {
5309
- e.preventDefault();
5310
- this.reload();
5311
- }
5312
- break;
5313
- case 'Delete':
5314
- if (this.options.actions.deleteAction) {
5315
- const selectedRows = this.getSelectedRows();
5316
- if (selectedRows.length > 0) {
5317
- e.preventDefault();
5318
- this.bulkDelete();
5319
- }
5320
- }
5321
- break;
5322
- case 'a':
5323
- if (e.ctrlKey && this.options.selecting && this.options.multiselect) {
5324
- e.preventDefault();
5325
- this.toggleSelectAll(true);
5326
- }
5327
- break;
5328
- case 'Escape':
5329
- // Close any open modals
5330
- Object.values(this.modals).forEach(modal => {
5331
- if (modal.isOpen) modal.close();
5332
- });
5333
- break;
5334
- }
5335
- });
5336
- }
5337
-
5338
- // Real-time updates via WebSocket
5339
- enableRealTimeUpdates(websocketUrl) {
5340
- if (!websocketUrl) return;
5341
-
5342
- this.websocket = new WebSocket(websocketUrl);
5343
-
5344
- this.websocket.onmessage = (event) => {
5345
- try {
5346
- const data = JSON.parse(event.data);
5347
- this.handleRealTimeUpdate(data);
5348
- } catch (error) {
5349
- this.logger.error('Failed to parse WebSocket message', error);
5350
- }
5351
- };
5352
-
5353
- this.websocket.onerror = (error) => {
5354
- this.logger.error('WebSocket error', error);
5355
- };
5356
-
5357
- this.websocket.onclose = () => {
5358
- this.logger.info('WebSocket connection closed');
5359
- // Attempt to reconnect after delay
5360
- setTimeout(() => {
5361
- if (this.websocket.readyState === WebSocket.CLOSED) {
5362
- this.enableRealTimeUpdates(websocketUrl);
5363
- }
5364
- }, 5000);
5365
- };
5366
- }
5367
-
5368
- handleRealTimeUpdate(data) {
5369
- switch (data.type) {
5370
- case 'record_added':
5371
- this.addRecordToTable(data.record);
5372
- break;
5373
- case 'record_updated':
5374
- this.updateRecordInTable(data.record);
5375
- break;
5376
- case 'record_deleted':
5377
- this.removeRecordFromTable(data.recordKey);
5378
- break;
5379
- case 'refresh':
5380
- this.reload();
5381
- break;
5382
- }
5383
- }
5384
-
5385
- addRecordToTable(record) {
5386
- const row = this.createTableRow(record);
5387
-
5388
- // Add to beginning or end based on sorting
5389
- if (this.state.sorting.length > 0) {
5390
- // Would need to calculate correct position based on sort
5391
- this.elements.tableBody.appendChild(row);
5392
- } else {
5393
- this.elements.tableBody.appendChild(row);
5394
- }
5395
-
5396
- this.state.records.push(record);
5397
- this.removeNoDataRow();
5398
- this.refreshRowStyles();
5399
-
5400
- // Show animation
5401
- if (this.options.animationsEnabled) {
5402
- this.showRowAnimation(row, 'added');
5403
- }
5404
- }
5405
-
5406
- updateRecordInTable(record) {
5407
- const keyValue = this.getKeyValue(record);
5408
- const existingRow = this.getRowByKey(keyValue);
5409
-
5410
- if (existingRow) {
5411
- this.updateRowData(existingRow, record);
5412
-
5413
- if (this.options.animationsEnabled) {
5414
- this.showRowAnimation(existingRow, 'updated');
5415
- }
5416
- }
5417
- }
5418
-
5419
- removeRecordFromTable(keyValue) {
5420
- const row = this.getRowByKey(keyValue);
5421
- if (row) {
5422
- this.removeRowFromTable(row);
5423
-
5424
- // Remove from state
5425
- this.state.records = this.state.records.filter(r =>
5426
- this.getKeyValue(r) !== keyValue
5427
- );
5428
- }
5429
- }
5430
-
5431
- showRowAnimation(row, type) {
5432
- const animationClass = `ftable-row-${type}`;
5433
- FTableDOMHelper.addClass(row, animationClass);
5434
-
5435
- setTimeout(() => {
5436
- FTableDOMHelper.removeClass(row, animationClass);
5437
- }, 2000);
5438
- }
5439
-
5440
5354
  // Plugin system for extensions
5441
5355
  use(plugin, options = {}) {
5442
5356
  if (typeof plugin === 'function') {
@@ -5617,65 +5531,20 @@ class FTable extends FTableEventEmitter {
5617
5531
 
5618
5532
  const messages = this.options.messages || {};
5619
5533
 
5620
- // Get prefix/suffix if defined
5621
- const prefix = messages.sortingInfoPrefix ? `<span class="ftable-sorting-prefix">${messages.sortingInfoPrefix}</span> ` : '';
5622
- const suffix = messages.sortingInfoSuffix ? ` <span class="ftable-sorting-suffix">${messages.sortingInfoSuffix}</span>` : '';
5623
-
5624
5534
  if (this.state.sorting.length === 0) {
5625
5535
  container.innerHTML = messages.sortingInfoNone || '';
5626
5536
  return;
5627
5537
  }
5628
5538
 
5539
+ // Get prefix/suffix if defined
5540
+ const prefix = messages.sortingInfoPrefix ? `<span class="ftable-sorting-prefix">${messages.sortingInfoPrefix}</span> ` : '';
5541
+ const suffix = messages.sortingInfoSuffix ? ` <span class="ftable-sorting-suffix">${messages.sortingInfoSuffix}</span>` : '';
5542
+
5629
5543
  // Build sorted fields list with translated directions
5630
5544
  const sortingInfo = this.getSortingInfo();
5631
5545
 
5632
5546
  // Combine with prefix and suffix
5633
5547
  container.innerHTML = `${prefix}${sortingInfo}${suffix}`;
5634
-
5635
- // Add reset sorting button
5636
- if (this.state.sorting.length > 0) {
5637
- const resetSortBtn = document.createElement('button');
5638
- resetSortBtn.textContent = messages.resetSorting || 'Reset Sorting';
5639
- resetSortBtn.style.marginLeft = '10px';
5640
- resetSortBtn.classList.add('ftable-sorting-reset-btn');
5641
- resetSortBtn.addEventListener('click', (e) => {
5642
- e.preventDefault();
5643
- this.state.sorting = [];
5644
- this.updateSortingHeaders();
5645
- this.load();
5646
- this.saveState();
5647
- });
5648
- container.appendChild(resetSortBtn);
5649
- }
5650
-
5651
- // Add reset table button if enabled
5652
- if (this.options.tableReset) {
5653
- const resetTableBtn = document.createElement('button');
5654
- resetTableBtn.textContent = messages.resetTable || 'Reset Table';
5655
- resetTableBtn.style.marginLeft = '10px';
5656
- resetTableBtn.classList.add('ftable-table-reset-btn');
5657
- resetTableBtn.addEventListener('click', (e) => {
5658
- e.preventDefault();
5659
- const confirmMsg = messages.resetTableConfirm;
5660
- if (confirm(confirmMsg)) {
5661
- this.userPrefs.remove('column-settings');
5662
- this.userPrefs.remove('table-state');
5663
-
5664
- // Clear any in-memory state that might affect rendering
5665
- this.state.sorting = [];
5666
- this.state.pageSize = this.options.pageSize;
5667
-
5668
- // Reset field visibility to default
5669
- this.columnList.forEach(fieldName => {
5670
- const field = this.options.fields[fieldName];
5671
- // Reset to default: hidden only if explicitly set
5672
- field.visibility = field.visibility === 'fixed' ? 'fixed' : 'visible';
5673
- });
5674
- location.reload();
5675
- }
5676
- });
5677
- container.appendChild(resetTableBtn);
5678
- }
5679
5548
  }
5680
5549
 
5681
5550
  /**