UNPKG

@canard/schema-form-antd-plugin

Version:

Ant Design components plugin for @canard/schema-form providing pre-built form inputs with advanced components like date pickers, sliders, and file uploads

615 lines (589 loc) 27.2 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { Typography, Flex, Button, Checkbox, Switch, DatePicker, InputNumber, Radio, Slider, Input, Select, TimePicker } from 'antd'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { map } from '@winglet/common-utils/array'; import { useHandle } from '@winglet/react-utils/hook'; import { useMemo, useCallback, useRef, useState, useEffect } from 'react'; import dayjs from 'dayjs'; const FormError = ({ errorMessage }) => (jsx(Typography.Text, { type: "danger", children: errorMessage })); const FormGroup = ({ node, depth, path, name, required, Input, errorMessage, }) => { if (depth === 0) return jsx(Input, {}); if (node.group === 'branch') { return (jsxs("fieldset", { style: { marginBottom: 5, marginLeft: 5 * depth, }, children: [jsx("legend", { children: jsx(Typography.Text, { children: name }) }), jsx(Input, {})] })); } else { return (jsxs("div", { style: { marginBottom: 5, marginLeft: 5 * depth, }, children: [jsxs(Flex, { gap: 10, align: "center", children: [node.parentNode?.type !== 'array' && (jsxs("label", { htmlFor: path, children: [jsx(Typography.Text, { children: name }), required && (jsx(Typography.Text, { type: "danger", style: { marginLeft: 4 }, children: "*" }))] })), jsx(Input, {})] }), jsx(Typography.Text, { type: "danger", children: errorMessage })] })); } }; const FormInput = ({ Input }) => jsx(Input, {}); const FormLabel = ({ name, path, required }) => (jsxs("label", { htmlFor: path, children: [jsx(Typography.Text, { children: name }), required && (jsx(Typography.Text, { type: "danger", style: { marginLeft: 4 }, children: "*" }))] })); const Add = (props) => (jsx(Button, { type: "primary", icon: jsx(PlusOutlined, {}), ...props, children: "Add" })); const Remove = (props) => (jsx(Button, { type: "link", icon: jsx(MinusCircleOutlined, {}), ...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(ChildNodeComponent, {}, 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, disabled, value, onChange, }) => { const [indeterminate, checked] = useMemo(() => [ value !== undefined && typeof value !== 'boolean', value ?? undefined, ], [value]); const handleChange = useHandle((event) => { onChange(!!event.target.checked); }); return (jsx(Checkbox, { id: path, name: name, disabled: disabled, indeterminate: indeterminate, checked: checked, onChange: handleChange })); }; const FormTypeInputBooleanDefinition = { Component: FormTypeInputBoolean, test: { type: 'boolean', }, }; const FormTypeInputBooleanSwitch = ({ path, jsonSchema, disabled, value, onChange, context, size, }) => { const [checkedLabel, uncheckedLabel] = useMemo(() => { const alias = context?.checkboxLabels || jsonSchema.options?.alias || {}; return [alias.checked, alias.unchecked]; }, [context, jsonSchema]); const handleChange = useHandle((input) => { onChange(input); }); return (jsx(Switch, { disabled: disabled, checked: value ?? undefined, checkedChildren: checkedLabel, unCheckedChildren: uncheckedLabel, onChange: handleChange, size: size || context?.switchSize }, path)); }; const FormTypeInputBooleanSwitchDefinition = { Component: FormTypeInputBooleanSwitch, test: ({ type, formType }) => type === 'boolean' && formType === 'switch', }; const DATA_FORMAT$1 = 'YYYY-MM-DD'; const FormTypeInputDate = ({ path, name, jsonSchema, defaultValue, onChange, context, size, }) => { const handleChange = useHandle((value) => { onChange(value?.format(DATA_FORMAT$1) || ''); }); const initialValue = useMemo(() => (defaultValue ? dayjs(defaultValue, DATA_FORMAT$1) : undefined), [defaultValue]); const disabledDate = useMemo(() => { const { minimum, maximum } = jsonSchema.options || {}; return (date) => { if (!date) return false; if (!minimum && !maximum) return false; if (minimum && date.isBefore(dayjs(minimum, DATA_FORMAT$1), 'day')) return true; if (maximum && date.isAfter(dayjs(maximum, DATA_FORMAT$1), 'day')) return true; return false; }; }, [jsonSchema?.options]); return (jsx(DatePicker, { id: path, name: name, disabledDate: disabledDate, defaultValue: initialValue, onChange: handleChange, size: size || context?.size })); }; const FormTypeInputDateDefinition = { Component: FormTypeInputDate, test: { type: 'string', format: 'date', }, }; const DATE_FORMAT$3 = 'YYYY-MM-DD'; const FormTypeInputDateRange = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, context, size, }) => { const handleChange = useHandle((range) => { const [start, end] = range || []; if (!start || !end) return; onChange([start.format(DATE_FORMAT$3), end.format(DATE_FORMAT$3)]); }); const initialValue = useMemo(() => defaultValue?.length === 2 ? [ dayjs(defaultValue[0], DATE_FORMAT$3), dayjs(defaultValue[1], DATE_FORMAT$3), ] : undefined, [defaultValue]); const disabledDate = useMemo(() => { const { minimum, maximum } = jsonSchema.options || {}; return (date) => { if (!date) return false; if (!minimum && !maximum) return false; if (minimum && date.isBefore(dayjs(minimum, DATE_FORMAT$3), 'day')) return true; if (maximum && date.isAfter(dayjs(maximum, DATE_FORMAT$3), 'day')) return true; return false; }; }, [jsonSchema?.options]); return (jsx(DatePicker.RangePicker, { id: path, name: name, picker: "date", disabled: disabled, readOnly: readOnly, disabledDate: disabledDate, defaultValue: initialValue, onChange: handleChange, onPanelChange: handleChange, size: size || context?.size })); }; const FormTypeInputDateRangeDefinition = { Component: FormTypeInputDateRange, test: ({ type, format, formType, jsonSchema }) => type === 'array' && (format === 'date-range' || formType === 'dateRange') && jsonSchema.items?.type === 'string', }; const DATA_FORMAT = 'YYYY-MM'; const FormTypeInputMonth = ({ path, name, jsonSchema, defaultValue, onChange, context, size, }) => { const handleChange = useHandle((value) => { onChange(value?.format(DATA_FORMAT) || ''); }); const initialValue = useMemo(() => (defaultValue ? dayjs(defaultValue, DATA_FORMAT) : undefined), [defaultValue]); const disabledDate = useMemo(() => { const { minimum, maximum } = jsonSchema.options || {}; return (date) => { if (!date) return false; if (!minimum && !maximum) return false; if (minimum && date.isBefore(dayjs(minimum, DATA_FORMAT), 'month')) return true; if (maximum && date.isAfter(dayjs(maximum, DATA_FORMAT), 'month')) return true; return false; }; }, [jsonSchema?.options]); return (jsx(DatePicker, { id: path, name: name, picker: "month", disabledDate: disabledDate, defaultValue: initialValue, onChange: handleChange, size: size || context?.size })); }; const FormTypeInputMonthDefinition = { Component: FormTypeInputMonth, test: { type: 'string', format: 'month', }, }; const DATE_FORMAT$2 = 'YYYY-MM'; const FormTypeInputMonthRange = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, context, size, }) => { const handleChange = useHandle((range) => { const [start, end] = range || []; if (!start || !end) return; onChange([start.format(DATE_FORMAT$2), end.format(DATE_FORMAT$2)]); }); const initialValue = useMemo(() => defaultValue?.length === 2 ? [ dayjs(defaultValue[0], DATE_FORMAT$2), dayjs(defaultValue[1], DATE_FORMAT$2), ] : undefined, [defaultValue]); const disabledDate = useMemo(() => { const { minimum, maximum } = jsonSchema.options || {}; return (date) => { if (!date) return false; if (!minimum && !maximum) return false; if (minimum && date.isBefore(dayjs(minimum, DATE_FORMAT$2), 'day')) return true; if (maximum && date.isAfter(dayjs(maximum, DATE_FORMAT$2), 'day')) return true; return false; }; }, [jsonSchema?.options]); return (jsx(DatePicker.RangePicker, { id: path, name: name, picker: "month", disabled: disabled, readOnly: readOnly, disabledDate: disabledDate, defaultValue: initialValue, onChange: handleChange, onPanelChange: handleChange, size: size || context?.size })); }; const FormTypeInputMonthRangeDefinition = { Component: FormTypeInputMonthRange, test: ({ type, format, formType, jsonSchema }) => type === 'array' && (format === 'month-range' || formType === 'monthRange') && jsonSchema.items?.type === 'string', }; const FormTypeInputNumber = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, context, formatter, parser, size, }) => { const handleChange = useHandle((value) => { onChange(value); }); return (jsx(InputNumber, { id: path, name: name, readOnly: readOnly, disabled: disabled, placeholder: jsonSchema.placeholder, min: jsonSchema.minimum, max: jsonSchema.maximum, step: jsonSchema.multipleOf, formatter: formatter, parser: parser, defaultValue: defaultValue ?? undefined, onChange: handleChange, size: size || context?.size })); }; const FormTypeInputNumberDefinition = { Component: FormTypeInputNumber, test: { type: ['number', 'integer'], }, }; const FormTypeInputRadioGroup = ({ path, name, jsonSchema, disabled, defaultValue, onChange, context, size, }) => { const options = useMemo(() => { const alias = context.radioLabels || jsonSchema.options?.alias || {}; return jsonSchema.enum ? map(jsonSchema.enum, (rawValue) => { const value = '' + rawValue; return { value, rawValue, label: alias[value] || value, }; }) : []; }, [context, jsonSchema]); const initialValue = useMemo(() => options.find((option) => option.rawValue === defaultValue)?.value, [defaultValue, options]); const handleChange = useHandle((event) => { const rawValue = options.find((option) => option.value === event.target.value)?.rawValue; if (rawValue === undefined) return; onChange(rawValue); }); return (jsx(Radio.Group, { id: path, name: name, disabled: disabled, defaultValue: initialValue, options: options, onChange: handleChange, size: size || context?.size })); }; const FormTypeInputRadioGroupDefinition = { Component: FormTypeInputRadioGroup, test: ({ type, formType, jsonSchema }) => { return ((type === 'string' || type === 'number' || type === 'integer') && (formType === 'radio' || formType === 'radiogroup') && !!jsonSchema.enum?.length); }, }; const FormTypeInputSlider = ({ path, jsonSchema, disabled, defaultValue, onChange, }) => { const handleChange = useCallback((value) => { if (value === null) onChange(NaN); else onChange(value); }, [onChange]); const { min, max, step, ...changeHandler } = useMemo(() => { return { min: jsonSchema.minimum, max: jsonSchema.maximum, step: jsonSchema.multipleOf, ...(jsonSchema.options?.lazy === false ? { onChange: handleChange } : { onChangeComplete: handleChange }), }; }, [handleChange, jsonSchema]); return (jsx(Slider, { id: path, disabled: disabled, min: min, max: max, step: step, defaultValue: defaultValue ?? undefined, ...changeHandler })); }; const FormTypeInputSliderDefinition = { Component: FormTypeInputSlider, test: { type: ['number', 'integer'], formType: 'slider', }, }; const FormTypeInputString = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, context, size, }) => { const TextInput = useMemo(() => { if (jsonSchema.format === 'password' || jsonSchema.formType === 'password') return Input.Password; else return Input; }, [jsonSchema]); const handleChange = useHandle((event) => { onChange(event.target.value); }); return (jsx(TextInput, { id: path, name: name, readOnly: readOnly, disabled: disabled, placeholder: jsonSchema.placeholder, defaultValue: defaultValue ?? undefined, onChange: handleChange, size: size || context?.size })); }; const FormTypeInputStringDefinition = { Component: FormTypeInputString, test: { type: 'string', }, }; const FormTypeInputStringCheckbox = ({ name, jsonSchema, disabled, defaultValue, onChange, context, }) => { const options = useMemo(() => { const alias = context.checkboxLabels || jsonSchema.items?.options?.alias || jsonSchema.options?.alias || {}; return jsonSchema.items?.enum ? map(jsonSchema.items.enum, (rawValue) => { const value = '' + rawValue; return { value, rawValue, label: alias[value] || value, }; }) : []; }, [context, jsonSchema]); const handleChange = useHandle((value) => { const convertedValues = value .map((v) => options.find((opt) => opt.value === v)?.rawValue) .filter((v) => v !== undefined); onChange(convertedValues); }); const initialValue = useMemo(() => { if (defaultValue == null) return undefined; return defaultValue.map((v) => '' + v); }, [defaultValue]); return (jsx(Checkbox.Group, { name: name, style: { display: 'flex' }, options: options, disabled: disabled, defaultValue: initialValue, onChange: handleChange })); }; const FormTypeInputStringCheckboxDefinition = { Component: FormTypeInputStringCheckbox, test: ({ type, formType, jsonSchema }) => { return (type === 'array' && formType === 'checkbox' && jsonSchema.items.type === 'string' && jsonSchema.items.enum?.length); }, }; const FormTypeInputStringEnum = ({ path, jsonSchema, disabled, defaultValue, onChange, context, size, }) => { const [schema, alias, mode] = useMemo(() => { const alias = context.enumLabels || jsonSchema.items?.options?.alias || jsonSchema.options?.alias; if (jsonSchema.type === 'array') return [jsonSchema.items, alias, 'multiple']; else return [jsonSchema, alias, undefined]; }, [context, jsonSchema]); const options = useMemo(() => { return schema.enum ? map(schema.enum, (rawValue) => { const value = '' + rawValue; return { value, rawValue, label: alias?.['' + value] || value }; }) : []; }, [alias, schema]); const Options = useMemo(() => { return map(options, ({ value, label }, index) => { return (jsx(Select.Option, { value: value, children: label }, index + value)); }); }, [options]); const initialValue = useMemo(() => { if (defaultValue === undefined) return undefined; if (Array.isArray(defaultValue)) return defaultValue.map((v) => '' + v); else return '' + defaultValue; }, [defaultValue]); const handleChange = useHandle((value) => { if (Array.isArray(value)) { const rawValues = value .map((v) => options.find((option) => option.value === v)?.rawValue) .filter((v) => v !== undefined); return onChange(rawValues); } else { const rawValue = options.find((option) => option.value === value)?.rawValue; if (rawValue === undefined) return; onChange(rawValue); } }); return (jsx(Select, { id: path, mode: mode, placeholder: jsonSchema.placeholder, disabled: disabled, defaultValue: initialValue, onChange: handleChange, style: { width: '100%' }, size: size || context?.size, children: Options })); }; const FormTypeInputStringEnumDefinition = { Component: FormTypeInputStringEnum, test: ({ type, jsonSchema }) => { return ((type === 'string' && jsonSchema.enum?.length) || (type === 'array' && jsonSchema.items.type === 'string' && jsonSchema.items.enum?.length)); }, }; const FormTypeInputStringSwitch = ({ path, jsonSchema, disabled, value, onChange, context, size, }) => { const [checked, unchecked] = useMemo(() => { const [checked, unchecked] = jsonSchema.enum || []; return [ checked !== undefined ? checked : 'on', unchecked !== undefined ? unchecked : 'off', ]; }, [jsonSchema]); const [checkedLabel, uncheckedLabel] = useMemo(() => { const alias = context.switchLabels || jsonSchema.options?.alias || {}; return [alias['' + checked] || checked, alias['' + unchecked] || unchecked]; }, [checked, unchecked, context, jsonSchema]); const handleChange = useHandle((input) => { onChange(input ? checked : unchecked); }); return (jsx(Switch, { disabled: disabled, checked: value === checked, checkedChildren: checkedLabel, unCheckedChildren: uncheckedLabel, onChange: handleChange, size: size || context?.switchSize }, path)); }; const FormTypeInputStringSwitchDefinition = { Component: FormTypeInputStringSwitch, test: ({ type, formType, jsonSchema }) => type === 'string' && formType === 'switch' && jsonSchema.enum?.length === 2, }; const FormTypeInputTextarea = ({ path, name, jsonSchema, disabled, readOnly, defaultValue, onChange, context, size, }) => { const handleChange = useHandle((value) => { onChange(value.target.value); }); const autoSize = useMemo(() => ({ minRows: jsonSchema.minRows, maxRows: jsonSchema.maxRows, }), [jsonSchema]); return (jsx(Input.TextArea, { id: path, name: name, disabled: disabled, readOnly: readOnly, autoSize: autoSize, placeholder: jsonSchema.placeholder, defaultValue: defaultValue ?? undefined, onChange: handleChange, size: size || context?.size })); }; const FormTypeInputTextareaDefinition = { Component: FormTypeInputTextarea, test: ({ type, format, formType }) => type === 'string' && (format === 'textarea' || formType === 'textarea'), }; const DATE_FORMAT$1 = 'HH:mm:00Z'; const FormTypeInputTime = ({ path, name, disabled, readOnly, defaultValue, onChange, context, size, }) => { const handleChange = useHandle((date) => { onChange(date?.format(DATE_FORMAT$1) || ''); }); const initialValue = useMemo(() => (defaultValue ? dayjs(defaultValue, DATE_FORMAT$1) : undefined), [defaultValue]); return (jsx(TimePicker, { id: path, name: name, disabled: disabled, readOnly: readOnly, format: 'HH:mm', defaultValue: initialValue, onChange: handleChange, size: size || context?.size })); }; const FormTypeInputTimeDefinition = { Component: FormTypeInputTime, test: { type: 'string', format: 'time', }, }; const DATE_FORMAT = 'HH:mm:00Z'; const FormTypeInputTimeRange = ({ path, name, jsonSchema, readOnly, disabled, defaultValue, onChange, context, size, }) => { const initialValue = useMemo(() => defaultValue?.length === 2 ? [ dayjs(defaultValue[0], DATE_FORMAT), dayjs(defaultValue[1], DATE_FORMAT), ] : undefined, [defaultValue]); const disabledDate = useMemo(() => { const { minimum, maximum } = jsonSchema.options || {}; return (date) => { if (!date) return false; if (!minimum && !maximum) return false; if (minimum && date.isBefore(dayjs(minimum, DATE_FORMAT), 'minute')) return true; if (maximum && date.isAfter(dayjs(maximum, DATE_FORMAT), 'minute')) return true; return false; }; }, [jsonSchema?.options]); const handleChange = useHandle((range) => { const [start, end] = range || []; if (!start || !end) return; onChange([start.format(DATE_FORMAT), end.format(DATE_FORMAT)]); }); return (jsx(DatePicker.RangePicker, { id: path, name: name, disabled: disabled, readOnly: readOnly, format: DATE_FORMAT, showTime: { format: 'HH:mm' }, disabledDate: disabledDate, defaultValue: initialValue, onChange: handleChange, onPanelChange: handleChange, size: size || context?.size })); }; const FormTypeInputTimeRangeDefinition = { Component: FormTypeInputTimeRange, test: ({ type, format, formType, jsonSchema }) => type === 'array' && (format === 'time-range' || formType === 'timeRange') && jsonSchema.items?.type === 'string', }; const DEFAULT_PROTOCOLS = ['http', 'https', 'ftp', 'mailto', 'tel']; 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, readOnly, disabled, defaultValue, onChange, context, size, }) => { const normalizedProtocols = useMemo(() => { const rawProtocols = jsonSchema.options?.protocols || DEFAULT_PROTOCOLS; return rawProtocols.map(normalizeProtocol); }, [jsonSchema]); const inputRef = 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 = useCallback((value) => { setProtocol(value); const currentUri = inputRef.current?.input?.value || ''; const separator = getProtocolSeparator(value); const newValue = currentUri ? `${value}${separator}${currentUri}` : `${value}${separator}`; onChange(newValue); }, [onChange]); const handleUriChange = useCallback((event) => { const newUri = event.target.value; const separator = getProtocolSeparator(protocol); const newValue = newUri ? `${protocol}${separator}${newUri}` : ''; onChange(newValue); }, [protocol, onChange]); const ProtocolDropdown = useMemo(() => { return (jsx(Select, { value: protocol, onChange: handleProtocolChange, disabled: disabled || readOnly, children: map(normalizedProtocols, (prot) => (jsx(Select.Option, { value: prot, children: formatProtocolDisplay(prot) }, prot))) })); }, [protocol, normalizedProtocols, disabled, readOnly, handleProtocolChange]); return (jsx(Input, { ref: inputRef, id: path, name: name, readOnly: readOnly, disabled: disabled, addonBefore: ProtocolDropdown, placeholder: jsonSchema.placeholder, defaultValue: initialUri, onChange: handleUriChange, size: size || context?.size })); }; const FormTypeInputUriDefinition = { Component: FormTypeInputUri, test: ({ type, format, formType }) => type === 'string' && (format === 'uri' || formType === 'uri'), }; const formTypeInputDefinitions = [ FormTypeInputBooleanSwitchDefinition, FormTypeInputStringCheckboxDefinition, FormTypeInputStringSwitchDefinition, FormTypeInputUriDefinition, FormTypeInputMonthDefinition, FormTypeInputDateDefinition, FormTypeInputTimeDefinition, FormTypeInputMonthRangeDefinition, FormTypeInputDateRangeDefinition, FormTypeInputTimeRangeDefinition, FormTypeInputRadioGroupDefinition, FormTypeInputStringEnumDefinition, FormTypeInputArrayDefinition, FormTypeInputSliderDefinition, FormTypeInputTextareaDefinition, FormTypeInputStringDefinition, FormTypeInputNumberDefinition, FormTypeInputBooleanDefinition, ]; const plugin = { FormGroup, FormLabel, FormInput, FormError, formTypeInputDefinitions, }; export { plugin };