Skip to content

Commit

Permalink
feat: Editable properties of bubble
Browse files Browse the repository at this point in the history
  • Loading branch information
tabzzz1 committed Dec 9, 2024
1 parent 32ddbe7 commit ade91c4
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 21 deletions.
79 changes: 62 additions & 17 deletions components/bubble/Bubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -40,6 +42,7 @@ const Bubble: React.ForwardRefRenderFunction<BubbleRef, BubbleProps> = (props, r
onTypingComplete,
header,
footer,
editable = {},
...otherHtmlProps
} = props;

Expand All @@ -60,6 +63,28 @@ const Bubble: React.ForwardRefRenderFunction<BubbleRef, BubbleProps> = (props, r
// ===================== Component Config =========================
const contextConfig = useXComponentConfig('bubble');

// =========================== Editable ===========================
const [enableEdit, editConfig] = useMergedConfig<BubbleProps['editable']>(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);

Expand Down Expand Up @@ -119,23 +144,43 @@ const Bubble: React.ForwardRefRenderFunction<BubbleRef, BubbleProps> = (props, r
contentNode = mergedContent as React.ReactNode;
}

let fullContent: React.ReactNode = (
<div
style={{
...contextConfig.styles.content,
...styles.content,
}}
className={classnames(
`${prefixCls}-content`,
`${prefixCls}-content-${variant}`,
shape && `${prefixCls}-content-${shape}`,
contextConfig.classNames.content,
classNames.content,
)}
>
{contentNode}
</div>
);
let fullContent: React.ReactNode =
enableEdit && isEditing ? (
<Editor
prefixCls={prefixCls}
value={mergedContent as string}
onChange={onEditChange}
onCancel={onEditCancel}
onEnd={onEditEnd}
editorStyle={{
...contextConfig.styles.editor,
...styles.editor,
}}
editorClassName={classnames(
`${prefixCls}-editor`,
contextConfig.classNames.editor,
classNames.editor,
)}
editorTextAreaConfig={editConfig?.editorTextAreaConfig}
editorButtonConfig={editConfig?.editorButtonConfig}
/>
) : (
<div
style={{
...contextConfig.styles.content,
...styles.content,
}}
className={classnames(
`${prefixCls}-content`,
`${prefixCls}-content-${variant}`,
shape && `${prefixCls}-content-${shape}`,
contextConfig.classNames.content,
classNames.content,
)}
>
{contentNode}
</div>
);

if (header || footer) {
fullContent = (
Expand Down
120 changes: 120 additions & 0 deletions components/bubble/Editor.tsx
Original file line number Diff line number Diff line change
@@ -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<EditableProps> = (props) => {
const {
prefixCls,
editorClassName: className,
editorStyle,
value,
onChange,
onCancel,
onEnd,
editorTextAreaConfig,
editorButtonConfig,
} = props;
const textAreaRef = React.useRef<TextAreaRef>(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<HTMLTextAreaElement> = ({ 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) => (
<Button key={index} size="small" {...config.option} onClick={onCancel}>
{config.text || 'Cancel'}
</Button>
))
) : (
<Button size="small" onClick={onCancel}>
Cancel
</Button>
);
const SaveButton = () =>
editorButtonConfig ? (
editorButtonConfig
.filter((config) => config.type === 'save')
.map((config, index) => (
<Button key={index} type="primary" size="small" {...config.option} onClick={confirmEnd}>
{config.text || 'Save'}
</Button>
))
) : (
<Button type="primary" size="small" onClick={confirmEnd}>
Save
</Button>
);

return wrapCSSVar(
<div className={editorClassName} style={editorStyle}>
<Flex gap="small" vertical flex="auto">
<TextArea
variant="borderless"
ref={textAreaRef}
value={current}
onChange={onTextAreaChange}
autoSize={{ minRows: 2, maxRows: 3 }}
{...editorTextAreaConfig}
/>
<Flex gap="small" justify="end">
<CancelButton />
<SaveButton />
</Flex>
</Flex>
</div>,
);
};

export default Editor;
7 changes: 7 additions & 0 deletions components/bubble/demo/editable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## zh-CN

通过设置 `editable` 属性,开启对 `content` 的编辑效果。

## en-US

Enable the editing effect of `content` by setting the `editable` property.
128 changes: 128 additions & 0 deletions components/bubble/demo/editable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {
DeleteOutlined,
EditOutlined,
LeftOutlined,
RightOutlined,
UserOutlined,
} from '@ant-design/icons';
import { Bubble } from '@ant-design/x';
import { Button, Flex } from 'antd';
import React from 'react';
import { EditConfig } from '../interface';

const fooAvatar: React.CSSProperties = {
color: '#f56a00',
backgroundColor: '#fde3cf',
};

const barAvatar: React.CSSProperties = {
color: '#fff',
backgroundColor: '#87d068',
};

const hideAvatar: React.CSSProperties = {
visibility: 'hidden',
};

const App = () => {
const [editing, setEditing] = React.useState(false);
const [currentIndex, setCurrentIndex] = React.useState(0);
const [editHistory, setEditHistory] = React.useState(['Good morning, how are you?']); // 编辑历史记录

const triggerEdit = () => {
setEditing((prev) => !prev);
};

const handleLeftClick = () => {
if (currentIndex > 0) {
setCurrentIndex((prev) => prev - 1);
}
};

const handleRightClick = () => {
if (currentIndex < editHistory.length - 1) {
setCurrentIndex((prev) => prev + 1);
}
};

const cancelEdit = () => {
setEditing(false);
};

const endEdit = (c: string) => {
setEditing(false);
setEditHistory((prev) => [...prev, c]);
setCurrentIndex(editHistory.length);
};

const editConfig: EditConfig = {
editing,
onCancel: cancelEdit,
onEnd: endEdit,
editorTextAreaConfig: { autoSize: { minRows: 2, maxRows: 4 } },
editorButtonConfig: [
{
type: 'cancel',
text: 'Cancel',
option: { size: 'small' },
},
{
type: 'save',
text: 'Save',
option: { size: 'small', type: 'primary' },
},
],
};

return (
<Flex gap="middle" vertical>
<Bubble
placement="end"
content={editHistory[currentIndex]}
editable={editConfig}
avatar={{ icon: <UserOutlined />, style: barAvatar }}
header={
editHistory.length > 1 &&
!editing && (
<Flex justify="end">
<Button
size="small"
type="text"
icon={<LeftOutlined />}
onClick={handleLeftClick}
disabled={currentIndex === 0}
/>
<span>{`${currentIndex + 1} / ${editHistory.length}`}</span>
<Button
size="small"
type="text"
icon={<RightOutlined />}
onClick={handleRightClick}
disabled={currentIndex === editHistory.length - 1}
/>
</Flex>
)
}
footer={
<Flex justify="end">
<Button size="small" type="text" icon={<DeleteOutlined />} />
<Button size="small" type="text" icon={<EditOutlined />} onClick={triggerEdit} />
</Flex>
}
/>
<Bubble
placement="start"
content="Hi, good morning, I'm fine!"
avatar={{ icon: <UserOutlined />, style: fooAvatar }}
/>
<Bubble
placement="start"
content="What a beautiful day!"
styles={{ avatar: hideAvatar }}
avatar={{}}
/>
</Flex>
);
};

export default App;
18 changes: 18 additions & 0 deletions components/bubble/hooks/useMergedConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react';

export default function useMergedConfig<Target>(
propConfig: any,
templateConfig?: Target,
): readonly [boolean, Target] {
return React.useMemo<readonly [boolean, Target]>(() => {
const support = !!propConfig;

return [
support,
{
...templateConfig,
...(support && typeof propConfig === 'object' ? propConfig : null),
},
] as const;
}, [propConfig]);
}
1 change: 1 addition & 0 deletions components/bubble/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Often used when chatting.
<code src="./demo/avatar-and-placement.tsx">Placement and avatar</code>
<code src="./demo/header-and-footer.tsx">Header and footer</code>
<code src="./demo/loading.tsx">Loading</code>
<code src="./demo/editable.tsx">Editing effect</code>
<code src="./demo/typing.tsx">Typing effect</code>
<code src="./demo/markdown.tsx">Content render</code>
<code src="./demo/variant.tsx">Variant</code>
Expand Down
1 change: 1 addition & 0 deletions components/bubble/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ demo:
<code src="./demo/avatar-and-placement.tsx">支持位置和头像</code>
<code src="./demo/header-and-footer.tsx">头和尾</code>
<code src="./demo/loading.tsx">加载中</code>
<code src="./demo/editable.tsx">编辑效果</code>
<code src="./demo/typing.tsx">打字效果</code>
<code src="./demo/markdown.tsx">自定义渲染</code>
<code src="./demo/variant.tsx">变体</code>
Expand Down
Loading

0 comments on commit ade91c4

Please sign in to comment.