1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-04-13 18:01:53 +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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 263 additions and 12 deletions

View File

@ -31,6 +31,7 @@
"devDependencies": {
"@babel/cli": "^7.7.4",
"@babel/core": "^7.7.4",
"@babel/helper-call-delegate": "^7.7.4",
"@babel/plugin-external-helpers": "^7.7.4",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-transform-modules-commonjs": "^7.7.4",
@ -92,4 +93,4 @@
"source-map-loader": "^0.2.4",
"typescript": "^3.7.2"
}
}
}

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()

158
site/examples/iframe.js Normal file
View File

@ -0,0 +1,158 @@
import React, { useCallback, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import isHotkey from 'is-hotkey'
import { Editable, withReact, useSlate, Slate, ReactEditor } from 'slate-react'
import { Editor, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import { Button, Icon, Toolbar } from '../components'
const HOTKEYS = {
'mod+b': 'bold',
'mod+i': 'italic',
'mod+u': 'underline',
'mod+`': 'code',
}
const IFrameExample = () => {
const [value, setValue] = useState(initialValue)
const renderElement = useCallback(
({ attributes, children }) => <p {...attributes}>{children}</p>,
[]
)
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
const handleBlur = useCallback(() => ReactEditor.deselect(editor), [editor])
return (
<Slate editor={editor} value={value} onChange={value => setValue(value)}>
<Toolbar>
<MarkButton format="bold" icon="format_bold" />
<MarkButton format="italic" icon="format_italic" />
<MarkButton format="underline" icon="format_underlined" />
<MarkButton format="code" icon="code" />
</Toolbar>
<IFrame onBlur={handleBlur}>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder="Enter some rich text…"
spellCheck
autoFocus
onKeyDown={event => {
for (const hotkey in HOTKEYS) {
if (isHotkey(hotkey, event)) {
event.preventDefault()
const mark = HOTKEYS[hotkey]
toggleMark(editor, mark)
}
}
}}
/>
</IFrame>
</Slate>
)
}
const toggleMark = (editor, format) => {
const isActive = isMarkActive(editor, format)
if (isActive) {
Editor.removeMark(editor, format)
} else {
Editor.addMark(editor, format, true)
}
}
const isMarkActive = (editor, format) => {
const marks = Editor.marks(editor)
return marks ? marks[format] === true : false
}
const Leaf = ({ attributes, children, leaf }) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}
if (leaf.code) {
children = <code>{children}</code>
}
if (leaf.italic) {
children = <em>{children}</em>
}
if (leaf.underline) {
children = <u>{children}</u>
}
return <span {...attributes}>{children}</span>
}
const MarkButton = ({ format, icon }) => {
const editor = useSlate()
return (
<Button
active={isMarkActive(editor, format)}
onMouseDown={event => {
event.preventDefault()
toggleMark(editor, format)
}}
>
<Icon>{icon}</Icon>
</Button>
)
}
const IFrame = ({ children, ...props }) => {
const [contentRef, setContentRef] = useState(null)
const mountNode =
contentRef &&
contentRef.contentWindow &&
contentRef.contentWindow.document.body
return (
<iframe {...props} ref={setContentRef}>
{mountNode && createPortal(React.Children.only(children), mountNode)}
</iframe>
)
}
const initialValue = [
{
type: 'paragraph',
children: [
{
text: 'In this example, the document gets rendered into a controlled ',
},
{ text: '<iframe>', code: true },
{
text: '. This is ',
},
{
text: 'particularly',
italic: true,
},
{
text:
' useful, when you need to separate the styles for your editor contents from the ones addressing your UI.',
},
],
},
{
type: 'paragraph',
children: [
{
text: 'This also the only reliable method to preview any ',
},
{
text: 'media queries',
bold: true,
},
{
text: ' in your CSS.',
},
],
},
]
export default IFrameExample

View File

@ -25,6 +25,7 @@ import RichText from '../../examples/richtext'
import SearchHighlighting from '../../examples/search-highlighting'
import CodeHighlighting from '../../examples/code-highlighting'
import Tables from '../../examples/tables'
import IFrames from '../../examples/iframe'
// node
import { getAllExamples } from '../api'
@ -48,6 +49,7 @@ const EXAMPLES = [
['Search Highlighting', SearchHighlighting, 'search-highlighting'],
['Code Highlighting', CodeHighlighting, 'code-highlighting'],
['Tables', Tables, 'tables'],
['Rendering in iframes', IFrames, 'iframe'],
]
const Header = props => (

View File

@ -70,6 +70,11 @@ input:focus {
border-color: blue;
}
iframe {
width: 100%;
border: 1px solid #eee;
}
[data-slate-editor] > * + * {
margin-top: 1em;
}

View File

@ -191,6 +191,14 @@
"@babel/helper-annotate-as-pure" "^7.10.4"
"@babel/types" "^7.10.4"
"@babel/helper-call-delegate@^7.7.4":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.12.13.tgz#119ef367451f90bed006c685816ba60fc33fee78"
integrity sha512-K1kF0RXK/GpdS9OZDlBllG0+RQQtyzG/TC+nk0VkrUry4l4Xh2T7HdDsDOVlXQY/KcqvE/JQ84pKjKucdrg3FQ==
dependencies:
"@babel/helper-hoist-variables" "^7.12.13"
"@babel/types" "^7.12.13"
"@babel/helper-compilation-targets@^7.10.4", "@babel/helper-compilation-targets@^7.9.6":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2"
@ -263,6 +271,13 @@
dependencies:
"@babel/types" "^7.10.4"
"@babel/helper-hoist-variables@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.12.13.tgz#13aba58b7480b502362316ea02f52cca0e9796cd"
integrity sha512-KSC5XSj5HreRhYQtZ3cnSnQwDzgnbdUDEFsxkN0m6Q3WrCRt72xrnZ8+h+pX7YxM7hr87zIO3a/v5p/H3TrnVw==
dependencies:
"@babel/types" "^7.12.13"
"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5":
version "7.11.0"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df"
@ -357,6 +372,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
"@babel/helper-validator-identifier@^7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
"@babel/helper-wrap-function@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87"
@ -1275,6 +1295,15 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@babel/types@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.13.tgz#8be1aa8f2c876da11a9cf650c0ecf656913ad611"
integrity sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==
dependencies:
"@babel/helper-validator-identifier" "^7.12.11"
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@emotion/cache@^10.0.27":
version "10.0.29"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"