@swiftwc/ui 1.0.0-canary.0 → 1.0.0-canary.2

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 (262) hide show
  1. package/generated/buses/alert-dialog.d.ts +2 -0
  2. package/generated/buses/alert-dialog.js +1 -0
  3. package/generated/buses/confirmation-dialog.d.ts +2 -0
  4. package/generated/buses/confirmation-dialog.js +1 -0
  5. package/generated/buses/index.d.ts +6 -0
  6. package/generated/buses/index.js +6 -0
  7. package/generated/buses/lifecycle-observer.d.ts +2 -0
  8. package/generated/buses/lifecycle-observer.js +1 -0
  9. package/generated/client/index.d.ts +30 -0
  10. package/generated/client/index.js +278 -0
  11. package/generated/components/alert-dialog.d.ts +11 -0
  12. package/generated/components/alert-dialog.js +94 -0
  13. package/generated/components/bordered-button.d.ts +17 -0
  14. package/generated/components/bordered-button.js +53 -0
  15. package/generated/components/bordered-prominent-button.d.ts +17 -0
  16. package/generated/components/bordered-prominent-button.js +53 -0
  17. package/generated/components/borderless-button.d.ts +17 -0
  18. package/generated/components/borderless-button.js +53 -0
  19. package/generated/components/color-scheme.d.ts +12 -0
  20. package/generated/components/color-scheme.js +51 -0
  21. package/generated/components/confirmation-dialog.d.ts +12 -0
  22. package/generated/components/confirmation-dialog.js +108 -0
  23. package/generated/components/content-unavailable.d.ts +30 -0
  24. package/generated/components/content-unavailable.js +79 -0
  25. package/generated/components/content-view.d.ts +13 -0
  26. package/generated/components/content-view.js +15 -0
  27. package/generated/components/date-picker.d.ts +52 -0
  28. package/generated/components/date-picker.js +468 -0
  29. package/generated/components/detail-placeholder.d.ts +11 -0
  30. package/generated/components/detail-placeholder.js +15 -0
  31. package/generated/components/disclosure-group.d.ts +17 -0
  32. package/generated/components/disclosure-group.js +62 -0
  33. package/generated/components/fine-tooltip.d.ts +15 -0
  34. package/generated/components/fine-tooltip.js +108 -0
  35. package/generated/components/form-view.d.ts +16 -0
  36. package/generated/components/form-view.js +63 -0
  37. package/generated/components/glass-button.d.ts +16 -0
  38. package/generated/components/glass-button.js +50 -0
  39. package/generated/components/glass-prominent-button.d.ts +16 -0
  40. package/generated/components/glass-prominent-button.js +50 -0
  41. package/generated/components/h-divider.d.ts +11 -0
  42. package/generated/components/h-divider.js +15 -0
  43. package/generated/components/h-flex-stack.d.ts +16 -0
  44. package/generated/components/h-flex-stack.js +18 -0
  45. package/generated/components/h-stack.d.ts +22 -0
  46. package/generated/components/h-stack.js +24 -0
  47. package/generated/components/image-view.d.ts +18 -0
  48. package/generated/components/image-view.js +65 -0
  49. package/generated/components/index.d.ts +53 -0
  50. package/generated/components/index.js +53 -0
  51. package/generated/components/label-view.d.ts +27 -0
  52. package/generated/components/label-view.js +84 -0
  53. package/generated/components/labeled-content.d.ts +33 -0
  54. package/generated/components/labeled-content.js +160 -0
  55. package/generated/components/list-view.d.ts +19 -0
  56. package/generated/components/list-view.js +24 -0
  57. package/generated/components/menu-view.d.ts +20 -0
  58. package/generated/components/menu-view.js +131 -0
  59. package/generated/components/navigation-large-title.d.ts +17 -0
  60. package/generated/components/navigation-large-title.js +183 -0
  61. package/generated/components/navigation-split-view.d.ts +15 -0
  62. package/generated/components/navigation-split-view.js +22 -0
  63. package/generated/components/navigation-stack.d.ts +15 -0
  64. package/generated/components/navigation-stack.js +26 -0
  65. package/generated/components/navigation-title.d.ts +18 -0
  66. package/generated/components/navigation-title.js +69 -0
  67. package/generated/components/picker-view.d.ts +51 -0
  68. package/generated/components/picker-view.js +1201 -0
  69. package/generated/components/plain-button.d.ts +7 -0
  70. package/generated/components/plain-button.js +20 -0
  71. package/generated/components/progress-view.d.ts +20 -0
  72. package/generated/components/progress-view.js +120 -0
  73. package/generated/components/screen-view.d.ts +5 -0
  74. package/generated/components/screen-view.js +10 -0
  75. package/generated/components/scroll-view-proxy.d.ts +13 -0
  76. package/generated/components/scroll-view-proxy.js +15 -0
  77. package/generated/components/scroll-view.d.ts +22 -0
  78. package/generated/components/scroll-view.js +230 -0
  79. package/generated/components/search-view.d.ts +12 -0
  80. package/generated/components/search-view.js +24 -0
  81. package/generated/components/section-view.d.ts +21 -0
  82. package/generated/components/section-view.js +61 -0
  83. package/generated/components/sheet-view.d.ts +11 -0
  84. package/generated/components/sheet-view.js +40 -0
  85. package/generated/components/sidebar-toggle.d.ts +15 -0
  86. package/generated/components/sidebar-toggle.js +84 -0
  87. package/generated/components/sidebar-view.d.ts +10 -0
  88. package/generated/components/sidebar-view.js +36 -0
  89. package/generated/components/sticky-container.d.ts +11 -0
  90. package/generated/components/sticky-container.js +24 -0
  91. package/generated/components/tab-bar.d.ts +10 -0
  92. package/generated/components/tab-bar.js +37 -0
  93. package/generated/components/tab-item.d.ts +10 -0
  94. package/generated/components/tab-item.js +127 -0
  95. package/generated/components/tab-view.d.ts +31 -0
  96. package/generated/components/tab-view.js +194 -0
  97. package/generated/components/table-view.d.ts +26 -0
  98. package/generated/components/table-view.js +156 -0
  99. package/generated/components/text-field.d.ts +42 -0
  100. package/generated/components/text-field.js +517 -0
  101. package/generated/components/text-view.d.ts +11 -0
  102. package/generated/components/text-view.js +16 -0
  103. package/generated/components/toggle-view.d.ts +43 -0
  104. package/generated/components/toggle-view.js +366 -0
  105. package/generated/components/tool-bar-item-group.d.ts +13 -0
  106. package/generated/components/tool-bar-item-group.js +29 -0
  107. package/generated/components/tool-bar-item.d.ts +16 -0
  108. package/generated/components/tool-bar-item.js +66 -0
  109. package/generated/components/tool-bar.d.ts +26 -0
  110. package/generated/components/tool-bar.js +92 -0
  111. package/generated/components/v-divider.d.ts +11 -0
  112. package/generated/components/v-divider.js +15 -0
  113. package/generated/components/v-flex-stack.d.ts +16 -0
  114. package/generated/components/v-flex-stack.js +18 -0
  115. package/generated/components/v-keyboard.d.ts +20 -0
  116. package/generated/components/v-keyboard.js +85 -0
  117. package/generated/components/v-stack.d.ts +25 -0
  118. package/generated/components/v-stack.js +27 -0
  119. package/generated/css/index.css +10964 -0
  120. package/generated/events/alert-dialog.d.ts +13 -0
  121. package/generated/events/alert-dialog.js +1 -0
  122. package/generated/events/confirmation-dialog.d.ts +14 -0
  123. package/generated/events/confirmation-dialog.js +1 -0
  124. package/generated/events/date-picker.d.ts +13 -0
  125. package/generated/events/date-picker.js +1 -0
  126. package/generated/events/index.d.ts +9 -0
  127. package/generated/events/index.js +9 -0
  128. package/generated/events/lifecycle-observer.d.ts +18 -0
  129. package/generated/events/lifecycle-observer.js +1 -0
  130. package/generated/events/picker-view.d.ts +29 -0
  131. package/generated/events/picker-view.js +1 -0
  132. package/generated/events/scroll-view.d.ts +18 -0
  133. package/generated/events/scroll-view.js +1 -0
  134. package/generated/events/tab-view.d.ts +54 -0
  135. package/generated/events/tab-view.js +1 -0
  136. package/generated/events/text-field.d.ts +13 -0
  137. package/generated/events/text-field.js +1 -0
  138. package/generated/events/toggle-view.d.ts +14 -0
  139. package/generated/events/toggle-view.js +1 -0
  140. package/generated/i18n/index.d.ts +33 -0
  141. package/generated/i18n/index.js +164 -0
  142. package/generated/i18n/locales/el.d.ts +16 -0
  143. package/generated/i18n/locales/el.js +16 -0
  144. package/generated/i18n/locales/en.d.ts +16 -0
  145. package/generated/i18n/locales/en.js +16 -0
  146. package/generated/i18n/locales/index.d.ts +4 -0
  147. package/generated/i18n/locales/index.js +4 -0
  148. package/generated/index.d.ts +2 -0
  149. package/generated/index.js +3 -0
  150. package/generated/internal/class/cleanup-registry.d.ts +5 -0
  151. package/generated/internal/class/cleanup-registry.js +38 -0
  152. package/generated/internal/class/css-style-observer.d.ts +8 -0
  153. package/generated/internal/class/css-style-observer.js +25 -0
  154. package/generated/internal/class/form-associated-base.d.ts +16 -0
  155. package/generated/internal/class/form-associated-base.js +76 -0
  156. package/generated/internal/class/mutation-observer-set.d.ts +9 -0
  157. package/generated/internal/class/mutation-observer-set.js +42 -0
  158. package/generated/internal/class/mutation-observer-singleton.d.ts +10 -0
  159. package/generated/internal/class/mutation-observer-singleton.js +64 -0
  160. package/generated/internal/class/navigation-path.d.ts +15 -0
  161. package/generated/internal/class/navigation-path.js +84 -0
  162. package/generated/internal/class/navigation-view.d.ts +8 -0
  163. package/generated/internal/class/navigation-view.js +70 -0
  164. package/generated/internal/class/resize-observer-singleton.d.ts +7 -0
  165. package/generated/internal/class/resize-observer-singleton.js +22 -0
  166. package/generated/internal/decorators/adaptive-slot.d.ts +1 -0
  167. package/generated/internal/decorators/adaptive-slot.js +47 -0
  168. package/generated/internal/decorators/custom-element.d.ts +5 -0
  169. package/generated/internal/decorators/custom-element.js +5 -0
  170. package/generated/internal/decorators/index.d.ts +6 -0
  171. package/generated/internal/decorators/index.js +6 -0
  172. package/generated/internal/decorators/microtask-on-connected.d.ts +39 -0
  173. package/generated/internal/decorators/microtask-on-connected.js +67 -0
  174. package/generated/internal/dev-flags.d.ts +4 -0
  175. package/generated/internal/dev-flags.js +1 -0
  176. package/generated/internal/privateNamespace.d.ts +14 -0
  177. package/generated/internal/privateNamespace.js +159 -0
  178. package/generated/internal/utils/$.d.ts +9 -0
  179. package/generated/internal/utils/$.js +11 -0
  180. package/generated/internal/utils/add.d.ts +1 -0
  181. package/generated/internal/utils/add.js +7 -0
  182. package/generated/internal/utils/ancestors.d.ts +1 -0
  183. package/generated/internal/utils/ancestors.js +27 -0
  184. package/generated/internal/utils/clamp.d.ts +1 -0
  185. package/generated/internal/utils/clamp.js +4 -0
  186. package/generated/internal/utils/compare-big-decimals.d.ts +1 -0
  187. package/generated/internal/utils/compare-big-decimals.js +80 -0
  188. package/generated/internal/utils/css-time.d.ts +1 -0
  189. package/generated/internal/utils/css-time.js +3 -0
  190. package/generated/internal/utils/debounce.d.ts +1 -0
  191. package/generated/internal/utils/debounce.js +13 -0
  192. package/generated/internal/utils/debug.d.ts +1 -0
  193. package/generated/internal/utils/debug.js +4 -0
  194. package/generated/internal/utils/define-translations.d.ts +1 -0
  195. package/generated/internal/utils/define-translations.js +3 -0
  196. package/generated/internal/utils/ensure-placeholder.d.ts +1 -0
  197. package/generated/internal/utils/ensure-placeholder.js +41 -0
  198. package/generated/internal/utils/frame.d.ts +1 -0
  199. package/generated/internal/utils/frame.js +6 -0
  200. package/generated/internal/utils/index.d.ts +54 -0
  201. package/generated/internal/utils/index.js +54 -0
  202. package/generated/internal/utils/is-inside.d.ts +1 -0
  203. package/generated/internal/utils/is-inside.js +4 -0
  204. package/generated/internal/utils/kebab-case.d.ts +1 -0
  205. package/generated/internal/utils/kebab-case.js +8 -0
  206. package/generated/internal/utils/list-active.d.ts +7 -0
  207. package/generated/internal/utils/list-active.js +83 -0
  208. package/generated/internal/utils/microtask.d.ts +1 -0
  209. package/generated/internal/utils/microtask.js +3 -0
  210. package/generated/internal/utils/next-all.d.ts +1 -0
  211. package/generated/internal/utils/next-all.js +10 -0
  212. package/generated/internal/utils/next.d.ts +1 -0
  213. package/generated/internal/utils/next.js +9 -0
  214. package/generated/internal/utils/onoff.d.ts +45 -0
  215. package/generated/internal/utils/onoff.js +31 -0
  216. package/generated/internal/utils/prev-all.d.ts +1 -0
  217. package/generated/internal/utils/prev-all.js +10 -0
  218. package/generated/internal/utils/prev.d.ts +1 -0
  219. package/generated/internal/utils/prev.js +9 -0
  220. package/generated/internal/utils/prop.d.ts +18 -0
  221. package/generated/internal/utils/prop.js +30 -0
  222. package/generated/internal/utils/set.d.ts +1 -0
  223. package/generated/internal/utils/set.js +7 -0
  224. package/generated/internal/utils/siblings.d.ts +1 -0
  225. package/generated/internal/utils/siblings.js +10 -0
  226. package/generated/internal/utils/sleep.d.ts +1 -0
  227. package/generated/internal/utils/sleep.js +3 -0
  228. package/generated/internal/utils/slow-hide-show.d.ts +1 -0
  229. package/generated/internal/utils/slow-hide-show.js +16 -0
  230. package/generated/internal/utils/timeout.d.ts +5 -0
  231. package/generated/internal/utils/timeout.js +16 -0
  232. package/generated/internal/utils/touch-glass.d.ts +7 -0
  233. package/generated/internal/utils/touch-glass.js +19 -0
  234. package/generated/internal/vendor/morphdom.d.ts +6 -0
  235. package/generated/internal/vendor/morphdom.js +720 -0
  236. package/generated/morphdom/index.d.ts +8 -0
  237. package/generated/morphdom/index.js +8 -0
  238. package/generated/morphdom/morph.d.ts +4 -0
  239. package/generated/morphdom/morph.js +10 -0
  240. package/generated/morphdom/query-morph.d.ts +5 -0
  241. package/generated/morphdom/query-morph.js +12 -0
  242. package/generated/morphdom/render-to-string.d.ts +12 -0
  243. package/generated/morphdom/render-to-string.js +48 -0
  244. package/generated/morphdom/unsafe-html.d.ts +2 -0
  245. package/generated/morphdom/unsafe-html.js +4 -0
  246. package/generated/namespace-browser/base.d.ts +45 -0
  247. package/generated/namespace-browser/base.js +115 -0
  248. package/generated/namespace-browser/event.d.ts +10 -0
  249. package/generated/namespace-browser/event.js +1 -0
  250. package/generated/namespace-browser/index.d.ts +7 -0
  251. package/generated/namespace-browser/index.js +1 -0
  252. package/generated/snapshot/index.d.ts +9 -0
  253. package/generated/snapshot/index.js +58 -0
  254. package/generated/tpl/html.d.ts +5 -0
  255. package/generated/tpl/html.js +7 -0
  256. package/generated/tpl/index.d.ts +6 -0
  257. package/generated/tpl/index.js +6 -0
  258. package/generated/tpl/is-template-result.d.ts +3 -0
  259. package/generated/tpl/is-template-result.js +1 -0
  260. package/generated/tpl/render.d.ts +2 -0
  261. package/generated/tpl/render.js +132 -0
  262. package/package.json +19 -18
@@ -0,0 +1,1201 @@
1
+ var _a;
2
+ import { I18n } from '../i18n';
3
+ import { CleanupRegistry } from '../internal/class/cleanup-registry';
4
+ import { FormAssociatedBase, getInternals } from '../internal/class/form-associated-base';
5
+ import { MutationObserverSet } from '../internal/class/mutation-observer-set';
6
+ import { NavigationPath } from '../internal/class/navigation-path';
7
+ import { queryInsertPosition, startViewTransition } from '../internal/privateNamespace';
8
+ import { $, ancestors, debug, kebabCase, onoff } from '../internal/utils';
9
+ import { queryMorph } from '../morphdom';
10
+ import { html, render } from '../tpl';
11
+ const pickerStyles = ['menu', 'inline', 'navigation-link', 'sheet', 'radio-group', 'automatic'];
12
+ const update = (path, node, overwrite = true) => {
13
+ debug(`PickerView: update`);
14
+ if (!(path instanceof NavigationPath))
15
+ throw new Error('invalid view');
16
+ const { component, page } = path;
17
+ if (!component)
18
+ return;
19
+ const position = queryInsertPosition(component); //'afterend'
20
+ // const lookFor = 'beforebegin' === position ? 'previousElementSibling' : 'nextElementSibling'
21
+ if (page)
22
+ page.insertAdjacentElement(position, node);
23
+ };
24
+ const reflectSpawnedPage = (current, source) => {
25
+ debug(`PickerView: reflectSpawnedPage`);
26
+ const { page: oldSv, toolBarConfig: oldToolbar } = new NavigationPath(current).hydrate(), { page: newSv, toolBarConfig: newToolbar } = new NavigationPath(source).hydrate();
27
+ const oldList = oldSv?.querySelector('list-view'), newList = newSv?.querySelector('list-view'), oldBackBtn = oldToolbar?.at(0)?.querySelector('button'), newBackBtn = newToolbar?.at(0)?.querySelector('button');
28
+ // pre replace list (always exists)
29
+ const oldOpenedDetails = [...(oldList?.querySelectorAll('list-view details[open]>summary') ?? [])].map((item) => item.textContent.trim());
30
+ // replace list (always exists)
31
+ if (oldList && newList)
32
+ oldList.replaceWith(newList);
33
+ // post replace list (always exists)
34
+ for (const label of oldOpenedDetails)
35
+ for (const summary of newList?.querySelectorAll('list-view details>summary') ?? [])
36
+ if (summary.textContent.trim() === label) {
37
+ summary.parentElement?.setAttribute('open', 'open');
38
+ break;
39
+ }
40
+ // replace backbtns (always exist)
41
+ if (oldBackBtn && newBackBtn)
42
+ oldBackBtn.replaceWith(newBackBtn);
43
+ // overwrite navtitle (always exists)
44
+ oldSv?.setAttribute('navigation-inline-title', newSv?.getAttribute('navigation-inline-title') ?? '');
45
+ // replace search (if exists)
46
+ oldList?.style.setProperty('--list--sticky-block-size', newList?.style.getPropertyValue('--list--sticky-block-size') ?? '');
47
+ // replace search (if exists)
48
+ const oldSearch = oldSv?.querySelector('sticky-container'), newSearch = newSv?.querySelector('sticky-container');
49
+ // if already exists, DO NOTHING dont mess up the input element
50
+ if (newSearch) {
51
+ if (!oldSearch)
52
+ oldSv?.insertAdjacentHTML('afterend', `<sticky-container edge="navbar" style="order: -1" padding>
53
+ <v-stack spacing="0" alignment="fill">
54
+ <input is="search-view" placeholder="Search">
55
+ <!--<button type="button">Filters</button>-->
56
+ </v-stack>
57
+ </sticky-container>`);
58
+ }
59
+ else
60
+ oldSearch?.remove();
61
+ };
62
+ const extractTag = (node) => {
63
+ debug(`PickerView: extractTag`);
64
+ return node instanceof HTMLOptionElement ? (((node.getAttribute('value') ?? node.textContent?.trim()) || node.getAttribute('label')) ?? '') : (node.value ?? '');
65
+ }, extractLabel = (node) => {
66
+ debug(`PickerView: extractLabelFromGroup`);
67
+ if (node instanceof Element)
68
+ if (node instanceof HTMLOptionElement)
69
+ return (node.getAttribute('label') ?? node.getAttribute('value') ?? node.textContent?.trim()) || null;
70
+ else
71
+ return node.getAttribute('DATALIST' === node.tagName ? 'data-label' : 'label');
72
+ else
73
+ return node.title ?? node.value ?? null;
74
+ // return node instanceof Element ? node.getAttribute('DATALIST' === node.tagName ? 'data-label' : 'label') : (node.title ?? node.value ?? null)
75
+ }, extractIcon = (node) => {
76
+ debug(`PickerView: extractIcon`);
77
+ // return node instanceof Element ? node.getAttribute('data-system-image') : (node.systemImage ?? null)
78
+ if (node instanceof Element)
79
+ return node.getAttribute('data-system-image');
80
+ else
81
+ return node.systemImage ?? null;
82
+ }, extractSubtitle = (node) => {
83
+ debug(`PickerView: extractSubtitle`);
84
+ if (node instanceof Element)
85
+ return node.getAttribute('data-subtitle');
86
+ else
87
+ return node.subtitle ?? null;
88
+ };
89
+ const allLeaves = (node) => node.children.every((c) => 0 === c.children.length);
90
+ const parseDictionary = (value) => {
91
+ let dictionary = [];
92
+ try {
93
+ dictionary = JSON.parse(value ?? '[]');
94
+ }
95
+ catch {
96
+ console.error('invalid-dictionary');
97
+ }
98
+ return dictionary;
99
+ };
100
+ const indexGroups = (nodes, parentPath = '') => {
101
+ const map = new Map();
102
+ nodes.forEach((node, i) => {
103
+ const id = parentPath ? `${parentPath}.${i}` : `${i}`;
104
+ map.set(id, node); // no DOM write — works identically for Element and DictEntry
105
+ const children = node instanceof Element ? Array.from(node.children) : node.children;
106
+ for (const [cid, cnode] of indexGroups(children, id))
107
+ map.set(cid, cnode);
108
+ });
109
+ return map;
110
+ };
111
+ const collectLeafValues = (node) => (node.children.length ? node.children.flatMap(collectLeafValues) : [node.value]);
112
+ const flattenDictionary = (tree) => {
113
+ const out1 = {}, out2 = {}, stack = [tree], idx = [0];
114
+ while (stack.length) {
115
+ const frame = stack[stack.length - 1], i = idx[idx.length - 1];
116
+ if (i >= frame.length) {
117
+ stack.pop();
118
+ idx.pop();
119
+ continue;
120
+ }
121
+ idx[idx.length - 1]++;
122
+ const { value, title, systemImage, children } = frame[i];
123
+ out1[value] = title;
124
+ out2[value] = systemImage;
125
+ if (children.length) {
126
+ stack.push(children);
127
+ idx.push(0);
128
+ }
129
+ }
130
+ return { labels: out1, icons: out2 };
131
+ };
132
+ /**
133
+ * @summary A control that selects one value from a set of options.
134
+ *
135
+ * @attr {vertical|horizontal|auto} label-value-placement
136
+ *
137
+ * @attr {boolean} horizontal-radio-group-layout
138
+ *
139
+ * @slot — The default slot
140
+ * @slot label
141
+ * @slot list
142
+ * @slot {HTMLOptionElement[]} validity-options
143
+ */
144
+ export class PickerView extends FormAssociatedBase {
145
+ static get observedAttributes() {
146
+ return [
147
+ 'prompt',
148
+ 'prompt-icon',
149
+ 'label',
150
+ 'name',
151
+ /**
152
+ * @type {menu|inline|navigation-link|sheet|automatic}
153
+ */
154
+ 'picker-style',
155
+ 'selection',
156
+ /**
157
+ * @type {boolean}
158
+ */
159
+ 'searchable',
160
+ 'current-value-label',
161
+ 'current-value-icon',
162
+ /**
163
+ * Adds a help tooltip to the trigger of the picker, if style supports one
164
+ */
165
+ 'help',
166
+ /**
167
+ * Renders all options using this array
168
+ * @type {DictEntry[]}
169
+ */
170
+ 'dictionary',
171
+ /**
172
+ * @type {boolean}
173
+ */
174
+ 'required',
175
+ ];
176
+ }
177
+ static #templates = new Map();
178
+ #guuid = self.crypto.randomUUID();
179
+ #spawn;
180
+ #makeGroupClickHandler(el, groupId) {
181
+ return async (evt) => {
182
+ evt.stopImmediatePropagation();
183
+ evt.preventDefault();
184
+ const { target } = evt;
185
+ if (!(target instanceof HTMLElement))
186
+ return;
187
+ const newPage = this.#spawnPage(el instanceof Element ? Array.from(el.children) : el.children, 'content-view', groupId, this.hasAttribute('searchable'), extractLabel(el));
188
+ if (!newPage)
189
+ return;
190
+ newPage.dataset.groupId = groupId; // <-- tag the spawned page
191
+ const path = new NavigationPath(target)?.hydrate();
192
+ await startViewTransition(target, 'forwards', async () => {
193
+ update(path, newPage);
194
+ this.#reflectSelectionOnButtons();
195
+ });
196
+ };
197
+ }
198
+ #spawnPage = (elements, tag, parentGroupId, searchable = false, title) => {
199
+ debug(`${_a.name} #spawnPage`);
200
+ const body = tag === 'sheet-view'
201
+ ? $(html `<dialog is="sheet-view">
202
+ <scroll-view>
203
+ <v-stack placement="leading fill">
204
+ <list-view preferred-expanded-style="inset"></list-view>
205
+ </v-stack>
206
+ </scroll-view>
207
+ <tool-bar>
208
+ <tool-bar-item slot="top-bar-leading">
209
+ <button type="button" tabindex="0">
210
+ <label-view system-image="x"></label-view>
211
+ </button>
212
+ </tool-bar-item>
213
+ </tool-bar>
214
+ </dialog>`, '>1')
215
+ : $(html `<content-view>
216
+ <scroll-view>
217
+ <v-stack placement="leading fill">
218
+ <list-view preferred-expanded-style="inset"></list-view>
219
+ </v-stack>
220
+ </scroll-view>
221
+ <tool-bar>
222
+ <tool-bar-item slot="top-bar-leading">
223
+ <button type="button" tabindex="0">
224
+ <label-view system-image="caret-left"></label-view>
225
+ </button>
226
+ </tool-bar-item>
227
+ </tool-bar>
228
+ </content-view>`, '>1'), sv = body.querySelector('scroll-view'), list = body.querySelector('list-view'), backBtn = body.querySelector('button');
229
+ if (title)
230
+ sv?.setAttribute('navigation-inline-title', title);
231
+ if (searchable) {
232
+ list?.style.setProperty('--list--sticky-block-size', '70px');
233
+ list?.insertAdjacentHTML('afterend', `<sticky-container edge="navbar" style="order: -1" padding>
234
+ <v-stack spacing="0" alignment="fill">
235
+ <input is="search-view" placeholder="Search">
236
+ <!--<button type="button">Filters</button>-->
237
+ </v-stack>
238
+ </sticky-container>`);
239
+ const searchInput = sv?.querySelector('input'); // FIXME: compoennt is=search-view??
240
+ searchInput?.addEventListener('focus', ({ target }) => {
241
+ if (this.#spawn)
242
+ this.dispatchEvent(new CustomEvent('picker:searchfocus', {
243
+ detail: { element: this.#spawn, search: target instanceof HTMLInputElement ? target.value : '' },
244
+ bubbles: true,
245
+ composed: true,
246
+ }));
247
+ });
248
+ searchInput?.addEventListener('blur', ({ target }) => {
249
+ if (this.#spawn)
250
+ this.dispatchEvent(new CustomEvent('picker:searchblur', {
251
+ detail: { element: this.#spawn, search: target instanceof HTMLInputElement ? target.value : '' },
252
+ bubbles: true,
253
+ composed: true,
254
+ }));
255
+ });
256
+ searchInput?.addEventListener('input', ({ target }) => {
257
+ if (this.#spawn)
258
+ this.dispatchEvent(new CustomEvent('picker:searchinput', {
259
+ detail: { element: this.#spawn, search: target instanceof HTMLInputElement ? target.value : '' },
260
+ bubbles: true,
261
+ composed: true,
262
+ }));
263
+ });
264
+ }
265
+ backBtn?.setAttribute('help', I18n.t('ButtonRole').Back);
266
+ backBtn?.addEventListener('click', async (evt) => {
267
+ evt.stopImmediatePropagation();
268
+ evt.preventDefault();
269
+ const { target } = evt;
270
+ if (!(target instanceof HTMLElement))
271
+ return;
272
+ const { component } = new NavigationPath(target)?.hydrate();
273
+ await startViewTransition(target, 'backwards', async () => {
274
+ component?.remove();
275
+ });
276
+ });
277
+ for (const [i, el] of elements.entries()) {
278
+ if (el instanceof Element)
279
+ switch (el.tagName) {
280
+ case 'OPTGROUP': {
281
+ const group = this.#wrapOptgroupTag(el);
282
+ this.#reflectButtons([...el.children], group);
283
+ for (const btn of group.querySelectorAll(':scope>button'))
284
+ btn.addEventListener('click', this.#handlePageBtnClick);
285
+ list?.appendChild(group);
286
+ break;
287
+ }
288
+ case 'OPTION': {
289
+ const btn = this.#wrapOptionTag(el);
290
+ btn.addEventListener('click', this.#handlePageBtnClick);
291
+ list?.appendChild(btn);
292
+ break;
293
+ }
294
+ default: {
295
+ const btn = _a.#wrapOptionSpawnTag(el);
296
+ const groupId = parentGroupId ? `${parentGroupId}.${i}` : `${i}`;
297
+ // const btn = $<HTMLButtonElement>(
298
+ // `<button type="button" tabindex="0" navigation-link><h-stack distribution="leading" template="auto spacer"><label-view data-role="check" style="visibility: hidden"><image-view slot="icon" system-name="check"></image-view></label-view><label-view><span></span></label-view></h-stack></button>`,
299
+ // '>1'
300
+ // ),
301
+ // label = btn.querySelector<LabelView>(':scope>h-stack>label-view:nth-child(2)')
302
+ // const node = el as HTMLElement
303
+ // if (label && node.dataset.label) renderLabelTitle(label, node.dataset.label) //label?.setAttribute('title', el.dataset.label)
304
+ btn.dataset.groupId = groupId;
305
+ btn.addEventListener('click', this.#makeGroupClickHandler(el, groupId));
306
+ list?.appendChild(btn);
307
+ break;
308
+ }
309
+ }
310
+ else {
311
+ if (el.children.length)
312
+ if (allLeaves(el)) {
313
+ const group = this.#wrapOptgroupTag(el);
314
+ this.#reflectButtons(el.children, group);
315
+ for (const btn of group.querySelectorAll(':scope>button'))
316
+ btn.addEventListener('click', this.#handlePageBtnClick);
317
+ list?.appendChild(group);
318
+ }
319
+ else {
320
+ const btn = _a.#wrapOptionSpawnTag(el);
321
+ const groupId = parentGroupId ? `${parentGroupId}.${i}` : `${i}`;
322
+ btn.dataset.groupId = groupId;
323
+ btn.addEventListener('click', this.#makeGroupClickHandler(el, groupId));
324
+ list?.appendChild(btn);
325
+ }
326
+ else {
327
+ const btn = this.#wrapOptionTag(el);
328
+ btn.addEventListener('click', this.#handlePageBtnClick);
329
+ list?.appendChild(btn);
330
+ }
331
+ }
332
+ }
333
+ return body;
334
+ };
335
+ #resyncSpawnedPages = (freshRoot) => {
336
+ if (!this.#spawn)
337
+ return;
338
+ const groupMap = indexGroups(freshRoot);
339
+ for (const el of this.#spawn.querySelectorAll('content-view')) {
340
+ const depth = ancestors('content-view,[is=sheet-view]', el).indexOf(this.#spawn);
341
+ if (0 >= depth)
342
+ continue;
343
+ const groupId = el.dataset.groupId;
344
+ const source = groupId ? groupMap.get(groupId) : undefined;
345
+ if (!source) {
346
+ el.remove();
347
+ continue;
348
+ }
349
+ const children = source instanceof Element ? Array.from(source.children) : source.children;
350
+ const title = source instanceof Element ? extractLabel(source) : (source.title ?? null);
351
+ const newPage = this.#spawnPage(children, 'content-view', groupId, this.hasAttribute('searchable'), title);
352
+ newPage.dataset.groupId = groupId;
353
+ reflectSpawnedPage(el, newPage);
354
+ }
355
+ };
356
+ #renderButtons(input) {
357
+ debug(`${_a.name} #renderButtons`);
358
+ switch (input.mode) {
359
+ case 'dictionary': {
360
+ const { labels, icons } = flattenDictionary(input.source);
361
+ this.#lastRenderedLabelMap = labels;
362
+ this.#lastRenderedIconMap = icons;
363
+ // const collectLeafValues = (node: DictEntry): string[] => (node.children.length ? node.children.flatMap(collectLeafValues) : [node.value])
364
+ // collectGroups = (nodes: DictEntry[]): string[][] => {
365
+ // const groups: string[][] = []
366
+ // const walk = (list: DictEntry[]) => {
367
+ // for (const node of list) {
368
+ // if (!node.children.length) continue
369
+ // if (!allLeaves(node)) groups.push(collectLeafValues(node))
370
+ // walk(node.children)
371
+ // }
372
+ // }
373
+ // walk(nodes)
374
+ // return groups
375
+ // }
376
+ this.#lastIndexedRoot = input.source; //this.#lastRenderedGroupMap = collectGroups(dictionary)
377
+ break;
378
+ }
379
+ case 'list': {
380
+ this.#lastRenderedLabelMap = {};
381
+ this.#lastRenderedIconMap = {};
382
+ for (const el of input.source)
383
+ if (el.matches('option')) {
384
+ const k = extractTag(el);
385
+ this.#lastRenderedLabelMap[k] = el.getAttribute('label') ?? undefined;
386
+ this.#lastRenderedIconMap[k] = extractIcon(el) ?? undefined;
387
+ }
388
+ else
389
+ for (const opt of el.querySelectorAll('option')) {
390
+ const k = extractTag(opt);
391
+ this.#lastRenderedLabelMap[k] = opt.getAttribute('label') ?? undefined;
392
+ this.#lastRenderedIconMap[k] = extractIcon(opt) ?? undefined;
393
+ }
394
+ this.#lastIndexedRoot = input.source;
395
+ // this.#lastRenderedGroupMap = assigned.flatMap<string[]>((el) =>
396
+ // [...(el.matches('datalist') ? [el as HTMLDataListElement] : []), ...el.querySelectorAll<HTMLDataListElement>('datalist')].map((dl) =>
397
+ // Array.from(dl.options).map((opt) => extractTagFromOption(opt))
398
+ // )
399
+ // )
400
+ break;
401
+ }
402
+ }
403
+ switch (this.pickerStyle) {
404
+ case 'sheet':
405
+ case 'navigation-link': {
406
+ // current value label only
407
+ const currentValueLabel = this.querySelector(':scope>label-view:not([slot])') ?? this.appendChild($(html `<label-view></label-view>`, '>1'));
408
+ // reset state
409
+ // if (currentValueLabel) {
410
+ // renderLabelIcon(currentValueLabel, 'dots-three') // overwritten
411
+ // renderLabelTitle(currentValueLabel, this.#currentValueLabel) // overwritten
412
+ // }
413
+ // clear all siblings
414
+ for (const el of this.querySelectorAll(':scope>:not([slot])'))
415
+ if (currentValueLabel !== el)
416
+ el.remove();
417
+ CleanupRegistry.unregister(this, 'trigger');
418
+ CleanupRegistry.register(this, onoff('click', this.#handleTriggerClick, currentValueLabel).on(), 'trigger');
419
+ if (this.hasAttribute('help'))
420
+ currentValueLabel?.setAttribute('help', this.getAttribute('help') ?? '');
421
+ // rebuild snapshot(tree)
422
+ if (!this.#spawn)
423
+ break;
424
+ // rerender level 0
425
+ reflectSpawnedPage(this.#spawn, this.#spawnPage(input.source, 'DIALOG' === this.#spawn.tagName ? 'sheet-view' : 'content-view', undefined, this.hasAttribute('searchable'), this.getAttribute('label')));
426
+ this.#resyncSpawnedPages(input.source);
427
+ // // FIXME:
428
+ // for (const el of this.#spawn.querySelectorAll<HTMLElement>('content-view')) {
429
+ // const depth = $.ancestors('content-view,[is=sheet-view]', el).indexOf(this.#spawn)
430
+ // if (0 >= depth) continue
431
+ // const datalist = this.querySelector<HTMLElement>(`:scope>${Array.from({ length: depth }, () => 'datalist').join('>')}`)
432
+ // if (!datalist) {
433
+ // el.remove()
434
+ // break
435
+ // }
436
+ // reflectSpawnedElement(el, this.#spawnPage(Array.from(datalist.children), 'content-view', this.hasAttribute('searchable'), datalist.dataset.label))
437
+ // }
438
+ break;
439
+ }
440
+ case 'menu': {
441
+ const menu = this.querySelector(':scope>menu-view:not([slot])') ?? this.appendChild($(html `<menu-view></menu-view>`, '>1'));
442
+ // reset state
443
+ menu.innerHTML = '';
444
+ menu.tabIndex = 0;
445
+ // clear all siblings
446
+ for (const el of this.querySelectorAll(':scope>:not([slot])'))
447
+ if (menu !== el)
448
+ el.remove();
449
+ const currentValueLabel = menu.querySelector(':scope>label-view[slot=label]') ?? menu.appendChild($(html `<label-view slot="label"></label-view>`, '>1'));
450
+ // if (currentValueLabel) {
451
+ // renderLabelIcon(currentValueLabel, 'dots-three') // overwritten
452
+ // renderLabelTitle(currentValueLabel, this.#currentValueLabel) // overwritten
453
+ // }
454
+ if (this.hasAttribute('help'))
455
+ currentValueLabel?.setAttribute('help', this.getAttribute('help') ?? '');
456
+ this.#reflectButtons(input.source, menu);
457
+ break;
458
+ }
459
+ case 'radio-group': {
460
+ // capture
461
+ // const stack = this.querySelector<HTMLElement>(':scope>v-stack:not([slot])') ?? this.appendChild<HTMLElement>($(html`<v-stack></v-stack>`, '>1'))
462
+ // reset state
463
+ // stack.innerHTML = ''
464
+ // clear all siblings
465
+ for (const el of this.querySelectorAll(':scope>:not([slot])'))
466
+ el.remove();
467
+ // add buttons
468
+ this.#reflectButtons(input.source, this);
469
+ break;
470
+ }
471
+ case 'inline':
472
+ default: {
473
+ const sectionTpl = html `<section-view></section-view>`;
474
+ const inlineList = this.querySelector(':scope>list-view:not([slot])') ?? this.appendChild($(html `<list-view>${sectionTpl}</list-view>`, '>1')), section = inlineList.querySelector(':scope>section-view') ?? inlineList.appendChild($(sectionTpl, '>1'));
475
+ // reset state
476
+ section.innerHTML = '';
477
+ // clear all siblings
478
+ for (const el of this.querySelectorAll(':scope>:not([slot])'))
479
+ if (inlineList !== el)
480
+ el.remove();
481
+ // add label as a plain element
482
+ const value = this.getAttribute('label');
483
+ if (value) {
484
+ const hStack = $(html `<h-stack distribution="leading" template="auto spacer">
485
+ <label-view data-role="check" style="visibility: hidden">
486
+ <image-view slot="icon" system-name="check"></image-view>
487
+ </label-view>
488
+ <label-view><span></span></label-view>
489
+ </h-stack>`, '>1');
490
+ queryMorph('label-view:nth-child(2)', html `<label-view>${value ? html `<span>${value}</span>` : null}</label-view>`, hStack);
491
+ // if (label) renderLabelTitle(label, value) //label.setAttribute('title', value)
492
+ section.insertAdjacentElement('beforeend', hStack);
493
+ }
494
+ if ('dictionary' === input.mode)
495
+ this.#reflectButtons(input.source.filter((el) => 0 === el.children.length), section);
496
+ else
497
+ this.#reflectButtons(input.source.filter((el) => el.matches('option')), section);
498
+ break;
499
+ }
500
+ }
501
+ this.#reflectSelectionOnButtons();
502
+ this.#reflectSelectionOnCurrentValueLabel();
503
+ }
504
+ // #renderDictionary = (dictionary: Dictionary) => {
505
+ // debug(`${PickerView.name} ⚡️ mutation`)
506
+ // this.#renderButtons({ mode: 'dictionary', source: dictionary })
507
+ // }
508
+ #renderSlotted = (entries) => {
509
+ debug(`${_a.name} ⚡️ mutation`);
510
+ this.#renderButtons(this.#resolveSource()); //this.#renderButtons({ mode: 'list', source: this.#slots?.get('list')?.assignedElements({ flatten: true }) ?? [] })
511
+ };
512
+ #renderValidityMsgs = (entries) => {
513
+ debug(`${_a.name} ⚡️ mutation`);
514
+ this.setValidity(this.validity, this.validationMessage);
515
+ };
516
+ #lastRenderedStyle; //string | null
517
+ #shadowRoot;
518
+ #slots = new Map();
519
+ #validityObservers = new MutationObserverSet(this.#renderValidityMsgs);
520
+ #observers = new MutationObserverSet(this.#renderSlotted);
521
+ #customValidity = '';
522
+ #selection = '';
523
+ get selection() {
524
+ return this.#selection;
525
+ }
526
+ set selection(v) {
527
+ if (Object.is(this.#selection, v))
528
+ return;
529
+ this.#selection = v;
530
+ this.#reflectSelectionOnButtons();
531
+ this.#reflectSelectionOnCurrentValueLabel();
532
+ this.#sendValueToForm();
533
+ }
534
+ #lastRenderedLabelMap = {};
535
+ #lastRenderedIconMap = {};
536
+ #lastIndexedRoot = [];
537
+ get #currentValueLabel() {
538
+ return ((this.getAttribute('current-value-label') ?? '').replaceAll('{{selection}}', this.#selection).replaceAll('{{currentValueLabel}}', this.#selection) || this.#lastRenderedLabelMap[this.#selection]);
539
+ }
540
+ get #currentValueIcon() {
541
+ return (this.getAttribute('current-value-icon') ?? '') || this.#lastRenderedIconMap[this.#selection];
542
+ }
543
+ get #internals() {
544
+ return getInternals(this);
545
+ }
546
+ get template() {
547
+ if (!_a.#templates.has(this.pickerStyle))
548
+ switch (this.pickerStyle) {
549
+ case 'sheet':
550
+ case 'navigation-link':
551
+ _a.#templates.set(this.pickerStyle, $(html `
552
+ <label part="root picker-stack">
553
+ <div part="root picker-label-stack">
554
+ <slot name="label"></slot>
555
+ </div>
556
+ <div part="root picker-input-stack">
557
+ <slot></slot>
558
+ </div>
559
+ <slot name="list" hidden></slot>
560
+ <slot name="validity-options" hidden></slot>
561
+ </label>
562
+ `));
563
+ break;
564
+ case 'menu':
565
+ _a.#templates.set(this.pickerStyle, $(html `
566
+ <label part="root picker-stack">
567
+ <div part="root picker-label-stack">
568
+ <slot name="label"></slot>
569
+ </div>
570
+ <div part="root picker-input-stack">
571
+ <slot></slot>
572
+ </div>
573
+ <slot name="list" hidden></slot>
574
+ <slot name="validity-options" hidden></slot>
575
+ </label>
576
+ `));
577
+ break;
578
+ case 'radio-group':
579
+ _a.#templates.set(this.pickerStyle, $(html `
580
+ <label part="root picker-stack">
581
+ <div part="root picker-label-stack">
582
+ <slot name="label"></slot>
583
+ </div>
584
+ <div part="root picker-input-stack">
585
+ <slot></slot>
586
+ </div>
587
+ <slot name="list" hidden></slot>
588
+ <slot name="validity-options" hidden></slot>
589
+ </label>
590
+ `));
591
+ break;
592
+ // break
593
+ // case 'compact':
594
+ // PickerView.#templates.set(
595
+ // style,
596
+ // Object.assign(document.createElement('template'), {
597
+ // innerHTML: String.raw`
598
+ // <label part="compact-picker">
599
+ // <input type="text" part="compact-input">
600
+ // </label>
601
+ // `,
602
+ // })
603
+ // )
604
+ // break
605
+ // case 'fancy':
606
+ // PickerView.#templates.set(
607
+ // style,
608
+ // Object.assign(document.createElement('template'), {
609
+ // innerHTML: String.raw`
610
+ // <div part="fancy-picker">
611
+ // <span>Fancy Picker</span>
612
+ // <input type="text" part="fancy-input">
613
+ // </div>
614
+ // `,
615
+ // })
616
+ // )
617
+ // case 'gg':
618
+ // PickerView.#templates.set(
619
+ // style,
620
+ // Object.assign(document.createElement('template'), {
621
+ // innerHTML: String.raw`
622
+ // <label part="root text-field-stack">
623
+ // <div part="root text-field-label-stack">
624
+ // <slot name="label"></slot>
625
+ // </div>
626
+ // <div part="root text-field-input-stack">
627
+ // <input type="text" part="root input text-field-form-input" list="tickmarks">
628
+ // <datalist id="tickmarks">
629
+ // <option value="0" label="0%"></option>
630
+ // </datalist>
631
+ // </div>
632
+ // <slot name="list" hidden></slot>
633
+ // </label>`,
634
+ // })
635
+ // )
636
+ // break
637
+ case 'inline':
638
+ default:
639
+ _a.#templates.set(this.pickerStyle, $(html ` <label part="root picker-stack">
640
+ <div part="root picker-label-stack">
641
+ <slot name="label"></slot>
642
+ </div>
643
+ <div part="root picker-input-stack">
644
+ <slot></slot>
645
+ </div>
646
+ <slot name="list" hidden></slot>
647
+ <slot name="validity-options" hidden></slot>
648
+ </label>`));
649
+ break;
650
+ }
651
+ return _a.#templates.get(this.pickerStyle);
652
+ }
653
+ constructor() {
654
+ super();
655
+ this.#shadowRoot = this.attachShadow({ mode: 'closed' });
656
+ }
657
+ attributeChangedCallback(name, oldValue, newValue) {
658
+ debug(`${_a.name} ⚡️ attr-change [${name}] ("${oldValue}" → "${newValue}")`);
659
+ switch (name) {
660
+ // case 'horizontal-radio-group-layout':
661
+ // if (oldValue === newValue) break
662
+ // this.#renderSlotted([])
663
+ // break
664
+ case 'prompt':
665
+ case 'prompt-icon':
666
+ if (oldValue === newValue)
667
+ break;
668
+ this.#reflectSelectionOnCurrentValueLabel(); //this.#reflectPlaceholder(newValue)
669
+ break;
670
+ case 'label':
671
+ this.#reflectLabel(newValue);
672
+ // this.#sendValueToForm()
673
+ break;
674
+ case 'picker-style':
675
+ if (oldValue === newValue)
676
+ break;
677
+ this.#render();
678
+ break;
679
+ case 'searchable':
680
+ if (oldValue === newValue)
681
+ break;
682
+ this.#renderSlotted([]);
683
+ break;
684
+ case 'current-value-label':
685
+ case 'current-value-icon':
686
+ // if (oldValue === newValue) break
687
+ this.#reflectSelectionOnCurrentValueLabel();
688
+ break;
689
+ case 'help':
690
+ // if (oldValue === newValue) break
691
+ this.#reflectTriggerHelp();
692
+ break;
693
+ case 'dictionary':
694
+ if (oldValue === newValue)
695
+ break;
696
+ this.#renderSlotted([]); // re-resolve: dictionary present → wins; removed → falls back to slotted //this.#renderDictionary(parseDictionary(newValue))
697
+ break;
698
+ case 'name':
699
+ case 'required':
700
+ if (oldValue === newValue)
701
+ break;
702
+ this.#sendValueToForm(false);
703
+ break;
704
+ case 'selection':
705
+ if (oldValue === newValue)
706
+ break;
707
+ this.#selection = newValue ?? '';
708
+ // FIXME: What about other callbacks from this.#selection=???? Somehow they run correctly
709
+ this.#sendValueToForm(false);
710
+ break;
711
+ }
712
+ }
713
+ disconnectedCallback() {
714
+ debug(`${_a.name} ⚡️ disconnect`);
715
+ CleanupRegistry.unregister(this);
716
+ this.#validityObservers.unobserveAll();
717
+ this.#observers.unobserveAll();
718
+ }
719
+ connectedCallback() {
720
+ debug(`${_a.name} ⚡️ connect`);
721
+ CleanupRegistry.register(this, onoff('localechange', () => {
722
+ this.#renderSlotted([]);
723
+ }, I18n.on).on());
724
+ CleanupRegistry.register(this, onoff('click', this.#handleClick, this).on());
725
+ CleanupRegistry.register(this, onoff('input', this.#handleInput, this).on());
726
+ if (!this.hasAttribute('picker-style'))
727
+ this.#render(); // skip if already rendered by attr-change during upgrade!
728
+ // finally
729
+ if (!this.hasAttribute('selection'))
730
+ return;
731
+ this.#selection = this.getAttribute('selection') ?? '';
732
+ this.#sendValueToForm(false);
733
+ }
734
+ get pickerStyle() {
735
+ const attr = 'picker-style';
736
+ return pickerStyles.includes(this.getAttribute(attr) ?? '') ? this.getAttribute(attr) : 'automatic';
737
+ }
738
+ #resolveSource() {
739
+ if (this.hasAttribute('dictionary'))
740
+ return { mode: 'dictionary', source: parseDictionary(this.getAttribute('dictionary')) };
741
+ return { mode: 'list', source: this.#slots?.get('list')?.assignedElements({ flatten: true }) ?? [] };
742
+ }
743
+ #render() {
744
+ debug(`${_a.name} ⚡️ #render (${this.pickerStyle})`);
745
+ // const style = this.getAttribute('picker-style')
746
+ if (this.#lastRenderedStyle === this.pickerStyle)
747
+ return; // skip if already applied
748
+ this.#lastRenderedStyle = this.pickerStyle;
749
+ // clear shadow DOM
750
+ this.#shadowRoot.replaceChildren(document.importNode(this.template, true));
751
+ CleanupRegistry.unregister(this, 'slots');
752
+ for (const slot of this.#shadowRoot.querySelectorAll('slot'))
753
+ this.#slots?.set(slot.name, slot);
754
+ CleanupRegistry.register(this, () => {
755
+ this.#slots = new Map();
756
+ }, 'slots');
757
+ CleanupRegistry.unregister(this, 'validities');
758
+ CleanupRegistry.register(this, onoff('slotchange', this.#handleValiditiesSlotchange, this.#slots?.get('validity-options')).on(), 'validities');
759
+ CleanupRegistry.unregister(this, 'datalist'); //off1()
760
+ CleanupRegistry.register(this, onoff('slotchange', this.#handleListSlotchange, this.#slots?.get('list')).on(), 'datalist');
761
+ // #renderSlotted should run automatically now by slotchange initial event
762
+ // switch (this.pickerStyle) {
763
+ // }
764
+ }
765
+ #sendValueToForm = (dispatchEvent = true) => {
766
+ // input.value has already been updated/synced !!
767
+ if (this.matches(':disabled'))
768
+ return this.setValidity({});
769
+ if (this.hasAttribute('required')) {
770
+ if (!this.#selection) {
771
+ this.setValidity({ badInput: true }, 'invalid-selection');
772
+ }
773
+ else
774
+ this.setValidity({});
775
+ }
776
+ else
777
+ this.setValidity({});
778
+ const entries = new FormData();
779
+ entries.append(this.name, this.#selection);
780
+ this.#internals.setFormValue(entries);
781
+ if (dispatchEvent)
782
+ this.dispatchEvent(new CustomEvent('selection', { detail: { selection: this.#selection }, bubbles: true, composed: true }));
783
+ };
784
+ #handleTriggerClick = async ({ type, target }) => {
785
+ debug(`${_a.name} ⚡️ ${type}`);
786
+ if (!(target instanceof HTMLElement))
787
+ return;
788
+ this.#spawn?.remove?.();
789
+ const { source } = this.#resolveSource();
790
+ const level0 = this.#spawnPage(source, // this.hasAttribute('dictionary') ? parseDictionary(this.getAttribute('dictionary')) : (this.#slots?.get('list')?.assignedElements() ?? []),
791
+ 'sheet' === this.pickerStyle ? 'sheet-view' : 'content-view', undefined, this.hasAttribute('searchable'), this.getAttribute('label'));
792
+ if (!level0)
793
+ return;
794
+ const path = new NavigationPath(target)?.hydrate();
795
+ await startViewTransition(target, 'forwards', async () => {
796
+ this.#spawn = level0;
797
+ update(path, level0);
798
+ this.#reflectSelectionOnButtons();
799
+ });
800
+ };
801
+ #handlePageBtnClick = async (evt) => {
802
+ debug(`${_a.name} ⚡️ ${evt?.type}`);
803
+ evt.stopImmediatePropagation();
804
+ evt.preventDefault();
805
+ const { currentTarget: btn } = evt;
806
+ if (!(btn instanceof HTMLElement))
807
+ return;
808
+ // const btn = target.closest('button')
809
+ // if (!btn) return
810
+ const proceed = () => {
811
+ this.#spawn?.remove();
812
+ this.#selection = btn.getAttribute('value') ?? '';
813
+ this.#reflectSelectionOnButtons();
814
+ this.#reflectSelectionOnCurrentValueLabel();
815
+ this.#sendValueToForm();
816
+ };
817
+ const { body } = new NavigationPath(this.#spawn)?.hydrate();
818
+ if (!body)
819
+ return proceed();
820
+ await startViewTransition(body, 'backwards', async () => {
821
+ proceed();
822
+ });
823
+ };
824
+ #handleClick({ type, target }) {
825
+ debug(`${_a.name} ⚡️ ${type}`);
826
+ if (!(target instanceof HTMLElement))
827
+ return;
828
+ const btn = target.closest('button');
829
+ if (!btn)
830
+ return;
831
+ this.#selection = btn.getAttribute('value') ?? '';
832
+ this.#reflectSelectionOnButtons();
833
+ this.#reflectSelectionOnCurrentValueLabel();
834
+ this.#sendValueToForm();
835
+ }
836
+ #handleInput({ type, target }) {
837
+ debug(`${_a.name} ⚡️ ${type}`);
838
+ if (!(target instanceof HTMLElement))
839
+ return;
840
+ switch (this.pickerStyle) {
841
+ case 'radio-group':
842
+ const radio = target.closest('input[type="radio"]');
843
+ if (!radio)
844
+ return;
845
+ this.#selection = radio.getAttribute('value') ?? '';
846
+ this.#reflectSelectionOnButtons();
847
+ this.#reflectSelectionOnCurrentValueLabel();
848
+ this.#sendValueToForm();
849
+ break;
850
+ }
851
+ }
852
+ #handleValiditiesSlotchange = ({ type, target: slot }) => {
853
+ debug(`${_a.name} ⚡️ ${type}`);
854
+ if (!(slot instanceof HTMLSlotElement))
855
+ return;
856
+ const assigned = slot.assignedElements();
857
+ this.#validityObservers.syncObservations(assigned, ['value', 'label']);
858
+ // if (0 < assigned.length)
859
+ this.#renderValidityMsgs([]);
860
+ };
861
+ #handleListSlotchange = ({ type, target: slot }) => {
862
+ debug(`${_a.name} ⚡️ ${type}`);
863
+ if (!(slot instanceof HTMLSlotElement))
864
+ return;
865
+ const assigned = slot.assignedElements();
866
+ this.#observers.syncObservations(assigned);
867
+ // if (0 < assigned.length)
868
+ this.#renderSlotted([]);
869
+ };
870
+ static #wrapOptionSpawnTag(node) {
871
+ debug(`${_a.name} #wrapOptionSpawnTag`);
872
+ const title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
873
+ const mount = document.createElement('div');
874
+ render(html `<button type="button" tabindex="0" navigation-link>
875
+ <h-stack distribution="leading" template="auto spacer">
876
+ <label-view data-role="check" style="visibility: hidden">
877
+ <image-view slot="icon" system-name="check"></image-view>
878
+ </label-view>
879
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
880
+ </h-stack>
881
+ </button>`, mount);
882
+ return mount.firstElementChild; //btn
883
+ }
884
+ #wrapOptionTag(node) {
885
+ debug(`${_a.name} #wrapOptionTag`);
886
+ const tag = extractTag(node), title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
887
+ const mount = document.createElement('div');
888
+ switch (this.pickerStyle) {
889
+ case 'radio-group':
890
+ render(_a.#morphRadioGroupBtn({ name: this.#guuid, title, subtitle, icon, tag }), mount);
891
+ return mount.firstElementChild; //btn
892
+ default:
893
+ render(html `<button type="button" tabindex="0" value="${tag}">
894
+ <h-stack distribution="leading" template="auto spacer">
895
+ <label-view data-role="check">
896
+ <image-view slot="icon" system-name="check"></image-view>
897
+ </label-view>
898
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
899
+ </h-stack>
900
+ </button>`, mount);
901
+ return mount.firstElementChild; //btn
902
+ }
903
+ }
904
+ static #morphRadioGroupGroup({ name, title, subtitle, icon }) {
905
+ return html `<label>
906
+ <h-stack distribution="fill" template="auto spacer" spacing="5">
907
+ <input type="radio" name="${name}" style="min-inline-size: 20px; margin: 0" disabled />
908
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
909
+ </h-stack>
910
+ </label>`;
911
+ }
912
+ static #morphRadioGroupBtn({ name, title, subtitle, icon, tag }) {
913
+ return html `<label>
914
+ <h-stack distribution="fill" template="auto spacer" spacing="5">
915
+ <input type="radio" name="${name}" value="${tag}" style="min-inline-size: 20px; margin: 0" />
916
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
917
+ </h-stack>
918
+ </label>`;
919
+ }
920
+ static #morphITSLabel({ title, subtitle, icon }) {
921
+ return html `<h-stack distribution="fill" template="${icon ? 'auto spacer' : null}">
922
+ ${icon ? html `<label-view system-image="${icon}"></label-view>` : null}
923
+ <v-stack spacing="3" alignment="fill">
924
+ <label-view title="${title}"></label-view>
925
+ ${subtitle ? html `<label-view foreground="secondary" font="callout" title="${subtitle}"></label-view>` : null}
926
+ </v-stack>
927
+ </h-stack>`;
928
+ }
929
+ #wrapOptgroupTag(node) {
930
+ debug(`${_a.name} #wrapOptgroupTag`);
931
+ const title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
932
+ const mount = document.createElement('div');
933
+ switch (this.pickerStyle) {
934
+ case 'radio-group':
935
+ render(_a.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon }),
936
+ // this.hasAttribute('horizontal-radio-group-layout')
937
+ // ? html`<h-flex-stack style="flex-wrap: wrap">${PickerView.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon })}</h-flex-stack>`
938
+ // : html`<v-stack>${PickerView.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon })}</v-stack>`,
939
+ mount);
940
+ return mount.firstElementChild;
941
+ default:
942
+ render(html `<details is="disclosure-group" disclosure-style="marker-trailing">
943
+ <summary>
944
+ <h-stack distribution="leading" template="auto spacer">
945
+ <label-view data-role="check" style="visibility: hidden">
946
+ <image-view slot="icon" system-name="check"></image-view>
947
+ </label-view>
948
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
949
+ </h-stack>
950
+ </summary>
951
+ </details>`, mount);
952
+ return mount.firstElementChild;
953
+ }
954
+ }
955
+ #wrapDatalistTag(node) {
956
+ const title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
957
+ const mount = document.createElement('div');
958
+ switch (this.pickerStyle) {
959
+ case 'radio-group':
960
+ render(_a.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon }),
961
+ // this.hasAttribute('horizontal-radio-group-layout')
962
+ // ? html`<h-flex-stack style="flex-wrap: wrap">${PickerView.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon })}</h-flex-stack>`
963
+ // : html`<v-stack>${PickerView.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon })}</v-stack>`,
964
+ mount);
965
+ return mount.firstElementChild;
966
+ default:
967
+ render(html `<menu-view tabindex="0">
968
+ <h-stack slot="label" distribution="leading" template="auto spacer">
969
+ <label-view data-role="check" style="visibility: hidden">
970
+ <image-view slot="icon" system-name="check"></image-view>
971
+ </label-view>
972
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
973
+ </h-stack>
974
+ </menu-view>`, mount);
975
+ return mount.firstElementChild;
976
+ }
977
+ }
978
+ #reflectButtons(nodes, container) {
979
+ debug(`${_a.name} #reflectButtons`);
980
+ const flatten = 'radio-group' === this.pickerStyle;
981
+ for (const node of nodes)
982
+ if (node instanceof Element)
983
+ switch (node.tagName) {
984
+ case 'DATALIST': {
985
+ if (flatten) {
986
+ this.#reflectButtons([...node.children], container);
987
+ break;
988
+ }
989
+ const group = this.#wrapDatalistTag(node);
990
+ this.#reflectButtons([...node.children], group);
991
+ container.appendChild(group);
992
+ break;
993
+ }
994
+ case 'OPTGROUP': {
995
+ if (flatten) {
996
+ this.#reflectButtons([...node.children], container);
997
+ break;
998
+ }
999
+ const group = this.#wrapOptgroupTag(node);
1000
+ this.#reflectButtons([...node.children], group);
1001
+ container.appendChild(group);
1002
+ break;
1003
+ }
1004
+ case 'OPTION':
1005
+ default: {
1006
+ container.appendChild(this.#wrapOptionTag(node));
1007
+ break;
1008
+ }
1009
+ }
1010
+ else {
1011
+ if (node.children.length)
1012
+ if (flatten)
1013
+ this.#reflectButtons(node.children, container);
1014
+ else if (allLeaves(node)) {
1015
+ const group = this.#wrapOptgroupTag(node);
1016
+ this.#reflectButtons(node.children, group);
1017
+ container.appendChild(group);
1018
+ }
1019
+ else {
1020
+ const group = this.#wrapDatalistTag(node);
1021
+ this.#reflectButtons(node.children, group);
1022
+ container.appendChild(group);
1023
+ }
1024
+ else
1025
+ container.appendChild(this.#wrapOptionTag(node));
1026
+ }
1027
+ }
1028
+ // #reflectPlaceholder(value: string | null) {
1029
+ // debug(`#reflectPlaceholder`)
1030
+ // const input = this.#shadowRoot.querySelector('input')
1031
+ // if (input) {
1032
+ // if (value) input.setAttribute('prompt', value)
1033
+ // else input.removeAttribute('prompt')
1034
+ // }
1035
+ // }
1036
+ #reflectLabel(value) {
1037
+ debug(`${_a.name} #reflectLabel`);
1038
+ queryMorph('[slot=label]', html `<label-view slot="label">${value ? html `<span>${value}</span>` : null}</label-view>`, this);
1039
+ this.#renderSlotted([]);
1040
+ }
1041
+ #reflectSelectionOnButtons() {
1042
+ debug(`${_a.name} #reflectSelectionOnButtons`);
1043
+ self.requestAnimationFrame(() => {
1044
+ this.ariaCurrent = this.#selection;
1045
+ const groupMap = indexGroups(this.#lastIndexedRoot);
1046
+ const groupContainsSelection = (source) => source instanceof Element ? [...source.querySelectorAll('option')].some((opt) => extractTag(opt) === this.#selection) : collectLeafValues(source).includes(this.#selection);
1047
+ const syncButtons = (root) => {
1048
+ // 1. plain value buttons
1049
+ for (const el of root.querySelectorAll('button[value]:not([slot])'))
1050
+ // $.prop('visibility', el.getAttribute('value') === this.#selection ? 'visible' : 'hidden', el.querySelector<HTMLElement>('label-view[data-role="check"]'))
1051
+ el.querySelector('label-view[data-role="check"]')?.style.setProperty('visibility', el.getAttribute('value') === this.#selection ? 'visible' : 'hidden');
1052
+ // 2. details/optgroups
1053
+ for (const details of root.querySelectorAll('details[is="disclosure-group"]')) {
1054
+ const hasSelectedDescendant = [...details.querySelectorAll('button[value]')].some((btn) => btn.getAttribute('value') === this.#selection);
1055
+ details.querySelector(':scope>summary label-view[data-role="check"]')?.style.setProperty('visibility', hasSelectedDescendant ? 'visible' : 'hidden');
1056
+ }
1057
+ // 3. nav-link buttons — resolved by groupId, same map used for resync
1058
+ for (const btn of root.querySelectorAll('button[navigation-link]:not([value])')) {
1059
+ const source = btn.dataset.groupId ? groupMap.get(btn.dataset.groupId) : undefined;
1060
+ const hasSelectedDescendant = source ? groupContainsSelection(source) : false;
1061
+ btn.querySelector('label-view[data-role="check"]')?.style.setProperty('visibility', hasSelectedDescendant ? 'visible' : 'hidden');
1062
+ }
1063
+ // 4. radio inputs
1064
+ for (const el of root.querySelectorAll('input[type=radio][value]'))
1065
+ el.checked = el.getAttribute('value') === this.#selection;
1066
+ };
1067
+ syncButtons(this);
1068
+ if (this.#spawn)
1069
+ syncButtons(this.#spawn);
1070
+ });
1071
+ }
1072
+ /**
1073
+ * Overwrite cvlabel with the label prop of the current(find[value === #selection]) option/dictentry
1074
+ */
1075
+ #reflectSelectionOnCurrentValueLabel() {
1076
+ debug(`${_a.name} #reflectSelectionOnCurrentValueLabel`);
1077
+ self.requestAnimationFrame(() => {
1078
+ switch (this.pickerStyle) {
1079
+ case 'sheet':
1080
+ case 'navigation-link': {
1081
+ const currentValueLabel = this.querySelector(':scope>label-view:not([slot])');
1082
+ if (!currentValueLabel)
1083
+ break;
1084
+ // if (!cvl) currentValueLabel.setAttribute('foreground', 'secondary')
1085
+ // else currentValueLabel.removeAttribute('foreground')
1086
+ const title = this.#currentValueLabel || this.#selection || this.getAttribute('prompt'), systemImage = this.#currentValueIcon || this.getAttribute('prompt-icon');
1087
+ queryMorph(':not([slot])', html `<span>${title}</span>`, currentValueLabel, { removeIf: !title });
1088
+ queryMorph('[slot=icon]', html `<image-view slot="icon" system-name="${systemImage}"></image-view>`, currentValueLabel, { removeIf: !systemImage });
1089
+ // morph(
1090
+ // htmx`<label-view>${systemImage ? htmx`<image-view slot="icon" system-name="${systemImage}"></image-view>` : null}${title ? htmx`<span>${title}</span>` : null}</label-view>`,
1091
+ // currentValueLabel
1092
+ // ) NOTE: not this bc/ help tooltip right now is added to trigger label and this mangles it
1093
+ break;
1094
+ }
1095
+ case 'menu': {
1096
+ const currentValueLabel = this.querySelector(':scope>menu-view:not([slot])>label-view[slot=label]');
1097
+ if (!currentValueLabel)
1098
+ break;
1099
+ // if (!cvl) currentValueLabel.setAttribute('foreground', 'secondary')
1100
+ // else currentValueLabel.removeAttribute('foreground')
1101
+ const title = this.#currentValueLabel || this.#selection || this.getAttribute('prompt'), systemImage = this.#currentValueIcon || this.getAttribute('prompt-icon');
1102
+ queryMorph(':not([slot])', html `<span>${title}</span>`, currentValueLabel, { removeIf: !title });
1103
+ queryMorph('[slot=icon]', html `<image-view slot="icon" system-name="${systemImage}"></image-view>`, currentValueLabel, { removeIf: !systemImage });
1104
+ // morph(
1105
+ // htmx`<label-view slot="label">${systemImage ? htmx`<image-view slot="icon" system-name="${systemImage}"></image-view>` : null}${title ? htmx`<span>${title}</span>` : null}</label-view>`,
1106
+ // currentValueLabel
1107
+ // ) NOTE: not this bc/ help tooltip right now is added to trigger label and this mangles it
1108
+ break;
1109
+ }
1110
+ case 'inline':
1111
+ default: {
1112
+ //
1113
+ break;
1114
+ }
1115
+ }
1116
+ });
1117
+ }
1118
+ #reflectTriggerHelp() {
1119
+ debug(`${_a.name} #reflectTriggerHelp`);
1120
+ let trigger;
1121
+ switch (this.pickerStyle) {
1122
+ case 'sheet':
1123
+ case 'navigation-link': {
1124
+ trigger = this.querySelector(':scope>label-view:not([slot])') ?? undefined;
1125
+ }
1126
+ case 'menu': {
1127
+ trigger = this.querySelector(':scope>menu-view:not([slot])>label-view[slot=label]') ?? undefined;
1128
+ }
1129
+ case 'inline':
1130
+ default: {
1131
+ //udnefined
1132
+ }
1133
+ }
1134
+ if (this.hasAttribute('help'))
1135
+ trigger?.setAttribute('help', this.getAttribute('help') ?? '');
1136
+ else
1137
+ trigger?.removeAttribute('help');
1138
+ }
1139
+ /**
1140
+ * Form participation property
1141
+ */
1142
+ get name() {
1143
+ return this.getAttribute('name') ?? this.getAttribute('label') ?? this.querySelector(':scope>[slot=label]')?.textContent ?? '';
1144
+ }
1145
+ setValidity = (flags, message, anchor) => {
1146
+ // let msg
1147
+ // if (message)
1148
+ for (const k in flags) {
1149
+ const key = k; // ✅ type-safe cast
1150
+ if (true !== flags[key])
1151
+ continue;
1152
+ for (const el of this.#slots?.get('validity-options')?.assignedElements({ flatten: true }) ?? []) {
1153
+ if (!el.matches('option'))
1154
+ continue;
1155
+ const { label, value } = el;
1156
+ if (`${kebabCase(key)}` === value) {
1157
+ message = label;
1158
+ break;
1159
+ }
1160
+ else if (`${kebabCase(key)}:${message}` === value) {
1161
+ message = label;
1162
+ break;
1163
+ }
1164
+ }
1165
+ }
1166
+ if (!message)
1167
+ for (const k in flags) {
1168
+ const key = k; // ✅ type-safe cast
1169
+ if (true !== flags[key])
1170
+ continue;
1171
+ message = kebabCase(key);
1172
+ break;
1173
+ }
1174
+ debug(`${_a.name} ⚡️ validity-change`);
1175
+ return this.#internals.setValidity(flags, this.#customValidity || message, anchor);
1176
+ };
1177
+ setCustomValidity = (message) => {
1178
+ this.#customValidity = message;
1179
+ if (this.#customValidity)
1180
+ this.#internals.setValidity({ ...this.#internals.validity, customError: true }, message);
1181
+ else
1182
+ this.#sendValueToForm(false);
1183
+ };
1184
+ formStateRestoreCallback(state, reason) {
1185
+ //
1186
+ }
1187
+ formAssociatedCallback(form) {
1188
+ this.#sendValueToForm(false);
1189
+ }
1190
+ formDisabledCallback(disabled) {
1191
+ for (const btn of this.#shadowRoot.querySelectorAll('button'))
1192
+ btn.toggleAttribute('disabled', !disabled);
1193
+ }
1194
+ formResetCallback() {
1195
+ this.#selection = '';
1196
+ this.#reflectSelectionOnButtons();
1197
+ this.#reflectSelectionOnCurrentValueLabel();
1198
+ this.#sendValueToForm(false);
1199
+ }
1200
+ }
1201
+ _a = PickerView;