@zeyos/zx 2.1.0 → 2.2.0

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 (311) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +15 -0
  3. package/dist/types/compat/base.d.ts +156 -0
  4. package/dist/types/compat/globals.d.ts +20 -0
  5. package/dist/types/compat/index.d.ts +128 -0
  6. package/dist/types/compat/map/basic.d.ts +175 -0
  7. package/dist/types/compat/map/data.d.ts +102 -0
  8. package/dist/types/compat/map/date-time.d.ts +80 -0
  9. package/dist/types/compat/map/factory.d.ts +9 -0
  10. package/dist/types/compat/map/form.d.ts +98 -0
  11. package/dist/types/compat/map/helpers.d.ts +12 -0
  12. package/dist/types/compat/map/http.d.ts +36 -0
  13. package/dist/types/compat/map/layout.d.ts +106 -0
  14. package/dist/types/compat/map/options.d.ts +80 -0
  15. package/dist/types/compat/map/select.d.ts +95 -0
  16. package/dist/types/compat/map/stubs.d.ts +32 -0
  17. package/dist/types/compat/map/utilities.d.ts +37 -0
  18. package/dist/types/compat-entry.d.ts +5 -0
  19. package/dist/types/components/badge/badge.d.ts +46 -0
  20. package/dist/types/components/breadcrumb/breadcrumb.d.ts +200 -0
  21. package/dist/types/components/button/button.d.ts +42 -0
  22. package/dist/types/components/check-button/check-button.d.ts +98 -0
  23. package/dist/types/components/checklist/checklist.d.ts +143 -0
  24. package/dist/types/components/checklist/field-adapter.d.ts +5 -0
  25. package/dist/types/components/context-menu/context-menu.d.ts +130 -0
  26. package/dist/types/components/copy/copy-button.d.ts +48 -0
  27. package/dist/types/components/copy/copy-input.d.ts +91 -0
  28. package/dist/types/components/data-filter/data-filter.d.ts +250 -0
  29. package/dist/types/components/data-filter/filter-core.d.ts +52 -0
  30. package/dist/types/components/date-picker/date-picker.d.ts +137 -0
  31. package/dist/types/components/date-picker/date-range-picker.d.ts +350 -0
  32. package/dist/types/components/date-picker/field-adapter.d.ts +6 -0
  33. package/dist/types/components/date-picker/month-picker.d.ts +75 -0
  34. package/dist/types/components/date-picker/range-field-adapter.d.ts +7 -0
  35. package/dist/types/components/date-picker/time-picker.d.ts +127 -0
  36. package/dist/types/components/datebox/date-range-box.d.ts +218 -0
  37. package/dist/types/components/datebox/datebox.d.ts +160 -0
  38. package/dist/types/components/datebox/field-adapter.d.ts +5 -0
  39. package/dist/types/components/dialog/dialog.d.ts +284 -0
  40. package/dist/types/components/dropdown/dropdown.d.ts +118 -0
  41. package/dist/types/components/empty-state/empty-state.d.ts +38 -0
  42. package/dist/types/components/field/field.d.ts +231 -0
  43. package/dist/types/components/field-upload/field-adapter.d.ts +5 -0
  44. package/dist/types/components/field-upload/field-upload.d.ts +213 -0
  45. package/dist/types/components/fieldset/fieldset.d.ts +125 -0
  46. package/dist/types/components/finder/finder.d.ts +252 -0
  47. package/dist/types/components/form/form.d.ts +194 -0
  48. package/dist/types/components/groupbox/groupbox.d.ts +89 -0
  49. package/dist/types/components/layout/layout.d.ts +132 -0
  50. package/dist/types/components/loading/inline-loading.d.ts +86 -0
  51. package/dist/types/components/loading/progress-bar.d.ts +168 -0
  52. package/dist/types/components/loading/spinner.d.ts +34 -0
  53. package/dist/types/components/master-panel/master-panel.d.ts +112 -0
  54. package/dist/types/components/menu-button/menu-button.d.ts +142 -0
  55. package/dist/types/components/message/message.d.ts +212 -0
  56. package/dist/types/components/modal/modal.d.ts +105 -0
  57. package/dist/types/components/multi-value-editor/field-adapter.d.ts +5 -0
  58. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +66 -0
  59. package/dist/types/components/navigation-bar/navigation-bar.d.ts +229 -0
  60. package/dist/types/components/number-field/field-adapter.d.ts +5 -0
  61. package/dist/types/components/number-field/number-field.d.ts +292 -0
  62. package/dist/types/components/pagination/pagination.d.ts +267 -0
  63. package/dist/types/components/panel/panel.d.ts +194 -0
  64. package/dist/types/components/permission/permission.d.ts +93 -0
  65. package/dist/types/components/rating/field-adapter.d.ts +5 -0
  66. package/dist/types/components/rating/rating.d.ts +213 -0
  67. package/dist/types/components/search/search.d.ts +98 -0
  68. package/dist/types/components/select/field-adapter.d.ts +5 -0
  69. package/dist/types/components/select/filter.d.ts +11 -0
  70. package/dist/types/components/select/select.d.ts +235 -0
  71. package/dist/types/components/skeleton/skeleton.d.ts +97 -0
  72. package/dist/types/components/slider/field-adapter.d.ts +5 -0
  73. package/dist/types/components/slider/slider.d.ts +216 -0
  74. package/dist/types/components/split-view/split-view.d.ts +335 -0
  75. package/dist/types/components/stepper/stepper.d.ts +314 -0
  76. package/dist/types/components/stepper/steps.d.ts +131 -0
  77. package/dist/types/components/tabbox/tabbox.d.ts +230 -0
  78. package/dist/types/components/table/sort.d.ts +49 -0
  79. package/dist/types/components/table/table.d.ts +872 -0
  80. package/dist/types/components/tag-picker/field-adapter.d.ts +5 -0
  81. package/dist/types/components/tag-picker/tag-picker.d.ts +375 -0
  82. package/dist/types/components/timebox/field-adapter.d.ts +5 -0
  83. package/dist/types/components/timebox/timebox.d.ts +121 -0
  84. package/dist/types/components/toggle/field-adapter.d.ts +5 -0
  85. package/dist/types/components/toggle/toggle.d.ts +94 -0
  86. package/dist/types/components/toolbar/toolbar.d.ts +230 -0
  87. package/dist/types/components/tooltip/tooltip.d.ts +183 -0
  88. package/dist/types/components/tree/hierarchy.d.ts +113 -0
  89. package/dist/types/components/tree/tree.d.ts +383 -0
  90. package/dist/types/components/truncate/truncate.d.ts +61 -0
  91. package/dist/types/components/value-list/field-adapter.d.ts +5 -0
  92. package/dist/types/components/value-list/value-list.d.ts +143 -0
  93. package/dist/types/core/breakpoint.d.ts +68 -0
  94. package/dist/types/core/component.d.ts +115 -0
  95. package/dist/types/core/date.d.ts +50 -0
  96. package/dist/types/core/dom.d.ts +86 -0
  97. package/dist/types/core/export.d.ts +70 -0
  98. package/dist/types/core/fontawesome.d.ts +107 -0
  99. package/dist/types/core/format.d.ts +107 -0
  100. package/dist/types/core/http.d.ts +122 -0
  101. package/dist/types/core/i18n.d.ts +35 -0
  102. package/dist/types/core/icons.d.ts +137 -0
  103. package/dist/types/core/keyboard.d.ts +72 -0
  104. package/dist/types/core/position.d.ts +45 -0
  105. package/dist/types/core/storage.d.ts +46 -0
  106. package/dist/types/core/util.d.ts +117 -0
  107. package/dist/types/elements/define.d.ts +73 -0
  108. package/dist/types/elements/reflect.d.ts +27 -0
  109. package/dist/types/index.d.ts +79 -0
  110. package/dist/types/zeyos/connect.d.ts +65 -0
  111. package/dist/types/zeyos/form.d.ts +140 -0
  112. package/dist/types/zeyos/icons.d.ts +104 -0
  113. package/dist/types/zeyos/index.d.ts +12 -0
  114. package/dist/types/zeyos/modules.d.ts +110 -0
  115. package/dist/types/zeyos/query.d.ts +157 -0
  116. package/dist/types/zeyos/schema.d.ts +79 -0
  117. package/dist/types/zeyos/select.d.ts +167 -0
  118. package/dist/types/zeyos/table.d.ts +205 -0
  119. package/dist/zx-compat.esm.js +362 -33
  120. package/dist/zx-compat.esm.min.js +1 -1
  121. package/dist/zx-compat.global.js +359 -67
  122. package/dist/zx-compat.global.min.js +3 -3
  123. package/dist/zx-zeyos.esm.js +342 -43
  124. package/dist/zx-zeyos.esm.min.js +1 -1
  125. package/dist/zx.css +68 -6
  126. package/dist/zx.esm.js +346 -67
  127. package/dist/zx.esm.min.js +3 -3
  128. package/dist/zx.global.js +346 -67
  129. package/dist/zx.global.min.js +3 -3
  130. package/dist/zx.min.css +1 -1
  131. package/docs/api.json +48 -2
  132. package/docs/llms.md +28 -0
  133. package/package.json +37 -9
  134. package/src/compat/base.js +291 -0
  135. package/src/compat/globals.js +165 -0
  136. package/src/compat/index.js +84 -0
  137. package/src/compat/map/basic.js +464 -0
  138. package/src/compat/map/data.js +232 -0
  139. package/src/compat/map/date-time.js +177 -0
  140. package/src/compat/map/factory.js +45 -0
  141. package/src/compat/map/form.js +330 -0
  142. package/src/compat/map/helpers.js +46 -0
  143. package/src/compat/map/http.js +117 -0
  144. package/src/compat/map/layout.js +256 -0
  145. package/src/compat/map/options.js +267 -0
  146. package/src/compat/map/select.js +198 -0
  147. package/src/compat/map/stubs.js +26 -0
  148. package/src/compat/map/utilities.js +113 -0
  149. package/src/compat-entry.js +11 -0
  150. package/src/components/badge/badge.css +147 -0
  151. package/src/components/badge/badge.js +92 -0
  152. package/src/components/breadcrumb/breadcrumb.css +105 -0
  153. package/src/components/breadcrumb/breadcrumb.js +331 -0
  154. package/src/components/button/button.css +104 -0
  155. package/src/components/button/button.js +76 -0
  156. package/src/components/check-button/check-button.css +66 -0
  157. package/src/components/check-button/check-button.js +167 -0
  158. package/src/components/checklist/checklist.css +65 -0
  159. package/src/components/checklist/checklist.js +282 -0
  160. package/src/components/checklist/field-adapter.js +46 -0
  161. package/src/components/context-menu/context-menu.css +54 -0
  162. package/src/components/context-menu/context-menu.js +334 -0
  163. package/src/components/copy/copy-button.css +27 -0
  164. package/src/components/copy/copy-button.js +78 -0
  165. package/src/components/copy/copy-input.css +59 -0
  166. package/src/components/copy/copy-input.js +123 -0
  167. package/src/components/data-filter/data-filter.css +53 -0
  168. package/src/components/data-filter/data-filter.js +400 -0
  169. package/src/components/data-filter/filter-core.js +128 -0
  170. package/src/components/date-picker/date-picker.css +181 -0
  171. package/src/components/date-picker/date-picker.js +500 -0
  172. package/src/components/date-picker/date-range-picker.css +264 -0
  173. package/src/components/date-picker/date-range-picker.js +990 -0
  174. package/src/components/date-picker/field-adapter.js +36 -0
  175. package/src/components/date-picker/month-picker.js +223 -0
  176. package/src/components/date-picker/range-field-adapter.js +31 -0
  177. package/src/components/date-picker/time-picker.css +191 -0
  178. package/src/components/date-picker/time-picker.js +406 -0
  179. package/src/components/datebox/date-range-box.css +128 -0
  180. package/src/components/datebox/date-range-box.js +492 -0
  181. package/src/components/datebox/datebox.css +108 -0
  182. package/src/components/datebox/datebox.js +358 -0
  183. package/src/components/datebox/field-adapter.js +34 -0
  184. package/src/components/dialog/dialog.css +74 -0
  185. package/src/components/dialog/dialog.js +450 -0
  186. package/src/components/dropdown/dropdown.css +24 -0
  187. package/src/components/dropdown/dropdown.js +288 -0
  188. package/src/components/empty-state/empty-state.css +80 -0
  189. package/src/components/empty-state/empty-state.js +92 -0
  190. package/src/components/field/field.css +127 -0
  191. package/src/components/field/field.js +605 -0
  192. package/src/components/field-upload/field-adapter.js +21 -0
  193. package/src/components/field-upload/field-upload.css +108 -0
  194. package/src/components/field-upload/field-upload.js +475 -0
  195. package/src/components/fieldset/fieldset.css +37 -0
  196. package/src/components/fieldset/fieldset.js +307 -0
  197. package/src/components/finder/finder.css +98 -0
  198. package/src/components/finder/finder.js +509 -0
  199. package/src/components/form/form.css +32 -0
  200. package/src/components/form/form.js +302 -0
  201. package/src/components/groupbox/groupbox.css +51 -0
  202. package/src/components/groupbox/groupbox.js +199 -0
  203. package/src/components/layout/layout.css +117 -0
  204. package/src/components/layout/layout.js +136 -0
  205. package/src/components/loading/inline-loading.css +34 -0
  206. package/src/components/loading/inline-loading.js +140 -0
  207. package/src/components/loading/progress-bar.css +115 -0
  208. package/src/components/loading/progress-bar.js +259 -0
  209. package/src/components/loading/spinner.css +58 -0
  210. package/src/components/loading/spinner.js +57 -0
  211. package/src/components/master-panel/master-panel.css +51 -0
  212. package/src/components/master-panel/master-panel.js +203 -0
  213. package/src/components/menu-button/menu-button.css +49 -0
  214. package/src/components/menu-button/menu-button.js +294 -0
  215. package/src/components/message/message.css +122 -0
  216. package/src/components/message/message.js +499 -0
  217. package/src/components/modal/modal.css +47 -0
  218. package/src/components/modal/modal.js +203 -0
  219. package/src/components/multi-value-editor/field-adapter.js +44 -0
  220. package/src/components/multi-value-editor/multi-value-editor.css +56 -0
  221. package/src/components/multi-value-editor/multi-value-editor.js +170 -0
  222. package/src/components/navigation-bar/navigation-bar.css +130 -0
  223. package/src/components/navigation-bar/navigation-bar.js +450 -0
  224. package/src/components/number-field/field-adapter.js +40 -0
  225. package/src/components/number-field/number-field.css +77 -0
  226. package/src/components/number-field/number-field.js +545 -0
  227. package/src/components/pagination/pagination.css +95 -0
  228. package/src/components/pagination/pagination.js +481 -0
  229. package/src/components/panel/panel.css +123 -0
  230. package/src/components/panel/panel.js +336 -0
  231. package/src/components/permission/permission.css +47 -0
  232. package/src/components/permission/permission.js +198 -0
  233. package/src/components/rating/field-adapter.js +34 -0
  234. package/src/components/rating/rating.css +97 -0
  235. package/src/components/rating/rating.js +395 -0
  236. package/src/components/search/search.css +70 -0
  237. package/src/components/search/search.js +193 -0
  238. package/src/components/select/field-adapter.js +24 -0
  239. package/src/components/select/filter.js +39 -0
  240. package/src/components/select/select.css +180 -0
  241. package/src/components/select/select.js +663 -0
  242. package/src/components/skeleton/skeleton.css +76 -0
  243. package/src/components/skeleton/skeleton.js +139 -0
  244. package/src/components/slider/field-adapter.js +36 -0
  245. package/src/components/slider/slider.css +181 -0
  246. package/src/components/slider/slider.js +399 -0
  247. package/src/components/split-view/split-view.css +139 -0
  248. package/src/components/split-view/split-view.js +806 -0
  249. package/src/components/stepper/stepper.css +234 -0
  250. package/src/components/stepper/stepper.js +459 -0
  251. package/src/components/stepper/steps.js +131 -0
  252. package/src/components/tabbox/tabbox.css +281 -0
  253. package/src/components/tabbox/tabbox.js +484 -0
  254. package/src/components/table/sort.js +85 -0
  255. package/src/components/table/table.css +364 -0
  256. package/src/components/table/table.js +1693 -0
  257. package/src/components/tag-picker/field-adapter.js +35 -0
  258. package/src/components/tag-picker/tag-picker.css +169 -0
  259. package/src/components/tag-picker/tag-picker.js +837 -0
  260. package/src/components/timebox/field-adapter.js +24 -0
  261. package/src/components/timebox/timebox.css +51 -0
  262. package/src/components/timebox/timebox.js +276 -0
  263. package/src/components/toggle/field-adapter.js +24 -0
  264. package/src/components/toggle/toggle.css +66 -0
  265. package/src/components/toggle/toggle.js +150 -0
  266. package/src/components/toolbar/toolbar.css +81 -0
  267. package/src/components/toolbar/toolbar.js +511 -0
  268. package/src/components/tooltip/tooltip.css +36 -0
  269. package/src/components/tooltip/tooltip.js +420 -0
  270. package/src/components/tree/hierarchy.js +151 -0
  271. package/src/components/tree/tree.css +99 -0
  272. package/src/components/tree/tree.js +784 -0
  273. package/src/components/truncate/truncate.css +20 -0
  274. package/src/components/truncate/truncate.js +92 -0
  275. package/src/components/value-list/field-adapter.js +24 -0
  276. package/src/components/value-list/value-list.css +108 -0
  277. package/src/components/value-list/value-list.js +336 -0
  278. package/src/core/breakpoint.js +150 -0
  279. package/src/core/component.js +292 -0
  280. package/src/core/date.js +216 -0
  281. package/src/core/dom.js +260 -0
  282. package/src/core/export.js +204 -0
  283. package/src/core/fontawesome.js +177 -0
  284. package/src/core/format.js +283 -0
  285. package/src/core/http.js +246 -0
  286. package/src/core/i18n.js +83 -0
  287. package/src/core/icons.js +283 -0
  288. package/src/core/keyboard.js +240 -0
  289. package/src/core/position.js +177 -0
  290. package/src/core/storage.js +175 -0
  291. package/src/core/util.js +336 -0
  292. package/src/elements/define.js +715 -0
  293. package/src/elements/reflect.js +79 -0
  294. package/src/index.js +131 -0
  295. package/src/zeyos/connect.js +142 -0
  296. package/src/zeyos/form.js +254 -0
  297. package/src/zeyos/icons.js +92 -0
  298. package/src/zeyos/index.js +21 -0
  299. package/src/zeyos/modules.js +269 -0
  300. package/src/zeyos/query.js +199 -0
  301. package/src/zeyos/schema.js +474 -0
  302. package/src/zeyos/select.js +175 -0
  303. package/src/zeyos/table.js +272 -0
  304. package/styles/base.css +124 -0
  305. package/styles/icons.css +34 -0
  306. package/styles/tokens/dark.css +84 -0
  307. package/styles/tokens/density.css +9 -0
  308. package/styles/tokens/global.css +80 -0
  309. package/styles/tokens/modules.css +109 -0
  310. package/styles/tokens/semantic.css +49 -0
  311. package/styles/zx.css +61 -0
@@ -0,0 +1,663 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h } from '../../core/dom.js';
3
+ import { icon } from '../../core/icons.js';
4
+ import { typeahead } from '../../core/keyboard.js';
5
+ import { position } from '../../core/position.js';
6
+ import { uid } from '../../core/util.js';
7
+ import { matchItems } from './filter.js';
8
+
9
+ /** @typedef {string|number|boolean|symbol|bigint|null|undefined} Primitive */
10
+ /** @typedef {Record<string, any>|Primitive} SelectItem */
11
+ /** @typedef {string|((item: SelectItem) => unknown)} SelectValueReader */
12
+ /** @typedef {string|((item: SelectItem) => string)} SelectLabelReader */
13
+ /**
14
+ * @typedef {Object} SelectOptions
15
+ * @property {SelectItem[]} [items=[]] Available values.
16
+ * @property {SelectValueReader} [valueKey='ID'] Item ID property or reader.
17
+ * @property {SelectLabelReader} [labelKey='name'] Item label property or reader.
18
+ * @property {((item: SelectItem) => Node|string)|null} [renderItem=null] Option renderer.
19
+ * @property {((item: SelectItem) => string)|null} [renderValue=null] Selected-value renderer.
20
+ * @property {unknown} [value=null] Initially selected ID or item.
21
+ * @property {boolean} [disabled=false] Whether interaction is disabled.
22
+ * @property {string} [placeholder=''] Empty control text.
23
+ * @property {boolean} [clearable=false] Whether selection may be cleared.
24
+ * @property {false|'local'|((query: string) => Promise<SelectItem[]>|SelectItem[])} [filter=false] Filtering mode.
25
+ * @property {Array<SelectValueReader>|null} [searchKeys=null] Fields searched by local filtering.
26
+ * @property {number} [minQuery=0] Minimum async query length.
27
+ * @property {number} [debounce=200] Async query delay in milliseconds.
28
+ * @property {number} [listHeight=280] Maximum list height in pixels.
29
+ * @property {SelectValueReader|null} [groupKey=null] Optional group reader.
30
+ * @property {Record<string, string>|Record<string, Record<string, string>>} [msg] Localized messages.
31
+ * @property {(event: CustomEvent<SelectChangeDetail>) => void} [onchange] Change listener.
32
+ * @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open listener.
33
+ * @property {(event: CustomEvent<Record<string, never>>) => void} [onclose] Close listener.
34
+ * @property {(event: CustomEvent<SelectQueryDetail>) => void} [onquery] Async-query listener.
35
+ * @property {(event: CustomEvent<SelectLoadedDetail>) => void} [onloaded] Async-loaded listener.
36
+ */
37
+ /** @typedef {{value: unknown, item: SelectItem|null}} SelectChangeDetail */
38
+ /** @typedef {{query: string}} SelectQueryDetail */
39
+ /** @typedef {{items: SelectItem[]}} SelectLoadedDetail */
40
+ /** @typedef {{item: SelectItem|null, clear: boolean, element: HTMLElement}} SelectOptionEntry */
41
+ /** @typedef {{silent?: boolean}} SelectSetOptions */
42
+
43
+ const PRIORITY_KEYS = [
44
+ 'priority.lowest',
45
+ 'priority.low',
46
+ 'priority.normal',
47
+ 'priority.high',
48
+ 'priority.highest'
49
+ ];
50
+ const PRIORITY_FALLBACKS = ['Lowest', 'Low', 'Normal', 'High', 'Highest'];
51
+
52
+ /**
53
+ * APG editable combobox with optional local or asynchronous filtering.
54
+ *
55
+ * @fires Select#change
56
+ * @fires Select#open
57
+ * @fires Select#close
58
+ * @fires Select#query
59
+ * @fires Select#loaded
60
+ * @extends {Component<SelectOptions>}
61
+ */
62
+ export class Select extends Component {
63
+ static cssName = 'select';
64
+
65
+ /** @type {SelectOptions} */
66
+ static defaults = {
67
+ items: [],
68
+ valueKey: 'ID',
69
+ labelKey: 'name',
70
+ renderItem: null,
71
+ renderValue: null,
72
+ value: null,
73
+ disabled: false,
74
+ placeholder: '',
75
+ clearable: false,
76
+ filter: false,
77
+ searchKeys: null,
78
+ minQuery: 0,
79
+ debounce: 200,
80
+ listHeight: 280,
81
+ groupKey: null
82
+ };
83
+
84
+
85
+ /** @returns {HTMLElement} */
86
+ render() {
87
+ // Instance state initialized here because render() runs inside the base
88
+ // constructor, before class-field initializers would run (and clobber it).
89
+ this._items = []; this._visibleItems = []; this._selected = null; this._optionEntries = []; this._activeIndex = -1; this._open = false; this._disabled = false; this._loading = false; this._requestSequence = 0; this._queryTimer = null; this._position = null; this._createdRoot = false; this._root = null; this._originalChildren = []; this._originalAttributes = new Map();
90
+ this._createdRoot = this.el === null;
91
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
92
+ this.el = root;
93
+ this._root = root;
94
+ if (!this._createdRoot) this._originalChildren = Array.from(root.childNodes);
95
+ this._rememberAttribute(root, 'data-state');
96
+ this._rememberAttribute(root, 'data-loading');
97
+ this._rememberAttribute(root, 'aria-disabled');
98
+
99
+ const listId = uid('zx-select-list');
100
+ const input = h('input', {
101
+ ref: 'input',
102
+ class: 'zx-select__input',
103
+ type: 'text',
104
+ role: 'combobox',
105
+ autocomplete: 'off',
106
+ spellcheck: false,
107
+ ariaExpanded: 'false',
108
+ ariaControls: listId,
109
+ ariaAutocomplete: this.options.filter ? 'list' : 'none',
110
+ placeholder: String(this.options.placeholder ?? ''),
111
+ readOnly: !this.options.filter
112
+ });
113
+ const clear = h('button', {
114
+ ref: 'clear',
115
+ class: 'zx-icon-btn zx-select__clear',
116
+ type: 'button',
117
+ tabIndex: -1,
118
+ ariaLabel: this._message('select.clear', 'Clear selection'),
119
+ hidden: true
120
+ }, icon('x', { size: 13 }));
121
+ const loading = h('span', {
122
+ ref: 'loading',
123
+ class: 'zx-select__loading',
124
+ role: 'status',
125
+ ariaLabel: this._message('select.loading', 'Loading'),
126
+ hidden: true
127
+ });
128
+ const toggle = h('button', {
129
+ ref: 'toggle',
130
+ class: 'zx-select__toggle',
131
+ type: 'button',
132
+ tabIndex: -1,
133
+ ariaLabel: this._message('select.toggle', 'Toggle options')
134
+ }, icon('chevron-down', { size: 16 }));
135
+ const control = h('div', { ref: 'control', class: 'zx-select__control' },
136
+ input, clear, loading, toggle
137
+ );
138
+ const list = h('div', {
139
+ ref: 'list',
140
+ class: 'zx-select__list',
141
+ id: listId,
142
+ role: 'listbox'
143
+ });
144
+ list.style.maxBlockSize = `${Math.max(0, Number(this.options.listHeight) || 0)}px`;
145
+ const panel = h('div', {
146
+ ref: 'panel',
147
+ class: 'zx-select__panel',
148
+ popover: 'manual'
149
+ }, list);
150
+ root.replaceChildren(control, panel);
151
+
152
+ this._items = Array.isArray(this.options.items) ? this.options.items.slice() : [];
153
+ this._visibleItems = this._items.slice();
154
+ this._disabled = Boolean(this.options.disabled);
155
+ this._handleTypeahead = typeahead(
156
+ () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
157
+ (_option, index) => {
158
+ if (!this._open) this.open();
159
+ this._setActive(this._itemEntryIndex(index));
160
+ }
161
+ );
162
+
163
+ this.listen(input, 'click', () => this.open());
164
+ this.listen(input, 'input', () => this._onInput());
165
+ this.listen(input, 'keydown', (event) => this._onKeydown(/** @type {KeyboardEvent} */ (event)));
166
+ this.listen(toggle, 'click', () => {
167
+ this.focus();
168
+ if (this._open) this.close();
169
+ else this.open();
170
+ });
171
+ this.listen(clear, 'click', () => {
172
+ this.set(null);
173
+ this.focus();
174
+ });
175
+ this.listen(panel, 'pointerdown', (event) => event.preventDefault());
176
+ this.listen(panel, 'click', (event) => this._onOptionClick(event));
177
+ this.listen(document, 'pointerdown', (event) => {
178
+ if (this._open && !root.contains(/** @type {Node} */ (event.target))) this.close();
179
+ }, { capture: true });
180
+
181
+ this._renderOptions();
182
+ this.set(this.options.value, { silent: true });
183
+ this._syncDisabled();
184
+ return root;
185
+ }
186
+
187
+ /** Selected item ID, or null. */
188
+ get value() {
189
+ return this._selected === null ? null : this._itemValue(this._selected);
190
+ }
191
+
192
+ /** @param {unknown} id Selected item ID. */
193
+ set value(id) {
194
+ this.set(id);
195
+ }
196
+
197
+ /** Selected item, or null. @returns {SelectItem|null} */
198
+ get selected() {
199
+ return this._selected;
200
+ }
201
+
202
+ /**
203
+ * Selects an item or item ID.
204
+ * @param {unknown} idOrItem Item ID, item, or null.
205
+ * @param {SelectSetOptions} [options={}] Update behavior.
206
+ * @returns {this}
207
+ * @fires Select#change
208
+ */
209
+ set(idOrItem, { silent = false } = {}) {
210
+ const previousValue = this.value;
211
+ const item = idOrItem == null ? null : this._findItem(idOrItem);
212
+ this._selected = item;
213
+ this._syncInputValue();
214
+ this._renderOptions();
215
+ const nextValue = this.value;
216
+ if (!silent && !Object.is(previousValue, nextValue)) {
217
+ this.emit('change', { value: nextValue, item });
218
+ }
219
+ return this;
220
+ }
221
+
222
+ /**
223
+ * Replaces the available items and preserves selection by ID when possible.
224
+ * @param {SelectItem[]} items New items.
225
+ * @returns {this}
226
+ */
227
+ setItems(items) {
228
+ const selectedValue = this.value;
229
+ this._items = Array.isArray(items) ? items.slice() : [];
230
+ this._visibleItems = this._items.slice();
231
+ this._selected = selectedValue == null ? null :
232
+ this._items.find((item) => Object.is(this._itemValue(item), selectedValue)) ?? null;
233
+ this._syncInputValue();
234
+ this._renderOptions();
235
+ this._position?.update();
236
+ return this;
237
+ }
238
+
239
+ /**
240
+ * Restores the configured initial value.
241
+ * @param {{silent?: boolean}} [options={}] Reset behavior.
242
+ * @returns {this}
243
+ */
244
+ reset({ silent = false } = {}) {
245
+ this.set(this.options.value, { silent });
246
+ return this;
247
+ }
248
+
249
+ /** Opens the option panel. @returns {this} @fires Select#open */
250
+ open() {
251
+ if (this._disabled || this._open) return this;
252
+ this._open = true;
253
+ this.el.setAttribute('data-state', 'open');
254
+ this.refs.input.setAttribute('aria-expanded', 'true');
255
+ this._renderOptions();
256
+ this._position = position(this.refs.control, /** @type {HTMLElement} */ (this.refs.panel), {
257
+ placement: 'bottom-start',
258
+ flip: true,
259
+ matchWidth: true
260
+ });
261
+ // Set the active option only after position() shows the popover — scrollIntoView
262
+ // is a no-op while the panel is still hidden.
263
+ this._setActive(this._selectedEntryIndex());
264
+ if (this._activeIndex < 0 && this._optionEntries.length > 0) this._setActive(0);
265
+ if (this.options.filter && this._selected !== null) {
266
+ /** @type {HTMLInputElement} */ (this.refs.input).select();
267
+ }
268
+ if (typeof this.options.filter === 'function') this._scheduleAsyncQuery('');
269
+ this.emit('open', {});
270
+ return this;
271
+ }
272
+
273
+ /** Closes the option panel and restores the selected label. @returns {this} @fires Select#close */
274
+ close() {
275
+ if (!this._open) return this;
276
+ this._open = false;
277
+ this._cancelQuery();
278
+ this._setLoading(false);
279
+ this._position?.destroy();
280
+ this._position = null;
281
+ this.el.setAttribute('data-state', 'closed');
282
+ this.refs.input.setAttribute('aria-expanded', 'false');
283
+ this.refs.input.removeAttribute('aria-activedescendant');
284
+ this._activeIndex = -1;
285
+ this._visibleItems = this._items.slice();
286
+ this._syncInputValue();
287
+ this.emit('close', {});
288
+ return this;
289
+ }
290
+
291
+ /** Enables user interaction. @returns {this} */
292
+ enable() {
293
+ this._disabled = false;
294
+ this._syncDisabled();
295
+ return this;
296
+ }
297
+
298
+ /** Disables user interaction and closes the panel. @returns {this} */
299
+ disable() {
300
+ this.close();
301
+ this._disabled = true;
302
+ this._syncDisabled();
303
+ return this;
304
+ }
305
+
306
+ /** Moves focus to the combobox input. @returns {this} */
307
+ focus() {
308
+ /** @type {HTMLInputElement} */ (this.refs.input).focus();
309
+ return this;
310
+ }
311
+
312
+ /**
313
+ * Creates the five-level priority preset.
314
+ * @param {Element|string|null} target Component target.
315
+ * @param {SelectOptions} [options={}] Select overrides other than fixed items and renderer.
316
+ * @returns {Select}
317
+ */
318
+ static priority(target, options = {}) {
319
+ const requestedValue = options.value ?? null;
320
+ const select = new Select(target, {
321
+ ...options,
322
+ value: null,
323
+ items: [],
324
+ renderItem(item) {
325
+ return h('span', { class: 'zx-select__priority' },
326
+ h('span', {
327
+ class: 'zx-select__priority-icon',
328
+ dataset: { level: item.level },
329
+ ariaHidden: 'true'
330
+ }),
331
+ h('span', {}, String(item.name))
332
+ );
333
+ }
334
+ });
335
+ select.setItems(PRIORITY_KEYS.map((key, level) => ({
336
+ ID: level,
337
+ name: select._message(key, PRIORITY_FALLBACKS[level]),
338
+ level
339
+ })));
340
+ select.set(requestedValue, { silent: true });
341
+ return select;
342
+ }
343
+
344
+ /** Cleans up the panel, pending async work, and enhanced target contents. @returns {void} */
345
+ destroy() {
346
+ this._cancelQuery();
347
+ this._position?.destroy();
348
+ this._position = null;
349
+ const root = /** @type {HTMLElement} */ (this.el);
350
+ if (!this._createdRoot && root) {
351
+ root.replaceChildren(...this._originalChildren);
352
+ for (const [name, value] of this._originalAttributes) {
353
+ if (value === null) root.removeAttribute(name);
354
+ else root.setAttribute(name, value);
355
+ }
356
+ }
357
+ super.destroy();
358
+ }
359
+
360
+ /** @param {KeyboardEvent} event @returns {void} */
361
+ _onKeydown(event) {
362
+ if (this._disabled) return;
363
+ if (event.key === 'ArrowDown') {
364
+ event.preventDefault();
365
+ if (!this._open) this.open();
366
+ else this._moveActive(1);
367
+ return;
368
+ }
369
+ if (event.key === 'ArrowUp') {
370
+ event.preventDefault();
371
+ if (!this._open) {
372
+ this.open();
373
+ this._setActive(this._optionEntries.length - 1);
374
+ } else this._moveActive(-1);
375
+ return;
376
+ }
377
+ if (event.key === 'Home' && this._open) {
378
+ event.preventDefault();
379
+ this._setActive(0);
380
+ return;
381
+ }
382
+ if (event.key === 'End' && this._open) {
383
+ event.preventDefault();
384
+ this._setActive(this._optionEntries.length - 1);
385
+ return;
386
+ }
387
+ if (event.key === 'Enter' && this._open) {
388
+ event.preventDefault();
389
+ this._chooseEntry(this._optionEntries[this._activeIndex]);
390
+ return;
391
+ }
392
+ if (event.key === 'Escape' && this._open) {
393
+ event.preventDefault();
394
+ this.close();
395
+ return;
396
+ }
397
+ if (event.key === 'Tab' && this._open) {
398
+ this.close();
399
+ return;
400
+ }
401
+ if (!this.options.filter && isPrintable(event)) this._handleTypeahead(event);
402
+ }
403
+
404
+ /** @returns {void} */
405
+ _onInput() {
406
+ if (!this.options.filter) return;
407
+ const query = /** @type {HTMLInputElement} */ (this.refs.input).value;
408
+ if (!this._open) this.open();
409
+ if (this.options.filter === 'local') {
410
+ const keys = this.options.searchKeys ?? [this.options.labelKey];
411
+ this._visibleItems = matchItems(this._items, query, keys);
412
+ this._renderOptions();
413
+ this._setActive(this._optionEntries.length > 0 ? 0 : -1);
414
+ this._position?.update();
415
+ } else if (typeof this.options.filter === 'function') {
416
+ this._scheduleAsyncQuery(query);
417
+ }
418
+ }
419
+
420
+ /** @param {Event} event @returns {void} */
421
+ _onOptionClick(event) {
422
+ const option = /** @type {Element|null} */ (event.target)?.closest?.('[role="option"]');
423
+ if (!option || !this.refs.panel.contains(option)) return;
424
+ this._chooseEntry(this._optionEntries[Number(option.getAttribute('data-index'))]);
425
+ }
426
+
427
+ /** @param {SelectOptionEntry|undefined} entry @returns {void} */
428
+ _chooseEntry(entry) {
429
+ if (!entry) return;
430
+ this.set(entry.clear ? null : entry.item);
431
+ this.close();
432
+ this.focus();
433
+ }
434
+
435
+ /** @param {number} delta @returns {void} */
436
+ _moveActive(delta) {
437
+ const length = this._optionEntries.length;
438
+ if (length === 0) return;
439
+ this._setActive((this._activeIndex + delta + length) % length);
440
+ }
441
+
442
+ /** @param {number} index @returns {void} */
443
+ _setActive(index) {
444
+ this._activeIndex = index >= 0 && index < this._optionEntries.length ? index : -1;
445
+ this._optionEntries.forEach((entry, entryIndex) => {
446
+ if (entryIndex === this._activeIndex) entry.element.setAttribute('data-active', 'true');
447
+ else entry.element.removeAttribute('data-active');
448
+ });
449
+ const active = this._optionEntries[this._activeIndex]?.element;
450
+ if (active) {
451
+ this.refs.input.setAttribute('aria-activedescendant', active.id);
452
+ active.scrollIntoView?.({ block: 'nearest' });
453
+ } else {
454
+ this.refs.input.removeAttribute('aria-activedescendant');
455
+ }
456
+ }
457
+
458
+ /** @returns {void} */
459
+ _renderOptions() {
460
+ const list = this.refs.list;
461
+ if (!list) return;
462
+ list.replaceChildren();
463
+ this._optionEntries = [];
464
+ let lastGroup = Symbol('initial-group');
465
+
466
+ if (this.options.clearable) {
467
+ this._appendOption(null, true, this.options.placeholder || this._message('select.none', 'No selection'));
468
+ }
469
+ for (const item of this._visibleItems) {
470
+ if (this.options.groupKey) {
471
+ const group = this._read(item, this.options.groupKey);
472
+ if (!Object.is(group, lastGroup)) {
473
+ list.append(h('div', {
474
+ class: 'zx-select__group',
475
+ role: 'presentation'
476
+ }, String(group ?? '')));
477
+ lastGroup = group;
478
+ }
479
+ }
480
+ this._appendOption(item, false);
481
+ }
482
+ if (this._visibleItems.length === 0 && !this._loading) {
483
+ list.append(h('div', {
484
+ class: 'zx-select__empty',
485
+ role: 'status'
486
+ }, this._message('select.empty', 'No matches')));
487
+ }
488
+ this._setActive(-1);
489
+ }
490
+
491
+ /** @param {SelectItem|null} item @param {boolean} clear @param {string} [label] @returns {void} */
492
+ _appendOption(item, clear, label) {
493
+ const selected = clear ? this._selected === null :
494
+ this._selected !== null && Object.is(this._itemValue(item), this.value);
495
+ const index = this._optionEntries.length;
496
+ const content = clear ? String(label ?? '') : this._renderItem(item);
497
+ const option = h('div', {
498
+ class: 'zx-select__option',
499
+ id: uid('zx-select-option'),
500
+ role: 'option',
501
+ ariaSelected: String(selected),
502
+ dataset: { index }
503
+ }, h('span', { class: 'zx-select__option-content' }, content));
504
+ if (selected) option.append(icon('check', { size: 15 }));
505
+ this.refs.list.append(option);
506
+ this._optionEntries.push({ item, clear, element: option });
507
+ }
508
+
509
+ /** @param {SelectItem|null} item @returns {Node|string} */
510
+ _renderItem(item) {
511
+ if (typeof this.options.renderItem === 'function') {
512
+ const rendered = this.options.renderItem(item);
513
+ return rendered && typeof rendered === 'object' && 'nodeType' in rendered ?
514
+ /** @type {Node} */ (rendered) : String(rendered ?? '');
515
+ }
516
+ return this._itemLabel(item);
517
+ }
518
+
519
+ /** @param {string} query @returns {void} */
520
+ _scheduleAsyncQuery(query) {
521
+ if (this._queryTimer !== null) clearTimeout(this._queryTimer);
522
+ this._requestSequence += 1;
523
+ const sequence = this._requestSequence;
524
+ if (query.length < Math.max(0, Number(this.options.minQuery) || 0)) {
525
+ this._setLoading(false);
526
+ this._visibleItems = [];
527
+ this._renderOptions();
528
+ return;
529
+ }
530
+ this._setLoading(true);
531
+ this._renderOptions();
532
+ this._position?.update();
533
+ const delay = Math.max(0, Number(this.options.debounce) || 0);
534
+ this._queryTimer = setTimeout(() => {
535
+ this._queryTimer = null;
536
+ this._runAsyncQuery(query, sequence);
537
+ }, delay);
538
+ }
539
+
540
+ /** @param {string} query @param {number} sequence @returns {Promise<void>} */
541
+ async _runAsyncQuery(query, sequence) {
542
+ if (sequence !== this._requestSequence || typeof this.options.filter !== 'function') return;
543
+ this.emit('query', { query });
544
+ try {
545
+ const result = await this.options.filter(query);
546
+ if (sequence !== this._requestSequence || !this._open) return;
547
+ this._items = Array.isArray(result) ? result.slice() : [];
548
+ this._visibleItems = this._items.slice();
549
+ this._setLoading(false);
550
+ this._renderOptions();
551
+ this._setActive(this._optionEntries.length > 0 ? 0 : -1);
552
+ this._position?.update();
553
+ this.emit('loaded', { items: this._items.slice() });
554
+ } catch {
555
+ if (sequence !== this._requestSequence || !this._open) return;
556
+ this._items = [];
557
+ this._visibleItems = [];
558
+ this._setLoading(false);
559
+ this._renderOptions();
560
+ this._position?.update();
561
+ }
562
+ }
563
+
564
+ /** @returns {void} */
565
+ _cancelQuery() {
566
+ if (this._queryTimer !== null) clearTimeout(this._queryTimer);
567
+ this._queryTimer = null;
568
+ this._requestSequence += 1;
569
+ }
570
+
571
+ /** @param {boolean} loading @returns {void} */
572
+ _setLoading(loading) {
573
+ this._loading = loading;
574
+ this.refs.loading.hidden = !loading;
575
+ this.refs.input.setAttribute('aria-busy', String(loading));
576
+ this.refs.list.setAttribute('aria-busy', String(loading));
577
+ if (loading) this._root?.setAttribute('data-loading', 'true');
578
+ else this._root?.removeAttribute('data-loading');
579
+ }
580
+
581
+ /** @returns {void} */
582
+ _syncInputValue() {
583
+ if (!this.refs.input) return;
584
+ const input = /** @type {HTMLInputElement} */ (this.refs.input);
585
+ input.value = this._selected === null ? '' :
586
+ (typeof this.options.renderValue === 'function' ?
587
+ String(this.options.renderValue(this._selected) ?? '') : this._itemLabel(this._selected));
588
+ this.refs.clear.hidden = !this.options.clearable || this._selected === null;
589
+ }
590
+
591
+ /** @returns {void} */
592
+ _syncDisabled() {
593
+ const input = /** @type {HTMLInputElement} */ (this.refs.input);
594
+ const clear = /** @type {HTMLButtonElement} */ (this.refs.clear);
595
+ const toggle = /** @type {HTMLButtonElement} */ (this.refs.toggle);
596
+ input.disabled = this._disabled;
597
+ clear.disabled = this._disabled;
598
+ toggle.disabled = this._disabled;
599
+ this._root?.setAttribute('aria-disabled', String(this._disabled));
600
+ }
601
+
602
+ /** @param {unknown} candidate @returns {SelectItem|null} */
603
+ _findItem(candidate) {
604
+ const direct = this._items.find((item) => item === candidate);
605
+ if (direct !== undefined) return direct;
606
+ const value = candidate !== null && typeof candidate === 'object' ?
607
+ this._itemValue(/** @type {SelectItem} */ (candidate)) : candidate;
608
+ return this._items.find((item) => Object.is(this._itemValue(item), value)) ?? null;
609
+ }
610
+
611
+ /** @param {SelectItem|null} item @returns {unknown} */
612
+ _itemValue(item) {
613
+ return this._read(item, this.options.valueKey);
614
+ }
615
+
616
+ /** @param {SelectItem|null} item @returns {string} */
617
+ _itemLabel(item) {
618
+ const value = this._read(item, this.options.labelKey);
619
+ return String(value ?? '');
620
+ }
621
+
622
+ /** @param {SelectItem|null} item @param {SelectValueReader|SelectLabelReader} reader @returns {unknown} */
623
+ _read(item, reader) {
624
+ if (typeof reader === 'function') return reader(item);
625
+ if (item === null || typeof item !== 'object') return item;
626
+ return item[reader];
627
+ }
628
+
629
+ /** @returns {number} */
630
+ _selectedEntryIndex() {
631
+ if (this._selected === null) return this.options.clearable ? 0 : -1;
632
+ return this._optionEntries.findIndex((entry) =>
633
+ !entry.clear && Object.is(this._itemValue(entry.item), this.value)
634
+ );
635
+ }
636
+
637
+ /** @param {number} itemIndex @returns {number} */
638
+ _itemEntryIndex(itemIndex) {
639
+ return itemIndex + (this.options.clearable ? 1 : 0);
640
+ }
641
+
642
+ /** @param {string} name @returns {void} */
643
+ _rememberAttribute(element, name) {
644
+ this._originalAttributes.set(name, element.getAttribute(name));
645
+ }
646
+
647
+ /** @param {string} key @param {string} fallback @returns {string} */
648
+ _message(key, fallback) {
649
+ const message = this.msg(key);
650
+ return message === key ? fallback : message;
651
+ }
652
+ }
653
+
654
+ /** Change emitted when the selected value changes. @event Select#change @type {CustomEvent<SelectChangeDetail>} */
655
+ /** Panel-open notification. @event Select#open @type {CustomEvent<Record<string, never>>} */
656
+ /** Panel-close notification. @event Select#close @type {CustomEvent<Record<string, never>>} */
657
+ /** Async-query start notification. @event Select#query @type {CustomEvent<SelectQueryDetail>} */
658
+ /** Async results notification. @event Select#loaded @type {CustomEvent<SelectLoadedDetail>} */
659
+
660
+ /** @param {KeyboardEvent} event @returns {boolean} */
661
+ function isPrintable(event) {
662
+ return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
663
+ }