diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 533845c366..5a980c239e 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -80,13 +80,33 @@ const Children = (props: Parameters[0]) => ( // The number of Editable components currently mounted. let mountedCount = 0 +/** + * `ElementType` represents the `type` string property on any `Element`. + */ + +export type ElementType = Element extends infer E + ? E extends { type: string } + ? E['type'] + : never + : never + +/** + * `TypedElement` represents the `Element` with a `type` property of `T`. + */ + +export type TypedElement = Element extends infer E + ? E extends { type: T } + ? E + : never + : never + /** * `RenderElementProps` are passed to the `renderElement` handler. */ -export interface RenderElementProps { +export interface RenderElementProps { children: any - element: Element + element: ElementType extends T ? Element : TypedElement attributes: { 'data-slate-node': 'element' 'data-slate-inline'?: true @@ -96,19 +116,71 @@ export interface RenderElementProps { } } +/** + * `ElementRenderer` renders an element. + */ + +export type ElementRenderer = ( + props: RenderElementProps +) => JSX.Element + +/** + * `ElementRenderers` maps element types to render functions. + */ + +export type ElementRenderers = { + [T in ElementType]?: ElementRenderer +} + +/** + * `TextType` represents the `type` string property on any `Text`. + */ + +export type TextType = Text extends infer T + ? T extends { type: string } + ? T['type'] + : never + : never + +/** + * `TypedText` represents the `Text` with a `type` property of `T`. + */ + +export type TypedText = Text extends infer L + ? L extends { type: T } + ? T + : never + : never + /** * `RenderLeafProps` are passed to the `renderLeaf` handler. */ -export interface RenderLeafProps { +export interface RenderLeafProps { children: any - leaf: Text - text: Text + leaf: TextType extends T ? Text : TypedText + text: TextType extends T ? Text : TypedText attributes: { 'data-slate-leaf': true } } +/** + * `LeafRenderer` renders a leaf. + */ + +export type LeafRenderer = ( + props: RenderLeafProps +) => JSX.Element + +/** + * `LeafRenderers` maps leaf types to render functions. + */ + +export type LeafRenderers = { + [T in TextType]?: LeafRenderer +} + /** * `EditableProps` are passed to the `` component. */ @@ -120,8 +192,10 @@ export type EditableProps = { readOnly?: boolean role?: string style?: React.CSSProperties - renderElement?: (props: RenderElementProps) => JSX.Element - renderLeaf?: (props: RenderLeafProps) => JSX.Element + renderElement?: + | ((props: RenderElementProps) => JSX.Element) + | ElementRenderers + renderLeaf?: ((props: RenderLeafProps) => JSX.Element) | LeafRenderers renderPlaceholder?: (props: RenderPlaceholderProps) => JSX.Element scrollSelectionIntoView?: (editor: ReactEditor, domRange: DOMRange) => void as?: React.ElementType diff --git a/packages/slate-react/src/components/element.tsx b/packages/slate-react/src/components/element.tsx index 52b1521031..7c379e1265 100644 --- a/packages/slate-react/src/components/element.tsx +++ b/packages/slate-react/src/components/element.tsx @@ -14,6 +14,8 @@ import { } from '../utils/weak-maps' import { isDecoratorRangeListEqual } from '../utils/range-list' import { + ElementRenderers, + LeafRenderers, RenderElementProps, RenderLeafProps, RenderPlaceholderProps, @@ -26,9 +28,11 @@ import { const Element = (props: { decorations: Range[] element: SlateElement - renderElement?: (props: RenderElementProps) => JSX.Element + renderElement?: + | ((props: RenderElementProps) => JSX.Element) + | ElementRenderers renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element - renderLeaf?: (props: RenderLeafProps) => JSX.Element + renderLeaf?: ((props: RenderLeafProps) => JSX.Element) | LeafRenderers selection: Range | null }) => { const { @@ -131,6 +135,18 @@ const Element = (props: { NODE_TO_PARENT.set(text, element) } + if (typeof renderElement !== 'function') { + const elementType = Reflect.get(element, 'type') + if (typeof elementType === 'string' && elementType in renderElement) { + return renderElement[elementType]({ attributes, children, element }) + } + return ( + + {children} + + ) + } + return renderElement({ attributes, children, element }) } diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index 823a56c9b7..ba69bdeb1e 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -6,7 +6,11 @@ import { EDITOR_TO_PLACEHOLDER_ELEMENT, EDITOR_TO_STYLE_ELEMENT, } from '../utils/weak-maps' -import { RenderLeafProps, RenderPlaceholderProps } from './editable' +import { + LeafRenderers, + RenderLeafProps, + RenderPlaceholderProps, +} from './editable' import { useSlateStatic } from '../hooks/use-slate-static' /** @@ -18,7 +22,7 @@ const Leaf = (props: { leaf: Text parent: Element renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element - renderLeaf?: (props: RenderLeafProps) => JSX.Element + renderLeaf?: ((props: RenderLeafProps) => JSX.Element) | LeafRenderers text: Text }) => { const { @@ -126,6 +130,18 @@ const Leaf = (props: { 'data-slate-leaf': true, } + if (typeof renderLeaf !== 'function') { + const leafType = Reflect.get(leaf, 'type') + if (typeof leafType === 'string' && leafType in renderLeaf) { + return renderLeaf[leafType]({ attributes, children, leaf, text }) + } + return ( + + {children} + + ) + } + return renderLeaf({ attributes, children, leaf, text }) } diff --git a/packages/slate-react/src/components/text.tsx b/packages/slate-react/src/components/text.tsx index 1e0a054379..937d4ffc09 100644 --- a/packages/slate-react/src/components/text.tsx +++ b/packages/slate-react/src/components/text.tsx @@ -8,7 +8,11 @@ import { ELEMENT_TO_NODE, NODE_TO_ELEMENT, } from '../utils/weak-maps' -import { RenderLeafProps, RenderPlaceholderProps } from './editable' +import { + LeafRenderers, + RenderLeafProps, + RenderPlaceholderProps, +} from './editable' import Leaf from './leaf' /** @@ -20,7 +24,7 @@ const Text = (props: { isLast: boolean parent: Element renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element - renderLeaf?: (props: RenderLeafProps) => JSX.Element + renderLeaf?: ((props: RenderLeafProps) => JSX.Element) | LeafRenderers text: SlateText }) => { const { diff --git a/packages/slate-react/src/hooks/use-children.tsx b/packages/slate-react/src/hooks/use-children.tsx index e9997b94a7..b4fe6135e2 100644 --- a/packages/slate-react/src/hooks/use-children.tsx +++ b/packages/slate-react/src/hooks/use-children.tsx @@ -8,6 +8,8 @@ import { useSlateStatic } from './use-slate-static' import { useDecorate } from './use-decorate' import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps' import { + ElementRenderers, + LeafRenderers, RenderElementProps, RenderLeafProps, RenderPlaceholderProps, @@ -21,9 +23,11 @@ import { SelectedContext } from './use-selected' const useChildren = (props: { decorations: Range[] node: Ancestor - renderElement?: (props: RenderElementProps) => JSX.Element + renderElement?: + | ((props: RenderElementProps) => JSX.Element) + | ElementRenderers renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element - renderLeaf?: (props: RenderLeafProps) => JSX.Element + renderLeaf?: ((props: RenderLeafProps) => JSX.Element) | LeafRenderers selection: Range | null }) => { const { diff --git a/packages/slate-react/src/index.ts b/packages/slate-react/src/index.ts index d8cb331f21..dcf7bbb1be 100644 --- a/packages/slate-react/src/index.ts +++ b/packages/slate-react/src/index.ts @@ -1,6 +1,8 @@ // Components export { Editable, + ElementRenderer, + ElementRenderers, RenderElementProps, RenderLeafProps, RenderPlaceholderProps, diff --git a/site/examples/inlines.tsx b/site/examples/inlines.tsx index db1d20055a..1c9d0ae645 100644 --- a/site/examples/inlines.tsx +++ b/site/examples/inlines.tsx @@ -98,7 +98,10 @@ const InlinesExample = () => { } + renderElement={{ + button: EditableButtonComponent, + link: LinkComponent, + }} renderLeaf={props => } placeholder="Enter some text..." onKeyDown={onKeyDown} @@ -230,7 +233,11 @@ const InlineChromiumBugfix = () => ( ) -const LinkComponent = ({ attributes, children, element }) => { +const LinkComponent: SlateReact.ElementRenderer<'link'> = ({ + attributes, + children, + element, +}) => { const selected = useSelected() return ( { ) } -const EditableButtonComponent = ({ attributes, children }) => { +const EditableButtonComponent: SlateReact.ElementRenderer<'button'> = ({ + attributes, + children, +}) => { return ( /* Note that this is not a true button, but a span with button-like CSS. @@ -283,18 +293,6 @@ const EditableButtonComponent = ({ attributes, children }) => { ) } -const Element = props => { - const { attributes, children, element } = props - switch (element.type) { - case 'link': - return - case 'button': - return - default: - return

{children}

- } -} - const Text = props => { const { attributes, children, leaf } = props return (