mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-09-08 22:20:41 +02:00
Avoid TextString
unmount when rendering zero-width strings
This commit is contained in:
@@ -25,7 +25,7 @@ const String = (props: {
|
|||||||
// COMPAT: Render text inside void nodes with a zero-width space.
|
// COMPAT: Render text inside void nodes with a zero-width space.
|
||||||
// So the node can contain selection but the text is not visible.
|
// So the node can contain selection but the text is not visible.
|
||||||
if (editor.isVoid(parent)) {
|
if (editor.isVoid(parent)) {
|
||||||
return <ZeroWidthString length={Node.string(parent).length} />
|
return <TextString text="" zeroWidth length={Node.string(parent).length} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// COMPAT: If this is the last text node in an empty block, render a zero-
|
// COMPAT: If this is the last text node in an empty block, render a zero-
|
||||||
@@ -37,14 +37,18 @@ const String = (props: {
|
|||||||
!editor.isInline(parent) &&
|
!editor.isInline(parent) &&
|
||||||
Editor.string(editor, parentPath) === ''
|
Editor.string(editor, parentPath) === ''
|
||||||
) {
|
) {
|
||||||
return <ZeroWidthString isLineBreak isMarkPlaceholder={isMarkPlaceholder} />
|
return (
|
||||||
|
<TextString text="" isLineBreak isMarkPlaceholder={isMarkPlaceholder} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// COMPAT: If the text is empty, it's because it's on the edge of an inline
|
// COMPAT: If the text is empty, it's because it's on the edge of an inline
|
||||||
// node, so we render a zero-width space so that the selection can be
|
// node, so we render a zero-width space so that the selection can be
|
||||||
// inserted next to it still.
|
// inserted next to it still.
|
||||||
if (leaf.text === '') {
|
if (leaf.text === '') {
|
||||||
return <ZeroWidthString isMarkPlaceholder={isMarkPlaceholder} />
|
return (
|
||||||
|
<TextString text="" zeroWidth isMarkPlaceholder={isMarkPlaceholder} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// COMPAT: Browsers will collapse trailing new lines at the end of blocks,
|
// COMPAT: Browsers will collapse trailing new lines at the end of blocks,
|
||||||
@@ -59,14 +63,52 @@ const String = (props: {
|
|||||||
/**
|
/**
|
||||||
* Leaf strings with text in them.
|
* Leaf strings with text in them.
|
||||||
*/
|
*/
|
||||||
const TextString = (props: { text: string; isTrailing?: boolean }) => {
|
const TextString = (props: {
|
||||||
const { text, isTrailing = false } = props
|
text: string
|
||||||
|
isTrailing?: boolean
|
||||||
|
zeroWidth?: boolean
|
||||||
|
length?: number
|
||||||
|
isLineBreak?: boolean
|
||||||
|
isMarkPlaceholder?: boolean
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
text,
|
||||||
|
isTrailing = false,
|
||||||
|
zeroWidth,
|
||||||
|
length,
|
||||||
|
isLineBreak,
|
||||||
|
isMarkPlaceholder,
|
||||||
|
} = props
|
||||||
const ref = useRef<HTMLSpanElement>(null)
|
const ref = useRef<HTMLSpanElement>(null)
|
||||||
const getTextContent = () => {
|
const getTextContent = () => {
|
||||||
|
if (zeroWidth) {
|
||||||
|
return '\uFEFF'
|
||||||
|
}
|
||||||
|
|
||||||
return `${text ?? ''}${isTrailing ? '\n' : ''}`
|
return `${text ?? ''}${isTrailing ? '\n' : ''}`
|
||||||
}
|
}
|
||||||
const [initialText] = useState(getTextContent)
|
const [initialText] = useState(getTextContent)
|
||||||
|
|
||||||
|
let attributes: Record<string, any> = {
|
||||||
|
'data-slate-string': true,
|
||||||
|
}
|
||||||
|
let children: React.ReactNode = initialText
|
||||||
|
|
||||||
|
if (zeroWidth || isLineBreak) {
|
||||||
|
attributes = {
|
||||||
|
'data-slate-zero-width': isLineBreak ? 'n' : 'z',
|
||||||
|
'data-slate-length': length ?? 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLineBreak) {
|
||||||
|
children = <br />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMarkPlaceholder) {
|
||||||
|
attributes['data-slate-mark-placeholder'] = true
|
||||||
|
}
|
||||||
|
|
||||||
// This is the actual text rendering boundary where we interface with the DOM
|
// This is the actual text rendering boundary where we interface with the DOM
|
||||||
// The text is not rendered as part of the virtual DOM, as since we handle basic character insertions natively,
|
// The text is not rendered as part of the virtual DOM, as since we handle basic character insertions natively,
|
||||||
// updating the DOM is not a one way dataflow anymore. What we need here is not reconciliation and diffing
|
// updating the DOM is not a one way dataflow anymore. What we need here is not reconciliation and diffing
|
||||||
@@ -77,6 +119,10 @@ const TextString = (props: { text: string; isTrailing?: boolean }) => {
|
|||||||
|
|
||||||
// useLayoutEffect: updating our span before browser paint
|
// useLayoutEffect: updating our span before browser paint
|
||||||
useIsomorphicLayoutEffect(() => {
|
useIsomorphicLayoutEffect(() => {
|
||||||
|
if (isLineBreak) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// null coalescing text to make sure we're not outputing "null" as a string in the extreme case it is nullish at runtime
|
// null coalescing text to make sure we're not outputing "null" as a string in the extreme case it is nullish at runtime
|
||||||
const textWithTrailing = getTextContent()
|
const textWithTrailing = getTextContent()
|
||||||
|
|
||||||
@@ -90,45 +136,23 @@ const TextString = (props: { text: string; isTrailing?: boolean }) => {
|
|||||||
|
|
||||||
// We intentionally render a memoized <span> that only receives the initial text content when the component is mounted.
|
// We intentionally render a memoized <span> that only receives the initial text content when the component is mounted.
|
||||||
// We defer to the layout effect above to update the `textContent` of the span element when needed.
|
// We defer to the layout effect above to update the `textContent` of the span element when needed.
|
||||||
return <MemoizedText ref={ref}>{initialText}</MemoizedText>
|
|
||||||
}
|
|
||||||
|
|
||||||
const MemoizedText = memo(
|
|
||||||
forwardRef<HTMLSpanElement, { children: string }>((props, ref) => {
|
|
||||||
return (
|
|
||||||
<span data-slate-string ref={ref}>
|
|
||||||
{props.children}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leaf strings without text, render as zero-width strings.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const ZeroWidthString = (props: {
|
|
||||||
length?: number
|
|
||||||
isLineBreak?: boolean
|
|
||||||
isMarkPlaceholder?: boolean
|
|
||||||
}) => {
|
|
||||||
const { length = 0, isLineBreak = false, isMarkPlaceholder = false } = props
|
|
||||||
|
|
||||||
const attributes = {
|
|
||||||
'data-slate-zero-width': isLineBreak ? 'n' : 'z',
|
|
||||||
'data-slate-length': length,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMarkPlaceholder) {
|
|
||||||
attributes['data-slate-mark-placeholder'] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span {...attributes}>
|
<MemoizedText ref={ref} {...attributes}>
|
||||||
{!IS_ANDROID || !isLineBreak ? '\uFEFF' : null}
|
{children}
|
||||||
{isLineBreak ? <br /> : null}
|
</MemoizedText>
|
||||||
</span>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MemoizedText = memo(
|
||||||
|
forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
|
||||||
|
({ children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<span {...props} ref={ref}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
export default String
|
export default String
|
||||||
|
Reference in New Issue
Block a user