@keenthemes/ktui 1.0.10 → 1.0.12

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 (139) hide show
  1. package/README.md +2 -2
  2. package/dist/ktui.js +1283 -1100
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/examples/select/basic-usage.html +43 -0
  6. package/examples/select/combobox-icons.html +58 -0
  7. package/examples/select/combobox.html +46 -0
  8. package/examples/select/description.html +69 -0
  9. package/examples/select/disable-option.html +43 -0
  10. package/examples/select/disable-select.html +34 -0
  11. package/examples/select/icon-description.html +56 -0
  12. package/examples/select/icon-multiple.html +58 -0
  13. package/examples/select/icon.html +58 -0
  14. package/examples/select/max-selection.html +39 -0
  15. package/examples/select/modal.html +70 -0
  16. package/examples/select/multiple.html +42 -0
  17. package/examples/select/placeholder.html +43 -0
  18. package/examples/select/remote-data.html +32 -0
  19. package/examples/select/search.html +49 -0
  20. package/examples/select/tags-icons.html +58 -0
  21. package/examples/select/tags-selected.html +59 -0
  22. package/examples/select/tags.html +58 -0
  23. package/examples/select/template-customization.html +65 -0
  24. package/examples/select/test.html +94 -0
  25. package/examples/toast/example.html +427 -0
  26. package/lib/cjs/components/component.js +1 -1
  27. package/lib/cjs/components/component.js.map +1 -1
  28. package/lib/cjs/components/datatable/datatable.js +22 -6
  29. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  30. package/lib/cjs/components/modal/modal.js +0 -4
  31. package/lib/cjs/components/modal/modal.js.map +1 -1
  32. package/lib/cjs/components/select/combobox.js +38 -120
  33. package/lib/cjs/components/select/combobox.js.map +1 -1
  34. package/lib/cjs/components/select/config.js +4 -16
  35. package/lib/cjs/components/select/config.js.map +1 -1
  36. package/lib/cjs/components/select/dropdown.js +10 -49
  37. package/lib/cjs/components/select/dropdown.js.map +1 -1
  38. package/lib/cjs/components/select/index.js +2 -1
  39. package/lib/cjs/components/select/index.js.map +1 -1
  40. package/lib/cjs/components/select/option.js +21 -4
  41. package/lib/cjs/components/select/option.js.map +1 -1
  42. package/lib/cjs/components/select/remote.js +1 -37
  43. package/lib/cjs/components/select/remote.js.map +1 -1
  44. package/lib/cjs/components/select/search.js +11 -41
  45. package/lib/cjs/components/select/search.js.map +1 -1
  46. package/lib/cjs/components/select/select.js +213 -326
  47. package/lib/cjs/components/select/select.js.map +1 -1
  48. package/lib/cjs/components/select/tags.js +39 -31
  49. package/lib/cjs/components/select/tags.js.map +1 -1
  50. package/lib/cjs/components/select/templates.js +120 -179
  51. package/lib/cjs/components/select/templates.js.map +1 -1
  52. package/lib/cjs/components/select/types.js +0 -12
  53. package/lib/cjs/components/select/types.js.map +1 -1
  54. package/lib/cjs/components/select/utils.js +204 -257
  55. package/lib/cjs/components/select/utils.js.map +1 -1
  56. package/lib/cjs/components/toast/index.js +10 -0
  57. package/lib/cjs/components/toast/index.js.map +1 -0
  58. package/lib/cjs/components/toast/toast.js +543 -0
  59. package/lib/cjs/components/toast/toast.js.map +1 -0
  60. package/lib/cjs/components/toast/types.js +7 -0
  61. package/lib/cjs/components/toast/types.js.map +1 -0
  62. package/lib/cjs/helpers/dom.js +24 -0
  63. package/lib/cjs/helpers/dom.js.map +1 -1
  64. package/lib/cjs/index.js +5 -1
  65. package/lib/cjs/index.js.map +1 -1
  66. package/lib/esm/components/component.js +1 -1
  67. package/lib/esm/components/component.js.map +1 -1
  68. package/lib/esm/components/datatable/datatable.js +22 -6
  69. package/lib/esm/components/datatable/datatable.js.map +1 -1
  70. package/lib/esm/components/modal/modal.js +0 -4
  71. package/lib/esm/components/modal/modal.js.map +1 -1
  72. package/lib/esm/components/select/combobox.js +39 -121
  73. package/lib/esm/components/select/combobox.js.map +1 -1
  74. package/lib/esm/components/select/config.js +3 -15
  75. package/lib/esm/components/select/config.js.map +1 -1
  76. package/lib/esm/components/select/dropdown.js +10 -49
  77. package/lib/esm/components/select/dropdown.js.map +1 -1
  78. package/lib/esm/components/select/index.js +1 -1
  79. package/lib/esm/components/select/index.js.map +1 -1
  80. package/lib/esm/components/select/option.js +21 -4
  81. package/lib/esm/components/select/option.js.map +1 -1
  82. package/lib/esm/components/select/remote.js +1 -37
  83. package/lib/esm/components/select/remote.js.map +1 -1
  84. package/lib/esm/components/select/search.js +12 -42
  85. package/lib/esm/components/select/search.js.map +1 -1
  86. package/lib/esm/components/select/select.js +214 -327
  87. package/lib/esm/components/select/select.js.map +1 -1
  88. package/lib/esm/components/select/tags.js +39 -31
  89. package/lib/esm/components/select/tags.js.map +1 -1
  90. package/lib/esm/components/select/templates.js +119 -178
  91. package/lib/esm/components/select/templates.js.map +1 -1
  92. package/lib/esm/components/select/types.js +1 -11
  93. package/lib/esm/components/select/types.js.map +1 -1
  94. package/lib/esm/components/select/utils.js +201 -255
  95. package/lib/esm/components/select/utils.js.map +1 -1
  96. package/lib/esm/components/toast/index.js +6 -0
  97. package/lib/esm/components/toast/index.js.map +1 -0
  98. package/lib/esm/components/toast/toast.js +540 -0
  99. package/lib/esm/components/toast/toast.js.map +1 -0
  100. package/lib/esm/components/toast/types.js +6 -0
  101. package/lib/esm/components/toast/types.js.map +1 -0
  102. package/lib/esm/helpers/dom.js +24 -0
  103. package/lib/esm/helpers/dom.js.map +1 -1
  104. package/lib/esm/index.js +3 -0
  105. package/lib/esm/index.js.map +1 -1
  106. package/package.json +8 -6
  107. package/src/components/alert/alert.css +20 -2
  108. package/src/components/badge/badge.css +5 -0
  109. package/src/components/component.ts +4 -0
  110. package/src/components/datatable/datatable.ts +24 -16
  111. package/src/components/drawer/drawer.css +1 -1
  112. package/src/components/input/input.css +3 -1
  113. package/src/components/link/link.css +2 -2
  114. package/src/components/modal/modal.css +18 -2
  115. package/src/components/modal/modal.ts +0 -5
  116. package/src/components/select/combobox.ts +42 -149
  117. package/src/components/select/config.ts +38 -33
  118. package/src/components/select/dropdown.ts +8 -55
  119. package/src/components/select/index.ts +1 -1
  120. package/src/components/select/option.ts +28 -7
  121. package/src/components/select/remote.ts +2 -42
  122. package/src/components/select/search.ts +14 -54
  123. package/src/components/select/select.css +49 -0
  124. package/src/components/select/select.ts +231 -437
  125. package/src/components/select/tags.ts +40 -37
  126. package/src/components/select/templates.ts +166 -303
  127. package/src/components/select/types.ts +0 -10
  128. package/src/components/select/utils.ts +214 -304
  129. package/src/components/table/table.css +1 -1
  130. package/src/components/textarea/textarea.css +2 -1
  131. package/src/components/toast/index.ts +7 -0
  132. package/src/components/toast/toast.css +60 -0
  133. package/src/components/toast/toast.ts +605 -0
  134. package/src/components/toast/types.ts +169 -0
  135. package/src/helpers/dom.ts +30 -0
  136. package/src/index.ts +4 -0
  137. package/styles/main.css +3 -0
  138. package/styles/vars.css +138 -0
  139. package/styles.css +1 -0
@@ -4,57 +4,44 @@
4
4
  */
5
5
 
6
6
  import { KTSelectConfigInterface, KTSelectOption } from './config';
7
- import { SelectMode } from './types';
7
+ import { renderTemplateString } from './utils';
8
8
 
9
9
  /**
10
10
  * Default HTML string templates for KTSelect. All UI structure is defined here.
11
11
  * Users can override any template by providing a matching key in the config.templates object.
12
12
  */
13
- const defaultTemplateStrings = {
14
- dropdownContent: `<div data-kt-select-dropdown-content class="kt-select-dropdown hidden" style="z-index: {{zindex}};">{{content}}</div>`,
15
- optionsContainer: `<ul role="listbox" aria-label="{{label}}" data-kt-select-options-container style="max-height: {{height}}px; overflow-y: auto;">{{options}}</ul>`,
16
- emptyOption: `<option value="">{{placeholder}}</option>`,
17
- errorOption: `<option value="" disabled selected>{{errorMessage}}</option>`, // Template for error <option>
18
-
19
- loadMore: `<li class="py-2 px-4 text-center text-gray-600 cursor-pointer hover:bg-gray-100" data-kt-select-load-more>{{loadMoreText}}</li>`,
20
- dropdown: `<div data-kt-select-dropdown-content class="absolute z-10 w-full mt-2 bg-white border border-gray-200 rounded-md shadow-md">
21
- {{search}}
22
- <ul role="listbox" aria-label="{{label}}" data-kt-select-options-container style="max-height: {{height}}px; overflow-y: auto;">
23
- {{options}}
24
- </ul>
25
- </div>`,
26
- error: `<li class="px-3 py-2 text-red-500" role="alert">{{errorMessage}}</li>`,
27
-
28
- highlight: `<span class="highlight">{{text}}</span>`,
29
- main: `<div data-kt-select-wrapper class="relative" data-kt-select-mode="{{mode}}"></div>`,
30
- displayCombobox: `<div class="relative flex items-center w-full">
31
- <input data-kt-select-search data-kt-select-display data-kt-select-value type="text" class="flex-1 w-full items-center justify-between px-3 py-2 border border-gray-300 rounded-md cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-400" placeholder="{{placeholder}}" role="searchbox" aria-label="{{label}}" {{disabled}} />
32
- <button type="button" data-kt-select-clear-button class="absolute right-3 hidden text-gray-400 hover:text-gray-600" aria-label="Clear selection">
33
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
34
- <line x1="18" y1="6" x2="6" y2="18"></line>
35
- <line x1="6" y1="6" x2="18" y2="18"></line>
36
- </svg>
37
- </button>
38
- </div>`,
39
-
40
- icon: `<span class="option-icon mr-2"><img src="{{icon}}" class="rounded-full w-6 h-6" /></span>`,
41
- description: `<div class="option-description text-sm text-gray-500">{{description}}</div>`,
42
-
43
- display: `<div data-kt-select-display class="flex items-center justify-between px-3 py-2 border border-gray-300 rounded-md cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-400" tabindex="{{tabindex}}" role="button" aria-haspopup="listbox" aria-expanded="false" aria-label="{{label}}" {{disabled}}>
44
- <span data-kt-select-value>{{placeholder}}</span>
45
- <span data-kt-select-arrow class="ml-2">
46
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
47
- <polyline points="6 9 12 15 18 9"></polyline>
48
- </svg>
49
- </span>
50
- </div>`,
51
- option: `<li data-kt-select-option data-value="{{value}}" class="px-3 py-2 cursor-pointer hover:bg-gray-100 flex items-center{{selectedClass}}{{disabledClass}}" role="option" {{selected}} {{disabled}}>{{icon}}<div class="option-content"><div class="option-title" data-kt-option-title>{{text}}</div>{{description}}</div></li>`,
52
-
53
- optionGroup: `<li role="group" aria-label="{{label}}" class="py-1"><div class="px-3 py-1 text-xs font-semibold text-gray-500 uppercase">{{label}}</div><ul>{{optionsHtml}}</ul></li>`,
54
- search: `<div class="px-3 py-2 border-b border-gray-200"><input type="text" data-kt-select-search placeholder="{{searchPlaceholder}}" class="w-full border-none focus:outline-none text-sm" role="searchbox" aria-label="{{searchPlaceholder}}"/></div>`,
55
- noResults: `<li class="px-3 py-2 text-gray-500" role="status">{{searchNotFoundText}}</li>`,
56
- loading: `<li class="px-3 py-2 text-gray-500 italic" role="status" aria-live="polite">{{loadingMessage}}</li>`,
57
- tag: `<div data-kt-select-tag class="inline-flex items-center bg-blue-50 border border-blue-100 rounded px-2 py-1 text-sm mr-1 mb-1"><span>{{title}}</span><span data-kt-select-remove-button data-value="{{id}}" class="ml-1 text-blue-400 hover:text-blue-600 cursor-pointer" role="button" aria-label="Remove {{safeTitle}}" tabindex="0"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></span></div>`,
13
+ export const coreTemplateStrings = {
14
+ dropdown: `<div data-kt-select-dropdown class="kt-select-dropdown hidden {{class}}" style="z-index: {{zindex}};">{{content}}</div>`,
15
+ options: `<ul role="listbox" aria-label="{{label}}" class="kt-select-options {{class}}" data-kt-select-options="true">{{content}}</ul>`,
16
+ error: `<li class="kt-select-error" role="alert">{{content}}</li>`,
17
+ highlight: `<span data-kt-select-highlight class="kt-select-highlight highlighted {{class}}">{{text}}</span>`,
18
+ wrapper: `<div data-kt-select-wrapper class="kt-select-wrapper {{class}}"></div>`,
19
+ combobox: `
20
+ <div data-kt-select-combobox data-kt-select-display class="kt-select-combobox {{class}}">
21
+ <input class="kt-input kt-select-combobox-input" data-kt-select-search="true" data-kt-select-value="true" type="text" placeholder="{{placeholder}}" role="searchbox" aria-label="{{label}}" {{disabled}} />
22
+ <button type="button" data-kt-select-clear-button="true" class="kt-select-combobox-clear-btn" aria-label="Clear selection">
23
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
24
+ <line x1="18" y1="6" x2="6" y2="18"></line>
25
+ <line x1="6" y1="6" x2="18" y2="18"></line>
26
+ </svg>
27
+ </button>
28
+ </div>
29
+ `,
30
+ display: `
31
+ <div data-kt-select-display class="kt-select-display {{class}}" tabindex="{{tabindex}}" role="button" data-selected="0" aria-haspopup="listbox" aria-expanded="false" aria-label="{{label}}" {{disabled}}>
32
+ <div data-kt-select-value="true" class="kt-select-label">{{content}}</div>
33
+ </div>
34
+ `,
35
+ placeholder: `<div data-kt-select-placeholder class="kt-select-placeholder {{class}}">{{content}}</div>`,
36
+ option: `<li data-kt-select-option data-value="{{value}}" data-text="{{text}}" class="kt-select-option {{class}}" role="option" {{selected}} {{disabled}}>{{content}}</li>`,
37
+ search: `<div data-kt-select-search class="kt-select-search {{class}}"><input type="text" data-kt-select-search="true" placeholder="{{searchPlaceholder}}" class="kt-input kt-select-search-input" role="searchbox" aria-label="{{searchPlaceholder}}"/></div>`,
38
+ empty: `<li data-kt-select-empty class="kt-select-no-result {{class}}" role="status">{{content}}</li>`,
39
+ loading: `<li class="kt-select-loading {{class}}" role="status" aria-live="polite">{{content}}</li>`,
40
+ tag: `<div data-kt-select-tag="true" class="kt-select-tag {{class}}">
41
+ {{content}}
42
+ </div>`,
43
+ loadMore: `<li class="kt-select-load-more {{class}}" data-kt-select-load-more="true">{{content}}</li>`,
44
+ tagRemoveButton: `<button type="button" data-kt-select-remove-button class="kt-select-tag-remove" aria-label="Remove tag" tabindex="0"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="3" x2="9" y2="9"/><line x1="9" y1="3" x2="3" y2="9"/></svg></button>`,
58
45
  };
59
46
 
60
47
  /**
@@ -65,27 +52,15 @@ export interface KTSelectTemplateInterface {
65
52
  /**
66
53
  * Renders the dropdown content container
67
54
  */
68
- dropdownContent: (
55
+ dropdown: (
69
56
  config: KTSelectConfigInterface & { zindex?: number; content?: string },
70
57
  ) => HTMLElement;
71
58
  /**
72
59
  * Renders the options container
73
60
  */
74
- optionsContainer: (
61
+ options: (
75
62
  config: KTSelectConfigInterface & { options?: string },
76
63
  ) => HTMLElement;
77
- /**
78
- * Renders an empty <option> for native select
79
- */
80
- emptyOption: (
81
- config: KTSelectConfigInterface & { placeholder?: string },
82
- ) => HTMLOptionElement;
83
- /**
84
- * Renders an error <option> for the native select
85
- */
86
- errorOption: (
87
- config: KTSelectConfigInterface & { errorMessage: string },
88
- ) => HTMLElement;
89
64
  /**
90
65
  * Renders the load more button for pagination
91
66
  */
@@ -98,45 +73,27 @@ export interface KTSelectTemplateInterface {
98
73
  highlight: (config: KTSelectConfigInterface, text: string) => HTMLElement;
99
74
 
100
75
  // Main components
101
- main: (config: KTSelectConfigInterface) => HTMLElement;
76
+ wrapper: (config: KTSelectConfigInterface) => HTMLElement;
102
77
  display: (config: KTSelectConfigInterface) => HTMLElement;
103
- dropdown: (
104
- config: KTSelectConfigInterface,
105
- optionsHtml: string,
106
- ) => HTMLElement;
107
-
108
- // Icon rendering
109
- icon: (icon: string, config: KTSelectConfigInterface) => HTMLElement;
110
- description: (
111
- description: string,
112
- config: KTSelectConfigInterface,
113
- ) => HTMLElement;
114
78
 
115
79
  // Option rendering
116
80
  option: (
117
81
  option: KTSelectOption | HTMLOptionElement,
118
82
  config: KTSelectConfigInterface,
119
83
  ) => HTMLElement;
120
- optionGroup: (
121
- label: string,
122
- optionsHtml: string,
123
- config: KTSelectConfigInterface,
124
- ) => HTMLElement;
125
84
 
126
85
  // Search and empty states
127
86
  search: (config: KTSelectConfigInterface) => HTMLElement;
128
- noResults: (config: KTSelectConfigInterface) => HTMLElement;
87
+ empty: (config: KTSelectConfigInterface) => HTMLElement;
129
88
  loading: (
130
89
  config: KTSelectConfigInterface,
131
90
  loadingMessage: string,
132
91
  ) => HTMLElement;
133
92
 
134
93
  // Multi-select
135
- tag: (option: KTSelectOption, config: KTSelectConfigInterface) => HTMLElement;
136
- selectedDisplay: (
137
- selectedOptions: KTSelectOption[],
138
- config: KTSelectConfigInterface,
139
- ) => string;
94
+ tag: (option: HTMLOptionElement, config: KTSelectConfigInterface) => HTMLElement;
95
+
96
+ placeholder: (config: KTSelectConfigInterface) => HTMLElement;
140
97
  }
141
98
 
142
99
  /**
@@ -151,14 +108,14 @@ function stringToElement(html: string): HTMLElement {
151
108
  /**
152
109
  * User-supplied template overrides. Use setTemplateStrings() to add or update.
153
110
  */
154
- let userTemplateStrings: Partial<typeof defaultTemplateStrings> = {};
111
+ let userTemplateStrings: Partial<typeof coreTemplateStrings> = {};
155
112
 
156
113
  /**
157
114
  * Register or update user template overrides.
158
115
  * @param templates Partial template object to merge with defaults.
159
116
  */
160
117
  export function setTemplateStrings(
161
- templates: Partial<typeof defaultTemplateStrings>,
118
+ templates: Partial<typeof coreTemplateStrings>,
162
119
  ): void {
163
120
  userTemplateStrings = { ...userTemplateStrings, ...templates };
164
121
  }
@@ -169,15 +126,17 @@ export function setTemplateStrings(
169
126
  */
170
127
  export function getTemplateStrings(
171
128
  config?: KTSelectConfigInterface,
172
- ): typeof defaultTemplateStrings {
129
+ ): typeof coreTemplateStrings {
173
130
  const templates =
174
131
  config && typeof config === 'object' && 'templates' in config
175
132
  ? (config as any).templates
176
133
  : undefined;
134
+
177
135
  if (templates) {
178
- return { ...defaultTemplateStrings, ...userTemplateStrings, ...templates };
136
+ return { ...coreTemplateStrings, ...userTemplateStrings, ...templates };
179
137
  }
180
- return { ...defaultTemplateStrings, ...userTemplateStrings };
138
+
139
+ return { ...coreTemplateStrings, ...userTemplateStrings };
181
140
  }
182
141
 
183
142
  /**
@@ -189,58 +148,42 @@ export const defaultTemplates: KTSelectTemplateInterface = {
189
148
  */
190
149
  highlight: (config: KTSelectConfigInterface, text: string) => {
191
150
  const template = getTemplateStrings(config).highlight;
192
- const html = template.replace('{{text}}', text);
151
+ const html = template.replace('{{text}}', text).replace('{{class}}', config.highlightClass || '');
193
152
  return stringToElement(html);
194
153
  },
195
154
 
196
155
  /**
197
156
  * Renders the dropdown content
198
157
  */
199
- dropdownContent: (
158
+ dropdown: (
200
159
  config: KTSelectConfigInterface & { zindex?: number; content?: string },
201
160
  ) => {
202
- const template = getTemplateStrings(config).dropdownContent;
161
+ let template = getTemplateStrings(config).dropdown;
162
+ let content = config.content || '';
163
+ if (config.dropdownTemplate) {
164
+ content = renderTemplateString(config.dropdownTemplate, {
165
+ zindex: config.zindex ? String(config.zindex) : '',
166
+ content: config.content || '',
167
+ class: config.dropdownClass || '',
168
+ });
169
+ }
203
170
  const html = template
204
171
  .replace('{{zindex}}', config.zindex ? String(config.zindex) : '')
205
- .replace('{{content}}', config.content || '');
172
+ .replace('{{content}}', content)
173
+ .replace('{{class}}', config.dropdownClass || '');
206
174
  return stringToElement(html);
207
175
  },
208
176
 
209
177
  /**
210
178
  * Renders the options container for the dropdown
211
179
  */
212
- optionsContainer: (
213
- config: KTSelectConfigInterface & { options?: string },
214
- ) => {
215
- const template = getTemplateStrings(config).optionsContainer;
180
+ options: (config: KTSelectConfigInterface & { options?: string }) => {
181
+ const template = getTemplateStrings(config).options;
216
182
  const html = template
217
183
  .replace('{{label}}', config.label || 'Options')
218
184
  .replace('{{height}}', config.height ? String(config.height) : '250')
219
- .replace('{{options}}', config.options || '');
220
- return stringToElement(html);
221
- },
222
-
223
- /**
224
- * Renders an empty option in the dropdown
225
- */
226
- emptyOption: (config: KTSelectConfigInterface & { placeholder?: string }) => {
227
- const template = getTemplateStrings(config).emptyOption;
228
- const html = template.replace(
229
- '{{placeholder}}',
230
- config.placeholder || 'Select...',
231
- );
232
- return stringToElement(html) as HTMLOptionElement;
233
- },
234
-
235
- /**
236
- * Renders an error option in the dropdown
237
- */
238
- errorOption: (config: KTSelectConfigInterface & { errorMessage: string }) => {
239
- const template = getTemplateStrings(config).errorOption;
240
- const html = template.replace(
241
- '{{errorMessage}}',
242
- config.errorMessage || 'An error occurred',
243
- );
185
+ .replace('{{options}}', config.options || '')
186
+ .replace('{{class}}', config.optionsClass || '');
244
187
  return stringToElement(html);
245
188
  },
246
189
 
@@ -261,68 +204,47 @@ export const defaultTemplates: KTSelectTemplateInterface = {
261
204
  config: KTSelectConfigInterface & { errorMessage: string },
262
205
  ): string => {
263
206
  const template = getTemplateStrings(config).error;
264
- return template.replace(
265
- '{{errorMessage}}',
266
- config.errorMessage || 'An error occurred',
267
- );
207
+ return template
208
+ .replace('{{errorMessage}}', config.errorMessage || 'An error occurred')
209
+ .replace('{{class}}', config.errorClass || '');
268
210
  },
269
211
  /**
270
212
  * Renders the main container for the select component
271
213
  */
272
- main: (config: KTSelectConfigInterface): HTMLElement => {
273
- const html = getTemplateStrings(config).main.replace(
274
- '{{mode}}',
275
- config.mode || '',
276
- );
277
- return stringToElement(html);
214
+ wrapper: (config: KTSelectConfigInterface): HTMLElement => {
215
+ const html = getTemplateStrings(config).wrapper
216
+ .replace('{{class}}', config.wrapperClass || '');
217
+ const element = stringToElement(html);
218
+ element.setAttribute('data-kt-select-combobox', config.combobox ? 'true' : 'false');
219
+ element.setAttribute('data-kt-select-tags', config.tags ? 'true' : 'false');
220
+ return element;
278
221
  },
279
222
 
280
223
  /**
281
224
  * Renders the display element (trigger) for the select
282
225
  */
283
226
  display: (config: KTSelectConfigInterface): HTMLElement => {
284
- const isCombobox = config.mode === SelectMode.COMBOBOX;
285
- if (isCombobox) {
227
+ if (config.combobox) {
286
228
  let html = getTemplateStrings(config)
287
- .displayCombobox.replace(
288
- /{{placeholder}}/g,
289
- config.placeholder || 'Select...',
290
- )
229
+ .combobox.replace(/{{placeholder}}/g, config.placeholder || 'Select...')
291
230
  .replace(
292
231
  /{{label}}/g,
293
232
  config.label || config.placeholder || 'Select...',
294
233
  )
295
- .replace('{{disabled}}', config.disabled ? 'disabled' : '');
234
+ .replace('{{disabled}}', config.disabled ? 'disabled' : '')
235
+ .replace('{{class}}', config.displayClass || '');
296
236
  return stringToElement(html);
297
237
  }
298
- let html = getTemplateStrings(config)
299
- .display.replace('{{tabindex}}', config.disabled ? '-1' : '0')
238
+
239
+ let content = config.label || config.placeholder || 'Select...';
240
+
241
+ let html = getTemplateStrings(config).display
242
+ .replace('{{tabindex}}', config.disabled ? '-1' : '0')
300
243
  .replace('{{label}}', config.label || config.placeholder || 'Select...')
301
244
  .replace('{{disabled}}', config.disabled ? 'aria-disabled="true"' : '')
302
- .replace('{{placeholder}}', config.placeholder || 'Select...');
303
- return stringToElement(html);
304
- },
305
-
306
- /**
307
- * Renders the dropdown content container
308
- */
309
- dropdown: (
310
- config: KTSelectConfigInterface,
311
- optionsHtml: string,
312
- ): HTMLElement => {
313
- const isCombobox = config.mode === SelectMode.COMBOBOX;
314
- const hasSearch = config.enableSearch && !isCombobox;
315
- const template = getTemplateStrings(config).dropdown;
316
- let searchHtml = '';
317
- if (hasSearch) {
318
- const searchElement = defaultTemplates.search(config);
319
- searchHtml = searchElement.outerHTML;
320
- }
321
- const html = template
322
- .replace('{{search}}', searchHtml)
323
- .replace('{{options}}', optionsHtml)
324
- .replace('{{label}}', config.label || 'Options')
325
- .replace('{{height}}', config.height ? String(config.height) : '250');
245
+ .replace('{{placeholder}}', config.placeholder || 'Select...')
246
+ .replace('{{class}}', config.displayClass || '')
247
+ .replace('{{content}}', content);
326
248
  return stringToElement(html);
327
249
  },
328
250
 
@@ -331,7 +253,7 @@ export const defaultTemplates: KTSelectTemplateInterface = {
331
253
  */
332
254
  option: (
333
255
  option: KTSelectOption | HTMLOptionElement,
334
- config: KTSelectConfigInterface & { templates: KTSelectTemplateInterface },
256
+ config: KTSelectConfigInterface,
335
257
  ): HTMLElement => {
336
258
  const isHtmlOption = option instanceof HTMLOptionElement;
337
259
 
@@ -344,92 +266,26 @@ export const defaultTemplates: KTSelectTemplateInterface = {
344
266
  ? option.selected
345
267
  : !!(option as KTSelectOption).selected;
346
268
 
347
- // Prefer data-kt-select-option (JSON) if present
348
- let description: string | undefined;
349
- let icon: string | undefined;
350
- if (isHtmlOption) {
351
- const json = option.getAttribute('data-kt-select-option');
352
- if (json) {
353
- try {
354
- const optionData = JSON.parse(json);
355
- description = optionData?.description;
356
- icon = optionData?.icon;
357
- } catch (e) {
358
- // fallback to legacy attributes if JSON is invalid
359
- description =
360
- option.getAttribute('data-kt-select-option-description') ||
361
- undefined;
362
- icon = option.getAttribute('data-kt-select-option-icon') || undefined;
363
- }
364
- } else {
365
- description =
366
- option.getAttribute('data-kt-select-option-description') || undefined;
367
- icon = option.getAttribute('data-kt-select-option-icon') || undefined;
368
- }
369
- } else {
370
- description = (option as KTSelectOption).description;
371
- icon = (option as KTSelectOption).icon;
269
+ let content = text;
270
+ if (config.optionTemplate) {
271
+ // Use the user template to render the content, but only for {{content}}
272
+ content = renderTemplateString(config.optionTemplate, {
273
+ value,
274
+ text,
275
+ class: config.optionClass || '',
276
+ selected: selected ? 'aria-selected="true"' : 'aria-selected="false"',
277
+ disabled: disabled ? 'aria-disabled="true"' : '',
278
+ content: text,
279
+ });
372
280
  }
373
281
 
374
- // Build option element with proper accessibility attributes
375
- const selectedClass = selected ? ' selected' : '';
376
- const disabledClass = disabled ? ' disabled' : '';
377
- let html = getTemplateStrings(config)
378
- .option.replace('{{value}}', value)
379
- .replace('{{selectedClass}}', selectedClass)
380
- .replace('{{disabledClass}}', disabledClass)
381
- .replace(
382
- '{{selected}}',
383
- selected ? 'aria-selected="true"' : 'aria-selected="false"',
384
- )
385
- .replace('{{disabled}}', disabled ? 'aria-disabled="true"' : '')
386
- .replace(
387
- /{{icon}}/g,
388
- icon ? defaultTemplates.icon(icon, config).outerHTML : '',
389
- )
282
+ const html = getTemplateStrings(config).option
283
+ .replace('{{value}}', value)
390
284
  .replace('{{text}}', text)
391
- .replace(
392
- /{{description}}/g,
393
- description
394
- ? defaultTemplates.description(description, config).outerHTML
395
- : '',
396
- );
397
- return stringToElement(html);
398
- },
399
-
400
- /**
401
- * Renders an icon
402
- */
403
- icon: (icon: string, config: KTSelectConfigInterface): HTMLElement => {
404
- const html = getTemplateStrings(config).icon.replace('{{icon}}', icon);
405
- return stringToElement(html);
406
- },
407
-
408
- /**
409
- * Renders a description
410
- */
411
- description: (
412
- description: string,
413
- config: KTSelectConfigInterface,
414
- ): HTMLElement => {
415
- const html = getTemplateStrings(config).description.replace(
416
- '{{description}}',
417
- description,
418
- );
419
- return stringToElement(html);
420
- },
421
-
422
- /**
423
- * Renders an option group with header
424
- */
425
- optionGroup: (
426
- label: string,
427
- optionsHtml: string,
428
- config: KTSelectConfigInterface,
429
- ): HTMLElement => {
430
- let html = getTemplateStrings(config)
431
- .optionGroup.replace(/{{label}}/g, label)
432
- .replace('{{optionsHtml}}', optionsHtml);
285
+ .replace('{{selected}}', selected ? 'aria-selected="true"' : 'aria-selected="false"')
286
+ .replace('{{disabled}}', disabled ? 'aria-disabled="true"' : '')
287
+ .replace('{{content}}', content)
288
+ .replace('{{class}}', config.optionClass || '');
433
289
  return stringToElement(html);
434
290
  },
435
291
 
@@ -437,21 +293,25 @@ export const defaultTemplates: KTSelectTemplateInterface = {
437
293
  * Renders the search input
438
294
  */
439
295
  search: (config: KTSelectConfigInterface): HTMLElement => {
440
- let html = getTemplateStrings(config).search.replace(
441
- '{{searchPlaceholder}}',
442
- config.searchPlaceholder || 'Search...',
443
- );
296
+ let html = getTemplateStrings(config)
297
+ .search.replace(
298
+ '{{searchPlaceholder}}',
299
+ config.searchPlaceholder || 'Search...',
300
+ )
301
+ .replace('{{class}}', config.searchClass || '');
444
302
  return stringToElement(html);
445
303
  },
446
304
 
447
305
  /**
448
306
  * Renders the no results message
449
307
  */
450
- noResults: (config: KTSelectConfigInterface): HTMLElement => {
451
- let html = getTemplateStrings(config).noResults.replace(
452
- '{{searchNotFoundText}}',
453
- config.searchNotFoundText || 'No results found',
454
- );
308
+ empty: (config: KTSelectConfigInterface): HTMLElement => {
309
+ let html = getTemplateStrings(config)
310
+ .empty.replace(
311
+ '{{searchNotFoundText}}',
312
+ config.searchNotFoundText || 'No results found',
313
+ )
314
+ .replace('{{class}}', config.emptyClass || '');
455
315
  return stringToElement(html);
456
316
  },
457
317
 
@@ -462,10 +322,12 @@ export const defaultTemplates: KTSelectTemplateInterface = {
462
322
  config: KTSelectConfigInterface,
463
323
  loadingMessage: string,
464
324
  ): HTMLElement => {
465
- let html = getTemplateStrings(config).loading.replace(
466
- '{{loadingMessage}}',
467
- loadingMessage || 'Loading options...',
468
- );
325
+ let html = getTemplateStrings(config)
326
+ .loading.replace(
327
+ '{{loadingMessage}}',
328
+ loadingMessage || 'Loading options...',
329
+ )
330
+ .replace('{{class}}', config.loadingClass || '');
469
331
  return stringToElement(html);
470
332
  },
471
333
 
@@ -473,59 +335,60 @@ export const defaultTemplates: KTSelectTemplateInterface = {
473
335
  * Renders a tag for multi-select
474
336
  */
475
337
  tag: (
476
- option: KTSelectOption,
338
+ option: HTMLOptionElement,
477
339
  config: KTSelectConfigInterface,
478
340
  ): HTMLElement => {
479
- // Escape HTML characters for aria-label to prevent HTML injection
480
- const escapeHTML = (str: string) => {
481
- return str.replace(/[&<>"']/g, (match) => {
482
- const escapeMap: Record<string, string> = {
483
- '&': '&amp;',
484
- '<': '&lt;',
485
- '>': '&gt;',
486
- '"': '&quot;',
487
- "'": '&#39;',
488
- };
489
- return escapeMap[match];
341
+ let template = getTemplateStrings(config).tag;
342
+ let content = option.title;
343
+ if (config.tagTemplate) {
344
+ let tagTemplate = config.tagTemplate;
345
+
346
+ const text = option.getAttribute('data-text');
347
+ const value = option.getAttribute('data-value');
348
+
349
+ // Replace all {{varname}} in option.innerHTML with values from _config
350
+ Object.entries((config.optionsConfig as any)[value] || {}).forEach(([key, value]) => {
351
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
352
+ tagTemplate = tagTemplate.replace(new RegExp(`{{${key}}}`, 'g'), String(value));
353
+ }
490
354
  });
491
- };
492
355
 
493
- // Ensure we have plain text for the aria-label
494
- const safeTitle = escapeHTML(option.title);
495
- let html = getTemplateStrings(config)
496
- .tag.replace('{{title}}', option.title)
356
+ content = renderTemplateString(tagTemplate, {
357
+ title: option.title,
358
+ id: option.id,
359
+ class: config.tagClass || '',
360
+ content: option.innerHTML,
361
+ text: option.innerText,
362
+ });
363
+ }
364
+
365
+ content += getTemplateStrings(config).tagRemoveButton;
366
+
367
+ const html = template
368
+ .replace('{{title}}', option.title)
497
369
  .replace('{{id}}', option.id)
498
- .replace('{{safeTitle}}', safeTitle);
370
+ .replace('{{content}}', content)
371
+ .replace('{{class}}', config.tagClass || '');
499
372
  return stringToElement(html);
500
373
  },
501
374
 
502
375
  /**
503
- * Formats the display of selected values
376
+ * Renders the placeholder for the select
504
377
  */
505
- selectedDisplay: (
506
- selectedOptions: KTSelectOption[],
507
- config: KTSelectConfigInterface,
508
- ): string => {
509
- if (!selectedOptions || selectedOptions.length === 0) {
510
- return config.placeholder || 'Select...';
511
- }
378
+ placeholder: (config: KTSelectConfigInterface): HTMLElement => {
379
+ let html = getTemplateStrings(config)
380
+ .placeholder.replace('{{class}}', config.placeholderClass || '');
381
+
382
+ let content = config.placeholder || 'Select...';
512
383
 
513
- if (config.multiple) {
514
- if (
515
- config.renderSelected &&
516
- typeof config.renderSelected === 'function'
517
- ) {
518
- return config.renderSelected(selectedOptions);
519
- }
520
-
521
- if (config.showSelectedCount) {
522
- const count = selectedOptions.length;
523
- return `${count} ${count === 1 ? 'item' : 'items'} selected`;
524
- }
525
-
526
- return selectedOptions.map((option) => option.title).join(', ');
527
- } else {
528
- return selectedOptions[0].title;
384
+ if (config.placeholderTemplate) {
385
+ content = renderTemplateString(config.placeholderTemplate, {
386
+ placeholder: config.placeholder || 'Select...',
387
+ class: config.placeholderClass || '',
388
+ });
529
389
  }
390
+
391
+ html = html.replace('{{content}}', content);
392
+ return stringToElement(html);
530
393
  },
531
394
  };