1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-20 06:01:24 +02:00

Add renderPlaceholder (#4190)

This commit is contained in:
Julian Krispel-Samsel
2021-04-23 21:22:11 +01:00
committed by GitHub
parent 3c5cb198df
commit ea2eefefb8
9 changed files with 138 additions and 28 deletions

View File

@@ -0,0 +1,5 @@
---
'slate-react': patch
---
Added a `renderPlaceholder` prop to the `<Editable>` component for customizing how placeholders are rendered.

View File

@@ -34,9 +34,9 @@ import {
getDefaultView, getDefaultView,
isDOMElement, isDOMElement,
isDOMNode, isDOMNode,
DOMStaticRange,
isPlainTextOnlyPaste, isPlainTextOnlyPaste,
} from '../utils/dom' } from '../utils/dom'
import { import {
EDITOR_TO_ELEMENT, EDITOR_TO_ELEMENT,
ELEMENT_TO_NODE, ELEMENT_TO_NODE,
@@ -98,6 +98,7 @@ export type EditableProps = {
style?: React.CSSProperties style?: React.CSSProperties
renderElement?: (props: RenderElementProps) => JSX.Element renderElement?: (props: RenderElementProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element renderLeaf?: (props: RenderLeafProps) => JSX.Element
renderPlaceholder?: (props: RenderPlaceholderProps) => JSX.Element
as?: React.ElementType as?: React.ElementType
} & React.TextareaHTMLAttributes<HTMLDivElement> } & React.TextareaHTMLAttributes<HTMLDivElement>
@@ -114,6 +115,7 @@ export const Editable = (props: EditableProps) => {
readOnly = false, readOnly = false,
renderElement, renderElement,
renderLeaf, renderLeaf,
renderPlaceholder = props => <DefaultPlaceholder {...props} />,
style = {}, style = {},
as: Component = 'div', as: Component = 'div',
...attributes ...attributes
@@ -225,6 +227,7 @@ export const Editable = (props: EditableProps) => {
scrollMode: 'if-needed', scrollMode: 'if-needed',
boundary: el, boundary: el,
}) })
// @ts-ignore
delete leafEl.getBoundingClientRect delete leafEl.getBoundingClientRect
} else { } else {
domSelection.removeAllRanges() domSelection.removeAllRanges()
@@ -1046,6 +1049,7 @@ export const Editable = (props: EditableProps) => {
decorations, decorations,
node: editor, node: editor,
renderElement, renderElement,
renderPlaceholder,
renderLeaf, renderLeaf,
selection: editor.selection, selection: editor.selection,
})} })}
@@ -1055,6 +1059,29 @@ export const Editable = (props: EditableProps) => {
) )
} }
/**
* The props that get passed to renderPlaceholder
*/
export type RenderPlaceholderProps = {
children: any
attributes: {
'data-slate-placeholder': boolean
dir?: 'rtl'
contentEditable: boolean
ref: React.RefObject<any>
style: React.CSSProperties
}
}
/**
* The default placeholder element
*/
export const DefaultPlaceholder = ({
attributes,
children,
}: RenderPlaceholderProps) => <span {...attributes}>{children}</span>
/** /**
* A default memoized decorate function. * A default memoized decorate function.
*/ */

View File

@@ -15,7 +15,11 @@ import {
KEY_TO_ELEMENT, KEY_TO_ELEMENT,
} from '../utils/weak-maps' } from '../utils/weak-maps'
import { isDecoratorRangeListEqual } from '../utils/range-list' import { isDecoratorRangeListEqual } from '../utils/range-list'
import { RenderElementProps, RenderLeafProps } from './editable' import {
RenderElementProps,
RenderLeafProps,
RenderPlaceholderProps,
} from './editable'
/** /**
* Element. * Element.
@@ -25,6 +29,7 @@ const Element = (props: {
decorations: Range[] decorations: Range[]
element: SlateElement element: SlateElement
renderElement?: (props: RenderElementProps) => JSX.Element renderElement?: (props: RenderElementProps) => JSX.Element
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element renderLeaf?: (props: RenderLeafProps) => JSX.Element
selection: Range | null selection: Range | null
}) => { }) => {
@@ -32,6 +37,7 @@ const Element = (props: {
decorations, decorations,
element, element,
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />, renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
renderPlaceholder,
renderLeaf, renderLeaf,
selection, selection,
} = props } = props
@@ -44,6 +50,7 @@ const Element = (props: {
decorations, decorations,
node: element, node: element,
renderElement, renderElement,
renderPlaceholder,
renderLeaf, renderLeaf,
selection, selection,
}) })
@@ -98,7 +105,13 @@ const Element = (props: {
position: 'absolute', position: 'absolute',
}} }}
> >
<Text decorations={[]} isLast={false} parent={element} text={text} /> <Text
renderPlaceholder={renderPlaceholder}
decorations={[]}
isLast={false}
parent={element}
text={text}
/>
</Tag> </Tag>
) )

View File

@@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'
import { Element, Text } from 'slate' import { Element, Text } from 'slate'
import String from './string' import String from './string'
import { PLACEHOLDER_SYMBOL } from '../utils/weak-maps' import { PLACEHOLDER_SYMBOL } from '../utils/weak-maps'
import { RenderLeafProps } from './editable' import { RenderLeafProps, RenderPlaceholderProps } from './editable'
// auto-incrementing key for String component, force it refresh to // auto-incrementing key for String component, force it refresh to
// prevent inconsistent rendering by React with IME input // prevent inconsistent rendering by React with IME input
@@ -15,6 +15,7 @@ const Leaf = (props: {
isLast: boolean isLast: boolean
leaf: Text leaf: Text
parent: Element parent: Element
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element renderLeaf?: (props: RenderLeafProps) => JSX.Element
text: Text text: Text
}) => { }) => {
@@ -23,6 +24,7 @@ const Leaf = (props: {
isLast, isLast,
text, text,
parent, parent,
renderPlaceholder,
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />, renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
} = props } = props
@@ -43,7 +45,7 @@ const Leaf = (props: {
return () => { return () => {
editorEl.style.minHeight = 'auto' editorEl.style.minHeight = 'auto'
} }
}, [placeholderRef]) }, [placeholderRef, leaf])
let children = ( let children = (
<String <String
@@ -56,27 +58,28 @@ const Leaf = (props: {
) )
if (leaf[PLACEHOLDER_SYMBOL]) { if (leaf[PLACEHOLDER_SYMBOL]) {
const placeholderProps: RenderPlaceholderProps = {
children: leaf.placeholder,
attributes: {
'data-slate-placeholder': true,
style: {
position: 'absolute',
pointerEvents: 'none',
width: '100%',
maxWidth: '100%',
display: 'block',
opacity: '0.333',
userSelect: 'none',
textDecoration: 'none',
},
contentEditable: false,
ref: placeholderRef,
},
}
children = ( children = (
<React.Fragment> <React.Fragment>
<span {renderPlaceholder(placeholderProps)}
ref={placeholderRef}
contentEditable={false}
style={{
pointerEvents: 'none',
display: 'inline-block',
width: '100%',
maxWidth: '100%',
whiteSpace: 'nowrap',
opacity: '0.333',
userSelect: 'none',
fontStyle: 'normal',
fontWeight: 'normal',
textDecoration: 'none',
position: 'absolute',
}}
>
{leaf.placeholder}
</span>
{children} {children}
</React.Fragment> </React.Fragment>
) )
@@ -99,6 +102,7 @@ const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
next.parent === prev.parent && next.parent === prev.parent &&
next.isLast === prev.isLast && next.isLast === prev.isLast &&
next.renderLeaf === prev.renderLeaf && next.renderLeaf === prev.renderLeaf &&
next.renderPlaceholder === prev.renderPlaceholder &&
next.text === prev.text && next.text === prev.text &&
next.leaf.text === prev.leaf.text && next.leaf.text === prev.leaf.text &&
Text.matches(next.leaf, prev.leaf) && Text.matches(next.leaf, prev.leaf) &&

View File

@@ -3,7 +3,7 @@ import { Range, Element, Text as SlateText } from 'slate'
import Leaf from './leaf' import Leaf from './leaf'
import { ReactEditor, useSlateStatic } from '..' import { ReactEditor, useSlateStatic } from '..'
import { RenderLeafProps } from './editable' import { RenderLeafProps, RenderPlaceholderProps } from './editable'
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect' import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
import { import {
KEY_TO_ELEMENT, KEY_TO_ELEMENT,
@@ -20,10 +20,18 @@ const Text = (props: {
decorations: Range[] decorations: Range[]
isLast: boolean isLast: boolean
parent: Element parent: Element
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element renderLeaf?: (props: RenderLeafProps) => JSX.Element
text: SlateText text: SlateText
}) => { }) => {
const { decorations, isLast, parent, renderLeaf, text } = props const {
decorations,
isLast,
parent,
renderPlaceholder,
renderLeaf,
text,
} = props
const editor = useSlateStatic() const editor = useSlateStatic()
const ref = useRef<HTMLSpanElement>(null) const ref = useRef<HTMLSpanElement>(null)
const leaves = SlateText.decorations(text, decorations) const leaves = SlateText.decorations(text, decorations)
@@ -37,6 +45,7 @@ const Text = (props: {
<Leaf <Leaf
isLast={isLast && i === leaves.length - 1} isLast={isLast && i === leaves.length - 1}
key={`${key.id}-${i}`} key={`${key.id}-${i}`}
renderPlaceholder={renderPlaceholder}
leaf={leaf} leaf={leaf}
text={text} text={text}
parent={parent} parent={parent}

View File

@@ -7,7 +7,11 @@ import { ReactEditor } from '..'
import { useSlateStatic } from './use-slate-static' import { useSlateStatic } from './use-slate-static'
import { useDecorate } from './use-decorate' import { useDecorate } from './use-decorate'
import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps' import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'
import { RenderElementProps, RenderLeafProps } from '../components/editable' import {
RenderElementProps,
RenderLeafProps,
RenderPlaceholderProps,
} from '../components/editable'
/** /**
* Children. * Children.
@@ -17,10 +21,18 @@ const useChildren = (props: {
decorations: Range[] decorations: Range[]
node: Ancestor node: Ancestor
renderElement?: (props: RenderElementProps) => JSX.Element renderElement?: (props: RenderElementProps) => JSX.Element
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element renderLeaf?: (props: RenderLeafProps) => JSX.Element
selection: Range | null selection: Range | null
}) => { }) => {
const { decorations, node, renderElement, renderLeaf, selection } = props const {
decorations,
node,
renderElement,
renderPlaceholder,
renderLeaf,
selection,
} = props
const decorate = useDecorate() const decorate = useDecorate()
const editor = useSlateStatic() const editor = useSlateStatic()
const path = ReactEditor.findPath(editor, node) const path = ReactEditor.findPath(editor, node)
@@ -53,6 +65,7 @@ const useChildren = (props: {
element={n} element={n}
key={key.id} key={key.id}
renderElement={renderElement} renderElement={renderElement}
renderPlaceholder={renderPlaceholder}
renderLeaf={renderLeaf} renderLeaf={renderLeaf}
selection={sel} selection={sel}
/> />
@@ -64,6 +77,7 @@ const useChildren = (props: {
key={key.id} key={key.id}
isLast={isLeafBlock && i === node.children.length - 1} isLast={isLeafBlock && i === node.children.length - 1}
parent={node} parent={node}
renderPlaceholder={renderPlaceholder}
renderLeaf={renderLeaf} renderLeaf={renderLeaf}
text={n} text={n}
/> />

View File

@@ -3,6 +3,8 @@ export {
RenderElementProps, RenderElementProps,
RenderLeafProps, RenderLeafProps,
Editable, Editable,
RenderPlaceholderProps,
DefaultPlaceholder,
} from './components/editable' } from './components/editable'
export { DefaultElement } from './components/element' export { DefaultElement } from './components/element'
export { DefaultLeaf } from './components/leaf' export { DefaultLeaf } from './components/leaf'

View File

@@ -0,0 +1,34 @@
import React, { useState, useMemo } from 'react'
import { createEditor, Descendant } from 'slate'
import { Slate, Editable, withReact } from 'slate-react'
import { withHistory } from 'slate-history'
const PlainTextExample = () => {
const [value, setValue] = useState<Descendant[]>(initialValue)
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
return (
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
<Editable
placeholder="Type something"
renderPlaceholder={({ children, attributes }) => (
<div {...attributes}>
<p>{children}</p>
<pre>
Use the renderPlaceholder prop to customize rendering of the
placeholder
</pre>
</div>
)}
/>
</Slate>
)
}
const initialValue: Descendant[] = [
{
type: 'paragraph',
children: [{ text: '' }],
},
]
export default PlainTextExample

View File

@@ -26,6 +26,7 @@ import SearchHighlighting from '../../examples/search-highlighting'
import ShadowDOM from '../../examples/shadow-dom' import ShadowDOM from '../../examples/shadow-dom'
import Tables from '../../examples/tables' import Tables from '../../examples/tables'
import IFrames from '../../examples/iframe' import IFrames from '../../examples/iframe'
import CustomPlaceholder from '../../examples/custom-placeholder'
// node // node
import { getAllExamples } from '../api' import { getAllExamples } from '../api'
@@ -50,6 +51,7 @@ const EXAMPLES = [
['Shadow DOM', ShadowDOM, 'shadow-dom'], ['Shadow DOM', ShadowDOM, 'shadow-dom'],
['Tables', Tables, 'tables'], ['Tables', Tables, 'tables'],
['Rendering in iframes', IFrames, 'iframe'], ['Rendering in iframes', IFrames, 'iframe'],
['Custom placeholder', CustomPlaceholder, 'custom-placeholder'],
] ]
const Header = props => ( const Header = props => (