sunmao-sdk
Version:
榫卯-开箱即用赋能-sdk
410 lines (388 loc) • 11.9 kB
JSX
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">( {description} )</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>
);
};