@liedekef/ftable 1.0.0 → 1.1.0

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