mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-02-21 07:32:24 +01:00
Add renderPlaceholder (#4190)
This commit is contained in:
parent
3c5cb198df
commit
ea2eefefb8
5
.changeset/few-planets-brush.md
Normal file
5
.changeset/few-planets-brush.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
'slate-react': patch
|
||||
---
|
||||
|
||||
Added a `renderPlaceholder` prop to the `<Editable>` component for customizing how placeholders are rendered.
|
@ -34,9 +34,9 @@ import {
|
||||
getDefaultView,
|
||||
isDOMElement,
|
||||
isDOMNode,
|
||||
DOMStaticRange,
|
||||
isPlainTextOnlyPaste,
|
||||
} from '../utils/dom'
|
||||
|
||||
import {
|
||||
EDITOR_TO_ELEMENT,
|
||||
ELEMENT_TO_NODE,
|
||||
@ -98,6 +98,7 @@ export type EditableProps = {
|
||||
style?: React.CSSProperties
|
||||
renderElement?: (props: RenderElementProps) => JSX.Element
|
||||
renderLeaf?: (props: RenderLeafProps) => JSX.Element
|
||||
renderPlaceholder?: (props: RenderPlaceholderProps) => JSX.Element
|
||||
as?: React.ElementType
|
||||
} & React.TextareaHTMLAttributes<HTMLDivElement>
|
||||
|
||||
@ -114,6 +115,7 @@ export const Editable = (props: EditableProps) => {
|
||||
readOnly = false,
|
||||
renderElement,
|
||||
renderLeaf,
|
||||
renderPlaceholder = props => <DefaultPlaceholder {...props} />,
|
||||
style = {},
|
||||
as: Component = 'div',
|
||||
...attributes
|
||||
@ -225,6 +227,7 @@ export const Editable = (props: EditableProps) => {
|
||||
scrollMode: 'if-needed',
|
||||
boundary: el,
|
||||
})
|
||||
// @ts-ignore
|
||||
delete leafEl.getBoundingClientRect
|
||||
} else {
|
||||
domSelection.removeAllRanges()
|
||||
@ -1046,6 +1049,7 @@ export const Editable = (props: EditableProps) => {
|
||||
decorations,
|
||||
node: editor,
|
||||
renderElement,
|
||||
renderPlaceholder,
|
||||
renderLeaf,
|
||||
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.
|
||||
*/
|
||||
|
@ -15,7 +15,11 @@ import {
|
||||
KEY_TO_ELEMENT,
|
||||
} from '../utils/weak-maps'
|
||||
import { isDecoratorRangeListEqual } from '../utils/range-list'
|
||||
import { RenderElementProps, RenderLeafProps } from './editable'
|
||||
import {
|
||||
RenderElementProps,
|
||||
RenderLeafProps,
|
||||
RenderPlaceholderProps,
|
||||
} from './editable'
|
||||
|
||||
/**
|
||||
* Element.
|
||||
@ -25,6 +29,7 @@ const Element = (props: {
|
||||
decorations: Range[]
|
||||
element: SlateElement
|
||||
renderElement?: (props: RenderElementProps) => JSX.Element
|
||||
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
|
||||
renderLeaf?: (props: RenderLeafProps) => JSX.Element
|
||||
selection: Range | null
|
||||
}) => {
|
||||
@ -32,6 +37,7 @@ const Element = (props: {
|
||||
decorations,
|
||||
element,
|
||||
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
|
||||
renderPlaceholder,
|
||||
renderLeaf,
|
||||
selection,
|
||||
} = props
|
||||
@ -44,6 +50,7 @@ const Element = (props: {
|
||||
decorations,
|
||||
node: element,
|
||||
renderElement,
|
||||
renderPlaceholder,
|
||||
renderLeaf,
|
||||
selection,
|
||||
})
|
||||
@ -98,7 +105,13 @@ const Element = (props: {
|
||||
position: 'absolute',
|
||||
}}
|
||||
>
|
||||
<Text decorations={[]} isLast={false} parent={element} text={text} />
|
||||
<Text
|
||||
renderPlaceholder={renderPlaceholder}
|
||||
decorations={[]}
|
||||
isLast={false}
|
||||
parent={element}
|
||||
text={text}
|
||||
/>
|
||||
</Tag>
|
||||
)
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'
|
||||
import { Element, Text } from 'slate'
|
||||
import String from './string'
|
||||
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
|
||||
// prevent inconsistent rendering by React with IME input
|
||||
@ -15,6 +15,7 @@ const Leaf = (props: {
|
||||
isLast: boolean
|
||||
leaf: Text
|
||||
parent: Element
|
||||
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
|
||||
renderLeaf?: (props: RenderLeafProps) => JSX.Element
|
||||
text: Text
|
||||
}) => {
|
||||
@ -23,6 +24,7 @@ const Leaf = (props: {
|
||||
isLast,
|
||||
text,
|
||||
parent,
|
||||
renderPlaceholder,
|
||||
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
|
||||
} = props
|
||||
|
||||
@ -43,7 +45,7 @@ const Leaf = (props: {
|
||||
return () => {
|
||||
editorEl.style.minHeight = 'auto'
|
||||
}
|
||||
}, [placeholderRef])
|
||||
}, [placeholderRef, leaf])
|
||||
|
||||
let children = (
|
||||
<String
|
||||
@ -56,27 +58,28 @@ const Leaf = (props: {
|
||||
)
|
||||
|
||||
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 = (
|
||||
<React.Fragment>
|
||||
<span
|
||||
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>
|
||||
{renderPlaceholder(placeholderProps)}
|
||||
{children}
|
||||
</React.Fragment>
|
||||
)
|
||||
@ -99,6 +102,7 @@ const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
|
||||
next.parent === prev.parent &&
|
||||
next.isLast === prev.isLast &&
|
||||
next.renderLeaf === prev.renderLeaf &&
|
||||
next.renderPlaceholder === prev.renderPlaceholder &&
|
||||
next.text === prev.text &&
|
||||
next.leaf.text === prev.leaf.text &&
|
||||
Text.matches(next.leaf, prev.leaf) &&
|
||||
|
@ -3,7 +3,7 @@ import { Range, Element, Text as SlateText } from 'slate'
|
||||
|
||||
import Leaf from './leaf'
|
||||
import { ReactEditor, useSlateStatic } from '..'
|
||||
import { RenderLeafProps } from './editable'
|
||||
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
|
||||
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
|
||||
import {
|
||||
KEY_TO_ELEMENT,
|
||||
@ -20,10 +20,18 @@ const Text = (props: {
|
||||
decorations: Range[]
|
||||
isLast: boolean
|
||||
parent: Element
|
||||
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
|
||||
renderLeaf?: (props: RenderLeafProps) => JSX.Element
|
||||
text: SlateText
|
||||
}) => {
|
||||
const { decorations, isLast, parent, renderLeaf, text } = props
|
||||
const {
|
||||
decorations,
|
||||
isLast,
|
||||
parent,
|
||||
renderPlaceholder,
|
||||
renderLeaf,
|
||||
text,
|
||||
} = props
|
||||
const editor = useSlateStatic()
|
||||
const ref = useRef<HTMLSpanElement>(null)
|
||||
const leaves = SlateText.decorations(text, decorations)
|
||||
@ -37,6 +45,7 @@ const Text = (props: {
|
||||
<Leaf
|
||||
isLast={isLast && i === leaves.length - 1}
|
||||
key={`${key.id}-${i}`}
|
||||
renderPlaceholder={renderPlaceholder}
|
||||
leaf={leaf}
|
||||
text={text}
|
||||
parent={parent}
|
||||
|
@ -7,7 +7,11 @@ import { ReactEditor } from '..'
|
||||
import { useSlateStatic } from './use-slate-static'
|
||||
import { useDecorate } from './use-decorate'
|
||||
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.
|
||||
@ -17,10 +21,18 @@ const useChildren = (props: {
|
||||
decorations: Range[]
|
||||
node: Ancestor
|
||||
renderElement?: (props: RenderElementProps) => JSX.Element
|
||||
renderPlaceholder: (props: RenderPlaceholderProps) => JSX.Element
|
||||
renderLeaf?: (props: RenderLeafProps) => JSX.Element
|
||||
selection: Range | null
|
||||
}) => {
|
||||
const { decorations, node, renderElement, renderLeaf, selection } = props
|
||||
const {
|
||||
decorations,
|
||||
node,
|
||||
renderElement,
|
||||
renderPlaceholder,
|
||||
renderLeaf,
|
||||
selection,
|
||||
} = props
|
||||
const decorate = useDecorate()
|
||||
const editor = useSlateStatic()
|
||||
const path = ReactEditor.findPath(editor, node)
|
||||
@ -53,6 +65,7 @@ const useChildren = (props: {
|
||||
element={n}
|
||||
key={key.id}
|
||||
renderElement={renderElement}
|
||||
renderPlaceholder={renderPlaceholder}
|
||||
renderLeaf={renderLeaf}
|
||||
selection={sel}
|
||||
/>
|
||||
@ -64,6 +77,7 @@ const useChildren = (props: {
|
||||
key={key.id}
|
||||
isLast={isLeafBlock && i === node.children.length - 1}
|
||||
parent={node}
|
||||
renderPlaceholder={renderPlaceholder}
|
||||
renderLeaf={renderLeaf}
|
||||
text={n}
|
||||
/>
|
||||
|
@ -3,6 +3,8 @@ export {
|
||||
RenderElementProps,
|
||||
RenderLeafProps,
|
||||
Editable,
|
||||
RenderPlaceholderProps,
|
||||
DefaultPlaceholder,
|
||||
} from './components/editable'
|
||||
export { DefaultElement } from './components/element'
|
||||
export { DefaultLeaf } from './components/leaf'
|
||||
|
34
site/examples/custom-placeholder.tsx
Normal file
34
site/examples/custom-placeholder.tsx
Normal 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
|
@ -26,6 +26,7 @@ import SearchHighlighting from '../../examples/search-highlighting'
|
||||
import ShadowDOM from '../../examples/shadow-dom'
|
||||
import Tables from '../../examples/tables'
|
||||
import IFrames from '../../examples/iframe'
|
||||
import CustomPlaceholder from '../../examples/custom-placeholder'
|
||||
|
||||
// node
|
||||
import { getAllExamples } from '../api'
|
||||
@ -50,6 +51,7 @@ const EXAMPLES = [
|
||||
['Shadow DOM', ShadowDOM, 'shadow-dom'],
|
||||
['Tables', Tables, 'tables'],
|
||||
['Rendering in iframes', IFrames, 'iframe'],
|
||||
['Custom placeholder', CustomPlaceholder, 'custom-placeholder'],
|
||||
]
|
||||
|
||||
const Header = props => (
|
||||
|
Loading…
x
Reference in New Issue
Block a user