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

slate-react: MVP for working with non-global window objects (fix for #3819) (#4079)

* mvp implementation for working with non-global window instances

* remove unused element renderer

* fix typo in comment

* fix wrong example reference

* Add @babel/helper-call-delegate to fix build error

Co-authored-by: Lukas Buenger <lukasbuenger@gmail.com>
This commit is contained in:
Sunny Hirai
2021-02-16 19:40:15 -08:00
committed by GitHub
parent 6d66d87f67
commit 513771c82a
9 changed files with 263 additions and 12 deletions

View File

@@ -29,6 +29,7 @@ import {
DOMElement,
DOMNode,
DOMRange,
getDefaultView,
isDOMElement,
isDOMNode,
isDOMText,
@@ -42,6 +43,7 @@ import {
NODE_TO_ELEMENT,
IS_FOCUSED,
PLACEHOLDER_SYMBOL,
EDITOR_TO_WINDOW,
} from '../utils/weak-maps'
// COMPAT: Firefox/Edge Legacy don't support the `beforeinput` event
@@ -132,7 +134,9 @@ export const Editable = (props: EditableProps) => {
// Update element-related weak maps with the DOM element ref.
useIsomorphicLayoutEffect(() => {
if (ref.current) {
let window
if (ref.current && (window = getDefaultView(ref.current))) {
EDITOR_TO_WINDOW.set(editor, window)
EDITOR_TO_ELEMENT.set(editor, ref.current)
NODE_TO_ELEMENT.set(editor, ref.current)
ELEMENT_TO_NODE.set(ref.current, editor)
@@ -144,6 +148,7 @@ 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()
if (state.isComposing || !domSelection || !ReactEditor.isFocused(editor)) {
@@ -354,8 +359,9 @@ export const Editable = (props: EditableProps) => {
case 'insertFromYank':
case 'insertReplacementText':
case 'insertText': {
if (data instanceof DataTransfer) {
ReactEditor.insertData(editor, data)
const window = ReactEditor.getWindow(editor)
if (data instanceof window.DataTransfer) {
ReactEditor.insertData(editor, data as DataTransfer)
} else if (typeof data === 'string') {
Editor.insertText(editor, data)
}
@@ -394,6 +400,7 @@ 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 el = ReactEditor.toDOMNode(editor, editor)
const domSelection = window.getSelection()
@@ -436,6 +443,7 @@ export const Editable = (props: EditableProps) => {
// fire for any change to the selection inside the editor. (2019/11/04)
// https://github.com/facebook/react/issues/5785
useIsomorphicLayoutEffect(() => {
const window = ReactEditor.getWindow(editor)
window.document.addEventListener('selectionchange', onDOMSelectionChange)
return () => {
@@ -527,6 +535,8 @@ export const Editable = (props: EditableProps) => {
return
}
const window = ReactEditor.getWindow(editor)
// COMPAT: If the current `activeElement` is still the previous
// one, this is due to the window being blurred when the tab
// itself becomes unfocused, so we want to abort early to allow to
@@ -735,6 +745,7 @@ 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
// COMPAT: If the editor has nested editable elements, the focus

View File

@@ -10,6 +10,7 @@ import {
NODE_TO_INDEX,
NODE_TO_KEY,
NODE_TO_PARENT,
EDITOR_TO_WINDOW,
} from '../utils/weak-maps'
import {
DOMElement,
@@ -20,6 +21,7 @@ import {
DOMStaticRange,
isDOMElement,
normalizeDOMPoint,
isDOMSelection,
} from '../utils/dom'
/**
@@ -32,6 +34,18 @@ export interface ReactEditor extends Editor {
}
export const ReactEditor = {
/**
* Return the host window of the current editor.
*/
getWindow(editor: ReactEditor): Window {
const window = EDITOR_TO_WINDOW.get(editor)
if (!window) {
throw new Error('Unable to find a host window element for this editor')
}
return window
},
/**
* Find a key for a Slate node.
*/
@@ -104,7 +118,7 @@ export const ReactEditor = {
blur(editor: ReactEditor): void {
const el = ReactEditor.toDOMNode(editor, editor)
IS_FOCUSED.set(editor, false)
const window = ReactEditor.getWindow(editor)
if (window.document.activeElement === el) {
el.blur()
}
@@ -118,6 +132,7 @@ export const ReactEditor = {
const el = ReactEditor.toDOMNode(editor, editor)
IS_FOCUSED.set(editor, true)
const window = ReactEditor.getWindow(editor)
if (window.document.activeElement !== el) {
el.focus({ preventScroll: true })
}
@@ -129,6 +144,7 @@ export const ReactEditor = {
deselect(editor: ReactEditor): void {
const { selection } = editor
const window = ReactEditor.getWindow(editor)
const domSelection = window.getSelection()
if (domSelection && domSelection.rangeCount > 0) {
@@ -284,6 +300,7 @@ export const ReactEditor = {
? domAnchor
: ReactEditor.toDOMPoint(editor, focus)
const window = ReactEditor.getWindow(editor)
const domRange = window.document.createRange()
const [startNode, startOffset] = isBackward ? domFocus : domAnchor
const [endNode, endOffset] = isBackward ? domAnchor : domFocus
@@ -410,6 +427,7 @@ export const ReactEditor = {
// can determine what the offset relative to the text node is.
if (leafNode) {
textNode = leafNode.closest('[data-slate-node="text"]')!
const window = ReactEditor.getWindow(editor)
const range = window.document.createRange()
range.setStart(textNode, 0)
range.setEnd(nearestNode, nearestOffset)
@@ -476,10 +494,9 @@ export const ReactEditor = {
editor: ReactEditor,
domRange: DOMRange | DOMStaticRange | DOMSelection
): Range {
const el =
domRange instanceof Selection
? domRange.anchorNode
: domRange.startContainer
const el = isDOMSelection(domRange)
? domRange.anchorNode
: domRange.startContainer
let anchorNode
let anchorOffset
let focusNode
@@ -487,7 +504,7 @@ export const ReactEditor = {
let isCollapsed
if (el) {
if (domRange instanceof Selection) {
if (isDOMSelection(domRange)) {
anchorNode = domRange.anchorNode
anchorOffset = domRange.anchorOffset
focusNode = domRange.focusNode

View File

@@ -23,8 +23,26 @@ export {
DOMStaticRange,
}
declare global {
interface Window {
Selection: typeof Selection['constructor']
DataTransfer: typeof DataTransfer['constructor']
Node: typeof Node['constructor']
}
}
export type DOMPoint = [Node, number]
/**
* Returns the host window of a DOM node
*/
export const getDefaultView = (value: any): Window | null => {
return (
(value && value.ownerDocument && value.ownerDocument.defaultView) || null
)
}
/**
* Check if a DOM node is a comment node.
*/
@@ -46,7 +64,17 @@ export const isDOMElement = (value: any): value is DOMElement => {
*/
export const isDOMNode = (value: any): value is DOMNode => {
return value instanceof Node
const window = getDefaultView(value)
return !!window && value instanceof window.Node
}
/**
* Check if a value is a DOM selection.
*/
export const isDOMSelection = (value: any): value is DOMSelection => {
const window = value && value.anchorNode && getDefaultView(value.anchorNode)
return !!window && value instanceof window.Selection
}
/**

View File

@@ -14,7 +14,7 @@ export const NODE_TO_PARENT: WeakMap<Node, Ancestor> = new WeakMap()
* Weak maps that allow us to go between Slate nodes and DOM nodes. These
* are used to resolve DOM event-related logic into Slate actions.
*/
export const EDITOR_TO_WINDOW: WeakMap<Editor, Window> = new WeakMap()
export const EDITOR_TO_ELEMENT: WeakMap<Editor, HTMLElement> = new WeakMap()
export const EDITOR_TO_PLACEHOLDER: WeakMap<Editor, string> = new WeakMap()
export const ELEMENT_TO_NODE: WeakMap<HTMLElement, Node> = new WeakMap()