mirror of https://github.com/msgbyte/tailchat
feat: tailchat-design增加富文本编辑器
parent
bf551557c1
commit
6177899b5f
@ -0,0 +1,51 @@
|
||||
import { isArray, ObjectMark, RemirrorJSON } from 'remirror';
|
||||
|
||||
/**
|
||||
* 转换成BBCode
|
||||
*/
|
||||
export function transformToBBCode(json: RemirrorJSON): string {
|
||||
if (json.type === 'doc') {
|
||||
return (json.content ?? []).map(transformToBBCode).join('\n');
|
||||
}
|
||||
|
||||
if (json.type === 'paragraph') {
|
||||
return (json.content ?? []).map(transformToBBCode).join('');
|
||||
}
|
||||
|
||||
if (json.type === 'text') {
|
||||
let text = json.text ?? '';
|
||||
|
||||
if (isArray(json.marks)) {
|
||||
(json.marks ?? []).forEach((mark) => {
|
||||
if (typeof mark === 'string') {
|
||||
mark = { type: mark };
|
||||
}
|
||||
text = applyMarks(mark, text);
|
||||
});
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 为text增加mark包裹
|
||||
*/
|
||||
function applyMarks(mark: ObjectMark, text: string): string {
|
||||
if (mark.type === 'bold') {
|
||||
return `[b]${text}[/b]`;
|
||||
}
|
||||
if (mark.type === 'underline') {
|
||||
return `[u]${text}[/u]`;
|
||||
}
|
||||
if (mark.type === 'italic') {
|
||||
return `[i]${text}[/i]`;
|
||||
}
|
||||
if (mark.type === 'code') {
|
||||
return `[code]${text}[/code]`;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
.remirror-editor-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tailchat-rich-editor {
|
||||
height: 100%;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.tailchat-rich-editor p {
|
||||
margin: 0;
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Remirror,
|
||||
useRemirror,
|
||||
OnChangeJSON,
|
||||
EditorComponent,
|
||||
} from '@remirror/react';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import type { RemirrorJSON } from 'remirror';
|
||||
import { Toolbar } from './toolbar';
|
||||
import { extensions } from './extensions';
|
||||
import { transformToBBCode } from './bbcode';
|
||||
import './editor.css';
|
||||
|
||||
interface RichEditorProps extends React.PropsWithChildren {
|
||||
initContent: string;
|
||||
onChange: (bbcode: string) => void;
|
||||
}
|
||||
export const RichEditor: React.FC<RichEditorProps> = React.memo((props) => {
|
||||
const { manager, state } = useRemirror({
|
||||
extensions,
|
||||
content: props.initContent,
|
||||
stringHandler: 'html',
|
||||
selection: 'end',
|
||||
});
|
||||
|
||||
const handleChange = useMemoizedFn((json: RemirrorJSON) => {
|
||||
props.onChange(transformToBBCode(json));
|
||||
});
|
||||
|
||||
return (
|
||||
<Remirror
|
||||
classNames={['tailchat-rich-editor']}
|
||||
manager={manager}
|
||||
initialContent={state}
|
||||
>
|
||||
<Toolbar />
|
||||
<EditorComponent />
|
||||
<OnChangeJSON onChange={handleChange} />
|
||||
{props.children}
|
||||
</Remirror>
|
||||
);
|
||||
});
|
||||
RichEditor.displayName = 'RichEditor';
|
@ -0,0 +1,16 @@
|
||||
import {
|
||||
BoldExtension,
|
||||
CodeExtension,
|
||||
ItalicExtension,
|
||||
UnderlineExtension,
|
||||
} from 'remirror/extensions';
|
||||
|
||||
/**
|
||||
* 富文本编辑器使用的拓展
|
||||
*/
|
||||
export const extensions = () => [
|
||||
new BoldExtension(),
|
||||
new ItalicExtension(),
|
||||
new UnderlineExtension(),
|
||||
new CodeExtension(),
|
||||
];
|
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { RichEditor } from './editor';
|
||||
|
||||
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
export default {
|
||||
title: 'Tailchat/RichEditor',
|
||||
component: RichEditor,
|
||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||
argTypes: {},
|
||||
} as ComponentMeta<typeof RichEditor>;
|
||||
|
||||
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
|
||||
const Template: ComponentStory<typeof RichEditor> = (args) => (
|
||||
<div style={{ height: 1000, width: '100%' }}>
|
||||
<RichEditor {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
// More on args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
Default.args = {
|
||||
initContent: '<p>Hi <strong>Friend</strong></p>',
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
/**
|
||||
* 富文本编辑器
|
||||
*/
|
||||
|
||||
export const RichEditor = React.lazy(() =>
|
||||
import('./editor').then((module) => ({ default: module.RichEditor }))
|
||||
);
|
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FloatingToolbar,
|
||||
CommandButtonGroup,
|
||||
ToggleBoldButton,
|
||||
ToggleItalicButton,
|
||||
ToggleUnderlineButton,
|
||||
ToggleCodeButton,
|
||||
} from '@remirror/react';
|
||||
|
||||
/**
|
||||
* 菜单
|
||||
*/
|
||||
export const Toolbar: React.FC = React.memo(() => {
|
||||
return (
|
||||
<FloatingToolbar>
|
||||
<CommandButtonGroup>
|
||||
<ToggleBoldButton />
|
||||
<ToggleItalicButton />
|
||||
<ToggleUnderlineButton />
|
||||
<ToggleCodeButton />
|
||||
</CommandButtonGroup>
|
||||
</FloatingToolbar>
|
||||
);
|
||||
});
|
||||
Toolbar.displayName = 'Toolbar';
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"importsNotUsedAsValues": "error",
|
||||
"experimentalDecorators": true,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue