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 >
0 commit comments