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

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.
@@ -1,6 +1,7 @@
1
1
  var _a;
2
2
  import { I18n } from '../i18n';
3
3
  import { CleanupRegistry } from '../internal/class/cleanup-registry';
4
+ import { DictEntry, Dictionary } from '../internal/class/dict-entry';
4
5
  import { FormAssociatedBase, getInternals } from '../internal/class/form-associated-base';
5
6
  import { MutationObserverSet } from '../internal/class/mutation-observer-set';
6
7
  import { NavigationPath } from '../internal/class/navigation-path';
@@ -8,7 +9,7 @@ import { queryInsertPosition, startViewTransition } from '../internal/privateNam
8
9
  import { $, ancestors, debug, kebabCase, onoff } from '../internal/utils';
9
10
  import { queryMorph } from '../morphdom';
10
11
  import { html, render } from '../tpl';
11
- const pickerStyles = ['menu', 'inline', 'navigation-link', 'sheet', 'radio-group', 'automatic'];
12
+ const pickerStyles = ['menu', 'inline', 'navigation-link', 'sheet', 'radio-group', 'palette', 'segmented', 'automatic'];
12
13
  const update = (path, node, overwrite = true) => {
13
14
  debug(`PickerView: update`);
14
15
  if (!(path instanceof NavigationPath))
@@ -59,76 +60,6 @@ const reflectSpawnedPage = (current, source) => {
59
60
  else
60
61
  oldSearch?.remove();
61
62
  };
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
63
  /**
133
64
  * @summary A control that selects one value from a set of options.
134
65
  *
@@ -184,7 +115,7 @@ export class PickerView extends FormAssociatedBase {
184
115
  const { target } = evt;
185
116
  if (!(target instanceof HTMLElement))
186
117
  return;
187
- const newPage = this.#spawnPage(el instanceof Element ? Array.from(el.children) : el.children, 'content-view', groupId, this.hasAttribute('searchable'), extractLabel(el));
118
+ const newPage = this.#spawnPage(el.children, 'content-view', groupId, this.hasAttribute('searchable'), el.title);
188
119
  if (!newPage)
189
120
  return;
190
121
  newPage.dataset.groupId = groupId; // <-- tag the spawned page
@@ -195,7 +126,7 @@ export class PickerView extends FormAssociatedBase {
195
126
  });
196
127
  };
197
128
  }
198
- #spawnPage = (elements, tag, parentGroupId, searchable = false, title) => {
129
+ #spawnPage = (entries, tag, parentGroupId, searchable = false, title) => {
199
130
  debug(`${_a.name} #spawnPage`);
200
131
  const body = tag === 'sheet-view'
201
132
  ? $(html `<dialog is="sheet-view">
@@ -274,68 +205,33 @@ export class PickerView extends FormAssociatedBase {
274
205
  component?.remove();
275
206
  });
276
207
  });
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);
208
+ for (const [i, el] of entries.entries()) {
209
+ if (DictEntry.isLeaf(el)) {
210
+ const btn = this.#wrapOptionTag(el);
211
+ btn.addEventListener('click', this.#handlePageBtnClick);
212
+ list?.appendChild(btn);
213
+ }
214
+ else if (DictEntry.isGroup(el)) {
215
+ const group = this.#wrapOptgroupTag(el);
216
+ this.#reflectButtons(el.children, group);
217
+ for (const btn of group.querySelectorAll(':scope>button'))
328
218
  btn.addEventListener('click', this.#handlePageBtnClick);
329
- list?.appendChild(btn);
330
- }
219
+ list?.appendChild(group);
220
+ }
221
+ else {
222
+ const btn = _a.#wrapOptionSpawnTag(el);
223
+ const groupId = parentGroupId ? `${parentGroupId}.${i}` : `${i}`;
224
+ btn.dataset.groupId = groupId;
225
+ btn.addEventListener('click', this.#makeGroupClickHandler(el, groupId));
226
+ list?.appendChild(btn);
331
227
  }
332
228
  }
333
229
  return body;
334
230
  };
335
- #resyncSpawnedPages = (freshRoot) => {
231
+ #resyncSpawnedPages = (fresh) => {
336
232
  if (!this.#spawn)
337
233
  return;
338
- const groupMap = indexGroups(freshRoot);
234
+ const groupMap = Dictionary.index(fresh);
339
235
  for (const el of this.#spawn.querySelectorAll('content-view')) {
340
236
  const depth = ancestors('content-view,[is=sheet-view]', el).indexOf(this.#spawn);
341
237
  if (0 >= depth)
@@ -346,60 +242,19 @@ export class PickerView extends FormAssociatedBase {
346
242
  el.remove();
347
243
  continue;
348
244
  }
349
- const children = source instanceof Element ? Array.from(source.children) : source.children;
350
- const title = source instanceof Element ? extractLabel(source) : (source.title ?? null);
245
+ const children = source.children;
246
+ const title = source.title ?? null;
351
247
  const newPage = this.#spawnPage(children, 'content-view', groupId, this.hasAttribute('searchable'), title);
352
248
  newPage.dataset.groupId = groupId;
353
249
  reflectSpawnedPage(el, newPage);
354
250
  }
355
251
  };
356
- #renderButtons(input) {
252
+ #renderButtons(source) {
357
253
  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
- }
254
+ const { labels, icons } = Dictionary.flatten(source);
255
+ this.#lastRenderedLabelMap = labels;
256
+ this.#lastRenderedIconMap = icons;
257
+ this.#lastIndexedRoot = source;
403
258
  switch (this.pickerStyle) {
404
259
  case 'sheet':
405
260
  case 'navigation-link': {
@@ -422,8 +277,8 @@ export class PickerView extends FormAssociatedBase {
422
277
  if (!this.#spawn)
423
278
  break;
424
279
  // 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);
280
+ reflectSpawnedPage(this.#spawn, this.#spawnPage(source, 'DIALOG' === this.#spawn.tagName ? 'sheet-view' : 'content-view', undefined, this.hasAttribute('searchable'), this.getAttribute('label')));
281
+ this.#resyncSpawnedPages(source);
427
282
  // // FIXME:
428
283
  // for (const el of this.#spawn.querySelectorAll<HTMLElement>('content-view')) {
429
284
  // const depth = $.ancestors('content-view,[is=sheet-view]', el).indexOf(this.#spawn)
@@ -453,9 +308,11 @@ export class PickerView extends FormAssociatedBase {
453
308
  // }
454
309
  if (this.hasAttribute('help'))
455
310
  currentValueLabel?.setAttribute('help', this.getAttribute('help') ?? '');
456
- this.#reflectButtons(input.source, menu);
311
+ this.#reflectButtons(source, menu);
457
312
  break;
458
313
  }
314
+ case 'segmented':
315
+ case 'palette':
459
316
  case 'radio-group': {
460
317
  // capture
461
318
  // const stack = this.querySelector<HTMLElement>(':scope>v-stack:not([slot])') ?? this.appendChild<HTMLElement>($(html`<v-stack></v-stack>`, '>1'))
@@ -465,7 +322,7 @@ export class PickerView extends FormAssociatedBase {
465
322
  for (const el of this.querySelectorAll(':scope>:not([slot])'))
466
323
  el.remove();
467
324
  // add buttons
468
- this.#reflectButtons(input.source, this);
325
+ this.#reflectButtons(source, this);
469
326
  break;
470
327
  }
471
328
  case 'inline':
@@ -491,10 +348,7 @@ export class PickerView extends FormAssociatedBase {
491
348
  // if (label) renderLabelTitle(label, value) //label.setAttribute('title', value)
492
349
  section.insertAdjacentElement('beforeend', hStack);
493
350
  }
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);
351
+ this.#reflectButtons(source.filter(DictEntry.isLeaf), section);
498
352
  break;
499
353
  }
500
354
  }
@@ -575,6 +429,8 @@ export class PickerView extends FormAssociatedBase {
575
429
  </label>
576
430
  `));
577
431
  break;
432
+ case 'segmented':
433
+ case 'palette':
578
434
  case 'radio-group':
579
435
  _a.#templates.set(this.pickerStyle, $(html `
580
436
  <label part="root picker-stack">
@@ -737,8 +593,8 @@ export class PickerView extends FormAssociatedBase {
737
593
  }
738
594
  #resolveSource() {
739
595
  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 }) ?? [] };
596
+ return Dictionary.parse(this.getAttribute('dictionary'));
597
+ return Dictionary.fromElements(this.#slots?.get('list')?.assignedElements({ flatten: true }) ?? []);
742
598
  }
743
599
  #render() {
744
600
  debug(`${_a.name} ⚡️ #render (${this.pickerStyle})`);
@@ -786,7 +642,7 @@ export class PickerView extends FormAssociatedBase {
786
642
  if (!(target instanceof HTMLElement))
787
643
  return;
788
644
  this.#spawn?.remove?.();
789
- const { source } = this.#resolveSource();
645
+ const source = this.#resolveSource();
790
646
  const level0 = this.#spawnPage(source, // this.hasAttribute('dictionary') ? parseDictionary(this.getAttribute('dictionary')) : (this.#slots?.get('list')?.assignedElements() ?? []),
791
647
  'sheet' === this.pickerStyle ? 'sheet-view' : 'content-view', undefined, this.hasAttribute('searchable'), this.getAttribute('label'));
792
648
  if (!level0)
@@ -838,6 +694,8 @@ export class PickerView extends FormAssociatedBase {
838
694
  if (!(target instanceof HTMLElement))
839
695
  return;
840
696
  switch (this.pickerStyle) {
697
+ case 'segmented':
698
+ case 'palette':
841
699
  case 'radio-group':
842
700
  const radio = target.closest('input[type="radio"]');
843
701
  if (!radio)
@@ -869,25 +727,30 @@ export class PickerView extends FormAssociatedBase {
869
727
  };
870
728
  static #wrapOptionSpawnTag(node) {
871
729
  debug(`${_a.name} #wrapOptionSpawnTag`);
872
- const title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
873
730
  const mount = document.createElement('div');
874
731
  render(html `<button type="button" tabindex="0" navigation-link>
875
732
  <h-stack distribution="leading" template="auto spacer">
876
733
  <label-view data-role="check" style="visibility: hidden">
877
734
  <image-view slot="icon" system-name="check"></image-view>
878
735
  </label-view>
879
- ${_a.#morphITSLabel({ title, subtitle, icon })}
736
+ ${_a.#morphITSLabel(node)}
880
737
  </h-stack>
881
738
  </button>`, mount);
882
739
  return mount.firstElementChild; //btn
883
740
  }
884
741
  #wrapOptionTag(node) {
885
742
  debug(`${_a.name} #wrapOptionTag`);
886
- const tag = extractTag(node), title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
743
+ const tag = node.value;
887
744
  const mount = document.createElement('div');
888
745
  switch (this.pickerStyle) {
746
+ case 'segmented':
747
+ render(_a.#morphSegmentedBtn({ name: this.#guuid, node, tag }), mount);
748
+ return mount.firstElementChild; //btn
749
+ case 'palette':
750
+ render(_a.#morphPaletteBtn({ name: this.#guuid, node, tag }), mount);
751
+ return mount.firstElementChild; //btn
889
752
  case 'radio-group':
890
- render(_a.#morphRadioGroupBtn({ name: this.#guuid, title, subtitle, icon, tag }), mount);
753
+ render(_a.#morphRadioGroupBtn({ name: this.#guuid, node, tag }), mount);
891
754
  return mount.firstElementChild; //btn
892
755
  default:
893
756
  render(html `<button type="button" tabindex="0" value="${tag}">
@@ -895,31 +758,82 @@ export class PickerView extends FormAssociatedBase {
895
758
  <label-view data-role="check">
896
759
  <image-view slot="icon" system-name="check"></image-view>
897
760
  </label-view>
898
- ${_a.#morphITSLabel({ title, subtitle, icon })}
761
+ ${_a.#morphITSLabel(node)}
899
762
  </h-stack>
900
763
  </button>`, mount);
901
764
  return mount.firstElementChild; //btn
902
765
  }
903
766
  }
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>`;
767
+ static #morphRadioGroupBtn({ name, node, tag }) {
768
+ if ('string' === typeof tag)
769
+ return html `<label tabindex="0">
770
+ <h-stack distribution="fill" template="auto spacer" spacing="5">
771
+ <input type="radio" name="${name}" value="${tag}" />
772
+ ${_a.#morphITSLabel(node)}
773
+ </h-stack>
774
+ </label>`;
775
+ else
776
+ return html `<label>
777
+ <h-stack distribution="fill" template="auto spacer" spacing="5">
778
+ <input type="radio" name="${name}" disabled />
779
+ ${_a.#morphITSLabel(node)}
780
+ </h-stack>
781
+ </label>`;
911
782
  }
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>`;
783
+ static #morphPaletteBtn({ name, node, tag }) {
784
+ if ('string' === typeof tag) {
785
+ const style = node.src ? `background-image: url('${node.src}')` : null;
786
+ return html `<label tabindex="0">
787
+ <v-stack template="auto spacer" spacing="5">
788
+ <input type="radio" name="${name}" value="${tag}" style="${style}" />
789
+ ${_a.#morphITSLabel(node)}
790
+ </v-stack>
791
+ </label>`;
792
+ }
793
+ else
794
+ return html `<label>
795
+ <v-stack template="auto spacer" spacing="5">
796
+ <input type="radio" name="${name}" disabled />
797
+ ${_a.#morphITSLabel(node)}
798
+ </v-stack>
799
+ </label>`;
919
800
  }
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}
801
+ static #morphSegmentedBtn({ name, node, tag }) {
802
+ const cssUrl = (src) => {
803
+ if ('string' !== typeof src)
804
+ return null;
805
+ try {
806
+ const { protocol, href } = new URL(src, location.href);
807
+ if (protocol !== 'https:' && protocol !== 'http:' && protocol !== 'blob:')
808
+ return null;
809
+ // URL.href already percent-encodes " < > ` space and \ in path/query.
810
+ // Encode the remaining chars that matter inside url('...')
811
+ return href.replace(/['()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
812
+ }
813
+ catch {
814
+ return null;
815
+ }
816
+ };
817
+ if ('string' === typeof tag) {
818
+ const style = node.src ? `background-image: url('${cssUrl(node.src)}')` : null;
819
+ return html `<label tabindex="0">
820
+ <v-stack template="auto spacer" spacing="5">
821
+ <input type="radio" name="${name}" value="${tag}" style="${style}" />
822
+ ${_a.#morphITSLabel(node)}
823
+ </v-stack>
824
+ </label>`;
825
+ }
826
+ else
827
+ return html `<label>
828
+ <v-stack template="auto spacer" spacing="5">
829
+ <input type="radio" name="${name}" disabled />
830
+ ${_a.#morphITSLabel(node)}
831
+ </v-stack>
832
+ </label>`;
833
+ }
834
+ static #morphITSLabel({ title, subtitle, systemImage }) {
835
+ return html `<h-stack distribution="fill" template="${systemImage ? 'auto spacer' : null}">
836
+ ${systemImage ? html `<label-view system-image="${systemImage}"></label-view>` : null}
923
837
  <v-stack spacing="3" alignment="fill">
924
838
  <label-view title="${title}"></label-view>
925
839
  ${subtitle ? html `<label-view foreground="secondary" font="callout" title="${subtitle}"></label-view>` : null}
@@ -928,15 +842,16 @@ export class PickerView extends FormAssociatedBase {
928
842
  }
929
843
  #wrapOptgroupTag(node) {
930
844
  debug(`${_a.name} #wrapOptgroupTag`);
931
- const title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
932
845
  const mount = document.createElement('div');
933
846
  switch (this.pickerStyle) {
847
+ case 'segmented':
848
+ render(_a.#morphSegmentedBtn({ name: this.#guuid, node }), mount);
849
+ return mount.firstElementChild;
850
+ case 'palette':
851
+ render(_a.#morphPaletteBtn({ name: this.#guuid, node }), mount);
852
+ return mount.firstElementChild;
934
853
  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);
854
+ render(_a.#morphRadioGroupBtn({ name: this.#guuid, node }), mount);
940
855
  return mount.firstElementChild;
941
856
  default:
942
857
  render(html `<details is="disclosure-group" disclosure-style="marker-trailing">
@@ -945,7 +860,7 @@ export class PickerView extends FormAssociatedBase {
945
860
  <label-view data-role="check" style="visibility: hidden">
946
861
  <image-view slot="icon" system-name="check"></image-view>
947
862
  </label-view>
948
- ${_a.#morphITSLabel({ title, subtitle, icon })}
863
+ ${_a.#morphITSLabel(node)}
949
864
  </h-stack>
950
865
  </summary>
951
866
  </details>`, mount);
@@ -953,15 +868,16 @@ export class PickerView extends FormAssociatedBase {
953
868
  }
954
869
  }
955
870
  #wrapDatalistTag(node) {
956
- const title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
957
871
  const mount = document.createElement('div');
958
872
  switch (this.pickerStyle) {
873
+ case 'segmented':
874
+ render(_a.#morphSegmentedBtn({ name: this.#guuid, node }), mount);
875
+ return mount.firstElementChild;
876
+ case 'palette':
877
+ render(_a.#morphPaletteBtn({ name: this.#guuid, node }), mount);
878
+ return mount.firstElementChild;
959
879
  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);
880
+ render(_a.#morphRadioGroupBtn({ name: this.#guuid, node }), mount);
965
881
  return mount.firstElementChild;
966
882
  default:
967
883
  render(html `<menu-view tabindex="0">
@@ -969,7 +885,7 @@ export class PickerView extends FormAssociatedBase {
969
885
  <label-view data-role="check" style="visibility: hidden">
970
886
  <image-view slot="icon" system-name="check"></image-view>
971
887
  </label-view>
972
- ${_a.#morphITSLabel({ title, subtitle, icon })}
888
+ ${_a.#morphITSLabel(node)}
973
889
  </h-stack>
974
890
  </menu-view>`, mount);
975
891
  return mount.firstElementChild;
@@ -977,53 +893,21 @@ export class PickerView extends FormAssociatedBase {
977
893
  }
978
894
  #reflectButtons(nodes, container) {
979
895
  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));
896
+ const flatten = ['radio-group', 'palette', 'segmented'].includes(this.pickerStyle);
897
+ for (const node of nodes) {
898
+ if (DictEntry.isLeaf(node)) {
899
+ container.appendChild(this.#wrapOptionTag(node));
900
+ continue;
901
+ }
902
+ if (flatten) {
903
+ container.appendChild(this.#wrapOptgroupTag(node)); // header row, same markup as the datalist one in these styles
904
+ this.#reflectButtons(node.children, container);
905
+ continue;
1026
906
  }
907
+ const group = DictEntry.isGroup(node) ? this.#wrapOptgroupTag(node) : this.#wrapDatalistTag(node);
908
+ this.#reflectButtons(node.children, group);
909
+ container.appendChild(group);
910
+ }
1027
911
  }
1028
912
  // #reflectPlaceholder(value: string | null) {
1029
913
  // debug(`#reflectPlaceholder`)
@@ -1042,8 +926,8 @@ export class PickerView extends FormAssociatedBase {
1042
926
  debug(`${_a.name} #reflectSelectionOnButtons`);
1043
927
  self.requestAnimationFrame(() => {
1044
928
  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);
929
+ const groupMap = Dictionary.index(this.#lastIndexedRoot);
930
+ const groupContainsSelection = (e) => DictEntry.leafValues(e).includes(this.#selection);
1047
931
  const syncButtons = (root) => {
1048
932
  // 1. plain value buttons
1049
933
  for (const el of root.querySelectorAll('button[value]:not([slot])'))
@@ -1122,13 +1006,16 @@ export class PickerView extends FormAssociatedBase {
1122
1006
  case 'sheet':
1123
1007
  case 'navigation-link': {
1124
1008
  trigger = this.querySelector(':scope>label-view:not([slot])') ?? undefined;
1009
+ break;
1125
1010
  }
1126
1011
  case 'menu': {
1127
1012
  trigger = this.querySelector(':scope>menu-view:not([slot])>label-view[slot=label]') ?? undefined;
1013
+ break;
1128
1014
  }
1129
1015
  case 'inline':
1130
1016
  default: {
1131
1017
  //udnefined
1018
+ break;
1132
1019
  }
1133
1020
  }
1134
1021
  if (this.hasAttribute('help'))