@liedekef/ftable 1.0.0 → 1.1.1

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.
package/ftable.js CHANGED
@@ -1,4 +1,10 @@
1
- // Modern fTable - Vanilla JS Refactor
1
+
2
+ (function (global, factory) {
3
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4
+ typeof define === 'function' && define.amd ? define(factory) :
5
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.FTable = factory());
6
+ }(this, (function () {
7
+ // Modern fTable - Vanilla JS Refactor
2
8
 
3
9
  const JTABLE_DEFAULT_MESSAGES = {
4
10
  serverCommunicationError: 'An error occurred while communicating to the server.',
@@ -653,10 +659,6 @@ class FTableFormBuilder {
653
659
  }
654
660
  });
655
661
 
656
- /*if (this.options.formCreated) {
657
- this.options.formCreated(form, formType, record);
658
- }*/
659
-
660
662
  // Set up dependency listeners after all fields are created
661
663
  this.setupDependencyListeners(form);
662
664
 
@@ -1335,7 +1337,7 @@ class FTable extends FTableEventEmitter {
1335
1337
 
1336
1338
  this.options = this.mergeOptions(options);
1337
1339
  this.logger = new FTableLogger(this.options.logLevel);
1338
- this.userPrefs = new FTableUserPreferences('', this.options.saveFTableUserPreferencesMethod);
1340
+ this.userPrefs = new FTableUserPreferences('', this.options.saveUserPreferencesMethod);
1339
1341
  this.formBuilder = new FTableFormBuilder(this.options, this);
1340
1342
 
1341
1343
  this.state = {
@@ -1351,6 +1353,7 @@ class FTable extends FTableEventEmitter {
1351
1353
  this.elements = {};
1352
1354
  this.modals = {};
1353
1355
  this.searchTimeout = null; // For debouncing
1356
+ this.lastSortEvent = null;
1354
1357
  this._recalculatedOnce = false;
1355
1358
 
1356
1359
  // store it on the DOM too, so people can access it
@@ -1368,23 +1371,28 @@ class FTable extends FTableEventEmitter {
1368
1371
  animationsEnabled: true,
1369
1372
  loadingAnimationDelay: 1000,
1370
1373
  defaultDateFormat: 'yyyy-mm-dd',
1371
- saveFTableUserPreferences: true,
1372
- saveFTableUserPreferencesMethod: 'localStorage',
1374
+ saveUserPreferences: true,
1375
+ saveUserPreferencesMethod: 'localStorage',
1373
1376
  defaultSorting: '',
1374
1377
 
1375
1378
  // Paging
1376
1379
  paging: false,
1380
+ pageList: 'normal',
1377
1381
  pageSize: 10,
1378
1382
  gotoPageArea: 'combobox',
1379
1383
 
1380
1384
  // Sorting
1381
1385
  sorting: false,
1382
1386
  multiSorting: false,
1387
+ multiSortingCtrlKey: true,
1383
1388
 
1384
1389
  // Selection
1385
1390
  selecting: false,
1386
1391
  multiselect: false,
1387
1392
 
1393
+ // child tables
1394
+ openChildAsAccordion: false,
1395
+
1388
1396
  // Toolbar search
1389
1397
  toolbarsearch: false, // Enable/disable toolbar search row
1390
1398
  toolbarreset: true, // Show reset button
@@ -1621,6 +1629,9 @@ class FTable extends FTableEventEmitter {
1621
1629
  if (field.list === undefined) {
1622
1630
  field.list = true;
1623
1631
  }
1632
+ if (field.sorting === undefined) {
1633
+ field.sorting = true;
1634
+ }
1624
1635
  if (!field.hasOwnProperty('visibility')) {
1625
1636
  field.visibility = 'visible';
1626
1637
  }
@@ -1748,9 +1759,9 @@ class FTable extends FTableEventEmitter {
1748
1759
  });
1749
1760
 
1750
1761
  // Add selecting column if enabled
1751
- if (this.options.selecting) {
1762
+ if (this.options.selecting && this.options.selectingCheckboxes) {
1752
1763
  const selectHeader = FTableDOMHelper.create('th', {
1753
- className: 'ftable-column-header ftable-column-header-select',
1764
+ className: `ftable-column-header ftable-column-header-select`,
1754
1765
  parent: headerRow
1755
1766
  });
1756
1767
 
@@ -1770,7 +1781,7 @@ class FTable extends FTableEventEmitter {
1770
1781
  this.columnList.forEach(fieldName => {
1771
1782
  const field = this.options.fields[fieldName];
1772
1783
  const th = FTableDOMHelper.create('th', {
1773
- className: 'ftable-column-header',
1784
+ className: `ftable-column-header ${field.listClass || ''} ${field.listClassHeader || ''}`,
1774
1785
  attributes: { 'data-field-name': fieldName },
1775
1786
  parent: headerRow
1776
1787
  });
@@ -1785,6 +1796,10 @@ class FTable extends FTableEventEmitter {
1785
1796
  parent: th
1786
1797
  });
1787
1798
 
1799
+ if (field.tooltip) {
1800
+ container.setAttribute('title', field.tooltip);
1801
+ }
1802
+
1788
1803
  FTableDOMHelper.create('span', {
1789
1804
  className: 'ftable-column-header-text',
1790
1805
  text: field.title || fieldName,
@@ -1794,7 +1809,12 @@ class FTable extends FTableEventEmitter {
1794
1809
  // Make sortable if enabled
1795
1810
  if (this.options.sorting && field.sorting !== false) {
1796
1811
  FTableDOMHelper.addClass(th, 'ftable-column-header-sortable');
1797
- th.addEventListener('click', () => this.sortByColumn(fieldName));
1812
+ th.addEventListener('click', (e) => {
1813
+ e.preventDefault();
1814
+ // Store event for multiSortingCtrlKey logic
1815
+ this.lastSortEvent = e;
1816
+ this.sortByColumn(fieldName);
1817
+ });
1798
1818
  }
1799
1819
 
1800
1820
  // Add resize handler if column resizing is enabled
@@ -1845,7 +1865,7 @@ class FTable extends FTableEventEmitter {
1845
1865
  });
1846
1866
 
1847
1867
  // Add empty cell for selecting column if enabled
1848
- if (this.options.selecting) {
1868
+ if (this.options.selecting && this.options.selectingCheckboxes) {
1849
1869
  FTableDOMHelper.create('th', { parent: searchRow });
1850
1870
  }
1851
1871
 
@@ -2131,14 +2151,14 @@ class FTable extends FTableEventEmitter {
2131
2151
  document.removeEventListener('mouseup', handleMouseUp);
2132
2152
 
2133
2153
  // Save column width preference if enabled
2134
- if (this.options.saveFTableUserPreferences) {
2154
+ if (this.options.saveUserPreferences) {
2135
2155
  this.saveColumnSettings();
2136
2156
  }
2137
2157
  };
2138
2158
  }
2139
2159
 
2140
2160
  saveColumnSettings() {
2141
- if (!this.options.saveFTableUserPreferences) return;
2161
+ if (!this.options.saveUserPreferences) return;
2142
2162
 
2143
2163
  const settings = {};
2144
2164
  this.columnList.forEach(fieldName => {
@@ -2156,7 +2176,7 @@ class FTable extends FTableEventEmitter {
2156
2176
  }
2157
2177
 
2158
2178
  saveState() {
2159
- if (!this.options.saveFTableUserPreferences) return;
2179
+ if (!this.options.saveUserPreferences) return;
2160
2180
 
2161
2181
  const state = {
2162
2182
  sorting: this.state.sorting,
@@ -2167,7 +2187,7 @@ class FTable extends FTableEventEmitter {
2167
2187
  }
2168
2188
 
2169
2189
  loadColumnSettings() {
2170
- if (!this.options.saveFTableUserPreferences) return;
2190
+ if (!this.options.saveUserPreferences) return;
2171
2191
 
2172
2192
  const settingsJson = this.userPrefs.get('column-settings');
2173
2193
  if (!settingsJson) return;
@@ -2187,7 +2207,7 @@ class FTable extends FTableEventEmitter {
2187
2207
  }
2188
2208
 
2189
2209
  loadState() {
2190
- if (!this.options.saveFTableUserPreferences) return;
2210
+ if (!this.options.saveUserPreferences) return;
2191
2211
 
2192
2212
  const stateJson = this.userPrefs.get('table-state');
2193
2213
  if (!stateJson) return;
@@ -2266,9 +2286,6 @@ class FTable extends FTableEventEmitter {
2266
2286
  onClick: () => {
2267
2287
  this.modals.addRecord.close();
2268
2288
  this.emit('formClosed', { form: this.currentForm, formType: 'create', record: null });
2269
- /*if (this.options.formClosed) {
2270
- this.options.formClosed(this.currentForm, 'create', null);
2271
- }*/
2272
2289
  }
2273
2290
  },
2274
2291
  {
@@ -2621,6 +2638,11 @@ class FTable extends FTableEventEmitter {
2621
2638
  parent: this.elements.toolbarDiv
2622
2639
  });
2623
2640
 
2641
+ // Add title/tooltip if provided
2642
+ if (item.tooltip) {
2643
+ button.setAttribute('title', item.tooltip);
2644
+ }
2645
+
2624
2646
  // Add icon if provided
2625
2647
  if (item.icon) {
2626
2648
  const img = FTableDOMHelper.create('img', {
@@ -2683,12 +2705,12 @@ class FTable extends FTableEventEmitter {
2683
2705
  }
2684
2706
 
2685
2707
  setupFTableUserPreferences() {
2686
- if (this.options.saveFTableUserPreferences) {
2708
+ if (this.options.saveUserPreferences) {
2687
2709
  const prefix = this.userPrefs.generatePrefix(
2688
2710
  this.options.tableId || '',
2689
2711
  this.fieldList
2690
2712
  );
2691
- this.userPrefs = new FTableUserPreferences(prefix, this.options.saveFTableUserPreferencesMethod);
2713
+ this.userPrefs = new FTableUserPreferences(prefix, this.options.saveUserPreferencesMethod);
2692
2714
 
2693
2715
  // Load saved column settings
2694
2716
  this.loadState();
@@ -2860,7 +2882,7 @@ class FTable extends FTableEventEmitter {
2860
2882
  row.recordData = record;
2861
2883
 
2862
2884
  // Add selecting checkbox if enabled
2863
- if (this.options.selecting) {
2885
+ if (this.options.selecting && this.options.selectingCheckboxes) {
2864
2886
  this.addSelectingCell(row);
2865
2887
  }
2866
2888
 
@@ -3085,10 +3107,7 @@ class FTable extends FTableEventEmitter {
3085
3107
  this.modals.addRecord.close();
3086
3108
 
3087
3109
  // Call formClosed
3088
- // this.emit('formClosed', { form: this.currentForm, formType: 'create', record: null });
3089
- if (this.options.formClosed) {
3090
- this.options.formClosed(this.currentForm, 'create', null);
3091
- }
3110
+ this.emit('formClosed', { form: this.currentForm, formType: 'create', record: null });
3092
3111
 
3093
3112
  if (result.Message) {
3094
3113
  this.showInfo(result.Message);
@@ -3135,17 +3154,14 @@ class FTable extends FTableEventEmitter {
3135
3154
  this.modals.editRecord.close();
3136
3155
 
3137
3156
  // Call formClosed
3138
- // this.emit('formClosed', { form: this.currentForm, formType: 'edit', record: result.Record || formData });
3139
- if (this.options.formClosed) {
3140
- this.options.formClosed(this.currentForm, 'edit', this.currentEditingRow.recordData);
3141
- }
3157
+ this.emit('formClosed', { form: this.currentForm, formType: 'edit', record: this.currentEditingRow.recordData });
3142
3158
 
3143
3159
  // Update the row with new data
3144
3160
  this.updateRowData(this.currentEditingRow, result.Record || formData);
3145
3161
  if (result.Message) {
3146
3162
  this.showInfo(result.Message);
3147
3163
  }
3148
- this.emit('recordUpdated', { record: result.Record || formData });
3164
+ this.emit('recordUpdated', { record: result.Record || formData, row: this.currentEditingRow });
3149
3165
  } else {
3150
3166
  this.showError(result.Message || 'Update failed');
3151
3167
  }
@@ -3451,25 +3467,42 @@ class FTable extends FTableEventEmitter {
3451
3467
 
3452
3468
  // Sorting Methods
3453
3469
  sortByColumn(fieldName) {
3470
+ const field = this.options.fields[fieldName];
3471
+
3472
+ if (!field || field.sorting === false) return;
3473
+
3454
3474
  const existingSortIndex = this.state.sorting.findIndex(s => s.fieldName === fieldName);
3455
-
3456
- if (!this.options.multiSorting) {
3457
- this.state.sorting = [];
3458
- }
3459
-
3475
+ let isSorted = true;
3476
+ let newDirection = 'ASC';
3460
3477
  if (existingSortIndex >= 0) {
3461
- const currentSort = this.state.sorting[existingSortIndex];
3462
- if (currentSort.direction === 'ASC') {
3463
- currentSort.direction = 'DESC';
3478
+ const wasAsc = this.state.sorting[existingSortIndex].direction === 'ASC';
3479
+ if (wasAsc) {
3480
+ newDirection = 'DESC';
3481
+ this.state.sorting[existingSortIndex].direction = newDirection;
3464
3482
  } else {
3465
- this.state.sorting.splice(existingSortIndex, 1);
3483
+ this.state.sorting.splice(existingSortIndex,1);
3484
+ isSorted = false;
3466
3485
  }
3467
3486
  } else {
3468
- this.state.sorting.push({ fieldName, direction: 'ASC' });
3487
+ this.state.sorting.push({ fieldName, direction: newDirection });
3469
3488
  }
3470
-
3489
+
3490
+ // Handle multiSortingCtrlKey: did user press Ctrl/Cmd?
3491
+ const isCtrlPressed = this.lastSortEvent?.ctrlKey || this.lastSortEvent?.metaKey; // metaKey for Mac
3492
+
3493
+ if (this.options.multiSorting) {
3494
+ // If multiSorting is enabled, respect multiSortingCtrlKey
3495
+ if (this.options.multiSortingCtrlKey && !isCtrlPressed) {
3496
+ // Not using Ctrl → treat as single sort (clear others)
3497
+ this.state.sorting = isSorted ? [{ fieldName, direction: newDirection }] : [];
3498
+ }
3499
+ } else {
3500
+ // If multiSorting is disabled, always clear other sorts
3501
+ this.state.sorting = isSorted ? [{ fieldName, direction: newDirection }] : [];
3502
+ }
3503
+
3471
3504
  this.updateSortingHeaders();
3472
- this.load(); // Reload with new sorting
3505
+ this.load();
3473
3506
  this.saveState();
3474
3507
  }
3475
3508
 
@@ -3528,27 +3561,29 @@ class FTable extends FTableEventEmitter {
3528
3561
  this.createPageButton('‹', this.state.currentPage - 1, this.state.currentPage === 1, 'ftable-page-number-previous');
3529
3562
 
3530
3563
  // Page numbers
3531
- const pageNumbers = this.calculatePageNumbers(totalPages);
3532
- let lastNumber = 0;
3533
-
3534
- pageNumbers.forEach(pageNum => {
3535
- if (pageNum - lastNumber > 1) {
3536
- FTableDOMHelper.create('span', {
3537
- className: 'ftable-page-number-space',
3538
- text: '...',
3539
- parent: this.elements.pagingListArea
3540
- });
3541
- }
3542
-
3543
- this.createPageButton(
3544
- pageNum.toString(),
3545
- pageNum,
3546
- false,
3547
- pageNum === this.state.currentPage ? 'ftable-page-number ftable-page-number-active' : 'ftable-page-number'
3548
- );
3549
-
3550
- lastNumber = pageNum;
3551
- });
3564
+ if (this.options.pageList == 'normal') {
3565
+ const pageNumbers = this.calculatePageNumbers(totalPages);
3566
+ let lastNumber = 0;
3567
+
3568
+ pageNumbers.forEach(pageNum => {
3569
+ if (pageNum - lastNumber > 1) {
3570
+ FTableDOMHelper.create('span', {
3571
+ className: 'ftable-page-number-space',
3572
+ text: '...',
3573
+ parent: this.elements.pagingListArea
3574
+ });
3575
+ }
3576
+
3577
+ this.createPageButton(
3578
+ pageNum.toString(),
3579
+ pageNum,
3580
+ false,
3581
+ pageNum === this.state.currentPage ? 'ftable-page-number ftable-page-number-active' : 'ftable-page-number'
3582
+ );
3583
+
3584
+ lastNumber = pageNum;
3585
+ });
3586
+ }
3552
3587
 
3553
3588
  // Next and Last buttons
3554
3589
  this.createPageButton('›', this.state.currentPage + 1, this.state.currentPage >= totalPages, 'ftable-page-number-next');
@@ -3956,7 +3991,7 @@ class FTable extends FTableEventEmitter {
3956
3991
  if (columnIndex >= 0) {
3957
3992
  // Calculate actual column index (accounting for selecting column)
3958
3993
  let actualIndex = columnIndex + 1; // CSS nth-child is 1-based
3959
- if (this.options.selecting) {
3994
+ if (this.options.selecting && this.options.selectingCheckboxes) {
3960
3995
  actualIndex += 1; // Account for selecting column
3961
3996
  }
3962
3997
 
@@ -3973,7 +4008,7 @@ class FTable extends FTableEventEmitter {
3973
4008
  }
3974
4009
 
3975
4010
  // Save column settings
3976
- if (this.options.saveFTableUserPreferences) {
4011
+ if (this.options.saveUserPreferences) {
3977
4012
  this.saveColumnSettings();
3978
4013
  this.saveState(); // sorting might affect state
3979
4014
  }
@@ -4171,27 +4206,6 @@ class FTable extends FTableEventEmitter {
4171
4206
  });
4172
4207
  }
4173
4208
 
4174
- // Performance optimization for large datasets
4175
- enableVirtualScrolling(options = {}) {
4176
- const virtualOptions = {
4177
- rowHeight: 40,
4178
- overscan: 5,
4179
- ...options
4180
- };
4181
-
4182
- // This would implement virtual scrolling for performance with large datasets
4183
- // Simplified version - full implementation would be more complex
4184
- this.virtualScrolling = {
4185
- enabled: true,
4186
- ...virtualOptions,
4187
- visibleRange: { start: 0, end: 0 },
4188
- scrollContainer: null
4189
- };
4190
-
4191
- // Replace table body with virtual scroll container
4192
- // Implementation would calculate visible rows and only render those
4193
- }
4194
-
4195
4209
  // Real-time updates via WebSocket
4196
4210
  enableRealTimeUpdates(websocketUrl) {
4197
4211
  if (!websocketUrl) return;
@@ -4315,6 +4329,15 @@ class FTable extends FTableEventEmitter {
4315
4329
  return this;
4316
4330
  }
4317
4331
 
4332
+ editRecordByKey(keyValue) {
4333
+ const row = this.getRowByKey(keyValue);
4334
+ if (row) {
4335
+ this.editRecord(row);
4336
+ } else {
4337
+ this.showError(`Record with key '${keyValue}' not found`);
4338
+ }
4339
+ }
4340
+
4318
4341
  async editRecordViaAjax(recordId, url, params = {}) {
4319
4342
  try {
4320
4343
  // Get the actual key field name (e.g., 'asset_id', 'user_id', etc.)
@@ -4354,6 +4377,10 @@ class FTable extends FTableEventEmitter {
4354
4377
  }
4355
4378
 
4356
4379
  openChildTable(parentRow, childOptions, onInit) {
4380
+ // Close any open child tables if accordion mode
4381
+ if (this.options.openChildAsAccordion) {
4382
+ this.closeAllChildTables();
4383
+ }
4357
4384
  // Prevent multiple child tables
4358
4385
  this.closeChildTable(parentRow);
4359
4386
 
@@ -4422,6 +4449,31 @@ class FTable extends FTableEventEmitter {
4422
4449
  }
4423
4450
  }
4424
4451
 
4452
+ closeAllChildTables() {
4453
+ Object.values(this.elements.tableRows).forEach(row => {
4454
+ if (row.childTable) {
4455
+ this.closeChildTable(row);
4456
+ }
4457
+ });
4458
+ }
4459
+
4460
+ getSortingInfo() {
4461
+ // Build sorted fields list with translated directions
4462
+ const messages = this.options.messages || {};
4463
+ const sortingInfo = this.state.sorting.map(s => {
4464
+ const field = this.options.fields[s.fieldName];
4465
+ const title = field?.title || s.fieldName;
4466
+
4467
+ // Translate direction
4468
+ const directionText = s.direction === 'ASC'
4469
+ ? (messages.ascending || 'ascending')
4470
+ : (messages.descending || 'descending');
4471
+
4472
+ return `${title} (${directionText})`;
4473
+ }).join(', ');
4474
+ return sortingInfo;
4475
+ }
4476
+
4425
4477
  renderSortingInfo() {
4426
4478
  if (!this.options.sortingInfoSelector || !this.options.sorting) return;
4427
4479
 
@@ -4443,20 +4495,10 @@ class FTable extends FTableEventEmitter {
4443
4495
  }
4444
4496
 
4445
4497
  // Build sorted fields list with translated directions
4446
- const sortedItems = this.state.sorting.map(s => {
4447
- const field = this.options.fields[s.fieldName];
4448
- const title = field?.title || s.fieldName;
4449
-
4450
- // Translate direction
4451
- const directionText = s.direction === 'ASC'
4452
- ? (messages.ascending || 'ascending')
4453
- : (messages.descending || 'descending');
4454
-
4455
- return `${title} (${directionText})`;
4456
- }).join(', ');
4498
+ const sortingInfo = this.getSortingInfo();
4457
4499
 
4458
4500
  // Combine with prefix and suffix
4459
- container.innerHTML = `${prefix}${sortedItems}${suffix}`;
4501
+ container.innerHTML = `${prefix}${sortingInfo}${suffix}`;
4460
4502
 
4461
4503
  // Add reset sorting button
4462
4504
  if (this.state.sorting.length > 0) {
@@ -4747,3 +4789,6 @@ table.load();
4747
4789
  */
4748
4790
 
4749
4791
  window.FTable = FTable;
4792
+
4793
+ return FTable;
4794
+ })));