@marianmeres/stuic 2.0.0-next.5 → 2.0.3
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 +1 -1
- package/dist/components/Button/Button.svelte.d.ts +1 -1
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +47 -12
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte.d.ts +3 -2
- package/dist/components/ButtonGroupRadio/index.css +11 -2
- 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 +278 -120
- 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,32 +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
|
-
clear: "Clear",
|
|
17
|
-
search_placeholder: "Type to search...",
|
|
18
|
-
search_submit_placeholder: "Type to search and/or submit...",
|
|
19
|
-
cardinality_full: "Max selection reached",
|
|
20
|
-
select_from_list: "Please select from the list",
|
|
21
|
-
x_close: "Clear input or close [esc]",
|
|
22
|
-
unknown_allowed: "Select from the list or type and submit any value",
|
|
23
|
-
unknown_not_allowed: "Select values from the list only",
|
|
24
|
-
};
|
|
25
|
-
return m[k] ?? k;
|
|
26
|
-
}
|
|
27
|
-
</script>
|
|
28
|
-
|
|
29
|
-
<script lang="ts">
|
|
30
2
|
import { createClog } from "@marianmeres/clog";
|
|
31
3
|
import { iconBsSearch } from "@marianmeres/icons-fns/bootstrap/iconBsSearch.js";
|
|
32
4
|
import { iconLucideCheck } from "@marianmeres/icons-fns/lucide/iconLucideCheck.js";
|
|
@@ -34,7 +6,7 @@
|
|
|
34
6
|
import { iconLucideSquare } from "@marianmeres/icons-fns/lucide/iconLucideSquare.js";
|
|
35
7
|
import { ItemCollection, type Item } from "@marianmeres/item-collection";
|
|
36
8
|
import { Debounced, watch } from "runed";
|
|
37
|
-
import { type Snippet } from "svelte";
|
|
9
|
+
import { tick, type Snippet } from "svelte";
|
|
38
10
|
import { tooltip } from "../../actions/index.js";
|
|
39
11
|
import { type ValidateOptions } from "../../actions/validate.svelte.js";
|
|
40
12
|
import { getId } from "../../utils/get-id.js";
|
|
@@ -51,7 +23,48 @@
|
|
|
51
23
|
import X from "../X/X.svelte";
|
|
52
24
|
import InputWrap from "./_internal/InputWrap.svelte";
|
|
53
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";
|
|
29
|
+
|
|
30
|
+
export interface Option {
|
|
31
|
+
label: string;
|
|
32
|
+
value: any;
|
|
33
|
+
}
|
|
54
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">
|
|
55
68
|
const clog = createClog("FieldOptions");
|
|
56
69
|
|
|
57
70
|
const iconCheckboxEmpty = iconLucideSquare;
|
|
@@ -96,23 +109,29 @@
|
|
|
96
109
|
//
|
|
97
110
|
classOption?: string;
|
|
98
111
|
classOptionActive?: string;
|
|
112
|
+
classOptgroup?: string;
|
|
99
113
|
//
|
|
100
114
|
classModalField?: string;
|
|
101
115
|
noScrollLock?: boolean;
|
|
102
116
|
//
|
|
103
117
|
style?: string;
|
|
104
|
-
t?:
|
|
118
|
+
t?: TranslateFn;
|
|
105
119
|
//
|
|
106
120
|
renderValue?: (strigifiedItems: string) => string;
|
|
107
|
-
getOptions: (
|
|
121
|
+
getOptions: (
|
|
122
|
+
q: string,
|
|
123
|
+
current: Item[]
|
|
124
|
+
) => Promise<{ coll?: ItemCollection<Item>; found: Item[] }>;
|
|
108
125
|
notifications?: NotificationsStack;
|
|
109
126
|
// -1 no limit
|
|
110
127
|
// +n max selected limit
|
|
111
128
|
cardinality?: number;
|
|
112
129
|
renderOptionLabel?: (item: Item) => string;
|
|
130
|
+
renderOptionGroup?: (s: string) => string;
|
|
113
131
|
// whether to allow adding unknown options
|
|
114
132
|
allowUnknown?: boolean;
|
|
115
|
-
|
|
133
|
+
showIconsCheckbox?: boolean;
|
|
134
|
+
showIconsRadio?: boolean;
|
|
116
135
|
searchPlaceholder?: string;
|
|
117
136
|
name: string;
|
|
118
137
|
itemIdPropName?: string;
|
|
@@ -152,6 +171,7 @@
|
|
|
152
171
|
//
|
|
153
172
|
classOption,
|
|
154
173
|
classOptionActive,
|
|
174
|
+
classOptgroup,
|
|
155
175
|
//
|
|
156
176
|
style,
|
|
157
177
|
//
|
|
@@ -164,8 +184,10 @@
|
|
|
164
184
|
notifications,
|
|
165
185
|
cardinality: _cardinality = Infinity,
|
|
166
186
|
renderOptionLabel,
|
|
187
|
+
renderOptionGroup = (s: string) => `${s}`.replaceAll("_", " "),
|
|
167
188
|
allowUnknown = false,
|
|
168
|
-
|
|
189
|
+
showIconsCheckbox = true,
|
|
190
|
+
showIconsRadio = false,
|
|
169
191
|
searchPlaceholder,
|
|
170
192
|
name,
|
|
171
193
|
itemIdPropName = "id",
|
|
@@ -177,6 +199,7 @@
|
|
|
177
199
|
let isFetching = $state(false);
|
|
178
200
|
let cardinality = $derived(_cardinality === -1 ? Infinity : _cardinality);
|
|
179
201
|
let isMultiple = $derived(cardinality > 1);
|
|
202
|
+
let showIcons = $derived(isMultiple ? showIconsCheckbox : showIconsRadio);
|
|
180
203
|
|
|
181
204
|
//
|
|
182
205
|
let wrappedValidate: Omit<ValidateOptions, "setValidationResult"> = $derived({
|
|
@@ -208,7 +231,8 @@
|
|
|
208
231
|
}
|
|
209
232
|
|
|
210
233
|
function sortFn(a: Item, b: Item) {
|
|
211
|
-
|
|
234
|
+
const withOptGroup = (i: Item) => `${i.optgroup || ""}__${_renderOptionLabel(i)}`;
|
|
235
|
+
return withOptGroup(a).localeCompare(withOptGroup(b), undefined, {
|
|
212
236
|
sensitivity: "base",
|
|
213
237
|
});
|
|
214
238
|
}
|
|
@@ -238,13 +262,29 @@
|
|
|
238
262
|
// now, create the reactive, subscribed variants
|
|
239
263
|
let options = $derived($_optionsColl);
|
|
240
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
|
+
|
|
241
273
|
// $inspect("options", options);
|
|
242
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();
|
|
243
279
|
|
|
244
280
|
let activeEl: HTMLButtonElement | undefined = $state();
|
|
245
|
-
let optionsBox:
|
|
281
|
+
let optionsBox: HTMLDivElement | undefined = $state();
|
|
246
282
|
let modalEl: HTMLDivElement | undefined = $state();
|
|
247
283
|
|
|
284
|
+
// add_new dance...
|
|
285
|
+
let addNewBtn: HTMLButtonElement | undefined = $state();
|
|
286
|
+
let isAddNewBtnActive = $state(false);
|
|
287
|
+
|
|
248
288
|
// set value on open
|
|
249
289
|
watch(
|
|
250
290
|
() => modal.visibility().visible,
|
|
@@ -265,7 +305,7 @@
|
|
|
265
305
|
// scroll the active option into view
|
|
266
306
|
$effect(() => {
|
|
267
307
|
if (modal.visibility().visible && options.active?.[itemIdPropName]) {
|
|
268
|
-
activeEl = qsa(`#${
|
|
308
|
+
activeEl = qsa(`#${btn_id(options.active[itemIdPropName])}`, optionsBox)[0] as any;
|
|
269
309
|
activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
270
310
|
activeEl?.focus();
|
|
271
311
|
} else {
|
|
@@ -282,10 +322,12 @@
|
|
|
282
322
|
isFetching = true;
|
|
283
323
|
getOptions(currVal, selected.items)
|
|
284
324
|
.then((res) => {
|
|
325
|
+
const { found, coll } = res;
|
|
326
|
+
|
|
285
327
|
// always update the existing with recent server data
|
|
286
|
-
_selectedColl.patchMany(
|
|
328
|
+
_selectedColl.patchMany(found);
|
|
287
329
|
// continue normally, with (server) provided options...
|
|
288
|
-
_optionsColl.clear().addMany(
|
|
330
|
+
_optionsColl.clear().addMany(found);
|
|
289
331
|
})
|
|
290
332
|
.catch((e) => {
|
|
291
333
|
console.error(e);
|
|
@@ -296,32 +338,23 @@
|
|
|
296
338
|
);
|
|
297
339
|
|
|
298
340
|
// internal DRY
|
|
299
|
-
function
|
|
341
|
+
function btn_id(id: string | number, prefix = "btn-") {
|
|
300
342
|
return prefix + strHash(`${id}`.repeat(3));
|
|
301
343
|
}
|
|
302
344
|
|
|
303
345
|
// "inner" submit
|
|
304
346
|
function try_submit(force = false) {
|
|
347
|
+
clog("try_submit", innerValue);
|
|
305
348
|
if (innerValue) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (!allowUnknown) {
|
|
310
|
-
return notifications?.error(t("select_from_list"), { ttl: 1000 });
|
|
311
|
-
}
|
|
312
|
-
found = { [itemIdPropName]: 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 });
|
|
313
352
|
}
|
|
314
353
|
|
|
315
|
-
if (!
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// we might have added a new one, so add it to options as well
|
|
321
|
-
// (will be noop if already exists)...
|
|
322
|
-
if (allowUnknown) {
|
|
323
|
-
_optionsColl.add(found);
|
|
324
|
-
_optionsColl.setActive(found);
|
|
354
|
+
if (!found && !_optionsColl.size) {
|
|
355
|
+
return notifications?.error(t("click_add_new", { value: innerValue }), {
|
|
356
|
+
ttl: 1000,
|
|
357
|
+
});
|
|
325
358
|
}
|
|
326
359
|
|
|
327
360
|
// maybe submit
|
|
@@ -333,6 +366,27 @@
|
|
|
333
366
|
}
|
|
334
367
|
}
|
|
335
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
|
+
|
|
336
390
|
// "outer" submit - will set the outer bound value (always string) and close modal...
|
|
337
391
|
// further process is left on the consumer
|
|
338
392
|
function submit() {
|
|
@@ -341,6 +395,7 @@
|
|
|
341
395
|
innerValue = "";
|
|
342
396
|
_optionsColl.clear();
|
|
343
397
|
modal.close();
|
|
398
|
+
_dispatch_change_to_owner();
|
|
344
399
|
}
|
|
345
400
|
|
|
346
401
|
// clears, closes, submits nothing
|
|
@@ -348,6 +403,64 @@
|
|
|
348
403
|
innerValue = "";
|
|
349
404
|
_optionsColl.clear();
|
|
350
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;
|
|
351
464
|
}
|
|
352
465
|
</script>
|
|
353
466
|
|
|
@@ -359,10 +472,14 @@
|
|
|
359
472
|
if (["ArrowDown", "ArrowUp"].includes(e.key)) {
|
|
360
473
|
e.preventDefault();
|
|
361
474
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
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
|
+
}
|
|
366
483
|
}
|
|
367
484
|
|
|
368
485
|
// common UI convention: radios are selected by arrows
|
|
@@ -382,6 +499,7 @@
|
|
|
382
499
|
<div>
|
|
383
500
|
<FieldLikeButton
|
|
384
501
|
bind:value
|
|
502
|
+
bind:input={parentHiddenInputEl}
|
|
385
503
|
{name}
|
|
386
504
|
class={classProp}
|
|
387
505
|
{label}
|
|
@@ -437,7 +555,7 @@
|
|
|
437
555
|
>
|
|
438
556
|
<InputWrap
|
|
439
557
|
size={renderSize}
|
|
440
|
-
class={twMerge("m-
|
|
558
|
+
class={twMerge("m-2 mb-12 shadow-xl", classModalField)}
|
|
441
559
|
classInputBoxWrap={twMerge(
|
|
442
560
|
// always look like focused
|
|
443
561
|
`border border-input-accent dark:border-input-accent-dark`,
|
|
@@ -480,6 +598,7 @@
|
|
|
480
598
|
"hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
|
|
481
599
|
)}
|
|
482
600
|
tabindex={4}
|
|
601
|
+
disabled={!options.size}
|
|
483
602
|
>
|
|
484
603
|
{@html t("select_all")}
|
|
485
604
|
</button>
|
|
@@ -512,80 +631,119 @@
|
|
|
512
631
|
</div>
|
|
513
632
|
|
|
514
633
|
<!-- {#if options.items.length} -->
|
|
515
|
-
<
|
|
516
|
-
class={
|
|
517
|
-
"options
|
|
518
|
-
|
|
634
|
+
<div
|
|
635
|
+
class={[
|
|
636
|
+
"options overflow-y-auto overflow-x-hidden space-y-1",
|
|
637
|
+
"h-[220px] max-h-[220px]",
|
|
638
|
+
]}
|
|
519
639
|
bind:this={optionsBox}
|
|
520
640
|
tabindex="-1"
|
|
521
641
|
>
|
|
522
642
|
{#if isFetching && !options.items.length}
|
|
523
|
-
<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">
|
|
524
645
|
<Spinner class="w-4" />
|
|
525
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>
|
|
526
651
|
{/if}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
selected.items && _selectedColl.exists(item[itemIdPropName])}
|
|
531
|
-
<li class:active class="px-2">
|
|
652
|
+
|
|
653
|
+
{#if !isFetching && allowUnknown && innerValue && !have_option_label_like(options.items, innerValue)}
|
|
654
|
+
<div class="px-1">
|
|
532
655
|
<button
|
|
533
656
|
type="button"
|
|
534
|
-
|
|
535
|
-
onclick={
|
|
536
|
-
if (isMultiple) {
|
|
537
|
-
if (selected.isFull && !_selectedColl.exists(item)) {
|
|
538
|
-
return notifications?.error(t("cardinality_full"), {
|
|
539
|
-
ttl: 1000,
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
_selectedColl.toggleAdd(item);
|
|
543
|
-
} else {
|
|
544
|
-
_selectedColl.clear();
|
|
545
|
-
_selectedColl.add(item);
|
|
546
|
-
submit();
|
|
547
|
-
}
|
|
548
|
-
}}
|
|
549
|
-
class:active
|
|
550
|
-
class:selected={isSelected}
|
|
657
|
+
bind:this={addNewBtn}
|
|
658
|
+
onclick={add_new}
|
|
551
659
|
class={twMerge(
|
|
552
|
-
|
|
553
|
-
"w-full text-left rounded-md py-2 px-2.5 flex items-center space-x-2",
|
|
554
|
-
"text-ellipsis border border-transparent",
|
|
555
|
-
"focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
|
|
556
|
-
"focus-visible:outline-0 focus-visible:ring-0",
|
|
557
|
-
"hover:border-neutral-400 dark:hover:border-neutral-500",
|
|
558
|
-
isSelected && "bg-neutral-200 dark:bg-neutral-800",
|
|
660
|
+
BTN_CLS,
|
|
559
661
|
classOption,
|
|
560
|
-
|
|
561
|
-
active && classOptionActive
|
|
662
|
+
isAddNewBtnActive && classOptionActive
|
|
562
663
|
)}
|
|
563
|
-
tabindex="-1"
|
|
564
|
-
role="checkbox"
|
|
565
|
-
aria-checked={isSelected}
|
|
566
664
|
>
|
|
567
|
-
{
|
|
568
|
-
<span class={isSelected ? "opacity-100" : "opacity-25"}>
|
|
569
|
-
{#if isMultiple}
|
|
570
|
-
{#if isSelected}
|
|
571
|
-
{@html iconCheckboxCheck()}
|
|
572
|
-
{:else}
|
|
573
|
-
{@html iconCheckboxEmpty()}
|
|
574
|
-
{/if}
|
|
575
|
-
{:else if isSelected}
|
|
576
|
-
{@html iconRadioCheck()}
|
|
577
|
-
{:else}
|
|
578
|
-
{@html iconRadioEmpty()}
|
|
579
|
-
{/if}
|
|
580
|
-
</span>
|
|
581
|
-
{/if}
|
|
582
|
-
<span>{_renderOptionLabel(item)}</span>
|
|
665
|
+
{t("add_new", { value: innerValue })}
|
|
583
666
|
</button>
|
|
584
|
-
</
|
|
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>
|
|
585
743
|
{/each}
|
|
586
|
-
</
|
|
744
|
+
</div>
|
|
587
745
|
<!-- {/if} -->
|
|
588
|
-
<div class="p-2 flex items-end justify-between">
|
|
746
|
+
<div class="p-2 px-3 flex items-end justify-between">
|
|
589
747
|
<div class="text-xs opacity-50">
|
|
590
748
|
<!-- Use arrows to navigate. Spacebar and Enter to select and/or submit. -->
|
|
591
749
|
{#if allowUnknown}
|
|
@@ -625,7 +783,7 @@
|
|
|
625
783
|
"opacity-50 rounded",
|
|
626
784
|
"hover:opacity-100 hover:bg-neutral-200 dark:hover:bg-neutral-800",
|
|
627
785
|
"focus-visible:opacity-100 focus-visible:outline-0",
|
|
628
|
-
"
|
|
786
|
+
"focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
|
|
629
787
|
)}
|
|
630
788
|
use:tooltip
|
|
631
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;
|
|
@@ -112,9 +112,10 @@
|
|
|
112
112
|
class={twMerge(
|
|
113
113
|
"stuic-input",
|
|
114
114
|
_classCommon,
|
|
115
|
-
"mb-8
|
|
116
|
-
hasLabel && labelLeft &&
|
|
117
|
-
hasLabel && labelLeft && labelLeftWidth === "
|
|
115
|
+
"mb-8",
|
|
116
|
+
hasLabel && labelLeft && "flex",
|
|
117
|
+
hasLabel && labelLeft && labelLeftWidth === "normal" && "width-normal",
|
|
118
|
+
hasLabel && labelLeft && labelLeftWidth === "wide" && "width-wide",
|
|
118
119
|
classProp
|
|
119
120
|
)}
|
|
120
121
|
bind:clientWidth={width}
|
|
@@ -124,7 +125,7 @@
|
|
|
124
125
|
class={twMerge(
|
|
125
126
|
"label-box",
|
|
126
127
|
_classCommon,
|
|
127
|
-
"flex",
|
|
128
|
+
"flex flex-1",
|
|
128
129
|
labelLeft ? "left items-start mt-2" : "items-end",
|
|
129
130
|
classLabelBox
|
|
130
131
|
)}
|
|
@@ -152,8 +153,8 @@
|
|
|
152
153
|
class={twMerge(
|
|
153
154
|
"input-box",
|
|
154
155
|
_classCommon,
|
|
155
|
-
hasLabel && labelLeft && labelLeftWidth === "normal" && "
|
|
156
|
-
hasLabel && labelLeft && labelLeftWidth === "wide" && "
|
|
156
|
+
hasLabel && labelLeft && labelLeftWidth === "normal" && "flex-3",
|
|
157
|
+
hasLabel && labelLeft && labelLeftWidth === "wide" && "flex-2",
|
|
157
158
|
classInputBox
|
|
158
159
|
)}
|
|
159
160
|
>
|