1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-21 06:31:28 +02:00

Simplify implementation of custom editor styling (#5278)

* Switch back to using inline styles for default editor styles

* Add example page and test for editor styling

* Add section in docs for editor styling

* Add test for editor height being set to placeholder height

* Add changeset
This commit is contained in:
Kyle McLean
2023-01-31 19:17:27 -07:00
committed by GitHub
parent 0f83810704
commit 9c4097a26f
13 changed files with 257 additions and 94 deletions

View File

@@ -54,7 +54,7 @@ import {
EDITOR_TO_ELEMENT,
EDITOR_TO_FORCE_RENDER,
EDITOR_TO_PENDING_INSERTION_MARKS,
EDITOR_TO_STYLE_ELEMENT,
EDITOR_TO_PLACEHOLDER_ELEMENT,
EDITOR_TO_USER_MARKS,
EDITOR_TO_USER_SELECTION,
EDITOR_TO_WINDOW,
@@ -66,7 +66,6 @@ import {
NODE_TO_ELEMENT,
PLACEHOLDER_SYMBOL,
} from '../utils/weak-maps'
import { whereIfSupported } from '../utils/where-if-supported'
import { RestoreDOM } from './restore-dom/restore-dom'
import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager'
import { useTrackUserInput } from '../hooks/use-track-user-input'
@@ -77,9 +76,6 @@ const Children = (props: Parameters<typeof useChildren>[0]) => (
<React.Fragment>{useChildren(props)}</React.Fragment>
)
// The number of Editable components currently mounted.
let mountedCount = 0
/**
* `RenderElementProps` are passed to the `renderElement` handler.
*/
@@ -125,6 +121,7 @@ export type EditableProps = {
renderPlaceholder?: (props: RenderPlaceholderProps) => JSX.Element
scrollSelectionIntoView?: (editor: ReactEditor, domRange: DOMRange) => void
as?: React.ElementType
disableDefaultStyles?: boolean
} & React.TextareaHTMLAttributes<HTMLDivElement>
/**
@@ -142,8 +139,9 @@ export const Editable = (props: EditableProps) => {
renderLeaf,
renderPlaceholder = props => <DefaultPlaceholder {...props} />,
scrollSelectionIntoView = defaultScrollSelectionIntoView,
style = {},
style: userStyle = {},
as: Component = 'div',
disableDefaultStyles = false,
...attributes
} = props
const editor = useSlate()
@@ -806,45 +804,9 @@ export const Editable = (props: EditableProps) => {
})
})
useEffect(() => {
mountedCount++
if (mountedCount === 1) {
// Set global default styles for editors.
const defaultStylesElement = document.createElement('style')
defaultStylesElement.setAttribute('data-slate-default-styles', 'true')
const selector = '[data-slate-editor]'
const defaultStyles =
// Allow positioning relative to the editable element.
`position: relative;` +
// Prevent the default outline styles.
`outline: none;` +
// Preserve adjacent whitespace and new lines.
`white-space: pre-wrap;` +
// Allow words to break if they are too long.
`word-wrap: break-word;`
defaultStylesElement.innerHTML = whereIfSupported(selector, defaultStyles)
document.head.appendChild(defaultStylesElement)
}
return () => {
mountedCount--
if (mountedCount <= 0)
document.querySelector('style[data-slate-default-styles]')?.remove()
}
}, [])
useEffect(() => {
const styleElement = document.createElement('style')
document.head.appendChild(styleElement)
EDITOR_TO_STYLE_ELEMENT.set(editor, styleElement)
return () => {
styleElement.remove()
EDITOR_TO_STYLE_ELEMENT.delete(editor)
}
}, [])
const placeholderHeight = EDITOR_TO_PLACEHOLDER_ELEMENT.get(
editor
)?.getBoundingClientRect()?.height
return (
<ReadOnlyContext.Provider value={readOnly}>
@@ -875,7 +837,6 @@ export const Editable = (props: EditableProps) => {
: 'false'
}
data-slate-editor
data-slate-editor-id={editor.id}
data-slate-node="value"
// explicitly set this
contentEditable={!readOnly}
@@ -885,7 +846,26 @@ export const Editable = (props: EditableProps) => {
zindex={-1}
suppressContentEditableWarning
ref={ref}
style={style}
style={{
...(disableDefaultStyles
? {}
: {
// Allow positioning relative to the editable element.
position: 'relative',
// Prevent the default outline styles.
outline: 'none',
// Preserve adjacent whitespace and new lines.
whiteSpace: 'pre-wrap',
// Allow words to break if they are too long.
wordWrap: 'break-word',
// Make the minimum height that of the placeholder.
...(placeholderHeight
? { minHeight: placeholderHeight }
: {}),
}),
// Allow for passed-in styles to override anything.
...userStyle,
}}
onBeforeInput={useCallback(
(event: React.FormEvent<HTMLDivElement>) => {
// COMPAT: Certain browsers don't support the `beforeinput` event, so we

View File

@@ -5,11 +5,10 @@ import String from './string'
import {
PLACEHOLDER_SYMBOL,
EDITOR_TO_PLACEHOLDER_ELEMENT,
EDITOR_TO_STYLE_ELEMENT,
EDITOR_TO_FORCE_RENDER,
} from '../utils/weak-maps'
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
import { useSlateStatic } from '../hooks/use-slate-static'
import { whereIfSupported } from '../utils/where-if-supported'
/**
* Individual leaves in a text node with unique formatting.
@@ -32,6 +31,7 @@ const Leaf = (props: {
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
} = props
const lastPlaceholderRef = useRef<HTMLSpanElement | null>(null)
const placeholderRef = useRef<HTMLSpanElement | null>(null)
const editor = useSlateStatic()
@@ -62,28 +62,24 @@ const Leaf = (props: {
} else if (placeholderEl) {
// Create a new observer and observe the placeholder element.
const ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill
placeholderResizeObserver.current = new ResizeObserver(([{ target }]) => {
const styleElement = EDITOR_TO_STYLE_ELEMENT.get(editor)
if (styleElement) {
// Make the min-height the height of the placeholder.
const selector = `[data-slate-editor-id="${editor.id}"]`
const styles = `min-height: ${target.clientHeight}px;`
styleElement.innerHTML = whereIfSupported(selector, styles)
}
placeholderResizeObserver.current = new ResizeObserver(() => {
// Force a re-render of the editor so its min-height can be updated
// to the new height of the placeholder.
const forceRender = EDITOR_TO_FORCE_RENDER.get(editor)
forceRender?.()
})
placeholderResizeObserver.current.observe(placeholderEl)
}
if (!placeholderEl) {
if (!placeholderEl && lastPlaceholderRef.current) {
// No placeholder element, so no need for a resize observer.
const styleElement = EDITOR_TO_STYLE_ELEMENT.get(editor)
if (styleElement) {
// No min-height if there is no placeholder.
styleElement.innerHTML = ''
}
// Force a re-render of the editor so its min-height can be reset.
const forceRender = EDITOR_TO_FORCE_RENDER.get(editor)
forceRender?.()
}
lastPlaceholderRef.current = placeholderRef.current
return () => {
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
}

View File

@@ -29,10 +29,6 @@ export const EDITOR_TO_KEY_TO_ELEMENT: WeakMap<
Editor,
WeakMap<Key, HTMLElement>
> = new WeakMap()
export const EDITOR_TO_STYLE_ELEMENT: WeakMap<
Editor,
HTMLStyleElement
> = new WeakMap()
/**
* Weak maps for storing editor-related state.

View File

@@ -1,22 +0,0 @@
/**
* Returns a set of rules that use the `:where` selector if it is supported,
* otherwise it falls back to the provided selector on its own.
*
* The `:where` selector is used to give a selector a lower specificity,
* allowing the rule to be overridden by a user-defined stylesheet.
*
* Older browsers do not support the `:where` selector.
* If it is not supported, the selector will be used without `:where`,
* which means that the rule will have a higher specificity and a user-defined
* stylesheet will not be able to override it easily.
*/
export function whereIfSupported(selector: string, styles: string): string {
return (
`@supports (selector(:where(${selector}))) {` +
`:where(${selector}) { ${styles} }` +
`}` +
`@supports not (selector(:where(${selector}))) {` +
`${selector} { ${styles} }` +
`}`
)
}