svseeds 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,153 +1,107 @@
1
1
  <script module lang="ts">
2
2
  export type TagsInputProps = {
3
3
  values?: string[], // bindable
4
- type?: "left" | "right" | "bottom", // <"left">
4
+ value?: string, // bindable
5
+ type?: "left" | "right", // <"left">
5
6
  confirm?: string[], // <["Enter"]>
6
7
  trim?: boolean, // <true>
7
8
  unique?: boolean, // <true>
8
- min?: TagCountValidation,
9
- max?: TagCountValidation,
10
- validations?: TagsInputValidation[],
9
+ ariaErrMsgId?: string, // bindable
10
+ events?: TagsInputEvents,
11
11
  status?: string, // bindable <STATE.DEFAULT>
12
12
  style?: SVSStyle,
13
+ attributes?: HTMLInputAttributes,
14
+ action?: Action,
13
15
  element?: HTMLInputElement, // bindable
14
16
  deps?: TagsInputDeps,
15
17
  };
16
18
  export type TagsInputDeps = {
17
- svsTextField?: Omit<TextFieldProps, TextFieldReqdProps | TextFieldBindProps>,
18
19
  svsBadge?: Omit<BadgeProps, BadgeReqdProps | BadgeBindProps | "type" | "href">,
19
20
  };
20
21
  export type TagsInputReqdProps = never;
21
- export type TagsInputBindProps = "dark" | "status" | "element";
22
- export type TagsInputValidation = (values: string[]) => string;
23
- export type TagCountValidation = { value: number, message: string };
22
+ export type TagsInputBindProps = "values" | "value" | "ariaErrMsgId" | "status" | "element";
23
+ export type TagsInputEvents = { // cancel if true
24
+ onadd?: (values: string[], value: string) => void | boolean,
25
+ onremove?: (values: string[], value: string, index: number) => void | boolean,
26
+ };
24
27
 
28
+ type TagsInputTarget = { currentTarget: EventTarget & HTMLInputElement };
29
+ type BadgeTarget = { currentTarget: EventTarget & HTMLButtonElement; };
25
30
  const preset = "svs-tags-input";
26
31
  const CONFIRM_KEY = "Enter";
27
32
 
28
- import { untrack } from "svelte";
33
+ import { type Action } from "svelte/action";
34
+ import { type HTMLInputAttributes } from "svelte/elements";
29
35
  import { type SVSStyle, STATE, AREA, fnClass, omit } from "./core";
30
- import TextField, { type TextFieldProps, type TextFieldReqdProps, type TextFieldBindProps } from "./_TextField.svelte";
31
36
  import Badge, { type BadgeProps, type BadgeReqdProps, type BadgeBindProps } from "./_Badge.svelte";
32
37
  </script>
33
38
 
34
39
  <script lang="ts">
35
- let { values = $bindable([]), type = "left", confirm = [], trim = true, unique = true, min, max, validations = [], status = $bindable(""), style, element = $bindable(), deps }: TagsInputProps = $props();
40
+ let { values = $bindable([]), value = $bindable(""), type = "left", confirm = [], trim = true, unique = true, ariaErrMsgId = $bindable(), events, status = $bindable(""), style, attributes, action, element = $bindable(), deps }: TagsInputProps = $props();
36
41
 
37
42
  // *** Initialize *** //
38
43
  if (!status) status = STATE.DEFAULT;
39
44
  const cls = fnClass(preset, style);
45
+ const attrs = omit(attributes, "class", "type", "onkeydown");
40
46
  const confirmKeys = new Set([CONFIRM_KEY, ...confirm]);
41
- const textValidations = deps?.svsTextField?.validations ?? [];
42
- if (max) textValidations.unshift((_) => values.length >= max.value ? max.message : "");
43
- if (min) validations.unshift((values) => values.length < min.value ? min.message : "");
44
-
45
- const left = type === "left" || deps?.svsTextField?.left ? sideLeft : undefined;
46
- const right = type === "right" || deps?.svsTextField?.right ? sideRight : undefined;
47
- let value = $state("");
48
47
 
49
48
  // *** Initialize Deps *** //
50
49
  const svsBadge = {
51
50
  ...omit(deps?.svsBadge, "onclick", "right", "style"),
52
51
  style: deps?.svsBadge?.style ?? `${preset} svs-badge`,
53
52
  };
54
- const svsTextField = {
55
- ...omit(deps?.svsTextField, "validations", "style", "attributes"),
56
- style: deps?.svsTextField?.style ?? `${preset} svs-text-field`,
57
- attributes: { ...deps?.svsTextField?.attributes, onkeydown },
58
- };
59
-
60
- // *** Bind Handlers *** //
61
- $effect.pre(() => {
62
- values;
63
- untrack(() => validateTags());
64
- });
65
- function validateText(): boolean {
66
- if (!element) return false;
67
- for (const v of textValidations) {
68
- const msg = v(value);
69
- if (msg) return showMessageTemporarily(msg);
70
- }
71
- return true;
72
- }
73
- function showMessageTemporarily(msg: string): boolean {
74
- element?.setCustomValidity(msg);
75
- element?.checkValidity();
76
- element?.setCustomValidity("");
77
- return false;
78
- }
79
- function validateTags() {
80
- if (!element) return;
81
- for (const v of validations) {
82
- const msg = v(values);
83
- if (msg) return element.setCustomValidity(msg);
84
- }
85
- }
86
53
 
87
54
  // *** Event Handlers *** //
88
- function onkeydown(ev: KeyboardEvent) {
89
- deps?.svsTextField?.attributes?.onkeydown?.(ev as any);
55
+ function onkeydown(ev: KeyboardEvent & TagsInputTarget) {
56
+ attributes?.onkeydown?.(ev);
90
57
  if (!confirmKeys.has(ev.key) || ev.isComposing) return;
91
58
  ev.preventDefault();
59
+ if (events?.onadd?.(values, value)) return;
92
60
  if (trim) value = value.trim();
93
- const valid = validateText();
94
- if (valid) addTag();
95
- validateTags();
61
+ add();
96
62
  }
97
- function addTag() {
63
+ function add() {
98
64
  if (unique && values.includes(value)) value = "";
99
65
  if (!value) return;
100
- values.push(value);
66
+ values = [...values, value];
101
67
  value = "";
102
68
  }
103
- function removeTag(index: number): (ev: Event) => void {
69
+ function remove(index: number): (ev: MouseEvent & BadgeTarget) => void {
104
70
  return (ev) => {
105
- deps?.svsBadge?.onclick?.(ev as any);
106
- values.splice(index, 1);
107
- validateTags();
71
+ deps?.svsBadge?.onclick?.(ev);
72
+ if (events?.onremove?.(values, values[index], index)) return;
73
+ values = values.filter((_, i) => i !== index);
108
74
  };
109
75
  }
110
- $effect(() => untrack(() => validateTags()))
111
76
  </script>
112
77
 
113
78
  <!---------------------------------------->
114
79
 
115
- <TextField bind:value bind:status bind:element type="text" {left} {right} {...svsTextField} />
116
- {#if type === "bottom"}
117
- <div class={cls(AREA.BOTTOM, status)}>
118
- {@render tags()}
119
- </div>
120
- {/if}
121
-
122
- {#snippet sideLeft(status: string)}
123
- {#if type === "left"}
124
- {@render tags()}
80
+ <div class={cls(AREA.WHOLE, status)}>
81
+ {@render tags(type === "left")}
82
+ {#if action}
83
+ <input bind:value bind:this={element} class={cls(AREA.MAIN, status)} type="text" {onkeydown} {...attrs} use:action />
125
84
  {:else}
126
- {#if deps?.svsTextField?.left}
127
- {@render deps.svsTextField.left(status, "", element)}
128
- {/if}
85
+ <input bind:value bind:this={element} class={cls(AREA.MAIN, status)} type="text" {onkeydown} {...attrs} />
129
86
  {/if}
130
- {/snippet}
131
- {#snippet sideRight(status: string)}
132
- {#if type === "right"}
133
- {@render tags()}
134
- {:else}
135
- {#if deps?.svsTextField?.right}
136
- {@render deps.svsTextField.right(status, "", element)}
137
- {/if}
87
+ {@render tags(type === "right")}
88
+ </div>
89
+
90
+ {#snippet tags(render: boolean)}
91
+ {#if render}
92
+ <span class={cls(type, status)}>
93
+ {#each values as value, i}
94
+ <Badge bind:status type="right" onclick={remove(i)} {...svsBadge}>
95
+ {value}
96
+ {#snippet right(status)}
97
+ {#if deps?.svsBadge?.right}
98
+ {@render deps.svsBadge.right(status)}
99
+ {:else}
100
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="min-width: 5px; min-height: 5px;"><path d="M511.998 70.682 441.315 0 256.002 185.313 70.685 0 .002 70.692l185.314 185.314L.002 441.318 70.69 512l185.312-185.312L441.315 512l70.683-70.682-185.314-185.312z" /></svg>
101
+ {/if}
102
+ {/snippet}
103
+ </Badge>
104
+ {/each}
105
+ </span>
138
106
  {/if}
139
107
  {/snippet}
140
- {#snippet tags()}
141
- {#each values as value, i}
142
- <Badge type="right" onclick={removeTag(i)} {...svsBadge}>
143
- {#snippet right(status)}
144
- {#if deps?.svsBadge?.right}
145
- {@render deps.svsBadge.right(status)}
146
- {:else}
147
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="min-width: 5px; min-height: 5px;"><path d="M511.998 70.682 441.315 0 256.002 185.313 70.685 0 .002 70.692l185.314 185.314L.002 441.318 70.69 512l185.312-185.312L441.315 512l70.683-70.682-185.314-185.312z" /></svg>
148
- {/if}
149
- {/snippet}
150
- {value}
151
- </Badge>
152
- {/each}
153
- {/snippet}
@@ -1,31 +1,32 @@
1
1
  export type TagsInputProps = {
2
2
  values?: string[];
3
- type?: "left" | "right" | "bottom";
3
+ value?: string;
4
+ type?: "left" | "right";
4
5
  confirm?: string[];
5
6
  trim?: boolean;
6
7
  unique?: boolean;
7
- min?: TagCountValidation;
8
- max?: TagCountValidation;
9
- validations?: TagsInputValidation[];
8
+ ariaErrMsgId?: string;
9
+ events?: TagsInputEvents;
10
10
  status?: string;
11
11
  style?: SVSStyle;
12
+ attributes?: HTMLInputAttributes;
13
+ action?: Action;
12
14
  element?: HTMLInputElement;
13
15
  deps?: TagsInputDeps;
14
16
  };
15
17
  export type TagsInputDeps = {
16
- svsTextField?: Omit<TextFieldProps, TextFieldReqdProps | TextFieldBindProps>;
17
18
  svsBadge?: Omit<BadgeProps, BadgeReqdProps | BadgeBindProps | "type" | "href">;
18
19
  };
19
20
  export type TagsInputReqdProps = never;
20
- export type TagsInputBindProps = "dark" | "status" | "element";
21
- export type TagsInputValidation = (values: string[]) => string;
22
- export type TagCountValidation = {
23
- value: number;
24
- message: string;
21
+ export type TagsInputBindProps = "values" | "value" | "ariaErrMsgId" | "status" | "element";
22
+ export type TagsInputEvents = {
23
+ onadd?: (values: string[], value: string) => void | boolean;
24
+ onremove?: (values: string[], value: string, index: number) => void | boolean;
25
25
  };
26
+ import { type Action } from "svelte/action";
27
+ import { type HTMLInputAttributes } from "svelte/elements";
26
28
  import { type SVSStyle } from "./core";
27
- import { type TextFieldProps, type TextFieldReqdProps, type TextFieldBindProps } from "./_TextField.svelte";
28
29
  import { type BadgeProps, type BadgeReqdProps, type BadgeBindProps } from "./_Badge.svelte";
29
- declare const TagsInput: import("svelte").Component<TagsInputProps, {}, "status" | "element" | "values">;
30
+ declare const TagsInput: import("svelte").Component<TagsInputProps, {}, "value" | "status" | "element" | "values" | "ariaErrMsgId">;
30
31
  type TagsInput = ReturnType<typeof TagsInput>;
31
32
  export default TagsInput;
@@ -0,0 +1,169 @@
1
+ <script module lang="ts">
2
+ export type TagsInputFieldProps = {
3
+ label?: string,
4
+ extra?: string,
5
+ aux?: Snippet<[string, string[], HTMLInputElement | undefined]>, // Snippet<[status,values,element]>
6
+ left?: Snippet<[string, string[], HTMLInputElement | undefined]>, // Snippet<[status,values,element]>
7
+ right?: Snippet<[string, string[], HTMLInputElement | undefined]>, // Snippet<[status,values,element]>
8
+ bottom?: string,
9
+ descFirst?: boolean, // <false>
10
+ values?: string[], // bindable
11
+ min?: TagsInputFieldCountValidation,
12
+ max?: TagsInputFieldCountValidation,
13
+ constraints?: TagsInputFieldConstraint[],
14
+ validations?: TagsInputFieldValidation[],
15
+ status?: string, // bindable <STATE.DEFAULT>
16
+ style?: SVSStyle,
17
+ element?: HTMLInputElement, // bindable
18
+ deps?: TagsInputFieldDeps,
19
+ };
20
+ export type TagsInputFieldDeps = {
21
+ svsTagsInput?: Omit<TagsInputProps, TagsInputReqdProps | TagsInputBindProps | "deps">,
22
+ } & TagsInputDeps;
23
+ export type TagsInputFieldReqdProps = never;
24
+ export type TagsInputFieldBindProps = "values" | "status" | "element";
25
+ export type TagsInputFieldConstraint = (value: string, validity: ValidityState) => string;
26
+ export type TagsInputFieldValidation = (values: string[], validity: ValidityState) => string;
27
+ export type TagsInputFieldCountValidation = { value: number, message: string };
28
+
29
+ const preset = "svs-tags-input-field";
30
+
31
+ import { type Snippet, untrack } from "svelte";
32
+ import { type SVSStyle, STATE, AREA, elemId, fnClass, isNeutral, omit } from "./core";
33
+ import TagsInput, { type TagsInputProps, type TagsInputReqdProps, type TagsInputBindProps, type TagsInputDeps } from "./TagsInput.svelte";
34
+ </script>
35
+
36
+ <script lang="ts">
37
+ let { label, extra, aux, left, right, bottom, descFirst = false, values = $bindable([]), min, max, constraints = [], validations = [], status = $bindable(""), style, element = $bindable(), deps }: TagsInputFieldProps = $props();
38
+
39
+ // *** Initialize *** //
40
+ if (!status) status = STATE.DEFAULT;
41
+ const cls = fnClass(preset, style);
42
+ const id = deps?.svsTagsInput?.attributes?.id ? deps.svsTagsInput.attributes.id : elemId.get(label?.trim());
43
+ const idLabel = elemId.get(label?.trim());
44
+ const idDesc = elemId.get(bottom?.trim());
45
+ const idErr = idDesc ?? elemId.id;
46
+ let message = $state(bottom);
47
+ let value = $state("");
48
+ if (max) constraints.unshift(() => values.length >= max.value ? max.message : "");
49
+ if (min) validations.unshift(() => values.length < min.value ? min.message : "");
50
+
51
+ // *** Initialize Deps *** //
52
+ const svsBadge = {
53
+ ...omit(deps?.svsBadge, "style"),
54
+ style: deps?.svsBadge?.style ?? `${preset} svs-tags-input svs-badge`,
55
+ };
56
+ const svsTagsInput = {
57
+ ...omit(deps?.svsTagsInput, "style", "attributes"),
58
+ events: { onadd, onremove: deps?.svsTagsInput?.events?.onremove },
59
+ style: deps?.svsTagsInput?.style ?? `${preset} svs-tags-input`,
60
+ attributes: {
61
+ ...omit(deps?.svsTagsInput?.attributes, "id", "onchange", "oninvalid", "aria-describedby"),
62
+ id,
63
+ onchange,
64
+ oninvalid,
65
+ "aria-describedby": idDesc,
66
+ },
67
+ };
68
+
69
+ // *** Status *** //
70
+ let neutral = isNeutral(status) ? status : STATE.DEFAULT;
71
+ $effect(() => { neutral = isNeutral(status) ? status : neutral });
72
+ let live = $derived(status === STATE.INACTIVE ? "alert" : "status");
73
+ let idMsg = $derived(status === STATE.INACTIVE && message?.trim() ? idErr : undefined);
74
+ function shift(oninvalid: boolean = false, msg?: string) {
75
+ const vmsg = element?.validationMessage ?? "";
76
+ status = getStatus(oninvalid, vmsg, msg);
77
+ message = status === STATE.INACTIVE ? msg ? msg : vmsg ? vmsg : bottom : bottom;
78
+ }
79
+ function getStatus(oninvalid: boolean, vmsg: string, msg?: string): string {
80
+ if (msg || (oninvalid && vmsg)) return STATE.INACTIVE;
81
+ if (!values.length || vmsg) return neutral;
82
+ return STATE.ACTIVE;
83
+ }
84
+ function verify() {
85
+ if (!element) return;
86
+ for (const v of validations) {
87
+ const msg = v(values, element.validity);
88
+ if (msg) return element.setCustomValidity(msg);
89
+ }
90
+ element.setCustomValidity("");
91
+ }
92
+
93
+ // *** Bind Handlers *** //
94
+ $effect.pre(() => {
95
+ values;
96
+ untrack(() => validate());
97
+ });
98
+ function validate() {
99
+ verify();
100
+ shift();
101
+ }
102
+
103
+ // *** Event Handlers *** //
104
+ function onadd(values: string[], value: string): void | boolean {
105
+ if (deps?.svsTagsInput?.events?.onadd?.(values, value)) return true;
106
+ status = neutral;
107
+ shift(false, check());
108
+ return status === STATE.INACTIVE;
109
+ }
110
+ function check(): string | undefined {
111
+ if (!element) return;
112
+ for (const c of constraints) {
113
+ const msg = c(value, element.validity);
114
+ if (msg) return msg;
115
+ }
116
+ }
117
+ function onchange(ev: Event) {
118
+ deps?.svsTagsInput?.attributes?.onchange?.(ev as any);
119
+ if (!isNeutral(status)) shift();
120
+ }
121
+ function oninvalid(ev: Event) {
122
+ deps?.svsTagsInput?.attributes?.oninvalid?.(ev as any);
123
+ ev.preventDefault();
124
+ shift(true);
125
+ }
126
+ $effect(() => untrack(() => verify()));
127
+ </script>
128
+
129
+ <!---------------------------------------->
130
+
131
+ <div class={cls(AREA.WHOLE, status)} role="group" aria-labelledby={idLabel}>
132
+ {#if aux}
133
+ <div class={cls(AREA.TOP, status)}>
134
+ {@render lbl()}
135
+ <span class={cls(AREA.AUX, status)}>{@render aux(status, values, element)}</span>
136
+ </div>
137
+ {:else}
138
+ {@render lbl()}
139
+ {/if}
140
+ {@render desc(descFirst)}
141
+ <div class={cls(AREA.MIDDLE, status)}>
142
+ {@render side(AREA.LEFT, left)}
143
+ <TagsInput bind:values bind:value bind:status bind:element bind:ariaErrMsgId={idMsg} {...svsTagsInput} deps={{ svsBadge }} />
144
+ {@render side(AREA.RIGHT, right)}
145
+ </div>
146
+ {@render desc(!descFirst)}
147
+ </div>
148
+
149
+ {#snippet lbl()}
150
+ {#if label?.trim()}
151
+ <label class={cls(AREA.LABEL, status)} for={id} id={idLabel}>
152
+ {label}
153
+ {#if extra?.trim()}
154
+ <span class={cls(AREA.EXTRA, status)}>{extra}</span>
155
+ {/if}
156
+ </label>
157
+ {/if}
158
+ {/snippet}
159
+ {#snippet side(area: string, body?: Snippet<[string, string[], HTMLInputElement | undefined]>)}
160
+ {#if body}
161
+ <span class={cls(area, status)}>{@render body(status, values, element)}</span>
162
+ {/if}
163
+ {/snippet}
164
+ {#snippet desc(show: boolean)}
165
+ {#if show && message?.trim()}
166
+ <div class={cls(AREA.BOTTOM, status)} id={idDesc ?? idErr} role={live}>{message}</div>
167
+ {/if}
168
+ {/snippet}
169
+
@@ -0,0 +1,35 @@
1
+ export type TagsInputFieldProps = {
2
+ label?: string;
3
+ extra?: string;
4
+ aux?: Snippet<[string, string[], HTMLInputElement | undefined]>;
5
+ left?: Snippet<[string, string[], HTMLInputElement | undefined]>;
6
+ right?: Snippet<[string, string[], HTMLInputElement | undefined]>;
7
+ bottom?: string;
8
+ descFirst?: boolean;
9
+ values?: string[];
10
+ min?: TagsInputFieldCountValidation;
11
+ max?: TagsInputFieldCountValidation;
12
+ constraints?: TagsInputFieldConstraint[];
13
+ validations?: TagsInputFieldValidation[];
14
+ status?: string;
15
+ style?: SVSStyle;
16
+ element?: HTMLInputElement;
17
+ deps?: TagsInputFieldDeps;
18
+ };
19
+ export type TagsInputFieldDeps = {
20
+ svsTagsInput?: Omit<TagsInputProps, TagsInputReqdProps | TagsInputBindProps | "deps">;
21
+ } & TagsInputDeps;
22
+ export type TagsInputFieldReqdProps = never;
23
+ export type TagsInputFieldBindProps = "values" | "status" | "element";
24
+ export type TagsInputFieldConstraint = (value: string, validity: ValidityState) => string;
25
+ export type TagsInputFieldValidation = (values: string[], validity: ValidityState) => string;
26
+ export type TagsInputFieldCountValidation = {
27
+ value: number;
28
+ message: string;
29
+ };
30
+ import { type Snippet } from "svelte";
31
+ import { type SVSStyle } from "./core";
32
+ import { type TagsInputProps, type TagsInputReqdProps, type TagsInputBindProps, type TagsInputDeps } from "./TagsInput.svelte";
33
+ declare const TagsInputField: import("svelte").Component<TagsInputFieldProps, {}, "status" | "element" | "values">;
34
+ type TagsInputField = ReturnType<typeof TagsInputField>;
35
+ export default TagsInputField;
@@ -0,0 +1,134 @@
1
+ <script module lang="ts">
2
+ export type ToggleGroupFieldProps = {
3
+ options: SvelteMap<string, string> | Map<string, string>,
4
+ label?: string,
5
+ extra?: string,
6
+ aux?: Snippet<[string, string[]]>, // Snippet<[status,values]>
7
+ left?: Snippet<[string, string[]]>, // Snippet<[status,values]>
8
+ right?: Snippet<[string, string[]]>, // Snippet<[status,values]>
9
+ bottom?: string,
10
+ descFirst?: boolean, // <false>
11
+ values?: string[], // bindable
12
+ multiple?: boolean, // <true>
13
+ validations?: ToggleGroupFieldValidation[],
14
+ status?: string, // bindable <STATE.DEFAULT>
15
+ style?: SVSStyle,
16
+ deps?: ToggleGroupFieldDeps,
17
+ [key: string]: unknown | Snippet,
18
+ };
19
+ export type ToggleGroupFieldDeps = {
20
+ svsToggleGroup?: Omit<ToggleGroupProps, ToggleGroupReqdProps | ToggleGroupBindProps | "ariaDescId" | "multiple">,
21
+ };
22
+ export type ToggleGroupFieldReqdProps = "options";
23
+ export type ToggleGroupFieldBindProps = "values" | "status";
24
+ export type ToggleGroupFieldValidation = (values: string[]) => string;
25
+
26
+ const preset = "svs-toggle-group-field";
27
+
28
+ import { type Snippet, untrack } from "svelte";
29
+ import { type Action } from "svelte/action";
30
+ import { type SvelteMap } from "svelte/reactivity";
31
+ import { type SVSStyle, STATE, AREA, elemId, fnClass, isNeutral } from "./core";
32
+ import ToggleGroup, { type ToggleGroupProps, type ToggleGroupReqdProps, type ToggleGroupBindProps } from "./_ToggleGroup.svelte";
33
+ </script>
34
+
35
+ <script lang="ts">
36
+ let { options, label, extra, aux, left, right, bottom, descFirst = false, values = $bindable([]), multiple = true, validations = [], status = $bindable(""), style, attributes, elements = $bindable([]), deps, ...rest }: ToggleGroupFieldProps = $props();
37
+
38
+ // *** Initialize *** //
39
+ if (!status) status = STATE.DEFAULT;
40
+ const cls = fnClass(preset, style);
41
+ const idLabel = elemId.get(label?.trim());
42
+ const idDesc = elemId.get(bottom?.trim());
43
+ const idErr = idDesc ?? elemId.id;
44
+ let message = $state(bottom);
45
+ let element: HTMLInputElement | undefined = $state();
46
+
47
+ // *** Initialize Deps *** //
48
+ const svsToggleGroup = {
49
+ ...Object.fromEntries(Object.entries(rest).filter(([_, v]) => typeof v === "function")),
50
+ ariaDescId: idDesc,
51
+ style: deps?.svsToggleGroup?.style as SVSStyle ?? `${preset} svs-toggle-group`,
52
+ action: deps?.svsToggleGroup?.action as Action,
53
+ };
54
+
55
+ // *** Status *** //
56
+ let neutral = $state(isNeutral(status) ? status : STATE.DEFAULT);
57
+ $effect(() => { neutral = isNeutral(status) ? status : neutral });
58
+ let live = $derived(status === STATE.INACTIVE ? "alert" : "status");
59
+ let idMsg = $derived(status === STATE.INACTIVE && message?.trim() ? idErr : undefined);
60
+ function shift(oninvalid?: boolean) {
61
+ const vmsg = element?.validationMessage ?? "";
62
+ status = oninvalid && vmsg ? STATE.INACTIVE : (!values.length || vmsg) ? neutral : STATE.ACTIVE;
63
+ message = status === STATE.INACTIVE ? vmsg ? vmsg : bottom : bottom;
64
+ }
65
+ function verify() {
66
+ if (!element) return;
67
+ for (const v of validations) {
68
+ const msg = v(values);
69
+ if (msg) return element.setCustomValidity(msg);
70
+ }
71
+ element.setCustomValidity("");
72
+ }
73
+
74
+ // *** Bind Handlers *** //
75
+ $effect.pre(() => {
76
+ values;
77
+ untrack(() => validate());
78
+ });
79
+ function validate() {
80
+ verify();
81
+ shift();
82
+ }
83
+
84
+ // *** Event Handlers *** //
85
+ function oninvalid(ev: Event) {
86
+ ev.preventDefault();
87
+ shift(true)
88
+ }
89
+ $effect(() => untrack(() => verify()));
90
+ </script>
91
+
92
+ <!---------------------------------------->
93
+
94
+ {#if options.size}
95
+ <div class={cls(AREA.WHOLE, status)} role="group" aria-labelledby={idLabel}>
96
+ {#if aux}
97
+ <div class={cls(AREA.TOP, status)}>
98
+ {@render lbl()}
99
+ <span class={cls(AREA.AUX, status)}>{@render aux(status, values)}</span>
100
+ </div>
101
+ {:else}
102
+ {@render lbl()}
103
+ {/if}
104
+ {@render desc(descFirst)}
105
+ <div class={cls(AREA.MIDDLE, status)}>
106
+ {@render side(AREA.LEFT, left)}
107
+ <input bind:this={element} style="display: none;" aria-hidden="true" {oninvalid} />
108
+ <ToggleGroup bind:values bind:ariaErrMsgId={idMsg} bind:status={neutral} {options} {multiple} {...svsToggleGroup} />
109
+ {@render side(AREA.RIGHT, right)}
110
+ </div>
111
+ {@render desc(!descFirst)}
112
+ </div>
113
+ {/if}
114
+
115
+ {#snippet lbl()}
116
+ {#if label?.trim()}
117
+ <span class={cls(AREA.LABEL, status)} id={idLabel}>
118
+ {label}
119
+ {#if extra?.trim()}
120
+ <span class={cls(AREA.EXTRA, status)}>{extra}</span>
121
+ {/if}
122
+ </span>
123
+ {/if}
124
+ {/snippet}
125
+ {#snippet side(area: string, body?: Snippet<[string, string[]]>)}
126
+ {#if body}
127
+ <span class={cls(area, status)}>{@render body(status, values)}</span>
128
+ {/if}
129
+ {/snippet}
130
+ {#snippet desc(show: boolean)}
131
+ {#if show && message?.trim()}
132
+ <div class={cls(AREA.BOTTOM, status)} id={idDesc ?? idErr} role={live}>{message}</div>
133
+ {/if}
134
+ {/snippet}
@@ -0,0 +1,30 @@
1
+ export type ToggleGroupFieldProps = {
2
+ options: SvelteMap<string, string> | Map<string, string>;
3
+ label?: string;
4
+ extra?: string;
5
+ aux?: Snippet<[string, string[]]>;
6
+ left?: Snippet<[string, string[]]>;
7
+ right?: Snippet<[string, string[]]>;
8
+ bottom?: string;
9
+ descFirst?: boolean;
10
+ values?: string[];
11
+ multiple?: boolean;
12
+ validations?: ToggleGroupFieldValidation[];
13
+ status?: string;
14
+ style?: SVSStyle;
15
+ deps?: ToggleGroupFieldDeps;
16
+ [key: string]: unknown | Snippet;
17
+ };
18
+ export type ToggleGroupFieldDeps = {
19
+ svsToggleGroup?: Omit<ToggleGroupProps, ToggleGroupReqdProps | ToggleGroupBindProps | "ariaDescId" | "multiple">;
20
+ };
21
+ export type ToggleGroupFieldReqdProps = "options";
22
+ export type ToggleGroupFieldBindProps = "values" | "status";
23
+ export type ToggleGroupFieldValidation = (values: string[]) => string;
24
+ import { type Snippet } from "svelte";
25
+ import { type SvelteMap } from "svelte/reactivity";
26
+ import { type SVSStyle } from "./core";
27
+ import { type ToggleGroupProps, type ToggleGroupReqdProps, type ToggleGroupBindProps } from "./_ToggleGroup.svelte";
28
+ declare const ToggleGroupField: import("svelte").Component<ToggleGroupFieldProps, {}, "elements" | "status" | "values">;
29
+ type ToggleGroupField = ReturnType<typeof ToggleGroupField>;
30
+ export default ToggleGroupField;