mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-04-13 18:01:53 +02:00
* 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:
parent
6d66d87f67
commit
513771c82a
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
158
site/examples/iframe.js
Normal 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
|
@ -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 => (
|
||||
|
@ -70,6 +70,11 @@ input:focus {
|
||||
border-color: blue;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
[data-slate-editor] > * + * {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
29
yarn.lock
29
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user