1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-19 05:31:56 +02:00

Use shadow dom if available (#3749)

* getDirtyPaths can now be customized by Slate users (#4012)

* Moved getDirtyPaths() into the editor object so it can be customized via plugin

* docs: Update document in Chinese (#4017)

Co-authored-by: liuchengshuai001 <liuchengshuai001@ke.com>

* Removed unused import

* Use shadowRoot if available

* Removed optional chaining

* Added workaround for chrom bug in ShadowDOM

* Added shadow DOM example

* Add a shadow DOM example

Shadow DOM brings different behaviours for selection and active
elements. This adds an example where the editor is found within a shadow
DOM, in fact, the editor is two levels deep in nested shadow DOMs.

The handling of selections means that this editor doesn't work properly
so Slate will need to be made aware of the shadow DOM in order to fix
this.

* User DocumentOrShadowRoot for selection and active elements

If the editor is within a ShadowDom, the selections and active element
APIs are implemented on the ShadowRoot for Chrome. Other browsers still
use the Document's version of these APIs for the shadow DOM.

Instead of defaulting to `window.document`, find the appropriate root to
use for the editor in question.

* Add compatibility for Chrome's isCollapsed bug

Chrome will always return true for isCollapsed on a selection from the
shadow DOM. Work around this by instead computing this property on
Chrome.

https://bugs.chromium.org/p/chromium/issues/detail?id=447523

* Removed duplicated example

* Fixed possible null value

* Use existing PlainTextExample

* Re-added local Editor to have clear initialValue

* Optimize shadowRoot checkup

* Remove getDocumentOrShadowRoot util in favor of findDocumentOrShadowRoot

* Re-added getDocumentOrShadowRoot

* Put selectionchange listener on window.document

* Resetted changes from main branch

* Create tiny-walls-deliver.md

* Update tiny-walls-deliver.md

* Update tiny-walls-deliver.md

Co-authored-by: Tommy Dong <contact@tomdong.io>
Co-authored-by: Jacob <40483898+jacob-lcs@users.noreply.github.com>
Co-authored-by: liuchengshuai001 <liuchengshuai001@ke.com>
Co-authored-by: Andrew Scull <andrew.scull@live.com>
Co-authored-by: Ian Storm Taylor <ian@ianstormtaylor.com>
This commit is contained in:
David Ruisinger
2021-03-31 21:47:28 +02:00
committed by GitHub
parent 42d99af6fa
commit 0473d0bf93
7 changed files with 120 additions and 19 deletions

View File

@@ -0,0 +1,5 @@
---
'slate-react': patch
---
Fixes Slate to work with the Shadow DOM.

View File

@@ -32,7 +32,6 @@ import {
getDefaultView,
isDOMElement,
isDOMNode,
isDOMText,
DOMStaticRange,
isPlainTextOnlyPaste,
} from '../utils/dom'
@@ -148,8 +147,8 @@ export const Editable = (props: EditableProps) => {
// Whenever the editor updates, make sure the DOM selection state is in sync.
useIsomorphicLayoutEffect(() => {
const { selection } = editor
const window = ReactEditor.getWindow(editor)
const domSelection = window.getSelection()
const root = ReactEditor.findDocumentOrShadowRoot(editor)
const domSelection = root.getSelection()
if (state.isComposing || !domSelection || !ReactEditor.isFocused(editor)) {
return
@@ -400,10 +399,10 @@ export const Editable = (props: EditableProps) => {
const onDOMSelectionChange = useCallback(
throttle(() => {
if (!readOnly && !state.isComposing && !state.isUpdatingSelection) {
const window = ReactEditor.getWindow(editor)
const { activeElement } = window.document
const root = ReactEditor.findDocumentOrShadowRoot(editor)
const { activeElement } = root
const el = ReactEditor.toDOMNode(editor, editor)
const domSelection = window.getSelection()
const domSelection = root.getSelection()
if (activeElement === el) {
state.latestElement = activeElement
@@ -541,7 +540,8 @@ export const Editable = (props: EditableProps) => {
// one, this is due to the window being blurred when the tab
// itself becomes unfocused, so we want to abort early to allow to
// editor to stay focused when the tab becomes focused again.
if (state.latestElement === window.document.activeElement) {
const root = ReactEditor.findDocumentOrShadowRoot(editor)
if (state.latestElement === root.activeElement) {
return
}
@@ -745,8 +745,8 @@ export const Editable = (props: EditableProps) => {
!isEventHandled(event, attributes.onFocus)
) {
const el = ReactEditor.toDOMNode(editor, editor)
const window = ReactEditor.getWindow(editor)
state.latestElement = window.document.activeElement
const root = ReactEditor.findDocumentOrShadowRoot(editor)
state.latestElement = root.activeElement
// COMPAT: If the editor has nested editable elements, the focus
// can go to them. In Firefox, this must be prevented because it

View File

@@ -20,9 +20,10 @@ import {
DOMSelection,
DOMStaticRange,
isDOMElement,
normalizeDOMPoint,
isDOMSelection,
normalizeDOMPoint,
} from '../utils/dom'
import { IS_CHROME } from '../utils/environment'
/**
* A React and DOM-specific version of the `Editor` interface.
@@ -95,6 +96,29 @@ export const ReactEditor = {
)
},
/**
* Find the DOM node that implements DocumentOrShadowRoot for the editor.
*/
findDocumentOrShadowRoot(editor: ReactEditor): Document | ShadowRoot {
const el = ReactEditor.toDOMNode(editor, editor)
const root = el.getRootNode()
if (!(root instanceof Document || root instanceof ShadowRoot))
throw new Error(
`Unable to find DocumentOrShadowRoot for editor element: ${el}`
)
// COMPAT: Only Chrome implements the DocumentOrShadowRoot mixin for
// ShadowRoot; other browsers still implement it on the Document
// interface. (2020/08/08)
// https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot#Properties
if (root.getSelection === undefined && el.ownerDocument !== null)
return el.ownerDocument
return root
},
/**
* Check if the editor is focused.
*/
@@ -117,9 +141,10 @@ export const ReactEditor = {
blur(editor: ReactEditor): void {
const el = ReactEditor.toDOMNode(editor, editor)
const root = ReactEditor.findDocumentOrShadowRoot(editor)
IS_FOCUSED.set(editor, false)
const window = ReactEditor.getWindow(editor)
if (window.document.activeElement === el) {
if (root.activeElement === el) {
el.blur()
}
},
@@ -130,10 +155,10 @@ export const ReactEditor = {
focus(editor: ReactEditor): void {
const el = ReactEditor.toDOMNode(editor, editor)
const root = ReactEditor.findDocumentOrShadowRoot(editor)
IS_FOCUSED.set(editor, true)
const window = ReactEditor.getWindow(editor)
if (window.document.activeElement !== el) {
if (root.activeElement !== el) {
el.focus({ preventScroll: true })
}
},
@@ -143,9 +168,10 @@ export const ReactEditor = {
*/
deselect(editor: ReactEditor): void {
const el = ReactEditor.toDOMNode(editor, editor)
const { selection } = editor
const window = ReactEditor.getWindow(editor)
const domSelection = window.getSelection()
const root = ReactEditor.findDocumentOrShadowRoot(editor)
const domSelection = root.getSelection()
if (domSelection && domSelection.rangeCount > 0) {
domSelection.removeAllRanges()
@@ -509,7 +535,17 @@ export const ReactEditor = {
anchorOffset = domRange.anchorOffset
focusNode = domRange.focusNode
focusOffset = domRange.focusOffset
// COMPAT: There's a bug in chrome that always returns `true` for
// `isCollapsed` for a Selection that comes from a ShadowRoot.
// (2020/08/08)
// https://bugs.chromium.org/p/chromium/issues/detail?id=447523
if (IS_CHROME && hasShadowRoot()) {
isCollapsed =
domRange.anchorNode === domRange.focusNode &&
domRange.anchorOffset === domRange.focusOffset
} else {
isCollapsed = domRange.isCollapsed
}
} else {
anchorNode = domRange.startContainer
anchorOffset = domRange.startOffset

View File

@@ -127,6 +127,16 @@ export const normalizeDOMPoint = (domPoint: DOMPoint): DOMPoint => {
return [node, offset]
}
/**
* Determines wether the active element is nested within a shadowRoot
*/
export const hasShadowRoot = () => {
return !!(
window.document.activeElement && window.document.activeElement.shadowRoot
)
}
/**
* Get the nearest editable child at `index` in a `parent`, preferring
* `direction`.

View File

@@ -20,6 +20,9 @@ export const IS_EDGE_LEGACY =
typeof navigator !== 'undefined' &&
/Edge?\/(?:[0-6][0-9]|[0-7][0-8])/i.test(navigator.userAgent)
export const IS_CHROME =
typeof navigator !== 'undefined' && /Chrome/i.test(navigator.userAgent)
// Native beforeInput events don't work well with react on Chrome 75 and older, Chrome 76+ can use beforeInput
export const IS_CHROME_LEGACY =
typeof navigator !== 'undefined' &&

View File

@@ -0,0 +1,47 @@
import ReactDOM from 'react-dom'
import React, { useState, useMemo, useRef, useEffect } from 'react'
import { createEditor } from 'slate'
import { Slate, Editable, withReact } from 'slate-react'
import { withHistory } from 'slate-history'
const ShadowDOM = () => {
const container = useRef(null)
useEffect(() => {
if (container.current.shadowRoot) return
// Create a shadow DOM
const outerShadowRoot = container.current.attachShadow({ mode: 'open' })
const host = document.createElement('div')
outerShadowRoot.appendChild(host)
// Create a nested shadow DOM
const innerShadowRoot = host.attachShadow({ mode: 'open' })
const reactRoot = document.createElement('div')
innerShadowRoot.appendChild(reactRoot)
// Render the editor within the nested shadow DOM
ReactDOM.render(<ShadowEditor />, reactRoot)
})
return <div ref={container} />
}
const ShadowEditor = () => {
const [value, setValue] = useState(initialValue)
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
return (
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
<Editable placeholder="Enter some plain text..." />
</Slate>
)
}
const initialValue = [
{
children: [{ text: 'This Editor is rendered within a nested Shadow DOM.' }],
},
]
export default ShadowDOM

View File

@@ -23,7 +23,7 @@ import PlainText from '../../examples/plaintext'
import ReadOnly from '../../examples/read-only'
import RichText from '../../examples/richtext'
import SearchHighlighting from '../../examples/search-highlighting'
import CodeHighlighting from '../../examples/code-highlighting'
import ShadowDOM from '../../examples/shadow-dom'
import Tables from '../../examples/tables'
import IFrames from '../../examples/iframe'
@@ -47,7 +47,7 @@ const EXAMPLES = [
['Read-only', ReadOnly, 'read-only'],
['Rich Text', RichText, 'richtext'],
['Search Highlighting', SearchHighlighting, 'search-highlighting'],
['Code Highlighting', CodeHighlighting, 'code-highlighting'],
['Shadow DOM', ShadowDOM, 'shadow-dom'],
['Tables', Tables, 'tables'],
['Rendering in iframes', IFrames, 'iframe'],
]