@marianmeres/stuic 3.112.0 → 3.113.0
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.
|
@@ -33,7 +33,14 @@ export function autogrow(el, fn) {
|
|
|
33
33
|
// console.log(123, el.value);
|
|
34
34
|
if (enabled) {
|
|
35
35
|
el.style.height = "auto"; // Reset height to auto to correctly calculate scrollHeight
|
|
36
|
-
|
|
36
|
+
// `scrollHeight` excludes the border, but with `box-sizing: border-box` the
|
|
37
|
+
// height we set *includes* it — so without adding the vertical border back
|
|
38
|
+
// we undershoot by a couple px and a scrollbar lingers.
|
|
39
|
+
const cs = getComputedStyle(el);
|
|
40
|
+
const borderY = cs.boxSizing === "border-box"
|
|
41
|
+
? parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth)
|
|
42
|
+
: 0;
|
|
43
|
+
el.style.height = Math.max(min, Math.min(el.scrollHeight + borderY, max)) + "px";
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
// eventlistener strategy (we're not passing value)
|
|
@@ -42,13 +42,23 @@
|
|
|
42
42
|
addLabel?: string;
|
|
43
43
|
emptyMessage?: string;
|
|
44
44
|
onChange?: (value: string) => void;
|
|
45
|
+
/**
|
|
46
|
+
* When `true`, a value that *looks like* JSON (starts with `{`/`[`) but
|
|
47
|
+
* fails to parse becomes a blocking validation error on submit.
|
|
48
|
+
*
|
|
49
|
+
* Defaults to `false`: the component detects/parses JSON for convenience
|
|
50
|
+
* (pretty-print + the inline indicator) but does **not** enforce validity —
|
|
51
|
+
* whether a value must be valid JSON is a business rule the consumer owns.
|
|
52
|
+
* Unparseable values are simply stored as plain strings. Opt in to `true`
|
|
53
|
+
* only for a strictly JSON-only field.
|
|
54
|
+
*/
|
|
45
55
|
strictJsonValidation?: boolean;
|
|
46
56
|
t?: TranslateFn;
|
|
47
57
|
}
|
|
48
58
|
</script>
|
|
49
59
|
|
|
50
60
|
<script lang="ts">
|
|
51
|
-
import { iconPlus, iconTrash } from "../../icons/index.js";
|
|
61
|
+
import { iconAlertWarning, iconCode, iconPlus, iconTrash } from "../../icons/index.js";
|
|
52
62
|
import { tick } from "svelte";
|
|
53
63
|
import { autogrow } from "../../actions/autogrow.svelte.js";
|
|
54
64
|
import { validate as validateAction } from "../../actions/validate.svelte.js";
|
|
@@ -75,6 +85,7 @@
|
|
|
75
85
|
remove_entry: "Remove entry",
|
|
76
86
|
duplicate_keys: "Duplicate keys are not allowed",
|
|
77
87
|
invalid_json_syntax: "Invalid JSON syntax. Check for missing quotes or brackets.",
|
|
88
|
+
json_detected: "Valid JSON",
|
|
78
89
|
};
|
|
79
90
|
let out = m[k] ?? fallback ?? k;
|
|
80
91
|
return isPlainObject(values) ? replaceMap(out, values as any) : out;
|
|
@@ -118,7 +129,7 @@
|
|
|
118
129
|
addLabel,
|
|
119
130
|
emptyMessage,
|
|
120
131
|
onChange,
|
|
121
|
-
strictJsonValidation =
|
|
132
|
+
strictJsonValidation = false,
|
|
122
133
|
t = t_default,
|
|
123
134
|
}: Props = $props();
|
|
124
135
|
|
|
@@ -159,7 +170,7 @@
|
|
|
159
170
|
if (!isPlainObject(parsed)) return [];
|
|
160
171
|
return Object.entries(parsed).map(([key, val]) => ({
|
|
161
172
|
key,
|
|
162
|
-
value: typeof val === "string" ? val : JSON.stringify(val),
|
|
173
|
+
value: typeof val === "string" ? val : JSON.stringify(val, null, 2),
|
|
163
174
|
parsedValue: val,
|
|
164
175
|
}));
|
|
165
176
|
} catch (e) {
|
|
@@ -232,6 +243,35 @@
|
|
|
232
243
|
syncToValue();
|
|
233
244
|
}
|
|
234
245
|
|
|
246
|
+
// Pretty-print structured JSON (objects/arrays) on blur. Display-only: the
|
|
247
|
+
// parsed value is unchanged, so the serialized external `value` is identical
|
|
248
|
+
// and `syncToValue()` is intentionally not called. Primitives, plain strings,
|
|
249
|
+
// and invalid JSON are left exactly as typed (no surprising normalization).
|
|
250
|
+
function formatValueEntry(idx: number) {
|
|
251
|
+
const raw = entries[idx].value;
|
|
252
|
+
const trimmed = raw.trim();
|
|
253
|
+
if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) return;
|
|
254
|
+
try {
|
|
255
|
+
const parsed = JSON.parse(trimmed);
|
|
256
|
+
const pretty = JSON.stringify(parsed, null, 2);
|
|
257
|
+
if (pretty !== raw) {
|
|
258
|
+
entries[idx].value = pretty;
|
|
259
|
+
entries[idx].parsedValue = parsed;
|
|
260
|
+
entryJsonErrors[idx] = false;
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// invalid JSON: leave as typed; indicator + validation handle it
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Per-entry JSON signal for the subtle inline indicator.
|
|
268
|
+
function jsonState(entry: KeyValueEntry, idx: number): "valid" | "error" | "none" {
|
|
269
|
+
if (entryJsonErrors[idx]) return "error";
|
|
270
|
+
const v = entry.parsedValue;
|
|
271
|
+
if (isPlainObject(v) || Array.isArray(v)) return "valid";
|
|
272
|
+
return "none";
|
|
273
|
+
}
|
|
274
|
+
|
|
235
275
|
// Validation
|
|
236
276
|
let validation: ValidationResult | undefined = $state();
|
|
237
277
|
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
@@ -314,6 +354,7 @@
|
|
|
314
354
|
|
|
315
355
|
const INPUT_CLS = [
|
|
316
356
|
"rounded bg-(--stuic-color-input)",
|
|
357
|
+
"font-mono",
|
|
317
358
|
"focus:outline-none focus:ring-0",
|
|
318
359
|
"border border-(--stuic-color-border)",
|
|
319
360
|
"focus:border-(--stuic-color-border-hover)",
|
|
@@ -355,6 +396,7 @@
|
|
|
355
396
|
{:else}
|
|
356
397
|
<div class="p-2">
|
|
357
398
|
{#each entries as entry, idx (idx)}
|
|
399
|
+
{@const st = jsonState(entry, idx)}
|
|
358
400
|
<div
|
|
359
401
|
class={twMerge(
|
|
360
402
|
"flex gap-2 items-start py-2",
|
|
@@ -382,15 +424,42 @@
|
|
|
382
424
|
/>
|
|
383
425
|
|
|
384
426
|
<!-- Value textarea -->
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
427
|
+
<div class="relative">
|
|
428
|
+
<textarea
|
|
429
|
+
value={entry.value}
|
|
430
|
+
oninput={(e) => updateEntry(idx, "value", e.currentTarget.value)}
|
|
431
|
+
onblur={() => formatValueEntry(idx)}
|
|
432
|
+
placeholder={valuePlaceholder ?? t("value_placeholder")}
|
|
433
|
+
class={twMerge(
|
|
434
|
+
INPUT_CLS,
|
|
435
|
+
"w-full min-h-10 flex-none pr-6",
|
|
436
|
+
classValueInput
|
|
437
|
+
)}
|
|
438
|
+
{disabled}
|
|
439
|
+
{tabindex}
|
|
440
|
+
use:autogrow={() => ({ enabled: true, value: entry.value })}
|
|
441
|
+
></textarea>
|
|
442
|
+
|
|
443
|
+
<!-- Subtle JSON state indicator -->
|
|
444
|
+
{#if st !== "none"}
|
|
445
|
+
<span
|
|
446
|
+
class={twMerge(
|
|
447
|
+
"pointer-events-none absolute top-1.5 right-1.5",
|
|
448
|
+
st === "valid"
|
|
449
|
+
? "opacity-40"
|
|
450
|
+
: "text-amber-500 opacity-80"
|
|
451
|
+
)}
|
|
452
|
+
title={st === "valid"
|
|
453
|
+
? t("json_detected")
|
|
454
|
+
: t("invalid_json_syntax")}
|
|
455
|
+
aria-hidden="true"
|
|
456
|
+
>
|
|
457
|
+
{@html st === "valid"
|
|
458
|
+
? iconCode({ size: 14 })
|
|
459
|
+
: iconAlertWarning({ size: 14 })}
|
|
460
|
+
</span>
|
|
461
|
+
{/if}
|
|
462
|
+
</div>
|
|
394
463
|
</div>
|
|
395
464
|
|
|
396
465
|
<!-- Delete button -->
|
|
@@ -37,6 +37,16 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
|
37
37
|
addLabel?: string;
|
|
38
38
|
emptyMessage?: string;
|
|
39
39
|
onChange?: (value: string) => void;
|
|
40
|
+
/**
|
|
41
|
+
* When `true`, a value that *looks like* JSON (starts with `{`/`[`) but
|
|
42
|
+
* fails to parse becomes a blocking validation error on submit.
|
|
43
|
+
*
|
|
44
|
+
* Defaults to `false`: the component detects/parses JSON for convenience
|
|
45
|
+
* (pretty-print + the inline indicator) but does **not** enforce validity —
|
|
46
|
+
* whether a value must be valid JSON is a business rule the consumer owns.
|
|
47
|
+
* Unparseable values are simply stored as plain strings. Opt in to `true`
|
|
48
|
+
* only for a strictly JSON-only field.
|
|
49
|
+
*/
|
|
40
50
|
strictJsonValidation?: boolean;
|
|
41
51
|
t?: TranslateFn;
|
|
42
52
|
}
|