@keenthemes/ktui 1.0.25 → 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 (48) hide show
  1. package/dist/ktui.js +213 -33
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +210 -0
  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/formdata-remote-test.html +161 -0
  10. package/examples/select/modal-positioning-test.html +336 -0
  11. package/lib/cjs/components/datatable/datatable-checkbox.js +16 -3
  12. package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
  13. package/lib/cjs/components/datatable/datatable.js +3 -5
  14. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  15. package/lib/cjs/components/image-input/image-input.js.map +1 -1
  16. package/lib/cjs/components/modal/modal.js +3 -1
  17. package/lib/cjs/components/modal/modal.js.map +1 -1
  18. package/lib/cjs/components/select/config.js +5 -0
  19. package/lib/cjs/components/select/config.js.map +1 -1
  20. package/lib/cjs/components/select/dropdown.js +25 -2
  21. package/lib/cjs/components/select/dropdown.js.map +1 -1
  22. package/lib/cjs/components/select/select.js +161 -22
  23. package/lib/cjs/components/select/select.js.map +1 -1
  24. package/lib/cjs/components/select/templates.js.map +1 -1
  25. package/lib/esm/components/datatable/datatable-checkbox.js +16 -3
  26. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  27. package/lib/esm/components/datatable/datatable.js +3 -5
  28. package/lib/esm/components/datatable/datatable.js.map +1 -1
  29. package/lib/esm/components/image-input/image-input.js.map +1 -1
  30. package/lib/esm/components/modal/modal.js +3 -1
  31. package/lib/esm/components/modal/modal.js.map +1 -1
  32. package/lib/esm/components/select/config.js +5 -0
  33. package/lib/esm/components/select/config.js.map +1 -1
  34. package/lib/esm/components/select/dropdown.js +25 -2
  35. package/lib/esm/components/select/dropdown.js.map +1 -1
  36. package/lib/esm/components/select/select.js +161 -22
  37. package/lib/esm/components/select/select.js.map +1 -1
  38. package/lib/esm/components/select/templates.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/datatable/datatable-checkbox.ts +18 -3
  41. package/src/components/datatable/datatable.ts +3 -0
  42. package/src/components/datatable/types.ts +1 -0
  43. package/src/components/image-input/image-input.ts +12 -15
  44. package/src/components/modal/modal.ts +5 -1
  45. package/src/components/select/config.ts +6 -0
  46. package/src/components/select/dropdown.ts +32 -3
  47. package/src/components/select/select.ts +192 -35
  48. package/src/components/select/templates.ts +2 -1
@@ -28,10 +28,7 @@ export class KTImageInput extends KTComponent implements KTImageInputInterface {
28
28
  protected _lastMode: string;
29
29
  protected _selectedFile: File | null = null;
30
30
 
31
- constructor(
32
- element: HTMLElement,
33
- config?: KTImageInputConfigInterface,
34
- ) {
31
+ constructor(element: HTMLElement, config?: KTImageInputConfigInterface) {
35
32
  super();
36
33
 
37
34
  if (KTData.has(element as HTMLElement, this._name)) return;
@@ -39,14 +36,14 @@ export class KTImageInput extends KTComponent implements KTImageInputInterface {
39
36
  this._init(element);
40
37
  this._buildConfig(config);
41
38
 
42
- this._inputElement = this._element.querySelector('input[type="file"]')!;
43
- this._hiddenElement = this._element.querySelector('input[type="hidden"]')!;
44
- this._removeElement = this._element.querySelector(
45
- '[data-kt-image-input-remove]',
46
- )!;
47
- this._previewElement = this._element.querySelector(
48
- '[data-kt-image-input-preview]',
49
- )!;
39
+ this._inputElement = this._element.querySelector('input[type="file"]')!;
40
+ this._hiddenElement = this._element.querySelector('input[type="hidden"]')!;
41
+ this._removeElement = this._element.querySelector(
42
+ '[data-kt-image-input-remove]',
43
+ )!;
44
+ this._previewElement = this._element.querySelector(
45
+ '[data-kt-image-input-preview]',
46
+ )!;
50
47
 
51
48
  this._update();
52
49
  this._handlers();
@@ -87,9 +84,9 @@ export class KTImageInput extends KTComponent implements KTImageInputInterface {
87
84
  this._previewElement.style.backgroundImage = `url(${reader.result})`;
88
85
  };
89
86
 
90
- reader.readAsDataURL(this._inputElement.files[0]);
91
- this._selectedFile = this._inputElement.files[0];
92
- this._lastMode = 'new';
87
+ reader.readAsDataURL(this._inputElement.files[0]);
88
+ this._selectedFile = this._inputElement.files[0];
89
+ this._lastMode = 'new';
93
90
 
94
91
  this._element.classList.add('changed');
95
92
  this._removeElement.classList.remove('hidden');
@@ -50,7 +50,11 @@ export class KTModal extends KTComponent implements KTModalInterface {
50
50
  this._element.addEventListener('click', (event: Event) => {
51
51
  if (this._element !== event.target) return;
52
52
 
53
- if (this._getOption('backdropStatic') === false) {
53
+ // Only hide if both backdropStatic is false AND persistent is false
54
+ if (
55
+ this._getOption('backdropStatic') === false &&
56
+ KTUtils.stringToBoolean(this._getOption('persistent')) === false
57
+ ) {
54
58
  this._hide();
55
59
  }
56
60
  });
@@ -291,6 +291,12 @@ export class KTSelectState {
291
291
  }
292
292
 
293
293
  public setSelectedOptions(value: string | string[]): void {
294
+ // Handle empty array case first to prevent undefined elements
295
+ if (Array.isArray(value) && value.length === 0) {
296
+ this._selectedOptions = [];
297
+ return;
298
+ }
299
+
294
300
  if (
295
301
  this._config.multiple &&
296
302
  typeof value === 'string' &&
@@ -152,6 +152,31 @@ export class KTSelectDropdown extends KTComponent {
152
152
  }
153
153
  }
154
154
 
155
+ /**
156
+ * Detect if the select is inside a modal container
157
+ * @returns The modal element if found, null otherwise
158
+ */
159
+ private _getModalContainer(): HTMLElement | null {
160
+ return this._element.closest(
161
+ '[data-kt-modal], .kt-modal, .kt-modal-center',
162
+ ) as HTMLElement | null;
163
+ }
164
+
165
+ /**
166
+ * Get the appropriate positioning strategy based on context
167
+ * @returns 'fixed' if inside modal, 'absolute' otherwise
168
+ */
169
+ private _getPositioningStrategy(): 'fixed' | 'absolute' {
170
+ // Check if config explicitly sets strategy
171
+ if (this._config.dropdownStrategy) {
172
+ return this._config.dropdownStrategy as 'fixed' | 'absolute';
173
+ }
174
+
175
+ // Use fixed positioning if inside a modal (to handle transform-based centering)
176
+ const modalParent = this._getModalContainer();
177
+ return modalParent ? 'fixed' : 'absolute';
178
+ }
179
+
155
180
  /**
156
181
  * Initialize the Popper instance for dropdown positioning
157
182
  */
@@ -164,17 +189,21 @@ export class KTSelectDropdown extends KTComponent {
164
189
 
165
190
  // Get configuration options
166
191
  const placement = this._config.dropdownPlacement || 'bottom-start';
167
- const strategy = this._config.dropdownStrategy || 'fixed';
192
+ const strategy = this._getPositioningStrategy();
168
193
  const preventOverflow = this._config.dropdownPreventOverflow !== false;
169
194
  const flip = this._config.dropdownFlip !== false;
170
195
 
196
+ // Detect modal container for boundary
197
+ const modalParent = this._getModalContainer();
198
+ const boundary = modalParent || 'clippingParents';
199
+
171
200
  // Create new popper instance
172
201
  this._popperInstance = createPopper(
173
202
  this._toggleElement,
174
203
  this._dropdownElement,
175
204
  {
176
205
  placement: placement as Placement,
177
- strategy: strategy as 'fixed' | 'absolute',
206
+ strategy: strategy,
178
207
  modifiers: [
179
208
  {
180
209
  name: 'offset',
@@ -185,7 +214,7 @@ export class KTSelectDropdown extends KTComponent {
185
214
  {
186
215
  name: 'preventOverflow',
187
216
  options: {
188
- boundary: 'viewport',
217
+ boundary: boundary,
189
218
  altAxis: preventOverflow,
190
219
  },
191
220
  },
@@ -1223,6 +1223,24 @@ export class KTSelect extends KTComponent {
1223
1223
  });
1224
1224
  }
1225
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
+
1226
1244
  /**
1227
1245
  * Update selected option display value
1228
1246
  */
@@ -1231,6 +1249,9 @@ export class KTSelect extends KTComponent {
1231
1249
  const tagsEnabled = this._config.tags && this._tagsModule;
1232
1250
  const valueDisplayEl = this.getValueDisplayElement();
1233
1251
 
1252
+ // Sync native select value for FormData support
1253
+ this._syncNativeSelectValue();
1254
+
1234
1255
  if (tagsEnabled) {
1235
1256
  // Tags module will render tags if selectedOptions > 0, or clear them if selectedOptions === 0.
1236
1257
  this._tagsModule.updateTagsDisplay(selectedOptions);
@@ -1349,6 +1370,9 @@ export class KTSelect extends KTComponent {
1349
1370
  (opt as HTMLOptionElement).selected = false;
1350
1371
  });
1351
1372
 
1373
+ // Clear native select value
1374
+ (this._element as HTMLSelectElement).value = '';
1375
+
1352
1376
  this.updateSelectedOptionDisplay();
1353
1377
  this._updateSelectedOptionClass();
1354
1378
 
@@ -1805,46 +1829,178 @@ export class KTSelect extends KTComponent {
1805
1829
 
1806
1830
  /**
1807
1831
  * Update the dropdown to sync with native select element changes
1808
- * Optionally accepts new options to replace existing ones
1832
+ * For remote selects, refetches data from the server
1833
+ * Optionally accepts new options to replace existing ones (static selects only)
1809
1834
  * @param newOptions Optional array of new options [{value, text}, ...]
1810
1835
  * @public
1811
1836
  */
1812
1837
  public update(newOptions?: Array<{ value: string; text: string }>): void {
1813
- if (newOptions) {
1814
- // Clear existing options except placeholder
1815
- this._clearExistingOptions();
1816
-
1817
- // Add new options to native select
1818
- newOptions.forEach((opt) => {
1819
- const option = document.createElement('option');
1820
- option.value = opt.value;
1821
- option.textContent = opt.text;
1822
- this._element.appendChild(option);
1823
- });
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');
1824
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
+ }
1825
1934
 
1826
- // Rebuild dropdown from native select
1827
- this._rebuildOptionsFromNative();
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);
1828
1942
 
1829
- // Dispatch updated event
1830
- this._dispatchEvent('updated');
1831
- this._fireEvent('updated');
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
+ });
1832
1950
  }
1833
1951
 
1834
1952
  /**
1835
1953
  * Refresh the visual display and state without rebuilding options
1954
+ * For remote selects, refetches data from the server
1836
1955
  * @public
1837
1956
  */
1838
1957
  public refresh(): void {
1839
- // Sync internal state from native select first
1840
- this._syncSelectionFromNative();
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
+ });
1841
1974
 
1842
- // Reapply ARIA attributes
1843
- this._setAriaAttributes();
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();
1844
1999
 
1845
- // Dispatch refreshed event
1846
- this._dispatchEvent('refreshed');
1847
- this._fireEvent('refreshed');
2000
+ // Dispatch refreshed event
2001
+ this._dispatchEvent('refreshed');
2002
+ this._fireEvent('refreshed');
2003
+ }
1848
2004
  }
1849
2005
 
1850
2006
  /**
@@ -1853,7 +2009,6 @@ export class KTSelect extends KTComponent {
1853
2009
  * ========================================================================
1854
2010
  */
1855
2011
 
1856
-
1857
2012
  /**
1858
2013
  * Create instances of KTSelect for all matching elements
1859
2014
  */
@@ -2091,7 +2246,9 @@ export class KTSelect extends KTComponent {
2091
2246
  });
2092
2247
 
2093
2248
  if (this._config.debug) {
2094
- console.log(`Updated original select with ${items.length} search results`);
2249
+ console.log(
2250
+ `Updated original select with ${items.length} search results`,
2251
+ );
2095
2252
  }
2096
2253
  }
2097
2254
 
@@ -2367,16 +2524,16 @@ export class KTSelect extends KTComponent {
2367
2524
  return;
2368
2525
  }
2369
2526
  const selectOption = new KTSelectOption(optionElement, this._config);
2370
- const renderedOption = selectOption.render();
2371
- optionsContainer.appendChild(renderedOption);
2372
- });
2373
- // Update internal references
2374
- this._options = this._dropdownContentElement.querySelectorAll(
2375
- '[data-kt-select-option]',
2376
- ) as NodeListOf<HTMLElement>;
2527
+ const renderedOption = selectOption.render();
2528
+ optionsContainer.appendChild(renderedOption);
2529
+ });
2530
+ // Update internal references
2531
+ this._options = this._dropdownContentElement.querySelectorAll(
2532
+ '[data-kt-select-option]',
2533
+ ) as NodeListOf<HTMLElement>;
2534
+ }
2377
2535
  }
2378
- }
2379
- // Sync selection after rebuilding
2536
+ // Sync selection after rebuilding
2380
2537
  this._syncSelectionFromNative();
2381
2538
  this.updateSelectedOptionDisplay();
2382
2539
  this._updateSelectedOptionClass();
@@ -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;