UNPKG

@rjsf/core

Version:

A simple React component capable of building HTML forms out of a JSON schema.

1,342 lines (1,341 loc) 137 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('@rjsf/utils'), require('lodash/forEach'), require('lodash/get'), require('lodash/isEmpty'), require('lodash/isNil'), require('lodash/pick'), require('lodash/toPath'), require('lodash/cloneDeep'), require('lodash/isObject'), require('lodash/set'), require('lodash/uniqueId'), require('react/jsx-runtime'), require('lodash/omit'), require('markdown-to-jsx'), require('lodash/has'), require('lodash/unset')) : typeof define === 'function' && define.amd ? define(['exports', 'react', '@rjsf/utils', 'lodash/forEach', 'lodash/get', 'lodash/isEmpty', 'lodash/isNil', 'lodash/pick', 'lodash/toPath', 'lodash/cloneDeep', 'lodash/isObject', 'lodash/set', 'lodash/uniqueId', 'react/jsx-runtime', 'lodash/omit', 'markdown-to-jsx', 'lodash/has', 'lodash/unset'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JSONSchemaForm = {}, global.react, global.utils, global._forEach, global.get3, global.isEmpty, global._isNil, global._pick, global._toPath, global.cloneDeep, global.isObject, global.set, global.uniqueId, global.jsxRuntime, global.omit2, global.Markdown, global.has, global.unset)); })(this, (function (exports, react, utils, _forEach, get3, isEmpty, _isNil, _pick, _toPath, cloneDeep, isObject, set, uniqueId, jsxRuntime, omit2, Markdown, has, unset) { 'use strict'; // src/components/Form.tsx function generateRowId() { return uniqueId("rjsf-array-item-"); } function generateKeyedFormData(formData) { return !Array.isArray(formData) ? [] : formData.map((item) => { return { key: generateRowId(), item }; }); } function keyedToPlainFormData(keyedFormData) { if (Array.isArray(keyedFormData)) { return keyedFormData.map((keyedItem) => keyedItem.item); } return []; } var ArrayField = class extends react.Component { /** Constructs an `ArrayField` from the `props`, generating the initial keyed data from the `formData` * * @param props - The `FieldProps` for this template */ constructor(props) { super(props); const { formData = [] } = props; const keyedFormData = generateKeyedFormData(formData); this.state = { keyedFormData, updatedKeyedFormData: false }; } /** React lifecycle method that is called when the props are about to change allowing the state to be updated. It * regenerates the keyed form data and returns it * * @param nextProps - The next set of props data * @param prevState - The previous set of state data */ static getDerivedStateFromProps(nextProps, prevState) { if (prevState.updatedKeyedFormData) { return { updatedKeyedFormData: false }; } const nextFormData = Array.isArray(nextProps.formData) ? nextProps.formData : []; const previousKeyedFormData = prevState.keyedFormData || []; const newKeyedFormData = nextFormData.length === previousKeyedFormData.length ? previousKeyedFormData.map((previousKeyedFormDatum, index) => { return { key: previousKeyedFormDatum.key, item: nextFormData[index] }; }) : generateKeyedFormData(nextFormData); return { keyedFormData: newKeyedFormData }; } /** Returns the appropriate title for an item by getting first the title from the schema.items, then falling back to * the description from the schema.items, and finally the string "Item" */ get itemTitle() { const { schema, registry } = this.props; const { translateString } = registry; return get3( schema, [utils.ITEMS_KEY, "title"], get3(schema, [utils.ITEMS_KEY, "description"], translateString(utils.TranslatableString.ArrayItemTitle)) ); } /** Determines whether the item described in the schema is always required, which is determined by whether any item * may be null. * * @param itemSchema - The schema for the item * @return - True if the item schema type does not contain the "null" type */ isItemRequired(itemSchema) { if (Array.isArray(itemSchema.type)) { return !itemSchema.type.includes("null"); } return itemSchema.type !== "null"; } /** Determines whether more items can be added to the array. If the uiSchema indicates the array doesn't allow adding * then false is returned. Otherwise, if the schema indicates that there are a maximum number of items and the * `formData` matches that value, then false is returned, otherwise true is returned. * * @param formItems - The list of items in the form * @returns - True if the item is addable otherwise false */ canAddItem(formItems) { const { schema, uiSchema, registry } = this.props; let { addable } = utils.getUiOptions(uiSchema, registry.globalUiOptions); if (addable !== false) { if (schema.maxItems !== void 0) { addable = formItems.length < schema.maxItems; } else { addable = true; } } return addable; } /** Returns the default form information for an item based on the schema for that item. Deals with the possibility * that the schema is fixed and allows additional items. */ _getNewFormDataRow = () => { const { schema, registry } = this.props; const { schemaUtils } = registry; let itemSchema = schema.items; if (utils.isFixedItems(schema) && utils.allowAdditionalItems(schema)) { itemSchema = schema.additionalItems; } return schemaUtils.getDefaultFormState(itemSchema); }; /** Callback handler for when the user clicks on the add or add at index buttons. Creates a new row of keyed form data * either at the end of the list (when index is not specified) or inserted at the `index` when it is, adding it into * the state, and then returning `onChange()` with the plain form data converted from the keyed data * * @param event - The event for the click * @param [index] - The optional index at which to add the new data */ _handleAddClick(event, index) { if (event) { event.preventDefault(); } const { onChange, errorSchema } = this.props; const { keyedFormData } = this.state; let newErrorSchema; if (errorSchema) { newErrorSchema = {}; for (const idx in errorSchema) { const i = parseInt(idx); if (index === void 0 || i < index) { set(newErrorSchema, [i], errorSchema[idx]); } else if (i >= index) { set(newErrorSchema, [i + 1], errorSchema[idx]); } } } const newKeyedFormDataRow = { key: generateRowId(), item: this._getNewFormDataRow() }; const newKeyedFormData = [...keyedFormData]; if (index !== void 0) { newKeyedFormData.splice(index, 0, newKeyedFormDataRow); } else { newKeyedFormData.push(newKeyedFormDataRow); } this.setState( { keyedFormData: newKeyedFormData, updatedKeyedFormData: true }, () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema) ); } /** Callback handler for when the user clicks on the add button. Creates a new row of keyed form data at the end of * the list, adding it into the state, and then returning `onChange()` with the plain form data converted from the * keyed data * * @param event - The event for the click */ onAddClick = (event) => { this._handleAddClick(event); }; /** Callback handler for when the user clicks on the add button on an existing array element. Creates a new row of * keyed form data inserted at the `index`, adding it into the state, and then returning `onChange()` with the plain * form data converted from the keyed data * * @param index - The index at which the add button is clicked */ onAddIndexClick = (index) => { return (event) => { this._handleAddClick(event, index); }; }; /** Callback handler for when the user clicks on the copy button on an existing array element. Clones the row of * keyed form data at the `index` into the next position in the state, and then returning `onChange()` with the plain * form data converted from the keyed data * * @param index - The index at which the copy button is clicked */ onCopyIndexClick = (index) => { return (event) => { if (event) { event.preventDefault(); } const { onChange, errorSchema } = this.props; const { keyedFormData } = this.state; let newErrorSchema; if (errorSchema) { newErrorSchema = {}; for (const idx in errorSchema) { const i = parseInt(idx); if (i <= index) { set(newErrorSchema, [i], errorSchema[idx]); } else if (i > index) { set(newErrorSchema, [i + 1], errorSchema[idx]); } } } const newKeyedFormDataRow = { key: generateRowId(), item: cloneDeep(keyedFormData[index].item) }; const newKeyedFormData = [...keyedFormData]; if (index !== void 0) { newKeyedFormData.splice(index + 1, 0, newKeyedFormDataRow); } else { newKeyedFormData.push(newKeyedFormDataRow); } this.setState( { keyedFormData: newKeyedFormData, updatedKeyedFormData: true }, () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema) ); }; }; /** Callback handler for when the user clicks on the remove button on an existing array element. Removes the row of * keyed form data at the `index` in the state, and then returning `onChange()` with the plain form data converted * from the keyed data * * @param index - The index at which the remove button is clicked */ onDropIndexClick = (index) => { return (event) => { if (event) { event.preventDefault(); } const { onChange, errorSchema } = this.props; const { keyedFormData } = this.state; let newErrorSchema; if (errorSchema) { newErrorSchema = {}; for (const idx in errorSchema) { const i = parseInt(idx); if (i < index) { set(newErrorSchema, [i], errorSchema[idx]); } else if (i > index) { set(newErrorSchema, [i - 1], errorSchema[idx]); } } } const newKeyedFormData = keyedFormData.filter((_, i) => i !== index); this.setState( { keyedFormData: newKeyedFormData, updatedKeyedFormData: true }, () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema) ); }; }; /** Callback handler for when the user clicks on one of the move item buttons on an existing array element. Moves the * row of keyed form data at the `index` to the `newIndex` in the state, and then returning `onChange()` with the * plain form data converted from the keyed data * * @param index - The index of the item to move * @param newIndex - The index to where the item is to be moved */ onReorderClick = (index, newIndex) => { return (event) => { if (event) { event.preventDefault(); event.currentTarget.blur(); } const { onChange, errorSchema } = this.props; let newErrorSchema; if (errorSchema) { newErrorSchema = {}; for (const idx in errorSchema) { const i = parseInt(idx); if (i == index) { set(newErrorSchema, [newIndex], errorSchema[index]); } else if (i == newIndex) { set(newErrorSchema, [index], errorSchema[newIndex]); } else { set(newErrorSchema, [idx], errorSchema[i]); } } } const { keyedFormData } = this.state; function reOrderArray() { const _newKeyedFormData = keyedFormData.slice(); _newKeyedFormData.splice(index, 1); _newKeyedFormData.splice(newIndex, 0, keyedFormData[index]); return _newKeyedFormData; } const newKeyedFormData = reOrderArray(); this.setState( { keyedFormData: newKeyedFormData }, () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema) ); }; }; /** Callback handler used to deal with changing the value of the data in the array at the `index`. Calls the * `onChange` callback with the updated form data * * @param index - The index of the item being changed */ onChangeForIndex = (index) => { return (value, newErrorSchema, id) => { const { formData, onChange, errorSchema } = this.props; const arrayData = Array.isArray(formData) ? formData : []; const newFormData = arrayData.map((item, i) => { const jsonValue = typeof value === "undefined" ? null : value; return index === i ? jsonValue : item; }); onChange( newFormData, errorSchema && errorSchema && { ...errorSchema, [index]: newErrorSchema }, id ); }; }; /** Callback handler used to change the value for a checkbox */ onSelectChange = (value) => { const { onChange, idSchema } = this.props; onChange(value, void 0, idSchema && idSchema.$id); }; /** Renders the `ArrayField` depending on the specific needs of the schema and uischema elements */ render() { const { schema, uiSchema, idSchema, registry } = this.props; const { schemaUtils, translateString } = registry; if (!(utils.ITEMS_KEY in schema)) { const uiOptions = utils.getUiOptions(uiSchema); const UnsupportedFieldTemplate = utils.getTemplate( "UnsupportedFieldTemplate", registry, uiOptions ); return /* @__PURE__ */ jsxRuntime.jsx( UnsupportedFieldTemplate, { schema, idSchema, reason: translateString(utils.TranslatableString.MissingItems), registry } ); } if (schemaUtils.isMultiSelect(schema)) { return this.renderMultiSelect(); } if (utils.isCustomWidget(uiSchema)) { return this.renderCustomWidget(); } if (utils.isFixedItems(schema)) { return this.renderFixedArray(); } if (schemaUtils.isFilesArray(schema, uiSchema)) { return this.renderFiles(); } return this.renderNormalArray(); } /** Renders a normal array without any limitations of length */ renderNormalArray() { const { schema, uiSchema = {}, errorSchema, idSchema, name, title, disabled = false, readonly = false, autofocus = false, required = false, registry, onBlur, onFocus, idPrefix, idSeparator = "_", rawErrors } = this.props; const { keyedFormData } = this.state; const fieldTitle = schema.title || title || name; const { schemaUtils, formContext } = registry; const uiOptions = utils.getUiOptions(uiSchema); const _schemaItems = isObject(schema.items) ? schema.items : {}; const itemsSchema = schemaUtils.retrieveSchema(_schemaItems); const formData = keyedToPlainFormData(this.state.keyedFormData); const canAdd = this.canAddItem(formData); const arrayProps = { canAdd, items: keyedFormData.map((keyedItem, index) => { const { key, item } = keyedItem; const itemCast = item; const itemSchema = schemaUtils.retrieveSchema(_schemaItems, itemCast); const itemErrorSchema = errorSchema ? errorSchema[index] : void 0; const itemIdPrefix = idSchema.$id + idSeparator + index; const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator); return this.renderArrayFieldItem({ key, index, name: name && `${name}-${index}`, title: fieldTitle ? `${fieldTitle}-${index + 1}` : void 0, canAdd, canMoveUp: index > 0, canMoveDown: index < formData.length - 1, itemSchema, itemIdSchema, itemErrorSchema, itemData: itemCast, itemUiSchema: uiSchema.items, autofocus: autofocus && index === 0, onBlur, onFocus, rawErrors, totalItems: keyedFormData.length }); }), className: `field field-array field-array-of-${itemsSchema.type}`, disabled, idSchema, uiSchema, onAddClick: this.onAddClick, readonly, required, schema, title: fieldTitle, formContext, formData, rawErrors, registry }; const Template = utils.getTemplate("ArrayFieldTemplate", registry, uiOptions); return /* @__PURE__ */ jsxRuntime.jsx(Template, { ...arrayProps }); } /** Renders an array using the custom widget provided by the user in the `uiSchema` */ renderCustomWidget() { const { schema, idSchema, uiSchema, disabled = false, readonly = false, autofocus = false, required = false, hideError, placeholder, onBlur, onFocus, formData: items = [], registry, rawErrors, name } = this.props; const { widgets: widgets2, formContext, globalUiOptions, schemaUtils } = registry; const { widget, title: uiTitle, ...options } = utils.getUiOptions(uiSchema, globalUiOptions); const Widget = utils.getWidget(schema, widget, widgets2); const label = uiTitle ?? schema.title ?? name; const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); return /* @__PURE__ */ jsxRuntime.jsx( Widget, { id: idSchema.$id, name, multiple: true, onChange: this.onSelectChange, onBlur, onFocus, options, schema, uiSchema, registry, value: items, disabled, readonly, hideError, required, label, hideLabel: !displayLabel, placeholder, formContext, autofocus, rawErrors } ); } /** Renders an array as a set of checkboxes */ renderMultiSelect() { const { schema, idSchema, uiSchema, formData: items = [], disabled = false, readonly = false, autofocus = false, required = false, placeholder, onBlur, onFocus, registry, rawErrors, name } = this.props; const { widgets: widgets2, schemaUtils, formContext, globalUiOptions } = registry; const itemsSchema = schemaUtils.retrieveSchema(schema.items, items); const enumOptions = utils.optionsList(itemsSchema, uiSchema); const { widget = "select", title: uiTitle, ...options } = utils.getUiOptions(uiSchema, globalUiOptions); const Widget = utils.getWidget(schema, widget, widgets2); const label = uiTitle ?? schema.title ?? name; const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); return /* @__PURE__ */ jsxRuntime.jsx( Widget, { id: idSchema.$id, name, multiple: true, onChange: this.onSelectChange, onBlur, onFocus, options: { ...options, enumOptions }, schema, uiSchema, registry, value: items, disabled, readonly, required, label, hideLabel: !displayLabel, placeholder, formContext, autofocus, rawErrors } ); } /** Renders an array of files using the `FileWidget` */ renderFiles() { const { schema, uiSchema, idSchema, name, disabled = false, readonly = false, autofocus = false, required = false, onBlur, onFocus, registry, formData: items = [], rawErrors } = this.props; const { widgets: widgets2, formContext, globalUiOptions, schemaUtils } = registry; const { widget = "files", title: uiTitle, ...options } = utils.getUiOptions(uiSchema, globalUiOptions); const Widget = utils.getWidget(schema, widget, widgets2); const label = uiTitle ?? schema.title ?? name; const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); return /* @__PURE__ */ jsxRuntime.jsx( Widget, { options, id: idSchema.$id, name, multiple: true, onChange: this.onSelectChange, onBlur, onFocus, schema, uiSchema, value: items, disabled, readonly, required, registry, formContext, autofocus, rawErrors, label, hideLabel: !displayLabel } ); } /** Renders an array that has a maximum limit of items */ renderFixedArray() { const { schema, uiSchema = {}, formData = [], errorSchema, idPrefix, idSeparator = "_", idSchema, name, title, disabled = false, readonly = false, autofocus = false, required = false, registry, onBlur, onFocus, rawErrors } = this.props; const { keyedFormData } = this.state; let { formData: items = [] } = this.props; const fieldTitle = schema.title || title || name; const uiOptions = utils.getUiOptions(uiSchema); const { schemaUtils, formContext } = registry; const _schemaItems = isObject(schema.items) ? schema.items : []; const itemSchemas = _schemaItems.map( (item, index) => schemaUtils.retrieveSchema(item, formData[index]) ); const additionalSchema = isObject(schema.additionalItems) ? schemaUtils.retrieveSchema(schema.additionalItems, formData) : null; if (!items || items.length < itemSchemas.length) { items = items || []; items = items.concat(new Array(itemSchemas.length - items.length)); } const canAdd = this.canAddItem(items) && !!additionalSchema; const arrayProps = { canAdd, className: "field field-array field-array-fixed-items", disabled, idSchema, formData, items: keyedFormData.map((keyedItem, index) => { const { key, item } = keyedItem; const itemCast = item; const additional = index >= itemSchemas.length; const itemSchema = (additional && isObject(schema.additionalItems) ? schemaUtils.retrieveSchema(schema.additionalItems, itemCast) : itemSchemas[index]) || {}; const itemIdPrefix = idSchema.$id + idSeparator + index; const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator); const itemUiSchema = additional ? uiSchema.additionalItems || {} : Array.isArray(uiSchema.items) ? uiSchema.items[index] : uiSchema.items || {}; const itemErrorSchema = errorSchema ? errorSchema[index] : void 0; return this.renderArrayFieldItem({ key, index, name: name && `${name}-${index}`, title: fieldTitle ? `${fieldTitle}-${index + 1}` : void 0, canAdd, canRemove: additional, canMoveUp: index >= itemSchemas.length + 1, canMoveDown: additional && index < items.length - 1, itemSchema, itemData: itemCast, itemUiSchema, itemIdSchema, itemErrorSchema, autofocus: autofocus && index === 0, onBlur, onFocus, rawErrors, totalItems: keyedFormData.length }); }), onAddClick: this.onAddClick, readonly, required, registry, schema, uiSchema, title: fieldTitle, formContext, errorSchema, rawErrors }; const Template = utils.getTemplate("ArrayFieldTemplate", registry, uiOptions); return /* @__PURE__ */ jsxRuntime.jsx(Template, { ...arrayProps }); } /** Renders the individual array item using a `SchemaField` along with the additional properties required to be send * back to the `ArrayFieldItemTemplate`. * * @param props - The props for the individual array item to be rendered */ renderArrayFieldItem(props) { const { key, index, name, canAdd, canRemove = true, canMoveUp, canMoveDown, itemSchema, itemData, itemUiSchema, itemIdSchema, itemErrorSchema, autofocus, onBlur, onFocus, rawErrors, totalItems, title } = props; const { disabled, hideError, idPrefix, idSeparator, readonly, uiSchema, registry, formContext } = this.props; const { fields: { ArraySchemaField, SchemaField: SchemaField2 }, globalUiOptions } = registry; const ItemSchemaField = ArraySchemaField || SchemaField2; const { orderable = true, removable = true, copyable = false } = utils.getUiOptions(uiSchema, globalUiOptions); const has2 = { moveUp: orderable && canMoveUp, moveDown: orderable && canMoveDown, copy: copyable && canAdd, remove: removable && canRemove, toolbar: false }; has2.toolbar = Object.keys(has2).some((key2) => has2[key2]); return { children: /* @__PURE__ */ jsxRuntime.jsx( ItemSchemaField, { name, title, index, schema: itemSchema, uiSchema: itemUiSchema, formData: itemData, formContext, errorSchema: itemErrorSchema, idPrefix, idSeparator, idSchema: itemIdSchema, required: this.isItemRequired(itemSchema), onChange: this.onChangeForIndex(index), onBlur, onFocus, registry, disabled, readonly, hideError, autofocus, rawErrors } ), className: "array-item", disabled, canAdd, hasCopy: has2.copy, hasToolbar: has2.toolbar, hasMoveUp: has2.moveUp, hasMoveDown: has2.moveDown, hasRemove: has2.remove, index, totalItems, key, onAddIndexClick: this.onAddIndexClick, onCopyIndexClick: this.onCopyIndexClick, onDropIndexClick: this.onDropIndexClick, onReorderClick: this.onReorderClick, readonly, registry, schema: itemSchema, uiSchema: itemUiSchema }; } }; var ArrayField_default = ArrayField; function BooleanField(props) { const { schema, name, uiSchema, idSchema, formData, registry, required, disabled, readonly, hideError, autofocus, title, onChange, onFocus, onBlur, rawErrors } = props; const { title: schemaTitle } = schema; const { widgets: widgets2, formContext, translateString, globalUiOptions } = registry; const { widget = "checkbox", title: uiTitle, // Unlike the other fields, don't use `getDisplayLabel()` since it always returns false for the boolean type label: displayLabel = true, ...options } = utils.getUiOptions(uiSchema, globalUiOptions); const Widget = utils.getWidget(schema, widget, widgets2); const yes = translateString(utils.TranslatableString.YesLabel); const no = translateString(utils.TranslatableString.NoLabel); let enumOptions; const label = uiTitle ?? schemaTitle ?? title ?? name; if (Array.isArray(schema.oneOf)) { enumOptions = utils.optionsList( { oneOf: schema.oneOf.map((option) => { if (isObject(option)) { return { ...option, title: option.title || (option.const === true ? yes : no) }; } return void 0; }).filter((o) => o) // cast away the error that typescript can't grok is fixed }, uiSchema ); } else { const schemaWithEnumNames = schema; const enums = schema.enum ?? [true, false]; if (!schemaWithEnumNames.enumNames && enums.length === 2 && enums.every((v) => typeof v === "boolean")) { enumOptions = [ { value: enums[0], label: enums[0] ? yes : no }, { value: enums[1], label: enums[1] ? yes : no } ]; } else { enumOptions = utils.optionsList( { enum: enums, // NOTE: enumNames is deprecated, but still supported for now. enumNames: schemaWithEnumNames.enumNames }, uiSchema ); } } return /* @__PURE__ */ jsxRuntime.jsx( Widget, { options: { ...options, enumOptions }, schema, uiSchema, id: idSchema.$id, name, onChange, onFocus, onBlur, label, hideLabel: !displayLabel, value: formData, required, disabled, readonly, hideError, registry, formContext, autofocus, rawErrors } ); } var BooleanField_default = BooleanField; var AnyOfField = class extends react.Component { /** Constructs an `AnyOfField` with the given `props` to initialize the initially selected option in state * * @param props - The `FieldProps` for this template */ constructor(props) { super(props); const { formData, options, registry: { schemaUtils } } = this.props; const retrievedOptions = options.map((opt) => schemaUtils.retrieveSchema(opt, formData)); this.state = { retrievedOptions, selectedOption: this.getMatchingOption(0, formData, retrievedOptions) }; } /** React lifecycle method that is called when the props and/or state for this component is updated. It recomputes the * currently selected option based on the overall `formData` * * @param prevProps - The previous `FieldProps` for this template * @param prevState - The previous `AnyOfFieldState` for this template */ componentDidUpdate(prevProps, prevState) { const { formData, options, idSchema } = this.props; const { selectedOption } = this.state; let newState = this.state; if (!utils.deepEquals(prevProps.options, options)) { const { registry: { schemaUtils } } = this.props; const retrievedOptions = options.map((opt) => schemaUtils.retrieveSchema(opt, formData)); newState = { selectedOption, retrievedOptions }; } if (!utils.deepEquals(formData, prevProps.formData) && idSchema.$id === prevProps.idSchema.$id) { const { retrievedOptions } = newState; const matchingOption = this.getMatchingOption(selectedOption, formData, retrievedOptions); if (prevState && matchingOption !== selectedOption) { newState = { selectedOption: matchingOption, retrievedOptions }; } } if (newState !== this.state) { this.setState(newState); } } /** Determines the best matching option for the given `formData` and `options`. * * @param formData - The new formData * @param options - The list of options to choose from * @return - The index of the `option` that best matches the `formData` */ getMatchingOption(selectedOption, formData, options) { const { schema, registry: { schemaUtils } } = this.props; const discriminator = utils.getDiscriminatorFieldFromSchema(schema); const option = schemaUtils.getClosestMatchingOption(formData, options, selectedOption, discriminator); return option; } /** Callback handler to remember what the currently selected option is. In addition to that the `formData` is updated * to remove properties that are not part of the newly selected option schema, and then the updated data is passed to * the `onChange` handler. * * @param option - The new option value being selected */ onOptionChange = (option) => { const { selectedOption, retrievedOptions } = this.state; const { formData, onChange, registry } = this.props; const { schemaUtils } = registry; const intOption = option !== void 0 ? parseInt(option, 10) : -1; if (intOption === selectedOption) { return; } const newOption = intOption >= 0 ? retrievedOptions[intOption] : void 0; const oldOption = selectedOption >= 0 ? retrievedOptions[selectedOption] : void 0; let newFormData = schemaUtils.sanitizeDataForNewSchema(newOption, oldOption, formData); if (newOption) { newFormData = schemaUtils.getDefaultFormState(newOption, newFormData, "excludeObjectChildren"); } this.setState({ selectedOption: intOption }, () => { onChange(newFormData, void 0, this.getFieldId()); }); }; getFieldId() { const { idSchema, schema } = this.props; return `${idSchema.$id}${schema.oneOf ? "__oneof_select" : "__anyof_select"}`; } /** Renders the `AnyOfField` selector along with a `SchemaField` for the value of the `formData` */ render() { const { name, disabled = false, errorSchema = {}, formContext, onBlur, onFocus, readonly, registry, schema, uiSchema } = this.props; const { widgets: widgets2, fields: fields2, translateString, globalUiOptions, schemaUtils } = registry; const { SchemaField: _SchemaField } = fields2; const { selectedOption, retrievedOptions } = this.state; const { widget = "select", placeholder, autofocus, autocomplete, title = schema.title, ...uiOptions } = utils.getUiOptions(uiSchema, globalUiOptions); const Widget = utils.getWidget({ type: "number" }, widget, widgets2); const rawErrors = get3(errorSchema, utils.ERRORS_KEY, []); const fieldErrorSchema = omit2(errorSchema, [utils.ERRORS_KEY]); const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); const option = selectedOption >= 0 ? retrievedOptions[selectedOption] || null : null; let optionSchema; if (option) { const { required } = schema; optionSchema = required ? utils.mergeSchemas({ required }, option) : option; } let optionsUiSchema = []; if (utils.ONE_OF_KEY in schema && uiSchema && utils.ONE_OF_KEY in uiSchema) { if (Array.isArray(uiSchema[utils.ONE_OF_KEY])) { optionsUiSchema = uiSchema[utils.ONE_OF_KEY]; } else { console.warn(`uiSchema.oneOf is not an array for "${title || name}"`); } } else if (utils.ANY_OF_KEY in schema && uiSchema && utils.ANY_OF_KEY in uiSchema) { if (Array.isArray(uiSchema[utils.ANY_OF_KEY])) { optionsUiSchema = uiSchema[utils.ANY_OF_KEY]; } else { console.warn(`uiSchema.anyOf is not an array for "${title || name}"`); } } let optionUiSchema = uiSchema; if (selectedOption >= 0 && optionsUiSchema.length > selectedOption) { optionUiSchema = optionsUiSchema[selectedOption]; } const translateEnum = title ? utils.TranslatableString.TitleOptionPrefix : utils.TranslatableString.OptionPrefix; const translateParams = title ? [title] : []; const enumOptions = retrievedOptions.map((opt, index) => { const { title: uiTitle = opt.title } = utils.getUiOptions(optionsUiSchema[index]); return { label: uiTitle || translateString(translateEnum, translateParams.concat(String(index + 1))), value: index }; }); return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel panel-default panel-body", children: [ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "form-group", children: /* @__PURE__ */ jsxRuntime.jsx( Widget, { id: this.getFieldId(), name: `${name}${schema.oneOf ? "__oneof_select" : "__anyof_select"}`, schema: { type: "number", default: 0 }, onChange: this.onOptionChange, onBlur, onFocus, disabled: disabled || isEmpty(enumOptions), multiple: false, rawErrors, errorSchema: fieldErrorSchema, value: selectedOption >= 0 ? selectedOption : void 0, options: { enumOptions, ...uiOptions }, registry, formContext, placeholder, autocomplete, autofocus, label: title ?? name, hideLabel: !displayLabel, readonly } ) }), optionSchema && optionSchema.type !== "null" && /* @__PURE__ */ jsxRuntime.jsx(_SchemaField, { ...this.props, schema: optionSchema, uiSchema: optionUiSchema }) ] }); } }; var MultiSchemaField_default = AnyOfField; var trailingCharMatcherWithPrefix = /\.([0-9]*0)*$/; var trailingCharMatcher = /[0.]0*$/; function NumberField(props) { const { registry, onChange, formData, value: initialValue } = props; const [lastValue, setLastValue] = react.useState(initialValue); const { StringField: StringField2 } = registry.fields; let value = formData; const handleChange = react.useCallback( (value2, errorSchema, id) => { setLastValue(value2); if (`${value2}`.charAt(0) === ".") { value2 = `0${value2}`; } const processed = typeof value2 === "string" && value2.match(trailingCharMatcherWithPrefix) ? utils.asNumber(value2.replace(trailingCharMatcher, "")) : utils.asNumber(value2); onChange(processed, errorSchema, id); }, [onChange] ); if (typeof lastValue === "string" && typeof value === "number") { const re = new RegExp(`^(${String(value).replace(".", "\\.")})?\\.?0*$`); if (lastValue.match(re)) { value = lastValue; } } return /* @__PURE__ */ jsxRuntime.jsx(StringField2, { ...props, formData: value, onChange: handleChange }); } var NumberField_default = NumberField; var ObjectField = class extends react.Component { /** Set up the initial state */ state = { wasPropertyKeyModified: false, additionalProperties: {} }; /** Returns a flag indicating whether the `name` field is required in the object schema * * @param name - The name of the field to check for required-ness * @returns - True if the field `name` is required, false otherwise */ isRequired(name) { const { schema } = this.props; return Array.isArray(schema.required) && schema.required.indexOf(name) !== -1; } /** Returns the `onPropertyChange` handler for the `name` field. Handles the special case where a user is attempting * to clear the data for a field added as an additional property. Calls the `onChange()` handler with the updated * formData. * * @param name - The name of the property * @param addedByAdditionalProperties - Flag indicating whether this property is an additional property * @returns - The onPropertyChange callback for the `name` property */ onPropertyChange = (name, addedByAdditionalProperties = false) => { return (value, newErrorSchema, id) => { const { formData, onChange, errorSchema } = this.props; if (value === void 0 && addedByAdditionalProperties) { value = ""; } const newFormData = { ...formData, [name]: value }; onChange( newFormData, errorSchema && errorSchema && { ...errorSchema, [name]: newErrorSchema }, id ); }; }; /** Returns a callback to handle the onDropPropertyClick event for the given `key` which removes the old `key` data * and calls the `onChange` callback with it * * @param key - The key for which the drop callback is desired * @returns - The drop property click callback */ onDropPropertyClick = (key) => { return (event) => { event.preventDefault(); const { onChange, formData } = this.props; const copiedFormData = { ...formData }; unset(copiedFormData, key); onChange(copiedFormData); }; }; /** Computes the next available key name from the `preferredKey`, indexing through the already existing keys until one * that is already not assigned is found. * * @param preferredKey - The preferred name of a new key * @param [formData] - The form data in which to check if the desired key already exists * @returns - The name of the next available key from `preferredKey` */ getAvailableKey = (preferredKey, formData) => { const { uiSchema, registry } = this.props; const { duplicateKeySuffixSeparator = "-" } = utils.getUiOptions(uiSchema, registry.globalUiOptions); let index = 0; let newKey = preferredKey; while (has(formData, newKey)) { newKey = `${preferredKey}${duplicateKeySuffixSeparator}${++index}`; } return newKey; }; /** Returns a callback function that deals with the rename of a key for an additional property for a schema. That * callback will attempt to rename the key and move the existing data to that key, calling `onChange` when it does. * * @param oldValue - The old value of a field * @returns - The key change callback function */ onKeyChange = (oldValue) => { return (value, newErrorSchema) => { if (oldValue === value) { return; } const { formData, onChange, errorSchema } = this.props; value = this.getAvailableKey(value, formData); const newFormData = { ...formData }; const newKeys = { [oldValue]: value }; const keyValues = Object.keys(newFormData).map((key) => { const newKey = newKeys[key] || key; return { [newKey]: newFormData[key] }; }); const renamedObj = Object.assign({}, ...keyValues); this.setState({ wasPropertyKeyModified: true }); onChange( renamedObj, errorSchema && errorSchema && { ...errorSchema, [value]: newErrorSchema } ); }; }; /** Returns a default value to be used for a new additional schema property of the given `type` * * @param type - The type of the new additional schema property */ getDefaultValue(type) { const { registry: { translateString } } = this.props; switch (type) { case "array": return []; case "boolean": return false; case "null": return null; case "number": return 0; case "object": return {}; case "string": default: return translateString(utils.TranslatableString.NewStringDefault); } } /** Handles the adding of a new additional property on the given `schema`. Calls the `onChange` callback once the new * default data for that field has been added to the formData. * * @param schema - The schema element to which the new property is being added */ handleAddClick = (schema) => () => { if (!schema.additionalProperties) { return; } const { formData, onChange, registry } = this.props; const newFormData = { ...formData }; let type = void 0; let constValue = void 0; let defaultValue = void 0; if (isObject(schema.additionalProperties)) { type = schema.additionalProperties.type; constValue = schema.additionalProperties.const; defaultValue = schema.additionalProperties.default; let apSchema = schema.additionalProperties; if (utils.REF_KEY in apSchema) { const { schemaUtils } = registry; apSchema = schemaUtils.retrieveSchema({ $ref: apSchema[utils.REF_KEY] }, formData); type = apSchema.type; constValue = apSchema.const; defaultValue = apSchema.default; } if (!type && (utils.ANY_OF_KEY in apSchema || utils.ONE_OF_KEY in apSchema)) { type = "object"; } } const newKey = this.getAvailableKey("newKey", newFormData); const newValue = constValue ?? defaultValue ?? this.getDefaultValue(type); set(newFormData, newKey, newValue); onChange(newFormData); }; /** Renders the `ObjectField` from the given props */ render() { const { schema: rawSchema, uiSchema = {}, formData, errorSchema, idSchema, name, required = false, disabled, readonly, hideError, idPrefix, idSeparator, onBlur, onFocus, registry, title } = this.props; const { fields: fields2, formContext, schemaUtils, translateString, globalUiOptions } = registry; const { SchemaField: SchemaField2 } = fields2; const schema = schemaUtils.retrieveSchema(rawSchema, formData); const uiOptions = utils.getUiOptions(uiSchema, globalUiOptions); const { properties: schemaProperties = {} } = schema; const templateTitle = uiOptions.title ?? schema.title ?? title ?? name; const description = uiOptions.description ?? schema.description; let orderedProperties; try { const properties = Object.keys(schemaProperties); orderedProperties = utils.orderProperties(properties, uiOptions.order); } catch (err) { return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "config-error", style: { color: "red" }, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown, { options: { disableParsingRawHTML: true }, children: translateString(utils.TranslatableString.InvalidObjectField, [name || "root", err.message]) }) }), /* @__PURE__ */ jsxRuntime.jsx("pre", { children: JSON.stringify(schema) }) ] }); } const Template = utils.getTemplate("ObjectFieldTemplate", registry, uiOptions); const templateProps = { // getDisplayLabel() always returns false for object types, so just check the `uiOptions.label` title: uiOptions.label === false ? "" : templateTitle, description: uiOptions.label === false ? void 0 : description, properties: orderedProperties.map((name2) => { const addedByAdditionalProperties = has(schema, [utils.PROPERTIES_KEY, name2, utils.ADDITIONAL_PROPERTY_FLAG]); const fieldUiSchema = addedByAdditionalProperties ? uiSchema.additionalProperties : uiSchema[name2]; const hidden = utils.getUiOptions(fieldUiSchema).widget === "hidden"; const fieldIdSchema = get3(idSchema, [name2], {}); return { content: /* @__PURE__ */ jsxRuntime.jsx( SchemaField2, { name: name2, required: this.isRequired(name2), schema: get3(schema, [utils.PROPERTIES_KEY, name2], {}), uiSchema: fieldUiSchema, errorSchema: get3(errorSchema, name2), idSchema: fieldIdSchema, idPrefix, idSeparator, formData: get3(formData, name2), formContext, wasPropertyKeyModified: this.state.wasPropertyKeyModified, onKeyChange: this.onKeyChange(name2), onChange: this.onPropertyChange(name2, addedByAdditionalProperties), onBlur, onFocus, registry, disabled, readonly, hideError, onDropPropertyClick: this.onDropPropertyClick }, name2 ), name: name2, readonly, disabled, required, hidden }; }), readonly, disabled, required, idSchema, uiSchema, errorSchema, schema, formData, formContext, registry }; return /* @__PURE__ */ jsxRuntime.jsx(Template, { ...templateProps, onAddClick: this.handleAddClick }); } }; var Ob