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:
@@ -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
|
||||
|
@@ -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
|
||||
isCollapsed = domRange.isCollapsed
|
||||
// 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
|
||||
|
@@ -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`.
|
||||
|
@@ -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' &&
|
||||
|
Reference in New Issue
Block a user