@keenthemes/ktui 1.1.4 → 1.1.5

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.
Files changed (35) hide show
  1. package/dist/ktui.js +174 -49
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/lib/cjs/components/component.js +9 -0
  5. package/lib/cjs/components/component.js.map +1 -1
  6. package/lib/cjs/components/datatable/datatable-sort.js +80 -10
  7. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  8. package/lib/cjs/components/datatable/datatable.js +36 -5
  9. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  10. package/lib/cjs/components/drawer/drawer.js +43 -34
  11. package/lib/cjs/components/drawer/drawer.js.map +1 -1
  12. package/lib/cjs/components/dropdown/dropdown.js +6 -0
  13. package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
  14. package/lib/cjs/components/select/select.js +7 -4
  15. package/lib/cjs/components/select/select.js.map +1 -1
  16. package/lib/esm/components/component.js +9 -0
  17. package/lib/esm/components/component.js.map +1 -1
  18. package/lib/esm/components/datatable/datatable-sort.js +80 -10
  19. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  20. package/lib/esm/components/datatable/datatable.js +36 -5
  21. package/lib/esm/components/datatable/datatable.js.map +1 -1
  22. package/lib/esm/components/drawer/drawer.js +43 -34
  23. package/lib/esm/components/drawer/drawer.js.map +1 -1
  24. package/lib/esm/components/dropdown/dropdown.js +6 -0
  25. package/lib/esm/components/dropdown/dropdown.js.map +1 -1
  26. package/lib/esm/components/select/select.js +7 -4
  27. package/lib/esm/components/select/select.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/components/component.ts +10 -0
  30. package/src/components/datatable/datatable-sort.ts +89 -7
  31. package/src/components/datatable/datatable.ts +44 -13
  32. package/src/components/datatable/types.ts +14 -0
  33. package/src/components/drawer/drawer.ts +38 -35
  34. package/src/components/drawer/types.ts +4 -2
  35. package/src/components/dropdown/dropdown.ts +5 -0
@@ -44,7 +44,7 @@ export function createSortHandler<T = KTDataTableDataInterface>(
44
44
  dispatchEvent: (eventName: string, eventData?: any) => void,
45
45
  updateData: () => void,
46
46
  ): KTDataTableSortAPI<T> {
47
- // Helper to compare values for sorting
47
+ // Helper to compare values for sorting (string)
48
48
  function compareValues(
49
49
  a: unknown,
50
50
  b: unknown,
@@ -63,15 +63,80 @@ export function createSortHandler<T = KTDataTableDataInterface>(
63
63
  : 0;
64
64
  }
65
65
 
66
+ // Parse value for numeric sort: strip currency/commas, then parseFloat
67
+ function parseNumeric(value: unknown): number {
68
+ if (value === null || value === undefined || value === '') {
69
+ return Number.NaN;
70
+ }
71
+ const s = String(value).replace(/[^0-9.-]/g, '');
72
+ const n = parseFloat(s);
73
+ return Number.isNaN(n) ? Number.NaN : n;
74
+ }
75
+
76
+ // Compare two numbers; NaN sorts to the end for both asc and desc
77
+ function compareNumeric(
78
+ aNum: number,
79
+ bNum: number,
80
+ sortOrder: KTDataTableSortOrderInterface,
81
+ ): number {
82
+ const aNaN = Number.isNaN(aNum);
83
+ const bNaN = Number.isNaN(bNum);
84
+ if (aNaN && bNaN) return 0;
85
+ if (aNaN) return 1;
86
+ if (bNaN) return -1;
87
+ if (aNum < bNum) return sortOrder === 'asc' ? -1 : 1;
88
+ if (aNum > bNum) return sortOrder === 'asc' ? 1 : -1;
89
+ return 0;
90
+ }
91
+
92
+ function getColumnDef(
93
+ sortField: keyof T | number,
94
+ ): {
95
+ sortType?: 'string' | 'numeric';
96
+ sortValue?: (
97
+ cellValue: unknown,
98
+ rowData: KTDataTableDataInterface,
99
+ ) => number | string;
100
+ } | undefined {
101
+ const columns = config.columns;
102
+ if (!columns) return undefined;
103
+ const key =
104
+ typeof sortField === 'number'
105
+ ? (Object.keys(columns)[sortField] as keyof T | undefined)
106
+ : sortField;
107
+ return key !== undefined ? columns[key as string] : undefined;
108
+ }
109
+
66
110
  function sortData(
67
111
  data: T[],
68
112
  sortField: keyof T | number,
69
113
  sortOrder: KTDataTableSortOrderInterface,
70
114
  ): T[] {
115
+ const columnDef = getColumnDef(sortField);
116
+ const sortValueFn = columnDef?.sortValue;
117
+ const useNumeric =
118
+ !sortValueFn && columnDef?.sortType === 'numeric';
119
+
71
120
  return data.sort((a, b) => {
72
- const aValue = a[sortField as keyof T] as unknown;
73
- const bValue = b[sortField as keyof T] as unknown;
74
- return compareValues(aValue, bValue, sortOrder);
121
+ const aRaw = a[sortField as keyof T] as unknown;
122
+ const bRaw = b[sortField as keyof T] as unknown;
123
+
124
+ if (typeof sortValueFn === 'function') {
125
+ const aVal = sortValueFn(aRaw, a as KTDataTableDataInterface);
126
+ const bVal = sortValueFn(bRaw, b as KTDataTableDataInterface);
127
+ const aNum = typeof aVal === 'number' ? aVal : parseNumeric(aVal);
128
+ const bNum = typeof bVal === 'number' ? bVal : parseNumeric(bVal);
129
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
130
+ return compareNumeric(aNum, bNum, sortOrder);
131
+ }
132
+ return compareValues(aVal, bVal, sortOrder);
133
+ }
134
+ if (useNumeric) {
135
+ const aNum = parseNumeric(aRaw);
136
+ const bNum = parseNumeric(bRaw);
137
+ return compareNumeric(aNum, bNum, sortOrder);
138
+ }
139
+ return compareValues(aRaw, bRaw, sortOrder);
75
140
  });
76
141
  }
77
142
 
@@ -97,24 +162,41 @@ export function createSortHandler<T = KTDataTableDataInterface>(
97
162
  sortField: keyof T,
98
163
  sortOrder: KTDataTableSortOrderInterface,
99
164
  ): void {
165
+ const baseClass = config.sort?.classes?.base || '';
100
166
  const sortClass = sortOrder
101
167
  ? sortOrder === 'asc'
102
168
  ? config.sort?.classes?.asc || ''
103
169
  : config.sort?.classes?.desc || ''
104
170
  : '';
171
+ // Clear all headers: remove sort state so only the active column shows highlighted arrow
172
+ const allTh = theadElement.querySelectorAll('th');
173
+ allTh.forEach((header) => {
174
+ const el = header as HTMLElement;
175
+ el.setAttribute('aria-sort', 'none');
176
+ const sortElement = header.querySelector(`.${baseClass}`) as HTMLElement;
177
+ if (sortElement) {
178
+ sortElement.className = baseClass;
179
+ }
180
+ });
181
+ // Apply sort state to the active column so table.css [aria-sort='asc'] / [aria-sort='desc'] can highlight the arrow
105
182
  const th =
106
183
  typeof sortField === 'number'
107
- ? theadElement.querySelectorAll('th')[sortField]
184
+ ? allTh[sortField]
108
185
  : (theadElement.querySelector(
109
186
  `th[data-kt-datatable-column="${String(sortField)}"], th[data-kt-datatable-column-sort="${String(sortField)}"]`,
110
187
  ) as HTMLElement);
111
188
  if (th) {
112
189
  const sortElement = th.querySelector(
113
- `.${config.sort?.classes?.base}`,
190
+ `.${baseClass}`,
114
191
  ) as HTMLElement;
115
192
  if (sortElement) {
116
193
  sortElement.className =
117
- `${config.sort?.classes?.base} ${sortClass}`.trim();
194
+ `${baseClass} ${sortClass}`.trim();
195
+ }
196
+ if (sortOrder) {
197
+ th.setAttribute('aria-sort', sortOrder);
198
+ } else {
199
+ th.setAttribute('aria-sort', 'none');
118
200
  }
119
201
  }
120
202
  }
@@ -73,7 +73,14 @@ export class KTDataTable<T extends KTDataTableDataInterface>
73
73
  constructor(element: HTMLElement, config?: KTDataTableConfigInterface) {
74
74
  super();
75
75
 
76
- if (KTData.has(element as HTMLElement, this._name)) return;
76
+ if (KTData.has(element as HTMLElement, this._name)) {
77
+ // Already initialized (e.g. by createInstances). Merge user config so columns/sortType etc. apply.
78
+ const existing = KTDataTable.getInstance(element as HTMLElement);
79
+ if (existing && config) {
80
+ existing._mergeConfig(config);
81
+ }
82
+ return;
83
+ }
77
84
 
78
85
  this._defaultConfig = this._initDefaultConfig(config);
79
86
 
@@ -668,13 +675,13 @@ export class KTDataTable<T extends KTDataTableDataInterface>
668
675
  this._storeOriginalClasses();
669
676
 
670
677
  const rows = this._tbodyElement.querySelectorAll<HTMLTableRowElement>('tr');
671
-
678
+
672
679
  // Filter th elements to only include those with data-kt-datatable-column attribute
673
680
  const allThs: NodeListOf<HTMLTableCellElement> = this._theadElement
674
681
  ? this._theadElement.querySelectorAll('th')
675
682
  : ([] as unknown as NodeListOf<HTMLTableCellElement>);
676
-
677
- const ths: HTMLTableCellElement[] = Array.from(allThs).filter(th =>
683
+
684
+ const ths: HTMLTableCellElement[] = Array.from(allThs).filter(th =>
678
685
  th.hasAttribute('data-kt-datatable-column')
679
686
  );
680
687
 
@@ -708,15 +715,15 @@ export class KTDataTable<T extends KTDataTableDataInterface>
708
715
  */
709
716
  private _localTableHeaderInvalidate(): boolean {
710
717
  const { originalData } = this.getState();
711
-
718
+
712
719
  // Count only th elements with data-kt-datatable-column attribute
713
720
  const allThs: NodeListOf<HTMLTableCellElement> = this._theadElement
714
721
  ? this._theadElement.querySelectorAll('th')
715
722
  : ([] as unknown as NodeListOf<HTMLTableCellElement>);
716
- const currentTableHeaders = Array.from(allThs).filter(th =>
723
+ const currentTableHeaders = Array.from(allThs).filter(th =>
717
724
  th.hasAttribute('data-kt-datatable-column')
718
725
  ).length;
719
-
726
+
720
727
  const totalColumns = originalData.length
721
728
  ? Object.keys(originalData[0]).length
722
729
  : 0;
@@ -1015,10 +1022,12 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1015
1022
  const allThs: NodeListOf<HTMLTableCellElement> = this._theadElement
1016
1023
  ? this._theadElement.querySelectorAll('th')
1017
1024
  : ([] as unknown as NodeListOf<HTMLTableCellElement>);
1018
-
1019
- const ths: HTMLTableCellElement[] = Array.from(allThs).filter(th =>
1025
+
1026
+ const ths: HTMLTableCellElement[] = Array.from(allThs).filter(th =>
1020
1027
  th.hasAttribute('data-kt-datatable-column')
1021
1028
  );
1029
+ // When no th has data-kt-datatable-column, use all ths so we still render by column index (data extracted with numeric keys)
1030
+ const columnsToRender: HTMLTableCellElement[] = ths.length > 0 ? ths : Array.from(allThs);
1022
1031
 
1023
1032
  this._data.forEach((item: T, rowIndex: number) => {
1024
1033
  const row = document.createElement('tr');
@@ -1033,8 +1042,8 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1033
1042
  ? this.getState().originalDataAttributes[rowIndex]
1034
1043
  : null;
1035
1044
 
1036
- // Use the order of <th> elements with data-kt-datatable-column to render <td>s in the correct order
1037
- ths.forEach((th, colIndex) => {
1045
+ // Use columnsToRender so tables without data-kt-datatable-column still get cells (by index)
1046
+ columnsToRender.forEach((th, colIndex) => {
1038
1047
  const colName = th.getAttribute('data-kt-datatable-column');
1039
1048
  const td = document.createElement('td');
1040
1049
  let value: any;
@@ -1811,8 +1820,30 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1811
1820
  */
1812
1821
  public static init(): void {
1813
1822
  if (typeof document === 'undefined') return;
1814
- // Create instances of KTDataTable for all elements with a
1815
- // data-kt-datatable="true" attribute
1823
+ KTDataTable.createInstances();
1824
+ }
1825
+
1826
+ /**
1827
+ * Force reinitialization of datatables by clearing existing instances.
1828
+ * Useful for Livewire wire:navigate where the DOM is replaced and new tables need to be initialized.
1829
+ */
1830
+ public static reinit(): void {
1831
+ if (typeof document === 'undefined') return;
1832
+ const elements = document.querySelectorAll<HTMLElement>('[data-kt-datatable="true"]');
1833
+ elements.forEach((element) => {
1834
+ try {
1835
+ const instance = KTDataTable.getInstance(element);
1836
+ if (instance && typeof instance.dispose === 'function') {
1837
+ instance.dispose();
1838
+ }
1839
+ KTData.remove(element, 'datatable');
1840
+ element.removeAttribute('data-kt-datatable-initialized');
1841
+ element.classList.remove('datatable-initialized');
1842
+ } catch {
1843
+ // ignore per-element errors
1844
+ }
1845
+ });
1846
+ KTDataTable._instances.clear();
1816
1847
  KTDataTable.createInstances();
1817
1848
  }
1818
1849
 
@@ -101,6 +101,20 @@ export interface KTDataTableConfigInterface {
101
101
  rowData: KTDataTableDataInterface,
102
102
  row: HTMLTableRowElement,
103
103
  ) => void;
104
+ /**
105
+ * Sort comparison type for this column. When 'numeric', values are parsed
106
+ * (e.g. strip currency/commas) and compared as numbers.
107
+ */
108
+ sortType?: 'string' | 'numeric';
109
+ /**
110
+ * Custom value used for sorting. When set, this is used instead of the raw
111
+ * cell value (and instead of sortType). Return a number or string to sort by.
112
+ * Use for custom formats (e.g. dates, combined fields, custom parsing).
113
+ */
114
+ sortValue?: (
115
+ cellValue: KTDataTableDataInterface[keyof KTDataTableDataInterface] | string,
116
+ rowData: KTDataTableDataInterface,
117
+ ) => number | string;
104
118
  };
105
119
  };
106
120
 
@@ -33,6 +33,7 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
33
33
  persistent: false,
34
34
  container: '',
35
35
  focus: true,
36
+ keepInPlaceWithin: '',
36
37
  };
37
38
  protected override _config: KTDrawerConfigInterface = this._defaultConfig;
38
39
  protected _isOpen: boolean = false;
@@ -91,26 +92,19 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
91
92
 
92
93
  KTDrawer.hide();
93
94
 
94
- // If drawer needs to be in front of backdrop, ensure it's in body (for proper z-index stacking)
95
- // This ensures the drawer and backdrop are in the same stacking context
95
+ // When container="body", move drawer to body only if NOT inside an element matching keepInPlaceWithin.
96
+ // When keepInPlaceWithin is set (e.g. for SPA/persisted layouts), keeping the drawer in place lets the host preserve it across navigations.
96
97
  if (this._getOption('container') === 'body' && this._element.parentElement !== document.body) {
97
- // Store original parent for restoration when hiding
98
- if (!this._element.hasAttribute('data-kt-drawer-original-parent-id')) {
99
- const originalParent = this._element.parentElement;
100
- if (originalParent && originalParent !== document.body) {
101
- this._element.setAttribute('data-kt-drawer-original-parent-id', originalParent.id || '');
102
- // Store a reference to find the parent later (using closest to find Livewire component or header)
103
- const livewireComponent = originalParent.closest('[wire\\:id]');
104
- const header = originalParent.closest('header#header');
105
- if (livewireComponent) {
106
- this._element.setAttribute('data-kt-drawer-original-wire-id', (livewireComponent as HTMLElement).getAttribute('wire:id') || '');
107
- }
108
- if (header) {
109
- this._element.setAttribute('data-kt-drawer-original-in-header', 'true');
98
+ const keepInPlace = this._isKeepInPlace();
99
+ if (!keepInPlace) {
100
+ if (!this._element.hasAttribute('data-kt-drawer-original-parent-id')) {
101
+ const originalParent = this._element.parentElement;
102
+ if (originalParent && originalParent !== document.body) {
103
+ this._element.setAttribute('data-kt-drawer-original-parent-id', originalParent.id || '');
110
104
  }
111
105
  }
106
+ document.body.appendChild(this._element);
112
107
  }
113
- document.body.appendChild(this._element);
114
108
  }
115
109
 
116
110
  if (this._getOption('backdrop') === true) this._createBackdrop();
@@ -209,23 +203,11 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
209
203
  protected _handleContainer(): void {
210
204
  if (this._getOption('container')) {
211
205
  if (this._getOption('container') === 'body') {
212
- // Check if drawer is in a persisted Livewire component (like header with @persist)
213
- // If so, don't move it to body - keep it in place so Livewire can preserve it
214
- // This follows the same pattern as dropdowns/menus which work with wire:navigate
215
- const originalParent = this._element.parentNode;
216
- const isInPersistedComponent = originalParent &&
217
- ((originalParent as HTMLElement).closest('[wire\\:id]') !== null ||
218
- (originalParent as HTMLElement).closest('header#header') !== null);
219
-
220
- if (isInPersistedComponent) {
221
- // Don't move to body - keep in original location for Livewire persistence
222
- // Use fixed positioning to achieve the same visual effect
223
- // Ensure drawer has fixed positioning to work from its current location
206
+ if (this._isKeepInPlace()) {
224
207
  if (!this._element.style.position || this._element.style.position === 'static') {
225
208
  this._element.style.position = 'fixed';
226
209
  }
227
210
  } else {
228
- // Not in persisted component - safe to move to body (follows original behavior)
229
211
  document.body.appendChild(this._element);
230
212
  }
231
213
  } else {
@@ -236,6 +218,22 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
236
218
  }
237
219
  }
238
220
 
221
+ /** True when drawer is inside an element matching keepInPlaceWithin (so we keep it in place instead of moving to body). */
222
+ protected _isKeepInPlace(): boolean {
223
+ const selector = (this._getOption('keepInPlaceWithin') as string)?.trim();
224
+ if (!selector || !this._element?.parentElement) return false;
225
+ const parent = this._element.parentElement;
226
+ const selectors = selector.split(',').map((s) => s.trim()).filter(Boolean);
227
+ for (const sel of selectors) {
228
+ try {
229
+ if (parent.closest(sel) !== null) return true;
230
+ } catch {
231
+ // invalid selector, skip
232
+ }
233
+ }
234
+ return false;
235
+ }
236
+
239
237
  protected _autoFocus(): void {
240
238
  if (!this._element) return;
241
239
  const input: HTMLInputElement | null = this._element.querySelector(
@@ -253,7 +251,12 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
253
251
  this._backdropElement = document.createElement('DIV');
254
252
  this._backdropElement.style.zIndex = (zindex - 1).toString();
255
253
  this._backdropElement.setAttribute('data-kt-drawer-backdrop', 'true');
256
- document.body.append(this._backdropElement);
254
+ const parent = this._element.parentElement;
255
+ if (parent) {
256
+ parent.insertBefore(this._backdropElement, this._element);
257
+ } else {
258
+ document.body.append(this._backdropElement);
259
+ }
257
260
  KTDom.reflow(this._backdropElement);
258
261
  KTDom.addClass(
259
262
  this._backdropElement,
@@ -284,8 +287,8 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
284
287
  return KTUtils.stringToBoolean(this._getOption('enable'));
285
288
  }
286
289
 
287
- public toggle(): void {
288
- return this._toggle();
290
+ public toggle(relatedTarget?: HTMLElement): void {
291
+ return this._toggle(relatedTarget);
289
292
  }
290
293
 
291
294
  public show(relatedTarget?: HTMLElement): void {
@@ -504,7 +507,7 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
504
507
  const drawer = KTDrawer.getInstance(target);
505
508
 
506
509
  if (drawer) {
507
- drawer.toggle();
510
+ drawer.toggle(target);
508
511
  } else {
509
512
  // Drawer element not found - wait for it to appear (handles persisted Livewire components)
510
513
  // Check if drawer exists in persisted components (might be in header that's persisted)
@@ -522,7 +525,7 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
522
525
  // Get instance and toggle
523
526
  const drawerInstance = KTDrawer.getInstance(drawerElement);
524
527
  if (drawerInstance) {
525
- drawerInstance.toggle();
528
+ drawerInstance.toggle(target);
526
529
  }
527
530
  } else {
528
531
  // Drawer never appeared - trigger a reinit to see if it helps
@@ -537,7 +540,7 @@ export class KTDrawer extends KTComponent implements KTDrawerInterface {
537
540
  }
538
541
  const drawerInstance = KTDrawer.getInstance(drawerAfterReinit as HTMLElement);
539
542
  if (drawerInstance) {
540
- drawerInstance.toggle();
543
+ drawerInstance.toggle(target);
541
544
  }
542
545
  }
543
546
  }, 500);
@@ -17,9 +17,11 @@ export interface KTDrawerConfigInterface {
17
17
  persistent: boolean;
18
18
  focus: boolean;
19
19
  container: string;
20
+ /** When set, drawer is not moved to body when inside an element matching this selector (e.g. for SPA/persisted layouts). Comma-separated for multiple selectors. */
21
+ keepInPlaceWithin?: string;
20
22
  }
21
23
  export interface KTDrawerInterface {
22
- show(): void;
24
+ show(relatedTarget?: HTMLElement): void;
23
25
  hide(): void;
24
- toggle(): void;
26
+ toggle(relatedTarget?: HTMLElement): void;
25
27
  }
@@ -45,6 +45,8 @@ export class KTDropdown extends KTComponent implements KTDropdownInterface {
45
45
  protected _menuElement: HTMLElement;
46
46
  protected _isTransitioning: boolean = false;
47
47
  protected _isOpen: boolean = false;
48
+ /** Timestamp when _show() was last called; used to ignore duplicate _hide() from double handlers */
49
+ protected _shownAt: number = 0;
48
50
 
49
51
  constructor(element: HTMLElement, config?: KTDropdownConfigInterface) {
50
52
  super();
@@ -206,10 +208,13 @@ export class KTDropdown extends KTComponent implements KTDropdownInterface {
206
208
  this._fireEvent('shown');
207
209
  this._dispatchEvent('shown');
208
210
  });
211
+ this._shownAt = Date.now();
209
212
  }
210
213
 
211
214
  protected _hide(): void {
212
215
  if (!this._isOpen || this._isTransitioning) return;
216
+ // If another handler fired _hide() right after _show() (e.g. double initHandlers), ignore
217
+ if (this._shownAt && Date.now() - this._shownAt < 150) return;
213
218
 
214
219
  const payload = { cancel: false };
215
220
  this._fireEvent('hide', payload);