Skip to content

Commit 3975eac

Browse files
committed
2 parents b7453c1 + fe7b838 commit 3975eac

2 files changed

Lines changed: 116 additions & 56 deletions

File tree

Lines changed: 110 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,114 @@
11
<script lang="ts">
2-
import { activeResponses } from "$lib/store";
3-
import type { Dropdown } from "$lib/types";
4-
import { getContext } from "svelte";
5-
6-
export let component: Dropdown;
7-
8-
const id: number = getContext("id")
9-
10-
$: if ($activeResponses[id].data[component.id] === undefined) {
11-
$activeResponses[id].data[component.id] = component.options[0]
12-
}
13-
export let error: string | undefined = undefined;
14-
15-
// let runValidator = () => {
16-
// error = component.validator(choice);
17-
// }
18-
19-
// $: {
20-
// choice = String($activeResponses[id].data[component.id])
21-
// runValidator();
22-
// }
23-
24-
// // Run the validator at least once before mount
25-
// runValidator();
2+
import { activeResponses } from '$lib/store';
3+
import type { Dropdown } from '$lib/types';
4+
import { getContext } from 'svelte';
5+
6+
export let component: Dropdown;
7+
8+
const id: number = getContext('id');
9+
10+
const PLACEHOLDER = '[select something]';
11+
const OTHER = 'Other';
12+
13+
const hasPlaceholder = component.options.includes(PLACEHOLDER);
14+
const hasOther = component.options.includes(OTHER);
15+
16+
// Local state for the <select> element — decoupled from the store so we can
17+
// write a *resolved* value (e.g. the typed "Other" text) to the store instead.
18+
let selectedOption: string = component.options[0];
19+
let otherText: string = '';
20+
21+
export let error: string | undefined = undefined;
22+
23+
// Initialize store on first render if not already set
24+
$: if ($activeResponses[id].data[component.id] === undefined) {
25+
$activeResponses[id].data[component.id] = component.options[0];
26+
selectedOption = component.options[0];
27+
}
28+
29+
$: showOtherBox = hasOther && selectedOption === OTHER;
30+
31+
/**
32+
* Writes the resolved value to the store and updates the error state.
33+
* Mirrors the syncToStore() pattern used by MultiSelect.
34+
*/
35+
function syncToStore() {
36+
// Block submission when placeholder is active
37+
if (hasPlaceholder && selectedOption === PLACEHOLDER) {
38+
$activeResponses[id].data[component.id] = PLACEHOLDER;
39+
error = 'Please select a value';
40+
return;
41+
}
42+
43+
// When "Other" is chosen, write the typed text (not the word "Other")
44+
if (hasOther && selectedOption === OTHER) {
45+
if (otherText.trim().length === 0) {
46+
$activeResponses[id].data[component.id] = OTHER;
47+
error = 'Please describe "Other"';
48+
return;
49+
}
50+
$activeResponses[id].data[component.id] = otherText.trim();
51+
error = undefined;
52+
return;
53+
}
54+
55+
// Normal selection — run custom validator if provided
56+
$activeResponses[id].data[component.id] = selectedOption;
57+
error = component.validator ? (component.validator(selectedOption) ?? undefined) : undefined;
58+
}
59+
60+
// Re-sync whenever selectedOption or otherText changes
61+
$: {
62+
selectedOption;
63+
otherText;
64+
syncToStore();
65+
}
2666
</script>
2767

2868
<div class="my-2">
29-
<div class="sm:flex justify-between items-center gap-2">
30-
<span
31-
class="flex sm:inline mt-1 sm:mt-0">
32-
<!-- Display the dropdown menu if manual is false -->
33-
{#if !component.manual}
34-
<select
35-
bind:value={$activeResponses[id].data[component.id]}
36-
class="flex-auto sm:inline bg-primary w-80 md:w-96 min-w-max rounded-xl px-4 py-2 text-white remove-arrow cursor-pointer hover:bg-hover hover:drop-shadow-btn-hover"
37-
>
38-
{#each component.options as option}
39-
<option
40-
class="cursor-pointer"
41-
value={option}
42-
>{option}</option>
43-
{/each}
44-
</select>
45-
{:else}
46-
47-
<!-- Display a text input field if manual is true -->
48-
<input
49-
type="text"
50-
bind:value={$activeResponses[id].data[component.id]}
51-
class="flex-auto sm:inline bg-primary w-80 md:w-96 rounded-xl px-4 py-2 text-white remove-arrow focus:drop-shadow-btn-hover min-w-9"
52-
/>
53-
{/if}
54-
</span>
55-
</div>
56-
57-
{#if error != undefined}
58-
<div class="text-red-700 text-md font-bold w-max ml-[auto]">{error}</div>
59-
{/if}
60-
</div>
69+
<div class="sm:flex justify-between items-center gap-2">
70+
<span class="flex sm:inline mt-1 sm:mt-0">
71+
{#if !component.manual}
72+
<select
73+
bind:value={selectedOption}
74+
class="flex-auto sm:inline bg-primary w-80 md:w-96 min-w-max rounded-xl px-4 py-2 text-white remove-arrow cursor-pointer hover:bg-hover hover:drop-shadow-btn-hover"
75+
>
76+
{#each component.options as option}
77+
<option class="cursor-pointer" value={option}>{option}</option>
78+
{/each}
79+
</select>
80+
{:else}
81+
<!-- Display a text input field if manual is true -->
82+
<input
83+
type="text"
84+
bind:value={$activeResponses[id].data[component.id]}
85+
class="flex-auto sm:inline bg-primary w-80 md:w-96 rounded-xl px-4 py-2 text-white remove-arrow focus:drop-shadow-btn-hover min-w-9"
86+
/>
87+
{/if}
88+
</span>
89+
</div>
90+
91+
<!-- "Other" text box — only shown when "Other" is selected, matching MultiSelect's architecture -->
92+
{#if showOtherBox}
93+
<div class="mt-2">
94+
<span class="text-sm text-gray-400 mb-1 block">Describe "Other":</span>
95+
<textarea
96+
bind:value={otherText}
97+
placeholder="Specify a custom value…"
98+
on:keydown={(e) => {
99+
if (e.key === 'Enter') e.preventDefault();
100+
}}
101+
on:input={(e) => {
102+
const el = e.currentTarget;
103+
el.value = el.value.replace(/[\r\n]+/g, ' ');
104+
otherText = el.value;
105+
}}
106+
class="flex-auto h-[12vh] min-h-12 max-h-[40vh] bg-primary w-full rounded-xl px-4 py-2 text-white remove-arrow focus:drop-shadow-btn-hover scroll scrollbar-thin scrollbar-thumb-enabled scrollbar-track-background resize-none"
107+
></textarea>
108+
</div>
109+
{/if}
110+
111+
{#if error != undefined}
112+
<div class="text-red-700 text-md font-bold w-max ml-[auto]">{error}</div>
113+
{/if}
114+
</div>

src/lib/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ export type Checkbox = {
5858
initialValue?: number,
5959
}
6060

61+
62+
/**
63+
* If "Other" is present in `options`, a free-text box is revealed when the
64+
* scout selects it. The typed text replaces the bare word "Other" in the
65+
* output.
66+
*/
6167
export type Dropdown = {
6268
type: "Dropdown",
6369
id: string,

0 commit comments

Comments
 (0)