vira 31.16.2 → 31.17.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.
@@ -0,0 +1,769 @@
1
+ import { assertWrap, check } from '@augment-vir/assert';
2
+ import { omitObjectKeys } from '@augment-vir/common';
3
+ import { css, defineElementEvent, html, listen, nothing } from 'element-vir';
4
+ import { lucideIcons, X16Icon } from '../icons/index.js';
5
+ import { viraFontCssVars } from '../styles/font.js';
6
+ import { viraFormCssVars } from '../styles/form-styles.js';
7
+ import { ViraColorVariant, ViraEmphasis, ViraSize } from '../styles/form-variants.js';
8
+ import { defineViraElement } from '../util/define-vira-element.js';
9
+ import { createDefaultForJsonType, createResolveContext, deleteValueAtPath, getAdditionalPropertiesSchema, getAllowedJsonTypes, getDefinedProperties, getEnumValues, getItemSchema, getJsonType, getNewItemSchema, getPropertySchema, getRequiredProperties, getSchemaTitle, pathToKey, pickBranchForType, setValueAtPath, validateAgainstSchema, ViraJsonType, viraJsonTypeLabels, } from '../util/vira-json-schema.js';
10
+ import { ViraButton } from './vira-button.element.js';
11
+ import { ViraCheckbox } from './vira-checkbox.element.js';
12
+ import { ViraError } from './vira-error.element.js';
13
+ import { ViraInput, ViraInputType } from './vira-input.element.js';
14
+ import { ViraSelect } from './vira-select.element.js';
15
+ /**
16
+ * An editor for arbitrary JSON values, optionally constrained by a standard JSON Schema
17
+ * ({@link ViraJsonSchema}).
18
+ *
19
+ * @category Elements
20
+ */
21
+ export const ViraJsonForm = defineViraElement()({
22
+ tagName: 'vira-json-form',
23
+ events: {
24
+ valueChange: defineElementEvent(),
25
+ },
26
+ state() {
27
+ return {
28
+ pendingKeys: {},
29
+ pendingTypes: {},
30
+ pendingArrayValues: {},
31
+ showRaw: false,
32
+ };
33
+ },
34
+ styles: css `
35
+ :host {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 10px;
39
+ font-size: ${viraFormCssVars['vira-form-medium-text-size'].value};
40
+ color: ${viraFormCssVars['vira-form-foreground-color'].value};
41
+ }
42
+
43
+ .json-toolbar {
44
+ display: flex;
45
+ justify-content: flex-end;
46
+ }
47
+
48
+ .json-raw-pre {
49
+ margin: 0;
50
+ padding: 10px 12px;
51
+ border: 1px solid ${viraFormCssVars['vira-form-border-color'].value};
52
+ border-radius: ${viraFormCssVars['vira-form-wrapper-radius'].value};
53
+ background-color: ${viraFormCssVars['vira-form-background-color'].value};
54
+ font-family: ${viraFontCssVars['vira-monospace'].value};
55
+ font-size: ${viraFormCssVars['vira-form-small-text-size'].value};
56
+ white-space: pre-wrap;
57
+ word-break: break-word;
58
+ overflow-x: auto;
59
+ box-sizing: border-box;
60
+ }
61
+
62
+ .json-validation-errors {
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: 4px;
66
+ }
67
+
68
+ .json-validation-errors ul {
69
+ margin: 0;
70
+ padding-left: 20px;
71
+ }
72
+
73
+ .json-group {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: 8px;
77
+ padding: 10px 12px;
78
+ border: 1px solid ${viraFormCssVars['vira-form-border-color'].value};
79
+ border-radius: ${viraFormCssVars['vira-form-wrapper-radius'].value};
80
+ background-color: ${viraFormCssVars['vira-form-background-color'].value};
81
+ }
82
+
83
+ .json-group-header {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 8px;
87
+ }
88
+
89
+ .json-group-header-title {
90
+ flex-grow: 1;
91
+ font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
92
+ color: ${viraFormCssVars['vira-form-secondary-body-foreground'].value};
93
+ font-size: ${viraFormCssVars['vira-form-small-text-size'].value};
94
+ }
95
+
96
+ .json-row {
97
+ display: flex;
98
+ gap: 8px;
99
+ }
100
+
101
+ .json-row-primitive {
102
+ align-items: center;
103
+ }
104
+
105
+ .json-row-nested {
106
+ align-items: flex-start;
107
+ }
108
+
109
+ .json-row-label {
110
+ flex-shrink: 0;
111
+ min-width: 80px;
112
+ font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
113
+ word-break: break-word;
114
+ }
115
+
116
+ .json-row-nested .json-row-label {
117
+ padding-top: 10px;
118
+ }
119
+
120
+ .json-row-editor {
121
+ flex-grow: 1;
122
+ min-width: 0;
123
+ display: flex;
124
+ flex-direction: column;
125
+ }
126
+
127
+ .json-row-editor > * {
128
+ width: 100%;
129
+ max-width: 100%;
130
+ }
131
+
132
+ .json-row-delete {
133
+ flex-shrink: 0;
134
+ width: 24px;
135
+ display: flex;
136
+ justify-content: center;
137
+ }
138
+
139
+ .json-row-nested .json-row-delete {
140
+ padding-top: 4px;
141
+ }
142
+
143
+ .json-value-with-switcher {
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 6px;
147
+ }
148
+
149
+ .json-value-with-switcher > .json-value-editor-slot {
150
+ flex-grow: 1;
151
+ min-width: 0;
152
+ display: flex;
153
+ }
154
+
155
+ .json-value-with-switcher > .json-value-editor-slot > * {
156
+ flex-grow: 1;
157
+ min-width: 0;
158
+ }
159
+
160
+ .json-value-with-switcher > ${ViraSelect} {
161
+ flex-shrink: 0;
162
+ width: 130px;
163
+ }
164
+
165
+ .json-add-row {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 6px;
169
+ flex-wrap: wrap;
170
+ }
171
+
172
+ .json-add-row ${ViraInput}, .json-add-row ${ViraSelect} {
173
+ width: 160px;
174
+ }
175
+
176
+ .json-add-row .json-add-value-input {
177
+ flex-grow: 1;
178
+ width: auto;
179
+ min-width: 160px;
180
+ }
181
+
182
+ .json-null-indicator {
183
+ padding: 6px 10px;
184
+ color: ${viraFormCssVars['vira-form-placeholder-color'].value};
185
+ font-style: italic;
186
+ }
187
+
188
+ .json-empty-note {
189
+ color: ${viraFormCssVars['vira-form-placeholder-color'].value};
190
+ font-style: italic;
191
+ font-size: ${viraFormCssVars['vira-form-small-text-size'].value};
192
+ }
193
+ `,
194
+ render({ inputs, state, dispatch, events, updateState }) {
195
+ const isDisabled = !!inputs.isDisabled;
196
+ const resolveContext = createResolveContext(inputs.schema);
197
+ function jsonPrimitiveToString(entry) {
198
+ return entry == undefined ? 'null' : String(entry);
199
+ }
200
+ function emitRoot(newRoot) {
201
+ dispatch(new events.valueChange(newRoot));
202
+ }
203
+ function emitReplaceAt(path, newValue) {
204
+ emitRoot(setValueAtPath(inputs.value, path, newValue));
205
+ }
206
+ function emitDeleteAt(path) {
207
+ emitRoot(deleteValueAtPath(inputs.value, path));
208
+ }
209
+ function getPendingType(pathKey, allowedTypes) {
210
+ const current = state.pendingTypes[pathKey];
211
+ if (current && allowedTypes.includes(current)) {
212
+ return current;
213
+ }
214
+ return allowedTypes[0] ?? ViraJsonType.String;
215
+ }
216
+ function setPendingType(pathKey, type) {
217
+ updateState({
218
+ pendingTypes: {
219
+ ...state.pendingTypes,
220
+ [pathKey]: type,
221
+ },
222
+ });
223
+ }
224
+ function setPendingKey(pathKey, key) {
225
+ updateState({
226
+ pendingKeys: {
227
+ ...state.pendingKeys,
228
+ [pathKey]: key,
229
+ },
230
+ });
231
+ }
232
+ function getPendingArrayValue(pathKey, fallback) {
233
+ const stored = state.pendingArrayValues[pathKey];
234
+ return stored === undefined ? fallback : stored;
235
+ }
236
+ function setPendingArrayValue(pathKey, value) {
237
+ updateState({
238
+ pendingArrayValues: {
239
+ ...state.pendingArrayValues,
240
+ [pathKey]: value,
241
+ },
242
+ });
243
+ }
244
+ function clearPending(pathKey) {
245
+ updateState({
246
+ pendingKeys: omitObjectKeys(state.pendingKeys, [pathKey]),
247
+ pendingTypes: omitObjectKeys(state.pendingTypes, [pathKey]),
248
+ pendingArrayValues: omitObjectKeys(state.pendingArrayValues, [pathKey]),
249
+ });
250
+ }
251
+ function renderDeleteButton(onDelete) {
252
+ return html `
253
+ <${ViraButton.assign({
254
+ icon: X16Icon,
255
+ buttonEmphasis: ViraEmphasis.Subtle,
256
+ color: ViraColorVariant.Danger,
257
+ buttonSize: ViraSize.Small,
258
+ })}
259
+ title="Remove"
260
+ ${listen('click', onDelete)}
261
+ ></${ViraButton}>
262
+ `;
263
+ }
264
+ function renderPrimitive(path, value, schema) {
265
+ const type = getJsonType(value);
266
+ const enumValues = getEnumValues(schema, resolveContext);
267
+ if (enumValues && enumValues.length > 0) {
268
+ const options = enumValues.map((entry) => {
269
+ const asString = jsonPrimitiveToString(entry);
270
+ return {
271
+ value: asString,
272
+ label: asString,
273
+ };
274
+ });
275
+ const isPrimitive = value == undefined ||
276
+ check.isString(value) ||
277
+ check.isNumber(value) ||
278
+ check.isBoolean(value);
279
+ return html `
280
+ <${ViraSelect.assign({
281
+ options,
282
+ value: isPrimitive ? jsonPrimitiveToString(value) : undefined,
283
+ disabled: isDisabled,
284
+ })}
285
+ ${listen(ViraSelect.events.valueChange, (event) => {
286
+ const selected = enumValues.find((entry) => jsonPrimitiveToString(entry) === event.detail);
287
+ emitReplaceAt(path, selected ?? event.detail);
288
+ })}
289
+ ></${ViraSelect}>
290
+ `;
291
+ }
292
+ else if (type === ViraJsonType.Boolean) {
293
+ return html `
294
+ <${ViraCheckbox.assign({
295
+ value: value === true,
296
+ disabled: isDisabled,
297
+ })}
298
+ ${listen(ViraCheckbox.events.valueChange, (event) => {
299
+ emitReplaceAt(path, event.detail);
300
+ })}
301
+ ></${ViraCheckbox}>
302
+ `;
303
+ }
304
+ else if (type === ViraJsonType.Number || type === ViraJsonType.Integer) {
305
+ const schemaAllowedTypes = getAllowedJsonTypes(schema, resolveContext);
306
+ const isIntegerOnly = schemaAllowedTypes.length === 1 &&
307
+ schemaAllowedTypes[0] === ViraJsonType.Integer;
308
+ return html `
309
+ <${ViraInput.assign({
310
+ type: ViraInputType.Number,
311
+ value: check.isNumber(value) ? String(value) : '',
312
+ disabled: isDisabled,
313
+ allowedInputs: isIntegerOnly ? /[\d-]/ : /[\d.-]/,
314
+ })}
315
+ ${listen(ViraInput.events.valueChange, (event) => {
316
+ const text = event.detail;
317
+ const parsed = text === '' ? 0 : Number(text);
318
+ const clean = Number.isFinite(parsed) ? parsed : 0;
319
+ emitReplaceAt(path, isIntegerOnly ? Math.trunc(clean) : clean);
320
+ })}
321
+ ></${ViraInput}>
322
+ `;
323
+ }
324
+ else if (type === ViraJsonType.Null) {
325
+ return html `
326
+ <span class="json-null-indicator">null</span>
327
+ `;
328
+ }
329
+ return html `
330
+ <${ViraInput.assign({
331
+ value: check.isString(value) ? value : '',
332
+ disabled: isDisabled,
333
+ })}
334
+ ${listen(ViraInput.events.valueChange, (event) => {
335
+ emitReplaceAt(path, event.detail);
336
+ })}
337
+ ></${ViraInput}>
338
+ `;
339
+ }
340
+ function renderInlineValueEditor(type, currentValue, onChange) {
341
+ if (type === ViraJsonType.String) {
342
+ const text = check.isString(currentValue) ? currentValue : '';
343
+ return html `
344
+ <${ViraInput.assign({
345
+ value: text,
346
+ placeholder: 'new item value',
347
+ })}
348
+ class="json-add-value-input"
349
+ ${listen(ViraInput.events.valueChange, (event) => {
350
+ onChange(event.detail);
351
+ })}
352
+ ></${ViraInput}>
353
+ `;
354
+ }
355
+ else if (type === ViraJsonType.Number || type === ViraJsonType.Integer) {
356
+ const isIntegerOnly = type === ViraJsonType.Integer;
357
+ const numText = check.isNumber(currentValue) ? String(currentValue) : '';
358
+ return html `
359
+ <${ViraInput.assign({
360
+ type: ViraInputType.Number,
361
+ value: numText,
362
+ placeholder: 'new item value',
363
+ allowedInputs: isIntegerOnly ? /[\d-]/ : /[\d.-]/,
364
+ })}
365
+ class="json-add-value-input"
366
+ ${listen(ViraInput.events.valueChange, (event) => {
367
+ const text = event.detail;
368
+ const parsed = text === '' ? 0 : Number(text);
369
+ const clean = Number.isFinite(parsed) ? parsed : 0;
370
+ onChange(isIntegerOnly ? Math.trunc(clean) : clean);
371
+ })}
372
+ ></${ViraInput}>
373
+ `;
374
+ }
375
+ else if (type === ViraJsonType.Boolean) {
376
+ return html `
377
+ <${ViraCheckbox.assign({
378
+ value: currentValue === true,
379
+ })}
380
+ ${listen(ViraCheckbox.events.valueChange, (event) => {
381
+ onChange(event.detail);
382
+ })}
383
+ ></${ViraCheckbox}>
384
+ `;
385
+ }
386
+ return nothing;
387
+ }
388
+ function renderPlusButton({ isAddDisabled, tooltip, onClick, }) {
389
+ return html `
390
+ <${ViraButton.assign({
391
+ icon: lucideIcons.Plus,
392
+ color: ViraColorVariant.Plain,
393
+ isDisabled: isAddDisabled,
394
+ })}
395
+ title=${tooltip}
396
+ ${listen('click', () => {
397
+ if (isAddDisabled) {
398
+ return;
399
+ }
400
+ onClick();
401
+ })}
402
+ ></${ViraButton}>
403
+ `;
404
+ }
405
+ function renderObjectAddControl({ pathKey, allowedTypes, canAdd, onAdd, }) {
406
+ if (allowedTypes.length === 0) {
407
+ return nothing;
408
+ }
409
+ const isAddDisabled = !canAdd;
410
+ if (allowedTypes.length === 1) {
411
+ const onlyType = assertWrap.isDefined(allowedTypes[0]);
412
+ return renderPlusButton({
413
+ isAddDisabled,
414
+ tooltip: `Add ${viraJsonTypeLabels[onlyType]}`,
415
+ onClick: () => onAdd(onlyType),
416
+ });
417
+ }
418
+ const selectedType = getPendingType(pathKey, allowedTypes);
419
+ const options = allowedTypes.map((type) => {
420
+ return {
421
+ value: type,
422
+ label: viraJsonTypeLabels[type],
423
+ };
424
+ });
425
+ return html `
426
+ <${ViraSelect.assign({
427
+ options,
428
+ value: selectedType,
429
+ })}
430
+ ${listen(ViraSelect.events.valueChange, (event) => {
431
+ setPendingType(pathKey, event.detail);
432
+ })}
433
+ ></${ViraSelect}>
434
+ ${renderPlusButton({
435
+ isAddDisabled,
436
+ tooltip: `Add ${viraJsonTypeLabels[selectedType]}`,
437
+ onClick: () => onAdd(selectedType),
438
+ })}
439
+ `;
440
+ }
441
+ function renderArrayAddControl({ pathKey, allowedTypes, onAdd, }) {
442
+ if (allowedTypes.length === 0) {
443
+ return nothing;
444
+ }
445
+ else if (allowedTypes.length === 1) {
446
+ const onlyType = assertWrap.isDefined(allowedTypes[0]);
447
+ const isPrimitivePrimitive = onlyType === ViraJsonType.String ||
448
+ onlyType === ViraJsonType.Number ||
449
+ onlyType === ViraJsonType.Integer ||
450
+ onlyType === ViraJsonType.Boolean;
451
+ if (!isPrimitivePrimitive) {
452
+ return renderPlusButton({
453
+ isAddDisabled: false,
454
+ tooltip: `Add ${viraJsonTypeLabels[onlyType]}`,
455
+ onClick: () => onAdd(createDefaultForJsonType(onlyType)),
456
+ });
457
+ }
458
+ const fallback = createDefaultForJsonType(onlyType);
459
+ const pendingValue = getPendingArrayValue(pathKey, fallback);
460
+ const inlineEditor = renderInlineValueEditor(onlyType, pendingValue, (value) => setPendingArrayValue(pathKey, value));
461
+ return html `
462
+ ${inlineEditor}
463
+ ${renderPlusButton({
464
+ isAddDisabled: false,
465
+ tooltip: `Add ${viraJsonTypeLabels[onlyType]}`,
466
+ onClick: () => {
467
+ onAdd(pendingValue);
468
+ clearPending(pathKey);
469
+ },
470
+ })}
471
+ `;
472
+ }
473
+ const selectedType = getPendingType(pathKey, allowedTypes);
474
+ const options = allowedTypes.map((type) => {
475
+ return {
476
+ value: type,
477
+ label: viraJsonTypeLabels[type],
478
+ };
479
+ });
480
+ return html `
481
+ <${ViraSelect.assign({
482
+ options,
483
+ value: selectedType,
484
+ })}
485
+ ${listen(ViraSelect.events.valueChange, (event) => {
486
+ setPendingType(pathKey, event.detail);
487
+ })}
488
+ ></${ViraSelect}>
489
+ ${renderPlusButton({
490
+ isAddDisabled: false,
491
+ tooltip: `Add ${viraJsonTypeLabels[selectedType]}`,
492
+ onClick: () => onAdd(createDefaultForJsonType(selectedType)),
493
+ })}
494
+ `;
495
+ }
496
+ function renderObjectGroup(path, value, schema, onDelete) {
497
+ const pathKey = pathToKey(path);
498
+ const requiredKeys = new Set(getRequiredProperties(schema, resolveContext));
499
+ const definedProperties = getDefinedProperties(schema, resolveContext);
500
+ const definedKeys = new Set(Object.keys(definedProperties));
501
+ const presentKeys = Object.keys(value);
502
+ const additional = getAdditionalPropertiesSchema(schema, resolveContext);
503
+ const rowTemplates = presentKeys.map((key) => {
504
+ const childSchema = getPropertySchema(schema, key, resolveContext);
505
+ const isRequired = requiredKeys.has(key);
506
+ const isSchemaDefined = definedKeys.has(key);
507
+ const canDelete = !isDisabled && !isRequired;
508
+ const childValue = value[key] ?? null;
509
+ const childType = getJsonType(childValue);
510
+ const isChildNested = childType === ViraJsonType.Object || childType === ViraJsonType.Array;
511
+ const childPath = [
512
+ ...path,
513
+ key,
514
+ ];
515
+ const childOnDelete = canDelete
516
+ ? () => {
517
+ emitDeleteAt(childPath);
518
+ if (!isSchemaDefined) {
519
+ clearPending(pathKey);
520
+ }
521
+ }
522
+ : undefined;
523
+ return html `
524
+ <div
525
+ class="json-row ${isChildNested ? 'json-row-nested' : 'json-row-primitive'}"
526
+ >
527
+ <span class="json-row-label">${key}${isRequired ? '*' : ''}</span>
528
+ <span class="json-row-editor">
529
+ ${renderValue(childPath, childValue, childSchema, isChildNested ? childOnDelete : undefined)}
530
+ </span>
531
+ <span class="json-row-delete">
532
+ ${!isChildNested && childOnDelete
533
+ ? renderDeleteButton(childOnDelete)
534
+ : nothing}
535
+ </span>
536
+ </div>
537
+ `;
538
+ });
539
+ const missingDefinedKeys = [...definedKeys].filter((key) => !(key in value));
540
+ const suggestedKeyButtons = isDisabled
541
+ ? []
542
+ : missingDefinedKeys.map((key) => {
543
+ const childSchema = definedProperties[key];
544
+ const childAllowed = getAllowedJsonTypes(childSchema, resolveContext);
545
+ const addType = childAllowed[0] ?? ViraJsonType.String;
546
+ return html `
547
+ <${ViraButton.assign({
548
+ text: `"${key}"`,
549
+ icon: lucideIcons.Plus,
550
+ color: ViraColorVariant.Plain,
551
+ })}
552
+ ${listen('click', () => {
553
+ emitReplaceAt([
554
+ ...path,
555
+ key,
556
+ ], createDefaultForJsonType(addType));
557
+ })}
558
+ ></${ViraButton}>
559
+ `;
560
+ });
561
+ const pendingKey = state.pendingKeys[pathKey] ?? '';
562
+ const trimmedPendingKey = pendingKey.trim();
563
+ const canAddArbitraryField = !!trimmedPendingKey && !(trimmedPendingKey in value);
564
+ const additionalAllowedTypes = additional.allowed
565
+ ? getAllowedJsonTypes(additional.schema, resolveContext)
566
+ : [];
567
+ const arbitraryAddRow = additional.allowed && !isDisabled
568
+ ? html `
569
+ <div class="json-add-row">
570
+ <${ViraInput.assign({
571
+ value: pendingKey,
572
+ placeholder: 'new field name',
573
+ })}
574
+ ${listen(ViraInput.events.valueChange, (event) => {
575
+ setPendingKey(pathKey, event.detail);
576
+ })}
577
+ ></${ViraInput}>
578
+ ${renderObjectAddControl({
579
+ pathKey,
580
+ allowedTypes: additionalAllowedTypes,
581
+ canAdd: canAddArbitraryField,
582
+ onAdd: (type) => {
583
+ if (!canAddArbitraryField) {
584
+ return;
585
+ }
586
+ emitReplaceAt([
587
+ ...path,
588
+ trimmedPendingKey,
589
+ ], createDefaultForJsonType(type));
590
+ clearPending(pathKey);
591
+ },
592
+ })}
593
+ </div>
594
+ `
595
+ : nothing;
596
+ const title = getSchemaTitle(schema, resolveContext) || 'object';
597
+ return html `
598
+ <div class="json-group">
599
+ <div class="json-group-header">
600
+ <span class="json-group-header-title">${title}</span>
601
+ ${onDelete ? renderDeleteButton(onDelete) : nothing}
602
+ </div>
603
+ ${rowTemplates.length === 0 && suggestedKeyButtons.length === 0
604
+ ? html `
605
+ <span class="json-empty-note">(empty object)</span>
606
+ `
607
+ : nothing}
608
+ ${rowTemplates}
609
+ ${suggestedKeyButtons.length > 0
610
+ ? html `
611
+ <div class="json-add-row">${suggestedKeyButtons}</div>
612
+ `
613
+ : nothing}
614
+ ${arbitraryAddRow}
615
+ </div>
616
+ `;
617
+ }
618
+ function renderArrayGroup(path, value, schema, onDelete) {
619
+ const pathKey = pathToKey(path);
620
+ const newItemSchema = getNewItemSchema(schema, value.length, resolveContext);
621
+ const allowedItemTypes = getAllowedJsonTypes(newItemSchema, resolveContext);
622
+ const rowTemplates = value.map((item, index) => {
623
+ const childSchema = getItemSchema(schema, index, resolveContext);
624
+ const childType = getJsonType(item);
625
+ const isChildNested = childType === ViraJsonType.Object || childType === ViraJsonType.Array;
626
+ const childPath = [
627
+ ...path,
628
+ index,
629
+ ];
630
+ const childOnDelete = isDisabled
631
+ ? undefined
632
+ : () => {
633
+ emitDeleteAt(childPath);
634
+ };
635
+ return html `
636
+ <div
637
+ class="json-row ${isChildNested ? 'json-row-nested' : 'json-row-primitive'}"
638
+ >
639
+ <span class="json-row-label">[${index}]</span>
640
+ <span class="json-row-editor">
641
+ ${renderValue(childPath, item, childSchema, isChildNested ? childOnDelete : undefined)}
642
+ </span>
643
+ <span class="json-row-delete">
644
+ ${!isChildNested && childOnDelete
645
+ ? renderDeleteButton(childOnDelete)
646
+ : nothing}
647
+ </span>
648
+ </div>
649
+ `;
650
+ });
651
+ const title = getSchemaTitle(schema, resolveContext) || 'array';
652
+ const addRow = isDisabled
653
+ ? nothing
654
+ : html `
655
+ <div class="json-add-row">
656
+ ${renderArrayAddControl({
657
+ pathKey,
658
+ allowedTypes: allowedItemTypes,
659
+ onAdd: (newValue) => {
660
+ emitReplaceAt([
661
+ ...path,
662
+ value.length,
663
+ ], newValue);
664
+ },
665
+ })}
666
+ </div>
667
+ `;
668
+ return html `
669
+ <div class="json-group">
670
+ <div class="json-group-header">
671
+ <span class="json-group-header-title">${title}</span>
672
+ ${onDelete ? renderDeleteButton(onDelete) : nothing}
673
+ </div>
674
+ ${rowTemplates.length === 0
675
+ ? html `
676
+ <span class="json-empty-note">(empty array)</span>
677
+ `
678
+ : nothing}
679
+ ${rowTemplates} ${addRow}
680
+ </div>
681
+ `;
682
+ }
683
+ function renderValue(path, value, schema, onDelete) {
684
+ const concreteType = getJsonType(value);
685
+ const allowedTypes = getAllowedJsonTypes(schema, resolveContext);
686
+ const narrowedSchema = pickBranchForType(schema, concreteType, resolveContext);
687
+ if (check.isArray(value)) {
688
+ return renderArrayGroup(path, value, narrowedSchema, onDelete);
689
+ }
690
+ else if (check.isObject(value)) {
691
+ return renderObjectGroup(path, value, narrowedSchema, onDelete);
692
+ }
693
+ const editor = renderPrimitive(path, value, narrowedSchema);
694
+ const showSwitcher = !isDisabled &&
695
+ allowedTypes.length > 1 &&
696
+ !allowedTypes.includes(ViraJsonType.Object) &&
697
+ !allowedTypes.includes(ViraJsonType.Array);
698
+ if (!showSwitcher) {
699
+ return editor;
700
+ }
701
+ const switcherSelected = allowedTypes.includes(concreteType)
702
+ ? concreteType
703
+ : (allowedTypes[0] ?? concreteType);
704
+ const switcherOptions = allowedTypes.map((type) => {
705
+ return {
706
+ value: type,
707
+ label: viraJsonTypeLabels[type],
708
+ };
709
+ });
710
+ return html `
711
+ <div class="json-value-with-switcher">
712
+ <span class="json-value-editor-slot">${editor}</span>
713
+ <${ViraSelect.assign({
714
+ options: switcherOptions,
715
+ value: switcherSelected,
716
+ })}
717
+ title="Change type"
718
+ ${listen(ViraSelect.events.valueChange, (event) => {
719
+ const newType = event.detail;
720
+ emitReplaceAt(path, createDefaultForJsonType(newType));
721
+ })}
722
+ ></${ViraSelect}>
723
+ </div>
724
+ `;
725
+ }
726
+ const toolbarTemplate = html `
727
+ <div class="json-toolbar">
728
+ <${ViraButton.assign({
729
+ text: state.showRaw ? 'Rich' : 'Raw',
730
+ buttonEmphasis: ViraEmphasis.Subtle,
731
+ color: ViraColorVariant.Neutral,
732
+ buttonSize: ViraSize.Small,
733
+ })}
734
+ title=${state.showRaw ? 'Show rich editor' : 'Show raw JSON'}
735
+ ${listen('click', () => {
736
+ updateState({
737
+ showRaw: !state.showRaw,
738
+ });
739
+ })}
740
+ ></${ViraButton}>
741
+ </div>
742
+ `;
743
+ if (state.showRaw) {
744
+ return html `
745
+ ${toolbarTemplate}
746
+ <pre class="json-raw-pre">${JSON.stringify(inputs.value, undefined, 4)}</pre>
747
+ `;
748
+ }
749
+ const validationErrors = validateAgainstSchema(inputs.value, inputs.schema);
750
+ if (validationErrors.length > 0) {
751
+ return html `
752
+ ${toolbarTemplate}
753
+ <${ViraError}>
754
+ <div class="json-validation-errors">
755
+ <div>Value does not match schema:</div>
756
+ <ul>
757
+ ${validationErrors.map((errorMessage) => html `
758
+ <li>${errorMessage}</li>
759
+ `)}
760
+ </ul>
761
+ </div>
762
+ </${ViraError}>
763
+ `;
764
+ }
765
+ return html `
766
+ ${toolbarTemplate} ${renderValue([], inputs.value, inputs.schema, undefined)}
767
+ `;
768
+ },
769
+ });