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

738 lines (714 loc) 33.6 kB
'use strict'; const jsxRuntime = require('react/jsx-runtime'); const material = require('@mui/material'); const iconsMaterial = require('@mui/icons-material'); const array = require('@winglet/common-utils/array'); const hook = require('@winglet/react-utils/hook'); const react = require('react'); const AdapterDayjs = require('@mui/x-date-pickers/AdapterDayjs'); const DatePicker = require('@mui/x-date-pickers/DatePicker'); const LocalizationProvider = require('@mui/x-date-pickers/LocalizationProvider'); const dayjs = require('dayjs'); const xDatePickers = require('@mui/x-date-pickers'); const FormError = ({ errorMessage }) => (jsxRuntime.jsx(material.FormHelperText, { error: true, children: errorMessage })); const FormGroup = ({ node, depth, Input, errorMessage, }) => { if (depth === 0) return jsxRuntime.jsx(Input, {}); if (node.group === 'branch') { return (jsxRuntime.jsx(material.Box, { component: "fieldset", sx: { marginBottom: 1, marginLeft: depth * 2, border: '1px solid', borderColor: 'divider', borderRadius: 1, padding: 1, }, children: jsxRuntime.jsx(Input, {}) })); } else { return (jsxRuntime.jsxs(material.Box, { sx: { marginBottom: 2, marginLeft: depth * 2, }, children: [jsxRuntime.jsx(Input, {}), jsxRuntime.jsx(material.FormHelperText, { error: true, children: errorMessage })] })); } }; const FormInput = ({ Input }) => jsxRuntime.jsx(Input, {}); const FormLabel = ({ name, path, required }) => (jsxRuntime.jsx(material.FormLabel, { htmlFor: path, required: required, children: name })); const Add = (props) => (jsxRuntime.jsx(material.Button, { color: "primary", startIcon: jsxRuntime.jsx(iconsMaterial.Add, {}), ...props, children: "Add" })); const Remove = (props) => (jsxRuntime.jsx(material.Button, { color: "primary", startIcon: jsxRuntime.jsx(iconsMaterial.Delete, {}), ...props })); const FormTypeInputArray = ({ node, readOnly, disabled, ChildNodeComponents, style, }) => { const handleClick = hook.useHandle(() => { node.push(); }); const handleRemoveClick = hook.useHandle((index) => { node.remove(index); }); return (jsxRuntime.jsxs("div", { style: style, children: [ChildNodeComponents && array.map(ChildNodeComponents, (ChildNodeComponent, i) => (jsxRuntime.jsxs("div", { style: { display: 'flex' }, children: [jsxRuntime.jsx("div", { style: { flex: 1 }, children: jsxRuntime.jsx(ChildNodeComponent, { hideLabel: true }) }), !readOnly && (jsxRuntime.jsx(Remove, { title: "remove", disabled: disabled, onClick: () => handleRemoveClick(i) }))] }, ChildNodeComponent.key))), !readOnly && (jsxRuntime.jsx("div", { style: { marginLeft: 20 }, children: jsxRuntime.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] = react.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] = react.useMemo(() => { const isIndeterminate = defaultValue !== undefined && typeof defaultValue !== 'boolean'; return [isIndeterminate, !!defaultValue]; }, [defaultValue]); const handleChange = hook.useHandle((event) => { onChange(event.target.checked); }); return (jsxRuntime.jsx(material.FormControlLabel, { label: label, htmlFor: path, required: required, disabled: disabled, control: jsxRuntime.jsx(material.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 = hook.useHandle((event) => { onChange(event.target.checked); }); const [label, size] = react.useMemo(() => { if (hideLabel) return [undefined, sizeProp || context.size]; return [labelProp || jsonSchema.label || name, sizeProp || context.size]; }, [jsonSchema, context, labelProp, name, sizeProp, hideLabel]); return (jsxRuntime.jsx(material.FormControlLabel, { label: label, htmlFor: path, required: required, disabled: disabled, labelPlacement: "start", control: jsxRuntime.jsx(material.Switch, { id: path, name: name, defaultChecked: defaultValue, 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] = react.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 } = react.useMemo(() => { return { minDate: jsonSchema.minimum ? dayjs(jsonSchema.minimum) : undefined, maxDate: jsonSchema.maximum ? dayjs(jsonSchema.maximum) : undefined, }; }, [jsonSchema.minimum, jsonSchema.maximum]); const handleChange = hook.useHandle((newValue) => { if (newValue && newValue.isValid()) { onChange(newValue.format('YYYY-MM-DD')); } else { onChange(''); } }); const disableDate = hook.useHandle((date) => { if (minDate && date.isBefore(minDate, 'day')) return true; if (maxDate && date.isAfter(maxDate, 'day')) return true; return false; }); return (jsxRuntime.jsx(LocalizationProvider.LocalizationProvider, { dateAdapter: AdapterDayjs.AdapterDayjs, children: jsxRuntime.jsx(DatePicker.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] = react.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 = hook.useHandle((newValue) => { if (newValue && newValue.isValid()) { onChange(newValue.format('YYYY-MM')); } else { onChange(''); } }); return (jsxRuntime.jsx(LocalizationProvider.LocalizationProvider, { dateAdapter: AdapterDayjs.AdapterDayjs, children: jsxRuntime.jsx(DatePicker.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] = react.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 = hook.useHandle((event) => { const inputValue = event.target.value; if (inputValue === '') { onChange(NaN); return; } const numericValue = jsonSchema.type === 'integer' ? parseInt(inputValue, 10) : parseFloat(inputValue); if (!isNaN(numericValue)) { onChange(numericValue); } }); return (jsxRuntime.jsx(material.TextField, { id: path, name: name, type: "number", variant: variant, fullWidth: fullWidth, label: label, required: required, size: size, placeholder: jsonSchema.placeholder, defaultValue: defaultValue, 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] = react.useMemo(() => { if (hideLabel) return [undefined, sizeProp || context.size]; return [labelProp || jsonSchema.label || name, sizeProp || context.size]; }, [jsonSchema, context, labelProp, name, sizeProp, hideLabel]); const options = react.useMemo(() => { const enumValues = jsonSchema.enum || []; const radioLabels = jsonSchema.radioLabels || enumValues.map(String); return enumValues.map((val, index) => ({ value: String(val), originalValue: val, label: radioLabels[index] || String(val), })); }, [jsonSchema]); const handleChange = hook.useHandle((event) => { const newValue = event.target.value; const selectedOption = options.find((opt) => opt.value === newValue); if (selectedOption) { onChange(selectedOption.originalValue); } }); return (jsxRuntime.jsx(material.FormControlLabel, { label: label, htmlFor: path, required: required, disabled: disabled, labelPlacement: "start", style: { display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 8, }, control: jsxRuntime.jsx(material.RadioGroup, { name: name, defaultValue: defaultValue, onChange: handleChange, row: row, children: options.map((option) => (jsxRuntime.jsx(material.FormControlLabel, { value: option.value, disabled: disabled, control: jsxRuntime.jsx(material.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] = react.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 = hook.useHandle((_, newValue) => { if (!isLazy) { onChange(Array.isArray(newValue) ? newValue[0] : newValue); } }); const handleChangeCommitted = hook.useHandle((_, newValue) => { if (isLazy) { onChange(Array.isArray(newValue) ? newValue[0] : newValue); } }); return (jsxRuntime.jsxs(material.Box, { sx: { px: 2 }, children: [label && (jsxRuntime.jsxs(material.Typography, { variant: "body2", component: "label", htmlFor: path, sx: { mb: 1, display: 'block' }, children: [label, required && ' *'] })), jsxRuntime.jsx(material.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 } }), jsxRuntime.jsxs(material.Box, { sx: { display: 'flex', justifyContent: 'space-between', mt: 1 }, children: [jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: min }), jsxRuntime.jsx(material.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] = react.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 = react.useMemo(() => { return (jsonSchema.format === 'password' || jsonSchema.formType === 'password'); }, [jsonSchema]); const handleChange = hook.useHandle((event) => { onChange(event.target.value); }); return (jsxRuntime.jsx(material.TextField, { id: path, name: name, type: isPassword ? 'password' : 'text', variant: variant, fullWidth: fullWidth, placeholder: jsonSchema.placeholder, label: label, required: required, disabled: disabled, defaultValue: defaultValue, 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] = react.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 = react.useMemo(() => { const enumValues = jsonSchema.items.enum || []; const checkboxLabels = jsonSchema.items?.options?.alias; return enumValues.map((value) => ({ value, label: checkboxLabels?.[value] || value, })); }, [jsonSchema]); const handleToggle = hook.useHandle((optionValue) => { const currentValues = Array.isArray(defaultValue) ? defaultValue : []; const isSelected = currentValues.includes(optionValue); let newValues; if (isSelected) { newValues = currentValues.filter((val) => val !== optionValue); } else { newValues = [...currentValues, optionValue]; } onChange(newValues); }); return (jsxRuntime.jsx(material.FormControlLabel, { label: label, htmlFor: path, required: required, disabled: disabled, labelPlacement: "start", style: { display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 8, }, control: jsxRuntime.jsx(material.FormGroup, { row: row, children: options.map((option) => { const isChecked = Array.isArray(defaultValue) ? defaultValue.includes(option.value) : false; return (jsxRuntime.jsx(material.FormControlLabel, { disabled: disabled, control: jsxRuntime.jsx(material.Checkbox, { id: `${path}-${option.value}`, name: `${name}[]`, defaultChecked: 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] = react.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 = react.useMemo(() => jsonSchema.enum.map((value) => ({ value, label: jsonSchema.options?.alias?.[value] || value, })), [jsonSchema]); const handleChange = hook.useHandle((event) => { onChange(event.target.value); }); const labelId = react.useMemo(() => `label-${path}`, [path]); return (jsxRuntime.jsxs(material.FormControl, { fullWidth: fullWidth, variant: variant, children: [jsxRuntime.jsx(material.InputLabel, { id: labelId, children: label }), jsxRuntime.jsx(material.Select, { id: path, name: name, labelId: labelId, label: label, required: required, readOnly: readOnly, defaultValue: defaultValue, onChange: handleChange, disabled: disabled, size: size, children: options.map((option) => (jsxRuntime.jsx(material.MenuItem, { value: option.value, children: option.label }, option.value))) })] })); }; const FormTypeInputStringEnumDefinition = { Component: FormTypeInputStringEnum, test: ({ type, jsonSchema }) => type === 'string' && jsonSchema.enum && jsonSchema.enum.length > 0, }; const FormTypeInputStringSwitch = ({ path, name, jsonSchema, required, disabled, defaultValue, onChange, context, label: labelProp, size: sizeProp = 'medium', hideLabel, }) => { const [label, size] = react.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 } = react.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 = hook.useHandle((event) => { onChange(event.target.checked ? onValue : offValue); }); const switchSize = jsonSchema.switchSize || size; return (jsxRuntime.jsx(material.FormControlLabel, { label: label, htmlFor: path, required: required, disabled: disabled, labelPlacement: "start", sx: { alignItems: 'center', gap: 1, }, control: jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, sx: { alignItems: 'center' }, children: [jsxRuntime.jsx(material.Typography, { children: offLabel }), jsxRuntime.jsx(material.Switch, { id: path, name: name, defaultChecked: defaultValue === onValue, onChange: handleChange, disabled: disabled, size: switchSize }), jsxRuntime.jsx(material.Typography, { children: onLabel })] }) })); }; const FormTypeInputStringSwitchDefinition = { Component: FormTypeInputStringSwitch, test: ({ type, formType, jsonSchema }) => type === 'string' && formType === 'switch' && jsonSchema.enum && 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] = react.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 = hook.useHandle((event) => { onChange(event.target.value); }); const finalMinRows = minRows ?? jsonSchema.minRows ?? 3; const finalMaxRows = maxRows ?? jsonSchema.maxRows ?? 8; return (jsxRuntime.jsx(material.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] = react.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 = react.useMemo(() => { if (!defaultValue) return null; const today = dayjs().format('YYYY-MM-DD'); return dayjs(`${today}T${defaultValue}`); }, [defaultValue]); const handleChange = hook.useHandle((newValue) => { if (newValue && newValue.isValid()) { onChange(newValue.format('HH:mm:ss')); } else { onChange(''); } }); return (jsxRuntime.jsx(xDatePickers.LocalizationProvider, { dateAdapter: AdapterDayjs.AdapterDayjs, children: jsxRuntime.jsx(xDatePickers.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] = react.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 = react.useMemo(() => { const rawProtocols = protocolsProp || jsonSchema.options?.protocols || DEFAULT_PROTOCOLS; return rawProtocols.map(normalizeProtocol); }, [protocolsProp, jsonSchema]); const textFieldRef = react.useRef(null); const [protocol, setProtocol] = react.useState(normalizedProtocols[0]); const { initialProtocol, initialUri } = react.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]); react.useEffect(() => { setProtocol(initialProtocol); }, [initialProtocol]); const handleProtocolChange = hook.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 = hook.useHandle((event) => { const newUri = event.target.value; const separator = getProtocolSeparator(protocol); const newValue = newUri ? `${protocol}${separator}${newUri}` : ''; onChange(newValue); }); return (jsxRuntime.jsxs(material.Box, { children: [jsxRuntime.jsx(material.InputLabel, { htmlFor: path, required: required, children: label }), jsxRuntime.jsxs(material.Box, { sx: { display: 'flex', gap: 1, alignItems: 'flex-start' }, children: [jsxRuntime.jsx(material.Select, { value: protocol, onChange: handleProtocolChange, disabled: disabled || readOnly, size: size, displayEmpty: true, children: normalizedProtocols.map((prot) => (jsxRuntime.jsx(material.MenuItem, { value: prot, children: formatProtocolDisplay(prot) }, prot))) }), jsxRuntime.jsx(material.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, }; exports.plugin = plugin;