UNPKG

@canard/schema-form-mui-plugin

Version:

Material-UI (MUI) components plugin for @canard/schema-form providing pre-built form inputs with modern MUI styling and MUI X integration

759 lines (736 loc) 33.3 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { FormHelperText, Box, FormLabel as FormLabel$1, Button, FormControlLabel, Checkbox, Switch, TextField, RadioGroup, Radio, Typography, Slider, FormGroup as FormGroup$1, FormControl, InputLabel, Select, MenuItem, Stack } from '@mui/material'; import { Delete, Add as Add$1 } from '@mui/icons-material'; import { map } from '@winglet/common-utils/array'; import { useHandle } from '@winglet/react-utils/hook'; import { useMemo, useState, useRef, useEffect } from 'react'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import dayjs from 'dayjs'; import { LocalizationProvider as LocalizationProvider$1, TimePicker } from '@mui/x-date-pickers'; const FormError = ({ errorMessage }) => (jsx(FormHelperText, { error: true, children: errorMessage })); const FormGroup = ({ node, depth, Input, errorMessage, }) => { if (depth === 0) return jsx(Input, {}); if (node.group === 'branch') { return (jsx(Box, { component: "fieldset", sx: { marginBottom: 1, marginLeft: depth * 2, border: '1px solid', borderColor: 'divider', borderRadius: 1, padding: 1, }, children: jsx(Input, {}) })); } else { return (jsxs(Box, { sx: { marginBottom: 2, marginLeft: depth * 2, }, children: [jsx(Input, {}), jsx(FormHelperText, { error: true, children: errorMessage })] })); } }; const FormInput = ({ Input }) => jsx(Input, {}); const FormLabel = ({ name, path, required }) => (jsx(FormLabel$1, { htmlFor: path, required: required, children: name })); const Add = (props) => (jsx(Button, { color: "primary", startIcon: jsx(Add$1, {}), ...props, children: "Add" })); const Remove = (props) => (jsx(Button, { color: "primary", startIcon: jsx(Delete, {}), ...props })); const FormTypeInputArray = ({ node, readOnly, disabled, ChildNodeComponents, style, }) => { const handleClick = useHandle(() => { node.push(); }); const handleRemoveClick = useHandle((index) => { node.remove(index); }); return (jsxs("div", { style: style, children: [ChildNodeComponents && map(ChildNodeComponents, (ChildNodeComponent, i) => { const key = ChildNodeComponent.key; return (jsxs("div", { style: { display: 'flex' }, children: [jsx("div", { style: { flex: 1 }, children: jsx(ChildNodeComponent, { hideLabel: true }, key) }), !readOnly && (jsx(Remove, { title: "remove", disabled: disabled, onClick: () => handleRemoveClick(i) }))] }, key)); }), !readOnly && (jsx("div", { style: { marginLeft: 20 }, children: jsx(Add, { title: "add", disabled: disabled, onClick: handleClick }) }))] })); }; const FormTypeInputArrayDefinition = { Component: FormTypeInputArray, test: { type: 'array', }, }; const FormTypeInputBoolean = ({ path, name, jsonSchema, required, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp = 'medium', hideLabel, }) => { const [label, size] = useMemo(() => { if (hideLabel) return [undefined, sizeProp || context.size]; return [labelProp || jsonSchema.label || name, sizeProp || context.size]; }, [jsonSchema, context, labelProp, name, sizeProp, hideLabel]); const [indeterminate, defaultChecked] = useMemo(() => { const isIndeterminate = defaultValue !== undefined && typeof defaultValue !== 'boolean'; return [isIndeterminate, !!defaultValue]; }, [defaultValue]); const handleChange = useHandle((event) => { onChange(event.target.checked); }); return (jsx(FormControlLabel, { label: label, htmlFor: path, required: required, disabled: disabled, control: jsx(Checkbox, { id: path, name: name, disabled: disabled, indeterminate: indeterminate, defaultChecked: defaultChecked, onChange: handleChange, size: size }) })); }; const FormTypeInputBooleanDefinition = { Component: FormTypeInputBoolean, test: ({ type }) => type === 'boolean', }; const FormTypeInputBooleanSwitch = ({ path, name, required, jsonSchema, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp = 'medium', hideLabel, }) => { const handleChange = useHandle((event) => { onChange(event.target.checked); }); const [label, size] = useMemo(() => { if (hideLabel) return [undefined, sizeProp || context.size]; return [labelProp || jsonSchema.label || name, sizeProp || context.size]; }, [jsonSchema, context, labelProp, name, sizeProp, hideLabel]); return (jsx(FormControlLabel, { label: label, htmlFor: path, required: required, disabled: disabled, labelPlacement: "start", control: jsx(Switch, { id: path, name: name, defaultChecked: defaultValue === true, onChange: handleChange, disabled: disabled, size: size }) })); }; const FormTypeInputBooleanSwitchDefinition = { Component: FormTypeInputBooleanSwitch, test: ({ type, formType }) => type === 'boolean' && formType === 'switch', }; const FormTypeInputDate = ({ path, name, jsonSchema, required, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp, variant: variantProp, fullWidth: fullWidthProp, hideLabel, }) => { const [label, size, variant, fullWidth] = useMemo(() => { if (hideLabel) return [ undefined, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; return [ labelProp || jsonSchema.label || name, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; }, [ jsonSchema, context, labelProp, name, sizeProp, variantProp, fullWidthProp, hideLabel, ]); const { minDate, maxDate } = useMemo(() => { return { minDate: jsonSchema.minimum ? dayjs(jsonSchema.minimum) : undefined, maxDate: jsonSchema.maximum ? dayjs(jsonSchema.maximum) : undefined, }; }, [jsonSchema.minimum, jsonSchema.maximum]); const handleChange = useHandle((newValue) => { if (newValue && newValue.isValid()) { onChange(newValue.format('YYYY-MM-DD')); } else { onChange(null); } }); const disableDate = useHandle((date) => { if (minDate && date.isBefore(minDate, 'day')) return true; if (maxDate && date.isAfter(maxDate, 'day')) return true; return false; }); return (jsx(LocalizationProvider, { dateAdapter: AdapterDayjs, children: jsx(DatePicker, { label: label, defaultValue: defaultValue ? dayjs(defaultValue) : null, onChange: handleChange, disabled: disabled, minDate: minDate, maxDate: maxDate, shouldDisableDate: disableDate, slotProps: { textField: { id: path, name, required, size, variant, fullWidth, }, } }) })); }; const FormTypeInputDateDefinition = { Component: FormTypeInputDate, test: ({ type, format }) => type === 'string' && format === 'date', }; const FormTypeInputMonth = ({ path, name, jsonSchema, required, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp, variant: variantProp, fullWidth: fullWidthProp, hideLabel, }) => { const [label, size, variant, fullWidth] = useMemo(() => { if (hideLabel) return [ undefined, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; return [ labelProp || jsonSchema.label || name, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; }, [ jsonSchema, context, labelProp, name, sizeProp, variantProp, fullWidthProp, hideLabel, ]); const handleChange = useHandle((newValue) => { if (newValue && newValue.isValid()) { onChange(newValue.format('YYYY-MM')); } else { onChange(null); } }); return (jsx(LocalizationProvider, { dateAdapter: AdapterDayjs, children: jsx(DatePicker, { label: label, defaultValue: defaultValue ? dayjs(defaultValue) : null, onChange: handleChange, disabled: disabled, views: ['year', 'month'], openTo: "month", format: "YYYY-MM", slotProps: { textField: { id: path, name, required, size, variant, fullWidth, }, } }) })); }; const FormTypeInputMonthDefinition = { Component: FormTypeInputMonth, test: ({ type, format }) => type === 'string' && format === 'month', }; const FormTypeInputNumber = ({ path, name, jsonSchema, required, readOnly, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp, variant: variantProp, fullWidth: fullWidthProp, hideLabel, }) => { const [label, size, variant, fullWidth] = useMemo(() => { if (hideLabel) return [ undefined, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; return [ labelProp || jsonSchema.label || name, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; }, [ jsonSchema, context, labelProp, name, sizeProp, variantProp, fullWidthProp, hideLabel, ]); const handleChange = useHandle((event) => { const inputValue = event.target.value; if (inputValue === '') { onChange(null); return; } const numericValue = jsonSchema.type === 'integer' ? parseInt(inputValue, 10) : parseFloat(inputValue); if (!isNaN(numericValue)) { onChange(numericValue); } }); return (jsx(TextField, { id: path, name: name, type: "number", variant: variant, fullWidth: fullWidth, label: label, required: required, size: size, placeholder: jsonSchema.placeholder, defaultValue: defaultValue ?? undefined, onChange: handleChange, disabled: disabled, slotProps: { input: { readOnly, inputProps: { min: jsonSchema.minimum, max: jsonSchema.maximum, step: jsonSchema.multipleOf || (jsonSchema.type === 'integer' ? 1 : 'any'), }, }, } })); }; const FormTypeInputNumberDefinition = { Component: FormTypeInputNumber, test: { type: ['number', 'integer'], }, }; const FormTypeInputRadioGroup = ({ path, name, jsonSchema, required, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp = 'medium', row = true, hideLabel, }) => { const [label, size] = useMemo(() => { if (hideLabel) return [undefined, sizeProp || context.size]; return [labelProp || jsonSchema.label || name, sizeProp || context.size]; }, [jsonSchema, context, labelProp, name, sizeProp, hideLabel]); const options = useMemo(() => { const enumValues = jsonSchema.enum || []; const radioLabels = jsonSchema.radioLabels; const alias = jsonSchema.options?.alias || {}; return enumValues.map((rawValue, index) => { const value = '' + rawValue; return { value, rawValue, label: radioLabels?.[index] || alias[value] || value, }; }); }, [jsonSchema]); const initialValue = useMemo(() => options.find((option) => option.rawValue === defaultValue)?.value, [defaultValue, options]); const handleChange = useHandle((event) => { const newValue = event.target.value; const selectedOption = options.find((opt) => opt.value === newValue); if (selectedOption) { onChange(selectedOption.rawValue); } }); return (jsx(FormControlLabel, { label: label, htmlFor: path, required: required, disabled: disabled, labelPlacement: "start", style: { display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 8, }, control: jsx(RadioGroup, { name: name, defaultValue: initialValue, onChange: handleChange, row: row, children: options.map((option) => (jsx(FormControlLabel, { value: option.value, disabled: disabled, control: jsx(Radio, { size: size, id: `${path}-${option.value}` }), label: option.label }, option.value))) }) })); }; const FormTypeInputRadioGroupDefinition = { Component: FormTypeInputRadioGroup, test: ({ type, formType, jsonSchema }) => (type === 'string' || type === 'number' || type === 'integer') && (formType === 'radio' || formType === 'radiogroup') && !!jsonSchema.enum?.length, }; const FormTypeInputSlider = ({ path, name, jsonSchema, required, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp = 'medium', showMarks = false, hideLabel, }) => { const [label, size] = useMemo(() => { if (hideLabel) return [undefined, sizeProp || context.size]; return [labelProp || jsonSchema.label || name, sizeProp || context.size]; }, [jsonSchema, context, labelProp, name, sizeProp, hideLabel]); const min = jsonSchema.minimum ?? 0; const max = jsonSchema.maximum ?? 100; const step = jsonSchema.multipleOf ?? 1; const isLazy = jsonSchema.lazy ?? false; const handleChange = useHandle((_, newValue) => { if (!isLazy) { onChange(Array.isArray(newValue) ? newValue[0] : newValue); } }); const handleChangeCommitted = useHandle((_, newValue) => { if (isLazy) { onChange(Array.isArray(newValue) ? newValue[0] : newValue); } }); return (jsxs(Box, { sx: { px: 2 }, children: [label && (jsxs(Typography, { variant: "body2", component: "label", htmlFor: path, sx: { mb: 1, display: 'block' }, children: [label, required && ' *'] })), jsx(Slider, { id: path, name: name, defaultValue: typeof defaultValue === 'number' ? defaultValue : min, min: min, max: max, step: step, onChange: handleChange, onChangeCommitted: handleChangeCommitted, disabled: disabled, size: size, marks: showMarks, valueLabelDisplay: "auto", sx: { mt: 1 } }), jsxs(Box, { sx: { display: 'flex', justifyContent: 'space-between', mt: 1 }, children: [jsx(Typography, { variant: "caption", color: "text.secondary", children: min }), jsx(Typography, { variant: "caption", color: "text.secondary", children: max })] })] })); }; const FormTypeInputSliderDefinition = { Component: FormTypeInputSlider, test: ({ type, formType }) => (type === 'number' || type === 'integer') && formType === 'slider', }; const FormTypeInputString = ({ path, name, jsonSchema, required, readOnly, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp, variant: variantProp, fullWidth: fullWidthProp, hideLabel, }) => { const [label, size, variant, fullWidth] = useMemo(() => { if (hideLabel) return [ undefined, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; return [ labelProp || jsonSchema.label || name, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; }, [ jsonSchema, context, labelProp, name, sizeProp, variantProp, fullWidthProp, hideLabel, ]); const isPassword = useMemo(() => { return (jsonSchema.format === 'password' || jsonSchema.formType === 'password'); }, [jsonSchema]); const handleChange = useHandle((event) => { onChange(event.target.value); }); return (jsx(TextField, { id: path, name: name, type: isPassword ? 'password' : 'text', variant: variant, fullWidth: fullWidth, placeholder: jsonSchema.placeholder, label: label, required: required, disabled: disabled, defaultValue: defaultValue ?? undefined, onChange: handleChange, size: size, slotProps: { input: { readOnly, }, } })); }; const FormTypeInputStringDefinition = { Component: FormTypeInputString, test: { type: 'string', }, }; const FormTypeInputStringCheckbox = ({ path, name, jsonSchema, required, disabled, defaultValue = [], onChange, context, label: labelProp, size: sizeProp = 'medium', row = true, hideLabel, }) => { const [label, size] = useMemo(() => { if (hideLabel) return [undefined, sizeProp || context.size]; return [labelProp || jsonSchema.label || name, sizeProp || context.size]; }, [hideLabel, sizeProp, context.size, labelProp, jsonSchema, name]); const options = useMemo(() => { const enumValues = jsonSchema.items.enum || []; const checkboxLabels = jsonSchema.items?.options?.alias; return enumValues.map((rawValue) => { const value = '' + rawValue; return { value, rawValue, label: checkboxLabels?.[value] || value, }; }); }, [jsonSchema]); const [value, setValue] = useState(defaultValue.map((v) => '' + v)); const handleToggle = useHandle((optionValue) => { const currentValues = value; const isSelected = currentValues.includes(optionValue); let newValues; if (isSelected) { newValues = currentValues.filter((val) => val !== optionValue); } else { newValues = [...currentValues, optionValue]; } setValue(newValues); onChange(newValues); }); return (jsx(FormControlLabel, { label: label, htmlFor: path, required: required, disabled: disabled, labelPlacement: "start", style: { display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 8, }, control: jsx(FormGroup$1, { row: row, children: options.map((option) => { const isChecked = value.includes(option.value); return (jsx(FormControlLabel, { disabled: disabled, control: jsx(Checkbox, { id: `${path}-${option.value}`, name: `${name}[]`, checked: isChecked, onChange: () => handleToggle(option.value), size: size }), label: option.label }, option.value)); }) }) })); }; const FormTypeInputStringCheckboxDefinition = { Component: FormTypeInputStringCheckbox, test: ({ type, formType, jsonSchema }) => type === 'array' && formType === 'checkbox' && jsonSchema.items?.type === 'string' && jsonSchema.items?.enum && jsonSchema.items.enum.length > 0, }; const FormTypeInputStringEnum = ({ path, name, jsonSchema, required, readOnly, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp, variant: variantProp, fullWidth: fullWidthProp, hideLabel, }) => { const [label, size, variant, fullWidth] = useMemo(() => { if (hideLabel) return [ undefined, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; return [ labelProp || jsonSchema.label || name, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; }, [ jsonSchema, context, labelProp, name, sizeProp, variantProp, fullWidthProp, hideLabel, ]); const options = useMemo(() => jsonSchema.enum?.map((rawValue) => { const value = '' + rawValue; return { value, rawValue, label: jsonSchema.options?.alias?.[value] || value, }; }) || [], [jsonSchema]); const handleChange = useHandle((event) => { const rawValue = options.find((option) => option.value === event.target.value)?.rawValue; if (rawValue === undefined) return; onChange(rawValue); }); const labelId = useMemo(() => `label-${path}`, [path]); const initialValue = useMemo(() => { if (defaultValue === undefined) return undefined; if (defaultValue === null) return '' + null; return defaultValue; }, [defaultValue]); return (jsxs(FormControl, { fullWidth: fullWidth, variant: variant, children: [jsx(InputLabel, { id: labelId, children: label }), jsx(Select, { id: path, name: name, labelId: labelId, label: label, required: required, readOnly: readOnly, defaultValue: initialValue, onChange: handleChange, disabled: disabled, size: size, children: options.map((option) => (jsx(MenuItem, { value: option.value, children: option.label }, option.value))) })] })); }; const FormTypeInputStringEnumDefinition = { Component: FormTypeInputStringEnum, test: ({ type, jsonSchema }) => type === 'string' && !!jsonSchema.enum?.length, }; const FormTypeInputStringSwitch = ({ path, name, jsonSchema, required, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp = 'medium', hideLabel, }) => { const [label, size] = useMemo(() => { if (hideLabel) return [undefined, sizeProp || context.size]; return [labelProp || jsonSchema.label || name, sizeProp || context.size]; }, [hideLabel, sizeProp, context.size, labelProp, jsonSchema, name]); const { offValue, onValue, offLabel, onLabel } = useMemo(() => { const [first, second] = jsonSchema.enum; const [firstLabel, secondLabel] = jsonSchema.switchLabels || [ first, second, ]; return { offValue: first, onValue: second, offLabel: firstLabel, onLabel: secondLabel, }; }, [jsonSchema.enum, jsonSchema.switchLabels]); const handleChange = useHandle((event) => { onChange(event.target.checked ? onValue : offValue); }); const switchSize = jsonSchema.switchSize || size; return (jsx(FormControlLabel, { label: label, htmlFor: path, required: required, disabled: disabled, labelPlacement: "start", sx: { alignItems: 'center', gap: 1, }, control: jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: 'center' }, children: [jsx(Typography, { children: offLabel }), jsx(Switch, { id: path, name: name, defaultChecked: defaultValue === onValue, onChange: handleChange, disabled: disabled, size: switchSize }), jsx(Typography, { children: onLabel })] }) })); }; const FormTypeInputStringSwitchDefinition = { Component: FormTypeInputStringSwitch, test: ({ type, formType, jsonSchema }) => type === 'string' && formType === 'switch' && jsonSchema.enum?.length === 2, }; const FormTypeInputTextarea = ({ path, name, jsonSchema, required, readOnly, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp, variant: variantProp, fullWidth: fullWidthProp, minRows, maxRows, hideLabel, }) => { const [label, size, variant, fullWidth] = useMemo(() => { if (hideLabel) return [ undefined, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; return [ labelProp || jsonSchema.label || name, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; }, [ jsonSchema, context, labelProp, name, sizeProp, variantProp, fullWidthProp, hideLabel, ]); const handleChange = useHandle((event) => { onChange(event.target.value); }); const finalMinRows = minRows ?? jsonSchema.minRows ?? 3; const finalMaxRows = maxRows ?? jsonSchema.maxRows ?? 8; return (jsx(TextField, { id: path, name: name, multiline: true, variant: variant, fullWidth: fullWidth, label: label, required: required, size: size, placeholder: jsonSchema.placeholder, defaultValue: defaultValue, onChange: handleChange, disabled: disabled, minRows: finalMinRows, maxRows: finalMaxRows, slotProps: { input: { readOnly, }, }, sx: { '& .MuiInputBase-root': { alignItems: 'flex-start', }, } })); }; const FormTypeInputTextareaDefinition = { Component: FormTypeInputTextarea, test: ({ type, format, formType }) => type === 'string' && (format === 'textarea' || formType === 'textarea'), }; const FormTypeInputTime = ({ path, name, jsonSchema, required, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp, variant: variantProp, fullWidth: fullWidthProp, ampm: ampmProp, hideLabel, }) => { const [label, size, variant, fullWidth, ampm] = useMemo(() => { if (hideLabel) return [ undefined, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ampmProp ?? jsonSchema.ampm, ]; return [ labelProp || jsonSchema.label || name, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ampmProp ?? jsonSchema.ampm, ]; }, [ jsonSchema, context, labelProp, name, sizeProp, variantProp, fullWidthProp, ampmProp, hideLabel, ]); const timeValue = useMemo(() => { if (!defaultValue) return null; const today = dayjs().format('YYYY-MM-DD'); return dayjs(`${today}T${defaultValue}`); }, [defaultValue]); const handleChange = useHandle((newValue) => { if (newValue && newValue.isValid()) { onChange(newValue.format('HH:mm:ss')); } else { onChange(null); } }); return (jsx(LocalizationProvider$1, { dateAdapter: AdapterDayjs, children: jsx(TimePicker, { label: label, defaultValue: timeValue, onChange: handleChange, disabled: disabled, ampm: ampm, slotProps: { textField: { id: path, name, required, size, variant, fullWidth, }, } }) })); }; const FormTypeInputTimeDefinition = { Component: FormTypeInputTime, test: ({ type, format }) => type === 'string' && format === 'time', }; const DEFAULT_PROTOCOLS = ['http', 'https']; const normalizeProtocol = (protocol) => { return protocol.replace(/:\/\/$/, '').replace(/:$/, ''); }; const getProtocolSeparator = (protocol) => { const normalizedProtocol = normalizeProtocol(protocol); return ['mailto', 'tel'].includes(normalizedProtocol) ? ':' : '://'; }; const formatProtocolDisplay = (protocol) => { const normalizedProtocol = normalizeProtocol(protocol); const separator = getProtocolSeparator(normalizedProtocol); return `${normalizedProtocol}${separator}`; }; const parseUri = (uri) => { const singleColonMatch = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):(.*)$/); if (singleColonMatch && ['mailto', 'tel'].includes(singleColonMatch[1])) { return { protocol: singleColonMatch[1], path: singleColonMatch[2], }; } const doubleColonMatch = uri.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/(.*)$/); if (doubleColonMatch) { return { protocol: doubleColonMatch[1], path: doubleColonMatch[2], }; } return null; }; const FormTypeInputUri = ({ path, name, jsonSchema, required, readOnly, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp, variant: variantProp, fullWidth: fullWidthProp, protocols: protocolsProp, hideLabel, }) => { const [label, size, variant, fullWidth] = useMemo(() => { if (hideLabel) return [ undefined, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; return [ labelProp || jsonSchema.label || name, sizeProp || context.size, variantProp || context.variant, fullWidthProp ?? context.fullWidth, ]; }, [ jsonSchema, context, labelProp, name, sizeProp, variantProp, fullWidthProp, hideLabel, ]); const normalizedProtocols = useMemo(() => { const rawProtocols = protocolsProp || jsonSchema.options?.protocols || DEFAULT_PROTOCOLS; return rawProtocols.map(normalizeProtocol); }, [protocolsProp, jsonSchema]); const textFieldRef = useRef(null); const [protocol, setProtocol] = useState(normalizedProtocols[0]); const { initialProtocol, initialUri } = useMemo(() => { if (!defaultValue) { return { initialProtocol: normalizedProtocols[0], initialUri: '', }; } const parsed = parseUri(defaultValue); if (parsed) { const normalizedDetected = normalizeProtocol(parsed.protocol); return { initialProtocol: normalizedProtocols.includes(normalizedDetected) ? normalizedDetected : normalizedProtocols[0], initialUri: parsed.path, }; } return { initialProtocol: normalizedProtocols[0], initialUri: defaultValue, }; }, [defaultValue, normalizedProtocols]); useEffect(() => { setProtocol(initialProtocol); }, [initialProtocol]); const handleProtocolChange = useHandle((event) => { const newProtocol = event.target.value; setProtocol(newProtocol); const currentUri = textFieldRef.current?.value || ''; const separator = getProtocolSeparator(newProtocol); const newValue = currentUri ? `${newProtocol}${separator}${currentUri}` : `${newProtocol}${separator}`; onChange(newValue); }); const handleUriChange = useHandle((event) => { const newUri = event.target.value; const separator = getProtocolSeparator(protocol); const newValue = newUri ? `${protocol}${separator}${newUri}` : ''; onChange(newValue); }); return (jsxs(Box, { children: [jsx(InputLabel, { htmlFor: path, required: required, children: label }), jsxs(Box, { sx: { display: 'flex', gap: 1, alignItems: 'flex-start' }, children: [jsx(Select, { value: protocol, onChange: handleProtocolChange, disabled: disabled || readOnly, size: size, displayEmpty: true, children: normalizedProtocols.map((prot) => (jsx(MenuItem, { value: prot, children: formatProtocolDisplay(prot) }, prot))) }), jsx(TextField, { inputRef: textFieldRef, id: path, name: name, variant: variant, fullWidth: fullWidth, size: size, placeholder: jsonSchema.placeholder, defaultValue: initialUri, onChange: handleUriChange, disabled: disabled, slotProps: { input: { readOnly, }, } })] })] })); }; const FormTypeInputUriDefinition = { Component: FormTypeInputUri, test: ({ type, format, formType }) => type === 'string' && (format === 'uri' || formType === 'uri'), }; const formTypeInputDefinitions = [ FormTypeInputBooleanSwitchDefinition, FormTypeInputStringCheckboxDefinition, FormTypeInputStringSwitchDefinition, FormTypeInputUriDefinition, FormTypeInputMonthDefinition, FormTypeInputDateDefinition, FormTypeInputTimeDefinition, FormTypeInputRadioGroupDefinition, FormTypeInputStringEnumDefinition, FormTypeInputArrayDefinition, FormTypeInputSliderDefinition, FormTypeInputTextareaDefinition, FormTypeInputStringDefinition, FormTypeInputNumberDefinition, FormTypeInputBooleanDefinition, ]; const plugin = { FormGroup, FormLabel, FormInput, FormError, formTypeInputDefinitions, }; export { plugin };