import { Button, Card, Stack, Text, Wrap } from '@chakra-ui/react';
import isHotkey from 'is-hotkey';
import React, { KeyboardEvent, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import { createEditor, Descendant, Editor, Element as SlateElement, Transforms } from 'slate';
import { withHistory } from 'slate-history';
import {
Editable,
RenderElementProps,
RenderLeafProps,
Slate,
useSlate,
withReact
} from 'slate-react';
import {
CustomEditor,
CustomElement,
CustomElementType,
CustomElementWithAlign,
CustomTextKey
} from '../types';
import { Prose } from './ui/prose';
export function EditorSlate({
initialValue,
onChange,
readOnly
}: {
readonly initialValue?: Descendant[];
readonly onChange?: (values: Descendant[]) => void;
readonly readOnly?: boolean;
}) {
return (
<>
{
if (onChange != null) onChange(values);
}}
readOnly={readOnly}
/>
>
);
}
const HOTKEYS: Record = {
'mod+b': 'bold',
'mod+i': 'italic',
'mod+u': 'underline',
'mod+`': 'code'
};
const LIST_TYPES = ['numbered-list', 'bulleted-list'] as const;
const TEXT_ALIGN_TYPES = ['left', 'center', 'right', 'justify'] as const;
type AlignType = (typeof TEXT_ALIGN_TYPES)[number];
type ListType = (typeof LIST_TYPES)[number];
type CustomElementFormat = CustomElementType | AlignType | ListType;
function EditorViewport({
initialValue,
onChange,
readOnly
}: {
readonly initialValue: Descendant[];
readonly onChange: (values: Descendant[]) => void;
readonly readOnly?: boolean;
}) {
const renderElement = useCallback((props: RenderElementProps) => , []);
const renderLeaf = useCallback((props: RenderLeafProps) => , []);
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
return (
{!readOnly && (
)}
) => {
for (const hotkey in HOTKEYS) {
if (isHotkey(hotkey, event as any)) {
event.preventDefault();
const mark = HOTKEYS[hotkey];
toggleMark(editor, mark);
}
}
}}
/>
);
}
function toggleBlock(editor: CustomEditor, format: CustomElementFormat) {
const isActive = isBlockActive(editor, format, isAlignType(format) ? 'align' : 'type');
const isList = isListType(format);
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
isListType(n.type) &&
!isAlignType(format),
split: true
});
let newProperties: Partial;
if (isAlignType(format)) {
newProperties = {
align: isActive ? undefined : format
};
} else {
newProperties = {
type: isActive ? 'paragraph' : isList ? 'list-item' : format
};
}
Transforms.setNodes(editor, newProperties);
if (!isActive && isList) {
const block = { type: format, children: [] };
Transforms.wrapNodes(editor, block);
}
}
function toggleMark(editor: CustomEditor, format: CustomTextKey) {
const isActive = isMarkActive(editor, format);
if (isActive) {
Editor.removeMark(editor, format);
} else {
Editor.addMark(editor, format, true);
}
}
function isBlockActive(
editor: CustomEditor,
format: CustomElementFormat,
blockType: 'type' | 'align' = 'type'
) {
const { selection } = editor;
if (!selection) return false;
const [match] = Array.from(
Editor.nodes(editor, {
at: Editor.unhangRange(editor, selection),
match: n => {
if (!Editor.isEditor(n) && SlateElement.isElement(n)) {
if (blockType === 'align' && isAlignElement(n)) {
return n.align === format;
}
return n.type === format;
}
return false;
}
})
);
return Boolean(match);
}
function isMarkActive(editor: CustomEditor, format: CustomTextKey) {
const marks = Editor.marks(editor);
return marks ? marks[format] === true : false;
}
function Element({ attributes, children, element }: RenderElementProps) {
const style: React.CSSProperties = {};
if (isAlignElement(element)) {
style.textAlign = element.align as AlignType;
}
switch (element.type) {
case 'block-quote':
return (
{children}
);
case 'bulleted-list':
return (
{children}
);
case 'heading-one':
return (
{children}
);
case 'heading-two':
return (
{children}
);
case 'list-item':
return (
{children}
);
case 'numbered-list':
return (
{children}
);
default:
return (
{children}
);
}
}
function Leaf({ attributes, children, leaf }: RenderLeafProps) {
if (leaf.bold) {
children = {children};
}
if (leaf.code) {
children = {children};
}
if (leaf.italic) {
children = {children};
}
if (leaf.underline) {
children = {children};
}
return {children};
}
interface BlockButtonProps {
format: CustomElementFormat;
icon: string;
text?: string;
}
function BlockButton({ format, icon, text }: BlockButtonProps) {
const editor = useSlate();
return (
);
}
interface MarkButtonProps {
format: CustomTextKey;
icon: string;
text?: string;
}
function MarkButton({ format, icon, text }: MarkButtonProps) {
const editor = useSlate();
return (
);
}
function isAlignType(format: CustomElementFormat): format is AlignType {
return TEXT_ALIGN_TYPES.includes(format as AlignType);
}
function isListType(format: CustomElementFormat): format is ListType {
return LIST_TYPES.includes(format as ListType);
}
function isAlignElement(element: CustomElement): element is CustomElementWithAlign {
return 'align' in element;
}