|
|
@@ -0,0 +1,189 @@
|
|
|
+import React, { useLayoutEffect, useRef, useState } from 'react';
|
|
|
+import { isVoidField, Field } from '@formily/core';
|
|
|
+import { useField, observer } from '@formily/react';
|
|
|
+import { Popover } from 'antd';
|
|
|
+import { EditOutlined, CloseOutlined, MessageOutlined } from '@ant-design/icons';
|
|
|
+import { BaseItem, IFormItemProps } from '@formily/antd/lib/form-item';
|
|
|
+import { PopoverProps } from 'antd/lib/popover';
|
|
|
+import { useClickAway, usePrefixCls } from '@formily/antd/lib/__builtins__';
|
|
|
+import cls from 'classnames';
|
|
|
+import { get } from 'lodash';
|
|
|
+/**
|
|
|
+ * 默认Inline展示
|
|
|
+ */
|
|
|
+
|
|
|
+type IPopoverProps = PopoverProps;
|
|
|
+
|
|
|
+type ComposedEditable = React.FC<React.PropsWithChildren<IFormItemProps>> & {
|
|
|
+ Popover?: React.FC<React.PropsWithChildren<IPopoverProps & { title?: React.ReactNode }>>;
|
|
|
+};
|
|
|
+
|
|
|
+const useParentPattern = () => {
|
|
|
+ const field = useField<Field>();
|
|
|
+ return field?.parent?.pattern || field?.form?.pattern;
|
|
|
+};
|
|
|
+
|
|
|
+const useEditable = (): [boolean, (payload: boolean) => void] => {
|
|
|
+ const pattern = useParentPattern();
|
|
|
+ const field = useField<Field>();
|
|
|
+ useLayoutEffect(() => {
|
|
|
+ if (pattern === 'editable') {
|
|
|
+ return field.setPattern('readPretty');
|
|
|
+ }
|
|
|
+ }, [pattern]);
|
|
|
+ return [
|
|
|
+ field.pattern === 'editable',
|
|
|
+ (payload: boolean) => {
|
|
|
+ if (pattern !== 'editable') return;
|
|
|
+ field.setPattern(payload ? 'editable' : 'readPretty');
|
|
|
+ },
|
|
|
+ ];
|
|
|
+};
|
|
|
+
|
|
|
+const useFormItemProps = (): IFormItemProps => {
|
|
|
+ const field = useField();
|
|
|
+ if (isVoidField(field)) return {};
|
|
|
+ if (!field) return {};
|
|
|
+ const takeMessage = () => {
|
|
|
+ if (field.selfErrors.length) return field.selfErrors;
|
|
|
+ if (field.selfWarnings.length) return field.selfWarnings;
|
|
|
+ if (field.selfSuccesses.length) return field.selfSuccesses;
|
|
|
+ };
|
|
|
+
|
|
|
+ return {
|
|
|
+ feedbackStatus: field.validateStatus === 'validating' ? 'pending' : field.validateStatus,
|
|
|
+ feedbackText: takeMessage(),
|
|
|
+ extra: field.description,
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+export const Editable: ComposedEditable = observer((props) => {
|
|
|
+ const [editable, setEditable] = useEditable();
|
|
|
+ const pattern = useParentPattern();
|
|
|
+ const itemProps = useFormItemProps();
|
|
|
+ const field = useField<Field>();
|
|
|
+ const basePrefixCls = usePrefixCls();
|
|
|
+ const prefixCls = usePrefixCls('formily-editable');
|
|
|
+ const ref = useRef<boolean>();
|
|
|
+ const innerRef = useRef<HTMLDivElement | any>();
|
|
|
+ const recover = () => {
|
|
|
+ if (ref.current && !field?.errors?.length) {
|
|
|
+ setEditable(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const renderEditHelper = () => {
|
|
|
+ if (editable) return;
|
|
|
+ return (
|
|
|
+ <BaseItem {...props} {...itemProps}>
|
|
|
+ {pattern === 'editable' && <EditOutlined className={`${prefixCls}-edit-btn`} />}
|
|
|
+ {pattern !== 'editable' && <MessageOutlined className={`${prefixCls}-edit-btn`} />}
|
|
|
+ </BaseItem>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderCloseHelper = () => {
|
|
|
+ if (!editable) return;
|
|
|
+ return (
|
|
|
+ <BaseItem {...props}>
|
|
|
+ <CloseOutlined className={`${prefixCls}-close-btn`} />
|
|
|
+ </BaseItem>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ useClickAway((e) => {
|
|
|
+ const target = e.target as HTMLElement;
|
|
|
+ if (target?.closest(`.${basePrefixCls}-select-dropdown`)) return;
|
|
|
+ if (target?.closest(`.${basePrefixCls}-picker-dropdown`)) return;
|
|
|
+ if (target?.closest(`.${basePrefixCls}-cascader-menus`)) return;
|
|
|
+ recover();
|
|
|
+ }, innerRef);
|
|
|
+
|
|
|
+ const onClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
|
+ const target = e.target as HTMLElement;
|
|
|
+ const close = innerRef.current?.querySelector(`.${prefixCls}-close-btn`);
|
|
|
+ if (target?.contains(close) || close?.contains(target)) {
|
|
|
+ recover();
|
|
|
+ } else if (!ref.current) {
|
|
|
+ setTimeout(() => {
|
|
|
+ setEditable(true);
|
|
|
+ setTimeout(() => {
|
|
|
+ innerRef.current?.querySelector('input')?.focus();
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ ref.current = editable;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className={prefixCls} ref={innerRef} onClick={onClick}>
|
|
|
+ <div className={`${prefixCls}-content`}>
|
|
|
+ <BaseItem {...props} {...itemProps}>
|
|
|
+ {props.children}
|
|
|
+ </BaseItem>
|
|
|
+ {renderEditHelper()}
|
|
|
+ {renderCloseHelper()}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+});
|
|
|
+
|
|
|
+Editable.Popover = observer((props) => {
|
|
|
+ const field = useField<Field>();
|
|
|
+ // console.log(field.path.segments)
|
|
|
+ // console.log(field.form.query(field.path).pattern.segments)
|
|
|
+ const pattern = useParentPattern();
|
|
|
+ let title = props.title || field.title;
|
|
|
+ const [visible, setVisible] = useState(false);
|
|
|
+ const prefixCls = usePrefixCls('formily-editable');
|
|
|
+ const closePopover = async () => {
|
|
|
+ try {
|
|
|
+ await field.form.validate(`${field.address}.*`);
|
|
|
+ } finally {
|
|
|
+ const errors = field.form.queryFeedbacks({
|
|
|
+ type: 'error',
|
|
|
+ address: `${field.address}.*`,
|
|
|
+ });
|
|
|
+ if (errors?.length) return;
|
|
|
+ setVisible(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ const openPopover = () => {
|
|
|
+ setVisible(true);
|
|
|
+ };
|
|
|
+ if (field.title === '配置参数' && !props.title) {
|
|
|
+ const path = field.path.segments.filter((key: any) => key !== 'config');
|
|
|
+ const value = get(field.form.values, path)?.name;
|
|
|
+ title = value || '配置参数';
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ <Popover
|
|
|
+ {...props}
|
|
|
+ title={props.title || field.title}
|
|
|
+ visible={visible}
|
|
|
+ className={cls(prefixCls, props.className)}
|
|
|
+ content={props.children}
|
|
|
+ trigger="click"
|
|
|
+ destroyTooltipOnHide
|
|
|
+ onVisibleChange={(param) => {
|
|
|
+ if (param) {
|
|
|
+ openPopover();
|
|
|
+ } else {
|
|
|
+ closePopover();
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div>
|
|
|
+ <BaseItem className={`${prefixCls}-trigger`}>
|
|
|
+ <div className={`${prefixCls}-content`}>
|
|
|
+ <span className={`${prefixCls}-preview`}>{title}</span>
|
|
|
+ {pattern === 'editable' && <EditOutlined className={`${prefixCls}-edit-btn`} />}
|
|
|
+ {pattern !== 'editable' && <MessageOutlined className={`${prefixCls}-edit-btn`} />}
|
|
|
+ </div>
|
|
|
+ </BaseItem>
|
|
|
+ </div>
|
|
|
+ </Popover>
|
|
|
+ );
|
|
|
+});
|
|
|
+
|
|
|
+export default Editable;
|