@marianmeres/stuic 2.0.0-next.4 → 2.0.2
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/actions/file-dropzone.svelte.d.ts +8 -0
- package/dist/actions/file-dropzone.svelte.js +43 -0
- package/dist/actions/highlight-dragover.svelte.js +16 -3
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/resizable-width.svelte.d.ts +21 -0
- package/dist/actions/resizable-width.svelte.js +162 -0
- package/dist/actions/validate.svelte.js +13 -13
- package/dist/components/Backdrop/Backdrop.svelte +1 -1
- package/dist/components/Button/Button.svelte +2 -2
- package/dist/components/Button/Button.svelte.d.ts +1 -1
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +170 -0
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte.d.ts +26 -0
- package/dist/components/ButtonGroupRadio/index.css +23 -0
- package/dist/components/ButtonGroupRadio/index.d.ts +1 -0
- package/dist/components/ButtonGroupRadio/index.js +1 -0
- package/dist/components/CommandMenu/CommandMenu.svelte +365 -0
- package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +25 -0
- package/dist/components/CommandMenu/index.d.ts +1 -0
- package/dist/components/CommandMenu/index.js +1 -0
- package/dist/components/Input/FieldInput.svelte +1 -0
- package/dist/components/Input/FieldLikeButton.svelte +16 -7
- package/dist/components/Input/FieldLikeButton.svelte.d.ts +1 -1
- package/dist/components/Input/FieldOptions.svelte +308 -136
- package/dist/components/Input/FieldOptions.svelte.d.ts +15 -8
- package/dist/components/Input/_internal/InputWrap.svelte +7 -6
- package/dist/components/Modal/Modal.svelte +10 -5
- package/dist/components/ModalDialog/ModalDialog.svelte +25 -0
- package/dist/components/Notifications/Notifications.svelte +1 -1
- package/dist/components/Progress/_internal/Bar.svelte +1 -1
- package/dist/components/Spinner/SpinnerUnicode.svelte +130 -0
- package/dist/components/Spinner/SpinnerUnicode.svelte.d.ts +12 -0
- package/dist/components/Spinner/index.d.ts +1 -0
- package/dist/components/Spinner/index.js +1 -0
- package/dist/components/TypeaheadInput/TypeaheadInput.svelte +261 -0
- package/dist/components/TypeaheadInput/TypeaheadInput.svelte.d.ts +40 -0
- package/dist/components/TypeaheadInput/index.d.ts +1 -0
- package/dist/components/TypeaheadInput/index.js +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/escape-regex.d.ts +1 -0
- package/dist/utils/escape-regex.js +1 -0
- package/dist/utils/event-emitter.d.ts +18 -0
- package/dist/utils/event-emitter.js +40 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/is-plain-object.d.ts +2 -0
- package/dist/utils/is-plain-object.js +4 -0
- package/dist/utils/replace-map.d.ts +5 -0
- package/dist/utils/replace-map.js +22 -0
- package/dist/utils/seconds.d.ts +7 -0
- package/dist/utils/seconds.js +35 -0
- package/dist/utils/tw-merge.d.ts +2 -0
- package/dist/utils/tw-merge.js +4 -0
- package/dist/utils/unaccent.d.ts +6 -0
- package/dist/utils/unaccent.js +8 -0
- package/package.json +70 -66
- package/dist/components/ColResize/ColResize.svelte +0 -0
- package/dist/components/ColResize/ColResize.svelte.d.ts +0 -26
|
@@ -1,30 +1,4 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
export interface Option {
|
|
3
|
-
label: string;
|
|
4
|
-
value: any;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
// i18n ready
|
|
8
|
-
function t_default(k: string) {
|
|
9
|
-
const m: Record<string, string> = {
|
|
10
|
-
field_req_att: "This field requires attention. Please review and try again.",
|
|
11
|
-
cardinality_of: "of",
|
|
12
|
-
cardinality_selected: "selected",
|
|
13
|
-
submit: "Submit",
|
|
14
|
-
select_all: "Select all",
|
|
15
|
-
clear_all: "Clear all",
|
|
16
|
-
search_placeholder: "Type to search...",
|
|
17
|
-
cardinality_full: "Max selection reached",
|
|
18
|
-
select_from_list: "Please select from the list",
|
|
19
|
-
x_close: "Clear input or close [esc]",
|
|
20
|
-
unknown_allowed: "Select from the list or type and submit any value",
|
|
21
|
-
unknown_not_allowed: "Select values from the list only",
|
|
22
|
-
};
|
|
23
|
-
return m[k] ?? k;
|
|
24
|
-
}
|
|
25
|
-
</script>
|
|
26
|
-
|
|
27
|
-
<script lang="ts">
|
|
28
2
|
import { createClog } from "@marianmeres/clog";
|
|
29
3
|
import { iconBsSearch } from "@marianmeres/icons-fns/bootstrap/iconBsSearch.js";
|
|
30
4
|
import { iconLucideCheck } from "@marianmeres/icons-fns/lucide/iconLucideCheck.js";
|
|
@@ -32,7 +6,7 @@
|
|
|
32
6
|
import { iconLucideSquare } from "@marianmeres/icons-fns/lucide/iconLucideSquare.js";
|
|
33
7
|
import { ItemCollection, type Item } from "@marianmeres/item-collection";
|
|
34
8
|
import { Debounced, watch } from "runed";
|
|
35
|
-
import { type Snippet } from "svelte";
|
|
9
|
+
import { tick, type Snippet } from "svelte";
|
|
36
10
|
import { tooltip } from "../../actions/index.js";
|
|
37
11
|
import { type ValidateOptions } from "../../actions/validate.svelte.js";
|
|
38
12
|
import { getId } from "../../utils/get-id.js";
|
|
@@ -49,7 +23,48 @@
|
|
|
49
23
|
import X from "../X/X.svelte";
|
|
50
24
|
import InputWrap from "./_internal/InputWrap.svelte";
|
|
51
25
|
import FieldLikeButton from "./FieldLikeButton.svelte";
|
|
26
|
+
import { replaceMap } from "../../utils/replace-map.js";
|
|
27
|
+
import { isPlainObject } from "../../utils/is-plain-object.js";
|
|
28
|
+
import type { TranslateFn } from "../../types.js";
|
|
52
29
|
|
|
30
|
+
export interface Option {
|
|
31
|
+
label: string;
|
|
32
|
+
value: any;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// i18n ready
|
|
36
|
+
function t_default(
|
|
37
|
+
k: string,
|
|
38
|
+
values: false | null | undefined | Record<string, string | number> = null,
|
|
39
|
+
fallback: string | boolean = "",
|
|
40
|
+
i18nSpanWrap: boolean = true
|
|
41
|
+
) {
|
|
42
|
+
const m: Record<string, string> = {
|
|
43
|
+
field_req_att: "This field requires attention. Please review and try again.",
|
|
44
|
+
cardinality_of: "of max",
|
|
45
|
+
cardinality_selected: "selected",
|
|
46
|
+
submit: "Submit",
|
|
47
|
+
select_all: "Select results",
|
|
48
|
+
clear_all: "Clear selected",
|
|
49
|
+
clear: "Clear",
|
|
50
|
+
search_placeholder: "Type to search...",
|
|
51
|
+
search_submit_placeholder: "Type to search and/or submit...",
|
|
52
|
+
cardinality_full: "Max selection reached",
|
|
53
|
+
select_from_list: "Please select from the list only",
|
|
54
|
+
x_close: "Clear input or close [esc]",
|
|
55
|
+
unknown_allowed: "Select or type and submit",
|
|
56
|
+
unknown_not_allowed: "Select from the list",
|
|
57
|
+
no_results: "No results found.",
|
|
58
|
+
add_new: 'Add "{{value}}"...',
|
|
59
|
+
click_add_new: "You must add the value to continue",
|
|
60
|
+
};
|
|
61
|
+
let out = m[k] ?? fallback ?? k;
|
|
62
|
+
|
|
63
|
+
return isPlainObject(values) ? replaceMap(out, values as any) : out;
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<script lang="ts">
|
|
53
68
|
const clog = createClog("FieldOptions");
|
|
54
69
|
|
|
55
70
|
const iconCheckboxEmpty = iconLucideSquare;
|
|
@@ -94,23 +109,29 @@
|
|
|
94
109
|
//
|
|
95
110
|
classOption?: string;
|
|
96
111
|
classOptionActive?: string;
|
|
112
|
+
classOptgroup?: string;
|
|
97
113
|
//
|
|
98
114
|
classModalField?: string;
|
|
99
115
|
noScrollLock?: boolean;
|
|
100
116
|
//
|
|
101
117
|
style?: string;
|
|
102
|
-
t?:
|
|
118
|
+
t?: TranslateFn;
|
|
103
119
|
//
|
|
104
120
|
renderValue?: (strigifiedItems: string) => string;
|
|
105
|
-
getOptions: (
|
|
121
|
+
getOptions: (
|
|
122
|
+
q: string,
|
|
123
|
+
current: Item[]
|
|
124
|
+
) => Promise<{ coll?: ItemCollection<Item>; found: Item[] }>;
|
|
106
125
|
notifications?: NotificationsStack;
|
|
107
126
|
// -1 no limit
|
|
108
127
|
// +n max selected limit
|
|
109
128
|
cardinality?: number;
|
|
110
129
|
renderOptionLabel?: (item: Item) => string;
|
|
130
|
+
renderOptionGroup?: (s: string) => string;
|
|
111
131
|
// whether to allow adding unknown options
|
|
112
132
|
allowUnknown?: boolean;
|
|
113
|
-
|
|
133
|
+
showIconsCheckbox?: boolean;
|
|
134
|
+
showIconsRadio?: boolean;
|
|
114
135
|
searchPlaceholder?: string;
|
|
115
136
|
name: string;
|
|
116
137
|
itemIdPropName?: string;
|
|
@@ -150,6 +171,7 @@
|
|
|
150
171
|
//
|
|
151
172
|
classOption,
|
|
152
173
|
classOptionActive,
|
|
174
|
+
classOptgroup,
|
|
153
175
|
//
|
|
154
176
|
style,
|
|
155
177
|
//
|
|
@@ -162,8 +184,10 @@
|
|
|
162
184
|
notifications,
|
|
163
185
|
cardinality: _cardinality = Infinity,
|
|
164
186
|
renderOptionLabel,
|
|
187
|
+
renderOptionGroup = (s: string) => `${s}`.replaceAll("_", " "),
|
|
165
188
|
allowUnknown = false,
|
|
166
|
-
|
|
189
|
+
showIconsCheckbox = true,
|
|
190
|
+
showIconsRadio = false,
|
|
167
191
|
searchPlaceholder,
|
|
168
192
|
name,
|
|
169
193
|
itemIdPropName = "id",
|
|
@@ -175,6 +199,7 @@
|
|
|
175
199
|
let isFetching = $state(false);
|
|
176
200
|
let cardinality = $derived(_cardinality === -1 ? Infinity : _cardinality);
|
|
177
201
|
let isMultiple = $derived(cardinality > 1);
|
|
202
|
+
let showIcons = $derived(isMultiple ? showIconsCheckbox : showIconsRadio);
|
|
178
203
|
|
|
179
204
|
//
|
|
180
205
|
let wrappedValidate: Omit<ValidateOptions, "setValidationResult"> = $derived({
|
|
@@ -206,7 +231,8 @@
|
|
|
206
231
|
}
|
|
207
232
|
|
|
208
233
|
function sortFn(a: Item, b: Item) {
|
|
209
|
-
|
|
234
|
+
const withOptGroup = (i: Item) => `${i.optgroup || ""}__${_renderOptionLabel(i)}`;
|
|
235
|
+
return withOptGroup(a).localeCompare(withOptGroup(b), undefined, {
|
|
210
236
|
sensitivity: "base",
|
|
211
237
|
});
|
|
212
238
|
}
|
|
@@ -222,21 +248,43 @@
|
|
|
222
248
|
|
|
223
249
|
// second, the selected ones
|
|
224
250
|
const _selectedColl = new ItemCollection([], {
|
|
251
|
+
// svelte-ignore state_referenced_locally
|
|
225
252
|
cardinality,
|
|
226
253
|
sortFn,
|
|
227
254
|
idPropName: itemIdPropName,
|
|
228
255
|
});
|
|
229
256
|
|
|
257
|
+
// reconfigure if the prop ever changes during runtime (most likely will NOT)
|
|
258
|
+
$effect(() => {
|
|
259
|
+
_selectedColl.configure({ cardinality });
|
|
260
|
+
});
|
|
261
|
+
|
|
230
262
|
// now, create the reactive, subscribed variants
|
|
231
263
|
let options = $derived($_optionsColl);
|
|
232
264
|
let selected = $derived($_selectedColl);
|
|
265
|
+
|
|
266
|
+
// we need to know whether to show "Add xyz"...
|
|
267
|
+
function have_option_label_like(items: Item[], s: string) {
|
|
268
|
+
return items.some(
|
|
269
|
+
(item) => _renderOptionLabel(item).toLowerCase() === `${s}`.toLowerCase()
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
233
273
|
// $inspect("options", options);
|
|
234
274
|
// $inspect("selected", selected);
|
|
275
|
+
// $inspect("lastQuery", lastQuery, innerValue);
|
|
276
|
+
|
|
277
|
+
// hidden input which holds the final value (upon which validation happens)
|
|
278
|
+
let parentHiddenInputEl: HTMLInputElement | undefined = $state();
|
|
235
279
|
|
|
236
280
|
let activeEl: HTMLButtonElement | undefined = $state();
|
|
237
|
-
let optionsBox:
|
|
281
|
+
let optionsBox: HTMLDivElement | undefined = $state();
|
|
238
282
|
let modalEl: HTMLDivElement | undefined = $state();
|
|
239
283
|
|
|
284
|
+
// add_new dance...
|
|
285
|
+
let addNewBtn: HTMLButtonElement | undefined = $state();
|
|
286
|
+
let isAddNewBtnActive = $state(false);
|
|
287
|
+
|
|
240
288
|
// set value on open
|
|
241
289
|
watch(
|
|
242
290
|
() => modal.visibility().visible,
|
|
@@ -257,7 +305,7 @@
|
|
|
257
305
|
// scroll the active option into view
|
|
258
306
|
$effect(() => {
|
|
259
307
|
if (modal.visibility().visible && options.active?.[itemIdPropName]) {
|
|
260
|
-
activeEl = qsa(`#${
|
|
308
|
+
activeEl = qsa(`#${btn_id(options.active[itemIdPropName])}`, optionsBox)[0] as any;
|
|
261
309
|
activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
262
310
|
activeEl?.focus();
|
|
263
311
|
} else {
|
|
@@ -274,10 +322,12 @@
|
|
|
274
322
|
isFetching = true;
|
|
275
323
|
getOptions(currVal, selected.items)
|
|
276
324
|
.then((res) => {
|
|
325
|
+
const { found, coll } = res;
|
|
326
|
+
|
|
277
327
|
// always update the existing with recent server data
|
|
278
|
-
_selectedColl.patchMany(
|
|
328
|
+
_selectedColl.patchMany(found);
|
|
279
329
|
// continue normally, with (server) provided options...
|
|
280
|
-
_optionsColl.clear().addMany(
|
|
330
|
+
_optionsColl.clear().addMany(found);
|
|
281
331
|
})
|
|
282
332
|
.catch((e) => {
|
|
283
333
|
console.error(e);
|
|
@@ -288,17 +338,64 @@
|
|
|
288
338
|
);
|
|
289
339
|
|
|
290
340
|
// internal DRY
|
|
291
|
-
function
|
|
341
|
+
function btn_id(id: string | number, prefix = "btn-") {
|
|
292
342
|
return prefix + strHash(`${id}`.repeat(3));
|
|
293
343
|
}
|
|
294
344
|
|
|
295
|
-
//
|
|
345
|
+
// "inner" submit
|
|
346
|
+
function try_submit(force = false) {
|
|
347
|
+
clog("try_submit", innerValue);
|
|
348
|
+
if (innerValue) {
|
|
349
|
+
let found = have_option_label_like(_optionsColl.items, innerValue);
|
|
350
|
+
if (!found && !allowUnknown) {
|
|
351
|
+
return notifications?.error(t("select_from_list"), { ttl: 1000 });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!found && !_optionsColl.size) {
|
|
355
|
+
return notifications?.error(t("click_add_new", { value: innerValue }), {
|
|
356
|
+
ttl: 1000,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// maybe submit
|
|
361
|
+
if (_selectedColl.isFull || force) submit();
|
|
362
|
+
}
|
|
363
|
+
// enter on empty input always submits
|
|
364
|
+
else {
|
|
365
|
+
submit();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function add_new() {
|
|
370
|
+
// should be noop if called multiple times with same value
|
|
371
|
+
if (allowUnknown && innerValue) {
|
|
372
|
+
const item = { [itemIdPropName]: innerValue };
|
|
373
|
+
if (!isMultiple) _selectedColl.clear();
|
|
374
|
+
// actual selection addon
|
|
375
|
+
_selectedColl.add(item);
|
|
376
|
+
// we might have added a new one, so add it to options as well
|
|
377
|
+
// (will be noop if already exists)...
|
|
378
|
+
_optionsColl.add(item);
|
|
379
|
+
_optionsColl.setActive(item);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function _dispatch_change_to_owner() {
|
|
384
|
+
// trigger validation on the parent on each submit (emulate typical browser behaviour)
|
|
385
|
+
tick().then(() => {
|
|
386
|
+
parentHiddenInputEl?.dispatchEvent(new Event("change", { bubbles: true }));
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// "outer" submit - will set the outer bound value (always string) and close modal...
|
|
391
|
+
// further process is left on the consumer
|
|
296
392
|
function submit() {
|
|
297
393
|
// clog("modal submit", $state.snapshot(selected.items));
|
|
298
394
|
value = JSON.stringify(selected.items);
|
|
299
395
|
innerValue = "";
|
|
300
396
|
_optionsColl.clear();
|
|
301
397
|
modal.close();
|
|
398
|
+
_dispatch_change_to_owner();
|
|
302
399
|
}
|
|
303
400
|
|
|
304
401
|
// clears, closes, submits nothing
|
|
@@ -306,6 +403,64 @@
|
|
|
306
403
|
innerValue = "";
|
|
307
404
|
_optionsColl.clear();
|
|
308
405
|
modal?.close();
|
|
406
|
+
_dispatch_change_to_owner();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function _normalize_and_group_options(opts: Item[]): Map<string, Item[]> {
|
|
410
|
+
const groupped = new Map<string, Item[]>();
|
|
411
|
+
opts.forEach((o) => {
|
|
412
|
+
const optgLabel = renderOptionGroup(o.optgroup || "");
|
|
413
|
+
if (!groupped.has(optgLabel)) groupped.set(optgLabel, []);
|
|
414
|
+
const optgroup = groupped.get(optgLabel);
|
|
415
|
+
optgroup!.push(o);
|
|
416
|
+
});
|
|
417
|
+
return groupped;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const BTN_CLS = [
|
|
421
|
+
"no-focus-visible",
|
|
422
|
+
"text-left rounded-md py-2 px-2.5 flex items-center space-x-2",
|
|
423
|
+
"w-full",
|
|
424
|
+
"border border-transparent",
|
|
425
|
+
"focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
|
|
426
|
+
"focus-visible:outline-0 focus-visible:ring-0",
|
|
427
|
+
"hover:border-neutral-400 dark:hover:border-neutral-500",
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
// add new dance
|
|
431
|
+
$effect(() => {
|
|
432
|
+
if (addNewBtn && isAddNewBtnActive) {
|
|
433
|
+
addNewBtn?.focus();
|
|
434
|
+
_optionsColl.unsetActive(); // make sure to reset
|
|
435
|
+
}
|
|
436
|
+
if (!addNewBtn) {
|
|
437
|
+
isAddNewBtnActive = false;
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
function maybe_activate_add_new(isDown: boolean, isMeta: boolean) {
|
|
442
|
+
// no button, no activation
|
|
443
|
+
if (!addNewBtn) return false;
|
|
444
|
+
const isUp = !isDown;
|
|
445
|
+
|
|
446
|
+
// separating below into distinct ifs, so it's easily readable
|
|
447
|
+
|
|
448
|
+
// if first arrow down
|
|
449
|
+
if (!isAddNewBtnActive && isDown && _optionsColl.activeIndex === undefined) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// isActive and isUp (this is a noop, but we must break)
|
|
454
|
+
if (isAddNewBtnActive && isUp) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// isUp from first, or is metaUp
|
|
459
|
+
if (!isAddNewBtnActive && isUp && (_optionsColl.activeIndex === 0 || isMeta)) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return false;
|
|
309
464
|
}
|
|
310
465
|
</script>
|
|
311
466
|
|
|
@@ -317,10 +472,14 @@
|
|
|
317
472
|
if (["ArrowDown", "ArrowUp"].includes(e.key)) {
|
|
318
473
|
e.preventDefault();
|
|
319
474
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
e.
|
|
475
|
+
isAddNewBtnActive = maybe_activate_add_new(e.key === "ArrowDown", e.metaKey);
|
|
476
|
+
|
|
477
|
+
if (!isAddNewBtnActive) {
|
|
478
|
+
if (e.key === "ArrowUp") {
|
|
479
|
+
e.metaKey ? _optionsColl.setActiveFirst() : _optionsColl.setActivePrevious();
|
|
480
|
+
} else if (e.key === "ArrowDown") {
|
|
481
|
+
e.metaKey ? _optionsColl.setActiveLast() : _optionsColl.setActiveNext();
|
|
482
|
+
}
|
|
324
483
|
}
|
|
325
484
|
|
|
326
485
|
// common UI convention: radios are selected by arrows
|
|
@@ -340,6 +499,7 @@
|
|
|
340
499
|
<div>
|
|
341
500
|
<FieldLikeButton
|
|
342
501
|
bind:value
|
|
502
|
+
bind:input={parentHiddenInputEl}
|
|
343
503
|
{name}
|
|
344
504
|
class={classProp}
|
|
345
505
|
{label}
|
|
@@ -395,7 +555,7 @@
|
|
|
395
555
|
>
|
|
396
556
|
<InputWrap
|
|
397
557
|
size={renderSize}
|
|
398
|
-
class={twMerge("m-
|
|
558
|
+
class={twMerge("m-2 mb-12 shadow-xl", classModalField)}
|
|
399
559
|
classInputBoxWrap={twMerge(
|
|
400
560
|
// always look like focused
|
|
401
561
|
`border border-input-accent dark:border-input-accent-dark`,
|
|
@@ -413,40 +573,12 @@
|
|
|
413
573
|
tabindex={1}
|
|
414
574
|
{required}
|
|
415
575
|
{disabled}
|
|
416
|
-
placeholder={searchPlaceholder ??
|
|
576
|
+
placeholder={searchPlaceholder ??
|
|
577
|
+
t(allowUnknown ? "search_submit_placeholder" : "search_placeholder")}
|
|
417
578
|
onkeydown={(e) => {
|
|
418
579
|
if (e.key === "Enter") {
|
|
419
580
|
e.preventDefault();
|
|
420
|
-
|
|
421
|
-
if (innerValue) {
|
|
422
|
-
// doing label search, taking first result
|
|
423
|
-
let found = _optionsColl.search(innerValue)?.[0];
|
|
424
|
-
if (!found) {
|
|
425
|
-
if (!allowUnknown) {
|
|
426
|
-
return notifications?.error(t("select_from_list"), { ttl: 1000 });
|
|
427
|
-
}
|
|
428
|
-
found = { [itemIdPropName]: innerValue };
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (!isMultiple) _selectedColl.clear();
|
|
432
|
-
|
|
433
|
-
// actual selection addon
|
|
434
|
-
_selectedColl.add(found);
|
|
435
|
-
|
|
436
|
-
// we might have added a new one, so add it to options as well
|
|
437
|
-
// (will be noop if already exists)...
|
|
438
|
-
if (allowUnknown) {
|
|
439
|
-
_optionsColl.add(found);
|
|
440
|
-
_optionsColl.setActive(found);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// maybe submit
|
|
444
|
-
if (_selectedColl.isFull) submit();
|
|
445
|
-
}
|
|
446
|
-
// enter on empty input always submits
|
|
447
|
-
else {
|
|
448
|
-
submit();
|
|
449
|
-
}
|
|
581
|
+
try_submit();
|
|
450
582
|
}
|
|
451
583
|
}}
|
|
452
584
|
autocomplete="off"
|
|
@@ -466,6 +598,7 @@
|
|
|
466
598
|
"hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
|
|
467
599
|
)}
|
|
468
600
|
tabindex={4}
|
|
601
|
+
disabled={!options.size}
|
|
469
602
|
>
|
|
470
603
|
{@html t("select_all")}
|
|
471
604
|
</button>
|
|
@@ -484,7 +617,7 @@
|
|
|
484
617
|
tabindex={5}
|
|
485
618
|
disabled={!selected.items.length}
|
|
486
619
|
>
|
|
487
|
-
{@html t("clear_all")}
|
|
620
|
+
{@html t(cardinality === 1 ? "clear" : "clear_all")}
|
|
488
621
|
</button>
|
|
489
622
|
|
|
490
623
|
<span class="p-1 m-1 text-xs"> </span>
|
|
@@ -498,80 +631,119 @@
|
|
|
498
631
|
</div>
|
|
499
632
|
|
|
500
633
|
<!-- {#if options.items.length} -->
|
|
501
|
-
<
|
|
502
|
-
class={
|
|
503
|
-
"options
|
|
504
|
-
|
|
634
|
+
<div
|
|
635
|
+
class={[
|
|
636
|
+
"options overflow-y-auto overflow-x-hidden space-y-1",
|
|
637
|
+
"h-[220px] max-h-[220px]",
|
|
638
|
+
]}
|
|
505
639
|
bind:this={optionsBox}
|
|
506
640
|
tabindex="-1"
|
|
507
641
|
>
|
|
508
642
|
{#if isFetching && !options.items.length}
|
|
509
|
-
<div class="p-4 opacity-50">
|
|
643
|
+
<!-- <div class="p-4 opacity-50"> -->
|
|
644
|
+
<div class="flex opacity-50 text-sm h-full items-center justify-center">
|
|
510
645
|
<Spinner class="w-4" />
|
|
511
646
|
</div>
|
|
647
|
+
{:else if !options.items.length && !allowUnknown}
|
|
648
|
+
<div class="flex opacity-50 text-sm h-full items-center justify-center">
|
|
649
|
+
{@html t("no_results")}
|
|
650
|
+
</div>
|
|
512
651
|
{/if}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
selected.items && _selectedColl.exists(item[itemIdPropName])}
|
|
517
|
-
<li class:active class="px-2">
|
|
652
|
+
|
|
653
|
+
{#if !isFetching && allowUnknown && innerValue && !have_option_label_like(options.items, innerValue)}
|
|
654
|
+
<div class="px-1">
|
|
518
655
|
<button
|
|
519
656
|
type="button"
|
|
520
|
-
|
|
521
|
-
onclick={
|
|
522
|
-
if (isMultiple) {
|
|
523
|
-
if (selected.isFull && !_selectedColl.exists(item)) {
|
|
524
|
-
return notifications?.error(t("cardinality_full"), {
|
|
525
|
-
ttl: 1000,
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
_selectedColl.toggleAdd(item);
|
|
529
|
-
} else {
|
|
530
|
-
_selectedColl.clear();
|
|
531
|
-
_selectedColl.add(item);
|
|
532
|
-
submit();
|
|
533
|
-
}
|
|
534
|
-
}}
|
|
535
|
-
class:active
|
|
536
|
-
class:selected={isSelected}
|
|
657
|
+
bind:this={addNewBtn}
|
|
658
|
+
onclick={add_new}
|
|
537
659
|
class={twMerge(
|
|
538
|
-
|
|
539
|
-
"w-full text-left rounded-md py-2 px-2.5 flex items-center space-x-2",
|
|
540
|
-
"text-ellipsis border border-transparent",
|
|
541
|
-
"focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
|
|
542
|
-
"focus-visible:outline-0 focus-visible:ring-0",
|
|
543
|
-
"hover:border-neutral-400 dark:hover:border-neutral-500",
|
|
544
|
-
isSelected && "bg-neutral-200 dark:bg-neutral-800",
|
|
660
|
+
BTN_CLS,
|
|
545
661
|
classOption,
|
|
546
|
-
|
|
547
|
-
active && classOptionActive
|
|
662
|
+
isAddNewBtnActive && classOptionActive
|
|
548
663
|
)}
|
|
549
|
-
tabindex="-1"
|
|
550
|
-
role="checkbox"
|
|
551
|
-
aria-checked={isSelected}
|
|
552
664
|
>
|
|
553
|
-
{
|
|
554
|
-
<span class={isSelected ? "opacity-100" : "opacity-25"}>
|
|
555
|
-
{#if isMultiple}
|
|
556
|
-
{#if isSelected}
|
|
557
|
-
{@html iconCheckboxCheck()}
|
|
558
|
-
{:else}
|
|
559
|
-
{@html iconCheckboxEmpty()}
|
|
560
|
-
{/if}
|
|
561
|
-
{:else if isSelected}
|
|
562
|
-
{@html iconRadioCheck()}
|
|
563
|
-
{:else}
|
|
564
|
-
{@html iconRadioEmpty()}
|
|
565
|
-
{/if}
|
|
566
|
-
</span>
|
|
567
|
-
{/if}
|
|
568
|
-
<span>{_renderOptionLabel(item)}</span>
|
|
665
|
+
{t("add_new", { value: innerValue })}
|
|
569
666
|
</button>
|
|
570
|
-
</
|
|
667
|
+
</div>
|
|
668
|
+
{/if}
|
|
669
|
+
|
|
670
|
+
{#each _normalize_and_group_options(options.items) as [_optgroup, _opts]}
|
|
671
|
+
{#if _optgroup}
|
|
672
|
+
<div
|
|
673
|
+
class={twMerge(
|
|
674
|
+
"text-xs capitalize opacity-50 border-b border-black/10 mb-0.5 p-1 mx-1",
|
|
675
|
+
classOptgroup
|
|
676
|
+
)}
|
|
677
|
+
>
|
|
678
|
+
{_optgroup}
|
|
679
|
+
</div>
|
|
680
|
+
{/if}
|
|
681
|
+
<ul class="space-y-0.5">
|
|
682
|
+
<!-- {#each options.items as item} -->
|
|
683
|
+
{#each _opts as item (item[itemIdPropName])}
|
|
684
|
+
{@const active =
|
|
685
|
+
item[itemIdPropName] === options.active?.[itemIdPropName]}
|
|
686
|
+
{@const isSelected =
|
|
687
|
+
selected.items && _selectedColl.exists(item[itemIdPropName])}
|
|
688
|
+
<li class:active class="px-1">
|
|
689
|
+
<button
|
|
690
|
+
type="button"
|
|
691
|
+
id={btn_id(item[itemIdPropName])}
|
|
692
|
+
onclick={() => {
|
|
693
|
+
if (isMultiple) {
|
|
694
|
+
if (selected.isFull && !_selectedColl.exists(item)) {
|
|
695
|
+
return notifications?.error(t("cardinality_full"), {
|
|
696
|
+
ttl: 1000,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
_selectedColl.toggleAdd(item);
|
|
700
|
+
} else {
|
|
701
|
+
_selectedColl.clear();
|
|
702
|
+
_selectedColl.add(item);
|
|
703
|
+
submit();
|
|
704
|
+
}
|
|
705
|
+
}}
|
|
706
|
+
class:active
|
|
707
|
+
class:selected={isSelected}
|
|
708
|
+
class={twMerge(
|
|
709
|
+
BTN_CLS,
|
|
710
|
+
isSelected && "bg-neutral-200 dark:bg-neutral-800",
|
|
711
|
+
classOption,
|
|
712
|
+
// active && "border-neutral-400",
|
|
713
|
+
active && classOptionActive
|
|
714
|
+
)}
|
|
715
|
+
tabindex="-1"
|
|
716
|
+
role="checkbox"
|
|
717
|
+
aria-checked={isSelected}
|
|
718
|
+
>
|
|
719
|
+
{#if showIcons}
|
|
720
|
+
<span class={isSelected ? "opacity-100" : "opacity-25"}>
|
|
721
|
+
{#if isMultiple}
|
|
722
|
+
{#if isSelected}
|
|
723
|
+
{@html iconCheckboxCheck()}
|
|
724
|
+
{:else}
|
|
725
|
+
{@html iconCheckboxEmpty()}
|
|
726
|
+
{/if}
|
|
727
|
+
{:else if isSelected}
|
|
728
|
+
{@html iconRadioCheck()}
|
|
729
|
+
{:else}
|
|
730
|
+
{@html iconRadioEmpty()}
|
|
731
|
+
{/if}
|
|
732
|
+
</span>
|
|
733
|
+
{/if}
|
|
734
|
+
<span
|
|
735
|
+
class={twMerge(
|
|
736
|
+
"min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
|
|
737
|
+
)}>{_renderOptionLabel(item)}</span
|
|
738
|
+
>
|
|
739
|
+
</button>
|
|
740
|
+
</li>
|
|
741
|
+
{/each}
|
|
742
|
+
</ul>
|
|
571
743
|
{/each}
|
|
572
|
-
</
|
|
744
|
+
</div>
|
|
573
745
|
<!-- {/if} -->
|
|
574
|
-
<div class="p-2 flex items-end justify-between">
|
|
746
|
+
<div class="p-2 px-3 flex items-end justify-between">
|
|
575
747
|
<div class="text-xs opacity-50">
|
|
576
748
|
<!-- Use arrows to navigate. Spacebar and Enter to select and/or submit. -->
|
|
577
749
|
{#if allowUnknown}
|
|
@@ -585,9 +757,9 @@
|
|
|
585
757
|
class="control"
|
|
586
758
|
type="button"
|
|
587
759
|
variant="primary"
|
|
588
|
-
onclick={(e) => {
|
|
760
|
+
onclick={async (e) => {
|
|
589
761
|
e.preventDefault();
|
|
590
|
-
|
|
762
|
+
try_submit(true);
|
|
591
763
|
}}
|
|
592
764
|
tabindex={3}
|
|
593
765
|
>
|
|
@@ -611,7 +783,7 @@
|
|
|
611
783
|
"opacity-50 rounded",
|
|
612
784
|
"hover:opacity-100 hover:bg-neutral-200 dark:hover:bg-neutral-800",
|
|
613
785
|
"focus-visible:opacity-100 focus-visible:outline-0",
|
|
614
|
-
"
|
|
786
|
+
"focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
|
|
615
787
|
)}
|
|
616
788
|
use:tooltip
|
|
617
789
|
aria-label={t("x_close")}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
label: string;
|
|
3
|
-
value: any;
|
|
4
|
-
}
|
|
5
|
-
import { type Item } from "@marianmeres/item-collection";
|
|
1
|
+
import { ItemCollection, type Item } from "@marianmeres/item-collection";
|
|
6
2
|
import { type Snippet } from "svelte";
|
|
7
3
|
import { type ValidateOptions } from "../../actions/validate.svelte.js";
|
|
8
4
|
import { NotificationsStack } from "../Notifications/index.js";
|
|
9
5
|
import type { THC } from "../Thc/Thc.svelte";
|
|
6
|
+
import type { TranslateFn } from "../../types.js";
|
|
7
|
+
export interface Option {
|
|
8
|
+
label: string;
|
|
9
|
+
value: any;
|
|
10
|
+
}
|
|
10
11
|
type SnippetWithId = Snippet<[{
|
|
11
12
|
id: string;
|
|
12
13
|
}]>;
|
|
@@ -38,17 +39,23 @@ interface Props extends Record<string, any> {
|
|
|
38
39
|
classBelowBox?: string;
|
|
39
40
|
classOption?: string;
|
|
40
41
|
classOptionActive?: string;
|
|
42
|
+
classOptgroup?: string;
|
|
41
43
|
classModalField?: string;
|
|
42
44
|
noScrollLock?: boolean;
|
|
43
45
|
style?: string;
|
|
44
|
-
t?:
|
|
46
|
+
t?: TranslateFn;
|
|
45
47
|
renderValue?: (strigifiedItems: string) => string;
|
|
46
|
-
getOptions: (
|
|
48
|
+
getOptions: (q: string, current: Item[]) => Promise<{
|
|
49
|
+
coll?: ItemCollection<Item>;
|
|
50
|
+
found: Item[];
|
|
51
|
+
}>;
|
|
47
52
|
notifications?: NotificationsStack;
|
|
48
53
|
cardinality?: number;
|
|
49
54
|
renderOptionLabel?: (item: Item) => string;
|
|
55
|
+
renderOptionGroup?: (s: string) => string;
|
|
50
56
|
allowUnknown?: boolean;
|
|
51
|
-
|
|
57
|
+
showIconsCheckbox?: boolean;
|
|
58
|
+
showIconsRadio?: boolean;
|
|
52
59
|
searchPlaceholder?: string;
|
|
53
60
|
name: string;
|
|
54
61
|
itemIdPropName?: string;
|