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