mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-09 08:46:35 +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:
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": "^16.9.13",
|
||||||
"@types/react-dom": "^16.9.4",
|
"@types/react-dom": "^16.9.4",
|
||||||
"@types/react-test-renderer": "^16.8.0",
|
"@types/react-test-renderer": "^16.8.0",
|
||||||
|
"@types/resize-observer-browser": "^0.1.7",
|
||||||
"react": ">=16.8.0",
|
"react": ">=16.8.0",
|
||||||
"react-dom": ">=16.8.0",
|
"react-dom": ">=16.8.0",
|
||||||
"react-test-renderer": ">=16.8.0",
|
"react-test-renderer": ">=16.8.0",
|
||||||
|
@@ -55,6 +55,7 @@ import {
|
|||||||
EDITOR_TO_ELEMENT,
|
EDITOR_TO_ELEMENT,
|
||||||
EDITOR_TO_FORCE_RENDER,
|
EDITOR_TO_FORCE_RENDER,
|
||||||
EDITOR_TO_PENDING_INSERTION_MARKS,
|
EDITOR_TO_PENDING_INSERTION_MARKS,
|
||||||
|
EDITOR_TO_STYLE_ELEMENT,
|
||||||
EDITOR_TO_USER_MARKS,
|
EDITOR_TO_USER_MARKS,
|
||||||
EDITOR_TO_USER_SELECTION,
|
EDITOR_TO_USER_SELECTION,
|
||||||
EDITOR_TO_WINDOW,
|
EDITOR_TO_WINDOW,
|
||||||
@@ -76,6 +77,9 @@ const Children = (props: Parameters<typeof useChildren>[0]) => (
|
|||||||
<React.Fragment>{useChildren(props)}</React.Fragment>
|
<React.Fragment>{useChildren(props)}</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The number of Editable components currently mounted.
|
||||||
|
let mountedCount = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `RenderElementProps` are passed to the `renderElement` handler.
|
* `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 (
|
return (
|
||||||
<ReadOnlyContext.Provider value={readOnly}>
|
<ReadOnlyContext.Provider value={readOnly}>
|
||||||
<DecorateContext.Provider value={decorate}>
|
<DecorateContext.Provider value={decorate}>
|
||||||
@@ -831,6 +875,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
: 'false'
|
: 'false'
|
||||||
}
|
}
|
||||||
data-slate-editor
|
data-slate-editor
|
||||||
|
data-slate-editor-id={editor.id}
|
||||||
data-slate-node="value"
|
data-slate-node="value"
|
||||||
// explicitly set this
|
// explicitly set this
|
||||||
contentEditable={!readOnly}
|
contentEditable={!readOnly}
|
||||||
@@ -840,18 +885,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
zindex={-1}
|
zindex={-1}
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{
|
style={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,
|
|
||||||
}}
|
|
||||||
onBeforeInput={useCallback(
|
onBeforeInput={useCallback(
|
||||||
(event: React.FormEvent<HTMLDivElement>) => {
|
(event: React.FormEvent<HTMLDivElement>) => {
|
||||||
// COMPAT: Certain browsers don't support the `beforeinput` event, so we
|
// COMPAT: Certain browsers don't support the `beforeinput` event, so we
|
||||||
|
@@ -4,10 +4,10 @@ import String from './string'
|
|||||||
import {
|
import {
|
||||||
PLACEHOLDER_SYMBOL,
|
PLACEHOLDER_SYMBOL,
|
||||||
EDITOR_TO_PLACEHOLDER_ELEMENT,
|
EDITOR_TO_PLACEHOLDER_ELEMENT,
|
||||||
|
EDITOR_TO_STYLE_ELEMENT,
|
||||||
} from '../utils/weak-maps'
|
} from '../utils/weak-maps'
|
||||||
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
|
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
|
||||||
import { useSlateStatic } from '../hooks/use-slate-static'
|
import { useSlateStatic } from '../hooks/use-slate-static'
|
||||||
import { ReactEditor } from '..'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Individual leaves in a text node with unique formatting.
|
* Individual leaves in a text node with unique formatting.
|
||||||
@@ -33,19 +33,54 @@ const Leaf = (props: {
|
|||||||
const placeholderRef = useRef<HTMLSpanElement | null>(null)
|
const placeholderRef = useRef<HTMLSpanElement | null>(null)
|
||||||
const editor = useSlateStatic()
|
const editor = useSlateStatic()
|
||||||
|
|
||||||
|
const placeholderResizeObserver = useRef<ResizeObserver | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (placeholderResizeObserver.current) {
|
||||||
|
placeholderResizeObserver.current.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const placeholderEl = placeholderRef?.current
|
const placeholderEl = placeholderRef?.current
|
||||||
const editorEl = ReactEditor.toDOMNode(editor, editor)
|
|
||||||
|
|
||||||
if (!placeholderEl || !editorEl) {
|
if (placeholderEl) {
|
||||||
return
|
EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl)
|
||||||
|
} else {
|
||||||
|
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
editorEl.style.minHeight = `${placeholderEl.clientHeight}px`
|
if (placeholderResizeObserver.current) {
|
||||||
EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl)
|
// 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 () => {
|
return () => {
|
||||||
editorEl.style.minHeight = 'auto'
|
|
||||||
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
|
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor)
|
||||||
}
|
}
|
||||||
}, [placeholderRef, leaf])
|
}, [placeholderRef, leaf])
|
||||||
|
@@ -29,6 +29,10 @@ export const EDITOR_TO_KEY_TO_ELEMENT: WeakMap<
|
|||||||
Editor,
|
Editor,
|
||||||
WeakMap<Key, HTMLElement>
|
WeakMap<Key, HTMLElement>
|
||||||
> = new WeakMap()
|
> = new WeakMap()
|
||||||
|
export const EDITOR_TO_STYLE_ELEMENT: WeakMap<
|
||||||
|
Editor,
|
||||||
|
HTMLStyleElement
|
||||||
|
> = new WeakMap()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Weak maps for storing editor-related state.
|
* Weak maps for storing editor-related state.
|
||||||
|
@@ -8,7 +8,15 @@ const createNodeMock = () => ({
|
|||||||
getRootNode: () => global.document,
|
getRootNode: () => global.document,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
class MockResizeObserver {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
}
|
||||||
|
|
||||||
describe('slate-react', () => {
|
describe('slate-react', () => {
|
||||||
|
window.ResizeObserver = MockResizeObserver as any
|
||||||
|
|
||||||
describe('Editable', () => {
|
describe('Editable', () => {
|
||||||
describe('NODE_TO_KEY logic', () => {
|
describe('NODE_TO_KEY logic', () => {
|
||||||
it('should not unmount the node that gets split on a split_node operation', async () => {
|
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 { DIRTY_PATHS, DIRTY_PATH_KEYS, FLUSHING } from './utils/weak-maps'
|
||||||
import { TextUnit } from './interfaces/types'
|
import { TextUnit } from './interfaces/types'
|
||||||
|
|
||||||
|
let nextEditorId = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Slate `Editor` object.
|
* Create a new Slate `Editor` object.
|
||||||
*/
|
*/
|
||||||
@@ -26,6 +28,7 @@ export const createEditor = (): Editor => {
|
|||||||
operations: [],
|
operations: [],
|
||||||
selection: null,
|
selection: null,
|
||||||
marks: null,
|
marks: null,
|
||||||
|
id: nextEditorId++,
|
||||||
isInline: () => false,
|
isInline: () => false,
|
||||||
isVoid: () => false,
|
isVoid: () => false,
|
||||||
markableVoid: () => false,
|
markableVoid: () => false,
|
||||||
|
@@ -57,6 +57,7 @@ export interface BaseEditor {
|
|||||||
selection: Selection
|
selection: Selection
|
||||||
operations: Operation[]
|
operations: Operation[]
|
||||||
marks: EditorMarks | null
|
marks: EditorMarks | null
|
||||||
|
readonly id: number
|
||||||
|
|
||||||
// Schema-specific node behaviors.
|
// Schema-specific node behaviors.
|
||||||
isInline: (element: Element) => boolean
|
isInline: (element: Element) => boolean
|
||||||
|
@@ -3739,6 +3739,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/resolve@npm:0.0.8":
|
||||||
version: 0.0.8
|
version: 0.0.8
|
||||||
resolution: "@types/resolve@npm: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": ^16.9.13
|
||||||
"@types/react-dom": ^16.9.4
|
"@types/react-dom": ^16.9.4
|
||||||
"@types/react-test-renderer": ^16.8.0
|
"@types/react-test-renderer": ^16.8.0
|
||||||
|
"@types/resize-observer-browser": ^0.1.7
|
||||||
direction: ^1.0.3
|
direction: ^1.0.3
|
||||||
is-hotkey: ^0.1.6
|
is-hotkey: ^0.1.6
|
||||||
is-plain-object: ^5.0.0
|
is-plain-object: ^5.0.0
|
||||||
|
Reference in New Issue
Block a user