@stubber/form-fields 1.0.11 → 1.1.1
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.
- package/dist/fields/components/Select.svelte +78 -222
- package/package.json +2 -2
|
@@ -1,29 +1,20 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import {
|
|
3
|
-
import { deepEqual } from "fast-equals";
|
|
4
|
-
import _ from "lodash-es";
|
|
5
|
-
import { onMount, onDestroy } from "svelte";
|
|
6
|
-
import { writable } from "svelte/store";
|
|
7
|
-
import * as utils from "../../utils/index.js";
|
|
2
|
+
import { onMount, tick } from "svelte";
|
|
8
3
|
|
|
9
4
|
import { Label } from "@stubber/ui/label";
|
|
10
|
-
import
|
|
5
|
+
import * as Command from "@stubber/ui/command";
|
|
6
|
+
import * as Popover from "@stubber/ui/popover";
|
|
7
|
+
import { Button } from "@stubber/ui/button";
|
|
8
|
+
import { isArray, isEqual } from "lodash-es";
|
|
11
9
|
|
|
12
10
|
export let field;
|
|
13
11
|
|
|
14
|
-
const internal = writable();
|
|
15
|
-
const { clickOutside } = utils;
|
|
16
|
-
|
|
17
|
-
let is_focused = false;
|
|
18
|
-
let filter_text = "";
|
|
19
|
-
let input;
|
|
20
|
-
|
|
21
12
|
$: state_key = $field.state?.state_key;
|
|
22
13
|
$: label = $field.spec?.title;
|
|
23
14
|
$: hide_label = $field.spec?.hide_label;
|
|
24
15
|
$: isValid = !$field.state?.validation || $field.state?.validation?.valid;
|
|
25
16
|
$: validationMessage = $field.state?.validation?.message;
|
|
26
|
-
$: items =
|
|
17
|
+
$: items = isArray($field.spec?.params?.options)
|
|
27
18
|
? $field.spec?.params?.options?.map((o, index) => {
|
|
28
19
|
let { label, value } = o || {};
|
|
29
20
|
let _value = value !== undefined ? value : label;
|
|
@@ -32,222 +23,87 @@
|
|
|
32
23
|
return { _key, _label, _value };
|
|
33
24
|
})
|
|
34
25
|
: [];
|
|
35
|
-
$: filteritems = items.filter((i) => {
|
|
36
|
-
let _filter_text = filter_text?.toLowerCase();
|
|
37
|
-
if (!_filter_text) return true;
|
|
38
|
-
let _label = i?._label?.toLowerCase();
|
|
39
|
-
return _label?.includes(_filter_text);
|
|
40
|
-
});
|
|
41
26
|
|
|
42
27
|
onMount(() => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
base: initial_item?._value ?? null,
|
|
49
|
-
};
|
|
50
|
-
if (!f?.spec?.without_value_details) initial_data.base_label = initial_item?._label;
|
|
51
|
-
let initial_state_internal = {
|
|
52
|
-
...f?.state?.internal,
|
|
53
|
-
focused_index: -1,
|
|
54
|
-
selected_item_key: initial_item?._key,
|
|
55
|
-
};
|
|
56
|
-
_.set(f, "data", initial_data);
|
|
57
|
-
_.set(f, "state.internal", initial_state_internal);
|
|
58
|
-
if (!deepEqual(f, $field)) $field = f;
|
|
59
|
-
|
|
60
|
-
syncStoreToStore(
|
|
61
|
-
field,
|
|
62
|
-
internal,
|
|
63
|
-
(a, b) => {
|
|
64
|
-
let _clone = _.cloneDeep(a.state?.internal) || {};
|
|
65
|
-
|
|
66
|
-
// get parts from data
|
|
67
|
-
let item = items.find((i) => i._value === a?.data?.base);
|
|
68
|
-
if (item) {
|
|
69
|
-
filter_text = item?._label;
|
|
70
|
-
_clone.selected_item_key = item?._key;
|
|
71
|
-
|
|
72
|
-
// set field_label if changed
|
|
73
|
-
if (!deepEqual(a?.data?.base_label, item?._label) && !a?.spec?.without_value_details) {
|
|
74
|
-
$field.data.base_label = item?._label;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// set field state if changed
|
|
78
|
-
if (!deepEqual(a?.state?.internal, _clone)) {
|
|
79
|
-
$field.state.internal = _clone;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return _clone;
|
|
84
|
-
},
|
|
85
|
-
(a, b) => {
|
|
86
|
-
let _clone = _.cloneDeep(a) || {};
|
|
87
|
-
// update the state
|
|
88
|
-
_.set(_clone, "state.internal", _.cloneDeep(b));
|
|
89
|
-
// update the data
|
|
90
|
-
let item = items.find((i) => i._key === b?.selected_item_key);
|
|
91
|
-
_.set(_clone, "data.base", item?._value);
|
|
92
|
-
if (!a?.spec?.without_value_details) _.set(_clone, "data.base_label", item?._label);
|
|
93
|
-
return _clone;
|
|
28
|
+
if ($field.data.base && !$field.spec?.without_value_details) {
|
|
29
|
+
// If the field has a base value, we need to find the corresponding label
|
|
30
|
+
const item = items.find((i) => isEqual(i._value, $field.data.base));
|
|
31
|
+
if (item) {
|
|
32
|
+
$field.data.base_label = item._label;
|
|
94
33
|
}
|
|
95
|
-
);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
let teleportedNode = null;
|
|
99
|
-
onDestroy(() => {
|
|
100
|
-
if (teleportedNode) {
|
|
101
|
-
teleportedNode.remove();
|
|
102
|
-
teleportedNode = null;
|
|
103
34
|
}
|
|
104
35
|
});
|
|
105
36
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Get the original position and width
|
|
110
|
-
let rect = node.getBoundingClientRect();
|
|
111
|
-
let originalWidth = node.offsetWidth;
|
|
112
|
-
let originalHeight = node.offsetHeight;
|
|
37
|
+
function simpleSetSelected(item_key) {
|
|
38
|
+
const item = items.find((i) => i._key === item_key);
|
|
39
|
+
if (!item) return;
|
|
113
40
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
node.style.width = originalWidth + "px";
|
|
122
|
-
node.style.height = originalHeight + "px";
|
|
123
|
-
node.style.position = "absolute";
|
|
124
|
-
node.style.top = rect.top + window.scrollY + "px";
|
|
125
|
-
node.style.left = rect.left + "px";
|
|
126
|
-
|
|
127
|
-
// set z-index to 1000
|
|
128
|
-
node.style.zIndex = 1000;
|
|
41
|
+
field.update((f) => {
|
|
42
|
+
f.data.base = item?._value;
|
|
43
|
+
if (!f.spec?.without_value_details) {
|
|
44
|
+
f.data.base_label = item?._label;
|
|
45
|
+
}
|
|
46
|
+
return f;
|
|
47
|
+
});
|
|
129
48
|
}
|
|
130
49
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
input.blur();
|
|
50
|
+
let open = false;
|
|
51
|
+
|
|
52
|
+
$: dropdownLabel = items.find((i) => isEqual(i._value, $field.data.base))?._label ?? "Select...";
|
|
53
|
+
// We want to refocus the trigger button when the user selects
|
|
54
|
+
// an item from the list so users can continue navigating the
|
|
55
|
+
// rest of the form with the keyboard.
|
|
56
|
+
function closeAndFocusTrigger(triggerId) {
|
|
57
|
+
open = false;
|
|
58
|
+
tick().then(() => {
|
|
59
|
+
document.getElementById(triggerId)?.focus();
|
|
60
|
+
});
|
|
143
61
|
}
|
|
62
|
+
|
|
63
|
+
const collisionBoundary = document.querySelector("body");
|
|
144
64
|
</script>
|
|
145
65
|
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
flex h-7 w-full items-center justify-between rounded px-3 py-2
|
|
191
|
-
text-sm focus:outline-none
|
|
192
|
-
[&>span]:line-clamp-1 aria-[invalid]:border-destructive
|
|
193
|
-
placeholder:text-muted-foreground
|
|
194
|
-
{!isValid ? 'ring-danger-500' : 'ring-surface-300 focus:ring-transparent'}"
|
|
195
|
-
name={state_key}
|
|
196
|
-
bind:value={filter_text}
|
|
197
|
-
autocomplete="off"
|
|
198
|
-
/>
|
|
199
|
-
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
|
200
|
-
<i class="text-surface-300 fa-regular fa-arrows-up-down fa-fw" />
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
{#if is_focused}
|
|
204
|
-
<div use:teleport class="z-10 w-full inset-y-[70px] mt-1">
|
|
205
|
-
<div
|
|
206
|
-
transition:scale={{
|
|
207
|
-
start: 0.95,
|
|
208
|
-
opacity: 0,
|
|
209
|
-
duration: 50,
|
|
210
|
-
}}
|
|
211
|
-
class="bg-popover text-popover-foreground relative min-w-[8rem]
|
|
212
|
-
overflow-hidden rounded-md border shadow-md outline-none
|
|
213
|
-
max-h-[200px] overflow-y-auto w-full p-1"
|
|
214
|
-
>
|
|
215
|
-
{#if filteritems.length > 0}
|
|
216
|
-
<ul>
|
|
217
|
-
{#each filteritems as item, index (item._key)}
|
|
218
|
-
<li class="group {$internal.focused_index === index ? 'bg-primary-400' : ''} ">
|
|
219
|
-
<button
|
|
220
|
-
type="button"
|
|
221
|
-
on:click|preventDefault={() => setSelected(item)}
|
|
222
|
-
class="selectitem relative flex w-full cursor-default
|
|
223
|
-
select-none items-center rounded-sm py-1 pl-8 pr-2
|
|
224
|
-
text-sm outline-none hover:bg-accent
|
|
225
|
-
hover:text-accent-foreground
|
|
226
|
-
data-[highlighted]:bg-accent
|
|
227
|
-
data-[highlighted]:text-accent-foreground
|
|
228
|
-
data-[disabled]:pointer-events-none
|
|
229
|
-
data-[disabled]:opacity-50"
|
|
230
|
-
>
|
|
231
|
-
{item._label}
|
|
232
|
-
{#if item._key === $internal?.selected_item_key}
|
|
233
|
-
<i class="absolute left-2 h-3.5 w-3.5 fa-regular fa-check" />
|
|
234
|
-
{/if}
|
|
235
|
-
</button>
|
|
236
|
-
</li>
|
|
237
|
-
{/each}
|
|
238
|
-
</ul>
|
|
239
|
-
{:else}
|
|
240
|
-
<div class="flex items-center justify-center py-4">
|
|
241
|
-
<span class="text-sm text-surface-400">
|
|
242
|
-
{filter_text ? "No results found" : "Start typing to search..."}
|
|
243
|
-
</span>
|
|
244
|
-
</div>
|
|
245
|
-
{/if}
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
|
-
{/if}
|
|
249
|
-
{#if validationMessage}
|
|
250
|
-
<Label class={!isValid ? `text-danger-500` : `text-success-500`}>{validationMessage}</Label>
|
|
251
|
-
{/if}
|
|
252
|
-
</div>
|
|
253
|
-
{/if}
|
|
66
|
+
<div class="flex flex-col w-full text-surface-900 my-2">
|
|
67
|
+
<Label for="input_{state_key}" class="block py-2 {hide_label ? 'hidden' : ''}">
|
|
68
|
+
{label}
|
|
69
|
+
</Label>
|
|
70
|
+
<Popover.Root bind:open let:ids>
|
|
71
|
+
<Popover.Trigger asChild let:builder>
|
|
72
|
+
<Button
|
|
73
|
+
builders={[builder]}
|
|
74
|
+
variant="outline"
|
|
75
|
+
role="combobox"
|
|
76
|
+
aria-expanded={open}
|
|
77
|
+
class="w-full border-input bg-white bg-opacity-[15] ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 justify-between"
|
|
78
|
+
style="box-shadow: inset 0px 16px 16px -16px rgba(0, 0, 0, 0.1333);"
|
|
79
|
+
>
|
|
80
|
+
{dropdownLabel}
|
|
81
|
+
<i class="fas fa-sort ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
82
|
+
</Button>
|
|
83
|
+
</Popover.Trigger>
|
|
84
|
+
<Popover.Content sameWidth {collisionBoundary} class="p-0">
|
|
85
|
+
<Command.Root>
|
|
86
|
+
<Command.Input placeholder="Search..." />
|
|
87
|
+
<Command.Empty>No results found.</Command.Empty>
|
|
88
|
+
<Command.Group>
|
|
89
|
+
{#each items as item}
|
|
90
|
+
<Command.Item
|
|
91
|
+
value={item._key}
|
|
92
|
+
onSelect={(item_key) => {
|
|
93
|
+
simpleSetSelected(item_key);
|
|
94
|
+
closeAndFocusTrigger(ids.trigger);
|
|
95
|
+
}}
|
|
96
|
+
class={$field.data.base === item._value ? "bg-primary-100" : ""}
|
|
97
|
+
>
|
|
98
|
+
{item._label}
|
|
99
|
+
</Command.Item>
|
|
100
|
+
{/each}
|
|
101
|
+
</Command.Group>
|
|
102
|
+
</Command.Root>
|
|
103
|
+
</Popover.Content>
|
|
104
|
+
</Popover.Root>
|
|
105
|
+
|
|
106
|
+
{#if validationMessage}
|
|
107
|
+
<Label class={!isValid ? `text-danger-500` : `text-success-500`}>{validationMessage}</Label>
|
|
108
|
+
{/if}
|
|
109
|
+
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stubber/form-fields",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "An automatic form builder based on field specifications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"components",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@codemirror/commands": "^6.7.1",
|
|
58
58
|
"@codemirror/state": "^6.4.1",
|
|
59
59
|
"@codemirror/view": "^6.34.1",
|
|
60
|
-
"@stubber/ui": "^1.
|
|
60
|
+
"@stubber/ui": "^1.1.1",
|
|
61
61
|
"ag-grid-community": "^31.0.2",
|
|
62
62
|
"ag-grid-enterprise": "^31.0.2",
|
|
63
63
|
"currency-symbol-map": "^5.1.0",
|