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