@swiftwc/ui 0.0.0-dev.8 → 0.0.0-dev.81

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