@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/CHANGES.md ADDED
@@ -0,0 +1,8 @@
1
+ = 1.1.1 (2025/08/01) =
2
+ * Correct build and include esm
3
+
4
+ = 1.1.0 (2025/08/01) =
5
+ * Make sure all missing options are added and doc wiki is more up to date
6
+
7
+ = 1.0.0 (2025/07/31) =
8
+ * New version, independant of jQuery
package/ftable.esm.js CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  // Modern fTable - Vanilla JS Refactor
2
3
 
3
4
  const JTABLE_DEFAULT_MESSAGES = {
@@ -653,10 +654,6 @@ class FTableFormBuilder {
653
654
  }
654
655
  });
655
656
 
656
- /*if (this.options.formCreated) {
657
- this.options.formCreated(form, formType, record);
658
- }*/
659
-
660
657
  // Set up dependency listeners after all fields are created
661
658
  this.setupDependencyListeners(form);
662
659
 
@@ -1335,7 +1332,7 @@ class FTable extends FTableEventEmitter {
1335
1332
 
1336
1333
  this.options = this.mergeOptions(options);
1337
1334
  this.logger = new FTableLogger(this.options.logLevel);
1338
- this.userPrefs = new FTableUserPreferences('', this.options.saveFTableUserPreferencesMethod);
1335
+ this.userPrefs = new FTableUserPreferences('', this.options.saveUserPreferencesMethod);
1339
1336
  this.formBuilder = new FTableFormBuilder(this.options, this);
1340
1337
 
1341
1338
  this.state = {
@@ -1351,6 +1348,7 @@ class FTable extends FTableEventEmitter {
1351
1348
  this.elements = {};
1352
1349
  this.modals = {};
1353
1350
  this.searchTimeout = null; // For debouncing
1351
+ this.lastSortEvent = null;
1354
1352
  this._recalculatedOnce = false;
1355
1353
 
1356
1354
  // store it on the DOM too, so people can access it
@@ -1368,23 +1366,28 @@ class FTable extends FTableEventEmitter {
1368
1366
  animationsEnabled: true,
1369
1367
  loadingAnimationDelay: 1000,
1370
1368
  defaultDateFormat: 'yyyy-mm-dd',
1371
- saveFTableUserPreferences: true,
1372
- saveFTableUserPreferencesMethod: 'localStorage',
1369
+ saveUserPreferences: true,
1370
+ saveUserPreferencesMethod: 'localStorage',
1373
1371
  defaultSorting: '',
1374
1372
 
1375
1373
  // Paging
1376
1374
  paging: false,
1375
+ pageList: 'normal',
1377
1376
  pageSize: 10,
1378
1377
  gotoPageArea: 'combobox',
1379
1378
 
1380
1379
  // Sorting
1381
1380
  sorting: false,
1382
1381
  multiSorting: false,
1382
+ multiSortingCtrlKey: true,
1383
1383
 
1384
1384
  // Selection
1385
1385
  selecting: false,
1386
1386
  multiselect: false,
1387
1387
 
1388
+ // child tables
1389
+ openChildAsAccordion: false,
1390
+
1388
1391
  // Toolbar search
1389
1392
  toolbarsearch: false, // Enable/disable toolbar search row
1390
1393
  toolbarreset: true, // Show reset button
@@ -1621,6 +1624,9 @@ class FTable extends FTableEventEmitter {
1621
1624
  if (field.list === undefined) {
1622
1625
  field.list = true;
1623
1626
  }
1627
+ if (field.sorting === undefined) {
1628
+ field.sorting = true;
1629
+ }
1624
1630
  if (!field.hasOwnProperty('visibility')) {
1625
1631
  field.visibility = 'visible';
1626
1632
  }
@@ -1748,9 +1754,9 @@ class FTable extends FTableEventEmitter {
1748
1754
  });
1749
1755
 
1750
1756
  // Add selecting column if enabled
1751
- if (this.options.selecting) {
1757
+ if (this.options.selecting && this.options.selectingCheckboxes) {
1752
1758
  const selectHeader = FTableDOMHelper.create('th', {
1753
- className: 'ftable-column-header ftable-column-header-select',
1759
+ className: `ftable-column-header ftable-column-header-select`,
1754
1760
  parent: headerRow
1755
1761
  });
1756
1762
 
@@ -1770,7 +1776,7 @@ class FTable extends FTableEventEmitter {
1770
1776
  this.columnList.forEach(fieldName => {
1771
1777
  const field = this.options.fields[fieldName];
1772
1778
  const th = FTableDOMHelper.create('th', {
1773
- className: 'ftable-column-header',
1779
+ className: `ftable-column-header ${field.listClass || ''} ${field.listClassHeader || ''}`,
1774
1780
  attributes: { 'data-field-name': fieldName },
1775
1781
  parent: headerRow
1776
1782
  });
@@ -1785,6 +1791,10 @@ class FTable extends FTableEventEmitter {
1785
1791
  parent: th
1786
1792
  });
1787
1793
 
1794
+ if (field.tooltip) {
1795
+ container.setAttribute('title', field.tooltip);
1796
+ }
1797
+
1788
1798
  FTableDOMHelper.create('span', {
1789
1799
  className: 'ftable-column-header-text',
1790
1800
  text: field.title || fieldName,
@@ -1794,7 +1804,12 @@ class FTable extends FTableEventEmitter {
1794
1804
  // Make sortable if enabled
1795
1805
  if (this.options.sorting && field.sorting !== false) {
1796
1806
  FTableDOMHelper.addClass(th, 'ftable-column-header-sortable');
1797
- th.addEventListener('click', () => this.sortByColumn(fieldName));
1807
+ th.addEventListener('click', (e) => {
1808
+ e.preventDefault();
1809
+ // Store event for multiSortingCtrlKey logic
1810
+ this.lastSortEvent = e;
1811
+ this.sortByColumn(fieldName);
1812
+ });
1798
1813
  }
1799
1814
 
1800
1815
  // Add resize handler if column resizing is enabled
@@ -1845,7 +1860,7 @@ class FTable extends FTableEventEmitter {
1845
1860
  });
1846
1861
 
1847
1862
  // Add empty cell for selecting column if enabled
1848
- if (this.options.selecting) {
1863
+ if (this.options.selecting && this.options.selectingCheckboxes) {
1849
1864
  FTableDOMHelper.create('th', { parent: searchRow });
1850
1865
  }
1851
1866
 
@@ -2131,14 +2146,14 @@ class FTable extends FTableEventEmitter {
2131
2146
  document.removeEventListener('mouseup', handleMouseUp);
2132
2147
 
2133
2148
  // Save column width preference if enabled
2134
- if (this.options.saveFTableUserPreferences) {
2149
+ if (this.options.saveUserPreferences) {
2135
2150
  this.saveColumnSettings();
2136
2151
  }
2137
2152
  };
2138
2153
  }
2139
2154
 
2140
2155
  saveColumnSettings() {
2141
- if (!this.options.saveFTableUserPreferences) return;
2156
+ if (!this.options.saveUserPreferences) return;
2142
2157
 
2143
2158
  const settings = {};
2144
2159
  this.columnList.forEach(fieldName => {
@@ -2156,7 +2171,7 @@ class FTable extends FTableEventEmitter {
2156
2171
  }
2157
2172
 
2158
2173
  saveState() {
2159
- if (!this.options.saveFTableUserPreferences) return;
2174
+ if (!this.options.saveUserPreferences) return;
2160
2175
 
2161
2176
  const state = {
2162
2177
  sorting: this.state.sorting,
@@ -2167,7 +2182,7 @@ class FTable extends FTableEventEmitter {
2167
2182
  }
2168
2183
 
2169
2184
  loadColumnSettings() {
2170
- if (!this.options.saveFTableUserPreferences) return;
2185
+ if (!this.options.saveUserPreferences) return;
2171
2186
 
2172
2187
  const settingsJson = this.userPrefs.get('column-settings');
2173
2188
  if (!settingsJson) return;
@@ -2187,7 +2202,7 @@ class FTable extends FTableEventEmitter {
2187
2202
  }
2188
2203
 
2189
2204
  loadState() {
2190
- if (!this.options.saveFTableUserPreferences) return;
2205
+ if (!this.options.saveUserPreferences) return;
2191
2206
 
2192
2207
  const stateJson = this.userPrefs.get('table-state');
2193
2208
  if (!stateJson) return;
@@ -2266,9 +2281,6 @@ class FTable extends FTableEventEmitter {
2266
2281
  onClick: () => {
2267
2282
  this.modals.addRecord.close();
2268
2283
  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
2284
  }
2273
2285
  },
2274
2286
  {
@@ -2621,6 +2633,11 @@ class FTable extends FTableEventEmitter {
2621
2633
  parent: this.elements.toolbarDiv
2622
2634
  });
2623
2635
 
2636
+ // Add title/tooltip if provided
2637
+ if (item.tooltip) {
2638
+ button.setAttribute('title', item.tooltip);
2639
+ }
2640
+
2624
2641
  // Add icon if provided
2625
2642
  if (item.icon) {
2626
2643
  const img = FTableDOMHelper.create('img', {
@@ -2683,12 +2700,12 @@ class FTable extends FTableEventEmitter {
2683
2700
  }
2684
2701
 
2685
2702
  setupFTableUserPreferences() {
2686
- if (this.options.saveFTableUserPreferences) {
2703
+ if (this.options.saveUserPreferences) {
2687
2704
  const prefix = this.userPrefs.generatePrefix(
2688
2705
  this.options.tableId || '',
2689
2706
  this.fieldList
2690
2707
  );
2691
- this.userPrefs = new FTableUserPreferences(prefix, this.options.saveFTableUserPreferencesMethod);
2708
+ this.userPrefs = new FTableUserPreferences(prefix, this.options.saveUserPreferencesMethod);
2692
2709
 
2693
2710
  // Load saved column settings
2694
2711
  this.loadState();
@@ -2860,7 +2877,7 @@ class FTable extends FTableEventEmitter {
2860
2877
  row.recordData = record;
2861
2878
 
2862
2879
  // Add selecting checkbox if enabled
2863
- if (this.options.selecting) {
2880
+ if (this.options.selecting && this.options.selectingCheckboxes) {
2864
2881
  this.addSelectingCell(row);
2865
2882
  }
2866
2883
 
@@ -3085,10 +3102,7 @@ class FTable extends FTableEventEmitter {
3085
3102
  this.modals.addRecord.close();
3086
3103
 
3087
3104
  // 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
- }
3105
+ this.emit('formClosed', { form: this.currentForm, formType: 'create', record: null });
3092
3106
 
3093
3107
  if (result.Message) {
3094
3108
  this.showInfo(result.Message);
@@ -3135,17 +3149,14 @@ class FTable extends FTableEventEmitter {
3135
3149
  this.modals.editRecord.close();
3136
3150
 
3137
3151
  // 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
- }
3152
+ this.emit('formClosed', { form: this.currentForm, formType: 'edit', record: this.currentEditingRow.recordData });
3142
3153
 
3143
3154
  // Update the row with new data
3144
3155
  this.updateRowData(this.currentEditingRow, result.Record || formData);
3145
3156
  if (result.Message) {
3146
3157
  this.showInfo(result.Message);
3147
3158
  }
3148
- this.emit('recordUpdated', { record: result.Record || formData });
3159
+ this.emit('recordUpdated', { record: result.Record || formData, row: this.currentEditingRow });
3149
3160
  } else {
3150
3161
  this.showError(result.Message || 'Update failed');
3151
3162
  }
@@ -3451,25 +3462,42 @@ class FTable extends FTableEventEmitter {
3451
3462
 
3452
3463
  // Sorting Methods
3453
3464
  sortByColumn(fieldName) {
3465
+ const field = this.options.fields[fieldName];
3466
+
3467
+ if (!field || field.sorting === false) return;
3468
+
3454
3469
  const existingSortIndex = this.state.sorting.findIndex(s => s.fieldName === fieldName);
3455
-
3456
- if (!this.options.multiSorting) {
3457
- this.state.sorting = [];
3458
- }
3459
-
3470
+ let isSorted = true;
3471
+ let newDirection = 'ASC';
3460
3472
  if (existingSortIndex >= 0) {
3461
- const currentSort = this.state.sorting[existingSortIndex];
3462
- if (currentSort.direction === 'ASC') {
3463
- currentSort.direction = 'DESC';
3473
+ const wasAsc = this.state.sorting[existingSortIndex].direction === 'ASC';
3474
+ if (wasAsc) {
3475
+ newDirection = 'DESC';
3476
+ this.state.sorting[existingSortIndex].direction = newDirection;
3464
3477
  } else {
3465
- this.state.sorting.splice(existingSortIndex, 1);
3478
+ this.state.sorting.splice(existingSortIndex,1);
3479
+ isSorted = false;
3466
3480
  }
3467
3481
  } else {
3468
- this.state.sorting.push({ fieldName, direction: 'ASC' });
3482
+ this.state.sorting.push({ fieldName, direction: newDirection });
3469
3483
  }
3470
-
3484
+
3485
+ // Handle multiSortingCtrlKey: did user press Ctrl/Cmd?
3486
+ const isCtrlPressed = this.lastSortEvent?.ctrlKey || this.lastSortEvent?.metaKey; // metaKey for Mac
3487
+
3488
+ if (this.options.multiSorting) {
3489
+ // If multiSorting is enabled, respect multiSortingCtrlKey
3490
+ if (this.options.multiSortingCtrlKey && !isCtrlPressed) {
3491
+ // Not using Ctrl → treat as single sort (clear others)
3492
+ this.state.sorting = isSorted ? [{ fieldName, direction: newDirection }] : [];
3493
+ }
3494
+ } else {
3495
+ // If multiSorting is disabled, always clear other sorts
3496
+ this.state.sorting = isSorted ? [{ fieldName, direction: newDirection }] : [];
3497
+ }
3498
+
3471
3499
  this.updateSortingHeaders();
3472
- this.load(); // Reload with new sorting
3500
+ this.load();
3473
3501
  this.saveState();
3474
3502
  }
3475
3503
 
@@ -3528,27 +3556,29 @@ class FTable extends FTableEventEmitter {
3528
3556
  this.createPageButton('‹', this.state.currentPage - 1, this.state.currentPage === 1, 'ftable-page-number-previous');
3529
3557
 
3530
3558
  // 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
- });
3559
+ if (this.options.pageList == 'normal') {
3560
+ const pageNumbers = this.calculatePageNumbers(totalPages);
3561
+ let lastNumber = 0;
3562
+
3563
+ pageNumbers.forEach(pageNum => {
3564
+ if (pageNum - lastNumber > 1) {
3565
+ FTableDOMHelper.create('span', {
3566
+ className: 'ftable-page-number-space',
3567
+ text: '...',
3568
+ parent: this.elements.pagingListArea
3569
+ });
3570
+ }
3571
+
3572
+ this.createPageButton(
3573
+ pageNum.toString(),
3574
+ pageNum,
3575
+ false,
3576
+ pageNum === this.state.currentPage ? 'ftable-page-number ftable-page-number-active' : 'ftable-page-number'
3577
+ );
3578
+
3579
+ lastNumber = pageNum;
3580
+ });
3581
+ }
3552
3582
 
3553
3583
  // Next and Last buttons
3554
3584
  this.createPageButton('›', this.state.currentPage + 1, this.state.currentPage >= totalPages, 'ftable-page-number-next');
@@ -3956,7 +3986,7 @@ class FTable extends FTableEventEmitter {
3956
3986
  if (columnIndex >= 0) {
3957
3987
  // Calculate actual column index (accounting for selecting column)
3958
3988
  let actualIndex = columnIndex + 1; // CSS nth-child is 1-based
3959
- if (this.options.selecting) {
3989
+ if (this.options.selecting && this.options.selectingCheckboxes) {
3960
3990
  actualIndex += 1; // Account for selecting column
3961
3991
  }
3962
3992
 
@@ -3973,7 +4003,7 @@ class FTable extends FTableEventEmitter {
3973
4003
  }
3974
4004
 
3975
4005
  // Save column settings
3976
- if (this.options.saveFTableUserPreferences) {
4006
+ if (this.options.saveUserPreferences) {
3977
4007
  this.saveColumnSettings();
3978
4008
  this.saveState(); // sorting might affect state
3979
4009
  }
@@ -4171,27 +4201,6 @@ class FTable extends FTableEventEmitter {
4171
4201
  });
4172
4202
  }
4173
4203
 
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
4204
  // Real-time updates via WebSocket
4196
4205
  enableRealTimeUpdates(websocketUrl) {
4197
4206
  if (!websocketUrl) return;
@@ -4315,6 +4324,15 @@ class FTable extends FTableEventEmitter {
4315
4324
  return this;
4316
4325
  }
4317
4326
 
4327
+ editRecordByKey(keyValue) {
4328
+ const row = this.getRowByKey(keyValue);
4329
+ if (row) {
4330
+ this.editRecord(row);
4331
+ } else {
4332
+ this.showError(`Record with key '${keyValue}' not found`);
4333
+ }
4334
+ }
4335
+
4318
4336
  async editRecordViaAjax(recordId, url, params = {}) {
4319
4337
  try {
4320
4338
  // Get the actual key field name (e.g., 'asset_id', 'user_id', etc.)
@@ -4354,6 +4372,10 @@ class FTable extends FTableEventEmitter {
4354
4372
  }
4355
4373
 
4356
4374
  openChildTable(parentRow, childOptions, onInit) {
4375
+ // Close any open child tables if accordion mode
4376
+ if (this.options.openChildAsAccordion) {
4377
+ this.closeAllChildTables();
4378
+ }
4357
4379
  // Prevent multiple child tables
4358
4380
  this.closeChildTable(parentRow);
4359
4381
 
@@ -4422,6 +4444,31 @@ class FTable extends FTableEventEmitter {
4422
4444
  }
4423
4445
  }
4424
4446
 
4447
+ closeAllChildTables() {
4448
+ Object.values(this.elements.tableRows).forEach(row => {
4449
+ if (row.childTable) {
4450
+ this.closeChildTable(row);
4451
+ }
4452
+ });
4453
+ }
4454
+
4455
+ getSortingInfo() {
4456
+ // Build sorted fields list with translated directions
4457
+ const messages = this.options.messages || {};
4458
+ const sortingInfo = this.state.sorting.map(s => {
4459
+ const field = this.options.fields[s.fieldName];
4460
+ const title = field?.title || s.fieldName;
4461
+
4462
+ // Translate direction
4463
+ const directionText = s.direction === 'ASC'
4464
+ ? (messages.ascending || 'ascending')
4465
+ : (messages.descending || 'descending');
4466
+
4467
+ return `${title} (${directionText})`;
4468
+ }).join(', ');
4469
+ return sortingInfo;
4470
+ }
4471
+
4425
4472
  renderSortingInfo() {
4426
4473
  if (!this.options.sortingInfoSelector || !this.options.sorting) return;
4427
4474
 
@@ -4443,20 +4490,10 @@ class FTable extends FTableEventEmitter {
4443
4490
  }
4444
4491
 
4445
4492
  // 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(', ');
4493
+ const sortingInfo = this.getSortingInfo();
4457
4494
 
4458
4495
  // Combine with prefix and suffix
4459
- container.innerHTML = `${prefix}${sortedItems}${suffix}`;
4496
+ container.innerHTML = `${prefix}${sortingInfo}${suffix}`;
4460
4497
 
4461
4498
  // Add reset sorting button
4462
4499
  if (this.state.sorting.length > 0) {
@@ -4747,3 +4784,5 @@ table.load();
4747
4784
  */
4748
4785
 
4749
4786
  window.FTable = FTable;
4787
+
4788
+ export default FTable;