diff --git a/.changeset/small-laws-remember.md b/.changeset/small-laws-remember.md
deleted file mode 100644
index 0114f3acc..000000000
--- a/.changeset/small-laws-remember.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'slate-react': minor
----
-
-Added android keyboard support for slate editor
diff --git a/.eslintrc b/.eslintrc
index 33ad28423..52d68ca1d 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -151,6 +151,15 @@
}
],
"use-isnan": "error",
+ "valid-jsdoc": [
+ "error",
+ {
+ "prefer": {
+ "return": "returns"
+ },
+ "requireReturn": false
+ }
+ ],
"valid-typeof": "error",
"yield-star-spacing": [
"error",
@@ -170,4 +179,4 @@
}
}
]
-}
+}
\ No newline at end of file
diff --git a/packages/slate-react/src/components/android/ErrorBoundary.tsx b/packages/slate-react/src/components/android/ErrorBoundary.tsx
deleted file mode 100644
index 801113409..000000000
--- a/packages/slate-react/src/components/android/ErrorBoundary.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React, { PropsWithChildren } from 'react'
-
-export class ErrorBoundary extends React.Component<
- PropsWithChildren<{}>,
- never
-> {
- static getDerivedStateFromError(error: Error) {
- // Update state so the next render will show the fallback UI.
- return { hasError: true }
- }
-
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
- // eslint-disable-next-line no-console
- console.error(error)
- }
-
- render() {
- return this.props.children
- }
-}
diff --git a/packages/slate-react/src/components/android/android-editable.tsx b/packages/slate-react/src/components/android/android-editable.tsx
deleted file mode 100644
index d42fb2ac5..000000000
--- a/packages/slate-react/src/components/android/android-editable.tsx
+++ /dev/null
@@ -1,452 +0,0 @@
-import React, {
- useCallback,
- useEffect,
- useLayoutEffect,
- useMemo,
- useRef,
- useState,
-} from 'react'
-import { Descendant, Editor, Node, Range, Transforms } from 'slate'
-import throttle from 'lodash/throttle'
-import scrollIntoView from 'scroll-into-view-if-needed'
-
-import { DefaultPlaceholder, ReactEditor } from '../..'
-import { ReadOnlyContext } from '../../hooks/use-read-only'
-import { useSlate } from '../../hooks/use-slate'
-import { useIsomorphicLayoutEffect } from '../../hooks/use-isomorphic-layout-effect'
-import {
- DOMElement,
- getDefaultView,
- isPlainTextOnlyPaste,
-} from '../../utils/dom'
-import {
- EDITOR_TO_ELEMENT,
- EDITOR_TO_RESTORE_DOM,
- EDITOR_TO_WINDOW,
- ELEMENT_TO_NODE,
- IS_FOCUSED,
- IS_READ_ONLY,
- NODE_TO_ELEMENT,
- PLACEHOLDER_SYMBOL,
-} from '../../utils/weak-maps'
-import { AndroidInputManager } from './android-input-manager'
-import { EditableProps } from '../editable'
-import { ErrorBoundary } from './ErrorBoundary'
-import useChildren from '../../hooks/use-children'
-import {
- defaultDecorate,
- hasEditableTarget,
- isEventHandled,
- isTargetInsideVoid,
-} from '../editable'
-import { IS_FIREFOX } from '../../utils/environment'
-
-export const AndroidEditableNoError = (props: EditableProps): JSX.Element => {
- return (
-
-
-
- )
-}
-
-/**
- * Editable.
- */
-
-export const AndroidEditable = (props: EditableProps): JSX.Element => {
- const {
- autoFocus,
- decorate = defaultDecorate,
- onDOMBeforeInput: propsOnDOMBeforeInput,
- placeholder,
- readOnly = false,
- renderElement,
- renderLeaf,
- renderPlaceholder = props => ,
- style = {},
- as: Component = 'div',
- ...attributes
- } = props
- const editor = useSlate()
- const ref = useRef(null)
- const inputManager = useMemo(() => new AndroidInputManager(editor), [editor])
-
- // Update internal state on each render.
- IS_READ_ONLY.set(editor, readOnly)
-
- // Keep track of some state for the event handler logic.
- const state = useMemo(
- () => ({
- isComposing: false,
- isUpdatingSelection: false,
- latestElement: null as DOMElement | null,
- }),
- []
- )
-
- // Update element-related weak maps with the DOM element ref.
- useIsomorphicLayoutEffect(() => {
- 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)
- } else {
- NODE_TO_ELEMENT.delete(editor)
- }
- })
-
- // Update element-related weak maps with the DOM element ref.
- useIsomorphicLayoutEffect(() => {
- if (ref.current) {
- EDITOR_TO_ELEMENT.set(editor, ref.current)
- NODE_TO_ELEMENT.set(editor, ref.current)
- ELEMENT_TO_NODE.set(ref.current, editor)
- } else {
- NODE_TO_ELEMENT.delete(editor)
- }
- })
-
- // Whenever the editor updates, make sure the DOM selection state is in sync.
- useIsomorphicLayoutEffect(() => {
- const { selection } = editor
- const root = ReactEditor.findDocumentOrShadowRoot(editor)
- const domSelection = root.getSelection()
-
- if (state.isComposing || !domSelection || !ReactEditor.isFocused(editor)) {
- return
- }
-
- const hasDomSelection = domSelection.type !== 'None'
-
- // If the DOM selection is properly unset, we're done.
- if (!selection && !hasDomSelection) {
- return
- }
-
- // verify that the dom selection is in the editor
- const editorElement = EDITOR_TO_ELEMENT.get(editor)!
- let hasDomSelectionInEditor = false
- if (
- editorElement.contains(domSelection.anchorNode) &&
- editorElement.contains(domSelection.focusNode)
- ) {
- hasDomSelectionInEditor = true
- }
-
- // If the DOM selection is in the editor and the editor selection is already correct, we're done.
- if (hasDomSelection && hasDomSelectionInEditor && selection) {
- const slateRange = ReactEditor.toSlateRange(editor, domSelection, {
- exactMatch: true,
- })
- if (slateRange && Range.equals(slateRange, selection)) {
- return
- }
- }
-
- // when is being controlled through external value
- // then its children might just change - DOM responds to it on its own
- // but Slate's value is not being updated through any operation
- // and thus it doesn't transform selection on its own
- if (selection && !ReactEditor.hasRange(editor, selection)) {
- editor.selection = ReactEditor.toSlateRange(editor, domSelection, {
- exactMatch: false,
- })
- return
- }
-
- // Otherwise the DOM selection is out of sync, so update it.
- const el = ReactEditor.toDOMNode(editor, editor)
- state.isUpdatingSelection = true
-
- const newDomRange = selection && ReactEditor.toDOMRange(editor, selection)
-
- if (newDomRange) {
- if (Range.isBackward(selection!)) {
- domSelection.setBaseAndExtent(
- newDomRange.endContainer,
- newDomRange.endOffset,
- newDomRange.startContainer,
- newDomRange.startOffset
- )
- } else {
- domSelection.setBaseAndExtent(
- newDomRange.startContainer,
- newDomRange.startOffset,
- newDomRange.endContainer,
- newDomRange.endOffset
- )
- }
- const leafEl = newDomRange.startContainer.parentElement!
- leafEl.getBoundingClientRect = newDomRange.getBoundingClientRect.bind(
- newDomRange
- )
- scrollIntoView(leafEl, {
- scrollMode: 'if-needed',
- boundary: el,
- })
- // @ts-ignore
- delete leafEl.getBoundingClientRect
- } else {
- domSelection.removeAllRanges()
- }
-
- setTimeout(() => {
- // COMPAT: In Firefox, it's not enough to create a range, you also need
- // to focus the contenteditable element too. (2016/11/16)
- if (newDomRange && IS_FIREFOX) {
- el.focus()
- }
-
- state.isUpdatingSelection = false
- })
- })
-
- useLayoutEffect(() => {
- inputManager.onDidMount()
- return () => {
- inputManager.onWillUnmount()
- }
- }, [])
-
- const prevValue = useRef([])
- if (prevValue.current !== editor.children) {
- inputManager.onRender()
- prevValue.current = editor.children
- }
-
- useLayoutEffect(() => {
- inputManager.onDidUpdate()
- })
-
- // The autoFocus TextareaHTMLAttribute doesn't do anything on a div, so it
- // needs to be manually focused.
- useEffect(() => {
- if (ref.current && autoFocus) {
- ref.current.focus()
- }
- }, [autoFocus])
-
- // Listen on the native `selectionchange` event to be able to update any time
- // the selection changes. This is required because React's `onSelect` is leaky
- // and non-standard so it doesn't fire until after a selection has been
- // released. This causes issues in situations where another change happens
- // while a selection is being dragged.
- const onDOMSelectionChange = useCallback(
- throttle(() => {
- if (!readOnly && !state.isComposing && !state.isUpdatingSelection) {
- inputManager.onSelect()
-
- const root = ReactEditor.findDocumentOrShadowRoot(editor)
- const { activeElement } = root
- const el = ReactEditor.toDOMNode(editor, editor)
- const domSelection = root.getSelection()
-
- if (activeElement === el) {
- state.latestElement = activeElement
- IS_FOCUSED.set(editor, true)
- } else {
- IS_FOCUSED.delete(editor)
- }
-
- if (!domSelection) {
- return Transforms.deselect(editor)
- }
-
- const { anchorNode, focusNode } = domSelection
-
- const anchorNodeSelectable =
- hasEditableTarget(editor, anchorNode) ||
- isTargetInsideVoid(editor, anchorNode)
-
- const focusNodeSelectable =
- hasEditableTarget(editor, focusNode) ||
- isTargetInsideVoid(editor, focusNode)
-
- if (anchorNodeSelectable && focusNodeSelectable) {
- const range = ReactEditor.toSlateRange(editor, domSelection, {
- exactMatch: false,
- })
- Transforms.select(editor, range)
- } else {
- Transforms.deselect(editor)
- }
- }
- }, 100),
- [readOnly]
- )
-
- // Attach a native DOM event handler for `selectionchange`, because React's
- // built-in `onSelect` handler doesn't fire for all selection changes. It's a
- // leaky polyfill that only fires on keypresses or clicks. Instead, we want to
- // fire for any change to the selection inside the editor. (2019/11/04)
- // https://github.com/facebook/react/issues/5785
- useIsomorphicLayoutEffect(() => {
- window.document.addEventListener('selectionchange', onDOMSelectionChange)
-
- return () => {
- window.document.removeEventListener(
- 'selectionchange',
- onDOMSelectionChange
- )
- }
- }, [onDOMSelectionChange])
-
- const decorations = decorate([editor, []])
-
- if (
- placeholder &&
- editor.children.length === 1 &&
- Array.from(Node.texts(editor)).length === 1 &&
- Node.string(editor) === ''
- ) {
- const start = Editor.start(editor, [])
- decorations.push({
- [PLACEHOLDER_SYMBOL]: true,
- placeholder,
- anchor: start,
- focus: start,
- })
- }
-
- const [contentKey, setContentKey] = useState(0)
-
- const onRestoreDOM = useCallback(() => {
- setContentKey(prev => prev + 1)
- }, [contentKey])
- EDITOR_TO_RESTORE_DOM.set(editor, onRestoreDOM)
- useEffect(() => {
- return () => {
- EDITOR_TO_RESTORE_DOM.delete(editor)
- }
- }, [])
-
- return (
-
- ) => {
- if (
- hasEditableTarget(editor, event.target) &&
- !isEventHandled(event, attributes.onCompositionEnd)
- ) {
- state.isComposing = false
-
- inputManager.onCompositionEnd()
- }
- },
- [attributes.onCompositionEnd]
- )}
- onCompositionStart={useCallback(
- (event: React.CompositionEvent) => {
- if (
- hasEditableTarget(editor, event.target) &&
- !isEventHandled(event, attributes.onCompositionStart)
- ) {
- state.isComposing = true
-
- inputManager.onCompositionStart()
- }
- },
- [attributes.onCompositionStart]
- )}
- onCopy={useCallback(
- (event: React.ClipboardEvent) => {
- if (
- hasEditableTarget(editor, event.target) &&
- !isEventHandled(event, attributes.onCopy)
- ) {
- event.preventDefault()
- ReactEditor.setFragmentData(editor, event.clipboardData)
- }
- },
- [attributes.onCopy]
- )}
- onCut={useCallback(
- (event: React.ClipboardEvent) => {
- if (
- !readOnly &&
- hasEditableTarget(editor, event.target) &&
- !isEventHandled(event, attributes.onCut)
- ) {
- event.preventDefault()
- ReactEditor.setFragmentData(editor, event.clipboardData)
- const { selection } = editor
-
- if (selection && Range.isExpanded(selection)) {
- Editor.deleteFragment(editor)
- }
- }
- },
- [readOnly, attributes.onCut]
- )}
- onFocus={useCallback(
- (event: React.FocusEvent) => {
- if (
- !readOnly &&
- !state.isUpdatingSelection &&
- hasEditableTarget(editor, event.target) &&
- !isEventHandled(event, attributes.onFocus)
- ) {
- const el = ReactEditor.toDOMNode(editor, editor)
- state.latestElement = window.document.activeElement
-
- IS_FOCUSED.set(editor, true)
- }
- },
- [readOnly, attributes.onFocus]
- )}
- onKeyDown={useCallback(
- (event: React.KeyboardEvent) => {},
- [readOnly, attributes.onKeyDown]
- )}
- onPaste={useCallback(
- (event: React.ClipboardEvent) => {
- // This unfortunately needs to be handled with paste events instead.
- if (
- hasEditableTarget(editor, event.target) &&
- !isEventHandled(event, attributes.onPaste) &&
- isPlainTextOnlyPaste(event.nativeEvent) &&
- !readOnly
- ) {
- event.preventDefault()
- ReactEditor.insertData(editor, event.clipboardData)
- }
- },
- [readOnly, attributes.onPaste]
- )}
- >
- {useChildren({
- decorations,
- node: editor,
- renderElement,
- renderPlaceholder,
- renderLeaf,
- selection: editor.selection,
- })}
-
-
- )
-}
diff --git a/packages/slate-react/src/components/android/android-input-manager.ts b/packages/slate-react/src/components/android/android-input-manager.ts
deleted file mode 100644
index 972b264b9..000000000
--- a/packages/slate-react/src/components/android/android-input-manager.ts
+++ /dev/null
@@ -1,558 +0,0 @@
-import { ReactEditor } from '../../plugin/react-editor'
-import { Editor, Node as SlateNode, Path, Range, Transforms } from 'slate'
-import { Diff, diffText } from './diff-text'
-import { DOMNode } from '../../utils/dom'
-import {
- EDITOR_TO_ON_CHANGE,
- EDITOR_TO_RESTORE_DOM,
-} from '../../utils/weak-maps'
-
-const debug = (...message: any[]) => {}
-
-function restoreDOM(editor: ReactEditor) {
- try {
- const onRestoreDOM = EDITOR_TO_RESTORE_DOM.get(editor)
- if (onRestoreDOM) {
- onRestoreDOM()
- }
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error(err)
- }
-}
-
-function flushController(editor: ReactEditor): void {
- try {
- const onChange = EDITOR_TO_ON_CHANGE.get(editor)
- if (onChange) {
- onChange()
- }
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error(err)
- }
-}
-
-function renderSync(editor: ReactEditor, fn: () => void) {
- try {
- fn()
- flushController(editor)
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error(err)
- }
-}
-
-/**
- * Takes text from a dom node and an offset within that text and returns an
- * object with fixed text and fixed offset which removes zero width spaces
- * and adjusts the offset.
- *
- * Optionally, if an `isLastNode` argument is passed in, it will also remove
- * a trailing newline.
- */
-
-function fixTextAndOffset(
- prevText: string,
- prevOffset = 0,
- isLastNode = false
-) {
- let nextOffset = prevOffset
- let nextText = prevText
-
- // remove the last newline if we are in the last node of a block
- const lastChar = nextText.charAt(nextText.length - 1)
-
- if (isLastNode && lastChar === '\n') {
- nextText = nextText.slice(0, -1)
- }
-
- const maxOffset = nextText.length
-
- if (nextOffset > maxOffset) nextOffset = maxOffset
- return { text: nextText, offset: nextOffset }
-}
-
-/**
- * Based loosely on:
- *
- * https://github.com/facebook/draft-js/blob/master/src/component/handlers/composition/DOMObserver.js
- * https://github.com/ProseMirror/prosemirror-view/blob/master/src/domobserver.js
- *
- * But is an analysis mainly for `backspace` and `enter` as we handle
- * compositions as a single operation.
- *
- * @param editor
- */
-
-export class AndroidInputManager {
- /**
- * A MutationObserver that flushes to the method `flush`
- */
- private readonly observer: MutationObserver
-
- private rootEl?: HTMLElement = undefined
-
- /**
- * Object that keeps track of the most recent state
- */
-
- private lastPath?: Path = undefined
- private lastDiff?: Diff = undefined
- private lastRange?: Range = undefined
- private lastDomNode?: Node = undefined
-
- constructor(private editor: ReactEditor) {
- this.observer = new MutationObserver(this.flush)
- }
-
- onDidMount = () => {
- this.connect()
- }
-
- onDidUpdate = () => {
- this.connect()
- }
-
- /**
- * Connect the MutationObserver to a specific editor root element
- */
-
- connect = () => {
- debug('connect')
-
- const rootEl = ReactEditor.toDOMNode(this.editor, this.editor)
- if (this.rootEl === rootEl) return
- this.rootEl = rootEl
-
- debug('connect:run')
-
- this.observer.disconnect()
- this.observer.observe(rootEl, {
- childList: true,
- characterData: true,
- subtree: true,
- characterDataOldValue: true,
- })
- }
-
- onWillUnmount = () => {
- this.disconnect()
- }
-
- disconnect = () => {
- debug('disconnect')
- this.observer.disconnect()
- this.rootEl = undefined
- }
-
- onRender = () => {
- this.disconnect()
- this.clearDiff()
- }
-
- private clearDiff = () => {
- debug('clearDiff')
- this.bufferedMutations.length = 0
- this.lastPath = undefined
- this.lastDiff = undefined
- }
-
- /**
- * Clear the `last` properties related to an action only
- */
-
- private clearAction = () => {
- debug('clearAction')
-
- this.bufferedMutations.length = 0
- this.lastDiff = undefined
- this.lastDomNode = undefined
- }
-
- /**
- * Apply the last `diff`
- *
- * We don't want to apply the `diff` at the time it is created because we
- * may be in a composition. There are a few things that trigger the applying
- * of the saved diff. Sometimes on its own and sometimes immediately before
- * doing something else with the Editor.
- *
- * - `onCompositionEnd` event
- * - `onSelect` event only when the user has moved into a different node
- * - The user hits `enter`
- * - The user hits `backspace` and removes an inline node
- * - The user hits `backspace` and merges two blocks
- */
-
- private applyDiff = () => {
- debug('applyDiff')
- if (this.lastPath === undefined || this.lastDiff === undefined) return
- debug('applyDiff:run')
- const range: Range = {
- anchor: { path: this.lastPath, offset: this.lastDiff.start },
- focus: { path: this.lastPath, offset: this.lastDiff.end },
- }
-
- Transforms.insertText(this.editor, this.lastDiff.insertText, { at: range })
- }
-
- /**
- * Handle `enter` that splits block
- */
-
- private splitBlock = () => {
- debug('splitBlock')
-
- renderSync(this.editor, () => {
- this.applyDiff()
-
- Transforms.splitNodes(this.editor, { always: true })
- ReactEditor.focus(this.editor)
-
- this.clearAction()
- restoreDOM(this.editor)
- flushController(this.editor)
- })
- }
-
- /**
- * Handle `backspace` that merges blocks
- */
-
- private mergeBlock = () => {
- debug('mergeBlock')
-
- /**
- * The delay is required because hitting `enter`, `enter` then `backspace`
- * in a word results in the cursor being one position to the right in
- * Android 9.
- *
- * Slate sets the position to `0` and we even check it immediately after
- * setting it and it is correct, but somewhere Android moves it to the right.
- *
- * This happens only when using the virtual keyboard. Hitting enter on a
- * hardware keyboard does not trigger this bug.
- *
- * The call to `focus` is required because when we switch examples then
- * merge a block, we lose focus in Android 9 (possibly others).
- */
-
- window.requestAnimationFrame(() => {
- renderSync(this.editor, () => {
- this.applyDiff()
-
- Transforms.select(this.editor, this.lastRange!)
- Editor.deleteBackward(this.editor)
- ReactEditor.focus(this.editor)
-
- this.clearAction()
- restoreDOM(this.editor)
- flushController(this.editor)
- })
- })
- }
-
- /**
- * The requestId used to the save selection
- */
-
- private onSelectTimeoutId: number | null = null
- private bufferedMutations: MutationRecord[] = []
- private startActionFrameId: number | null = null
- private isFlushing = false
-
- /**
- * Mark the beginning of an action. The action happens when the
- * `requestAnimationFrame` expires.
- *
- * If `onKeyDown` is called again, it pushes the `action` to a new
- * `requestAnimationFrame` and cancels the old one.
- */
-
- private startAction = () => {
- debug('startAction')
- if (this.onSelectTimeoutId) {
- window.cancelAnimationFrame(this.onSelectTimeoutId)
- this.onSelectTimeoutId = null
- }
-
- this.isFlushing = true
-
- if (this.startActionFrameId) {
- window.cancelAnimationFrame(this.startActionFrameId)
- }
-
- this.startActionFrameId = window.requestAnimationFrame((): void => {
- if (this.bufferedMutations.length > 0) {
- this.flushAction(this.bufferedMutations)
- }
-
- this.startActionFrameId = null
- this.bufferedMutations.length = 0
- this.isFlushing = false
- })
- }
-
- /**
- * Handle MutationObserver flush
- *
- * @param mutations
- */
-
- flush = (mutations: MutationRecord[]) => {
- debug('flush')
- this.bufferedMutations.push(...mutations)
- this.startAction()
- }
-
- /**
- * Handle a `requestAnimationFrame` long batch of mutations.
- *
- * @param mutations
- */
-
- private flushAction = (mutations: MutationRecord[]) => {
- try {
- debug('flushAction', mutations.length, mutations)
-
- const removedNodes = mutations.filter(
- mutation => mutation.removedNodes.length > 0
- ).length
- const addedNodes = mutations.filter(
- mutation => mutation.addedNodes.length > 0
- ).length
-
- if (removedNodes > addedNodes) {
- this.mergeBlock()
- } else if (addedNodes > removedNodes) {
- this.splitBlock()
- } else {
- this.resolveDOMNode(mutations[0].target.parentNode!)
- }
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error(err)
- }
- }
-
- /**
- * Takes a DOM Node and resolves it against Slate's Document.
- *
- * Saves the changes to `last.diff` which can be applied later using
- * `applyDiff()`
- *
- * @param domNode
- */
-
- private resolveDOMNode = (domNode: DOMNode) => {
- debug('resolveDOMNode')
- let node
- try {
- node = ReactEditor.toSlateNode(this.editor, domNode)
- } catch (e) {
- // not in react model yet.
- return
- }
- const path = ReactEditor.findPath(this.editor, node)
- const prevText = SlateNode.string(node)
-
- // COMPAT: If this is the last leaf, and the DOM text ends in a new line,
- // we will have added another new line in 's render method to account
- // for browsers collapsing a single trailing new lines, so remove it.
- const [block] = Editor.parent(
- this.editor,
- ReactEditor.findPath(this.editor, node)
- )
- const isLastNode = block.children[block.children.length - 1] === node
-
- const fix = fixTextAndOffset(domNode.textContent!, 0, isLastNode)
-
- const nextText = fix.text
-
- debug('resolveDOMNode:pre:post', prevText, nextText)
-
- // If the text is no different, there is no diff.
- if (nextText === prevText) {
- this.lastDiff = undefined
- return
- }
-
- const diff = diffText(prevText, nextText)
- if (diff === null) {
- this.lastDiff = undefined
- return
- }
-
- this.lastPath = path
- this.lastDiff = diff
-
- debug('resolveDOMNode:diff', this.lastDiff)
- }
-
- /**
- * handle `onCompositionStart`
- */
-
- onCompositionStart = () => {
- debug('onCompositionStart')
- }
-
- /**
- * handle `onCompositionEnd`
- */
-
- onCompositionEnd = () => {
- debug('onCompositionEnd')
-
- /**
- * The timing on the `setTimeout` with `20` ms is sensitive.
- *
- * It cannot use `requestAnimationFrame` because it is too short.
- *
- * Android 9, for example, when you type `it ` the space will first trigger
- * a `compositionEnd` for the `it` part before the mutation for the ` `.
- * This means that we end up with `it` if we trigger too soon because it
- * is on the wrong value.
- */
-
- window.setTimeout(() => {
- if (this.lastDiff !== undefined) {
- debug('onCompositionEnd:applyDiff')
-
- renderSync(this.editor, () => {
- this.applyDiff()
-
- const domRange = window.getSelection()!.getRangeAt(0)
- const domText = domRange.startContainer.textContent!
- const offset = domRange.startOffset
-
- const fix = fixTextAndOffset(domText, offset)
-
- let range = ReactEditor.toSlateRange(this.editor, domRange, {
- exactMatch: true,
- })
- if (range !== null) {
- range = {
- ...range,
- anchor: {
- ...range.anchor,
- offset: fix.offset,
- },
- focus: {
- ...range.focus,
- offset: fix.offset,
- },
- }
-
- /**
- * We must call `restoreDOM` even though this is applying a `diff` which
- * should not require it. But if you type `it me. no.` on a blank line
- * with a block following it, the next line will merge with the this
- * line. A mysterious `keydown` with `input` of backspace appears in the
- * event stream which the user not React caused.
- *
- * `focus` is required as well because otherwise we lose focus on hitting
- * `enter` in such a scenario.
- */
-
- Transforms.select(this.editor, range)
- ReactEditor.focus(this.editor)
- }
-
- this.clearAction()
- restoreDOM(this.editor)
- })
- }
- }, 20)
- }
-
- /**
- * Handle `onSelect` event
- *
- * Save the selection after a `requestAnimationFrame`
- *
- * - If we're not in the middle of flushing mutations
- * - and cancel save if a mutation runs before the `requestAnimationFrame`
- */
-
- onSelect = () => {
- debug('onSelect:try')
-
- if (this.onSelectTimeoutId !== null) {
- window.cancelAnimationFrame(this.onSelectTimeoutId)
- this.onSelectTimeoutId = null
- }
-
- // Don't capture the last selection if the selection was made during the
- // flushing of DOM mutations. This means it is all part of one user action.
- if (this.isFlushing) return
-
- this.onSelectTimeoutId = window.requestAnimationFrame(() => {
- debug('onSelect:save-selection')
-
- const domSelection = window.getSelection()
- if (
- domSelection === null ||
- domSelection.anchorNode === null ||
- domSelection.anchorNode.textContent === null ||
- domSelection.focusNode === null ||
- domSelection.focusNode.textContent === null
- )
- return
-
- const { offset: anchorOffset } = fixTextAndOffset(
- domSelection.anchorNode.textContent,
- domSelection.anchorOffset
- )
- const { offset: focusOffset } = fixTextAndOffset(
- domSelection.focusNode!.textContent!,
- domSelection.focusOffset
- )
- let range = ReactEditor.toSlateRange(this.editor, domSelection, {
- exactMatch: true,
- })
- if (range !== null) {
- range = {
- focus: {
- path: range.focus.path,
- offset: focusOffset,
- },
- anchor: {
- path: range.anchor.path,
- offset: anchorOffset,
- },
- }
-
- debug('onSelect:save-data', {
- anchorNode: domSelection.anchorNode,
- anchorOffset: domSelection.anchorOffset,
- focusNode: domSelection.focusNode,
- focusOffset: domSelection.focusOffset,
- range,
- })
-
- // If the `domSelection` has moved into a new node, then reconcile with
- // `applyDiff`
- if (
- domSelection.isCollapsed &&
- this.lastDomNode !== domSelection.anchorNode &&
- this.lastDiff !== undefined
- ) {
- debug('onSelect:applyDiff', this.lastDiff)
- this.applyDiff()
- Transforms.select(this.editor, range)
-
- this.clearAction()
- flushController(this.editor)
- restoreDOM(this.editor)
- }
-
- this.lastRange = range
- this.lastDomNode = domSelection.anchorNode
- }
- })
- }
-}
-
-export default AndroidInputManager
diff --git a/packages/slate-react/src/components/android/diff-text.ts b/packages/slate-react/src/components/android/diff-text.ts
deleted file mode 100644
index 72f5530c1..000000000
--- a/packages/slate-react/src/components/android/diff-text.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * Returns the number of characters that are the same at the beginning of the
- * String.
- *
- * @param prev the previous text
- * @param next the next text
- * @returns the offset of the start of the difference; null if there is no difference
- */
-function getDiffStart(prev: string, next: string): number | null {
- const length = Math.min(prev.length, next.length)
-
- for (let i = 0; i < length; i++) {
- if (prev.charAt(i) !== next.charAt(i)) return i
- }
-
- if (prev.length !== next.length) return length
- return null
-}
-
-/**
- * Returns the number of characters that are the same at the end of the String
- * up to `max`. Max prevents double-counting characters when there are
- * multiple duplicate characters around the diff area.
- *
- * @param prev the previous text
- * @param next the next text
- * @param max the max length to test.
- * @returns number of characters that are the same at the end of the string
- */
-function getDiffEnd(prev: string, next: string, max: number): number | null {
- const prevLength = prev.length
- const nextLength = next.length
- const length = Math.min(prevLength, nextLength, max)
-
- for (let i = 0; i < length; i++) {
- const prevChar = prev.charAt(prevLength - i - 1)
- const nextChar = next.charAt(nextLength - i - 1)
- if (prevChar !== nextChar) return i
- }
-
- if (prev.length !== next.length) return length
- return null
-}
-
-type TextRange = {
- start: number
- end: number
-}
-
-/**
- * Takes two strings and returns an object representing two offsets. The
- * first, `start` represents the number of characters that are the same at
- * the front of the String. The `end` represents the number of characters
- * that are the same at the end of the String.
- *
- * Returns null if they are identical.
- *
- * @param prev the previous text
- * @param next the next text
- * @returns the difference text range; null if there are no differences.
- */
-function getDiffOffsets(prev: string, next: string): TextRange | null {
- if (prev === next) return null
- const start = getDiffStart(prev, next)
- if (start === null) return null
- const maxEnd = Math.min(prev.length - start, next.length - start)
- const end = getDiffEnd(prev, next, maxEnd)!
- if (end === null) return null
- return { start, end }
-}
-
-/**
- * Takes a text string and returns a slice from the string at the given text range
- *
- * @param text the text
- * @param offsets the text range
- * @returns the text slice at text range
- */
-function sliceText(text: string, offsets: TextRange): string {
- return text.slice(offsets.start, text.length - offsets.end)
-}
-
-/**
- * Takes two strings and returns a smart diff that can be used to describe the
- * change in a way that can be used as operations like inserting, removing or
- * replacing text.
- *
- * @param prev the previous text
- * @param next the next text
- * @returns the text difference
- */
-export function diffText(prev?: string, next?: string): Diff | null {
- if (prev === undefined || next === undefined) return null
- const offsets = getDiffOffsets(prev, next)
- if (offsets == null) return null
- const insertText = sliceText(next, offsets)
- const removeText = sliceText(prev, offsets)
- return {
- start: offsets.start,
- end: prev.length - offsets.end,
- insertText,
- removeText,
- }
-}
-
-export type Diff = {
- start: number
- end: number
- insertText: string
- removeText: string
-}
diff --git a/packages/slate-react/src/components/android/index.ts b/packages/slate-react/src/components/android/index.ts
deleted file mode 100644
index 4f6f30ec8..000000000
--- a/packages/slate-react/src/components/android/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { AndroidEditable, AndroidEditableNoError } from './android-editable'
diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx
index b2fe5139c..e08d2ebd5 100644
--- a/packages/slate-react/src/components/editable.tsx
+++ b/packages/slate-react/src/components/editable.tsx
@@ -1137,13 +1137,13 @@ export const DefaultPlaceholder = ({
* A default memoized decorate function.
*/
-export const defaultDecorate: (entry: NodeEntry) => Range[] = () => []
+const defaultDecorate: (entry: NodeEntry) => Range[] = () => []
/**
* Check if two DOM range objects are equal.
*/
-export const isRangeEqual = (a: DOMRange, b: DOMRange) => {
+const isRangeEqual = (a: DOMRange, b: DOMRange) => {
return (
(a.startContainer === b.startContainer &&
a.startOffset === b.startOffset &&
@@ -1160,7 +1160,7 @@ export const isRangeEqual = (a: DOMRange, b: DOMRange) => {
* Check if the target is in the editor.
*/
-export const hasTarget = (
+const hasTarget = (
editor: ReactEditor,
target: EventTarget | null
): target is DOMNode => {
@@ -1171,7 +1171,7 @@ export const hasTarget = (
* Check if the target is editable and in the editor.
*/
-export const hasEditableTarget = (
+const hasEditableTarget = (
editor: ReactEditor,
target: EventTarget | null
): target is DOMNode => {
@@ -1185,7 +1185,7 @@ export const hasEditableTarget = (
* Check if the target is inside void and in the editor.
*/
-export const isTargetInsideVoid = (
+const isTargetInsideVoid = (
editor: ReactEditor,
target: EventTarget | null
): boolean => {
@@ -1198,7 +1198,7 @@ export const isTargetInsideVoid = (
* Check if an event is overrided by a handler.
*/
-export const isEventHandled = <
+const isEventHandled = <
EventType extends React.SyntheticEvent
>(
event: EventType,
@@ -1216,7 +1216,7 @@ export const isEventHandled = <
* Check if a DOM event is overrided by a handler.
*/
-export const isDOMEventHandled = (
+const isDOMEventHandled = (
event: E,
handler?: (event: E) => void
) => {
diff --git a/packages/slate-react/src/index.ts b/packages/slate-react/src/index.ts
index 4297ffecc..8e9c46b5d 100644
--- a/packages/slate-react/src/index.ts
+++ b/packages/slate-react/src/index.ts
@@ -1,12 +1,8 @@
// Components
-// Environment-dependent Editable
-import { Editable as DefaultEditable } from './components/editable'
-import { AndroidEditableNoError as AndroidEditable } from './components/android/android-editable'
-import { IS_ANDROID } from './utils/environment'
-
export {
RenderElementProps,
RenderLeafProps,
+ Editable,
RenderPlaceholderProps,
DefaultPlaceholder,
} from './components/editable'
@@ -25,5 +21,3 @@ export { useSlate } from './hooks/use-slate'
// Plugin
export { ReactEditor } from './plugin/react-editor'
export { withReact } from './plugin/with-react'
-export const Editable = !IS_ANDROID ? DefaultEditable : AndroidEditable
-export { DefaultEditable, AndroidEditable }
diff --git a/packages/slate-react/src/utils/environment.ts b/packages/slate-react/src/utils/environment.ts
index f775a30b6..d11c8d245 100644
--- a/packages/slate-react/src/utils/environment.ts
+++ b/packages/slate-react/src/utils/environment.ts
@@ -7,9 +7,6 @@ export const IS_IOS =
export const IS_APPLE =
typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
-export const IS_ANDROID =
- typeof navigator !== 'undefined' && /Android/.test(navigator.userAgent)
-
export const IS_FIREFOX =
typeof navigator !== 'undefined' &&
/^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent)
diff --git a/packages/slate-react/src/utils/lines.ts b/packages/slate-react/src/utils/lines.ts
index 48bd592b2..960d77a55 100644
--- a/packages/slate-react/src/utils/lines.ts
+++ b/packages/slate-react/src/utils/lines.ts
@@ -26,9 +26,9 @@ const areRangesSameLine = (
* A helper utility that returns the end portion of a `Range`
* which is located on a single line.
*
- * @param editor The editor object to compare against
- * @param parentRange The parent range to compare against
- * @returns A valid portion of the parentRange which is one a single line
+ * @param {Editor} editor The editor object to compare against
+ * @param {Range} parentRange The parent range to compare against
+ * @returns {Range} A valid portion of the parentRange which is one a single line
*/
export const findCurrentLineRange = (
editor: ReactEditor,
diff --git a/packages/slate-react/src/utils/weak-maps.ts b/packages/slate-react/src/utils/weak-maps.ts
index b5c294664..c2fa295ad 100644
--- a/packages/slate-react/src/utils/weak-maps.ts
+++ b/packages/slate-react/src/utils/weak-maps.ts
@@ -37,8 +37,6 @@ export const IS_CLICKING: WeakMap = new WeakMap()
export const EDITOR_TO_ON_CHANGE = new WeakMap void>()
-export const EDITOR_TO_RESTORE_DOM = new WeakMap void>()
-
/**
* Symbols.
*/
diff --git a/site/examples/images.tsx b/site/examples/images.tsx
index b5f920f62..595377c96 100644
--- a/site/examples/images.tsx
+++ b/site/examples/images.tsx
@@ -1,13 +1,13 @@
-import React, { useMemo, useState } from 'react'
+import React, { useState, useMemo } from 'react'
import imageExtensions from 'image-extensions'
import isUrl from 'is-url'
import { Transforms, createEditor, Descendant } from 'slate'
import {
- Editable,
Slate,
- useFocused,
- useSelected,
+ Editable,
useSlateStatic,
+ useSelected,
+ useFocused,
withReact,
} from 'slate-react'
import { withHistory } from 'slate-history'
diff --git a/site/examples/richtext.tsx b/site/examples/richtext.tsx
index 21193fec0..243a182b0 100644
--- a/site/examples/richtext.tsx
+++ b/site/examples/richtext.tsx
@@ -1,12 +1,12 @@
import React, { useCallback, useMemo, useState } from 'react'
import isHotkey from 'is-hotkey'
-import { Editable, Slate, useSlate, withReact } from 'slate-react'
+import { Editable, withReact, useSlate, Slate } from 'slate-react'
import {
+ Editor,
+ Transforms,
createEditor,
Descendant,
- Editor,
Element as SlateElement,
- Transforms,
} from 'slate'
import { withHistory } from 'slate-history'
@@ -28,7 +28,7 @@ const RichTextExample = () => {
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
return (
-
+ setValue(value)}>
diff --git a/yarn.lock b/yarn.lock
index 9d67dbb42..771e2ca19 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2592,19 +2592,18 @@
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/react-dom@^16.9.4":
- version "16.9.11"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.11.tgz#752e223a1592a2c10f2668b215a0e0667f4faab1"
- integrity sha512-3UuR4MoWf5spNgrG6cwsmT9DdRghcR4IDFOzNZ6+wcmacxkFykcb5ji0nNVm9ckBT4BCxvCrJJbM4+EYsEEVIg==
+ version "16.9.8"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423"
+ integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==
dependencies:
- "@types/react" "^16"
+ "@types/react" "*"
-"@types/react@^16", "@types/react@^16.9.13":
- version "16.14.5"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.5.tgz#2c39b5cadefaf4829818f9219e5e093325979f4d"
- integrity sha512-YRRv9DNZhaVTVRh9Wmmit7Y0UFhEVqXqCSw3uazRWMxa2x85hWQZ5BN24i7GXZbaclaLXEcodEeIHsjBA8eAMw==
+"@types/react@*", "@types/react@^16.9.13":
+ version "16.9.46"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.46.tgz#f0326cd7adceda74148baa9bff6e918632f5069e"
+ integrity sha512-dbHzO3aAq1lB3jRQuNpuZ/mnu+CdD3H0WVaaBQA8LTT3S33xhVBUj232T8M3tAhSWJs/D/UqORYUlJNl/8VQZg==
dependencies:
"@types/prop-types" "*"
- "@types/scheduler" "*"
csstype "^3.0.2"
"@types/resolve@0.0.8":
@@ -2614,11 +2613,6 @@
dependencies:
"@types/node" "*"
-"@types/scheduler@*":
- version "0.16.1"
- resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
- integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
-
"@types/semver@^6.0.0":
version "6.2.2"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.2.2.tgz#5c27df09ca39e3c9beb4fae6b95f4d71426df0a9"
@@ -9545,9 +9539,9 @@ randomfill@^1.0.3:
safe-buffer "^5.1.0"
react-dom@^16.12.0:
- version "16.14.0"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
- integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
+ integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@@ -9609,9 +9603,9 @@ react-values@^0.3.0:
integrity sha512-K0SWzJBIuEDwWtDDZqbEm8XWaSy3LUJB7hZm1iHUo6wTwWQWD28TEn/T9YkbrJHrw2PNwZFL3nMKjkk09BmbqA==
react@^16.12.0:
- version "16.14.0"
- resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
- integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
+ integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"