@liedekef/ftable 1.3.5 → 1.3.7
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.esm.js +137 -377
- package/ftable.js +137 -377
- package/ftable.min.js +2 -2
- package/ftable.umd.js +137 -377
- package/localization/ftable.nl.js +4 -1
- package/package.json +1 -1
- package/themes/basic/ftable_basic.css +15 -6
- package/themes/basic/ftable_basic.min.css +1 -1
- package/themes/ftable_theme_base.less +17 -6
- package/themes/lightcolor/blue/ftable.css +15 -6
- package/themes/lightcolor/blue/ftable.min.css +1 -1
- package/themes/lightcolor/gray/ftable.css +15 -6
- package/themes/lightcolor/gray/ftable.min.css +1 -1
- package/themes/lightcolor/green/ftable.css +15 -6
- package/themes/lightcolor/green/ftable.min.css +1 -1
- package/themes/lightcolor/orange/ftable.css +15 -6
- package/themes/lightcolor/orange/ftable.min.css +1 -1
- package/themes/lightcolor/red/ftable.css +15 -6
- package/themes/lightcolor/red/ftable.min.css +1 -1
- package/themes/metro/blue/ftable.css +15 -6
- package/themes/metro/blue/ftable.min.css +1 -1
- package/themes/metro/brown/ftable.css +15 -6
- package/themes/metro/brown/ftable.min.css +1 -1
- package/themes/metro/crimson/ftable.css +15 -6
- package/themes/metro/crimson/ftable.min.css +1 -1
- package/themes/metro/darkgray/ftable.css +15 -6
- package/themes/metro/darkgray/ftable.min.css +1 -1
- package/themes/metro/darkorange/ftable.css +15 -6
- package/themes/metro/darkorange/ftable.min.css +1 -1
- package/themes/metro/green/ftable.css +15 -6
- package/themes/metro/green/ftable.min.css +1 -1
- package/themes/metro/lightgray/ftable.css +15 -6
- package/themes/metro/lightgray/ftable.min.css +1 -1
- package/themes/metro/pink/ftable.css +15 -6
- package/themes/metro/pink/ftable.min.css +1 -1
- package/themes/metro/purple/ftable.css +15 -6
- package/themes/metro/purple/ftable.min.css +1 -1
- package/themes/metro/red/ftable.css +15 -6
- package/themes/metro/red/ftable.min.css +1 -1
package/ftable.umd.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
|
|
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
|
|
|
@@ -623,124 +624,46 @@ class FTableFormBuilder {
|
|
|
623
624
|
this.options = options;
|
|
624
625
|
this.dependencies = new Map(); // Track field dependencies
|
|
625
626
|
this.optionsCache = new FTableOptionsCache();
|
|
626
|
-
this.originalFieldOptions = new Map(); // Store original field.options
|
|
627
|
-
this.resolvedFieldOptions = new Map(); // Store resolved options per context
|
|
628
|
-
|
|
629
|
-
// Initialize with empty cache objects
|
|
630
|
-
Object.keys(this.options.fields || {}).forEach(fieldName => {
|
|
631
|
-
this.resolvedFieldOptions.set(fieldName, {});
|
|
632
|
-
});
|
|
633
|
-
Object.entries(this.options.fields).forEach(([fieldName, field]) => {
|
|
634
|
-
this.originalFieldOptions.set(fieldName, {
|
|
635
|
-
options: field.options,
|
|
636
|
-
searchOptions: field.searchOptions
|
|
637
|
-
});
|
|
638
|
-
});
|
|
639
627
|
}
|
|
640
628
|
|
|
641
|
-
// Get options for
|
|
629
|
+
// Get options for a field, respecting context ('search' prefers searchOptions over options).
|
|
630
|
+
// URL-level caching and concurrent-request deduplication is handled by FTableOptionsCache
|
|
631
|
+
// inside resolveOptions
|
|
642
632
|
async getFieldOptions(fieldName, context = 'table', params = {}) {
|
|
643
633
|
const field = this.options.fields[fieldName];
|
|
644
|
-
const stored = this.originalFieldOptions.get(fieldName);
|
|
645
634
|
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
originalOptions = stored?.searchOptions ?? stored?.options;
|
|
651
|
-
} else {
|
|
652
|
-
originalOptions = stored?.options;
|
|
653
|
-
}
|
|
635
|
+
// For search context, prefer searchOptions and fall back to options
|
|
636
|
+
const optionsSource = (context === 'search')
|
|
637
|
+
? (field.searchOptions ?? field.options)
|
|
638
|
+
: field.options;
|
|
654
639
|
|
|
655
|
-
|
|
656
|
-
if (!originalOptions) {
|
|
657
|
-
return null;
|
|
658
|
-
}
|
|
640
|
+
if (!optionsSource) return null;
|
|
659
641
|
|
|
660
|
-
|
|
661
|
-
const shouldSkipCache = this.shouldForceRefreshForContext(field, context, params);
|
|
662
|
-
const cacheKey = this.generateOptionsCacheKey(context, params);
|
|
663
|
-
// Skip cache if configured or forceRefresh requested
|
|
664
|
-
if (!shouldSkipCache && !params.forceRefresh) {
|
|
665
|
-
const cached = this.resolvedFieldOptions.get(fieldName)[cacheKey];
|
|
666
|
-
if (cached) return cached;
|
|
667
|
-
}
|
|
642
|
+
const noCache = this.shouldSkipCache(field, context, params);
|
|
668
643
|
|
|
669
644
|
try {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
// we store the resolved option always
|
|
677
|
-
this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
|
|
678
|
-
return resolved;
|
|
645
|
+
return await this.resolveOptions(
|
|
646
|
+
{ ...field, options: optionsSource },
|
|
647
|
+
params,
|
|
648
|
+
context,
|
|
649
|
+
noCache
|
|
650
|
+
);
|
|
679
651
|
} catch (err) {
|
|
680
652
|
console.error(`Failed to resolve options for ${fieldName} (${context}):`, err);
|
|
681
|
-
return
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Clear resolved options for specific field or all fields
|
|
687
|
-
* @param {string|null} fieldName - Field name to clear, or null for all fields
|
|
688
|
-
* @param {string|null} context - Context to clear ('table', 'create', 'edit'), or null for all contexts
|
|
689
|
-
*/
|
|
690
|
-
clearResolvedOptions(fieldName = null, context = null) {
|
|
691
|
-
if (fieldName) {
|
|
692
|
-
// Clear specific field
|
|
693
|
-
if (this.resolvedFieldOptions.has(fieldName)) {
|
|
694
|
-
if (context) {
|
|
695
|
-
// Clear specific context for specific field
|
|
696
|
-
this.resolvedFieldOptions.get(fieldName)[context] = null;
|
|
697
|
-
} else {
|
|
698
|
-
// Clear all contexts for specific field
|
|
699
|
-
this.resolvedFieldOptions.set(fieldName, { table: null, create: null, edit: null });
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
} else {
|
|
703
|
-
// Clear all fields
|
|
704
|
-
if (context) {
|
|
705
|
-
// Clear specific context for all fields
|
|
706
|
-
this.resolvedFieldOptions.forEach((value, key) => {
|
|
707
|
-
this.resolvedFieldOptions.get(key)[context] = null;
|
|
708
|
-
});
|
|
709
|
-
} else {
|
|
710
|
-
// Clear everything
|
|
711
|
-
this.resolvedFieldOptions.forEach((value, key) => {
|
|
712
|
-
this.resolvedFieldOptions.set(key, { table: null, create: null, edit: null });
|
|
713
|
-
});
|
|
714
|
-
}
|
|
653
|
+
return optionsSource;
|
|
715
654
|
}
|
|
716
655
|
}
|
|
717
656
|
|
|
718
|
-
//
|
|
719
|
-
|
|
720
|
-
|
|
657
|
+
// Determine whether to bypass the URL cache for this field/context
|
|
658
|
+
shouldSkipCache(field, context, params) {
|
|
659
|
+
if (params.forceRefresh) return true;
|
|
721
660
|
if (!field.noCache) return false;
|
|
722
|
-
|
|
723
661
|
if (typeof field.noCache === 'boolean') return field.noCache;
|
|
724
662
|
if (typeof field.noCache === 'function') return field.noCache({ context, ...params });
|
|
725
663
|
if (typeof field.noCache === 'object') return field.noCache[context] === true;
|
|
726
|
-
|
|
727
664
|
return false;
|
|
728
665
|
}
|
|
729
666
|
|
|
730
|
-
generateOptionsCacheKey(context, params) {
|
|
731
|
-
// Create a unique key based on context and dependency values
|
|
732
|
-
const keyParts = [context];
|
|
733
|
-
|
|
734
|
-
if (params.dependedValues) {
|
|
735
|
-
// Include relevant dependency values in the cache key
|
|
736
|
-
Object.keys(params.dependedValues).sort().forEach(key => {
|
|
737
|
-
keyParts.push(`${key}=${params.dependedValues[key]}`);
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
return keyParts.join('|');
|
|
742
|
-
}
|
|
743
|
-
|
|
744
667
|
shouldIncludeField(field, formType) {
|
|
745
668
|
if (formType === 'create') {
|
|
746
669
|
return field.create !== false && !(field.key === true && field.create !== true);
|
|
@@ -813,12 +736,6 @@ class FTableFormBuilder {
|
|
|
813
736
|
return form;
|
|
814
737
|
}
|
|
815
738
|
|
|
816
|
-
shouldResolveOptions(options) {
|
|
817
|
-
return options &&
|
|
818
|
-
(typeof options === 'function' || typeof options === 'string') &&
|
|
819
|
-
!Array.isArray(options) &&
|
|
820
|
-
!(typeof options === 'object' && !Array.isArray(options) && Object.keys(options).length > 0);
|
|
821
|
-
}
|
|
822
739
|
|
|
823
740
|
buildDependencyMap() {
|
|
824
741
|
this.dependencies.clear();
|
|
@@ -2102,7 +2019,8 @@ class FTable extends FTableEventEmitter {
|
|
|
2102
2019
|
saveUserPreferences: true,
|
|
2103
2020
|
saveUserPreferencesMethod: 'localStorage',
|
|
2104
2021
|
defaultSorting: '',
|
|
2105
|
-
|
|
2022
|
+
tableResetButton: false,
|
|
2023
|
+
sortingResetButton: false,
|
|
2106
2024
|
|
|
2107
2025
|
// Paging
|
|
2108
2026
|
paging: false,
|
|
@@ -2177,13 +2095,13 @@ class FTable extends FTableEventEmitter {
|
|
|
2177
2095
|
}
|
|
2178
2096
|
|
|
2179
2097
|
// Start resolving in background
|
|
2180
|
-
this.
|
|
2098
|
+
this.resolveAllFieldOptionsForTable().then(() => {
|
|
2181
2099
|
// re-render dynamic options rows — no server call
|
|
2182
2100
|
// this is needed so that once options are resolved, the table shows correct display values
|
|
2183
2101
|
// why: load() can actually finish faster than option resolving (and calling refreshDisplayValues
|
|
2184
2102
|
// there is then pointless, since the resolving hasn't finished yet),
|
|
2185
2103
|
// so we need to do it when the options are actually resolved (here)
|
|
2186
|
-
// We could call await this.
|
|
2104
|
+
// We could call await this.resolveAllFieldOptionsForTable() during load, but that would slow down the loading ...
|
|
2187
2105
|
setTimeout(() => {
|
|
2188
2106
|
this.refreshDisplayValues();
|
|
2189
2107
|
}, 0);
|
|
@@ -2312,6 +2230,23 @@ class FTable extends FTableEventEmitter {
|
|
|
2312
2230
|
this.createPageSizeSelector();
|
|
2313
2231
|
}
|
|
2314
2232
|
|
|
2233
|
+
// Table reset button — resets column visibility/widths and pageSize (not sorting)
|
|
2234
|
+
if (this.options.tableResetButton) {
|
|
2235
|
+
const resetTableBtn = FTableDOMHelper.create('button', {
|
|
2236
|
+
className: 'ftable-toolbar-item ftable-table-reset-btn',
|
|
2237
|
+
textContent: this.options.messages.resetTable || 'Reset table',
|
|
2238
|
+
title: this.options.messages.resetTableTooltip || 'Resets column visibility, column widths and page size to defaults.',
|
|
2239
|
+
type: 'button',
|
|
2240
|
+
parent: this.elements.rightArea
|
|
2241
|
+
});
|
|
2242
|
+
resetTableBtn.addEventListener('click', (e) => {
|
|
2243
|
+
e.preventDefault();
|
|
2244
|
+
const msg = this.options.messages.resetTableConfirm;
|
|
2245
|
+
this.modals.resetTable.setContent(`<p>${msg}</p>`);
|
|
2246
|
+
this.modals.resetTable.show();
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2315
2250
|
}
|
|
2316
2251
|
|
|
2317
2252
|
createPageSizeSelector() {
|
|
@@ -2404,22 +2339,17 @@ class FTable extends FTableEventEmitter {
|
|
|
2404
2339
|
}
|
|
2405
2340
|
}
|
|
2406
2341
|
|
|
2407
|
-
async
|
|
2342
|
+
async resolveAllFieldOptionsForTable() {
|
|
2343
|
+
this.tableOptionsCache = new Map();
|
|
2344
|
+
|
|
2408
2345
|
const promises = this.columnList.map(async (fieldName) => {
|
|
2409
2346
|
const field = this.options.fields[fieldName];
|
|
2410
2347
|
if (field.action) return; // Skip action columns
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
|
|
2417
|
-
if (!this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey]) {
|
|
2418
|
-
await this.formBuilder.getFieldOptions(fieldName, 'table');
|
|
2419
|
-
}
|
|
2420
|
-
} catch (err) {
|
|
2421
|
-
console.error(`Failed to resolve table options for ${fieldName}:`, err);
|
|
2422
|
-
}
|
|
2348
|
+
try {
|
|
2349
|
+
const resolved = await this.formBuilder.getFieldOptions(fieldName, 'table');
|
|
2350
|
+
if (resolved) this.tableOptionsCache.set(fieldName, resolved);
|
|
2351
|
+
} catch (err) {
|
|
2352
|
+
console.error(`Failed to resolve table options for ${fieldName}:`, err);
|
|
2423
2353
|
}
|
|
2424
2354
|
});
|
|
2425
2355
|
|
|
@@ -2438,9 +2368,7 @@ class FTable extends FTableEventEmitter {
|
|
|
2438
2368
|
const cell = row.querySelector(`td[data-field-name="${fieldName}"]`);
|
|
2439
2369
|
if (!cell) continue;
|
|
2440
2370
|
|
|
2441
|
-
|
|
2442
|
-
const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
|
|
2443
|
-
const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
|
|
2371
|
+
const resolvedOptions = this.tableOptionsCache?.get(fieldName);
|
|
2444
2372
|
const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
|
|
2445
2373
|
cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
|
|
2446
2374
|
}
|
|
@@ -3288,6 +3216,10 @@ class FTable extends FTableEventEmitter {
|
|
|
3288
3216
|
this.createInfoModal();
|
|
3289
3217
|
this.createLoadingModal();
|
|
3290
3218
|
|
|
3219
|
+
if (this.options.tableResetButton) {
|
|
3220
|
+
this.createResetTableModal();
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3291
3223
|
// Initialize them (create DOM) once
|
|
3292
3224
|
Object.values(this.modals).forEach(modal => modal.create());
|
|
3293
3225
|
}
|
|
@@ -3373,6 +3305,34 @@ class FTable extends FTableEventEmitter {
|
|
|
3373
3305
|
});
|
|
3374
3306
|
}
|
|
3375
3307
|
|
|
3308
|
+
createResetTableModal() {
|
|
3309
|
+
this.modals.resetTable = new FtableModal({
|
|
3310
|
+
parent: this.elements.mainContainer,
|
|
3311
|
+
title: this.options.messages.resetTable || 'Reset table',
|
|
3312
|
+
className: 'ftable-reset-table-modal',
|
|
3313
|
+
closeOnOverlayClick: this.options.closeOnOverlayClick,
|
|
3314
|
+
buttons: [
|
|
3315
|
+
{
|
|
3316
|
+
text: this.options.messages.cancel,
|
|
3317
|
+
className: 'ftable-dialog-cancelbutton',
|
|
3318
|
+
onClick: () => this.modals.resetTable.close()
|
|
3319
|
+
},
|
|
3320
|
+
{
|
|
3321
|
+
text: this.options.messages.yes,
|
|
3322
|
+
className: 'ftable-dialog-savebutton',
|
|
3323
|
+
onClick: () => {
|
|
3324
|
+
this.userPrefs.remove('column-settings');
|
|
3325
|
+
// Preserve current sorting, only reset column settings and pageSize
|
|
3326
|
+
this.userPrefs.set('table-state', JSON.stringify({
|
|
3327
|
+
sorting: this.state.sorting
|
|
3328
|
+
}));
|
|
3329
|
+
location.reload();
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
]
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3376
3336
|
createErrorModal() {
|
|
3377
3337
|
this.modals.error = new FtableModal({
|
|
3378
3338
|
parent: this.elements.mainContainer,
|
|
@@ -3656,6 +3616,21 @@ class FTable extends FTableEventEmitter {
|
|
|
3656
3616
|
});
|
|
3657
3617
|
}
|
|
3658
3618
|
|
|
3619
|
+
// Sorting reset button — visible only when sorting differs from default
|
|
3620
|
+
if (this.options.sorting && this.options.sortingResetButton) {
|
|
3621
|
+
this.elements.sortingResetBtn = this.addToolbarButton({
|
|
3622
|
+
text: this.options.messages.resetSorting || 'Reset sorting',
|
|
3623
|
+
className: 'ftable-toolbar-item-sorting-reset',
|
|
3624
|
+
onClick: () => {
|
|
3625
|
+
this.state.sorting = [];
|
|
3626
|
+
this.updateSortingHeaders();
|
|
3627
|
+
this.load();
|
|
3628
|
+
this.saveState();
|
|
3629
|
+
}
|
|
3630
|
+
});
|
|
3631
|
+
FTableDOMHelper.hide(this.elements.sortingResetBtn); // hidden by default
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3659
3634
|
if (this.options.actions.createAction) {
|
|
3660
3635
|
this.addToolbarButton({
|
|
3661
3636
|
text: this.options.messages.addNewRecord,
|
|
@@ -4026,8 +4001,7 @@ class FTable extends FTableEventEmitter {
|
|
|
4026
4001
|
|
|
4027
4002
|
addDataCell(row, record, fieldName) {
|
|
4028
4003
|
const field = this.options.fields[fieldName];
|
|
4029
|
-
const
|
|
4030
|
-
const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
|
|
4004
|
+
const resolvedOptions = this.tableOptionsCache?.get(fieldName);
|
|
4031
4005
|
const value = this.getDisplayText(record, fieldName, resolvedOptions);
|
|
4032
4006
|
|
|
4033
4007
|
const cell = FTableDOMHelper.create('td', {
|
|
@@ -4530,8 +4504,7 @@ class FTable extends FTableEventEmitter {
|
|
|
4530
4504
|
if (!cell) return;
|
|
4531
4505
|
|
|
4532
4506
|
// Get display text
|
|
4533
|
-
const
|
|
4534
|
-
const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
|
|
4507
|
+
const resolvedOptions = this.tableOptionsCache?.get(fieldName);
|
|
4535
4508
|
const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
|
|
4536
4509
|
cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
|
|
4537
4510
|
cell.className = `${field.listClass || ''} ${field.listClassEntry || ''}`.trim();
|
|
@@ -4697,19 +4670,45 @@ class FTable extends FTableEventEmitter {
|
|
|
4697
4670
|
}
|
|
4698
4671
|
|
|
4699
4672
|
updateSortingHeaders() {
|
|
4700
|
-
// Clear all sorting classes
|
|
4673
|
+
// Clear all sorting classes and remove any existing sort badges
|
|
4701
4674
|
const headers = this.elements.table.querySelectorAll('.ftable-column-header-sortable');
|
|
4702
4675
|
headers.forEach(header => {
|
|
4703
4676
|
FTableDOMHelper.removeClass(header, 'ftable-column-header-sorted-asc ftable-column-header-sorted-desc');
|
|
4677
|
+
const existing = header.querySelector('.ftable-sort-badge');
|
|
4678
|
+
if (existing) existing.remove();
|
|
4704
4679
|
});
|
|
4705
|
-
|
|
4706
|
-
// Apply current sorting classes
|
|
4707
|
-
this.state.sorting.forEach(sort => {
|
|
4680
|
+
|
|
4681
|
+
// Apply current sorting classes and sort number badges
|
|
4682
|
+
this.state.sorting.forEach((sort, index) => {
|
|
4708
4683
|
const header = this.elements.table.querySelector(`[data-field-name="${sort.fieldName}"]`);
|
|
4709
|
-
if (header)
|
|
4710
|
-
|
|
4684
|
+
if (!header) return;
|
|
4685
|
+
|
|
4686
|
+
FTableDOMHelper.addClass(header, `ftable-column-header-sorted-${sort.direction.toLowerCase()}`);
|
|
4687
|
+
|
|
4688
|
+
// Sort number badge — only show when multisorting with more than 1 active sort
|
|
4689
|
+
if (this.options.multiSorting && this.state.sorting.length > 1) {
|
|
4690
|
+
const container = header.querySelector('.ftable-column-header-container');
|
|
4691
|
+
if (container) {
|
|
4692
|
+
FTableDOMHelper.create('span', {
|
|
4693
|
+
className: 'ftable-sort-badge',
|
|
4694
|
+
textContent: String(index + 1),
|
|
4695
|
+
parent: container
|
|
4696
|
+
});
|
|
4697
|
+
}
|
|
4711
4698
|
}
|
|
4712
4699
|
});
|
|
4700
|
+
|
|
4701
|
+
// Update visibility of the sorting reset toolbar button
|
|
4702
|
+
this._updateSortingResetButton();
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
_updateSortingResetButton() {
|
|
4706
|
+
if (!this.elements.sortingResetBtn) return;
|
|
4707
|
+
if (this.state.sorting.length === 0) {
|
|
4708
|
+
FTableDOMHelper.hide(this.elements.sortingResetBtn);
|
|
4709
|
+
} else {
|
|
4710
|
+
FTableDOMHelper.show(this.elements.sortingResetBtn);
|
|
4711
|
+
}
|
|
4713
4712
|
}
|
|
4714
4713
|
|
|
4715
4714
|
// Paging Methods
|
|
@@ -5291,8 +5290,8 @@ class FTable extends FTableEventEmitter {
|
|
|
5291
5290
|
// this.emit('columnVisibilityChanged', { field: field });
|
|
5292
5291
|
}
|
|
5293
5292
|
|
|
5294
|
-
// Responsive helpers
|
|
5295
5293
|
/*
|
|
5294
|
+
// Responsive helpers
|
|
5296
5295
|
makeResponsive() {
|
|
5297
5296
|
// Add responsive classes and behavior
|
|
5298
5297
|
FTableDOMHelper.addClass(this.elements.mainContainer, 'ftable-responsive');
|
|
@@ -5337,200 +5336,6 @@ class FTable extends FTableEventEmitter {
|
|
|
5337
5336
|
});
|
|
5338
5337
|
}
|
|
5339
5338
|
|
|
5340
|
-
// Advanced search functionality
|
|
5341
|
-
enableSearch(options = {}) {
|
|
5342
|
-
const searchOptions = {
|
|
5343
|
-
placeholder: 'Search...',
|
|
5344
|
-
debounceMs: 300,
|
|
5345
|
-
searchFields: this.columnList,
|
|
5346
|
-
...options
|
|
5347
|
-
};
|
|
5348
|
-
|
|
5349
|
-
const searchContainer = FTableDOMHelper.create('div', {
|
|
5350
|
-
className: 'ftable-search-container',
|
|
5351
|
-
parent: this.elements.toolbarDiv
|
|
5352
|
-
});
|
|
5353
|
-
|
|
5354
|
-
const searchInput = FTableDOMHelper.create('input', {
|
|
5355
|
-
attributes: {
|
|
5356
|
-
type: 'text',
|
|
5357
|
-
placeholder: searchOptions.placeholder,
|
|
5358
|
-
class: 'ftable-search-input'
|
|
5359
|
-
},
|
|
5360
|
-
parent: searchContainer
|
|
5361
|
-
});
|
|
5362
|
-
|
|
5363
|
-
// Debounced search
|
|
5364
|
-
let searchTimeout;
|
|
5365
|
-
searchInput.addEventListener('input', (e) => {
|
|
5366
|
-
clearTimeout(searchTimeout);
|
|
5367
|
-
searchTimeout = setTimeout(() => {
|
|
5368
|
-
this.performSearch(e.target.value, searchOptions.searchFields);
|
|
5369
|
-
}, searchOptions.debounceMs);
|
|
5370
|
-
});
|
|
5371
|
-
|
|
5372
|
-
return searchInput;
|
|
5373
|
-
}
|
|
5374
|
-
|
|
5375
|
-
async performSearch(query, searchFields) {
|
|
5376
|
-
if (!query.trim()) {
|
|
5377
|
-
return this.load(); // Clear search
|
|
5378
|
-
}
|
|
5379
|
-
|
|
5380
|
-
const searchParams = {
|
|
5381
|
-
search: query,
|
|
5382
|
-
searchFields: searchFields.join(',')
|
|
5383
|
-
};
|
|
5384
|
-
|
|
5385
|
-
return this.load(searchParams);
|
|
5386
|
-
}
|
|
5387
|
-
|
|
5388
|
-
// Keyboard shortcuts
|
|
5389
|
-
enableKeyboardShortcuts() {
|
|
5390
|
-
document.addEventListener('keydown', (e) => {
|
|
5391
|
-
// Only handle shortcuts when table has focus or is active
|
|
5392
|
-
if (!this.elements.mainContainer.contains(document.activeElement)) return;
|
|
5393
|
-
|
|
5394
|
-
switch (e.key) {
|
|
5395
|
-
case 'n':
|
|
5396
|
-
if (e.ctrlKey && this.options.actions.createAction) {
|
|
5397
|
-
e.preventDefault();
|
|
5398
|
-
this.showAddRecordForm();
|
|
5399
|
-
}
|
|
5400
|
-
break;
|
|
5401
|
-
case 'r':
|
|
5402
|
-
if (e.ctrlKey) {
|
|
5403
|
-
e.preventDefault();
|
|
5404
|
-
this.reload();
|
|
5405
|
-
}
|
|
5406
|
-
break;
|
|
5407
|
-
case 'Delete':
|
|
5408
|
-
if (this.options.actions.deleteAction) {
|
|
5409
|
-
const selectedRows = this.getSelectedRows();
|
|
5410
|
-
if (selectedRows.length > 0) {
|
|
5411
|
-
e.preventDefault();
|
|
5412
|
-
this.bulkDelete();
|
|
5413
|
-
}
|
|
5414
|
-
}
|
|
5415
|
-
break;
|
|
5416
|
-
case 'a':
|
|
5417
|
-
if (e.ctrlKey && this.options.selecting && this.options.multiselect) {
|
|
5418
|
-
e.preventDefault();
|
|
5419
|
-
this.toggleSelectAll(true);
|
|
5420
|
-
}
|
|
5421
|
-
break;
|
|
5422
|
-
case 'Escape':
|
|
5423
|
-
// Close any open modals
|
|
5424
|
-
Object.values(this.modals).forEach(modal => {
|
|
5425
|
-
if (modal.isOpen) modal.close();
|
|
5426
|
-
});
|
|
5427
|
-
break;
|
|
5428
|
-
}
|
|
5429
|
-
});
|
|
5430
|
-
}
|
|
5431
|
-
|
|
5432
|
-
// Real-time updates via WebSocket
|
|
5433
|
-
enableRealTimeUpdates(websocketUrl) {
|
|
5434
|
-
if (!websocketUrl) return;
|
|
5435
|
-
|
|
5436
|
-
this.websocket = new WebSocket(websocketUrl);
|
|
5437
|
-
|
|
5438
|
-
this.websocket.onmessage = (event) => {
|
|
5439
|
-
try {
|
|
5440
|
-
const data = JSON.parse(event.data);
|
|
5441
|
-
this.handleRealTimeUpdate(data);
|
|
5442
|
-
} catch (error) {
|
|
5443
|
-
this.logger.error('Failed to parse WebSocket message', error);
|
|
5444
|
-
}
|
|
5445
|
-
};
|
|
5446
|
-
|
|
5447
|
-
this.websocket.onerror = (error) => {
|
|
5448
|
-
this.logger.error('WebSocket error', error);
|
|
5449
|
-
};
|
|
5450
|
-
|
|
5451
|
-
this.websocket.onclose = () => {
|
|
5452
|
-
this.logger.info('WebSocket connection closed');
|
|
5453
|
-
// Attempt to reconnect after delay
|
|
5454
|
-
setTimeout(() => {
|
|
5455
|
-
if (this.websocket.readyState === WebSocket.CLOSED) {
|
|
5456
|
-
this.enableRealTimeUpdates(websocketUrl);
|
|
5457
|
-
}
|
|
5458
|
-
}, 5000);
|
|
5459
|
-
};
|
|
5460
|
-
}
|
|
5461
|
-
|
|
5462
|
-
handleRealTimeUpdate(data) {
|
|
5463
|
-
switch (data.type) {
|
|
5464
|
-
case 'record_added':
|
|
5465
|
-
this.addRecordToTable(data.record);
|
|
5466
|
-
break;
|
|
5467
|
-
case 'record_updated':
|
|
5468
|
-
this.updateRecordInTable(data.record);
|
|
5469
|
-
break;
|
|
5470
|
-
case 'record_deleted':
|
|
5471
|
-
this.removeRecordFromTable(data.recordKey);
|
|
5472
|
-
break;
|
|
5473
|
-
case 'refresh':
|
|
5474
|
-
this.reload();
|
|
5475
|
-
break;
|
|
5476
|
-
}
|
|
5477
|
-
}
|
|
5478
|
-
|
|
5479
|
-
addRecordToTable(record) {
|
|
5480
|
-
const row = this.createTableRow(record);
|
|
5481
|
-
|
|
5482
|
-
// Add to beginning or end based on sorting
|
|
5483
|
-
if (this.state.sorting.length > 0) {
|
|
5484
|
-
// Would need to calculate correct position based on sort
|
|
5485
|
-
this.elements.tableBody.appendChild(row);
|
|
5486
|
-
} else {
|
|
5487
|
-
this.elements.tableBody.appendChild(row);
|
|
5488
|
-
}
|
|
5489
|
-
|
|
5490
|
-
this.state.records.push(record);
|
|
5491
|
-
this.removeNoDataRow();
|
|
5492
|
-
this.refreshRowStyles();
|
|
5493
|
-
|
|
5494
|
-
// Show animation
|
|
5495
|
-
if (this.options.animationsEnabled) {
|
|
5496
|
-
this.showRowAnimation(row, 'added');
|
|
5497
|
-
}
|
|
5498
|
-
}
|
|
5499
|
-
|
|
5500
|
-
updateRecordInTable(record) {
|
|
5501
|
-
const keyValue = this.getKeyValue(record);
|
|
5502
|
-
const existingRow = this.getRowByKey(keyValue);
|
|
5503
|
-
|
|
5504
|
-
if (existingRow) {
|
|
5505
|
-
this.updateRowData(existingRow, record);
|
|
5506
|
-
|
|
5507
|
-
if (this.options.animationsEnabled) {
|
|
5508
|
-
this.showRowAnimation(existingRow, 'updated');
|
|
5509
|
-
}
|
|
5510
|
-
}
|
|
5511
|
-
}
|
|
5512
|
-
|
|
5513
|
-
removeRecordFromTable(keyValue) {
|
|
5514
|
-
const row = this.getRowByKey(keyValue);
|
|
5515
|
-
if (row) {
|
|
5516
|
-
this.removeRowFromTable(row);
|
|
5517
|
-
|
|
5518
|
-
// Remove from state
|
|
5519
|
-
this.state.records = this.state.records.filter(r =>
|
|
5520
|
-
this.getKeyValue(r) !== keyValue
|
|
5521
|
-
);
|
|
5522
|
-
}
|
|
5523
|
-
}
|
|
5524
|
-
|
|
5525
|
-
showRowAnimation(row, type) {
|
|
5526
|
-
const animationClass = `ftable-row-${type}`;
|
|
5527
|
-
FTableDOMHelper.addClass(row, animationClass);
|
|
5528
|
-
|
|
5529
|
-
setTimeout(() => {
|
|
5530
|
-
FTableDOMHelper.removeClass(row, animationClass);
|
|
5531
|
-
}, 2000);
|
|
5532
|
-
}
|
|
5533
|
-
|
|
5534
5339
|
// Plugin system for extensions
|
|
5535
5340
|
use(plugin, options = {}) {
|
|
5536
5341
|
if (typeof plugin === 'function') {
|
|
@@ -5711,65 +5516,20 @@ class FTable extends FTableEventEmitter {
|
|
|
5711
5516
|
|
|
5712
5517
|
const messages = this.options.messages || {};
|
|
5713
5518
|
|
|
5714
|
-
// Get prefix/suffix if defined
|
|
5715
|
-
const prefix = messages.sortingInfoPrefix ? `<span class="ftable-sorting-prefix">${messages.sortingInfoPrefix}</span> ` : '';
|
|
5716
|
-
const suffix = messages.sortingInfoSuffix ? ` <span class="ftable-sorting-suffix">${messages.sortingInfoSuffix}</span>` : '';
|
|
5717
|
-
|
|
5718
5519
|
if (this.state.sorting.length === 0) {
|
|
5719
5520
|
container.innerHTML = messages.sortingInfoNone || '';
|
|
5720
5521
|
return;
|
|
5721
5522
|
}
|
|
5722
5523
|
|
|
5524
|
+
// Get prefix/suffix if defined
|
|
5525
|
+
const prefix = messages.sortingInfoPrefix ? `<span class="ftable-sorting-prefix">${messages.sortingInfoPrefix}</span> ` : '';
|
|
5526
|
+
const suffix = messages.sortingInfoSuffix ? ` <span class="ftable-sorting-suffix">${messages.sortingInfoSuffix}</span>` : '';
|
|
5527
|
+
|
|
5723
5528
|
// Build sorted fields list with translated directions
|
|
5724
5529
|
const sortingInfo = this.getSortingInfo();
|
|
5725
5530
|
|
|
5726
5531
|
// Combine with prefix and suffix
|
|
5727
5532
|
container.innerHTML = `${prefix}${sortingInfo}${suffix}`;
|
|
5728
|
-
|
|
5729
|
-
// Add reset sorting button
|
|
5730
|
-
if (this.state.sorting.length > 0) {
|
|
5731
|
-
const resetSortBtn = document.createElement('button');
|
|
5732
|
-
resetSortBtn.textContent = messages.resetSorting || 'Reset Sorting';
|
|
5733
|
-
resetSortBtn.style.marginLeft = '10px';
|
|
5734
|
-
resetSortBtn.classList.add('ftable-sorting-reset-btn');
|
|
5735
|
-
resetSortBtn.addEventListener('click', (e) => {
|
|
5736
|
-
e.preventDefault();
|
|
5737
|
-
this.state.sorting = [];
|
|
5738
|
-
this.updateSortingHeaders();
|
|
5739
|
-
this.load();
|
|
5740
|
-
this.saveState();
|
|
5741
|
-
});
|
|
5742
|
-
container.appendChild(resetSortBtn);
|
|
5743
|
-
}
|
|
5744
|
-
|
|
5745
|
-
// Add reset table button if enabled
|
|
5746
|
-
if (this.options.tableReset) {
|
|
5747
|
-
const resetTableBtn = document.createElement('button');
|
|
5748
|
-
resetTableBtn.textContent = messages.resetTable || 'Reset Table';
|
|
5749
|
-
resetTableBtn.style.marginLeft = '10px';
|
|
5750
|
-
resetTableBtn.classList.add('ftable-table-reset-btn');
|
|
5751
|
-
resetTableBtn.addEventListener('click', (e) => {
|
|
5752
|
-
e.preventDefault();
|
|
5753
|
-
const confirmMsg = messages.resetTableConfirm;
|
|
5754
|
-
if (confirm(confirmMsg)) {
|
|
5755
|
-
this.userPrefs.remove('column-settings');
|
|
5756
|
-
this.userPrefs.remove('table-state');
|
|
5757
|
-
|
|
5758
|
-
// Clear any in-memory state that might affect rendering
|
|
5759
|
-
this.state.sorting = [];
|
|
5760
|
-
this.state.pageSize = this.options.pageSize;
|
|
5761
|
-
|
|
5762
|
-
// Reset field visibility to default
|
|
5763
|
-
this.columnList.forEach(fieldName => {
|
|
5764
|
-
const field = this.options.fields[fieldName];
|
|
5765
|
-
// Reset to default: hidden only if explicitly set
|
|
5766
|
-
field.visibility = field.visibility === 'fixed' ? 'fixed' : 'visible';
|
|
5767
|
-
});
|
|
5768
|
-
location.reload();
|
|
5769
|
-
}
|
|
5770
|
-
});
|
|
5771
|
-
container.appendChild(resetTableBtn);
|
|
5772
|
-
}
|
|
5773
5533
|
}
|
|
5774
5534
|
|
|
5775
5535
|
/**
|
|
@@ -28,8 +28,11 @@ FTable.setMessages({
|
|
|
28
28
|
sortingInfoPrefix: 'Sortering toegepast: ',
|
|
29
29
|
ascending: 'Oplopend',
|
|
30
30
|
descending: 'Aflopend',
|
|
31
|
+
resetTableConfirm: 'Hiermee worden de kolomzichtbaarheid, kolombreedtes en paginagrootte teruggezet naar de standaardinstellingen. Wilt u doorgaan?',
|
|
31
32
|
sortingInfoNone: 'Geen sortering toegepast',
|
|
32
33
|
csvExport: 'CSV',
|
|
33
34
|
printTable: '🖨️ Print',
|
|
34
|
-
cloneRecord: 'Clone Record'
|
|
35
|
+
cloneRecord: 'Clone Record',
|
|
36
|
+
resetSorting: 'Reset sorteren',
|
|
37
|
+
resetTable: 'Reset tabel'
|
|
35
38
|
});
|