diff --git a/components/bubble/Bubble.tsx b/components/bubble/Bubble.tsx index 1ef00ed6..e6f2c25c 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 useEditableConfig from './hooks/useEditableConfig'; 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,17 @@ const Bubble: React.ForwardRefRenderFunction = (props, r // ===================== Component Config ========================= const contextConfig = useXComponentConfig('bubble'); + // =========================== Editable =========================== + const { + enableEdit, + isEditing, + onEditorChange, + onEditorCancel, + onEditorEnd, + editorTextAreaConfig, + editorButtonsConfig, + } = useEditableConfig(editable); + // ============================ Typing ============================ const [typingEnabled, typingStep, typingInterval] = useTypingConfig(typing); @@ -119,23 +133,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..ee1e824e --- /dev/null +++ b/components/bubble/Editor.tsx @@ -0,0 +1,121 @@ +import classNames from 'classnames'; +import * as React from 'react'; + +import { Button, Flex, Input } from 'antd'; +import type { TextAreaRef } from 'antd/lib/input/TextArea'; +import type { EditConfig } from './interface'; +import useStyle from './style'; + +const { TextArea } = Input; + +interface EditableProps extends EditConfig { + prefixCls: string; + value: string; + onChange?: (value: string) => void; + onCancel?: () => void; + onEnd?: (value: string) => void; + className?: string; + style?: React.CSSProperties; + editorTextAreaConfig?: EditConfig['textarea']; + editorButtonsConfig?: EditConfig['buttons']; +} + +const Editor: React.FC = (props) => { + const { + prefixCls, + className, + style, + value, + onChange, + onCancel, + onEnd, + editorTextAreaConfig, + editorButtonsConfig, + } = 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 EditorButtons: React.FC = (editorButtonsConfig) => { + const defaultButtonsConfig = [ + { type: 'cancel', text: 'Cancel', option: {} }, + { type: 'save', text: 'Save', option: {} }, + ]; + + const buttonsConfig = + editorButtonsConfig && editorButtonsConfig.length > 0 + ? editorButtonsConfig + : defaultButtonsConfig; + + return buttonsConfig.map((config, index) => { + const { type, text, option } = config; + const handlers = { + cancel: onCancel, + save: confirmEnd, + }; + return ( + + ); + }); + }; + + return wrapCSSVar( +
+ +