vira
Version:
A simple and highly versatile design system using element-vir.
976 lines (950 loc) • 39.3 kB
JavaScript
import { assertWrap, check } from '@augment-vir/assert';
import { omitObjectKeys, wrapInTry, } from '@augment-vir/common';
import { css, defineElementEvent, html, listen, nothing } from 'element-vir';
import { Plus16Icon, X16Icon } from '../icons/index.js';
import { viraFontCssVars } from '../styles/font.js';
import { viraFormCssVars } from '../styles/form-styles.js';
import { ViraColorVariant, ViraEmphasis, ViraSize } from '../styles/form-variants.js';
import { defineViraElement } from '../util/define-vira-element.js';
import { allowsFreeformString, createDefaultForJsonType, createResolveContext, deleteValueAtPath, getAdditionalPropertiesSchema, getAllowedJsonTypes, getDefinedProperties, getItemSchema, getJsonType, getNewItemSchema, getPropertySchema, getRequiredProperties, getSchemaEnumValues, getSchemaTitle, getStringEnumValues, pathToKey, pickBranchForType, setValueAtPath, validateAgainstSchema, ViraJsonType, viraJsonTypeLabels, } from '../util/vira-json-schema.js';
import { ViraButton } from './vira-button.element.js';
import { ViraCheckbox } from './vira-checkbox.element.js';
import { ViraError } from './vira-error.element.js';
import { ViraInput, ViraInputType } from './vira-input.element.js';
import { ViraSelect } from './vira-select.element.js';
import { ViraTextArea } from './vira-text-area.element.js';
/**
* Editing mode for a string field that offers both a fixed set of enum options and free-form text.
*
* @category Internal
*/
export var ViraJsonStringMode;
(function (ViraJsonStringMode) {
/** Pick from the schema's enum options via a dropdown. */
ViraJsonStringMode["Options"] = "options";
/** Enter an arbitrary string via a text input. */
ViraJsonStringMode["Custom"] = "custom";
})(ViraJsonStringMode || (ViraJsonStringMode = {}));
/**
* An editor for arbitrary JSON values, optionally constrained by a standard JSON Schema
* ({@link ViraJsonSchema}).
*
* @category Elements
*/
export const ViraJsonForm = defineViraElement()({
tagName: 'vira-json-form',
events: {
valueChange: defineElementEvent(),
},
state() {
return {
pendingKeys: {},
pendingTypes: {},
pendingArrayValues: {},
stringModes: {},
showRaw: false,
rawDraft: undefined,
rawError: undefined,
};
},
styles: css `
:host {
display: flex;
flex-direction: column;
gap: 10px;
font-size: ${viraFormCssVars['vira-form-medium-text-size'].value};
color: ${viraFormCssVars['vira-form-foreground-color'].value};
}
.json-toolbar {
display: flex;
justify-content: flex-end;
}
${ViraTextArea}.json-raw-textarea {
width: 100%;
font-family: ${viraFontCssVars['vira-monospace'].value};
font-size: ${viraFormCssVars['vira-form-small-text-size'].value};
}
.json-validation-errors {
display: flex;
flex-direction: column;
gap: 4px;
}
.json-validation-errors ul {
margin: 0;
padding-left: 20px;
}
.json-group {
border-collapse: separate;
border-spacing: 0;
width: 100%;
box-sizing: border-box;
}
.json-group-nested {
margin-top: 4px;
padding-left: 12px;
padding-bottom: 8px;
border-left: 2px solid ${viraFormCssVars['vira-form-border-color'].value};
border-bottom: 2px solid ${viraFormCssVars['vira-form-border-color'].value};
}
.json-row-primitive > td {
padding: 4px 8px 4px 0;
vertical-align: middle;
}
.json-row-primitive > td:last-child {
padding-right: 0;
}
td.json-row-label {
text-align: left;
min-width: 80px;
white-space: nowrap;
font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
}
td.json-row-editor {
width: 100%;
}
td.json-row-editor > * {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
td.json-row-delete {
width: 24px;
text-align: center;
}
.json-row-nested > td {
padding: 4px 0;
}
.json-row-nested-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.json-row-nested-header > .json-row-label {
flex-grow: 1;
font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
word-break: break-word;
}
.json-type-tag {
font-weight: ${viraFontCssVars['vira-font-weight-normal'].value};
color: ${viraFormCssVars['vira-form-secondary-body-foreground'].value};
}
.json-value-with-switcher {
display: flex;
align-items: center;
gap: 6px;
}
.json-value-with-switcher > .json-value-editor-slot {
flex-grow: 1;
min-width: 0;
display: flex;
}
.json-value-with-switcher > .json-value-editor-slot > * {
flex-grow: 1;
min-width: 0;
}
.json-value-with-switcher > ${ViraSelect} {
flex-shrink: 0;
width: 130px;
}
.json-add-row {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
flex-wrap: wrap;
padding: 8px 0;
}
.json-add-row ${ViraInput}, .json-add-row ${ViraSelect} {
width: 160px;
}
.json-add-row .json-add-value-input {
flex-grow: 1;
width: auto;
min-width: 160px;
}
.json-null-indicator {
padding: 6px 10px;
color: ${viraFormCssVars['vira-form-placeholder-color'].value};
font-style: italic;
}
.json-empty-note {
display: block;
padding: 4px 0;
color: ${viraFormCssVars['vira-form-placeholder-color'].value};
font-style: italic;
font-size: ${viraFormCssVars['vira-form-small-text-size'].value};
}
`,
render({ inputs, state, dispatch, events, updateState, host }) {
const isDisabled = !!inputs.isDisabled;
const resolveContext = createResolveContext(inputs.schema);
function jsonPrimitiveToString(entry) {
return entry == undefined ? 'null' : String(entry);
}
function emitRoot(newRoot) {
dispatch(new events.valueChange(newRoot));
}
function emitReplaceAt(path, newValue) {
emitRoot(setValueAtPath({
root: inputs.value,
path,
newValue,
}));
}
function emitDeleteAt(path) {
emitRoot(deleteValueAtPath(inputs.value, path));
}
function getPendingType(pathKey, allowedTypes) {
const current = state.pendingTypes[pathKey];
if (current && allowedTypes.includes(current)) {
return current;
}
return allowedTypes[0] ?? ViraJsonType.String;
}
function setPendingType(pathKey, type) {
updateState({
pendingTypes: {
...state.pendingTypes,
[pathKey]: type,
},
});
}
function setPendingKey({ pathKey, key }) {
updateState({
pendingKeys: {
...state.pendingKeys,
[pathKey]: key,
},
});
}
function getPendingArrayValue(pathKey, fallback) {
const stored = state.pendingArrayValues[pathKey];
return stored === undefined ? fallback : stored;
}
function setPendingArrayValue(pathKey, value) {
updateState({
pendingArrayValues: {
...state.pendingArrayValues,
[pathKey]: value,
},
});
}
function getStringMode({ pathKey, value, enumValues, }) {
const stored = state.stringModes[pathKey];
if (stored) {
return stored;
}
return enumValues.includes(value)
? ViraJsonStringMode.Options
: ViraJsonStringMode.Custom;
}
function setStringMode(pathKey, mode) {
updateState({
stringModes: {
...state.stringModes,
[pathKey]: mode,
},
});
}
function clearPending(pathKey) {
updateState({
pendingKeys: omitObjectKeys(state.pendingKeys, [pathKey]),
pendingTypes: omitObjectKeys(state.pendingTypes, [pathKey]),
pendingArrayValues: omitObjectKeys(state.pendingArrayValues, [pathKey]),
});
}
function renderDeleteButton(onDelete) {
return html `
<${ViraButton.assign({
icon: X16Icon,
buttonEmphasis: ViraEmphasis.Subtle,
color: ViraColorVariant.Danger,
buttonSize: ViraSize.Small,
})}
title="Remove"
${listen('click', onDelete)}
></${ViraButton}>
`;
}
function renderPrimitive(path, value, schema) {
const type = getJsonType(value);
const enumValues = getSchemaEnumValues(schema, resolveContext);
if (enumValues && enumValues.length > 0) {
const options = enumValues.map((entry) => {
const asString = jsonPrimitiveToString(entry);
return {
value: asString,
label: asString,
};
});
const isPrimitive = value == undefined ||
check.isString(value) ||
check.isNumber(value) ||
check.isBoolean(value);
return html `
<${ViraSelect.assign({
options,
value: isPrimitive ? jsonPrimitiveToString(value) : undefined,
disabled: isDisabled,
})}
${listen(ViraSelect.events.valueChange, (event) => {
const selected = enumValues.find((entry) => jsonPrimitiveToString(entry) === event.detail);
emitReplaceAt(path, selected ?? event.detail);
})}
></${ViraSelect}>
`;
}
else if (type === ViraJsonType.Boolean) {
return html `
<${ViraCheckbox.assign({
value: value === true,
isDisabled,
})}
${listen(ViraCheckbox.events.valueChange, (event) => {
emitReplaceAt(path, event.detail);
})}
></${ViraCheckbox}>
`;
}
else if (type === ViraJsonType.Number || type === ViraJsonType.Integer) {
const schemaAllowedTypes = getAllowedJsonTypes(schema, resolveContext);
const isIntegerOnly = schemaAllowedTypes.length === 1 &&
schemaAllowedTypes[0] === ViraJsonType.Integer;
return html `
<${ViraInput.assign({
type: ViraInputType.Number,
value: check.isNumber(value) ? String(value) : '',
disabled: isDisabled,
allowedInputs: isIntegerOnly ? /[\d-]/ : /[\d.-]/,
})}
${listen(ViraInput.events.valueChange, (event) => {
const text = event.detail;
const parsed = text === '' ? 0 : Number(text);
const clean = Number.isFinite(parsed) ? parsed : 0;
emitReplaceAt(path, isIntegerOnly ? Math.trunc(clean) : clean);
})}
></${ViraInput}>
`;
}
else if (type === ViraJsonType.Null) {
return html `
<span class="json-null-indicator">null</span>
`;
}
else {
return html `
<${ViraInput.assign({
value: check.isString(value) ? value : '',
disabled: isDisabled,
})}
${listen(ViraInput.events.valueChange, (event) => {
emitReplaceAt(path, event.detail);
})}
></${ViraInput}>
`;
}
}
function renderInlineValueEditor(type, currentValue, onChange) {
if (type === ViraJsonType.String) {
const text = check.isString(currentValue) ? currentValue : '';
return html `
<${ViraInput.assign({
value: text,
placeholder: 'new item value',
})}
class="json-add-value-input"
${listen(ViraInput.events.valueChange, (event) => {
onChange(event.detail);
})}
></${ViraInput}>
`;
}
else if (type === ViraJsonType.Number || type === ViraJsonType.Integer) {
const isIntegerOnly = type === ViraJsonType.Integer;
const numText = check.isNumber(currentValue) ? String(currentValue) : '';
return html `
<${ViraInput.assign({
type: ViraInputType.Number,
value: numText,
placeholder: 'new item value',
allowedInputs: isIntegerOnly ? /[\d-]/ : /[\d.-]/,
})}
class="json-add-value-input"
${listen(ViraInput.events.valueChange, (event) => {
const text = event.detail;
const parsed = text === '' ? 0 : Number(text);
const clean = Number.isFinite(parsed) ? parsed : 0;
onChange(isIntegerOnly ? Math.trunc(clean) : clean);
})}
></${ViraInput}>
`;
}
else if (type === ViraJsonType.Boolean) {
return html `
<${ViraCheckbox.assign({
value: currentValue === true,
})}
${listen(ViraCheckbox.events.valueChange, (event) => {
onChange(event.detail);
})}
></${ViraCheckbox}>
`;
}
else {
return nothing;
}
}
function renderPlusButton({ isAddDisabled, tooltip, onClick, }) {
return html `
<${ViraButton.assign({
icon: Plus16Icon,
color: ViraColorVariant.Positive,
buttonSize: ViraSize.Small,
isDisabled: isAddDisabled,
})}
title=${tooltip}
${listen('click', () => {
if (isAddDisabled) {
return;
}
onClick();
})}
></${ViraButton}>
`;
}
function renderObjectAddControl({ pathKey, allowedTypes, canAdd, disabledReason, onAdd, }) {
if (allowedTypes.length === 0) {
return nothing;
}
const isAddDisabled = !canAdd;
if (allowedTypes.length === 1) {
const onlyType = assertWrap.isDefined(allowedTypes[0]);
return renderPlusButton({
isAddDisabled,
tooltip: isAddDisabled ? disabledReason : `Add ${viraJsonTypeLabels[onlyType]}`,
onClick: () => onAdd(onlyType),
});
}
const selectedType = getPendingType(pathKey, allowedTypes);
const options = allowedTypes.map((type) => {
return {
value: type,
label: viraJsonTypeLabels[type],
};
});
return html `
<${ViraSelect.assign({
options,
value: selectedType,
})}
${listen(ViraSelect.events.valueChange, (event) => {
setPendingType(pathKey, event.detail);
})}
></${ViraSelect}>
${renderPlusButton({
isAddDisabled,
tooltip: isAddDisabled
? disabledReason
: `Add ${viraJsonTypeLabels[selectedType]}`,
onClick: () => onAdd(selectedType),
})}
`;
}
function renderArrayAddControl({ pathKey, allowedTypes, onAdd, }) {
if (allowedTypes.length === 0) {
return nothing;
}
else if (allowedTypes.length === 1) {
const onlyType = assertWrap.isDefined(allowedTypes[0]);
const isPrimitivePrimitive = onlyType === ViraJsonType.String ||
onlyType === ViraJsonType.Number ||
onlyType === ViraJsonType.Integer ||
onlyType === ViraJsonType.Boolean;
if (!isPrimitivePrimitive) {
return renderPlusButton({
isAddDisabled: false,
tooltip: `Add ${viraJsonTypeLabels[onlyType]}`,
onClick: () => onAdd(createDefaultForJsonType(onlyType)),
});
}
const fallback = createDefaultForJsonType(onlyType);
const pendingValue = getPendingArrayValue(pathKey, fallback);
const inlineEditor = renderInlineValueEditor(onlyType, pendingValue, (value) => setPendingArrayValue(pathKey, value));
return html `
${inlineEditor}
${renderPlusButton({
isAddDisabled: false,
tooltip: `Add ${viraJsonTypeLabels[onlyType]}`,
onClick: () => {
onAdd(pendingValue);
clearPending(pathKey);
},
})}
`;
}
else {
const selectedType = getPendingType(pathKey, allowedTypes);
const options = allowedTypes.map((type) => {
return {
value: type,
label: viraJsonTypeLabels[type],
};
});
return html `
<${ViraSelect.assign({
options,
value: selectedType,
})}
${listen(ViraSelect.events.valueChange, (event) => {
setPendingType(pathKey, event.detail);
})}
></${ViraSelect}>
${renderPlusButton({
isAddDisabled: false,
tooltip: `Add ${viraJsonTypeLabels[selectedType]}`,
onClick: () => onAdd(createDefaultForJsonType(selectedType)),
})}
`;
}
}
function getNestedTypeLabel(schema, value) {
const concreteType = getJsonType(value);
const narrowedSchema = pickBranchForType(schema, concreteType, resolveContext);
return (getSchemaTitle(narrowedSchema, resolveContext) || viraJsonTypeLabels[concreteType]);
}
function renderObjectGroup(path, value, schema) {
const pathKey = pathToKey(path);
const isRoot = path.length === 0;
const requiredKeys = new Set(getRequiredProperties(schema, resolveContext));
const definedProperties = getDefinedProperties(schema, resolveContext);
const definedKeys = new Set(Object.keys(definedProperties));
const presentKeys = Object.keys(value);
const additional = getAdditionalPropertiesSchema(schema, resolveContext);
const rowTemplates = presentKeys.map((key) => {
const childSchema = getPropertySchema(schema, key, resolveContext);
const isRequired = requiredKeys.has(key);
const isSchemaDefined = definedKeys.has(key);
const canDelete = !isDisabled && !isRequired;
const childValue = value[key] ?? null;
const childType = getJsonType(childValue);
const isChildNested = childType === ViraJsonType.Object || childType === ViraJsonType.Array;
const childPath = [
...path,
key,
];
const childOnDelete = canDelete
? () => {
emitDeleteAt(childPath);
if (!isSchemaDefined) {
clearPending(pathKey);
}
}
: undefined;
if (isChildNested) {
return html `
<tr class="json-row-nested">
<td colspan="3">
<div class="json-row-nested-header">
<span class="json-row-label">
${key}${isRequired ? '*' : ''}
<span class="json-type-tag">
: ${getNestedTypeLabel(childSchema, childValue)}
</span>
</span>
${childOnDelete ? renderDeleteButton(childOnDelete) : nothing}
</div>
${renderValue(childPath, childValue, childSchema)}
</td>
</tr>
`;
}
return html `
<tr class="json-row-primitive">
<td class="json-row-label">${key}${isRequired ? '*' : ''}</td>
<td class="json-row-editor">
${renderValue(childPath, childValue, childSchema)}
</td>
<td class="json-row-delete">
${childOnDelete ? renderDeleteButton(childOnDelete) : nothing}
</td>
</tr>
`;
});
const missingDefinedKeys = [...definedKeys].filter((key) => !(key in value));
const suggestedKeyButtons = isDisabled
? []
: missingDefinedKeys.map((key) => {
const childSchema = definedProperties[key];
const childAllowed = getAllowedJsonTypes(childSchema, resolveContext);
const addType = childAllowed[0] ?? ViraJsonType.String;
return html `
<${ViraButton.assign({
text: `"${key}"`,
icon: Plus16Icon,
color: ViraColorVariant.Positive,
buttonSize: ViraSize.Small,
})}
${listen('click', () => {
emitReplaceAt([
...path,
key,
], createDefaultForJsonType(addType));
})}
></${ViraButton}>
`;
});
const pendingKey = state.pendingKeys[pathKey] ?? '';
const trimmedPendingKey = pendingKey.trim();
const canAddArbitraryField = !!trimmedPendingKey && !(trimmedPendingKey in value);
const additionalAllowedTypes = additional.allowed
? getAllowedJsonTypes(additional.schema, resolveContext)
: [];
const arbitraryAddDisabledReason = trimmedPendingKey
? `Field "${trimmedPendingKey}" already exists.`
: 'Enter a field name to add.';
const arbitraryAddRow = additional.allowed && !isDisabled
? html `
<tr>
<td colspan="3">
<div class="json-add-row">
<${ViraInput.assign({
value: pendingKey,
placeholder: 'new field name',
})}
${listen(ViraInput.events.valueChange, (event) => {
setPendingKey({
pathKey,
key: event.detail,
});
})}
></${ViraInput}>
${renderObjectAddControl({
pathKey,
allowedTypes: additionalAllowedTypes,
canAdd: canAddArbitraryField,
disabledReason: arbitraryAddDisabledReason,
onAdd: (type) => {
if (!canAddArbitraryField) {
return;
}
emitReplaceAt([
...path,
trimmedPendingKey,
], createDefaultForJsonType(type));
clearPending(pathKey);
},
})}
</div>
</td>
</tr>
`
: nothing;
return html `
<table class="json-group ${isRoot ? '' : 'json-group-nested'}">
<tbody>
${rowTemplates.length === 0 && suggestedKeyButtons.length === 0
? html `
<tr>
<td colspan="3">
<span class="json-empty-note">(empty object)</span>
</td>
</tr>
`
: nothing}
${rowTemplates}
${suggestedKeyButtons.length > 0
? html `
<tr>
<td colspan="3">
<div class="json-add-row">${suggestedKeyButtons}</div>
</td>
</tr>
`
: nothing}
${arbitraryAddRow}
</tbody>
</table>
`;
}
function renderArrayGroup(path, value, schema) {
const pathKey = pathToKey(path);
const isRoot = path.length === 0;
const newItemSchema = getNewItemSchema(schema, value.length, resolveContext);
const allowedItemTypes = getAllowedJsonTypes(newItemSchema, resolveContext);
const rowTemplates = value.map((item, index) => {
const childSchema = getItemSchema(schema, index, resolveContext);
const childType = getJsonType(item);
const isChildNested = childType === ViraJsonType.Object || childType === ViraJsonType.Array;
const childPath = [
...path,
index,
];
const childOnDelete = isDisabled
? undefined
: () => {
emitDeleteAt(childPath);
};
if (isChildNested) {
return html `
<tr class="json-row-nested">
<td colspan="3">
<div class="json-row-nested-header">
<span class="json-row-label">
[${index}]
<span class="json-type-tag">
: ${getNestedTypeLabel(childSchema, item)}
</span>
</span>
${childOnDelete ? renderDeleteButton(childOnDelete) : nothing}
</div>
${renderValue(childPath, item, childSchema)}
</td>
</tr>
`;
}
return html `
<tr class="json-row-primitive">
<td class="json-row-label">[${index}]</td>
<td class="json-row-editor">
${renderValue(childPath, item, childSchema)}
</td>
<td class="json-row-delete">
${childOnDelete ? renderDeleteButton(childOnDelete) : nothing}
</td>
</tr>
`;
});
const addRow = isDisabled
? nothing
: html `
<tr>
<td colspan="3">
<div class="json-add-row">
${renderArrayAddControl({
pathKey,
allowedTypes: allowedItemTypes,
onAdd: (newValue) => {
emitReplaceAt([
...path,
value.length,
], newValue);
},
})}
</div>
</td>
</tr>
`;
return html `
<table class="json-group ${isRoot ? '' : 'json-group-nested'}">
<tbody>
${rowTemplates.length === 0
? html `
<tr>
<td colspan="3">
<span class="json-empty-note">(empty array)</span>
</td>
</tr>
`
: nothing}
${rowTemplates} ${addRow}
</tbody>
</table>
`;
}
function renderStringEnumOrRaw(path, value, enumValues) {
const pathKey = pathToKey(path);
const mode = getStringMode({
pathKey,
value,
enumValues,
});
const editor = mode === ViraJsonStringMode.Options
? html `
<${ViraSelect.assign({
options: enumValues.map((entry) => {
return {
value: entry,
label: entry,
};
}),
value: enumValues.includes(value) ? value : undefined,
disabled: isDisabled,
})}
${listen(ViraSelect.events.valueChange, (event) => {
emitReplaceAt(path, event.detail);
})}
></${ViraSelect}>
`
: html `
<${ViraInput.assign({
value,
disabled: isDisabled,
})}
${listen(ViraInput.events.valueChange, (event) => {
emitReplaceAt(path, event.detail);
})}
></${ViraInput}>
`;
if (isDisabled) {
return editor;
}
const modeOptions = [
{
value: ViraJsonStringMode.Options,
label: 'Options',
},
{
value: ViraJsonStringMode.Custom,
label: 'Custom',
},
];
return html `
<div class="json-value-with-switcher">
<span class="json-value-editor-slot">${editor}</span>
<${ViraSelect.assign({
options: modeOptions,
value: mode,
})}
title="Choose from options or enter a custom value"
${listen(ViraSelect.events.valueChange, (event) => {
setStringMode(pathKey, event.detail);
})}
></${ViraSelect}>
</div>
`;
}
function renderValue(path, value, schema) {
const concreteType = getJsonType(value);
const allowedTypes = getAllowedJsonTypes(schema, resolveContext);
const narrowedSchema = pickBranchForType(schema, concreteType, resolveContext);
if (check.isArray(value)) {
return renderArrayGroup(path, value, narrowedSchema);
}
else if (check.isObject(value)) {
return renderObjectGroup(path, value, narrowedSchema);
}
/**
* When a string field constrains values to an enum but also permits free-form strings,
* offer both: an options dropdown plus a switcher to toggle to raw text entry.
*/
const isStringOnlyField = allowedTypes.length === 1 && allowedTypes[0] === ViraJsonType.String;
const stringEnumValues = isStringOnlyField
? getStringEnumValues(schema, resolveContext)
: [];
if (check.isString(value) &&
stringEnumValues.length > 0 &&
allowsFreeformString(schema, resolveContext)) {
return renderStringEnumOrRaw(path, value, stringEnumValues);
}
const editor = renderPrimitive(path, value, narrowedSchema);
const showSwitcher = !isDisabled &&
allowedTypes.length > 1 &&
!allowedTypes.includes(ViraJsonType.Object) &&
!allowedTypes.includes(ViraJsonType.Array);
if (!showSwitcher) {
return editor;
}
const switcherSelected = allowedTypes.includes(concreteType)
? concreteType
: (allowedTypes[0] ?? concreteType);
const switcherOptions = allowedTypes.map((type) => {
return {
value: type,
label: viraJsonTypeLabels[type],
};
});
return html `
<div class="json-value-with-switcher">
<span class="json-value-editor-slot">${editor}</span>
<${ViraSelect.assign({
options: switcherOptions,
value: switcherSelected,
})}
title="Change type"
${listen(ViraSelect.events.valueChange, (event) => {
const newType = event.detail;
emitReplaceAt(path, createDefaultForJsonType(newType));
})}
></${ViraSelect}>
</div>
`;
}
const toolbarTemplate = html `
<div class="json-toolbar">
<${ViraButton.assign({
text: state.showRaw ? 'Rich' : 'Raw',
buttonEmphasis: ViraEmphasis.Subtle,
color: ViraColorVariant.Neutral,
buttonSize: ViraSize.Small,
})}
title=${state.showRaw ? 'Show rich editor' : 'Show raw JSON'}
${listen('click', () => {
const goingToRaw = !state.showRaw;
host.style.minWidth = goingToRaw ? `${host.offsetWidth}px` : '';
updateState({
showRaw: goingToRaw,
rawDraft: undefined,
rawError: undefined,
});
})}
></${ViraButton}>
</div>
`;
if (state.showRaw) {
const rawText = state.rawDraft ?? JSON.stringify(inputs.value, undefined, 4);
return html `
${toolbarTemplate}
<${ViraTextArea.assign({
value: rawText,
disabled: isDisabled,
disableBrowserHelps: true,
rows: 12,
})}
class="json-raw-textarea"
${listen(ViraTextArea.events.valueChange, (event) => {
const text = event.detail;
const parsed = wrapInTry(() => JSON.parse(text));
if (parsed instanceof Error) {
updateState({
rawDraft: text,
rawError: parsed.message,
});
}
else {
updateState({
rawDraft: text,
rawError: undefined,
});
emitRoot(parsed);
}
})}
></${ViraTextArea}>
${state.rawError
? html `
<${ViraError}>${state.rawError}</${ViraError}>
`
: nothing}
`;
}
const validationErrors = validateAgainstSchema(inputs.value, inputs.schema);
if (validationErrors.length > 0) {
return html `
${toolbarTemplate}
<${ViraError}>
<div class="json-validation-errors">
<div>Value does not match schema:</div>
<ul>
${validationErrors.map((errorMessage) => html `
<li>${errorMessage}</li>
`)}
</ul>
</div>
</${ViraError}>
`;
}
return html `
${toolbarTemplate} ${renderValue([], inputs.value, inputs.schema)}
`;
},
});