UNPKG

sunmao-sdk

Version:

榫卯-开箱即用赋能-sdk

410 lines (388 loc) 11.9 kB
import React, { useRef, useEffect, useState } from "react"; import PropTypes from "prop-types"; import { getValidateText } from "./validate"; import { usePrevious } from "../hooks"; import { isHidden, isDependShow } from "./isHidden"; import { isLooselyNumber, isCssLength, convertValue, isDeepEqual, getEnum } from "./utils"; import FoldIcon from "../components/foldIcon"; // asField拆分成逻辑组件和展示组件,从而可替换展示组件的方式完全插拔fr的样式 export const asField = ({ FieldUI, Widget }) => { let FieldContainer = ({ className, column, showValidate, isRoot, hidden, props, showDescIcon, width, labelWidth, disabled, readOnly, options, schema, isEditing, ...rest }) => { const firstRender = useRef(true); const fieldTouched = useRef(false); const { displayType, rootValue = {}, formData = {}, dependShow, value: _value } = rest; const prevValue = usePrevious(_value); // most key of schema, disabled, readOnly, options, hidden, support for function expression useEffect(() => { if (showValidate) return; // 首次渲染不做, TODO: 万一首次渲染是用户输入触发的呢? if (firstRender.current) { firstRender.current = false; return; } // 已经动过了就不用验证是否动过 if (fieldTouched.current === true) return; // 之后每次改动就算touch了,尽量避免多余的去使用isDeepEqual,大的复杂表单性能会不好 if (isDeepEqual(prevValue, _value)) return; fieldTouched.current = true; }, [_value]); let _hidden = hidden, _className = className, _disabled = disabled, _readOnly = readOnly, _options = options, _schema = schema; const convertValues = () => { _hidden = convertValue(hidden, formData, rootValue); if (_hidden !== undefined && typeof _hidden !== "boolean") { _hidden = isHidden({ hidden: _hidden, rootValue, formData }); } _className = convertValue(className, formData, rootValue); _disabled = convertValue(disabled, formData, rootValue); _readOnly = convertValue(readOnly, formData, rootValue); _options = { ...options }; try { Object.entries(_options).forEach(([key, _val]) => { try { if (key.includes("_antdProps")) { _options[key.split("_")[0]] = _val; } else { _options[key] = convertValue(_val, formData, rootValue); } } catch { _options[key] = convertValue(_val, formData, rootValue); } }); } catch (e) {} // iterate over schema, and convert every key _schema = { ...schema }; Object.keys(_schema).forEach(key => { const availableKey = [ "title", "description", "format", "minimum", "maximum", "minLength", "maxLength", "pattern", "message", "min", "max", "step", "enum", "enumNames" ]; // TODO: need to cover more if (availableKey.indexOf(key) > -1) { _schema[key] = convertValue(_schema[key], formData, rootValue); } }); }; // 在编辑时使用快照,否则正常计算 let screenShot = useRef(); const saveScreenShot = () => { screenShot.current = {}; screenShot.current.hidden = _hidden; screenShot.current.className = _className; screenShot.current.disabled = _disabled; screenShot.current.readOnly = _readOnly; screenShot.current.options = _options; screenShot.current.schema = _schema; }; const readScreenShot = () => { _hidden = screenShot.current.hidden; _className = screenShot.current.className; _disabled = screenShot.current.disabled; _readOnly = screenShot.current.readOnly; _options = screenShot.current.options; _schema = screenShot.current.schema; }; if (!isEditing || !screenShot.current) { convertValues(); saveScreenShot(); } else { readScreenShot(); } if (_hidden) { return null; } // 历史方法,不建议使用ui:dependShow, 一律使用ui:hidden if (isDependShow({ formData, dependShow })) { return null; } // 传入组件的值 const _rest = { ...rest, schema: _schema, disabled: _disabled, readOnly: _readOnly, options: _options, formData: formData || {}, rootValue: rootValue || {} }; let isComplex = _schema.type === "object" || (_schema.type === "array" && getEnum(_schema) === undefined); const isModal = options && (options.modal || options.drawer); if (isModal) { isComplex = false; } const validateText = showValidate || fieldTouched.current ? getValidateText(_rest) : ""; // 必填*,label,描述,竖排时的校验语,只要存在一个,label就不为空 const showLabel = _schema.title || rest.description || rest.required || (displayType !== "row" && validateText); let columnStyle = {}; if (!isComplex && width) { columnStyle = { width, paddingRight: "12px" }; } else if (!isComplex && column > 1) { columnStyle = { width: `calc(100% /${column})`, paddingRight: "12px" }; } const fieldProps = { className: _className, columnStyle, displayType, isComplex, isRequired: rest.required, isRoot, schema: _schema, showDescIcon, showLabel, showValidate, validateText, labelWidth }; return ( <FieldUI {...fieldProps}> <Widget {..._rest} invalid={validateText} /> </FieldUI> ); }; FieldContainer.propTypes = { showValidate: PropTypes.bool, column: PropTypes.number, isRoot: PropTypes.bool, props: PropTypes.object, showDescIcon: PropTypes.bool, displayType: PropTypes.string }; FieldContainer.defaultProps = { showValidate: true, column: 1, isRoot: false, props: {}, showDescIcon: false, displayType: "column" }; return FieldContainer; }; export const DefaultFieldUI = ({ children, className, columnStyle, // 处理组件宽度,外部一般不需修改 displayType, // 展示方式:row 横 column 竖 isComplex, // 是否是复杂结构:对象和对象数组 isRequired, // 是否是必填项 isRoot, schema, showDescIcon, showLabel, // 是否展示label showValidate, // 是否展示校验 validateText, // 校验文字 labelWidth // label的长度 }) => { // field 整体 label 标签 content 内容 const { title, type, enum: _enum, description = "", "ui:widget": widget, "ui:options": options } = schema; const [collapsed, setCollapsed] = useState(options && options.collapsed); // 一个object是否可以折叠,options里collapsed这个值,且这个值只能是true或者false,代表初始是展开还是收起 const toggleCollapsed = () => setCollapsed(!collapsed); const objectCanCollapse = type === "object" && options && [false, true].indexOf(options.collapsed) > -1; const isCheckbox = type === "boolean" && widget !== "switch"; const isModal = options && (options.modal || options.drawer); let fieldClass = `fr-field w-100 ${isComplex ? "fr-field-complex" : ""}`; let labelClass = "fr-label mb2"; let contentClass = "fr-content"; switch (type) { case "object": if (isModal) { break; } if (title) { labelClass += " fr-label-object bb b--black-20 pb1 mt2 mb3"; // fr-label-object 无默认style,只是占位用于使用者样式覆盖 } if (!isRoot) { fieldClass += " fr-field-object"; // object的margin bottom由内部元素撑起 if (title) { contentClass += " ml3"; // 缩进 } } break; case "array": if (isModal) { break; } if (title && getEnum(schema) === undefined) { labelClass += " fr-label-array mt2 mb3"; } break; case "boolean": if (isCheckbox) { if (title) { labelClass = labelClass.replace("mb2", "mb0"); } contentClass += " flex items-center"; // checkbox高度短,需要居中对齐 fieldClass += " flex items-center flex-row-reverse justify-end"; } break; default: if (displayType === "row") { labelClass = labelClass.replace("mb2", "mb0"); } } // 横排时 if (displayType === "row" && !isComplex && !isCheckbox) { fieldClass += " flex"; labelClass += " flex-shrink-0 fr-label-row"; labelClass = labelClass.replace("mb2", "mb0"); contentClass += " flex-grow-1 relative"; } // 横排的checkbox if (displayType === "row" && isCheckbox) { contentClass += " flex justify-end"; } const _labelWidth = isLooselyNumber(labelWidth) ? Number(labelWidth) : isCssLength(labelWidth) ? labelWidth : 110; // 默认是 110px 的长度 let labelStyle = { width: _labelWidth }; if (isCheckbox) { labelStyle = { flexGrow: 1 }; } else if (isComplex || displayType === "column") { labelStyle = { flexGrow: 1 }; } return ( <div className={className ? `${className} ${fieldClass}` : fieldClass} style={columnStyle} > {showLabel && ( <div className={labelClass} style={labelStyle}> <label className={`fr-label-title ${ isCheckbox || displayType === "column" ? "no-colon" : "" }`} // boolean不带冒号 title={title} > {objectCanCollapse && ( <FoldIcon style={{ marginRight: 6 }} fold={collapsed} onClick={toggleCollapsed} /> )} {isRequired && <span className="fr-label-required"> *</span>} <span className={`${isComplex ? "b" : ""} ${ displayType === "column" ? "flex-none" : "" }`} > {title} </span> {description && (showDescIcon ? ( <span className="fr-tooltip-toggle" aria-label={description}> <i className="fr-tooltip-icon" /> <div className="fr-tooltip-container"> <i className="fr-tooltip-triangle" /> {description} </div> </span> ) : ( <span className="fr-desc ml2">(&nbsp;{description}&nbsp;)</span> ))} {displayType !== "row" && validateText && ( <span className="fr-validate">{validateText}</span> )} </label> </div> )} {/* TODO: animation or maybe using antd Collapse */} {objectCanCollapse && collapsed ? null : ( <div className={contentClass} style={ isCheckbox ? displayType === "row" ? { marginLeft: _labelWidth } : {} : { flexGrow: 1 } } > <div className={`fr-children ${ isComplex ? "flex-column" : "items-center" }`} > {children} </div> <span className={`fr-validate fr-validate-row ${ isComplex ? "relative" : "absolute" }`} > {displayType === "row" && validateText ? validateText : ""} </span> </div> )} </div> ); };