@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,20 @@
1
+ .zx-truncate {
2
+ min-inline-size: 0;
3
+ overflow: hidden;
4
+ text-overflow: ellipsis;
5
+ white-space: nowrap;
6
+ }
7
+
8
+ /*
9
+ * `-webkit-line-clamp` is still the only clamp every target browser implements, and it needs the
10
+ * legacy flexbox display to take effect. `line-clamp` is listed alongside it so the rule stops
11
+ * needing the prefix as soon as the standard property ships.
12
+ */
13
+ .zx-truncate[data-lines] {
14
+ display: -webkit-box;
15
+ -webkit-box-orient: vertical;
16
+ -webkit-line-clamp: var(--zx-truncate-lines, 2);
17
+ line-clamp: var(--zx-truncate-lines, 2);
18
+ white-space: normal;
19
+ overflow-wrap: anywhere;
20
+ }
@@ -0,0 +1,92 @@
1
+ import { resolveElement } from '../../core/dom.js';
2
+ import { clamp } from '../../core/util.js';
3
+
4
+ /**
5
+ * @typedef {Object} TruncateOptions
6
+ * @property {number} [lines=1] Lines to keep. One line ends in an ellipsis; more clamp the block.
7
+ * @property {boolean} [title=true] Whether a native `title` tooltip is set — and kept up to date —
8
+ * while the text is actually cut off, so the full value stays reachable on hover.
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} TruncateController
13
+ * @property {() => boolean} update Re-measures and returns whether the text is currently cut off.
14
+ * @property {() => boolean} isTruncated Whether the text was cut off at the last measurement.
15
+ * @property {() => void} destroy Removes the classes, the observer, and any title this set.
16
+ */
17
+
18
+ /**
19
+ * Clamps an element's text to a number of lines, and gives the cut-off text back on hover.
20
+ *
21
+ * The `title` half is what makes this more than a CSS class. A table cell that silently drops the
22
+ * end of a value is a data-loss bug wearing a layout costume; a tooltip that is always present is
23
+ * noise. Measuring on every resize means the tooltip appears exactly when the text is short of
24
+ * room and disappears when the column is widened.
25
+ *
26
+ * @param {Element|string} target Element or selector to clamp.
27
+ * @param {TruncateOptions} [options={}] Truncation options.
28
+ * @returns {TruncateController}
29
+ */
30
+ export function truncate(target, options = {}) {
31
+ const element = resolveElement(target);
32
+ if (!element) throw new TypeError('truncate() target could not be resolved');
33
+
34
+ const lines = clamp(Math.trunc(Number(options.lines) || 1), 1, 10);
35
+ const withTitle = options.title !== false;
36
+ const hadClass = element.classList.contains('zx-truncate');
37
+ const previousTitle = element.getAttribute('title');
38
+ let truncated = false;
39
+ let observer = null;
40
+
41
+ element.classList.add('zx-truncate');
42
+ if (lines > 1) {
43
+ element.dataset.lines = String(lines);
44
+ /** @type {HTMLElement} */ (element).style.setProperty('--zx-truncate-lines', String(lines));
45
+ }
46
+
47
+ const update = () => {
48
+ truncated = isTruncated(element);
49
+ if (withTitle) {
50
+ if (truncated) element.setAttribute('title', element.textContent?.trim() ?? '');
51
+ else if (previousTitle === null) element.removeAttribute('title');
52
+ else element.setAttribute('title', previousTitle);
53
+ }
54
+ return truncated;
55
+ };
56
+
57
+ update();
58
+ if (typeof ResizeObserver === 'function') {
59
+ observer = new ResizeObserver(update);
60
+ observer.observe(element);
61
+ }
62
+
63
+ return {
64
+ update,
65
+ isTruncated: () => truncated,
66
+ destroy: () => {
67
+ observer?.disconnect();
68
+ observer = null;
69
+ if (!hadClass) element.classList.remove('zx-truncate');
70
+ delete element.dataset.lines;
71
+ /** @type {HTMLElement} */ (element).style.removeProperty('--zx-truncate-lines');
72
+ if (!withTitle) return;
73
+ if (previousTitle === null) element.removeAttribute('title');
74
+ else element.setAttribute('title', previousTitle);
75
+ }
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Whether an element's text overflows the box it is in.
81
+ *
82
+ * Both axes are checked because the two clamping modes overflow differently: a single line runs
83
+ * out of width, a multi-line clamp runs out of height. The one-pixel tolerance absorbs the
84
+ * sub-pixel difference between scroll and client sizes that fractional zoom levels produce.
85
+ *
86
+ * @param {Element} element Element to measure.
87
+ * @returns {boolean}
88
+ */
89
+ export function isTruncated(element) {
90
+ return element.scrollWidth - element.clientWidth > 1
91
+ || element.scrollHeight - element.clientHeight > 1;
92
+ }
@@ -0,0 +1,24 @@
1
+ import { Field } from '../field/field.js';
2
+ import { ValueList } from './value-list.js';
3
+
4
+ /**
5
+ * Registers the ValueList-backed `valuelist` field type.
6
+ * @returns {void}
7
+ */
8
+ export function registerValueListFieldAdapter() {
9
+ Field.register('valuelist', (field, options) => {
10
+ const valueList = field.own(new ValueList(null, {
11
+ ...(options.props ?? {}),
12
+ values: Array.isArray(options.value) ? options.value : []
13
+ }));
14
+ field.listen(valueList.el, 'change', (event) => event.stopImmediatePropagation());
15
+ valueList.on('change', (event) => field.emit('change', { value: event.detail.values }));
16
+ return {
17
+ el: valueList.el,
18
+ get: () => valueList.getValues(),
19
+ set: (value) => valueList.setValues(Array.isArray(value) ? value : [], { silent: true }),
20
+ focus: () => valueList.focus(),
21
+ setDisabled: (disabled) => disabled ? valueList.disable() : valueList.enable()
22
+ };
23
+ });
24
+ }
@@ -0,0 +1,108 @@
1
+ .zx-value-list {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ align-items: center;
5
+ gap: var(--zx-space-2);
6
+ min-block-size: var(--zx-control-height);
7
+ padding: var(--zx-space-1);
8
+ border: 1px solid var(--zx-color-border-control);
9
+ border-radius: var(--zx-control-radius);
10
+ background: var(--zx-color-bg-control);
11
+ }
12
+
13
+ .zx-value-list:focus-within {
14
+ box-shadow: var(--zx-focus-ring);
15
+ }
16
+
17
+ .zx-value-list__list {
18
+ display: contents;
19
+ margin: 0;
20
+ padding: 0;
21
+ list-style: none;
22
+ }
23
+
24
+ .zx-value-list__chip {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ gap: var(--zx-space-1);
28
+ min-block-size: calc(var(--zx-control-height) - var(--zx-space-2));
29
+ max-inline-size: 100%;
30
+ padding-inline: var(--zx-space-2);
31
+ border: 1px solid var(--zx-color-border);
32
+ border-radius: var(--zx-radius-full);
33
+ background: var(--zx-color-bg-selected);
34
+ color: var(--zx-color-text);
35
+ cursor: grab;
36
+ }
37
+
38
+ .zx-value-list__chip:focus-visible {
39
+ box-shadow: var(--zx-focus-ring);
40
+ }
41
+
42
+ .zx-value-list__value {
43
+ overflow: hidden;
44
+ text-overflow: ellipsis;
45
+ text-wrap: nowrap;
46
+ }
47
+
48
+ .zx-value-list__remove {
49
+ display: inline-grid;
50
+ place-items: center;
51
+ min-inline-size: 1.25rem;
52
+ min-block-size: 1.25rem;
53
+ border-radius: var(--zx-radius-full);
54
+ color: var(--zx-color-text-muted);
55
+ cursor: pointer;
56
+ }
57
+
58
+ .zx-value-list__remove:hover {
59
+ background: var(--zx-color-bg-hover);
60
+ color: var(--zx-color-danger);
61
+ }
62
+
63
+ .zx-value-list__input {
64
+ flex: 1 1 8rem;
65
+ min-inline-size: 6rem;
66
+ border: 0;
67
+ background: inherit;
68
+ box-shadow: none;
69
+ }
70
+
71
+ .zx-value-list__input:focus-visible {
72
+ box-shadow: none;
73
+ }
74
+
75
+ .zx-value-list__status {
76
+ flex-basis: 100%;
77
+ color: var(--zx-color-danger);
78
+ font-size: var(--zx-text-sm);
79
+ }
80
+
81
+ .zx-value-list__status:empty {
82
+ display: none;
83
+ }
84
+
85
+ .zx-value-list__instructions {
86
+ position: absolute;
87
+ inline-size: 1px;
88
+ block-size: 1px;
89
+ overflow: hidden;
90
+ clip-path: inset(50%);
91
+ }
92
+
93
+ .zx-value-list[data-state="dragging"] .zx-value-list__chip {
94
+ cursor: grabbing;
95
+ }
96
+
97
+ .zx-value-list[data-disabled="true"] {
98
+ background: var(--zx-color-bg-page);
99
+ opacity: 0.65;
100
+ }
101
+
102
+ .zx-value-list[data-disabled="true"] .zx-value-list__chip {
103
+ cursor: default;
104
+ }
105
+
106
+ .zx-value-list[data-disabled="true"] .zx-value-list__remove {
107
+ display: none;
108
+ }
@@ -0,0 +1,336 @@
1
+ import { Component } from '../../core/component.js';
2
+ import { h } from '../../core/dom.js';
3
+ import { uid } from '../../core/util.js';
4
+
5
+ /**
6
+ * @typedef {Object} ValueListOptions
7
+ * @property {unknown[]} [values=[]] Initial ordered values.
8
+ * @property {string} [placeholder=''] Input placeholder.
9
+ * @property {boolean} [deletable=true] Whether values can be removed.
10
+ * @property {boolean} [sortable=true] Whether values can be reordered.
11
+ * @property {boolean} [unique=true] Whether duplicate strings are rejected.
12
+ * @property {((value: string) => boolean|string)|null} [validate=null] Value validator.
13
+ */
14
+
15
+ /**
16
+ * Tag/chip editor. It uses the APG listbox/option focus model: each chip is an option, arrow keys
17
+ * move focus, Ctrl+Left/Right reorders, and Delete/Backspace removes the focused option.
18
+ * @fires ValueList#change
19
+ * @fires ValueList#add
20
+ * @fires ValueList#remove
21
+ * @extends {Component<ValueListOptions>}
22
+ */
23
+ export class ValueList extends Component {
24
+ static cssName = 'value-list';
25
+
26
+ /** @type {ValueListOptions} */
27
+ static defaults = {
28
+ values: [],
29
+ placeholder: '',
30
+ deletable: true,
31
+ sortable: true,
32
+ unique: true,
33
+ validate: null
34
+ };
35
+
36
+ /** @returns {HTMLElement} */
37
+ render() {
38
+ const root = /** @type {HTMLElement} */ (this.el ?? h('div'));
39
+ this._ownedNodes = [];
40
+ this._disabled = false;
41
+ this._dragIndex = -1;
42
+ this._initialState = root.getAttribute('data-state');
43
+ this._initialDisabled = root.getAttribute('data-disabled');
44
+ this._values = normalizeValues(this.options.values, Boolean(this.options.unique));
45
+ const instructionsId = uid('zx-value-list-instructions');
46
+ this._list = h('ul', {
47
+ class: 'zx-value-list__list',
48
+ role: 'listbox',
49
+ ariaLabel: 'Values',
50
+ ariaOrientation: 'horizontal'
51
+ });
52
+ this._input = h('input', {
53
+ class: 'zx-value-list__input',
54
+ type: 'text',
55
+ placeholder: String(this.options.placeholder ?? ''),
56
+ ariaLabel: 'Add value',
57
+ ariaDescribedby: instructionsId
58
+ });
59
+ this._status = h('div', {
60
+ class: 'zx-value-list__status',
61
+ role: 'status',
62
+ ariaLive: 'polite'
63
+ });
64
+ this._instructions = h('span', {
65
+ class: 'zx-value-list__instructions',
66
+ id: instructionsId
67
+ }, 'Press Enter to add. Focus a value and press Ctrl plus Left or Right to reorder.');
68
+ this._ownedNodes.push(this._list, this._input, this._status, this._instructions);
69
+ root.append(this._list, this._input, this._status, this._instructions);
70
+
71
+ this.listen(this._input, 'keydown', (event) => this._handleInputKeydown(event));
72
+ this.listen(this._list, 'keydown', (event) => this._handleChipKeydown(event));
73
+ this.listen(this._list, 'click', (event) => this._handleChipClick(event));
74
+ this.listen(this._list, 'dragstart', (event) => this._handleDragStart(event));
75
+ this.listen(this._list, 'dragover', (event) => {
76
+ if (this._dragIndex >= 0 && !this._disabled) event.preventDefault();
77
+ });
78
+ this.listen(this._list, 'drop', (event) => this._handleDrop(event));
79
+ this.listen(this._list, 'dragend', () => this._finishDrag());
80
+ this.listen(root, 'click', (event) => {
81
+ if (event.target === root && !this._disabled) this.focus();
82
+ });
83
+ this._renderChips();
84
+ return root;
85
+ }
86
+
87
+ /**
88
+ * Returns a copy of the ordered values.
89
+ * @returns {string[]}
90
+ */
91
+ getValues() {
92
+ return this._values.slice();
93
+ }
94
+
95
+ /**
96
+ * Replaces all values.
97
+ * @param {unknown[]} values New values.
98
+ * @param {{silent?: boolean}} [options={}] Set `silent` to suppress `change`.
99
+ * @returns {this}
100
+ */
101
+ setValues(values, { silent = false } = {}) {
102
+ this._values = normalizeValues(values, Boolean(this.options.unique));
103
+ this._renderChips();
104
+ if (!silent) this.emit('change', { values: this.getValues() });
105
+ return this;
106
+ }
107
+
108
+ /**
109
+ * Validates and appends one value.
110
+ * @param {unknown} value Value to append.
111
+ * @returns {boolean} Whether the value was added.
112
+ * @fires ValueList#add
113
+ * @fires ValueList#change
114
+ */
115
+ addValue(value) {
116
+ if (this._disabled) return false;
117
+ const normalized = String(value ?? '').trim();
118
+ if (!normalized) return false;
119
+ if (this.options.unique && this._values.includes(normalized)) {
120
+ this._showError('This value already exists.');
121
+ return false;
122
+ }
123
+ if (typeof this.options.validate === 'function') {
124
+ const result = this.options.validate(normalized);
125
+ if (result !== true) {
126
+ this._showError(typeof result === 'string' ? result : 'This value is not valid.');
127
+ return false;
128
+ }
129
+ }
130
+ this._status.textContent = '';
131
+ this._input.removeAttribute('aria-invalid');
132
+ this._values.push(normalized);
133
+ this._renderChips();
134
+ this.emit('add', { value: normalized });
135
+ this.emit('change', { values: this.getValues() });
136
+ return true;
137
+ }
138
+
139
+ /**
140
+ * Removes the first matching value.
141
+ * @param {unknown} value Value to remove.
142
+ * @returns {boolean} Whether a value was removed.
143
+ * @fires ValueList#remove
144
+ * @fires ValueList#change
145
+ */
146
+ removeValue(value) {
147
+ if (this._disabled || !this.options.deletable) return false;
148
+ const index = this._values.indexOf(String(value));
149
+ if (index < 0) return false;
150
+ this._removeAt(index);
151
+ return true;
152
+ }
153
+
154
+ /**
155
+ * Focuses the value input.
156
+ * @returns {this}
157
+ */
158
+ focus() {
159
+ this._input.focus();
160
+ return this;
161
+ }
162
+
163
+ /**
164
+ * Enables editing.
165
+ * @returns {this}
166
+ */
167
+ enable() {
168
+ this._disabled = false;
169
+ this._input.disabled = false;
170
+ this.el.dataset.disabled = 'false';
171
+ this._renderChips();
172
+ return this;
173
+ }
174
+
175
+ /**
176
+ * Disables editing, removal, and reordering.
177
+ * @returns {this}
178
+ */
179
+ disable() {
180
+ this._disabled = true;
181
+ this._input.disabled = true;
182
+ this.el.dataset.disabled = 'true';
183
+ this._renderChips();
184
+ return this;
185
+ }
186
+
187
+ /** @returns {void} */
188
+ _renderChips(focusIndex = -1) {
189
+ const chips = this._values.map((value, index) => h('li', {
190
+ class: 'zx-value-list__chip',
191
+ role: 'option',
192
+ tabIndex: this._disabled ? -1 : 0,
193
+ draggable: Boolean(this.options.sortable) && !this._disabled,
194
+ ariaSelected: 'false',
195
+ ariaDisabled: String(this._disabled),
196
+ ariaLabel: this.options.deletable ? `${value}. Press Delete to remove.` : value,
197
+ dataset: { index }
198
+ }, h('span', { class: 'zx-value-list__value' }, value), this.options.deletable ? h('span', {
199
+ class: 'zx-value-list__remove',
200
+ dataset: { remove: '' },
201
+ ariaHidden: 'true'
202
+ }, '×') : null));
203
+ this._list.replaceChildren(...chips);
204
+ if (focusIndex >= 0) chips[Math.min(focusIndex, chips.length - 1)]?.focus();
205
+ }
206
+
207
+ /** @param {KeyboardEvent} event @returns {void} */
208
+ _handleInputKeydown(event) {
209
+ if (this._disabled) return;
210
+ if (event.key === 'Enter') {
211
+ event.preventDefault();
212
+ if (this.addValue(this._input.value)) this._input.value = '';
213
+ } else if (event.key === 'Backspace' && this._input.value === '' && this._values.length && this.options.deletable) {
214
+ event.preventDefault();
215
+ this._removeAt(this._values.length - 1);
216
+ }
217
+ }
218
+
219
+ /** @param {KeyboardEvent} event @returns {void} */
220
+ _handleChipKeydown(event) {
221
+ if (this._disabled) return;
222
+ const chip = event.target instanceof Element ? event.target.closest('[data-index]') : null;
223
+ if (!chip) return;
224
+ const index = Number(chip.dataset.index);
225
+ if ((event.key === 'Delete' || event.key === 'Backspace') && this.options.deletable) {
226
+ event.preventDefault();
227
+ this._removeAt(index, Math.min(index, this._values.length - 2));
228
+ return;
229
+ }
230
+ const horizontal = event.key === 'ArrowLeft' ? -1 : event.key === 'ArrowRight' ? 1 : 0;
231
+ const vertical = event.key === 'ArrowUp' ? -1 : event.key === 'ArrowDown' ? 1 : 0;
232
+ const direction = horizontal || vertical;
233
+ if (!direction) return;
234
+ event.preventDefault();
235
+ const next = Math.max(0, Math.min(this._values.length - 1, index + direction));
236
+ if (event.ctrlKey && horizontal && this.options.sortable) this._move(index, next, true);
237
+ else this._list.querySelector(`[data-index="${next}"]`)?.focus();
238
+ }
239
+
240
+ /** @param {MouseEvent} event @returns {void} */
241
+ _handleChipClick(event) {
242
+ const target = event.target instanceof Element ? event.target : null;
243
+ const chip = target?.closest('[data-index]');
244
+ if (!chip || this._disabled) return;
245
+ if (target.closest('[data-remove]') && this.options.deletable) this._removeAt(Number(chip.dataset.index));
246
+ else chip.focus();
247
+ }
248
+
249
+ /** @param {DragEvent} event @returns {void} */
250
+ _handleDragStart(event) {
251
+ if (this._disabled || !this.options.sortable) return;
252
+ const chip = event.target instanceof Element ? event.target.closest('[data-index]') : null;
253
+ if (!chip) return;
254
+ this._dragIndex = Number(chip.dataset.index);
255
+ this.el.dataset.state = 'dragging';
256
+ event.dataTransfer?.setData('text/plain', String(this._dragIndex));
257
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
258
+ }
259
+
260
+ /** @param {DragEvent} event @returns {void} */
261
+ _handleDrop(event) {
262
+ if (this._dragIndex < 0 || this._disabled) return;
263
+ event.preventDefault();
264
+ const chip = event.target instanceof Element ? event.target.closest('[data-index]') : null;
265
+ const target = chip ? Number(chip.dataset.index) : this._values.length - 1;
266
+ this._move(this._dragIndex, target, true);
267
+ this._finishDrag();
268
+ }
269
+
270
+ /** @returns {void} */
271
+ _finishDrag() {
272
+ this._dragIndex = -1;
273
+ if (this._initialState === null) this.el.removeAttribute('data-state');
274
+ else this.el.setAttribute('data-state', this._initialState);
275
+ }
276
+
277
+ /** @param {number} start @param {number} end @param {boolean} focus @returns {void} */
278
+ _move(start, end, focus) {
279
+ if (start === end || start < 0 || end < 0 || start >= this._values.length || end >= this._values.length) return;
280
+ const [value] = this._values.splice(start, 1);
281
+ this._values.splice(end, 0, value);
282
+ this._renderChips(focus ? end : -1);
283
+ this.emit('change', { values: this.getValues() });
284
+ }
285
+
286
+ /** @param {number} index @param {number} [focusIndex=-1] @returns {void} */
287
+ _removeAt(index, focusIndex = -1) {
288
+ const [value] = this._values.splice(index, 1);
289
+ if (value === undefined) return;
290
+ this._renderChips(focusIndex);
291
+ if (focusIndex < 0 && this._values.length === 0) this._input.focus();
292
+ this.emit('remove', { value });
293
+ this.emit('change', { values: this.getValues() });
294
+ }
295
+
296
+ /** @param {string} message @returns {void} */
297
+ _showError(message) {
298
+ this._status.textContent = message;
299
+ this._input.setAttribute('aria-invalid', 'true');
300
+ }
301
+
302
+ /** @returns {void} */
303
+ destroy() {
304
+ for (const node of this._ownedNodes ?? []) node.remove();
305
+ if (this._initialState === null) this.el.removeAttribute('data-state');
306
+ else this.el.setAttribute('data-state', this._initialState);
307
+ if (this._initialDisabled === null) this.el.removeAttribute('data-disabled');
308
+ else this.el.setAttribute('data-disabled', this._initialDisabled);
309
+ super.destroy();
310
+ }
311
+ }
312
+
313
+ /** @param {unknown} values @param {boolean} unique @returns {string[]} */
314
+ function normalizeValues(values, unique) {
315
+ if (!Array.isArray(values)) return [];
316
+ const normalized = values.map((value) => String(value));
317
+ return unique ? [...new Set(normalized)] : normalized;
318
+ }
319
+
320
+ /**
321
+ * Values changed event.
322
+ * @event ValueList#change
323
+ * @type {CustomEvent<{values: string[]}>
324
+ */
325
+
326
+ /**
327
+ * Value added event.
328
+ * @event ValueList#add
329
+ * @type {CustomEvent<{value: string}>
330
+ */
331
+
332
+ /**
333
+ * Value removed event.
334
+ * @event ValueList#remove
335
+ * @type {CustomEvent<{value: string}>
336
+ */