Skip to content

Commit

Permalink
Add renderPlaceholder (#4190)
Browse files Browse the repository at this point in the history
  • Loading branch information
juliankrispel committed Apr 23, 2021
1 parent 3c5cb19 commit ea2eefe
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/few-planets-brush.md
@@ -0,0 +1,5 @@
---
'slate-react': patch
---

Added a `renderPlaceholder` prop to the `<Editable>` component for customizing how placeholders are rendered.
29 changes: 28 additions & 1 deletion packages/slate-react/src/components/editable.tsx
Expand Up @@ -34,9 +34,9 @@ import {
getDefaultView,
isDOMElement,
isDOMNode,
DOMStaticRange,
isPlainTextOnlyPaste,
} from '../utils/dom'

import {
EDITOR_TO_ELEMENT,
ELEMENT_TO_NODE,
Expand Down Expand Up @@ -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>

Expand All @@ -114,6 +115,7 @@ export const Editable = (props: EditableProps) => {
readOnly = false,
renderElement,
renderLeaf,
renderPlaceholder = props => <DefaultPlaceholder {...props} />,
style = {},
as: Component = 'div',
...attributes
Expand Down Expand Up @@ -225,6 +227,7 @@ export const Editable = (props: EditableProps) => {
scrollMode: 'if-needed',
boundary: el,
})
// @ts-ignore
delete leafEl.getBoundingClientRect
} else {
domSelection.removeAllRanges()
Expand Down Expand Up @@ -1046,6 +1049,7 @@ export const Editable = (props: EditableProps) => {
decorations,
node: editor,
renderElement,
renderPlaceholder,
renderLeaf,
selection: editor.selection,
})}
Expand All @@ -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.
*/
Expand Down
17 changes: 15 additions & 2 deletions packages/slate-react/src/components/element.tsx
Expand Up @@ -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.
Expand All @@ -25,13 +29,15 @@ 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
}) => {
const {
decorations,
element,
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
renderPlaceholder,
renderLeaf,
selection,
} = props
Expand All @@ -44,6 +50,7 @@ const Element = (props: {
decorations,
node: element,
renderElement,
renderPlaceholder,
renderLeaf,
selection,
})
Expand Down Expand Up @@ -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>
)

Expand Down
46 changes: 25 additions & 21 deletions packages/slate-react/src/components/leaf.tsx
Expand Up @@ -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
Expand All @@ -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
}) => {
Expand All @@ -23,6 +24,7 @@ const Leaf = (props: {
isLast,
text,
parent,
renderPlaceholder,
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
} = props

Expand All @@ -43,7 +45,7 @@ const Leaf = (props: {
return () => {
editorEl.style.minHeight = 'auto'
}
}, [placeholderRef])
}, [placeholderRef, leaf])

let children = (
<String
Expand All @@ -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>
)
Expand All @@ -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) &&
Expand Down
13 changes: 11 additions & 2 deletions packages/slate-react/src/components/text.tsx
Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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}
Expand Down
18 changes: 16 additions & 2 deletions packages/slate-react/src/hooks/use-children.tsx
Expand Up @@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -53,6 +65,7 @@ const useChildren = (props: {
element={n}
key={key.id}
renderElement={renderElement}
renderPlaceholder={renderPlaceholder}
renderLeaf={renderLeaf}
selection={sel}
/>
Expand All @@ -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}
/>
Expand Down
2 changes: 2 additions & 0 deletions packages/slate-react/src/index.ts
Expand Up @@ -3,6 +3,8 @@ export {
RenderElementProps,
RenderLeafProps,
Editable,
RenderPlaceholderProps,
DefaultPlaceholder,
} from './components/editable'
export { DefaultElement } from './components/element'
export { DefaultLeaf } from './components/leaf'
Expand Down
34 changes: 34 additions & 0 deletions site/examples/custom-placeholder.tsx
@@ -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
2 changes: 2 additions & 0 deletions site/pages/examples/[example].tsx
Expand Up @@ -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'
Expand All @@ -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 => (
Expand Down

0 comments on commit ea2eefe

Please sign in to comment.