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"