1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-09-03 12:12:39 +02:00

Avoid TextString unmount when rendering zero-width strings

This commit is contained in:
Claudéric Demers
2023-03-02 13:45:50 -05:00
parent af3f828b12
commit efb58f3272

View File

@@ -25,7 +25,7 @@ const String = (props: {
// COMPAT: Render text inside void nodes with a zero-width space.
// So the node can contain selection but the text is not visible.
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-
@@ -37,14 +37,18 @@ const String = (props: {
!editor.isInline(parent) &&
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
// node, so we render a zero-width space so that the selection can be
// inserted next to it still.
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,
@@ -59,14 +63,52 @@ const String = (props: {
/**
* Leaf strings with text in them.
*/
const TextString = (props: { text: string; isTrailing?: boolean }) => {
const { text, isTrailing = false } = props
const TextString = (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 getTextContent = () => {
if (zeroWidth) {
return '\uFEFF'
}
return `${text ?? ''}${isTrailing ? '\n' : ''}`
}
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
// 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
@@ -77,6 +119,10 @@ const TextString = (props: { text: string; isTrailing?: boolean }) => {
// useLayoutEffect: updating our span before browser paint
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
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 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 (
<span {...attributes}>
{!IS_ANDROID || !isLineBreak ? '\uFEFF' : null}
{isLineBreak ? <br /> : null}
</span>
<MemoizedText ref={ref} {...attributes}>
{children}
</MemoizedText>
)
}
const MemoizedText = memo(
forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
({ children, ...props }, ref) => {
return (
<span {...props} ref={ref}>
{children}
</span>
)
}
)
)
export default String