kanban-app/frontend/src/components/RichTextEditor.tsx

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;