diff --git a/components/bubble/Bubble.tsx b/components/bubble/Bubble.tsx index 1ef00ed6..8591bb5f 100644 --- a/components/bubble/Bubble.tsx +++ b/components/bubble/Bubble.tsx @@ -4,6 +4,8 @@ import React from 'react'; import { Avatar } from 'antd'; import useXComponentConfig from '../_util/hooks/use-x-component-config'; import { useXProviderContext } from '../x-provider'; +import Editor from './Editor'; +import useMergedConfig from './hooks/useMergedConfig'; import useTypedEffect from './hooks/useTypedEffect'; import useTypingConfig from './hooks/useTypingConfig'; import type { BubbleProps } from './interface'; @@ -40,6 +42,7 @@ const Bubble: React.ForwardRefRenderFunction = (props, r onTypingComplete, header, footer, + editable = {}, ...otherHtmlProps } = props; @@ -60,6 +63,28 @@ const Bubble: React.ForwardRefRenderFunction = (props, r // ===================== Component Config ========================= const contextConfig = useXComponentConfig('bubble'); + // =========================== Editable =========================== + const [enableEdit, editConfig] = useMergedConfig(editable); + const [isEditing, setIsEditing] = React.useState(editConfig?.editing || false); + + React.useEffect(() => { + setIsEditing(editConfig?.editing || false); + }, [editConfig?.editing]); + + const onEditChange = (value: string) => { + editConfig?.onChange?.(value); + }; + + const onEditCancel = () => { + editConfig?.onCancel?.(); + setIsEditing(false); + }; + + const onEditEnd = (value: string) => { + editConfig?.onEnd?.(value); + setIsEditing(false); + }; + // ============================ Typing ============================ const [typingEnabled, typingStep, typingInterval] = useTypingConfig(typing); @@ -119,23 +144,43 @@ const Bubble: React.ForwardRefRenderFunction = (props, r contentNode = mergedContent as React.ReactNode; } - let fullContent: React.ReactNode = ( -
- {contentNode} -
- ); + let fullContent: React.ReactNode = + enableEdit && isEditing ? ( + + ) : ( +
+ {contentNode} +
+ ); if (header || footer) { fullContent = ( diff --git a/components/bubble/Editor.tsx b/components/bubble/Editor.tsx new file mode 100644 index 00000000..39a000f8 --- /dev/null +++ b/components/bubble/Editor.tsx @@ -0,0 +1,120 @@ +import classNames from 'classnames'; +import * as React from 'react'; + +import { Button, Flex, Input } from 'antd'; +import { TextAreaRef } from 'antd/lib/input/TextArea'; +import { EditConfig } from './interface'; +import useStyle from './style'; + +const { TextArea } = Input; + +interface EditableProps { + prefixCls: string; + value: string; + onChange?: (value: string) => void; + onCancel?: () => void; + onEnd?: (value: string) => void; + editorClassName?: string; + editorStyle?: React.CSSProperties; + editorTextAreaConfig?: EditConfig['editorTextAreaConfig']; + editorButtonConfig?: EditConfig['editorButtonConfig']; +} + +const Editor: React.FC = (props) => { + const { + prefixCls, + editorClassName: className, + editorStyle, + value, + onChange, + onCancel, + onEnd, + editorTextAreaConfig, + editorButtonConfig, + } = props; + const textAreaRef = React.useRef(null); + + const [current, setCurrent] = React.useState(value); + + React.useEffect(() => { + setCurrent(value); + }, [value]); + + React.useEffect(() => { + if (textAreaRef.current?.resizableTextArea) { + const { textArea } = textAreaRef.current.resizableTextArea; + textArea.focus(); + const { length } = textArea.value; + textArea.setSelectionRange(length, length); + } + }, []); + + const onTextAreaChange: React.ChangeEventHandler = ({ target }) => { + setCurrent(target.value.replace(/[\n\r]/g, '')); + onChange?.(target.value.replace(/[\n\r]/g, '')); + }; + + const confirmEnd = () => { + onEnd?.(current.trim()); + }; + + const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); + + const editorClassName = classNames( + prefixCls, + `${prefixCls}-editor`, + className, + hashId, + cssVarCls, + ); + + const CancelButton = () => + editorButtonConfig ? ( + editorButtonConfig + .filter((config) => config.type === 'cancel') + .map((config, index) => ( + + )) + ) : ( + + ); + const SaveButton = () => + editorButtonConfig ? ( + editorButtonConfig + .filter((config) => config.type === 'save') + .map((config, index) => ( + + )) + ) : ( + + ); + + return wrapCSSVar( +
+ +