@swiftwc/ui 0.0.0-dev.112 → 0.0.0-dev.114

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.
@@ -8,7 +8,7 @@ import { queryInsertPosition, startViewTransition } from '../internal/privateNam
8
8
  import { $, ancestors, debug, kebabCase, onoff } from '../internal/utils';
9
9
  import { queryMorph } from '../morphdom';
10
10
  import { html, render } from '../tpl';
11
- const pickerStyles = ['menu', 'inline', 'navigation-link', 'sheet', 'automatic'];
11
+ const pickerStyles = ['menu', 'inline', 'navigation-link', 'sheet', 'radio-group', 'automatic'];
12
12
  const update = (path, node, overwrite = true) => {
13
13
  debug(`PickerView: update`);
14
14
  if (!(path instanceof NavigationPath))
@@ -109,42 +109,73 @@ const indexGroups = (nodes, parentPath = '') => {
109
109
  return map;
110
110
  };
111
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
+ };
112
132
  /**
113
133
  * @summary A control that selects one value from a set of options.
114
134
  *
115
- * @attr {menu|inline|navigation-link|sheet|automatic} picker-style
116
- *
117
- * @attr help — Adds a help tooltip to the trigger of the picker, if style supports one
118
- *
119
- * @attr {DictEntry[]} dictionary — Renders all options using this array
120
- *
121
135
  * @attr {vertical|horizontal|auto} label-value-placement
122
136
  *
137
+ * @attr {boolean} horizontal-radio-group-layout
138
+ *
123
139
  * @slot — The default slot.
124
140
  * @slot label
125
141
  * @slot list
126
142
  * @slot {HTMLOptionElement[]} validity-options
127
143
  */
128
144
  export class PickerView extends FormAssociatedBase {
129
- static get ATTR() {
130
- return {
131
- PLACEHOLDER: 'prompt',
132
- PLACEHOLDER_ICON: 'prompt-icon',
133
- LABEL: 'label',
134
- PICKER_STYLE: 'picker-style',
135
- SELECTION: 'selection',
136
- SEARCHABLE: 'searchable',
137
- CURRENT_VALUE_LABEL: 'current-value-label',
138
- CURRENT_VALUE_ICON: 'current-value-icon',
139
- TRIGGER_HELP: 'help',
140
- DICTIONARY: 'dictionary',
141
- REQUIRED: 'required',
142
- };
143
- }
144
145
  static get observedAttributes() {
145
- return Object.values(this.ATTR);
146
+ return [
147
+ 'prompt',
148
+ 'prompt-icon',
149
+ 'label',
150
+ 'name',
151
+ /**
152
+ * @type {menu|inline|navigation-link|sheet|automatic}
153
+ */
154
+ 'picker-style',
155
+ 'selection',
156
+ /**
157
+ * @type {boolean}
158
+ */
159
+ 'searchable',
160
+ 'current-value-label',
161
+ 'current-value-icon',
162
+ /**
163
+ * Adds a help tooltip to the trigger of the picker, if style supports one
164
+ */
165
+ 'help',
166
+ /**
167
+ * Renders all options using this array
168
+ * @type {DictEntry[]}
169
+ */
170
+ 'dictionary',
171
+ /**
172
+ * @type {boolean}
173
+ */
174
+ 'required',
175
+ ];
146
176
  }
147
177
  static #templates = new Map();
178
+ #guuid = self.crypto.randomUUID();
148
179
  #spawn;
149
180
  #makeGroupClickHandler(el, groupId) {
150
181
  return async (evt) => {
@@ -247,15 +278,15 @@ export class PickerView extends FormAssociatedBase {
247
278
  if (el instanceof Element)
248
279
  switch (el.tagName) {
249
280
  case 'OPTGROUP': {
250
- const group = _a.#wrapOptgroupTag(el);
251
- _a.#reflectButtons([...el.children], group);
281
+ const group = this.#wrapOptgroupTag(el);
282
+ this.#reflectButtons([...el.children], group);
252
283
  for (const btn of group.querySelectorAll(':scope>button'))
253
284
  btn.addEventListener('click', this.#handlePageBtnClick);
254
285
  list?.appendChild(group);
255
286
  break;
256
287
  }
257
288
  case 'OPTION': {
258
- const btn = _a.#wrapOptionTag(el);
289
+ const btn = this.#wrapOptionTag(el);
259
290
  btn.addEventListener('click', this.#handlePageBtnClick);
260
291
  list?.appendChild(btn);
261
292
  break;
@@ -279,8 +310,8 @@ export class PickerView extends FormAssociatedBase {
279
310
  else {
280
311
  if (el.children.length)
281
312
  if (allLeaves(el)) {
282
- const group = _a.#wrapOptgroupTag(el);
283
- _a.#reflectButtons(el.children, group);
313
+ const group = this.#wrapOptgroupTag(el);
314
+ this.#reflectButtons(el.children, group);
284
315
  for (const btn of group.querySelectorAll(':scope>button'))
285
316
  btn.addEventListener('click', this.#handlePageBtnClick);
286
317
  list?.appendChild(group);
@@ -293,7 +324,7 @@ export class PickerView extends FormAssociatedBase {
293
324
  list?.appendChild(btn);
294
325
  }
295
326
  else {
296
- const btn = _a.#wrapOptionTag(el);
327
+ const btn = this.#wrapOptionTag(el);
297
328
  btn.addEventListener('click', this.#handlePageBtnClick);
298
329
  list?.appendChild(btn);
299
330
  }
@@ -326,28 +357,9 @@ export class PickerView extends FormAssociatedBase {
326
357
  debug(`${_a.name} #renderButtons`);
327
358
  switch (input.mode) {
328
359
  case 'dictionary': {
329
- const flattenDictionary = (tree) => {
330
- const out1 = {}, out2 = {}, stack = [tree], idx = [0];
331
- while (stack.length) {
332
- const frame = stack[stack.length - 1], i = idx[idx.length - 1];
333
- if (i >= frame.length) {
334
- stack.pop();
335
- idx.pop();
336
- continue;
337
- }
338
- idx[idx.length - 1]++;
339
- const { value, title, systemImage, children } = frame[i];
340
- out1[value] = title;
341
- out2[value] = systemImage;
342
- if (children.length) {
343
- stack.push(children);
344
- idx.push(0);
345
- }
346
- }
347
- return { labels: out1, icons: out2 };
348
- };
349
- this.#lastRenderedLabelMap = flattenDictionary(input.source).labels;
350
- this.#lastRenderedIconMap = flattenDictionary(input.source).icons;
360
+ const { labels, icons } = flattenDictionary(input.source);
361
+ this.#lastRenderedLabelMap = labels;
362
+ this.#lastRenderedIconMap = icons;
351
363
  // const collectLeafValues = (node: DictEntry): string[] => (node.children.length ? node.children.flatMap(collectLeafValues) : [node.value])
352
364
  // collectGroups = (nodes: DictEntry[]): string[][] => {
353
365
  // const groups: string[][] = []
@@ -404,8 +416,8 @@ export class PickerView extends FormAssociatedBase {
404
416
  el.remove();
405
417
  CleanupRegistry.unregister(this, 'trigger');
406
418
  CleanupRegistry.register(this, onoff('click', this.#handleTriggerClick, currentValueLabel).on(), 'trigger');
407
- if (this.hasAttribute(this.constructor.ATTR.TRIGGER_HELP))
408
- currentValueLabel?.setAttribute(this.constructor.ATTR.TRIGGER_HELP, this.getAttribute(this.constructor.ATTR.TRIGGER_HELP) ?? '');
419
+ if (this.hasAttribute('help'))
420
+ currentValueLabel?.setAttribute('help', this.getAttribute('help') ?? '');
409
421
  // rebuild snapshot(tree)
410
422
  if (!this.#spawn)
411
423
  break;
@@ -439,9 +451,21 @@ export class PickerView extends FormAssociatedBase {
439
451
  // renderLabelIcon(currentValueLabel, 'dots-three') // overwritten
440
452
  // renderLabelTitle(currentValueLabel, this.#currentValueLabel) // overwritten
441
453
  // }
442
- if (this.hasAttribute(this.constructor.ATTR.TRIGGER_HELP))
443
- currentValueLabel?.setAttribute(this.constructor.ATTR.TRIGGER_HELP, this.getAttribute(this.constructor.ATTR.TRIGGER_HELP) ?? '');
444
- _a.#reflectButtons(input.source, menu);
454
+ if (this.hasAttribute('help'))
455
+ currentValueLabel?.setAttribute('help', this.getAttribute('help') ?? '');
456
+ this.#reflectButtons(input.source, menu);
457
+ break;
458
+ }
459
+ case 'radio-group': {
460
+ // capture
461
+ // const stack = this.querySelector<HTMLElement>(':scope>v-stack:not([slot])') ?? this.appendChild<HTMLElement>($(html`<v-stack></v-stack>`, '>1'))
462
+ // reset state
463
+ // stack.innerHTML = ''
464
+ // clear all siblings
465
+ for (const el of this.querySelectorAll(':scope>:not([slot])'))
466
+ el.remove();
467
+ // add buttons
468
+ this.#reflectButtons(input.source, this);
445
469
  break;
446
470
  }
447
471
  case 'inline':
@@ -455,32 +479,35 @@ export class PickerView extends FormAssociatedBase {
455
479
  if (inlineList !== el)
456
480
  el.remove();
457
481
  // add label as a plain element
458
- const value = this.getAttribute(this.constructor.ATTR.LABEL);
482
+ const value = this.getAttribute('label');
459
483
  if (value) {
460
- const hStack = $(html `<h-stack distribution="leading" template="auto spacer"
461
- ><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
462
- ></h-stack>`, '>1');
484
+ const hStack = $(html `<h-stack distribution="leading" template="auto spacer">
485
+ <label-view data-role="check" style="visibility: hidden">
486
+ <image-view slot="icon" system-name="check"></image-view>
487
+ </label-view>
488
+ <label-view><span></span></label-view>
489
+ </h-stack>`, '>1');
463
490
  queryMorph('label-view:nth-child(2)', html `<label-view>${value ? html `<span>${value}</span>` : null}</label-view>`, hStack);
464
491
  // if (label) renderLabelTitle(label, value) //label.setAttribute('title', value)
465
492
  section.insertAdjacentElement('beforeend', hStack);
466
493
  }
467
494
  if ('dictionary' === input.mode)
468
- _a.#reflectButtons(input.source.filter((el) => 0 === el.children.length), section);
495
+ this.#reflectButtons(input.source.filter((el) => 0 === el.children.length), section);
469
496
  else
470
- _a.#reflectButtons(input.source.filter((el) => el.matches('option')), section);
497
+ this.#reflectButtons(input.source.filter((el) => el.matches('option')), section);
471
498
  break;
472
499
  }
473
500
  }
474
501
  this.#reflectSelectionOnButtons();
475
502
  this.#reflectSelectionOnCurrentValueLabel();
476
503
  }
477
- #renderDictionary = (dictionary) => {
478
- debug(`${_a.name} ⚡️ mutation`);
479
- this.#renderButtons({ mode: 'dictionary', source: dictionary });
480
- };
504
+ // #renderDictionary = (dictionary: Dictionary) => {
505
+ // debug(`${PickerView.name} ⚡️ mutation`)
506
+ // this.#renderButtons({ mode: 'dictionary', source: dictionary })
507
+ // }
481
508
  #renderSlotted = (entries) => {
482
509
  debug(`${_a.name} ⚡️ mutation`);
483
- this.#renderButtons({ mode: 'list', source: this.#slots?.get('list')?.assignedElements({ flatten: true }) ?? [] });
510
+ this.#renderButtons(this.#resolveSource()); //this.#renderButtons({ mode: 'list', source: this.#slots?.get('list')?.assignedElements({ flatten: true }) ?? [] })
484
511
  };
485
512
  #renderValidityMsgs = (entries) => {
486
513
  debug(`${_a.name} ⚡️ mutation`);
@@ -508,11 +535,10 @@ export class PickerView extends FormAssociatedBase {
508
535
  #lastRenderedIconMap = {};
509
536
  #lastIndexedRoot = [];
510
537
  get #currentValueLabel() {
511
- return ((this.getAttribute(this.constructor.ATTR.CURRENT_VALUE_LABEL) ?? '').replaceAll('{{selection}}', this.#selection).replaceAll('{{currentValueLabel}}', this.#selection) ||
512
- this.#lastRenderedLabelMap[this.#selection]);
538
+ return ((this.getAttribute('current-value-label') ?? '').replaceAll('{{selection}}', this.#selection).replaceAll('{{currentValueLabel}}', this.#selection) || this.#lastRenderedLabelMap[this.#selection]);
513
539
  }
514
540
  get #currentValueIcon() {
515
- return (this.getAttribute(this.constructor.ATTR.CURRENT_VALUE_ICON) ?? '') || this.#lastRenderedIconMap[this.#selection];
541
+ return (this.getAttribute('current-value-icon') ?? '') || this.#lastRenderedIconMap[this.#selection];
516
542
  }
517
543
  get #internals() {
518
544
  return getInternals(this);
@@ -548,32 +574,46 @@ export class PickerView extends FormAssociatedBase {
548
574
  <slot name="validity-options" hidden></slot>
549
575
  </label>
550
576
  `));
551
- // break
552
- // case 'compact':
553
- // PickerView.#templates.set(
554
- // style,
555
- // Object.assign(document.createElement('template'), {
556
- // innerHTML: String.raw`
557
- // <label part="compact-picker">
558
- // <input type="text" part="compact-input">
559
- // </label>
560
- // `,
561
- // })
562
- // )
563
- // break
564
- // case 'fancy':
565
- // PickerView.#templates.set(
566
- // style,
567
- // Object.assign(document.createElement('template'), {
568
- // innerHTML: String.raw`
569
- // <div part="fancy-picker">
570
- // <span>Fancy Picker</span>
571
- // <input type="text" part="fancy-input">
572
- // </div>
573
- // `,
574
- // })
575
- // )
576
577
  break;
578
+ case 'radio-group':
579
+ _a.#templates.set(this.pickerStyle, $(html `
580
+ <label part="root picker-stack">
581
+ <div part="root picker-label-stack">
582
+ <slot name="label"></slot>
583
+ </div>
584
+ <div part="root picker-input-stack">
585
+ <slot></slot>
586
+ </div>
587
+ <slot name="list" hidden></slot>
588
+ <slot name="validity-options" hidden></slot>
589
+ </label>
590
+ `));
591
+ break;
592
+ // break
593
+ // case 'compact':
594
+ // PickerView.#templates.set(
595
+ // style,
596
+ // Object.assign(document.createElement('template'), {
597
+ // innerHTML: String.raw`
598
+ // <label part="compact-picker">
599
+ // <input type="text" part="compact-input">
600
+ // </label>
601
+ // `,
602
+ // })
603
+ // )
604
+ // break
605
+ // case 'fancy':
606
+ // PickerView.#templates.set(
607
+ // style,
608
+ // Object.assign(document.createElement('template'), {
609
+ // innerHTML: String.raw`
610
+ // <div part="fancy-picker">
611
+ // <span>Fancy Picker</span>
612
+ // <input type="text" part="fancy-input">
613
+ // </div>
614
+ // `,
615
+ // })
616
+ // )
577
617
  // case 'gg':
578
618
  // PickerView.#templates.set(
579
619
  // style,
@@ -617,47 +657,56 @@ export class PickerView extends FormAssociatedBase {
617
657
  attributeChangedCallback(name, oldValue, newValue) {
618
658
  debug(`${_a.name} ⚡️ attr-change [${name}] ("${oldValue}" → "${newValue}")`);
619
659
  switch (name) {
620
- case this.constructor.ATTR.PLACEHOLDER:
621
- case this.constructor.ATTR.PLACEHOLDER_ICON:
660
+ // case 'horizontal-radio-group-layout':
661
+ // if (oldValue === newValue) break
662
+ // this.#renderSlotted([])
663
+ // break
664
+ case 'prompt':
665
+ case 'prompt-icon':
622
666
  if (oldValue === newValue)
623
667
  break;
624
668
  this.#reflectSelectionOnCurrentValueLabel(); //this.#reflectPlaceholder(newValue)
625
669
  break;
626
- case this.constructor.ATTR.LABEL:
670
+ case 'label':
627
671
  this.#reflectLabel(newValue);
628
672
  // this.#sendValueToForm()
629
673
  break;
630
- case this.constructor.ATTR.PICKER_STYLE:
674
+ case 'picker-style':
631
675
  if (oldValue === newValue)
632
676
  break;
633
677
  this.#render();
634
678
  break;
635
- case this.constructor.ATTR.SEARCHABLE:
679
+ case 'searchable':
636
680
  if (oldValue === newValue)
637
681
  break;
638
682
  this.#renderSlotted([]);
639
683
  break;
640
- case this.constructor.ATTR.CURRENT_VALUE_LABEL:
641
- case this.constructor.ATTR.CURRENT_VALUE_ICON:
684
+ case 'current-value-label':
685
+ case 'current-value-icon':
642
686
  // if (oldValue === newValue) break
643
687
  this.#reflectSelectionOnCurrentValueLabel();
644
688
  break;
645
- case this.constructor.ATTR.TRIGGER_HELP:
689
+ case 'help':
646
690
  // if (oldValue === newValue) break
647
691
  this.#reflectTriggerHelp();
648
692
  break;
649
- case this.constructor.ATTR.DICTIONARY:
693
+ case 'dictionary':
650
694
  if (oldValue === newValue)
651
695
  break;
652
- this.#renderDictionary(parseDictionary(newValue));
696
+ this.#renderSlotted([]); // re-resolve: dictionary present → wins; removed → falls back to slotted //this.#renderDictionary(parseDictionary(newValue))
653
697
  break;
654
- case this.constructor.ATTR.REQUIRED:
698
+ case 'name':
699
+ case 'required':
655
700
  if (oldValue === newValue)
656
701
  break;
657
702
  this.#sendValueToForm(false);
658
703
  break;
659
- case this.constructor.ATTR.SELECTION:
660
- // nothing happens
704
+ case 'selection':
705
+ if (oldValue === newValue)
706
+ break;
707
+ this.#selection = newValue ?? '';
708
+ // FIXME: What about other callbacks from this.#selection=???? Somehow they run correctly
709
+ this.#sendValueToForm(false);
661
710
  break;
662
711
  }
663
712
  }
@@ -673,7 +722,8 @@ export class PickerView extends FormAssociatedBase {
673
722
  this.#renderSlotted([]);
674
723
  }, I18n.on).on());
675
724
  CleanupRegistry.register(this, onoff('click', this.#handleClick, this).on());
676
- if (!this.hasAttribute(this.constructor.ATTR.PICKER_STYLE))
725
+ CleanupRegistry.register(this, onoff('input', this.#handleInput, this).on());
726
+ if (!this.hasAttribute('picker-style'))
677
727
  this.#render(); // skip if already rendered by attr-change during upgrade!
678
728
  // finally
679
729
  if (!this.hasAttribute('selection'))
@@ -682,12 +732,17 @@ export class PickerView extends FormAssociatedBase {
682
732
  this.#sendValueToForm(false);
683
733
  }
684
734
  get pickerStyle() {
685
- const attr = this.constructor.ATTR.PICKER_STYLE;
735
+ const attr = 'picker-style';
686
736
  return pickerStyles.includes(this.getAttribute(attr) ?? '') ? this.getAttribute(attr) : 'automatic';
687
737
  }
738
+ #resolveSource() {
739
+ if (this.hasAttribute('dictionary'))
740
+ return { mode: 'dictionary', source: parseDictionary(this.getAttribute('dictionary')) };
741
+ return { mode: 'list', source: this.#slots?.get('list')?.assignedElements({ flatten: true }) ?? [] };
742
+ }
688
743
  #render() {
689
744
  debug(`${_a.name} ⚡️ #render (${this.pickerStyle})`);
690
- // const style = this.getAttribute((this.constructor as typeof PickerView).ATTR.PICKER_STYLE)
745
+ // const style = this.getAttribute('picker-style')
691
746
  if (this.#lastRenderedStyle === this.pickerStyle)
692
747
  return; // skip if already applied
693
748
  this.#lastRenderedStyle = this.pickerStyle;
@@ -731,7 +786,9 @@ export class PickerView extends FormAssociatedBase {
731
786
  if (!(target instanceof HTMLElement))
732
787
  return;
733
788
  this.#spawn?.remove?.();
734
- 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'));
789
+ const { source } = this.#resolveSource();
790
+ const level0 = this.#spawnPage(source, // this.hasAttribute('dictionary') ? parseDictionary(this.getAttribute('dictionary')) : (this.#slots?.get('list')?.assignedElements() ?? []),
791
+ 'sheet' === this.pickerStyle ? 'sheet-view' : 'body-view', undefined, this.hasAttribute('searchable'), this.getAttribute('label'));
735
792
  if (!level0)
736
793
  return;
737
794
  const path = new NavigationPath(target)?.hydrate();
@@ -776,6 +833,22 @@ export class PickerView extends FormAssociatedBase {
776
833
  this.#reflectSelectionOnCurrentValueLabel();
777
834
  this.#sendValueToForm();
778
835
  }
836
+ #handleInput({ type, target }) {
837
+ debug(`${_a.name} ⚡️ ${type}`);
838
+ if (!(target instanceof HTMLElement))
839
+ return;
840
+ switch (this.pickerStyle) {
841
+ case 'radio-group':
842
+ const radio = target.closest('input[type="radio"]');
843
+ if (!radio)
844
+ return;
845
+ this.#selection = radio.getAttribute('value') ?? '';
846
+ this.#reflectSelectionOnButtons();
847
+ this.#reflectSelectionOnCurrentValueLabel();
848
+ this.#sendValueToForm();
849
+ break;
850
+ }
851
+ }
779
852
  #handleValiditiesSlotchange = ({ type, target: slot }) => {
780
853
  debug(`${_a.name} ⚡️ ${type}`);
781
854
  if (!(slot instanceof HTMLSlotElement))
@@ -803,188 +876,161 @@ export class PickerView extends FormAssociatedBase {
803
876
  <label-view data-role="check" style="visibility: hidden">
804
877
  <image-view slot="icon" system-name="check"></image-view>
805
878
  </label-view>
806
- <h-stack distribution="fill" template="${icon ? 'auto spacer' : null}">
807
- ${icon ? html `<label-view system-image="${icon}"></label-view>` : null}
808
- <v-stack spacing="3" alignment="fill">
809
- <label-view title="${title}"></label-view>
810
- ${subtitle ? html `<label-view foreground="secondary" font="callout" title="${subtitle}"></label-view>` : null}
811
- </v-stack>
812
- </h-stack>
879
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
813
880
  </h-stack>
814
881
  </button>`, mount);
815
- // const btn = $<HTMLButtonElement>(
816
- // `<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>`,
817
- // '>1'
818
- // ),
819
- // hStack = btn.querySelector<LabelView>(':scope>h-stack') ?? undefined
820
- // renderLabel(
821
- // ':scope>label-view:nth-child(2)',
822
- // `<label-view><span></span></label-view>`,
823
- // hStack,
824
- // extractLabelFromGroup(node as HTMLDataListElement),
825
- // extractImgFromGroup(node as HTMLDataListElement)
826
- // )
827
- // // label = btn.querySelector<LabelView>(':scope>h-stack>label-view:nth-child(2)')
828
- // // if (label) {
829
- // // const lbl = extractLabelFromGroup(node as HTMLDataListElement),
830
- // // img =extractImgFromGroup(node as HTMLDataListElement)
831
- // // if(lbl) renderLabelTitle(label, node.dataset.label) //label?.setAttribute('title', el.dataset.label)
832
- // // }
833
882
  return mount.firstElementChild; //btn
834
883
  }
835
- static #wrapOptionTag(node) {
884
+ #wrapOptionTag(node) {
836
885
  debug(`${_a.name} #wrapOptionTag`);
837
886
  const tag = extractTag(node), title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
838
887
  const mount = document.createElement('div');
839
- render(html `<button type="button" tabindex="0" value="${tag}">
840
- <h-stack distribution="leading" template="auto spacer">
841
- <label-view data-role="check">
842
- <image-view slot="icon" system-name="check"></image-view>
843
- </label-view>
844
- <h-stack distribution="fill" template="${icon ? 'auto spacer' : null}">
845
- ${icon ? html `<label-view system-image="${icon}"></label-view>` : null}
846
- <v-stack spacing="3" alignment="fill">
847
- <label-view title="${title}"></label-view>
848
- ${subtitle ? html `<label-view foreground="secondary" font="callout" title="${subtitle}"></label-view>` : null}
849
- </v-stack>
850
- </h-stack>
851
- </h-stack>
852
- </button>`, mount);
853
- // const btn = $(
854
- // `<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>`,
855
- // '>1'
856
- // ),
857
- // hStack = btn.querySelector<HTMLElement>(':scope>h-stack')
858
- // // chevron = hStack?.querySelector<HTMLElement>(':scope>label-view')
859
- // btn.setAttribute('value', extractTagFromOption(node))
860
- // // if (selection !== btn.getAttribute('value')) chevron?.style.setProperty('visibility', 'hidden')
861
- // const label = $<LabelView>(`<label-view></label-view>`, '>1')
862
- // renderLabelTitle(label, extractLabel(node)) // label.querySelector('span')!.textContent = extractCurrentValueFromOption(node) //label.setAttribute('title', extractCurrentValueFromOption(node))
863
- // renderLabelIcon(label, extractIcon(node))
864
- // hStack?.appendChild(label)
865
- return mount.firstElementChild; //btn
888
+ switch (this.pickerStyle) {
889
+ case 'radio-group':
890
+ render(_a.#morphRadioGroupBtn({ name: this.#guuid, title, subtitle, icon, tag }), mount);
891
+ return mount.firstElementChild; //btn
892
+ default:
893
+ render(html `<button type="button" tabindex="0" value="${tag}">
894
+ <h-stack distribution="leading" template="auto spacer">
895
+ <label-view data-role="check">
896
+ <image-view slot="icon" system-name="check"></image-view>
897
+ </label-view>
898
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
899
+ </h-stack>
900
+ </button>`, mount);
901
+ return mount.firstElementChild; //btn
902
+ }
903
+ }
904
+ static #morphRadioGroupGroup({ name, title, subtitle, icon }) {
905
+ return html `<label>
906
+ <h-stack distribution="fill" template="auto spacer" spacing="5">
907
+ <input type="radio" name="${name}" style="min-inline-size: 20px; margin: 0" disabled />
908
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
909
+ </h-stack>
910
+ </label>`;
866
911
  }
867
- static #wrapOptgroupTag(node) {
912
+ static #morphRadioGroupBtn({ name, title, subtitle, icon, tag }) {
913
+ return html `<label>
914
+ <h-stack distribution="fill" template="auto spacer" spacing="5">
915
+ <input type="radio" name="${name}" value="${tag}" style="min-inline-size: 20px; margin: 0" />
916
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
917
+ </h-stack>
918
+ </label>`;
919
+ }
920
+ static #morphITSLabel({ title, subtitle, icon }) {
921
+ return html `<h-stack distribution="fill" template="${icon ? 'auto spacer' : null}">
922
+ ${icon ? html `<label-view system-image="${icon}"></label-view>` : null}
923
+ <v-stack spacing="3" alignment="fill">
924
+ <label-view title="${title}"></label-view>
925
+ ${subtitle ? html `<label-view foreground="secondary" font="callout" title="${subtitle}"></label-view>` : null}
926
+ </v-stack>
927
+ </h-stack>`;
928
+ }
929
+ #wrapOptgroupTag(node) {
868
930
  debug(`${_a.name} #wrapOptgroupTag`);
869
931
  const title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
870
932
  const mount = document.createElement('div');
871
- render(html `<details is="disclosure-group" disclosure-style="marker-trailing">
872
- <summary>
873
- <h-stack distribution="leading" template="auto spacer">
874
- <label-view data-role="check" style="visibility: hidden">
875
- <image-view slot="icon" system-name="check"></image-view>
876
- </label-view>
877
- <h-stack distribution="fill" template="${icon ? 'auto spacer' : null}">
878
- ${icon ? html `<label-view system-image="${icon}"></label-view>` : null}
879
- <v-stack spacing="3" alignment="fill">
880
- <label-view title="${title}"></label-view>
881
- ${subtitle ? html `<label-view foreground="secondary" font="callout" title="${subtitle}"></label-view>` : null}
882
- </v-stack>
933
+ switch (this.pickerStyle) {
934
+ case 'radio-group':
935
+ render(_a.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon }),
936
+ // this.hasAttribute('horizontal-radio-group-layout')
937
+ // ? html`<h-flex-stack style="flex-wrap: wrap">${PickerView.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon })}</h-flex-stack>`
938
+ // : html`<v-stack>${PickerView.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon })}</v-stack>`,
939
+ mount);
940
+ return mount.firstElementChild;
941
+ default:
942
+ render(html `<details is="disclosure-group" disclosure-style="marker-trailing">
943
+ <summary>
944
+ <h-stack distribution="leading" template="auto spacer">
945
+ <label-view data-role="check" style="visibility: hidden">
946
+ <image-view slot="icon" system-name="check"></image-view>
947
+ </label-view>
948
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
949
+ </h-stack>
950
+ </summary>
951
+ </details>`, mount);
952
+ return mount.firstElementChild;
953
+ }
954
+ }
955
+ #wrapDatalistTag(node) {
956
+ const title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
957
+ const mount = document.createElement('div');
958
+ switch (this.pickerStyle) {
959
+ case 'radio-group':
960
+ render(_a.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon }),
961
+ // this.hasAttribute('horizontal-radio-group-layout')
962
+ // ? html`<h-flex-stack style="flex-wrap: wrap">${PickerView.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon })}</h-flex-stack>`
963
+ // : html`<v-stack>${PickerView.#morphRadioGroupGroup({ name: this.#guuid, title, subtitle, icon })}</v-stack>`,
964
+ mount);
965
+ return mount.firstElementChild;
966
+ default:
967
+ render(html `<menu-view tabindex="0">
968
+ <h-stack slot="label" distribution="leading" template="auto spacer">
969
+ <label-view data-role="check" style="visibility: hidden">
970
+ <image-view slot="icon" system-name="check"></image-view>
971
+ </label-view>
972
+ ${_a.#morphITSLabel({ title, subtitle, icon })}
883
973
  </h-stack>
884
- </h-stack>
885
- </summary>
886
- </details>`, mount);
887
- return mount.firstElementChild;
888
- // const labelT = `<label-view><span></span></label-view>`,
889
- // 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>`
890
- // const group = $(`<details is="disclosure-group" disclosure-style="marker-trailing">${summaryT}</details>`, '>1'), // NOTE: already applied, here it covers spawned sheets
891
- // hStack = group.querySelector(':scope>summary>h-stack') ?? undefined // ?? group.appendChild($(summaryT, '>1'))
892
- // // summaryLabel = summary.querySelector(':scope>label-view') ?? summary.appendChild($(labelT, '>1'))
893
- // // if (node.hasAttribute('label')) summaryLabel.setAttribute('title', node.getAttribute('label') ?? '')
894
- // // if (node.hasAttribute('data-system-image')) summaryLabel.setAttribute('system-image', node.getAttribute('data-system-image') ?? '')
895
- // renderLabel(':scope>label-view:nth-child(2)', labelT, hStack, extractLabel(node), extractIcon(node))
896
- // return group
974
+ </menu-view>`, mount);
975
+ return mount.firstElementChild;
976
+ }
897
977
  }
898
- // static #reflectButtons(nodes: Element[], container: Element): void
899
- // static #reflectButtons(nodes: Dictionary, container: Element): void
900
- static #reflectButtons(nodes, container) {
978
+ #reflectButtons(nodes, container) {
901
979
  debug(`${_a.name} #reflectButtons`);
980
+ const flatten = 'radio-group' === this.pickerStyle;
902
981
  for (const node of nodes)
903
982
  if (node instanceof Element)
904
983
  switch (node.tagName) {
905
984
  case 'DATALIST': {
906
- const title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
907
- const mount = document.createElement('div');
908
- render(html `<menu-view tabindex="0">
909
- <h-stack slot="label" distribution="leading" template="auto spacer">
910
- <label-view data-role="check" style="visibility: hidden">
911
- <image-view slot="icon" system-name="check"></image-view>
912
- </label-view>
913
- <h-stack distribution="fill" template="${icon ? 'auto spacer' : null}">
914
- ${icon ? html `<label-view system-image="${icon}"></label-view>` : null}
915
- <v-stack spacing="3" alignment="fill">
916
- <label-view title="${title}"></label-view>
917
- ${subtitle ? html `<label-view foreground="secondary" font="callout" title="${subtitle}"></label-view>` : null}
918
- </v-stack>
919
- </h-stack>
920
- </h-stack>
921
- </menu-view>`, mount);
922
- const group = mount.firstElementChild;
923
- // const group = $(
924
- // `<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>`,
925
- // '>1'
926
- // ),
927
- // hStack = group.querySelector(':scope>h-stack[slot=label]') ?? undefined
928
- // // label = group.querySelector(':scope>label-view[slot=label]') ?? group.appendChild($(`<label-view slot="label"></label-view>`, '>1'))
929
- // // if (node.hasAttribute('data-label')) label.setAttribute('title', node.getAttribute('data-label') ?? '')
930
- // // if (node.hasAttribute('data-system-image')) label.setAttribute('system-image', node.getAttribute('data-system-image') ?? '')
931
- // renderLabel(':scope>label-view:nth-child(2)', `<label-view><span></span></label-view>`, hStack, extractLabel(node as HTMLDataListElement), extractIcon(node as HTMLDataListElement))
932
- _a.#reflectButtons([...node.children], group);
985
+ if (flatten) {
986
+ this.#reflectButtons([...node.children], container);
987
+ break;
988
+ }
989
+ const group = this.#wrapDatalistTag(node);
990
+ this.#reflectButtons([...node.children], group);
933
991
  container.appendChild(group);
934
992
  break;
935
993
  }
936
994
  case 'OPTGROUP': {
937
- const group = _a.#wrapOptgroupTag(node);
938
- _a.#reflectButtons([...node.children], group);
995
+ if (flatten) {
996
+ this.#reflectButtons([...node.children], container);
997
+ break;
998
+ }
999
+ const group = this.#wrapOptgroupTag(node);
1000
+ this.#reflectButtons([...node.children], group);
939
1001
  container.appendChild(group);
940
1002
  break;
941
1003
  }
942
1004
  case 'OPTION':
943
1005
  default: {
944
- container.appendChild(_a.#wrapOptionTag(node));
1006
+ container.appendChild(this.#wrapOptionTag(node));
945
1007
  break;
946
1008
  }
947
1009
  }
948
1010
  else {
949
1011
  if (node.children.length)
950
- if (allLeaves(node)) {
951
- const group = _a.#wrapOptgroupTag(node);
952
- _a.#reflectButtons(node.children, group);
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);
953
1017
  container.appendChild(group);
954
1018
  }
955
1019
  else {
956
- const title = extractLabel(node), subtitle = extractSubtitle(node), icon = extractIcon(node);
957
- const mount = document.createElement('div');
958
- render(html `<menu-view tabindex="0">
959
- <h-stack slot="label" distribution="leading" template="auto spacer">
960
- <label-view data-role="check" style="visibility: hidden">
961
- <image-view slot="icon" system-name="check"></image-view>
962
- </label-view>
963
- <h-stack distribution="fill" template="${icon ? 'auto spacer' : null}">
964
- ${icon ? html `<label-view system-image="${icon}"></label-view>` : null}
965
- <v-stack spacing="3" alignment="fill">
966
- <label-view title="${title}"></label-view>
967
- ${subtitle ? html `<label-view foreground="secondary" font="callout" title="${subtitle}"></label-view>` : null}
968
- </v-stack>
969
- </h-stack>
970
- </h-stack>
971
- </menu-view>`, mount);
972
- const group = mount.firstElementChild;
973
- // const group = $(`<menu-view tabindex="0"></menu-view>`, '>1')
974
- // renderLabel(':scope>label-view[slot=label]', `<label-view slot="label"><span></span></label-view>`, group, extractLabel(node), extractIcon(node))
975
- _a.#reflectButtons(node.children, group);
1020
+ const group = this.#wrapDatalistTag(node);
1021
+ this.#reflectButtons(node.children, group);
976
1022
  container.appendChild(group);
977
1023
  }
978
1024
  else
979
- container.appendChild(_a.#wrapOptionTag(node));
1025
+ container.appendChild(this.#wrapOptionTag(node));
980
1026
  }
981
1027
  }
982
1028
  // #reflectPlaceholder(value: string | null) {
983
1029
  // debug(`#reflectPlaceholder`)
984
1030
  // const input = this.#shadowRoot.querySelector('input')
985
1031
  // if (input) {
986
- // if (value) input.setAttribute((this.constructor as typeof PickerView).ATTR.PLACEHOLDER, value)
987
- // else input.removeAttribute((this.constructor as typeof PickerView).ATTR.PLACEHOLDER)
1032
+ // if (value) input.setAttribute('prompt', value)
1033
+ // else input.removeAttribute('prompt')
988
1034
  // }
989
1035
  // }
990
1036
  #reflectLabel(value) {
@@ -999,11 +1045,11 @@ export class PickerView extends FormAssociatedBase {
999
1045
  const groupMap = indexGroups(this.#lastIndexedRoot);
1000
1046
  const groupContainsSelection = (source) => source instanceof Element ? [...source.querySelectorAll('option')].some((opt) => extractTag(opt) === this.#selection) : collectLeafValues(source).includes(this.#selection);
1001
1047
  const syncButtons = (root) => {
1002
- // 1. plain value buttons — unchanged
1048
+ // 1. plain value buttons
1003
1049
  for (const el of root.querySelectorAll('button[value]:not([slot])'))
1004
1050
  // $.prop('visibility', el.getAttribute('value') === this.#selection ? 'visible' : 'hidden', el.querySelector<HTMLElement>('label-view[data-role="check"]'))
1005
1051
  el.querySelector('label-view[data-role="check"]')?.style.setProperty('visibility', el.getAttribute('value') === this.#selection ? 'visible' : 'hidden');
1006
- // 2. details/optgroups — unchanged
1052
+ // 2. details/optgroups
1007
1053
  for (const details of root.querySelectorAll('details[is="disclosure-group"]')) {
1008
1054
  const hasSelectedDescendant = [...details.querySelectorAll('button[value]')].some((btn) => btn.getAttribute('value') === this.#selection);
1009
1055
  details.querySelector(':scope>summary label-view[data-role="check"]')?.style.setProperty('visibility', hasSelectedDescendant ? 'visible' : 'hidden');
@@ -1014,6 +1060,9 @@ export class PickerView extends FormAssociatedBase {
1014
1060
  const hasSelectedDescendant = source ? groupContainsSelection(source) : false;
1015
1061
  btn.querySelector('label-view[data-role="check"]')?.style.setProperty('visibility', hasSelectedDescendant ? 'visible' : 'hidden');
1016
1062
  }
1063
+ // 4. radio inputs
1064
+ for (const el of root.querySelectorAll('input[type=radio][value]'))
1065
+ el.checked = el.getAttribute('value') === this.#selection;
1017
1066
  };
1018
1067
  syncButtons(this);
1019
1068
  if (this.#spawn)
@@ -1034,7 +1083,7 @@ export class PickerView extends FormAssociatedBase {
1034
1083
  break;
1035
1084
  // if (!cvl) currentValueLabel.setAttribute('foreground', 'secondary')
1036
1085
  // else currentValueLabel.removeAttribute('foreground')
1037
- const title = this.#currentValueLabel || this.#selection || this.getAttribute(this.constructor.ATTR.PLACEHOLDER), systemImage = this.#currentValueIcon || this.getAttribute(this.constructor.ATTR.PLACEHOLDER_ICON);
1086
+ const title = this.#currentValueLabel || this.#selection || this.getAttribute('prompt'), systemImage = this.#currentValueIcon || this.getAttribute('prompt-icon');
1038
1087
  queryMorph(':not([slot])', html `<span>${title}</span>`, currentValueLabel, { removeIf: !title });
1039
1088
  queryMorph('[slot=icon]', html `<image-view slot="icon" system-name="${systemImage}"></image-view>`, currentValueLabel, { removeIf: !systemImage });
1040
1089
  // morph(
@@ -1049,7 +1098,7 @@ export class PickerView extends FormAssociatedBase {
1049
1098
  break;
1050
1099
  // if (!cvl) currentValueLabel.setAttribute('foreground', 'secondary')
1051
1100
  // else currentValueLabel.removeAttribute('foreground')
1052
- const title = this.#currentValueLabel || this.#selection || this.getAttribute(this.constructor.ATTR.PLACEHOLDER), systemImage = this.#currentValueIcon || this.getAttribute(this.constructor.ATTR.PLACEHOLDER_ICON);
1101
+ const title = this.#currentValueLabel || this.#selection || this.getAttribute('prompt'), systemImage = this.#currentValueIcon || this.getAttribute('prompt-icon');
1053
1102
  queryMorph(':not([slot])', html `<span>${title}</span>`, currentValueLabel, { removeIf: !title });
1054
1103
  queryMorph('[slot=icon]', html `<image-view slot="icon" system-name="${systemImage}"></image-view>`, currentValueLabel, { removeIf: !systemImage });
1055
1104
  // morph(
@@ -1082,10 +1131,10 @@ export class PickerView extends FormAssociatedBase {
1082
1131
  //udnefined
1083
1132
  }
1084
1133
  }
1085
- if (this.hasAttribute(this.constructor.ATTR.TRIGGER_HELP))
1086
- trigger?.setAttribute(this.constructor.ATTR.TRIGGER_HELP, this.getAttribute(this.constructor.ATTR.TRIGGER_HELP) ?? '');
1134
+ if (this.hasAttribute('help'))
1135
+ trigger?.setAttribute('help', this.getAttribute('help') ?? '');
1087
1136
  else
1088
- trigger?.removeAttribute(this.constructor.ATTR.TRIGGER_HELP);
1137
+ trigger?.removeAttribute('help');
1089
1138
  }
1090
1139
  /**
1091
1140
  * Form participation property