@keenthemes/ktui 1.0.24 → 1.0.26

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 (53) hide show
  1. package/dist/ktui.js +337 -18
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +319 -11
  5. package/examples/datatable/checkbox-events-test.html +400 -0
  6. package/examples/datatable/credentials-test.html +423 -0
  7. package/examples/datatable/remote-checkbox-test.html +365 -0
  8. package/examples/modal/persistent-test.html +205 -0
  9. package/examples/select/dark-mode-test.html +93 -0
  10. package/examples/select/dropdowncontainer-test.html +111 -0
  11. package/examples/select/dynamic-methods.html +273 -0
  12. package/examples/select/formdata-remote-test.html +161 -0
  13. package/examples/select/modal-positioning-test.html +336 -0
  14. package/examples/select/remote-data-preselected.html +283 -0
  15. package/lib/cjs/components/datatable/datatable-checkbox.js +16 -3
  16. package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
  17. package/lib/cjs/components/datatable/datatable.js +3 -5
  18. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  19. package/lib/cjs/components/image-input/image-input.js.map +1 -1
  20. package/lib/cjs/components/modal/modal.js +3 -1
  21. package/lib/cjs/components/modal/modal.js.map +1 -1
  22. package/lib/cjs/components/select/config.js +5 -0
  23. package/lib/cjs/components/select/config.js.map +1 -1
  24. package/lib/cjs/components/select/dropdown.js +25 -2
  25. package/lib/cjs/components/select/dropdown.js.map +1 -1
  26. package/lib/cjs/components/select/select.js +285 -7
  27. package/lib/cjs/components/select/select.js.map +1 -1
  28. package/lib/cjs/components/select/templates.js.map +1 -1
  29. package/lib/esm/components/datatable/datatable-checkbox.js +16 -3
  30. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  31. package/lib/esm/components/datatable/datatable.js +3 -5
  32. package/lib/esm/components/datatable/datatable.js.map +1 -1
  33. package/lib/esm/components/image-input/image-input.js.map +1 -1
  34. package/lib/esm/components/modal/modal.js +3 -1
  35. package/lib/esm/components/modal/modal.js.map +1 -1
  36. package/lib/esm/components/select/config.js +5 -0
  37. package/lib/esm/components/select/config.js.map +1 -1
  38. package/lib/esm/components/select/dropdown.js +25 -2
  39. package/lib/esm/components/select/dropdown.js.map +1 -1
  40. package/lib/esm/components/select/select.js +285 -7
  41. package/lib/esm/components/select/select.js.map +1 -1
  42. package/lib/esm/components/select/templates.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/components/datatable/datatable-checkbox.ts +18 -3
  45. package/src/components/datatable/datatable.ts +3 -0
  46. package/src/components/datatable/types.ts +1 -0
  47. package/src/components/image-input/image-input.ts +12 -15
  48. package/src/components/modal/modal.ts +5 -1
  49. package/src/components/select/config.ts +6 -0
  50. package/src/components/select/dropdown.ts +32 -3
  51. package/src/components/select/select.css +4 -4
  52. package/src/components/select/select.ts +350 -9
  53. package/src/components/select/templates.ts +2 -1
@@ -60,6 +60,7 @@ export class KTSelect extends KTComponent {
60
60
  private _eventManager: EventManager;
61
61
  private _typeToSearchBuffer: TypeToSearchBuffer = new TypeToSearchBuffer();
62
62
  private _mutationObserver: MutationObserver | null = null;
63
+ private _preSelectedValues: string[] = [];
63
64
 
64
65
  /**
65
66
  * Constructor: Initializes the select component
@@ -200,6 +201,21 @@ export class KTSelect extends KTComponent {
200
201
  * Clear existing options from the select element
201
202
  */
202
203
  private _clearExistingOptions() {
204
+ // Capture pre-selected values before clearing (for remote data)
205
+ const selectedOptions = Array.from(
206
+ this._element.querySelectorAll('option[selected]:not([value=""])'),
207
+ ) as HTMLOptionElement[];
208
+
209
+ if (selectedOptions.length > 0) {
210
+ this._preSelectedValues = selectedOptions.map((opt) => opt.value);
211
+ if (this._config.debug) {
212
+ console.log(
213
+ 'Captured pre-selected values before clearing:',
214
+ this._preSelectedValues,
215
+ );
216
+ }
217
+ }
218
+
203
219
  // Keep only the empty/placeholder option and remove the rest
204
220
  const options = Array.from(
205
221
  this._element.querySelectorAll('option:not([value=""])'),
@@ -268,7 +284,7 @@ export class KTSelect extends KTComponent {
268
284
  optionsContainer.appendChild(fragment);
269
285
 
270
286
  // Update options NodeList
271
- this._options = this._wrapperElement.querySelectorAll(
287
+ this._options = this._dropdownContentElement.querySelectorAll(
272
288
  '[data-kt-select-option]',
273
289
  ) as NodeListOf<HTMLElement>;
274
290
 
@@ -295,6 +311,67 @@ export class KTSelect extends KTComponent {
295
311
  // Initialize options
296
312
  this._preSelectOptions(this._element);
297
313
 
314
+ // Apply pre-selected values captured before remote data was loaded
315
+ if (this._preSelectedValues.length > 0) {
316
+ if (this._config.debug) {
317
+ console.log(
318
+ 'Applying pre-selected values after remote data loaded:',
319
+ this._preSelectedValues,
320
+ );
321
+ }
322
+
323
+ // Get all available option values from the loaded remote data
324
+ const availableValues = Array.from(
325
+ this._element.querySelectorAll('option'),
326
+ ).map((opt) => (opt as HTMLOptionElement).value);
327
+
328
+ // Filter pre-selected values to only those that exist in remote data
329
+ const validPreSelectedValues = this._preSelectedValues.filter((value) =>
330
+ availableValues.includes(value),
331
+ );
332
+
333
+ if (validPreSelectedValues.length > 0) {
334
+ // For single-select mode, only use the first value
335
+ const valuesToSelect = this._config.multiple
336
+ ? validPreSelectedValues
337
+ : [validPreSelectedValues[0]];
338
+
339
+ if (this._config.debug) {
340
+ console.log('Selecting matched values:', valuesToSelect);
341
+ }
342
+
343
+ // Get any existing selections from _preSelectOptions (e.g., data-kt-select-pre-selected)
344
+ const existingSelections = this._state.getSelectedOptions();
345
+
346
+ // Merge existing selections with native pre-selected values (no duplicates)
347
+ const allSelections = this._config.multiple
348
+ ? Array.from(new Set([...existingSelections, ...valuesToSelect]))
349
+ : valuesToSelect;
350
+
351
+ // Set all selections at once to avoid toggling issues
352
+ this._state.setSelectedOptions(allSelections);
353
+
354
+ // Update the native select element to match
355
+ Array.from(this._element.querySelectorAll('option')).forEach((opt) => {
356
+ (opt as HTMLOptionElement).selected = allSelections.includes(
357
+ opt.value,
358
+ );
359
+ });
360
+
361
+ // Update the visual display
362
+ this.updateSelectedOptionDisplay();
363
+ this._updateSelectedOptionClass();
364
+ } else if (this._config.debug) {
365
+ console.log(
366
+ 'None of the pre-selected values matched remote data:',
367
+ this._preSelectedValues,
368
+ );
369
+ }
370
+
371
+ // Clear the pre-selected values array after processing
372
+ this._preSelectedValues = [];
373
+ }
374
+
298
375
  // Apply disabled state if needed
299
376
  this._applyInitialDisabledState();
300
377
 
@@ -536,7 +613,7 @@ export class KTSelect extends KTComponent {
536
613
  });
537
614
 
538
615
  // Update options NodeList to include the new options
539
- this._options = this._wrapperElement.querySelectorAll(
616
+ this._options = this._dropdownContentElement.querySelectorAll(
540
617
  `[data-kt-select-option]`,
541
618
  ) as NodeListOf<HTMLElement>;
542
619
 
@@ -755,7 +832,7 @@ export class KTSelect extends KTComponent {
755
832
  '[data-kt-select-options]',
756
833
  ) as HTMLElement;
757
834
 
758
- this._options = this._wrapperElement.querySelectorAll(
835
+ this._options = this._dropdownContentElement.querySelectorAll(
759
836
  `[data-kt-select-option]`,
760
837
  ) as NodeListOf<HTMLElement>;
761
838
  }
@@ -1146,6 +1223,24 @@ export class KTSelect extends KTComponent {
1146
1223
  });
1147
1224
  }
1148
1225
 
1226
+ /**
1227
+ * Sync native select value attribute for FormData support
1228
+ */
1229
+ private _syncNativeSelectValue(): void {
1230
+ const selectedOptions = this.getSelectedOptions();
1231
+
1232
+ if (this._config.multiple) {
1233
+ // For multiple select, the selected options are marked via option.selected
1234
+ // The native select's value property will return the first selected option's value
1235
+ // FormData will include all selected values automatically
1236
+ } else {
1237
+ // For single select, set the value attribute explicitly
1238
+ const selectedValue =
1239
+ selectedOptions.length > 0 ? selectedOptions[0] : '';
1240
+ (this._element as HTMLSelectElement).value = selectedValue;
1241
+ }
1242
+ }
1243
+
1149
1244
  /**
1150
1245
  * Update selected option display value
1151
1246
  */
@@ -1154,6 +1249,9 @@ export class KTSelect extends KTComponent {
1154
1249
  const tagsEnabled = this._config.tags && this._tagsModule;
1155
1250
  const valueDisplayEl = this.getValueDisplayElement();
1156
1251
 
1252
+ // Sync native select value for FormData support
1253
+ this._syncNativeSelectValue();
1254
+
1157
1255
  if (tagsEnabled) {
1158
1256
  // Tags module will render tags if selectedOptions > 0, or clear them if selectedOptions === 0.
1159
1257
  this._tagsModule.updateTagsDisplay(selectedOptions);
@@ -1215,7 +1313,7 @@ export class KTSelect extends KTComponent {
1215
1313
  * Update CSS classes for selected options
1216
1314
  */
1217
1315
  private _updateSelectedOptionClass(): void {
1218
- const allOptions = this._wrapperElement.querySelectorAll(
1316
+ const allOptions = this._dropdownContentElement.querySelectorAll(
1219
1317
  `[data-kt-select-option]`,
1220
1318
  );
1221
1319
  const selectedValues = this._state.getSelectedOptions();
@@ -1272,6 +1370,9 @@ export class KTSelect extends KTComponent {
1272
1370
  (opt as HTMLOptionElement).selected = false;
1273
1371
  });
1274
1372
 
1373
+ // Clear native select value
1374
+ (this._element as HTMLSelectElement).value = '';
1375
+
1275
1376
  this.updateSelectedOptionDisplay();
1276
1377
  this._updateSelectedOptionClass();
1277
1378
 
@@ -1476,7 +1577,7 @@ export class KTSelect extends KTComponent {
1476
1577
  public showAllOptions() {
1477
1578
  // Get all options in the dropdown
1478
1579
  const options = Array.from(
1479
- this._wrapperElement.querySelectorAll(`[data-kt-select-option]`),
1580
+ this._dropdownContentElement.querySelectorAll(`[data-kt-select-option]`),
1480
1581
  );
1481
1582
 
1482
1583
  // Show all options by removing the hidden class and any inline styles
@@ -1665,10 +1766,248 @@ export class KTSelect extends KTComponent {
1665
1766
 
1666
1767
  /**
1667
1768
  * ========================================================================
1668
- * STATIC METHODS
1769
+ * DYNAMIC CONTROL METHODS
1669
1770
  * ========================================================================
1670
1771
  */
1671
1772
 
1773
+ /**
1774
+ * Programmatically enable the select component
1775
+ * @public
1776
+ */
1777
+ public enable(): void {
1778
+ // Update config state
1779
+ this._config.disabled = false;
1780
+
1781
+ // Remove disabled attribute from native select
1782
+ this._element.removeAttribute('disabled');
1783
+ this._element.classList.remove('disabled');
1784
+
1785
+ // Remove disabled state from wrapper and display elements
1786
+ if (this._wrapperElement) {
1787
+ this._wrapperElement.classList.remove('disabled');
1788
+ }
1789
+
1790
+ if (this._displayElement) {
1791
+ this._displayElement.removeAttribute('aria-disabled');
1792
+ }
1793
+
1794
+ // Dispatch enabled event
1795
+ this._dispatchEvent('enabled');
1796
+ this._fireEvent('enabled');
1797
+ }
1798
+
1799
+ /**
1800
+ * Programmatically disable the select component
1801
+ * @public
1802
+ */
1803
+ public disable(): void {
1804
+ // Update config state
1805
+ this._config.disabled = true;
1806
+
1807
+ // Close dropdown if currently open
1808
+ if (this._dropdownIsOpen) {
1809
+ this.closeDropdown();
1810
+ }
1811
+
1812
+ // Add disabled attribute to native select
1813
+ this._element.setAttribute('disabled', 'disabled');
1814
+ this._element.classList.add('disabled');
1815
+
1816
+ // Add disabled state to wrapper and display elements
1817
+ if (this._wrapperElement) {
1818
+ this._wrapperElement.classList.add('disabled');
1819
+ }
1820
+
1821
+ if (this._displayElement) {
1822
+ this._displayElement.setAttribute('aria-disabled', 'true');
1823
+ }
1824
+
1825
+ // Dispatch disabled event
1826
+ this._dispatchEvent('disabled');
1827
+ this._fireEvent('disabled');
1828
+ }
1829
+
1830
+ /**
1831
+ * Update the dropdown to sync with native select element changes
1832
+ * For remote selects, refetches data from the server
1833
+ * Optionally accepts new options to replace existing ones (static selects only)
1834
+ * @param newOptions Optional array of new options [{value, text}, ...]
1835
+ * @public
1836
+ */
1837
+ public update(newOptions?: Array<{ value: string; text: string }>): void {
1838
+ // For remote selects, refetch data
1839
+ if (this._config.remote && this._remoteModule) {
1840
+ this._remoteModule
1841
+ .fetchData()
1842
+ .then((items) => {
1843
+ // Clear existing options
1844
+ this._clearExistingOptions();
1845
+
1846
+ // Add new options from remote data
1847
+ items.forEach((item) => {
1848
+ const option = document.createElement('option');
1849
+ option.value = item.id;
1850
+ option.textContent = item.title;
1851
+ if (item.disabled) option.disabled = true;
1852
+ this._element.appendChild(option);
1853
+ });
1854
+
1855
+ // Rebuild dropdown
1856
+ this._rebuildOptionsFromNative();
1857
+
1858
+ // Dispatch updated event
1859
+ this._dispatchEvent('updated');
1860
+ this._fireEvent('updated');
1861
+ })
1862
+ .catch((error) => {
1863
+ console.error('Error updating remote data:', error);
1864
+ this._dispatchEvent('updateError');
1865
+ this._fireEvent('updateError');
1866
+ });
1867
+ } else {
1868
+ // For static selects, handle new options
1869
+ if (newOptions) {
1870
+ // Clear existing options except placeholder
1871
+ this._clearExistingOptions();
1872
+
1873
+ // Add new options to native select
1874
+ newOptions.forEach((opt) => {
1875
+ const option = document.createElement('option');
1876
+ option.value = opt.value;
1877
+ option.textContent = opt.text;
1878
+ this._element.appendChild(option);
1879
+ });
1880
+ }
1881
+
1882
+ // Rebuild dropdown from native select
1883
+ this._rebuildOptionsFromNative();
1884
+
1885
+ // Dispatch updated event
1886
+ this._dispatchEvent('updated');
1887
+ this._fireEvent('updated');
1888
+ }
1889
+ }
1890
+
1891
+ /**
1892
+ * Reload remote data and rebuild the dropdown
1893
+ * Only works with remote data enabled
1894
+ * @returns Promise that resolves when reload completes
1895
+ * @public
1896
+ */
1897
+ public reload(): Promise<void> {
1898
+ // Guard clause: only works with remote data
1899
+ if (!this._config.remote || !this._remoteModule) {
1900
+ console.warn('reload() only works with remote data enabled');
1901
+ return Promise.resolve();
1902
+ }
1903
+
1904
+ // Dispatch reload start event
1905
+ this._dispatchEvent('reloadStart');
1906
+ this._fireEvent('reloadStart');
1907
+
1908
+ // Fetch fresh remote data
1909
+ return this._remoteModule
1910
+ .fetchData()
1911
+ .then((items) => {
1912
+ // Clear existing options
1913
+ this._clearExistingOptions();
1914
+
1915
+ // Update state with new items
1916
+ return this._state.setItems(items).then(() => {
1917
+ // Generate new options HTML
1918
+ this._generateOptionsHtml(this._element);
1919
+
1920
+ // Update the dropdown
1921
+ this._updateDropdownWithNewOptions();
1922
+
1923
+ // Sync selection state from native select
1924
+ this._syncSelectionFromNative();
1925
+
1926
+ // Update visual display
1927
+ this.updateSelectedOptionDisplay();
1928
+ this._updateSelectedOptionClass();
1929
+
1930
+ // Update select all button state if applicable
1931
+ if (this._config.multiple && this._config.enableSelectAll) {
1932
+ this.updateSelectAllButtonState();
1933
+ }
1934
+
1935
+ // Dispatch reload complete event
1936
+ this._dispatchEvent('reloadComplete');
1937
+ this._fireEvent('reloadComplete');
1938
+ });
1939
+ })
1940
+ .catch((error) => {
1941
+ console.error('Error reloading remote data:', error);
1942
+
1943
+ // Dispatch reload error event with error details
1944
+ this._dispatchEvent('reloadError', { error });
1945
+ this._fireEvent('reloadError', { error });
1946
+
1947
+ // Re-throw error so caller can handle it
1948
+ throw error;
1949
+ });
1950
+ }
1951
+
1952
+ /**
1953
+ * Refresh the visual display and state without rebuilding options
1954
+ * For remote selects, refetches data from the server
1955
+ * @public
1956
+ */
1957
+ public refresh(): void {
1958
+ // For remote selects, refetch data
1959
+ if (this._config.remote && this._remoteModule) {
1960
+ this._remoteModule
1961
+ .fetchData()
1962
+ .then((items) => {
1963
+ // Clear existing options
1964
+ this._clearExistingOptions();
1965
+
1966
+ // Add new options
1967
+ items.forEach((item) => {
1968
+ const option = document.createElement('option');
1969
+ option.value = item.id;
1970
+ option.textContent = item.title;
1971
+ if (item.disabled) option.disabled = true;
1972
+ this._element.appendChild(option);
1973
+ });
1974
+
1975
+ // Rebuild dropdown
1976
+ this._rebuildOptionsFromNative();
1977
+
1978
+ // Sync selection state
1979
+ this._syncSelectionFromNative();
1980
+
1981
+ // Reapply ARIA attributes
1982
+ this._setAriaAttributes();
1983
+
1984
+ // Dispatch refreshed event
1985
+ this._dispatchEvent('refreshed');
1986
+ this._fireEvent('refreshed');
1987
+ })
1988
+ .catch((error) => {
1989
+ console.error('Error refreshing remote data:', error);
1990
+ this._dispatchEvent('refreshError');
1991
+ this._fireEvent('refreshError');
1992
+ });
1993
+ } else {
1994
+ // For static selects, just sync visual state
1995
+ this._syncSelectionFromNative();
1996
+
1997
+ // Reapply ARIA attributes
1998
+ this._setAriaAttributes();
1999
+
2000
+ // Dispatch refreshed event
2001
+ this._dispatchEvent('refreshed');
2002
+ this._fireEvent('refreshed');
2003
+ }
2004
+ }
2005
+
2006
+ /**
2007
+ * ========================================================================
2008
+ * STATIC METHODS
2009
+ * ========================================================================
2010
+ */
1672
2011
 
1673
2012
  /**
1674
2013
  * Create instances of KTSelect for all matching elements
@@ -1834,7 +2173,7 @@ export class KTSelect extends KTComponent {
1834
2173
  optionsContainer.innerHTML = this._originalOptionsHtml;
1835
2174
 
1836
2175
  // Update options NodeList
1837
- this._options = this._wrapperElement.querySelectorAll(
2176
+ this._options = this._dropdownContentElement.querySelectorAll(
1838
2177
  '[data-kt-select-option]',
1839
2178
  ) as NodeListOf<HTMLElement>;
1840
2179
 
@@ -1907,7 +2246,9 @@ export class KTSelect extends KTComponent {
1907
2246
  });
1908
2247
 
1909
2248
  if (this._config.debug) {
1910
- console.log(`Updated original select with ${items.length} search results`);
2249
+ console.log(
2250
+ `Updated original select with ${items.length} search results`,
2251
+ );
1911
2252
  }
1912
2253
  }
1913
2254
 
@@ -2187,7 +2528,7 @@ export class KTSelect extends KTComponent {
2187
2528
  optionsContainer.appendChild(renderedOption);
2188
2529
  });
2189
2530
  // Update internal references
2190
- this._options = this._wrapperElement.querySelectorAll(
2531
+ this._options = this._dropdownContentElement.querySelectorAll(
2191
2532
  '[data-kt-select-option]',
2192
2533
  ) as NodeListOf<HTMLElement>;
2193
2534
  }
@@ -393,7 +393,8 @@ export const defaultTemplates: KTSelectTemplateInterface = {
393
393
  config: KTSelectConfigInterface,
394
394
  ): HTMLElement => {
395
395
  let template = getTemplateStrings(config).tag;
396
- let preparedContent = option.textContent || option.innerText || option.value || ''; // Default content is the option's text
396
+ let preparedContent =
397
+ option.textContent || option.innerText || option.value || ''; // Default content is the option's text
397
398
 
398
399
  if (config.tagTemplate) {
399
400
  let tagTemplateString = config.tagTemplate;