126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
import React, { useMemo, useCallback } from 'react';
|
|
import { createEditor, Descendant, Editor } from 'slate';
|
|
import {
|
|
Slate,
|
|
Editable,
|
|
withReact as withReactPlugin,
|
|
useSlate,
|
|
RenderElementProps,
|
|
RenderLeafProps,
|
|
} from 'slate-react';
|
|
import { withHistory } from 'slate-history';
|
|
import { CustomEditor, CustomTextKey } from './custom-types';
|
|
import {
|
|
BlockButton,
|
|
Button,
|
|
Icon,
|
|
Leaf,
|
|
SlateRenderElement,
|
|
Toolbar,
|
|
withHtml,
|
|
} from './slate-editor-components';
|
|
|
|
export interface RichTextEditorProps {
|
|
value: Descendant[];
|
|
onChange: (value: Descendant[]) => void;
|
|
placeholder?: string;
|
|
readOnly?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
interface MarkButtonProps {
|
|
format: CustomTextKey;
|
|
icon: string;
|
|
}
|
|
|
|
const isMarkActive = (editor: CustomEditor, format: CustomTextKey) => {
|
|
const marks = Editor.marks(editor);
|
|
return marks ? marks[format] === true : false;
|
|
};
|
|
|
|
const toggleMark = (editor: CustomEditor, format: CustomTextKey) => {
|
|
const isActive = isMarkActive(editor, format);
|
|
|
|
if (isActive) {
|
|
Editor.removeMark(editor, format);
|
|
} else {
|
|
Editor.addMark(editor, format, true);
|
|
}
|
|
};
|
|
|
|
const MarkButton = ({ format, icon }: MarkButtonProps) => {
|
|
const editor = useSlate();
|
|
return (
|
|
<Button
|
|
active={isMarkActive(editor, format)}
|
|
onPointerDown={(event: any) => event.preventDefault()}
|
|
onClick={() => toggleMark(editor, format)}
|
|
>
|
|
<Icon>{icon}</Icon>
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
|
value,
|
|
onChange,
|
|
placeholder = 'Type something...',
|
|
readOnly = false,
|
|
className = '',
|
|
}) => {
|
|
const editor = useMemo(() => withHtml(withHistory(withReactPlugin(createEditor()))), []);
|
|
|
|
const renderElement = useCallback(
|
|
(props: RenderElementProps) => <SlateRenderElement {...props} />,
|
|
[]
|
|
);
|
|
|
|
// const renderElement = useCallback((props: any) => {
|
|
// switch (props.element.type) {
|
|
// case "block-quote":
|
|
// return <blockquote {...props.attributes}>{props.children}</blockquote>;
|
|
// case "bulleted-list":
|
|
// return <ul {...props.attributes}>{props.children}</ul>;
|
|
// case "list-item":
|
|
// return <li {...props.attributes}>{props.children}</li>;
|
|
// case "numbered-list":
|
|
// return <ol {...props.attributes}>{props.children}</ol>;
|
|
// default:
|
|
// return <p {...props.attributes}>{props.children}</p>;
|
|
// }
|
|
// }, []);
|
|
|
|
const renderLeaf = useCallback((props: RenderLeafProps) => <Leaf {...props} />, []);
|
|
|
|
return (
|
|
<div className={`bg-gray-800 rounded-lg border border-gray-700 ${className}`}>
|
|
<Slate editor={editor} initialValue={value} onChange={onChange}>
|
|
<Toolbar>
|
|
<MarkButton format="bold" icon="format_bold" />
|
|
<MarkButton format="italic" icon="format_italic" />
|
|
<MarkButton format="underline" icon="format_underlined" />
|
|
<MarkButton format="code" icon="code" />
|
|
<BlockButton format="heading-one" icon="looks_one" />
|
|
<BlockButton format="heading-two" icon="looks_two" />
|
|
<BlockButton format="block-quote" icon="format_quote" />
|
|
<BlockButton format="numbered-list" icon="format_list_numbered" />
|
|
<BlockButton format="bulleted-list" icon="format_list_bulleted" />
|
|
<BlockButton format="left" icon="format_align_left" />
|
|
<BlockButton format="center" icon="format_align_center" />
|
|
<BlockButton format="right" icon="format_align_right" />
|
|
<BlockButton format="justify" icon="format_align_justify" />
|
|
</Toolbar>
|
|
<Editable
|
|
renderElement={renderElement}
|
|
renderLeaf={renderLeaf}
|
|
placeholder={placeholder}
|
|
readOnly={readOnly}
|
|
className="min-h-[200px] p-4 text-gray-100 focus:outline-none"
|
|
spellCheck
|
|
/>
|
|
</Slate>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RichTextEditor;
|