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.
- package/dist/elements/index.d.ts +1 -0
- package/dist/elements/index.js +1 -0
- package/dist/elements/vira-json-form.element.d.ts +22 -0
- package/dist/elements/vira-json-form.element.js +769 -0
- package/dist/elements/vira-tabs.element.d.ts +6 -0
- package/dist/elements/vira-tabs.element.js +9 -3
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/vira-json-schema.d.ts +185 -0
- package/dist/util/vira-json-schema.js +625 -0
- package/package.json +2 -1
|
@@ -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
|
+
});
|