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