mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-04-21 13:51:59 +02:00
Use stylesheets to give Editable components default styles (#5206)
* Use stylesheet to give Editable components a default style * Give Editors a unique id * Use per-editor stylesheets to give editors a min-height * Make editor min-height respond to changes in placeholder height * Add changeset for stylesheet changes * Prevent unnecessary creations of ResizeObservers * Update yarn.lock
This commit is contained in:
parent
d1f90ebd12
commit
96b7fcdbf9
6
.changeset/strange-pens-lie.md
Normal file
6
.changeset/strange-pens-lie.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
'slate': minor
|
||||
'slate-react': minor
|
||||
---
|
||||
|
||||
Use stylesheet for default styles on Editable components
|
@ -30,6 +30,7 @@
|
||||
"@types/react": "^16.9.13",
|
||||
"@types/react-dom": "^16.9.4",
|
||||
"@types/react-test-renderer": "^16.8.0",
|
||||
"@types/resize-observer-browser": "^0.1.7",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"react-test-renderer": ">=16.8.0",
|
||||
|
@ -55,6 +55,7 @@ import {
|
||||
EDITOR_TO_ELEMENT,
|
||||
EDITOR_TO_FORCE_RENDER,
|
||||
EDITOR_TO_PENDING_INSERTION_MARKS,
|
||||
EDITOR_TO_STYLE_ELEMENT,
|
||||
EDITOR_TO_USER_MARKS,
|
||||
EDITOR_TO_USER_SELECTION,
|
||||
EDITOR_TO_WINDOW,
|
||||
@ -76,6 +77,9 @@ 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.
|
||||
*/
|
||||
@ -802,6 +806,46 @@ 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')
|
||||
defaultStylesElement.innerHTML =
|
||||
// :where is used to give these rules lower specificity so user stylesheets can override them.
|
||||
`:where([data-slate-editor]) {` +
|
||||
// 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;` +
|
||||
`}`
|
||||
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)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ReadOnlyContext.Provider value={readOnly}>
|
||||
<DecorateContext.Provider value={decorate}>
|
||||
@ -831,6 +875,7 @@ export const Editable = (props: EditableProps) => {
|
||||
: 'false'
|
||||
}
|
||||
data-slate-editor
|
||||
data-slate-editor-id={editor.id}
|
||||
data-slate-node="value"
|
||||
// explicitly set this
|
||||
contentEditable={!readOnly}
|
||||
@ -840,18 +885,7 @@ export const Editable = (props: EditableProps) => {
|
||||
zindex={-1}
|
||||
suppressContentEditableWarning
|
||||
ref={ref}
|
||||
style={{
|
||||
// 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',
|
||||
// Allow for passed-in styles to override anything.
|
||||
...style,
|
||||
}}
|
||||
style={style}
|
||||
onBeforeInput={useCallback(
|
||||
(event: React.FormEvent<HTMLDivElement>) => {
|
||||
// COMPAT: Certain browsers don't support the `beforeinput` event, so we
|
||||
|
@ -4,10 +4,10 @@ import String from './string'
|
||||
import {
|
||||
PLACEHOLDER_SYMBOL,
|
||||
EDITOR_TO_PLACEHOLDER_ELEMENT,
|
||||
EDITOR_TO_STYLE_ELEMENT,
|
||||
} from '../utils/weak-maps'
|
||||
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
|
||||
import { useSlateStatic } from '../hooks/use-slate-static'
|
||||
import { ReactEditor } from '..'
|
||||
|
||||
/**
|
||||
* Individual leaves in a text node with unique formatting.
|
||||
@ -33,19 +33,54 @@ const Leaf = (props: {
|
||||
const placeholderRef = useRef<HTMLSpanElement | null>(null)
|
||||
const editor = useSlateStatic()
|
||||
|
||||
const placeholderResizeObserver = useRef<ResizeObserver | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (placeholderResizeObserver.current) {
|
||||
placeholderResizeObserver.current.disconnect()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const placeholderEl = placeholderRef?.current
|
||||
const editorEl = ReactEditor.toDOMNode(editor, editor)
|
||||
|
||||
if (!placeholderEl || !editorEl) {
|
||||
return
|
||||
if (placeholderEl) {
|
||||
EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl)
|
||||
} else {
|
||||
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
|
||||
}
|
||||
|
||||
editorEl.style.minHeight = `${placeholderEl.clientHeight}px`
|
||||
EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl)
|
||||
if (placeholderResizeObserver.current) {
|
||||
// Update existing observer.
|
||||
placeholderResizeObserver.current.disconnect()
|
||||
if (placeholderEl)
|
||||
placeholderResizeObserver.current.observe(placeholderEl)
|
||||
} else if (placeholderEl) {
|
||||
// Create a new observer and observe the placeholder element.
|
||||
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 minHeight = `${target.clientHeight}px`
|
||||
styleElement.innerHTML = `:where([data-slate-editor-id="${editor.id}"]) { min-height: ${minHeight}; }`
|
||||
}
|
||||
})
|
||||
|
||||
placeholderResizeObserver.current.observe(placeholderEl)
|
||||
}
|
||||
|
||||
if (!placeholderEl) {
|
||||
// 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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
editorEl.style.minHeight = 'auto'
|
||||
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
|
||||
}
|
||||
}, [placeholderRef, leaf])
|
||||
|
@ -29,6 +29,10 @@ 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.
|
||||
|
@ -8,7 +8,15 @@ const createNodeMock = () => ({
|
||||
getRootNode: () => global.document,
|
||||
})
|
||||
|
||||
class MockResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
describe('slate-react', () => {
|
||||
window.ResizeObserver = MockResizeObserver as any
|
||||
|
||||
describe('Editable', () => {
|
||||
describe('NODE_TO_KEY logic', () => {
|
||||
it('should not unmount the node that gets split on a split_node operation', async () => {
|
||||
|
@ -16,6 +16,8 @@ import {
|
||||
import { DIRTY_PATHS, DIRTY_PATH_KEYS, FLUSHING } from './utils/weak-maps'
|
||||
import { TextUnit } from './interfaces/types'
|
||||
|
||||
let nextEditorId = 0
|
||||
|
||||
/**
|
||||
* Create a new Slate `Editor` object.
|
||||
*/
|
||||
@ -26,6 +28,7 @@ export const createEditor = (): Editor => {
|
||||
operations: [],
|
||||
selection: null,
|
||||
marks: null,
|
||||
id: nextEditorId++,
|
||||
isInline: () => false,
|
||||
isVoid: () => false,
|
||||
markableVoid: () => false,
|
||||
|
@ -57,6 +57,7 @@ export interface BaseEditor {
|
||||
selection: Selection
|
||||
operations: Operation[]
|
||||
marks: EditorMarks | null
|
||||
readonly id: number
|
||||
|
||||
// Schema-specific node behaviors.
|
||||
isInline: (element: Element) => boolean
|
||||
|
@ -3739,6 +3739,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/resize-observer-browser@npm:^0.1.7":
|
||||
version: 0.1.7
|
||||
resolution: "@types/resize-observer-browser@npm:0.1.7"
|
||||
checksum: 0377eaac8bb7a17b983b49a156006032380b459bfebefc54a5aa2f7f8a9786d2b60723e8837c61ef733330b478f4f26293e9edbdc8006238e4f80c878c56c988
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/resolve@npm:0.0.8":
|
||||
version: 0.0.8
|
||||
resolution: "@types/resolve@npm:0.0.8"
|
||||
@ -14235,6 +14242,7 @@ resolve@^2.0.0-next.3:
|
||||
"@types/react": ^16.9.13
|
||||
"@types/react-dom": ^16.9.4
|
||||
"@types/react-test-renderer": ^16.8.0
|
||||
"@types/resize-observer-browser": ^0.1.7
|
||||
direction: ^1.0.3
|
||||
is-hotkey: ^0.1.6
|
||||
is-plain-object: ^5.0.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user