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:
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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} }` +
|
||||
`}`
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user