@keenthemes/ktui 1.2.6 → 1.2.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/README.md +14 -5
- package/dist/ktui.js +3775 -2298
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +25 -5
- package/lib/cjs/components/datatable/datatable-checkbox.d.ts +37 -1
- package/lib/cjs/components/datatable/datatable-checkbox.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-checkbox.js +143 -156
- package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-column-utils.d.ts +30 -0
- package/lib/cjs/components/datatable/datatable-column-utils.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-column-utils.js +42 -0
- package/lib/cjs/components/datatable/datatable-column-utils.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-contracts.d.ts +2 -4
- package/lib/cjs/components/datatable/datatable-contracts.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-defaults.d.ts +20 -0
- package/lib/cjs/components/datatable/datatable-defaults.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-defaults.js +193 -0
- package/lib/cjs/components/datatable/datatable-defaults.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-layout-plugin.js +11 -1
- package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-local-provider.js +80 -24
- package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-pagination-renderer.js +3 -2
- package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-registry.d.ts +18 -0
- package/lib/cjs/components/datatable/datatable-registry.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-registry.js +66 -0
- package/lib/cjs/components/datatable/datatable-registry.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-remote-provider.js +1 -2
- package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-search-handler.d.ts +10 -0
- package/lib/cjs/components/datatable/datatable-search-handler.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-search-handler.js +65 -0
- package/lib/cjs/components/datatable/datatable-search-handler.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-sort.d.ts +31 -4
- package/lib/cjs/components/datatable/datatable-sort.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-sort.js +86 -58
- package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-spinner.d.ts +30 -0
- package/lib/cjs/components/datatable/datatable-spinner.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-spinner.js +54 -0
- package/lib/cjs/components/datatable/datatable-spinner.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-state-persistence.d.ts +19 -0
- package/lib/cjs/components/datatable/datatable-state-persistence.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-state-persistence.js +59 -0
- package/lib/cjs/components/datatable/datatable-state-persistence.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +2 -0
- package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-table-renderer.js +75 -16
- package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-utils.d.ts +10 -0
- package/lib/cjs/components/datatable/datatable-utils.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-utils.js +15 -0
- package/lib/cjs/components/datatable/datatable-utils.js.map +1 -0
- package/lib/cjs/components/datatable/datatable.d.ts +26 -34
- package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable.js +155 -492
- package/lib/cjs/components/datatable/datatable.js.map +1 -1
- package/lib/cjs/components/datatable/index.d.ts +1 -1
- package/lib/cjs/components/datatable/index.d.ts.map +1 -1
- package/lib/cjs/components/datatable/types.d.ts +100 -11
- package/lib/cjs/components/datatable/types.d.ts.map +1 -1
- package/lib/cjs/index.d.ts +1 -1
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +6 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/components/datatable/datatable-checkbox.d.ts +37 -1
- package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-checkbox.js +142 -155
- package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
- package/lib/esm/components/datatable/datatable-column-utils.d.ts +30 -0
- package/lib/esm/components/datatable/datatable-column-utils.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-column-utils.js +38 -0
- package/lib/esm/components/datatable/datatable-column-utils.js.map +1 -0
- package/lib/esm/components/datatable/datatable-contracts.d.ts +2 -4
- package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-defaults.d.ts +20 -0
- package/lib/esm/components/datatable/datatable-defaults.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-defaults.js +190 -0
- package/lib/esm/components/datatable/datatable-defaults.js.map +1 -0
- package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-layout-plugin.js +11 -1
- package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -1
- package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-local-provider.js +80 -24
- package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
- package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-pagination-renderer.js +3 -2
- package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
- package/lib/esm/components/datatable/datatable-registry.d.ts +18 -0
- package/lib/esm/components/datatable/datatable-registry.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-registry.js +63 -0
- package/lib/esm/components/datatable/datatable-registry.js.map +1 -0
- package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-remote-provider.js +1 -2
- package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -1
- package/lib/esm/components/datatable/datatable-search-handler.d.ts +10 -0
- package/lib/esm/components/datatable/datatable-search-handler.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-search-handler.js +62 -0
- package/lib/esm/components/datatable/datatable-search-handler.js.map +1 -0
- package/lib/esm/components/datatable/datatable-sort.d.ts +31 -4
- package/lib/esm/components/datatable/datatable-sort.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-sort.js +85 -57
- package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
- package/lib/esm/components/datatable/datatable-spinner.d.ts +30 -0
- package/lib/esm/components/datatable/datatable-spinner.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-spinner.js +51 -0
- package/lib/esm/components/datatable/datatable-spinner.js.map +1 -0
- package/lib/esm/components/datatable/datatable-state-persistence.d.ts +19 -0
- package/lib/esm/components/datatable/datatable-state-persistence.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-state-persistence.js +55 -0
- package/lib/esm/components/datatable/datatable-state-persistence.js.map +1 -0
- package/lib/esm/components/datatable/datatable-table-renderer.d.ts +2 -0
- package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-table-renderer.js +75 -16
- package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -1
- package/lib/esm/components/datatable/datatable-utils.d.ts +10 -0
- package/lib/esm/components/datatable/datatable-utils.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-utils.js +12 -0
- package/lib/esm/components/datatable/datatable-utils.js.map +1 -0
- package/lib/esm/components/datatable/datatable.d.ts +26 -34
- package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable.js +157 -494
- package/lib/esm/components/datatable/datatable.js.map +1 -1
- package/lib/esm/components/datatable/index.d.ts +1 -1
- package/lib/esm/components/datatable/index.d.ts.map +1 -1
- package/lib/esm/components/datatable/types.d.ts +100 -11
- package/lib/esm/components/datatable/types.d.ts.map +1 -1
- package/lib/esm/index.d.ts +1 -1
- package/lib/esm/index.d.ts.map +1 -1
- package/lib/esm/index.js +6 -0
- package/lib/esm/index.js.map +1 -1
- package/package.json +5 -1
- package/skills/ktui/SKILL.md +711 -0
- package/skills/ktui-datatable/SKILL.md +302 -0
- package/skills/ktui-install/SKILL.md +150 -0
- package/skills/ktui-select/SKILL.md +271 -0
- package/src/components/__tests__/component.test.ts +347 -0
- package/src/components/collapse/collapse.css +2 -2
- package/src/components/datatable/__tests__/architecture-boundaries.test.ts +56 -8
- package/src/components/datatable/__tests__/currency-sort.test.ts +25 -28
- package/src/components/datatable/__tests__/datatable-checkbox.test.ts +527 -0
- package/src/components/datatable/__tests__/datatable-column-utils.test.ts +117 -0
- package/src/components/datatable/__tests__/datatable-defaults.test.ts +57 -0
- package/src/components/datatable/__tests__/datatable-finalize-extended.test.ts +361 -0
- package/src/components/datatable/__tests__/datatable-fixed-layout.test.ts +427 -0
- package/src/components/datatable/__tests__/datatable-improvements.test.ts +484 -0
- package/src/components/datatable/__tests__/datatable-pagination-extended.test.ts +508 -0
- package/src/components/datatable/__tests__/datatable-public-api.test.ts +269 -0
- package/src/components/datatable/__tests__/datatable-registry.test.ts +172 -0
- package/src/components/datatable/__tests__/datatable-remote-provider.test.ts +468 -0
- package/src/components/datatable/__tests__/datatable-search-handler.test.ts +124 -0
- package/src/components/datatable/__tests__/datatable-sort-extended.test.ts +417 -0
- package/src/components/datatable/__tests__/datatable-spinner.test.ts +95 -0
- package/src/components/datatable/__tests__/datatable-table-renderer-extended.test.ts +425 -0
- package/src/components/datatable/__tests__/datatable-types.test.ts +117 -0
- package/src/components/datatable/__tests__/datatable-utils.test.ts +52 -0
- package/src/components/datatable/__tests__/multi-row-headers.test.ts +7 -7
- package/src/components/datatable/__tests__/pagination-reset.test.ts +129 -6
- package/src/components/datatable/__tests__/race-conditions.test.ts +11 -11
- package/src/components/datatable/__tests__/setup.ts +12 -4
- package/src/components/datatable/datatable-checkbox.ts +144 -145
- package/src/components/datatable/datatable-column-utils.ts +63 -0
- package/src/components/datatable/datatable-contracts.ts +2 -3
- package/src/components/datatable/datatable-defaults.ts +204 -0
- package/src/components/datatable/datatable-layout-plugin.ts +11 -1
- package/src/components/datatable/datatable-local-provider.ts +91 -28
- package/src/components/datatable/datatable-pagination-renderer.ts +3 -2
- package/src/components/datatable/datatable-registry.ts +89 -0
- package/src/components/datatable/datatable-remote-provider.ts +1 -3
- package/src/components/datatable/datatable-search-handler.ts +97 -0
- package/src/components/datatable/datatable-sort.ts +111 -66
- package/src/components/datatable/datatable-spinner.ts +103 -0
- package/src/components/datatable/datatable-state-persistence.ts +67 -0
- package/src/components/datatable/datatable-table-renderer.ts +81 -18
- package/src/components/datatable/datatable-utils.ts +12 -0
- package/src/components/datatable/datatable.ts +191 -580
- package/src/components/datatable/index.ts +3 -0
- package/src/components/datatable/types.ts +124 -23
- package/src/helpers/__tests__/dom.test.ts +776 -0
- package/src/helpers/__tests__/utils.test.ts +332 -0
- package/src/index.ts +10 -0
- package/skills/ktui-components/SKILL.md +0 -41
- package/skills/ktui-theming/SKILL.md +0 -50
- package/src/components/datatable/datatable-event-adapter.ts +0 -21
|
@@ -98,6 +98,10 @@ describe('KTDataTable - Pagination Reset', () => {
|
|
|
98
98
|
};
|
|
99
99
|
|
|
100
100
|
beforeEach(() => {
|
|
101
|
+
// Dispose previous test's datatable to cancel in-flight async operations
|
|
102
|
+
if (datatable) {
|
|
103
|
+
try { datatable.dispose(); } catch { /* already disposed */ }
|
|
104
|
+
}
|
|
101
105
|
// Clear any existing elements
|
|
102
106
|
document.body.innerHTML = '';
|
|
103
107
|
vi.clearAllMocks();
|
|
@@ -313,7 +317,7 @@ describe('KTDataTable - Pagination Reset', () => {
|
|
|
313
317
|
expect(datatable.getState().page).toBe(1);
|
|
314
318
|
|
|
315
319
|
// Apply third filter (page should stay at 1)
|
|
316
|
-
datatable.setFilter({ column: 'id', type: 'numeric', value:
|
|
320
|
+
datatable.setFilter({ column: 'id', type: 'numeric', value: 10 });
|
|
317
321
|
expect(datatable.getState().page).toBe(1);
|
|
318
322
|
});
|
|
319
323
|
|
|
@@ -438,7 +442,17 @@ describe('KTDataTable - Pagination Reset', () => {
|
|
|
438
442
|
});
|
|
439
443
|
|
|
440
444
|
describe('Scenario: State persistence respects pagination reset', () => {
|
|
445
|
+
const isLocalStorageAvailable = (): boolean => {
|
|
446
|
+
try {
|
|
447
|
+
localStorage.getItem('test');
|
|
448
|
+
return true;
|
|
449
|
+
} catch {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
441
454
|
it('should save page 1 to state when search resets pagination', async () => {
|
|
455
|
+
if (!isLocalStorageAvailable()) return;
|
|
442
456
|
const { container } = createMockDataTable(25);
|
|
443
457
|
|
|
444
458
|
// Enable state saving with unique namespace
|
|
@@ -476,6 +490,7 @@ describe('KTDataTable - Pagination Reset', () => {
|
|
|
476
490
|
});
|
|
477
491
|
|
|
478
492
|
it('should save page 1 to state when filter resets pagination', () => {
|
|
493
|
+
if (!isLocalStorageAvailable()) return;
|
|
479
494
|
const { container } = createMockDataTable(25);
|
|
480
495
|
|
|
481
496
|
datatable = new KTDataTable(container, {
|
|
@@ -503,6 +518,7 @@ describe('KTDataTable - Pagination Reset', () => {
|
|
|
503
518
|
});
|
|
504
519
|
|
|
505
520
|
it('should restore to page 1 with active search on reload', async () => {
|
|
521
|
+
if (!isLocalStorageAvailable()) return;
|
|
506
522
|
const { container } = createMockDataTable(25);
|
|
507
523
|
const namespace = 'test-datatable-restore';
|
|
508
524
|
|
|
@@ -570,6 +586,113 @@ describe('KTDataTable - Pagination Reset', () => {
|
|
|
570
586
|
expect(datatable.getState().totalPages).toBe(2);
|
|
571
587
|
});
|
|
572
588
|
|
|
589
|
+
it('does not shrink originalData when tbody checksum mismatches after pagination', async () => {
|
|
590
|
+
const { container } = createMockDataTable(18);
|
|
591
|
+
datatable = new KTDataTable(container, {
|
|
592
|
+
pageSize: 5,
|
|
593
|
+
stateSave: false,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
597
|
+
expect(datatable.getState().totalPages).toBe(4);
|
|
598
|
+
|
|
599
|
+
// Simulate a fetch before _contentChecksum was aligned with the paginated tbody.
|
|
600
|
+
datatable.getState()._contentChecksum = 'stale-checksum';
|
|
601
|
+
datatable.reload();
|
|
602
|
+
|
|
603
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
604
|
+
expect(datatable.getState().totalPages).toBe(4);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('keeps 4 pages when thead has checkbox and actions columns (bulk-actions demo)', async () => {
|
|
608
|
+
const container = document.createElement('div');
|
|
609
|
+
container.id = 'test-bulk-actions-datatable';
|
|
610
|
+
|
|
611
|
+
const table = document.createElement('table');
|
|
612
|
+
table.setAttribute('data-kt-datatable-table', 'true');
|
|
613
|
+
|
|
614
|
+
const thead = document.createElement('thead');
|
|
615
|
+
thead.innerHTML = `
|
|
616
|
+
<tr>
|
|
617
|
+
<th><input type="checkbox" data-kt-datatable-check="true" /></th>
|
|
618
|
+
<th data-kt-datatable-column="label">Label</th>
|
|
619
|
+
<th data-kt-datatable-column="method">Method</th>
|
|
620
|
+
<th data-kt-datatable-column="status">Status</th>
|
|
621
|
+
<th data-kt-datatable-column="lastSession">Last Session</th>
|
|
622
|
+
<th data-kt-datatable-column="actions"></th>
|
|
623
|
+
</tr>
|
|
624
|
+
`;
|
|
625
|
+
|
|
626
|
+
const tbody = document.createElement('tbody');
|
|
627
|
+
for (let i = 1; i <= 18; i++) {
|
|
628
|
+
const row = document.createElement('tr');
|
|
629
|
+
row.innerHTML = `
|
|
630
|
+
<td><input type="checkbox" data-kt-datatable-row-check="true" value="${i - 1}" /></td>
|
|
631
|
+
<td>User ${i}</td>
|
|
632
|
+
<td>Web</td>
|
|
633
|
+
<td>active</td>
|
|
634
|
+
<td>22 Jul 2024</td>
|
|
635
|
+
<td><button type="button">Edit</button></td>
|
|
636
|
+
`;
|
|
637
|
+
tbody.appendChild(row);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
table.appendChild(thead);
|
|
641
|
+
table.appendChild(tbody);
|
|
642
|
+
|
|
643
|
+
const infoElement = document.createElement('div');
|
|
644
|
+
infoElement.setAttribute('data-kt-datatable-info', 'true');
|
|
645
|
+
const sizeElement = document.createElement('select');
|
|
646
|
+
sizeElement.setAttribute('data-kt-datatable-size', 'true');
|
|
647
|
+
const paginationElement = document.createElement('div');
|
|
648
|
+
paginationElement.setAttribute('data-kt-datatable-pagination', 'true');
|
|
649
|
+
|
|
650
|
+
container.appendChild(table);
|
|
651
|
+
container.appendChild(infoElement);
|
|
652
|
+
container.appendChild(sizeElement);
|
|
653
|
+
container.appendChild(paginationElement);
|
|
654
|
+
document.body.appendChild(container);
|
|
655
|
+
|
|
656
|
+
datatable = new KTDataTable(container, {
|
|
657
|
+
pageSize: 5,
|
|
658
|
+
stateSave: false,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
662
|
+
|
|
663
|
+
expect(datatable.getState().totalPages).toBe(4);
|
|
664
|
+
datatable.goPage(2);
|
|
665
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
666
|
+
expect(datatable.getState().page).toBe(2);
|
|
667
|
+
expect(datatable.getState().totalPages).toBe(4);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('shows page 2 rows with tableLayout fixed and columns config (docs column-widths demo)', async () => {
|
|
671
|
+
const { container } = createMockDataTable(18);
|
|
672
|
+
datatable = new KTDataTable(container, {
|
|
673
|
+
pageSize: 5,
|
|
674
|
+
stateSave: false,
|
|
675
|
+
tableLayout: 'fixed',
|
|
676
|
+
columns: {
|
|
677
|
+
id: { width: '60px' },
|
|
678
|
+
name: { width: '140px' },
|
|
679
|
+
status: { width: '100px' },
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
684
|
+
|
|
685
|
+
datatable.goPage(2);
|
|
686
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
687
|
+
|
|
688
|
+
expect(datatable.getState().page).toBe(2);
|
|
689
|
+
|
|
690
|
+
const rows = container.querySelectorAll('tbody tr');
|
|
691
|
+
expect(rows.length).toBe(5);
|
|
692
|
+
expect(rows[0].cells[0].textContent).toBe('6');
|
|
693
|
+
expect(datatable.getState().totalPages).toBe(4);
|
|
694
|
+
});
|
|
695
|
+
|
|
573
696
|
it('should handle search reset on page 1 (no-op)', () => {
|
|
574
697
|
const { container } = createMockDataTable(25);
|
|
575
698
|
datatable = new KTDataTable(container, {
|
|
@@ -669,9 +792,9 @@ describe('KTDataTable - Pagination Reset', () => {
|
|
|
669
792
|
stateSave: false,
|
|
670
793
|
});
|
|
671
794
|
|
|
672
|
-
const
|
|
673
|
-
// Listen for '
|
|
674
|
-
container.addEventListener('
|
|
795
|
+
const updateSpy = vi.fn();
|
|
796
|
+
// Listen for 'kt.datatable.update' event directly (CustomEvent)
|
|
797
|
+
container.addEventListener('kt.datatable.update', updateSpy);
|
|
675
798
|
|
|
676
799
|
datatable.goPage(2);
|
|
677
800
|
datatable.search('test');
|
|
@@ -679,8 +802,8 @@ describe('KTDataTable - Pagination Reset', () => {
|
|
|
679
802
|
// Wait for async reload to complete
|
|
680
803
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
681
804
|
|
|
682
|
-
//
|
|
683
|
-
expect(
|
|
805
|
+
// update event should still fire
|
|
806
|
+
expect(updateSpy).toHaveBeenCalled();
|
|
684
807
|
});
|
|
685
808
|
});
|
|
686
809
|
});
|
|
@@ -407,13 +407,13 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
407
407
|
|
|
408
408
|
describe('Event Handling During Race Conditions', () => {
|
|
409
409
|
it('should fire fetch event for successful requests', async () => {
|
|
410
|
-
const
|
|
410
|
+
const updateEvents: Event[] = [];
|
|
411
411
|
|
|
412
412
|
const element = container.querySelector(
|
|
413
413
|
'[data-kt-datatable="true"]',
|
|
414
414
|
) as HTMLElement;
|
|
415
|
-
element.addEventListener('
|
|
416
|
-
|
|
415
|
+
element.addEventListener('kt.datatable.update', (e) => {
|
|
416
|
+
updateEvents.push(e);
|
|
417
417
|
});
|
|
418
418
|
|
|
419
419
|
const datatable = new KTDataTable(element, {
|
|
@@ -425,18 +425,18 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
425
425
|
datatable.search('test');
|
|
426
426
|
await waitFor(150); // Complete search
|
|
427
427
|
|
|
428
|
-
// Should fire
|
|
429
|
-
expect(
|
|
428
|
+
// Should fire update for initial and search
|
|
429
|
+
expect(updateEvents.length).toBeGreaterThanOrEqual(2);
|
|
430
430
|
});
|
|
431
431
|
|
|
432
432
|
it('should fire fetched event after successful data load', async () => {
|
|
433
|
-
const
|
|
433
|
+
const updateEvents: Event[] = [];
|
|
434
434
|
|
|
435
435
|
const element = container.querySelector(
|
|
436
436
|
'[data-kt-datatable="true"]',
|
|
437
437
|
) as HTMLElement;
|
|
438
|
-
element.addEventListener('
|
|
439
|
-
|
|
438
|
+
element.addEventListener('kt.datatable.update', (e) => {
|
|
439
|
+
updateEvents.push(e);
|
|
440
440
|
});
|
|
441
441
|
|
|
442
442
|
new KTDataTable(element, {
|
|
@@ -445,8 +445,8 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
445
445
|
|
|
446
446
|
await waitFor(150);
|
|
447
447
|
|
|
448
|
-
// Should fire
|
|
449
|
-
expect(
|
|
448
|
+
// Should fire update for initial request
|
|
449
|
+
expect(updateEvents.length).toBeGreaterThanOrEqual(1);
|
|
450
450
|
});
|
|
451
451
|
|
|
452
452
|
it('should not fire error events for AbortError', async () => {
|
|
@@ -455,7 +455,7 @@ describe('KTDataTable Race Condition Fixes', () => {
|
|
|
455
455
|
const element = container.querySelector(
|
|
456
456
|
'[data-kt-datatable="true"]',
|
|
457
457
|
) as HTMLElement;
|
|
458
|
-
element.addEventListener('
|
|
458
|
+
element.addEventListener('kt.datatable.error', (e) => {
|
|
459
459
|
errorEvents.push(e);
|
|
460
460
|
});
|
|
461
461
|
|
|
@@ -17,8 +17,12 @@ vi.mock('../../../index', () => ({
|
|
|
17
17
|
|
|
18
18
|
// Setup DOM environment before each test
|
|
19
19
|
beforeEach(() => {
|
|
20
|
-
// Clear localStorage
|
|
21
|
-
|
|
20
|
+
// Clear localStorage (may be unavailable in Node.js without --localstorage-file)
|
|
21
|
+
try {
|
|
22
|
+
localStorage.clear();
|
|
23
|
+
} catch {
|
|
24
|
+
// localStorage not available
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
// Reset document body
|
|
24
28
|
document.body.innerHTML = '';
|
|
@@ -29,8 +33,12 @@ beforeEach(() => {
|
|
|
29
33
|
|
|
30
34
|
// Cleanup after each test
|
|
31
35
|
afterEach(() => {
|
|
32
|
-
// Clear localStorage
|
|
33
|
-
|
|
36
|
+
// Clear localStorage (may be unavailable in Node.js without --localstorage-file)
|
|
37
|
+
try {
|
|
38
|
+
localStorage.clear();
|
|
39
|
+
} catch {
|
|
40
|
+
// localStorage not available
|
|
41
|
+
}
|
|
34
42
|
|
|
35
43
|
// Reset document body
|
|
36
44
|
document.body.innerHTML = '';
|
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
import {
|
|
9
9
|
KTDataTableConfigInterface,
|
|
10
10
|
KTDataTableCheckChangePayloadInterface,
|
|
11
|
-
KTDataTableStateInterface,
|
|
12
11
|
} from './types';
|
|
13
12
|
import KTEventHandler from '../../helpers/event-handler';
|
|
14
13
|
import { KTCallableType } from '../../types';
|
|
15
14
|
|
|
15
|
+
export interface KTDataTableCheckboxDeps {
|
|
16
|
+
getState: () => { selectedRows?: string[] };
|
|
17
|
+
setSelectedRows: (rows: string[]) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
export interface KTDataTableCheckboxAPI {
|
|
17
21
|
init(): void;
|
|
18
22
|
check(): void;
|
|
@@ -21,241 +25,236 @@ export interface KTDataTableCheckboxAPI {
|
|
|
21
25
|
isChecked(): boolean;
|
|
22
26
|
getChecked(): string[];
|
|
23
27
|
updateState(): void;
|
|
28
|
+
dispose(): void;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
export class KTDataTableCheckboxHandler implements KTDataTableCheckboxAPI {
|
|
32
|
+
private _element: HTMLElement;
|
|
33
|
+
private _config: KTDataTableConfigInterface;
|
|
34
|
+
private _fireEvent: (eventName: string, eventData?: object) => void;
|
|
35
|
+
private _deps: KTDataTableCheckboxDeps;
|
|
36
|
+
private _headerChecked = false;
|
|
37
|
+
private _headerCheckElement: HTMLInputElement | null = null;
|
|
38
|
+
private _targetElements: NodeListOf<HTMLInputElement> | null = null;
|
|
39
|
+
private _delegatedEventId: string | null = null;
|
|
40
|
+
private readonly _preserveSelection: boolean;
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
constructor(
|
|
43
|
+
element: HTMLElement,
|
|
44
|
+
config: KTDataTableConfigInterface,
|
|
45
|
+
fireEvent: (eventName: string, eventData?: object) => void,
|
|
46
|
+
deps: KTDataTableCheckboxDeps,
|
|
47
|
+
) {
|
|
48
|
+
this._element = element;
|
|
49
|
+
this._config = config;
|
|
50
|
+
this._fireEvent = fireEvent;
|
|
51
|
+
this._deps = deps;
|
|
52
|
+
this._preserveSelection = config.checkbox?.preserveSelection !== false;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
private _checkboxListener = (event: MouseEvent) => {
|
|
56
|
+
this._checkboxToggle(event);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
private _getSelectedRows(): string[] {
|
|
60
|
+
const rows = this._deps.getState().selectedRows;
|
|
61
|
+
return Array.isArray(rows) ? rows.map(String) : [];
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const state = ensureState();
|
|
58
|
-
state.selectedRows = Array.from(new Set(rows.map(String)));
|
|
64
|
+
private _setSelectedRows(rows: string[]) {
|
|
65
|
+
this._deps.setSelectedRows(Array.from(new Set(rows.map(String))));
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return Array.from(targetElements)
|
|
68
|
+
private _getVisibleRowIds(): string[] {
|
|
69
|
+
if (!this._targetElements) return [];
|
|
70
|
+
return Array.from(this._targetElements)
|
|
65
71
|
.map((el) => el.value)
|
|
66
72
|
.filter((v) => v != null && v !== '');
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
checkboxToggle(event);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
function init() {
|
|
75
|
-
const attrs = config.attributes;
|
|
75
|
+
public init() {
|
|
76
|
+
const attrs = this._config.attributes;
|
|
76
77
|
if (!attrs?.check || !attrs.checkbox) {
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
this._headerCheckElement =
|
|
81
|
+
this._element.querySelector<HTMLInputElement>(attrs.check);
|
|
82
|
+
if (!this._headerCheckElement) return;
|
|
83
|
+
this._headerChecked = this._headerCheckElement.checked;
|
|
84
|
+
this._targetElements =
|
|
85
|
+
this._element.querySelectorAll<HTMLInputElement>(attrs.checkbox);
|
|
86
|
+
this._checkboxHandler();
|
|
87
|
+
this._reapplyCheckedStates();
|
|
88
|
+
this._updateHeaderCheckboxState();
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
if (!
|
|
90
|
-
const rowCheckboxSelector =
|
|
91
|
+
private _checkboxHandler() {
|
|
92
|
+
if (!this._headerCheckElement) return;
|
|
93
|
+
const rowCheckboxSelector = this._config.attributes?.checkbox;
|
|
91
94
|
if (!rowCheckboxSelector) return;
|
|
92
|
-
|
|
93
|
-
KTEventHandler.on(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
this._headerCheckElement.addEventListener('click', this._checkboxListener);
|
|
96
|
+
this._delegatedEventId = KTEventHandler.on(
|
|
97
|
+
this._element,
|
|
98
|
+
rowCheckboxSelector,
|
|
99
|
+
'input',
|
|
100
|
+
((event?: Event) => {
|
|
101
|
+
if (event) this._handleRowCheckboxChange(event);
|
|
102
|
+
}) as KTCallableType,
|
|
103
|
+
);
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
function handleRowCheckboxChange(event: Event) {
|
|
106
|
+
private _handleRowCheckboxChange(event: Event) {
|
|
102
107
|
const input = event.target as HTMLInputElement;
|
|
103
108
|
if (!input) return;
|
|
104
109
|
const value = input.value;
|
|
105
|
-
let selectedRows =
|
|
110
|
+
let selectedRows = this._getSelectedRows();
|
|
106
111
|
const wasChecked = selectedRows.includes(value);
|
|
107
112
|
const isNowChecked = input.checked;
|
|
108
113
|
|
|
109
|
-
// Update state first
|
|
110
114
|
if (isNowChecked) {
|
|
111
115
|
if (!wasChecked) selectedRows.push(value);
|
|
112
116
|
} else {
|
|
113
117
|
selectedRows = selectedRows.filter((v) => v !== value);
|
|
114
118
|
}
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
this._setSelectedRows(selectedRows);
|
|
120
|
+
this._updateHeaderCheckboxState();
|
|
117
121
|
|
|
118
|
-
// Fire specific checked/unchecked events after state is updated
|
|
119
122
|
if (isNowChecked && !wasChecked) {
|
|
120
|
-
|
|
123
|
+
this._fireEvent('checked', { value });
|
|
121
124
|
} else if (!isNowChecked && wasChecked) {
|
|
122
|
-
|
|
125
|
+
this._fireEvent('unchecked', { value });
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
fireEvent('changed');
|
|
128
|
+
this._fireEvent('changed');
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Update state first, then fire events
|
|
133
|
-
change(checked);
|
|
134
|
-
// Fire checked/unchecked events after state is updated
|
|
131
|
+
private _checkboxToggle(_event?: Event) {
|
|
132
|
+
const checked = !this.isChecked();
|
|
133
|
+
this._change(checked);
|
|
135
134
|
const eventType = checked ? 'checked' : 'unchecked';
|
|
136
|
-
|
|
135
|
+
this._fireEvent(eventType);
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
function change(checked: boolean) {
|
|
138
|
+
private _change(checked: boolean) {
|
|
141
139
|
const payload: KTDataTableCheckChangePayloadInterface = { cancel: false };
|
|
142
|
-
|
|
140
|
+
this._fireEvent('change', payload);
|
|
143
141
|
if (payload.cancel === true) return;
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
if (
|
|
147
|
-
const visibleIds =
|
|
148
|
-
let selectedRows =
|
|
142
|
+
this._headerChecked = checked;
|
|
143
|
+
if (this._headerCheckElement) this._headerCheckElement.checked = checked;
|
|
144
|
+
if (this._targetElements) {
|
|
145
|
+
const visibleIds = this._getVisibleRowIds();
|
|
146
|
+
let selectedRows = this._getSelectedRows();
|
|
149
147
|
if (checked) {
|
|
150
|
-
|
|
151
|
-
selectedRows = preserveSelection
|
|
148
|
+
selectedRows = this._preserveSelection
|
|
152
149
|
? Array.from(new Set([...selectedRows, ...visibleIds]))
|
|
153
150
|
: visibleIds;
|
|
154
151
|
} else {
|
|
155
|
-
|
|
156
|
-
selectedRows = preserveSelection
|
|
152
|
+
selectedRows = this._preserveSelection
|
|
157
153
|
? selectedRows.filter((v) => !visibleIds.includes(v))
|
|
158
154
|
: [];
|
|
159
155
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
targetElements.forEach((element: HTMLInputElement) => {
|
|
156
|
+
this._setSelectedRows(selectedRows);
|
|
157
|
+
this._targetElements.forEach((element: HTMLInputElement) => {
|
|
163
158
|
if (element) {
|
|
164
159
|
element.checked = checked;
|
|
165
160
|
}
|
|
166
161
|
});
|
|
167
162
|
}
|
|
168
|
-
|
|
169
|
-
|
|
163
|
+
this._updateHeaderCheckboxState();
|
|
164
|
+
this._fireEvent('changed');
|
|
170
165
|
}
|
|
171
166
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
targetElements.forEach((element: HTMLInputElement) => {
|
|
167
|
+
private _reapplyCheckedStates() {
|
|
168
|
+
const selectedRows = this._getSelectedRows();
|
|
169
|
+
if (!this._targetElements) return;
|
|
170
|
+
this._targetElements.forEach((element: HTMLInputElement) => {
|
|
177
171
|
if (!element) return;
|
|
178
172
|
const value = element.value;
|
|
179
173
|
element.checked = selectedRows.includes(value);
|
|
180
|
-
// Update row class
|
|
181
174
|
const row = element.closest('tr');
|
|
182
|
-
if (row &&
|
|
175
|
+
if (row && this._config.checkbox?.checkedClass) {
|
|
183
176
|
if (element.checked) {
|
|
184
|
-
row.classList.add(
|
|
177
|
+
row.classList.add(this._config.checkbox.checkedClass);
|
|
185
178
|
} else {
|
|
186
|
-
row.classList.remove(
|
|
179
|
+
row.classList.remove(this._config.checkbox.checkedClass);
|
|
187
180
|
}
|
|
188
181
|
}
|
|
189
182
|
});
|
|
190
183
|
}
|
|
191
184
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const total = targetElements.length;
|
|
185
|
+
private _updateHeaderCheckboxState() {
|
|
186
|
+
if (!this._headerCheckElement || !this._targetElements) return;
|
|
187
|
+
const total = this._targetElements.length;
|
|
196
188
|
let checked = 0;
|
|
197
189
|
for (let i = 0; i < total; i++) {
|
|
198
|
-
if (
|
|
190
|
+
if (this._targetElements[i].checked) checked++;
|
|
199
191
|
}
|
|
200
192
|
if (checked === 0) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
193
|
+
this._headerCheckElement.indeterminate = false;
|
|
194
|
+
this._headerCheckElement.checked = false;
|
|
195
|
+
this._headerChecked = false;
|
|
204
196
|
} else if (checked > 0 && checked < total) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
197
|
+
this._headerCheckElement.indeterminate = true;
|
|
198
|
+
this._headerCheckElement.checked = false;
|
|
199
|
+
this._headerChecked = false;
|
|
208
200
|
} else if (checked === total) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
201
|
+
this._headerCheckElement.indeterminate = false;
|
|
202
|
+
this._headerCheckElement.checked = true;
|
|
203
|
+
this._headerChecked = true;
|
|
212
204
|
}
|
|
213
205
|
}
|
|
214
206
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return headerChecked;
|
|
207
|
+
public isChecked(): boolean {
|
|
208
|
+
return this._headerChecked;
|
|
218
209
|
}
|
|
219
210
|
|
|
220
|
-
|
|
221
|
-
return
|
|
211
|
+
public getChecked(): string[] {
|
|
212
|
+
return this._getSelectedRows();
|
|
222
213
|
}
|
|
223
214
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
215
|
+
public check() {
|
|
216
|
+
this._change(true);
|
|
217
|
+
this._reapplyCheckedStates();
|
|
218
|
+
this._updateHeaderCheckboxState();
|
|
228
219
|
}
|
|
229
220
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
221
|
+
public uncheck() {
|
|
222
|
+
this._change(false);
|
|
223
|
+
this._reapplyCheckedStates();
|
|
224
|
+
this._updateHeaderCheckboxState();
|
|
234
225
|
}
|
|
235
226
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
227
|
+
public toggle() {
|
|
228
|
+
this._checkboxToggle();
|
|
229
|
+
this._reapplyCheckedStates();
|
|
230
|
+
this._updateHeaderCheckboxState();
|
|
240
231
|
}
|
|
241
232
|
|
|
242
|
-
|
|
243
|
-
const rowCheckSel =
|
|
233
|
+
public updateState() {
|
|
234
|
+
const rowCheckSel = this._config.attributes?.checkbox;
|
|
244
235
|
if (!rowCheckSel) {
|
|
245
236
|
return;
|
|
246
237
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
238
|
+
this._targetElements =
|
|
239
|
+
this._element.querySelectorAll<HTMLInputElement>(rowCheckSel);
|
|
240
|
+
this._reapplyCheckedStates();
|
|
241
|
+
this._updateHeaderCheckboxState();
|
|
250
242
|
}
|
|
251
243
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
244
|
+
public dispose() {
|
|
245
|
+
if (this._headerCheckElement) {
|
|
246
|
+
this._headerCheckElement.removeEventListener(
|
|
247
|
+
'click',
|
|
248
|
+
this._checkboxListener,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const rowCheckboxSelector = this._config.attributes?.checkbox;
|
|
252
|
+
if (this._delegatedEventId && rowCheckboxSelector) {
|
|
253
|
+
KTEventHandler.off(this._element, 'input', this._delegatedEventId);
|
|
254
|
+
this._delegatedEventId = null;
|
|
255
|
+
}
|
|
256
|
+
this._headerCheckElement = null;
|
|
257
|
+
this._targetElements = null;
|
|
258
|
+
}
|
|
261
259
|
}
|
|
260
|
+
|