mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-30 10:29:48 +02:00
Refactor editor methods and fix JSDoc (#5307)
* feat * fix * docs * feat * Create two-books-bow.md * fix * feat * feat * fix * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * refactor * docs * docs * 🔀 * 🔀
This commit is contained in:
@@ -20,12 +20,14 @@ import {
|
||||
Text,
|
||||
Transforms,
|
||||
} from 'slate'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager'
|
||||
import useChildren from '../hooks/use-children'
|
||||
import { DecorateContext } from '../hooks/use-decorate'
|
||||
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
|
||||
import { ReadOnlyContext } from '../hooks/use-read-only'
|
||||
import { useSlate } from '../hooks/use-slate'
|
||||
import { useTrackUserInput } from '../hooks/use-track-user-input'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
import { TRIPLE_CLICK } from '../utils/constants'
|
||||
import {
|
||||
DOMElement,
|
||||
@@ -53,7 +55,6 @@ import {
|
||||
EDITOR_TO_ELEMENT,
|
||||
EDITOR_TO_FORCE_RENDER,
|
||||
EDITOR_TO_PENDING_INSERTION_MARKS,
|
||||
EDITOR_TO_PLACEHOLDER_ELEMENT,
|
||||
EDITOR_TO_USER_MARKS,
|
||||
EDITOR_TO_USER_SELECTION,
|
||||
EDITOR_TO_WINDOW,
|
||||
@@ -66,8 +67,6 @@ import {
|
||||
PLACEHOLDER_SYMBOL,
|
||||
} from '../utils/weak-maps'
|
||||
import { RestoreDOM } from './restore-dom/restore-dom'
|
||||
import { useAndroidInputManager } from '../hooks/android-input-manager/use-android-input-manager'
|
||||
import { useTrackUserInput } from '../hooks/use-track-user-input'
|
||||
import { AndroidInputManager } from '../hooks/android-input-manager/android-input-manager'
|
||||
|
||||
type DeferredOperation = () => void
|
||||
|
@@ -1,24 +1,24 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import getDirection from 'direction'
|
||||
import { Editor, Node, Range, Element as SlateElement } from 'slate'
|
||||
|
||||
import Text from './text'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Editor, Element as SlateElement, Node, Range } from 'slate'
|
||||
import { ReactEditor, useReadOnly, useSlateStatic } from '..'
|
||||
import useChildren from '../hooks/use-children'
|
||||
import { ReactEditor, useSlateStatic, useReadOnly } from '..'
|
||||
import {
|
||||
NODE_TO_ELEMENT,
|
||||
ELEMENT_TO_NODE,
|
||||
NODE_TO_PARENT,
|
||||
NODE_TO_INDEX,
|
||||
EDITOR_TO_KEY_TO_ELEMENT,
|
||||
} from '../utils/weak-maps'
|
||||
import { isElementDecorationsEqual } from '../utils/range-list'
|
||||
import {
|
||||
EDITOR_TO_KEY_TO_ELEMENT,
|
||||
ELEMENT_TO_NODE,
|
||||
NODE_TO_ELEMENT,
|
||||
NODE_TO_INDEX,
|
||||
NODE_TO_PARENT,
|
||||
} from '../utils/weak-maps'
|
||||
import {
|
||||
RenderElementProps,
|
||||
RenderLeafProps,
|
||||
RenderPlaceholderProps,
|
||||
} from './editable'
|
||||
|
||||
import Text from './text'
|
||||
|
||||
/**
|
||||
* Element.
|
||||
*/
|
||||
|
@@ -1,16 +1,16 @@
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { Editor, Node, Descendant, Scrubber } from 'slate'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Descendant, Editor, Node, Scrubber } from 'slate'
|
||||
import { FocusedContext } from '../hooks/use-focused'
|
||||
import { EditorContext } from '../hooks/use-slate-static'
|
||||
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
|
||||
import { SlateContext, SlateContextValue } from '../hooks/use-slate'
|
||||
import {
|
||||
useSelectorContext,
|
||||
SlateSelectorContext,
|
||||
} from '../hooks/use-slate-selector'
|
||||
import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps'
|
||||
import { EditorContext } from '../hooks/use-slate-static'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
import { IS_REACT_VERSION_17_OR_ABOVE } from '../utils/environment'
|
||||
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
|
||||
import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps'
|
||||
|
||||
/**
|
||||
* A wrapper around the provider to handle `onChange` events, because the editor
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import React, { useRef, useCallback } from 'react'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import { Element, Range, Text as SlateText } from 'slate'
|
||||
import { ReactEditor, useSlateStatic } from '..'
|
||||
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
|
||||
import { isTextDecorationsEqual } from '../utils/range-list'
|
||||
import {
|
||||
EDITOR_TO_KEY_TO_ELEMENT,
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import React from 'react'
|
||||
import { Editor, Range, Element, Ancestor, Descendant } from 'slate'
|
||||
|
||||
import ElementComponent from '../components/element'
|
||||
import TextComponent from '../components/text'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
import { useSlateStatic } from './use-slate-static'
|
||||
import { useDecorate } from './use-decorate'
|
||||
import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'
|
||||
import { Ancestor, Descendant, Editor, Element, Range } from 'slate'
|
||||
import {
|
||||
RenderElementProps,
|
||||
RenderLeafProps,
|
||||
RenderPlaceholderProps,
|
||||
} from '../components/editable'
|
||||
|
||||
import ElementComponent from '../components/element'
|
||||
import TextComponent from '../components/text'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'
|
||||
import { useDecorate } from './use-decorate'
|
||||
import { SelectedContext } from './use-selected'
|
||||
import { useSlateStatic } from './use-slate-static'
|
||||
|
||||
/**
|
||||
* Children.
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { RefObject, useEffect, useState } from 'react'
|
||||
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect'
|
||||
import { isDOMElement } from '../utils/dom'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
|
||||
export function useMutationObserver(
|
||||
node: RefObject<HTMLElement>,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
import { Editor } from 'slate'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
|
||||
/**
|
||||
* A React context for sharing the editor object.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef, useEffect } from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
import { useSlateStatic } from './use-slate-static'
|
||||
|
||||
|
@@ -1,30 +1,15 @@
|
||||
import {
|
||||
BaseEditor,
|
||||
Editor,
|
||||
Element,
|
||||
Node,
|
||||
Path,
|
||||
Point,
|
||||
Range,
|
||||
Scrubber,
|
||||
Transforms,
|
||||
Element,
|
||||
} from 'slate'
|
||||
|
||||
import { Key } from '../utils/key'
|
||||
import {
|
||||
EDITOR_TO_ELEMENT,
|
||||
ELEMENT_TO_NODE,
|
||||
IS_FOCUSED,
|
||||
IS_READ_ONLY,
|
||||
NODE_TO_INDEX,
|
||||
NODE_TO_KEY,
|
||||
NODE_TO_PARENT,
|
||||
EDITOR_TO_WINDOW,
|
||||
EDITOR_TO_KEY_TO_ELEMENT,
|
||||
IS_COMPOSING,
|
||||
EDITOR_TO_SCHEDULE_FLUSH,
|
||||
EDITOR_TO_PENDING_DIFFS,
|
||||
} from '../utils/weak-maps'
|
||||
import { TextDiff } from '../utils/diff-text'
|
||||
import {
|
||||
DOMElement,
|
||||
DOMNode,
|
||||
@@ -32,73 +17,360 @@ import {
|
||||
DOMRange,
|
||||
DOMSelection,
|
||||
DOMStaticRange,
|
||||
isDOMElement,
|
||||
isDOMSelection,
|
||||
isDOMNode,
|
||||
normalizeDOMPoint,
|
||||
hasShadowRoot,
|
||||
DOMText,
|
||||
hasShadowRoot,
|
||||
isDOMElement,
|
||||
isDOMNode,
|
||||
isDOMSelection,
|
||||
normalizeDOMPoint,
|
||||
} from '../utils/dom'
|
||||
import { IS_CHROME, IS_FIREFOX, IS_ANDROID } from '../utils/environment'
|
||||
import { IS_ANDROID, IS_CHROME, IS_FIREFOX } from '../utils/environment'
|
||||
|
||||
import { Key } from '../utils/key'
|
||||
import {
|
||||
EDITOR_TO_ELEMENT,
|
||||
EDITOR_TO_KEY_TO_ELEMENT,
|
||||
EDITOR_TO_PENDING_DIFFS,
|
||||
EDITOR_TO_SCHEDULE_FLUSH,
|
||||
EDITOR_TO_WINDOW,
|
||||
ELEMENT_TO_NODE,
|
||||
IS_COMPOSING,
|
||||
IS_FOCUSED,
|
||||
IS_READ_ONLY,
|
||||
NODE_TO_INDEX,
|
||||
NODE_TO_KEY,
|
||||
NODE_TO_PARENT,
|
||||
} from '../utils/weak-maps'
|
||||
|
||||
/**
|
||||
* A React and DOM-specific version of the `Editor` interface.
|
||||
*/
|
||||
|
||||
export interface ReactEditor extends BaseEditor {
|
||||
insertData: (data: DataTransfer) => void
|
||||
insertFragmentData: (data: DataTransfer) => boolean
|
||||
insertTextData: (data: DataTransfer) => boolean
|
||||
setFragmentData: (
|
||||
data: DataTransfer,
|
||||
originEvent?: 'drag' | 'copy' | 'cut'
|
||||
) => void
|
||||
hasRange: (editor: ReactEditor, range: Range) => boolean
|
||||
hasTarget: (
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
) => target is DOMNode
|
||||
hasEditableTarget: (
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
) => target is DOMNode
|
||||
hasRange: (editor: ReactEditor, range: Range) => boolean
|
||||
hasSelectableTarget: (
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
) => boolean
|
||||
hasTarget: (
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
) => target is DOMNode
|
||||
insertData: (data: DataTransfer) => void
|
||||
insertFragmentData: (data: DataTransfer) => boolean
|
||||
insertTextData: (data: DataTransfer) => boolean
|
||||
isTargetInsideNonReadonlyVoid: (
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
) => boolean
|
||||
setFragmentData: (
|
||||
data: DataTransfer,
|
||||
originEvent?: 'drag' | 'copy' | 'cut'
|
||||
) => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const ReactEditor = {
|
||||
export interface ReactEditorInterface {
|
||||
/**
|
||||
* Check if the user is currently composing inside the editor.
|
||||
* Experimental and android specific: Get pending diffs
|
||||
*/
|
||||
|
||||
isComposing(editor: ReactEditor): boolean {
|
||||
return !!IS_COMPOSING.get(editor)
|
||||
},
|
||||
androidPendingDiffs: (editor: Editor) => TextDiff[] | undefined
|
||||
|
||||
/**
|
||||
* Return the host window of the current editor.
|
||||
* Experimental and android specific: Flush all pending diffs and cancel composition at the next possible time.
|
||||
*/
|
||||
androidScheduleFlush: (editor: Editor) => void
|
||||
|
||||
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
|
||||
},
|
||||
/**
|
||||
* Blur the editor.
|
||||
*/
|
||||
blur: (editor: ReactEditor) => void
|
||||
|
||||
/**
|
||||
* Deselect the editor.
|
||||
*/
|
||||
deselect: (editor: ReactEditor) => void
|
||||
|
||||
/**
|
||||
* Find the DOM node that implements DocumentOrShadowRoot for the editor.
|
||||
*/
|
||||
findDocumentOrShadowRoot: (editor: ReactEditor) => Document | ShadowRoot
|
||||
|
||||
/**
|
||||
* Get the target range from a DOM `event`.
|
||||
*/
|
||||
findEventRange: (editor: ReactEditor, event: any) => Range
|
||||
|
||||
/**
|
||||
* Find a key for a Slate node.
|
||||
*/
|
||||
findKey: (editor: ReactEditor, node: Node) => Key
|
||||
|
||||
findKey(editor: ReactEditor, node: Node): Key {
|
||||
/**
|
||||
* Find the path of Slate node.
|
||||
*/
|
||||
findPath: (editor: ReactEditor, node: Node) => Path
|
||||
|
||||
/**
|
||||
* Focus the editor.
|
||||
*/
|
||||
focus: (editor: ReactEditor) => void
|
||||
|
||||
/**
|
||||
* Return the host window of the current editor.
|
||||
*/
|
||||
getWindow: (editor: ReactEditor) => Window
|
||||
|
||||
/**
|
||||
* Check if a DOM node is within the editor.
|
||||
*/
|
||||
hasDOMNode: (
|
||||
editor: ReactEditor,
|
||||
target: DOMNode,
|
||||
options?: { editable?: boolean }
|
||||
) => boolean
|
||||
|
||||
/**
|
||||
* Check if the target is editable and in the editor.
|
||||
*/
|
||||
hasEditableTarget: (
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
) => target is DOMNode
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
hasRange: (editor: ReactEditor, range: Range) => boolean
|
||||
|
||||
/**
|
||||
* Check if the target can be selectable
|
||||
*/
|
||||
hasSelectableTarget: (
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
) => boolean
|
||||
|
||||
/**
|
||||
* Check if the target is in the editor.
|
||||
*/
|
||||
hasTarget: (
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
) => target is DOMNode
|
||||
|
||||
/**
|
||||
* Insert data from a `DataTransfer` into the editor.
|
||||
*/
|
||||
insertData: (editor: ReactEditor, data: DataTransfer) => void
|
||||
|
||||
/**
|
||||
* Insert fragment data from a `DataTransfer` into the editor.
|
||||
*/
|
||||
insertFragmentData: (editor: ReactEditor, data: DataTransfer) => boolean
|
||||
|
||||
/**
|
||||
* Insert text data from a `DataTransfer` into the editor.
|
||||
*/
|
||||
insertTextData: (editor: ReactEditor, data: DataTransfer) => boolean
|
||||
|
||||
/**
|
||||
* Check if the user is currently composing inside the editor.
|
||||
*/
|
||||
isComposing: (editor: ReactEditor) => boolean
|
||||
|
||||
/**
|
||||
* Check if the editor is focused.
|
||||
*/
|
||||
isFocused: (editor: ReactEditor) => boolean
|
||||
|
||||
/**
|
||||
* Check if the editor is in read-only mode.
|
||||
*/
|
||||
isReadOnly: (editor: ReactEditor) => boolean
|
||||
|
||||
/**
|
||||
* Check if the target is inside void and in an non-readonly editor.
|
||||
*/
|
||||
isTargetInsideNonReadonlyVoid: (
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
) => boolean
|
||||
|
||||
/**
|
||||
* Sets data from the currently selected fragment on a `DataTransfer`.
|
||||
*/
|
||||
setFragmentData: (
|
||||
editor: ReactEditor,
|
||||
data: DataTransfer,
|
||||
originEvent?: 'drag' | 'copy' | 'cut'
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Find the native DOM element from a Slate node.
|
||||
*/
|
||||
toDOMNode: (editor: ReactEditor, node: Node) => HTMLElement
|
||||
|
||||
/**
|
||||
* Find a native DOM selection point from a Slate point.
|
||||
*/
|
||||
toDOMPoint: (editor: ReactEditor, point: Point) => DOMPoint
|
||||
|
||||
/**
|
||||
* Find a native DOM range from a Slate `range`.
|
||||
*
|
||||
* Notice: the returned range will always be ordinal regardless of the direction of Slate `range` due to DOM API limit.
|
||||
*
|
||||
* there is no way to create a reverse DOM Range using Range.setStart/setEnd
|
||||
* according to https://dom.spec.whatwg.org/#concept-range-bp-set.
|
||||
*/
|
||||
toDOMRange: (editor: ReactEditor, range: Range) => DOMRange
|
||||
|
||||
/**
|
||||
* Find a Slate node from a native DOM `element`.
|
||||
*/
|
||||
toSlateNode: (editor: ReactEditor, domNode: DOMNode) => Node
|
||||
|
||||
/**
|
||||
* Find a Slate point from a DOM selection's `domNode` and `domOffset`.
|
||||
*/
|
||||
toSlatePoint: <T extends boolean>(
|
||||
editor: ReactEditor,
|
||||
domPoint: DOMPoint,
|
||||
options: {
|
||||
exactMatch: boolean
|
||||
suppressThrow: T
|
||||
}
|
||||
) => T extends true ? Point | null : Point
|
||||
|
||||
/**
|
||||
* Find a Slate range from a DOM range or selection.
|
||||
*/
|
||||
toSlateRange: <T extends boolean>(
|
||||
editor: ReactEditor,
|
||||
domRange: DOMRange | DOMStaticRange | DOMSelection,
|
||||
options: {
|
||||
exactMatch: boolean
|
||||
suppressThrow: T
|
||||
}
|
||||
) => T extends true ? Range | null : Range
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const ReactEditor: ReactEditorInterface = {
|
||||
androidPendingDiffs: editor => EDITOR_TO_PENDING_DIFFS.get(editor),
|
||||
|
||||
androidScheduleFlush: editor => {
|
||||
EDITOR_TO_SCHEDULE_FLUSH.get(editor)?.()
|
||||
},
|
||||
|
||||
blur: editor => {
|
||||
const el = ReactEditor.toDOMNode(editor, editor)
|
||||
const root = ReactEditor.findDocumentOrShadowRoot(editor)
|
||||
IS_FOCUSED.set(editor, false)
|
||||
|
||||
if (root.activeElement === el) {
|
||||
el.blur()
|
||||
}
|
||||
},
|
||||
|
||||
deselect: editor => {
|
||||
const { selection } = editor
|
||||
const root = ReactEditor.findDocumentOrShadowRoot(editor)
|
||||
const domSelection = root.getSelection()
|
||||
|
||||
if (domSelection && domSelection.rangeCount > 0) {
|
||||
domSelection.removeAllRanges()
|
||||
}
|
||||
|
||||
if (selection) {
|
||||
Transforms.deselect(editor)
|
||||
}
|
||||
},
|
||||
|
||||
findDocumentOrShadowRoot: editor => {
|
||||
const el = ReactEditor.toDOMNode(editor, editor)
|
||||
const root = el.getRootNode()
|
||||
|
||||
if (
|
||||
(root instanceof Document || root instanceof ShadowRoot) &&
|
||||
root.getSelection != null
|
||||
) {
|
||||
return root
|
||||
}
|
||||
|
||||
return el.ownerDocument
|
||||
},
|
||||
|
||||
findEventRange: (editor, event) => {
|
||||
if ('nativeEvent' in event) {
|
||||
event = event.nativeEvent
|
||||
}
|
||||
|
||||
const { clientX: x, clientY: y, target } = event
|
||||
|
||||
if (x == null || y == null) {
|
||||
throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`)
|
||||
}
|
||||
|
||||
const node = ReactEditor.toSlateNode(editor, event.target)
|
||||
const path = ReactEditor.findPath(editor, node)
|
||||
|
||||
// If the drop target is inside a void node, move it into either the
|
||||
// next or previous node, depending on which side the `x` and `y`
|
||||
// coordinates are closest to.
|
||||
if (Element.isElement(node) && Editor.isVoid(editor, node)) {
|
||||
const rect = target.getBoundingClientRect()
|
||||
const isPrev = editor.isInline(node)
|
||||
? x - rect.left < rect.left + rect.width - x
|
||||
: y - rect.top < rect.top + rect.height - y
|
||||
|
||||
const edge = Editor.point(editor, path, {
|
||||
edge: isPrev ? 'start' : 'end',
|
||||
})
|
||||
const point = isPrev
|
||||
? Editor.before(editor, edge)
|
||||
: Editor.after(editor, edge)
|
||||
|
||||
if (point) {
|
||||
const range = Editor.range(editor, point)
|
||||
return range
|
||||
}
|
||||
}
|
||||
|
||||
// Else resolve a range from the caret position where the drop occured.
|
||||
let domRange
|
||||
const { document } = ReactEditor.getWindow(editor)
|
||||
|
||||
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
|
||||
if (document.caretRangeFromPoint) {
|
||||
domRange = document.caretRangeFromPoint(x, y)
|
||||
} else {
|
||||
const position = document.caretPositionFromPoint(x, y)
|
||||
|
||||
if (position) {
|
||||
domRange = document.createRange()
|
||||
domRange.setStart(position.offsetNode, position.offset)
|
||||
domRange.setEnd(position.offsetNode, position.offset)
|
||||
}
|
||||
}
|
||||
|
||||
if (!domRange) {
|
||||
throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`)
|
||||
}
|
||||
|
||||
// Resolve a Slate range from the DOM range.
|
||||
const range = ReactEditor.toSlateRange(editor, domRange, {
|
||||
exactMatch: false,
|
||||
suppressThrow: false,
|
||||
})
|
||||
return range
|
||||
},
|
||||
|
||||
findKey: (editor, node) => {
|
||||
let key = NODE_TO_KEY.get(node)
|
||||
|
||||
if (!key) {
|
||||
@@ -109,11 +381,7 @@ export const ReactEditor = {
|
||||
return key
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the path of Slate node.
|
||||
*/
|
||||
|
||||
findPath(editor: ReactEditor, node: Node): Path {
|
||||
findPath: (editor, node) => {
|
||||
const path: Path = []
|
||||
let child = node
|
||||
|
||||
@@ -143,59 +411,7 @@ export const ReactEditor = {
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the DOM node that implements DocumentOrShadowRoot for the editor.
|
||||
*/
|
||||
|
||||
findDocumentOrShadowRoot(editor: ReactEditor): Document | ShadowRoot {
|
||||
const el = ReactEditor.toDOMNode(editor, editor)
|
||||
const root = el.getRootNode()
|
||||
|
||||
if (
|
||||
(root instanceof Document || root instanceof ShadowRoot) &&
|
||||
root.getSelection != null
|
||||
) {
|
||||
return root
|
||||
}
|
||||
|
||||
return el.ownerDocument
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the editor is focused.
|
||||
*/
|
||||
|
||||
isFocused(editor: ReactEditor): boolean {
|
||||
return !!IS_FOCUSED.get(editor)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the editor is in read-only mode.
|
||||
*/
|
||||
|
||||
isReadOnly(editor: ReactEditor): boolean {
|
||||
return !!IS_READ_ONLY.get(editor)
|
||||
},
|
||||
|
||||
/**
|
||||
* Blur the editor.
|
||||
*/
|
||||
|
||||
blur(editor: ReactEditor): void {
|
||||
const el = ReactEditor.toDOMNode(editor, editor)
|
||||
const root = ReactEditor.findDocumentOrShadowRoot(editor)
|
||||
IS_FOCUSED.set(editor, false)
|
||||
|
||||
if (root.activeElement === el) {
|
||||
el.blur()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus the editor.
|
||||
*/
|
||||
|
||||
focus(editor: ReactEditor): void {
|
||||
focus: editor => {
|
||||
const el = ReactEditor.toDOMNode(editor, editor)
|
||||
const root = ReactEditor.findDocumentOrShadowRoot(editor)
|
||||
IS_FOCUSED.set(editor, true)
|
||||
@@ -205,33 +421,15 @@ export const ReactEditor = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deselect the editor.
|
||||
*/
|
||||
|
||||
deselect(editor: ReactEditor): void {
|
||||
const { selection } = editor
|
||||
const root = ReactEditor.findDocumentOrShadowRoot(editor)
|
||||
const domSelection = root.getSelection()
|
||||
|
||||
if (domSelection && domSelection.rangeCount > 0) {
|
||||
domSelection.removeAllRanges()
|
||||
}
|
||||
|
||||
if (selection) {
|
||||
Transforms.deselect(editor)
|
||||
getWindow: editor => {
|
||||
const window = EDITOR_TO_WINDOW.get(editor)
|
||||
if (!window) {
|
||||
throw new Error('Unable to find a host window element for this editor')
|
||||
}
|
||||
return window
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a DOM node is within the editor.
|
||||
*/
|
||||
|
||||
hasDOMNode(
|
||||
editor: ReactEditor,
|
||||
target: DOMNode,
|
||||
options: { editable?: boolean } = {}
|
||||
): boolean {
|
||||
hasDOMNode: (editor, target, options = {}) => {
|
||||
const { editable = false } = options
|
||||
const editorEl = ReactEditor.toDOMNode(editor, editor)
|
||||
let targetEl
|
||||
@@ -267,47 +465,53 @@ export const ReactEditor = {
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert data from a `DataTransfer` into the editor.
|
||||
*/
|
||||
hasEditableTarget: (editor, target): target is DOMNode =>
|
||||
isDOMNode(target) &&
|
||||
ReactEditor.hasDOMNode(editor, target, { editable: true }),
|
||||
|
||||
insertData(editor: ReactEditor, data: DataTransfer): void {
|
||||
hasRange: (editor, range) => {
|
||||
const { anchor, focus } = range
|
||||
return (
|
||||
Editor.hasPath(editor, anchor.path) && Editor.hasPath(editor, focus.path)
|
||||
)
|
||||
},
|
||||
|
||||
hasSelectableTarget: (editor, target) =>
|
||||
ReactEditor.hasEditableTarget(editor, target) ||
|
||||
ReactEditor.isTargetInsideNonReadonlyVoid(editor, target),
|
||||
|
||||
hasTarget: (editor, target): target is DOMNode =>
|
||||
isDOMNode(target) && ReactEditor.hasDOMNode(editor, target),
|
||||
|
||||
insertData: (editor, data) => {
|
||||
editor.insertData(data)
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert fragment data from a `DataTransfer` into the editor.
|
||||
*/
|
||||
insertFragmentData: (editor, data) => editor.insertFragmentData(data),
|
||||
|
||||
insertFragmentData(editor: ReactEditor, data: DataTransfer): boolean {
|
||||
return editor.insertFragmentData(data)
|
||||
insertTextData: (editor, data) => editor.insertTextData(data),
|
||||
|
||||
isComposing: editor => {
|
||||
return !!IS_COMPOSING.get(editor)
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert text data from a `DataTransfer` into the editor.
|
||||
*/
|
||||
isFocused: editor => !!IS_FOCUSED.get(editor),
|
||||
|
||||
insertTextData(editor: ReactEditor, data: DataTransfer): boolean {
|
||||
return editor.insertTextData(data)
|
||||
isReadOnly: editor => !!IS_READ_ONLY.get(editor),
|
||||
|
||||
isTargetInsideNonReadonlyVoid: (editor, target) => {
|
||||
if (IS_READ_ONLY.get(editor)) return false
|
||||
|
||||
const slateNode =
|
||||
ReactEditor.hasTarget(editor, target) &&
|
||||
ReactEditor.toSlateNode(editor, target)
|
||||
return Element.isElement(slateNode) && Editor.isVoid(editor, slateNode)
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets data from the currently selected fragment on a `DataTransfer`.
|
||||
*/
|
||||
setFragmentData: (editor, data, originEvent) =>
|
||||
editor.setFragmentData(data, originEvent),
|
||||
|
||||
setFragmentData(
|
||||
editor: ReactEditor,
|
||||
data: DataTransfer,
|
||||
originEvent?: 'drag' | 'copy' | 'cut'
|
||||
): void {
|
||||
editor.setFragmentData(data, originEvent)
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the native DOM element from a Slate node.
|
||||
*/
|
||||
|
||||
toDOMNode(editor: ReactEditor, node: Node): HTMLElement {
|
||||
toDOMNode: (editor, node) => {
|
||||
const KEY_TO_ELEMENT = EDITOR_TO_KEY_TO_ELEMENT.get(editor)
|
||||
const domNode = Editor.isEditor(node)
|
||||
? EDITOR_TO_ELEMENT.get(editor)
|
||||
@@ -322,11 +526,7 @@ export const ReactEditor = {
|
||||
return domNode
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a native DOM selection point from a Slate point.
|
||||
*/
|
||||
|
||||
toDOMPoint(editor: ReactEditor, point: Point): DOMPoint {
|
||||
toDOMPoint: (editor, point) => {
|
||||
const [node] = Editor.node(editor, point.path)
|
||||
const el = ReactEditor.toDOMNode(editor, node)
|
||||
let domPoint: DOMPoint | undefined
|
||||
@@ -398,16 +598,7 @@ export const ReactEditor = {
|
||||
return domPoint
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a native DOM range from a Slate `range`.
|
||||
*
|
||||
* Notice: the returned range will always be ordinal regardless of the direction of Slate `range` due to DOM API limit.
|
||||
*
|
||||
* there is no way to create a reverse DOM Range using Range.setStart/setEnd
|
||||
* according to https://dom.spec.whatwg.org/#concept-range-bp-set.
|
||||
*/
|
||||
|
||||
toDOMRange(editor: ReactEditor, range: Range): DOMRange {
|
||||
toDOMRange: (editor, range) => {
|
||||
const { anchor, focus } = range
|
||||
const isBackward = Range.isBackward(range)
|
||||
const domAnchor = ReactEditor.toDOMPoint(editor, anchor)
|
||||
@@ -437,11 +628,7 @@ export const ReactEditor = {
|
||||
return domRange
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a Slate node from a native DOM `element`.
|
||||
*/
|
||||
|
||||
toSlateNode(editor: ReactEditor, domNode: DOMNode): Node {
|
||||
toSlateNode: (editor, domNode) => {
|
||||
let domEl = isDOMElement(domNode) ? domNode : domNode.parentElement
|
||||
|
||||
if (domEl && !domEl.hasAttribute('data-slate-node')) {
|
||||
@@ -457,87 +644,14 @@ export const ReactEditor = {
|
||||
return node
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the target range from a DOM `event`.
|
||||
*/
|
||||
|
||||
findEventRange(editor: ReactEditor, event: any): Range {
|
||||
if ('nativeEvent' in event) {
|
||||
event = event.nativeEvent
|
||||
}
|
||||
|
||||
const { clientX: x, clientY: y, target } = event
|
||||
|
||||
if (x == null || y == null) {
|
||||
throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`)
|
||||
}
|
||||
|
||||
const node = ReactEditor.toSlateNode(editor, event.target)
|
||||
const path = ReactEditor.findPath(editor, node)
|
||||
|
||||
// If the drop target is inside a void node, move it into either the
|
||||
// next or previous node, depending on which side the `x` and `y`
|
||||
// coordinates are closest to.
|
||||
if (Element.isElement(node) && Editor.isVoid(editor, node)) {
|
||||
const rect = target.getBoundingClientRect()
|
||||
const isPrev = editor.isInline(node)
|
||||
? x - rect.left < rect.left + rect.width - x
|
||||
: y - rect.top < rect.top + rect.height - y
|
||||
|
||||
const edge = Editor.point(editor, path, {
|
||||
edge: isPrev ? 'start' : 'end',
|
||||
})
|
||||
const point = isPrev
|
||||
? Editor.before(editor, edge)
|
||||
: Editor.after(editor, edge)
|
||||
|
||||
if (point) {
|
||||
const range = Editor.range(editor, point)
|
||||
return range
|
||||
}
|
||||
}
|
||||
|
||||
// Else resolve a range from the caret position where the drop occured.
|
||||
let domRange
|
||||
const { document } = ReactEditor.getWindow(editor)
|
||||
|
||||
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
|
||||
if (document.caretRangeFromPoint) {
|
||||
domRange = document.caretRangeFromPoint(x, y)
|
||||
} else {
|
||||
const position = document.caretPositionFromPoint(x, y)
|
||||
|
||||
if (position) {
|
||||
domRange = document.createRange()
|
||||
domRange.setStart(position.offsetNode, position.offset)
|
||||
domRange.setEnd(position.offsetNode, position.offset)
|
||||
}
|
||||
}
|
||||
|
||||
if (!domRange) {
|
||||
throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`)
|
||||
}
|
||||
|
||||
// Resolve a Slate range from the DOM range.
|
||||
const range = ReactEditor.toSlateRange(editor, domRange, {
|
||||
exactMatch: false,
|
||||
suppressThrow: false,
|
||||
})
|
||||
return range
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a Slate point from a DOM selection's `domNode` and `domOffset`.
|
||||
*/
|
||||
|
||||
toSlatePoint<T extends boolean>(
|
||||
toSlatePoint: <T extends boolean>(
|
||||
editor: ReactEditor,
|
||||
domPoint: DOMPoint,
|
||||
options: {
|
||||
exactMatch: boolean
|
||||
suppressThrow: T
|
||||
}
|
||||
): T extends true ? Point | null : Point {
|
||||
): T extends true ? Point | null : Point => {
|
||||
const { exactMatch, suppressThrow } = options
|
||||
const [nearestNode, nearestOffset] = exactMatch
|
||||
? domPoint
|
||||
@@ -695,18 +809,14 @@ export const ReactEditor = {
|
||||
return { path, offset } as T extends true ? Point | null : Point
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a Slate range from a DOM range or selection.
|
||||
*/
|
||||
|
||||
toSlateRange<T extends boolean>(
|
||||
toSlateRange: <T extends boolean>(
|
||||
editor: ReactEditor,
|
||||
domRange: DOMRange | DOMStaticRange | DOMSelection,
|
||||
options: {
|
||||
exactMatch: boolean
|
||||
suppressThrow: T
|
||||
}
|
||||
): T extends true ? Range | null : Range {
|
||||
): T extends true ? Range | null : Range => {
|
||||
const { exactMatch, suppressThrow } = options
|
||||
const el = isDOMSelection(domRange)
|
||||
? domRange.anchorNode
|
||||
@@ -839,76 +949,4 @@ export const ReactEditor = {
|
||||
|
||||
return (range as unknown) as T extends true ? Range | null : Range
|
||||
},
|
||||
|
||||
hasRange(editor: ReactEditor, range: Range): boolean {
|
||||
const { anchor, focus } = range
|
||||
return (
|
||||
Editor.hasPath(editor, anchor.path) && Editor.hasPath(editor, focus.path)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the target is in the editor.
|
||||
*/
|
||||
hasTarget(
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
): target is DOMNode {
|
||||
return isDOMNode(target) && ReactEditor.hasDOMNode(editor, target)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the target is editable and in the editor.
|
||||
*/
|
||||
hasEditableTarget(
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
): target is DOMNode {
|
||||
return (
|
||||
isDOMNode(target) &&
|
||||
ReactEditor.hasDOMNode(editor, target, { editable: true })
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the target can be selectable
|
||||
*/
|
||||
hasSelectableTarget(
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
): boolean {
|
||||
return (
|
||||
ReactEditor.hasEditableTarget(editor, target) ||
|
||||
ReactEditor.isTargetInsideNonReadonlyVoid(editor, target)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the target is inside void and in an non-readonly editor.
|
||||
*/
|
||||
isTargetInsideNonReadonlyVoid(
|
||||
editor: ReactEditor,
|
||||
target: EventTarget | null
|
||||
): boolean {
|
||||
if (IS_READ_ONLY.get(editor)) return false
|
||||
|
||||
const slateNode =
|
||||
ReactEditor.hasTarget(editor, target) &&
|
||||
ReactEditor.toSlateNode(editor, target)
|
||||
return Element.isElement(slateNode) && Editor.isVoid(editor, slateNode)
|
||||
},
|
||||
|
||||
/**
|
||||
* Experimental and android specific: Flush all pending diffs and cancel composition at the next possible time.
|
||||
*/
|
||||
androidScheduleFlush(editor: Editor) {
|
||||
EDITOR_TO_SCHEDULE_FLUSH.get(editor)?.()
|
||||
},
|
||||
|
||||
/**
|
||||
* Experimental and android specific: Get pending diffs
|
||||
*/
|
||||
androidPendingDiffs(editor: Editor) {
|
||||
return EDITOR_TO_PENDING_DIFFS.get(editor)
|
||||
},
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
* Utilities for single-line deletion
|
||||
*/
|
||||
|
||||
import { Range, Editor } from 'slate'
|
||||
import { Editor, Range } from 'slate'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
|
||||
const doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => {
|
||||
|
3
packages/slate-react/src/utils/types.ts
Normal file
3
packages/slate-react/src/utils/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => infer R
|
||||
? (...args: P) => R
|
||||
: never
|
78
packages/slate/src/core/apply.ts
Normal file
78
packages/slate/src/core/apply.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { PathRef } from '../interfaces/path-ref'
|
||||
import { PointRef } from '../interfaces/point-ref'
|
||||
import { RangeRef } from '../interfaces/range-ref'
|
||||
import { DIRTY_PATH_KEYS, DIRTY_PATHS, FLUSHING } from '../utils/weak-maps'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { WithEditorFirstArg } from '../utils/types'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
|
||||
export const apply: WithEditorFirstArg<Editor['apply']> = (editor, op) => {
|
||||
for (const ref of Editor.pathRefs(editor)) {
|
||||
PathRef.transform(ref, op)
|
||||
}
|
||||
|
||||
for (const ref of Editor.pointRefs(editor)) {
|
||||
PointRef.transform(ref, op)
|
||||
}
|
||||
|
||||
for (const ref of Editor.rangeRefs(editor)) {
|
||||
RangeRef.transform(ref, op)
|
||||
}
|
||||
|
||||
const oldDirtyPaths = DIRTY_PATHS.get(editor) || []
|
||||
const oldDirtyPathKeys = DIRTY_PATH_KEYS.get(editor) || new Set()
|
||||
let dirtyPaths: Path[]
|
||||
let dirtyPathKeys: Set<string>
|
||||
|
||||
const add = (path: Path | null) => {
|
||||
if (path) {
|
||||
const key = path.join(',')
|
||||
|
||||
if (!dirtyPathKeys.has(key)) {
|
||||
dirtyPathKeys.add(key)
|
||||
dirtyPaths.push(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Path.operationCanTransformPath(op)) {
|
||||
dirtyPaths = []
|
||||
dirtyPathKeys = new Set()
|
||||
for (const path of oldDirtyPaths) {
|
||||
const newPath = Path.transform(path, op)
|
||||
add(newPath)
|
||||
}
|
||||
} else {
|
||||
dirtyPaths = oldDirtyPaths
|
||||
dirtyPathKeys = oldDirtyPathKeys
|
||||
}
|
||||
|
||||
const newDirtyPaths = editor.getDirtyPaths(op)
|
||||
for (const path of newDirtyPaths) {
|
||||
add(path)
|
||||
}
|
||||
|
||||
DIRTY_PATHS.set(editor, dirtyPaths)
|
||||
DIRTY_PATH_KEYS.set(editor, dirtyPathKeys)
|
||||
Transforms.transform(editor, op)
|
||||
editor.operations.push(op)
|
||||
Editor.normalize(editor, {
|
||||
operation: op,
|
||||
})
|
||||
|
||||
// Clear any formats applied to the cursor if the selection changes.
|
||||
if (op.type === 'set_selection') {
|
||||
editor.marks = null
|
||||
}
|
||||
|
||||
if (!FLUSHING.get(editor)) {
|
||||
FLUSHING.set(editor, true)
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
FLUSHING.set(editor, false)
|
||||
editor.onChange({ operation: op })
|
||||
editor.operations = []
|
||||
})
|
||||
}
|
||||
}
|
83
packages/slate/src/core/get-dirty-paths.ts
Normal file
83
packages/slate/src/core/get-dirty-paths.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { WithEditorFirstArg } from '../utils/types'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Text } from '../interfaces/text'
|
||||
import { Node } from '../interfaces/node'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
|
||||
/**
|
||||
* Get the "dirty" paths generated from an operation.
|
||||
*/
|
||||
export const getDirtyPaths: WithEditorFirstArg<Editor['getDirtyPaths']> = (
|
||||
editor,
|
||||
op
|
||||
) => {
|
||||
switch (op.type) {
|
||||
case 'insert_text':
|
||||
case 'remove_text':
|
||||
case 'set_node': {
|
||||
const { path } = op
|
||||
return Path.levels(path)
|
||||
}
|
||||
|
||||
case 'insert_node': {
|
||||
const { node, path } = op
|
||||
const levels = Path.levels(path)
|
||||
const descendants = Text.isText(node)
|
||||
? []
|
||||
: Array.from(Node.nodes(node), ([, p]) => path.concat(p))
|
||||
|
||||
return [...levels, ...descendants]
|
||||
}
|
||||
|
||||
case 'merge_node': {
|
||||
const { path } = op
|
||||
const ancestors = Path.ancestors(path)
|
||||
const previousPath = Path.previous(path)
|
||||
return [...ancestors, previousPath]
|
||||
}
|
||||
|
||||
case 'move_node': {
|
||||
const { path, newPath } = op
|
||||
|
||||
if (Path.equals(path, newPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const oldAncestors: Path[] = []
|
||||
const newAncestors: Path[] = []
|
||||
|
||||
for (const ancestor of Path.ancestors(path)) {
|
||||
const p = Path.transform(ancestor, op)
|
||||
oldAncestors.push(p!)
|
||||
}
|
||||
|
||||
for (const ancestor of Path.ancestors(newPath)) {
|
||||
const p = Path.transform(ancestor, op)
|
||||
newAncestors.push(p!)
|
||||
}
|
||||
|
||||
const newParent = newAncestors[newAncestors.length - 1]
|
||||
const newIndex = newPath[newPath.length - 1]
|
||||
const resultPath = newParent.concat(newIndex)
|
||||
|
||||
return [...oldAncestors, ...newAncestors, resultPath]
|
||||
}
|
||||
|
||||
case 'remove_node': {
|
||||
const { path } = op
|
||||
const ancestors = Path.ancestors(path)
|
||||
return [...ancestors]
|
||||
}
|
||||
|
||||
case 'split_node': {
|
||||
const { path } = op
|
||||
const levels = Path.levels(path)
|
||||
const nextPath = Path.next(path)
|
||||
return [...levels, nextPath]
|
||||
}
|
||||
|
||||
default: {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
11
packages/slate/src/core/get-fragment.ts
Normal file
11
packages/slate/src/core/get-fragment.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Editor, Node } from '../interfaces'
|
||||
import { WithEditorFirstArg } from '../utils'
|
||||
|
||||
export const getFragment: WithEditorFirstArg<Editor['getFragment']> = editor => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection) {
|
||||
return Node.fragment(editor, selection)
|
||||
}
|
||||
return []
|
||||
}
|
5
packages/slate/src/core/index.ts
Normal file
5
packages/slate/src/core/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './apply'
|
||||
export * from './get-dirty-paths'
|
||||
export * from './get-fragment'
|
||||
export * from './normalize-node'
|
||||
export * from './should-normalize'
|
99
packages/slate/src/core/normalize-node.ts
Normal file
99
packages/slate/src/core/normalize-node.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { WithEditorFirstArg } from '../utils/types'
|
||||
import { Text } from '../interfaces/text'
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { Descendant, Node } from '../interfaces/node'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
|
||||
export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
|
||||
editor,
|
||||
entry
|
||||
) => {
|
||||
const [node, path] = entry
|
||||
|
||||
// There are no core normalizations for text nodes.
|
||||
if (Text.isText(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure that block and inline nodes have at least one text child.
|
||||
if (Element.isElement(node) && node.children.length === 0) {
|
||||
const child = { text: '' }
|
||||
Transforms.insertNodes(editor, child, {
|
||||
at: path.concat(0),
|
||||
voids: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Determine whether the node should have block or inline children.
|
||||
const shouldHaveInlines = Editor.isEditor(node)
|
||||
? false
|
||||
: Element.isElement(node) &&
|
||||
(editor.isInline(node) ||
|
||||
node.children.length === 0 ||
|
||||
Text.isText(node.children[0]) ||
|
||||
editor.isInline(node.children[0]))
|
||||
|
||||
// Since we'll be applying operations while iterating, keep track of an
|
||||
// index that accounts for any added/removed nodes.
|
||||
let n = 0
|
||||
|
||||
for (let i = 0; i < node.children.length; i++, n++) {
|
||||
const currentNode = Node.get(editor, path)
|
||||
if (Text.isText(currentNode)) continue
|
||||
const child = currentNode.children[n] as Descendant
|
||||
const prev = currentNode.children[n - 1] as Descendant
|
||||
const isLast = i === node.children.length - 1
|
||||
const isInlineOrText =
|
||||
Text.isText(child) || (Element.isElement(child) && editor.isInline(child))
|
||||
|
||||
// Only allow block nodes in the top-level children and parent blocks
|
||||
// that only contain block nodes. Similarly, only allow inline nodes in
|
||||
// other inline nodes, or parent blocks that only contain inlines and
|
||||
// text.
|
||||
if (isInlineOrText !== shouldHaveInlines) {
|
||||
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
|
||||
n--
|
||||
} else if (Element.isElement(child)) {
|
||||
// Ensure that inline nodes are surrounded by text nodes.
|
||||
if (editor.isInline(child)) {
|
||||
if (prev == null || !Text.isText(prev)) {
|
||||
const newChild = { text: '' }
|
||||
Transforms.insertNodes(editor, newChild, {
|
||||
at: path.concat(n),
|
||||
voids: true,
|
||||
})
|
||||
n++
|
||||
} else if (isLast) {
|
||||
const newChild = { text: '' }
|
||||
Transforms.insertNodes(editor, newChild, {
|
||||
at: path.concat(n + 1),
|
||||
voids: true,
|
||||
})
|
||||
n++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Merge adjacent text nodes that are empty or match.
|
||||
if (prev != null && Text.isText(prev)) {
|
||||
if (Text.equals(child, prev, { loose: true })) {
|
||||
Transforms.mergeNodes(editor, { at: path.concat(n), voids: true })
|
||||
n--
|
||||
} else if (prev.text === '') {
|
||||
Transforms.removeNodes(editor, {
|
||||
at: path.concat(n - 1),
|
||||
voids: true,
|
||||
})
|
||||
n--
|
||||
} else if (child.text === '') {
|
||||
Transforms.removeNodes(editor, {
|
||||
at: path.concat(n),
|
||||
voids: true,
|
||||
})
|
||||
n--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
packages/slate/src/core/should-normalize.ts
Normal file
17
packages/slate/src/core/should-normalize.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { WithEditorFirstArg } from '../utils/types'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
|
||||
export const shouldNormalize: WithEditorFirstArg<Editor['shouldNormalize']> = (
|
||||
editor,
|
||||
{ iteration, initialDirtyPathsLength }
|
||||
) => {
|
||||
const maxIterations = initialDirtyPathsLength * 42 // HACK: better way?
|
||||
|
||||
if (iteration > maxIterations) {
|
||||
throw new Error(
|
||||
`Could not completely normalize the editor after ${maxIterations} iterations! This is usually due to incorrect normalization logic that leaves a node in an invalid state.`
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
@@ -1,24 +1,92 @@
|
||||
import {
|
||||
Descendant,
|
||||
addMark,
|
||||
deleteFragment,
|
||||
Editor,
|
||||
Element,
|
||||
Node,
|
||||
Operation,
|
||||
Path,
|
||||
PathRef,
|
||||
PointRef,
|
||||
Range,
|
||||
RangeRef,
|
||||
Text,
|
||||
Transforms,
|
||||
getDirtyPaths,
|
||||
getFragment,
|
||||
insertBreak,
|
||||
insertFragment,
|
||||
insertNode,
|
||||
insertSoftBreak,
|
||||
insertText,
|
||||
normalizeNode,
|
||||
removeMark,
|
||||
shouldNormalize,
|
||||
} from './'
|
||||
import { TextUnit } from './interfaces/types'
|
||||
import { DIRTY_PATH_KEYS, DIRTY_PATHS, FLUSHING } from './utils/weak-maps'
|
||||
import { apply } from './core'
|
||||
import {
|
||||
above,
|
||||
after,
|
||||
before,
|
||||
deleteBackward,
|
||||
deleteForward,
|
||||
edges,
|
||||
elementReadOnly,
|
||||
end,
|
||||
first,
|
||||
fragment,
|
||||
getVoid,
|
||||
hasBlocks,
|
||||
hasInlines,
|
||||
hasPath,
|
||||
hasTexts,
|
||||
isBlock,
|
||||
isEdge,
|
||||
isEmpty,
|
||||
isEnd,
|
||||
isNormalizing,
|
||||
isStart,
|
||||
last,
|
||||
leaf,
|
||||
levels,
|
||||
marks,
|
||||
next,
|
||||
node,
|
||||
nodes,
|
||||
normalize,
|
||||
parent,
|
||||
path,
|
||||
pathRef,
|
||||
pathRefs,
|
||||
point,
|
||||
pointRef,
|
||||
pointRefs,
|
||||
positions,
|
||||
previous,
|
||||
range,
|
||||
rangeRef,
|
||||
rangeRefs,
|
||||
setNormalizing,
|
||||
start,
|
||||
string,
|
||||
unhangRange,
|
||||
withoutNormalizing,
|
||||
} from './editor'
|
||||
import { deleteText } from './transforms-text'
|
||||
import {
|
||||
collapse,
|
||||
deselect,
|
||||
move,
|
||||
select,
|
||||
setPoint,
|
||||
setSelection,
|
||||
} from './transforms-selection'
|
||||
import {
|
||||
insertNodes,
|
||||
liftNodes,
|
||||
mergeNodes,
|
||||
moveNodes,
|
||||
removeNodes,
|
||||
setNodes,
|
||||
splitNodes,
|
||||
unsetNodes,
|
||||
unwrapNodes,
|
||||
wrapNodes,
|
||||
} from './transforms-node'
|
||||
|
||||
/**
|
||||
* Create a new Slate `Editor` object.
|
||||
*/
|
||||
|
||||
export const createEditor = (): Editor => {
|
||||
const editor: Editor = {
|
||||
children: [],
|
||||
@@ -32,401 +100,87 @@ export const createEditor = (): Editor => {
|
||||
markableVoid: () => false,
|
||||
onChange: () => {},
|
||||
|
||||
apply: (op: Operation) => {
|
||||
for (const ref of Editor.pathRefs(editor)) {
|
||||
PathRef.transform(ref, op)
|
||||
}
|
||||
// Core
|
||||
apply: (...args) => apply(editor, ...args),
|
||||
|
||||
for (const ref of Editor.pointRefs(editor)) {
|
||||
PointRef.transform(ref, op)
|
||||
}
|
||||
// Editor
|
||||
addMark: (...args) => addMark(editor, ...args),
|
||||
deleteBackward: (...args) => deleteBackward(editor, ...args),
|
||||
deleteForward: (...args) => deleteForward(editor, ...args),
|
||||
deleteFragment: (...args) => deleteFragment(editor, ...args),
|
||||
getFragment: (...args) => getFragment(editor, ...args),
|
||||
insertBreak: (...args) => insertBreak(editor, ...args),
|
||||
insertSoftBreak: (...args) => insertSoftBreak(editor, ...args),
|
||||
insertFragment: (...args) => insertFragment(editor, ...args),
|
||||
insertNode: (...args) => insertNode(editor, ...args),
|
||||
insertText: (...args) => insertText(editor, ...args),
|
||||
normalizeNode: (...args) => normalizeNode(editor, ...args),
|
||||
removeMark: (...args) => removeMark(editor, ...args),
|
||||
getDirtyPaths: (...args) => getDirtyPaths(editor, ...args),
|
||||
shouldNormalize: (...args) => shouldNormalize(editor, ...args),
|
||||
|
||||
for (const ref of Editor.rangeRefs(editor)) {
|
||||
RangeRef.transform(ref, op)
|
||||
}
|
||||
|
||||
const oldDirtyPaths = DIRTY_PATHS.get(editor) || []
|
||||
const oldDirtyPathKeys = DIRTY_PATH_KEYS.get(editor) || new Set()
|
||||
let dirtyPaths: Path[]
|
||||
let dirtyPathKeys: Set<string>
|
||||
|
||||
const add = (path: Path | null) => {
|
||||
if (path) {
|
||||
const key = path.join(',')
|
||||
|
||||
if (!dirtyPathKeys.has(key)) {
|
||||
dirtyPathKeys.add(key)
|
||||
dirtyPaths.push(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Path.operationCanTransformPath(op)) {
|
||||
dirtyPaths = []
|
||||
dirtyPathKeys = new Set()
|
||||
for (const path of oldDirtyPaths) {
|
||||
const newPath = Path.transform(path, op)
|
||||
add(newPath)
|
||||
}
|
||||
} else {
|
||||
dirtyPaths = oldDirtyPaths
|
||||
dirtyPathKeys = oldDirtyPathKeys
|
||||
}
|
||||
|
||||
const newDirtyPaths = editor.getDirtyPaths(op)
|
||||
for (const path of newDirtyPaths) {
|
||||
add(path)
|
||||
}
|
||||
|
||||
DIRTY_PATHS.set(editor, dirtyPaths)
|
||||
DIRTY_PATH_KEYS.set(editor, dirtyPathKeys)
|
||||
Transforms.transform(editor, op)
|
||||
editor.operations.push(op)
|
||||
Editor.normalize(editor, {
|
||||
operation: op,
|
||||
})
|
||||
|
||||
// Clear any formats applied to the cursor if the selection changes.
|
||||
if (op.type === 'set_selection') {
|
||||
editor.marks = null
|
||||
}
|
||||
|
||||
if (!FLUSHING.get(editor)) {
|
||||
FLUSHING.set(editor, true)
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
FLUSHING.set(editor, false)
|
||||
editor.onChange({ operation: op })
|
||||
editor.operations = []
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
addMark: (key: string, value: any) => {
|
||||
const { selection, markableVoid } = editor
|
||||
|
||||
if (selection) {
|
||||
const match = (node: Node, path: Path) => {
|
||||
if (!Text.isText(node)) {
|
||||
return false // marks can only be applied to text
|
||||
}
|
||||
const [parentNode, parentPath] = Editor.parent(editor, path)
|
||||
return !editor.isVoid(parentNode) || editor.markableVoid(parentNode)
|
||||
}
|
||||
const expandedSelection = Range.isExpanded(selection)
|
||||
let markAcceptingVoidSelected = false
|
||||
if (!expandedSelection) {
|
||||
const [selectedNode, selectedPath] = Editor.node(editor, selection)
|
||||
if (selectedNode && match(selectedNode, selectedPath)) {
|
||||
const [parentNode] = Editor.parent(editor, selectedPath)
|
||||
markAcceptingVoidSelected =
|
||||
parentNode && editor.markableVoid(parentNode)
|
||||
}
|
||||
}
|
||||
if (expandedSelection || markAcceptingVoidSelected) {
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ [key]: value },
|
||||
{
|
||||
match,
|
||||
split: true,
|
||||
voids: true,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
const marks = {
|
||||
...(Editor.marks(editor) || {}),
|
||||
[key]: value,
|
||||
}
|
||||
|
||||
editor.marks = marks
|
||||
if (!FLUSHING.get(editor)) {
|
||||
editor.onChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
deleteBackward: (unit: TextUnit) => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
Transforms.delete(editor, { unit, reverse: true })
|
||||
}
|
||||
},
|
||||
|
||||
deleteForward: (unit: TextUnit) => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
Transforms.delete(editor, { unit })
|
||||
}
|
||||
},
|
||||
|
||||
deleteFragment: (direction?: 'forward' | 'backward') => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection && Range.isExpanded(selection)) {
|
||||
Transforms.delete(editor, { reverse: direction === 'backward' })
|
||||
}
|
||||
},
|
||||
|
||||
getFragment: () => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection) {
|
||||
return Node.fragment(editor, selection)
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
insertBreak: () => {
|
||||
Transforms.splitNodes(editor, { always: true })
|
||||
},
|
||||
|
||||
insertSoftBreak: () => {
|
||||
Transforms.splitNodes(editor, { always: true })
|
||||
},
|
||||
|
||||
insertFragment: (fragment: Node[]) => {
|
||||
Transforms.insertFragment(editor, fragment)
|
||||
},
|
||||
|
||||
insertNode: (node: Node) => {
|
||||
Transforms.insertNodes(editor, node)
|
||||
},
|
||||
|
||||
insertText: (text: string) => {
|
||||
const { selection, marks } = editor
|
||||
|
||||
if (selection) {
|
||||
if (marks) {
|
||||
const node = { text, ...marks }
|
||||
Transforms.insertNodes(editor, node)
|
||||
} else {
|
||||
Transforms.insertText(editor, text)
|
||||
}
|
||||
|
||||
editor.marks = null
|
||||
}
|
||||
},
|
||||
|
||||
normalizeNode: entry => {
|
||||
const [node, path] = entry
|
||||
|
||||
// There are no core normalizations for text nodes.
|
||||
if (Text.isText(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure that block and inline nodes have at least one text child.
|
||||
if (Element.isElement(node) && node.children.length === 0) {
|
||||
const child = { text: '' }
|
||||
Transforms.insertNodes(editor, child, {
|
||||
at: path.concat(0),
|
||||
voids: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Determine whether the node should have block or inline children.
|
||||
const shouldHaveInlines = Editor.isEditor(node)
|
||||
? false
|
||||
: Element.isElement(node) &&
|
||||
(editor.isInline(node) ||
|
||||
node.children.length === 0 ||
|
||||
Text.isText(node.children[0]) ||
|
||||
editor.isInline(node.children[0]))
|
||||
|
||||
// Since we'll be applying operations while iterating, keep track of an
|
||||
// index that accounts for any added/removed nodes.
|
||||
let n = 0
|
||||
|
||||
for (let i = 0; i < node.children.length; i++, n++) {
|
||||
const currentNode = Node.get(editor, path)
|
||||
if (Text.isText(currentNode)) continue
|
||||
const child = currentNode.children[n] as Descendant
|
||||
const prev = currentNode.children[n - 1] as Descendant
|
||||
const isLast = i === node.children.length - 1
|
||||
const isInlineOrText =
|
||||
Text.isText(child) ||
|
||||
(Element.isElement(child) && editor.isInline(child))
|
||||
|
||||
// Only allow block nodes in the top-level children and parent blocks
|
||||
// that only contain block nodes. Similarly, only allow inline nodes in
|
||||
// other inline nodes, or parent blocks that only contain inlines and
|
||||
// text.
|
||||
if (isInlineOrText !== shouldHaveInlines) {
|
||||
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
|
||||
n--
|
||||
} else if (Element.isElement(child)) {
|
||||
// Ensure that inline nodes are surrounded by text nodes.
|
||||
if (editor.isInline(child)) {
|
||||
if (prev == null || !Text.isText(prev)) {
|
||||
const newChild = { text: '' }
|
||||
Transforms.insertNodes(editor, newChild, {
|
||||
at: path.concat(n),
|
||||
voids: true,
|
||||
})
|
||||
n++
|
||||
} else if (isLast) {
|
||||
const newChild = { text: '' }
|
||||
Transforms.insertNodes(editor, newChild, {
|
||||
at: path.concat(n + 1),
|
||||
voids: true,
|
||||
})
|
||||
n++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Merge adjacent text nodes that are empty or match.
|
||||
if (prev != null && Text.isText(prev)) {
|
||||
if (Text.equals(child, prev, { loose: true })) {
|
||||
Transforms.mergeNodes(editor, { at: path.concat(n), voids: true })
|
||||
n--
|
||||
} else if (prev.text === '') {
|
||||
Transforms.removeNodes(editor, {
|
||||
at: path.concat(n - 1),
|
||||
voids: true,
|
||||
})
|
||||
n--
|
||||
} else if (child.text === '') {
|
||||
Transforms.removeNodes(editor, {
|
||||
at: path.concat(n),
|
||||
voids: true,
|
||||
})
|
||||
n--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeMark: (key: string) => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection) {
|
||||
const match = (node: Node, path: Path) => {
|
||||
if (!Text.isText(node)) {
|
||||
return false // marks can only be applied to text
|
||||
}
|
||||
const [parentNode, parentPath] = Editor.parent(editor, path)
|
||||
return !editor.isVoid(parentNode) || editor.markableVoid(parentNode)
|
||||
}
|
||||
const expandedSelection = Range.isExpanded(selection)
|
||||
let markAcceptingVoidSelected = false
|
||||
if (!expandedSelection) {
|
||||
const [selectedNode, selectedPath] = Editor.node(editor, selection)
|
||||
if (selectedNode && match(selectedNode, selectedPath)) {
|
||||
const [parentNode] = Editor.parent(editor, selectedPath)
|
||||
markAcceptingVoidSelected =
|
||||
parentNode && editor.markableVoid(parentNode)
|
||||
}
|
||||
}
|
||||
if (expandedSelection || markAcceptingVoidSelected) {
|
||||
Transforms.unsetNodes(editor, key, {
|
||||
match,
|
||||
split: true,
|
||||
voids: true,
|
||||
})
|
||||
} else {
|
||||
const marks = { ...(Editor.marks(editor) || {}) }
|
||||
delete marks[key]
|
||||
editor.marks = marks
|
||||
if (!FLUSHING.get(editor)) {
|
||||
editor.onChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the "dirty" paths generated from an operation.
|
||||
*/
|
||||
|
||||
getDirtyPaths: (op: Operation): Path[] => {
|
||||
switch (op.type) {
|
||||
case 'insert_text':
|
||||
case 'remove_text':
|
||||
case 'set_node': {
|
||||
const { path } = op
|
||||
return Path.levels(path)
|
||||
}
|
||||
|
||||
case 'insert_node': {
|
||||
const { node, path } = op
|
||||
const levels = Path.levels(path)
|
||||
const descendants = Text.isText(node)
|
||||
? []
|
||||
: Array.from(Node.nodes(node), ([, p]) => path.concat(p))
|
||||
|
||||
return [...levels, ...descendants]
|
||||
}
|
||||
|
||||
case 'merge_node': {
|
||||
const { path } = op
|
||||
const ancestors = Path.ancestors(path)
|
||||
const previousPath = Path.previous(path)
|
||||
return [...ancestors, previousPath]
|
||||
}
|
||||
|
||||
case 'move_node': {
|
||||
const { path, newPath } = op
|
||||
|
||||
if (Path.equals(path, newPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const oldAncestors: Path[] = []
|
||||
const newAncestors: Path[] = []
|
||||
|
||||
for (const ancestor of Path.ancestors(path)) {
|
||||
const p = Path.transform(ancestor, op)
|
||||
oldAncestors.push(p!)
|
||||
}
|
||||
|
||||
for (const ancestor of Path.ancestors(newPath)) {
|
||||
const p = Path.transform(ancestor, op)
|
||||
newAncestors.push(p!)
|
||||
}
|
||||
|
||||
const newParent = newAncestors[newAncestors.length - 1]
|
||||
const newIndex = newPath[newPath.length - 1]
|
||||
const resultPath = newParent.concat(newIndex)
|
||||
|
||||
return [...oldAncestors, ...newAncestors, resultPath]
|
||||
}
|
||||
|
||||
case 'remove_node': {
|
||||
const { path } = op
|
||||
const ancestors = Path.ancestors(path)
|
||||
return [...ancestors]
|
||||
}
|
||||
|
||||
case 'split_node': {
|
||||
const { path } = op
|
||||
const levels = Path.levels(path)
|
||||
const nextPath = Path.next(path)
|
||||
return [...levels, nextPath]
|
||||
}
|
||||
|
||||
default: {
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
shouldNormalize: ({ iteration, initialDirtyPathsLength }) => {
|
||||
const maxIterations = initialDirtyPathsLength * 42 // HACK: better way?
|
||||
|
||||
if (iteration > maxIterations) {
|
||||
throw new Error(
|
||||
`Could not completely normalize the editor after ${maxIterations} iterations! This is usually due to incorrect normalization logic that leaves a node in an invalid state.`
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
// Editor interface
|
||||
above: (...args) => above(editor, ...args),
|
||||
after: (...args) => after(editor, ...args),
|
||||
before: (...args) => before(editor, ...args),
|
||||
collapse: (...args) => collapse(editor, ...args),
|
||||
delete: (...args) => deleteText(editor, ...args),
|
||||
deselect: (...args) => deselect(editor, ...args),
|
||||
edges: (...args) => edges(editor, ...args),
|
||||
elementReadOnly: (...args) => elementReadOnly(editor, ...args),
|
||||
end: (...args) => end(editor, ...args),
|
||||
first: (...args) => first(editor, ...args),
|
||||
fragment: (...args) => fragment(editor, ...args),
|
||||
getMarks: (...args) => marks(editor, ...args),
|
||||
hasBlocks: (...args) => hasBlocks(editor, ...args),
|
||||
hasInlines: (...args) => hasInlines(editor, ...args),
|
||||
hasPath: (...args) => hasPath(editor, ...args),
|
||||
hasTexts: (...args) => hasTexts(editor, ...args),
|
||||
insertNodes: (...args) => insertNodes(editor, ...args),
|
||||
isBlock: (...args) => isBlock(editor, ...args),
|
||||
isEdge: (...args) => isEdge(editor, ...args),
|
||||
isEmpty: (...args) => isEmpty(editor, ...args),
|
||||
isEnd: (...args) => isEnd(editor, ...args),
|
||||
isNormalizing: (...args) => isNormalizing(editor, ...args),
|
||||
isStart: (...args) => isStart(editor, ...args),
|
||||
last: (...args) => last(editor, ...args),
|
||||
leaf: (...args) => leaf(editor, ...args),
|
||||
levels: (...args) => levels(editor, ...args),
|
||||
liftNodes: (...args) => liftNodes(editor, ...args),
|
||||
mergeNodes: (...args) => mergeNodes(editor, ...args),
|
||||
move: (...args) => move(editor, ...args),
|
||||
moveNodes: (...args) => moveNodes(editor, ...args),
|
||||
next: (...args) => next(editor, ...args),
|
||||
node: (...args) => node(editor, ...args),
|
||||
nodes: (...args) => nodes(editor, ...args),
|
||||
normalize: (...args) => normalize(editor, ...args),
|
||||
parent: (...args) => parent(editor, ...args),
|
||||
path: (...args) => path(editor, ...args),
|
||||
pathRef: (...args) => pathRef(editor, ...args),
|
||||
pathRefs: (...args) => pathRefs(editor, ...args),
|
||||
point: (...args) => point(editor, ...args),
|
||||
pointRef: (...args) => pointRef(editor, ...args),
|
||||
pointRefs: (...args) => pointRefs(editor, ...args),
|
||||
positions: (...args) => positions(editor, ...args),
|
||||
previous: (...args) => previous(editor, ...args),
|
||||
range: (...args) => range(editor, ...args),
|
||||
rangeRef: (...args) => rangeRef(editor, ...args),
|
||||
rangeRefs: (...args) => rangeRefs(editor, ...args),
|
||||
removeNodes: (...args) => removeNodes(editor, ...args),
|
||||
select: (...args) => select(editor, ...args),
|
||||
setNodes: (...args) => setNodes(editor, ...args),
|
||||
setNormalizing: (...args) => setNormalizing(editor, ...args),
|
||||
setPoint: (...args) => setPoint(editor, ...args),
|
||||
setSelection: (...args) => setSelection(editor, ...args),
|
||||
splitNodes: (...args) => splitNodes(editor, ...args),
|
||||
start: (...args) => start(editor, ...args),
|
||||
string: (...args) => string(editor, ...args),
|
||||
unhangRange: (...args) => unhangRange(editor, ...args),
|
||||
unsetNodes: (...args) => unsetNodes(editor, ...args),
|
||||
unwrapNodes: (...args) => unwrapNodes(editor, ...args),
|
||||
void: (...args) => getVoid(editor, ...args),
|
||||
withoutNormalizing: (...args) => withoutNormalizing(editor, ...args),
|
||||
wrapNodes: (...args) => wrapNodes(editor, ...args),
|
||||
}
|
||||
|
||||
return editor
|
||||
|
41
packages/slate/src/editor/above.ts
Normal file
41
packages/slate/src/editor/above.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Text } from '../interfaces/text'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Path } from '../interfaces/path'
|
||||
|
||||
export const above: EditorInterface['above'] = (editor, options = {}) => {
|
||||
const {
|
||||
voids = false,
|
||||
mode = 'lowest',
|
||||
at = editor.selection,
|
||||
match,
|
||||
} = options
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
const path = Editor.path(editor, at)
|
||||
const reverse = mode === 'lowest'
|
||||
|
||||
for (const [n, p] of Editor.levels(editor, {
|
||||
at: path,
|
||||
voids,
|
||||
match,
|
||||
reverse,
|
||||
})) {
|
||||
if (Text.isText(n)) continue
|
||||
if (Range.isRange(at)) {
|
||||
if (
|
||||
Path.isAncestor(p, at.anchor.path) &&
|
||||
Path.isAncestor(p, at.focus.path)
|
||||
) {
|
||||
return [n, p]
|
||||
}
|
||||
} else {
|
||||
if (!Path.equals(path, p)) {
|
||||
return [n, p]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
packages/slate/src/editor/add-mark.ts
Normal file
52
packages/slate/src/editor/add-mark.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Node } from '../interfaces/node'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Text } from '../interfaces/text'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { FLUSHING } from '../utils/weak-maps'
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const addMark: EditorInterface['addMark'] = (editor, key, value) => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection) {
|
||||
const match = (node: Node, path: Path) => {
|
||||
if (!Text.isText(node)) {
|
||||
return false // marks can only be applied to text
|
||||
}
|
||||
const [parentNode, parentPath] = Editor.parent(editor, path)
|
||||
return !editor.isVoid(parentNode) || editor.markableVoid(parentNode)
|
||||
}
|
||||
const expandedSelection = Range.isExpanded(selection)
|
||||
let markAcceptingVoidSelected = false
|
||||
if (!expandedSelection) {
|
||||
const [selectedNode, selectedPath] = Editor.node(editor, selection)
|
||||
if (selectedNode && match(selectedNode, selectedPath)) {
|
||||
const [parentNode] = Editor.parent(editor, selectedPath)
|
||||
markAcceptingVoidSelected =
|
||||
parentNode && editor.markableVoid(parentNode)
|
||||
}
|
||||
}
|
||||
if (expandedSelection || markAcceptingVoidSelected) {
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ [key]: value },
|
||||
{
|
||||
match,
|
||||
split: true,
|
||||
voids: true,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
const marks = {
|
||||
...(Editor.marks(editor) || {}),
|
||||
[key]: value,
|
||||
}
|
||||
|
||||
editor.marks = marks
|
||||
if (!FLUSHING.get(editor)) {
|
||||
editor.onChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
packages/slate/src/editor/after.ts
Normal file
27
packages/slate/src/editor/after.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const after: EditorInterface['after'] = (editor, at, options = {}) => {
|
||||
const anchor = Editor.point(editor, at, { edge: 'end' })
|
||||
const focus = Editor.end(editor, [])
|
||||
const range = { anchor, focus }
|
||||
const { distance = 1 } = options
|
||||
let d = 0
|
||||
let target
|
||||
|
||||
for (const p of Editor.positions(editor, {
|
||||
...options,
|
||||
at: range,
|
||||
})) {
|
||||
if (d > distance) {
|
||||
break
|
||||
}
|
||||
|
||||
if (d !== 0) {
|
||||
target = p
|
||||
}
|
||||
|
||||
d++
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
28
packages/slate/src/editor/before.ts
Normal file
28
packages/slate/src/editor/before.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const before: EditorInterface['before'] = (editor, at, options = {}) => {
|
||||
const anchor = Editor.start(editor, [])
|
||||
const focus = Editor.point(editor, at, { edge: 'start' })
|
||||
const range = { anchor, focus }
|
||||
const { distance = 1 } = options
|
||||
let d = 0
|
||||
let target
|
||||
|
||||
for (const p of Editor.positions(editor, {
|
||||
...options,
|
||||
at: range,
|
||||
reverse: true,
|
||||
})) {
|
||||
if (d > distance) {
|
||||
break
|
||||
}
|
||||
|
||||
if (d !== 0) {
|
||||
target = p
|
||||
}
|
||||
|
||||
d++
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
15
packages/slate/src/editor/delete-backward.ts
Normal file
15
packages/slate/src/editor/delete-backward.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Editor } from '../interfaces/editor'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { WithEditorFirstArg } from '../utils/types'
|
||||
|
||||
export const deleteBackward: WithEditorFirstArg<Editor['deleteBackward']> = (
|
||||
editor,
|
||||
unit
|
||||
) => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
Transforms.delete(editor, { unit, reverse: true })
|
||||
}
|
||||
}
|
15
packages/slate/src/editor/delete-forward.ts
Normal file
15
packages/slate/src/editor/delete-forward.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Editor } from '../interfaces/editor'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { WithEditorFirstArg } from '../utils/types'
|
||||
|
||||
export const deleteForward: WithEditorFirstArg<Editor['deleteForward']> = (
|
||||
editor,
|
||||
unit
|
||||
) => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
Transforms.delete(editor, { unit })
|
||||
}
|
||||
}
|
14
packages/slate/src/editor/delete-fragment.ts
Normal file
14
packages/slate/src/editor/delete-fragment.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const deleteFragment: EditorInterface['deleteFragment'] = (
|
||||
editor,
|
||||
{ direction = 'forward' } = {}
|
||||
) => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection && Range.isExpanded(selection)) {
|
||||
Transforms.delete(editor, { reverse: direction === 'backward' })
|
||||
}
|
||||
}
|
5
packages/slate/src/editor/edges.ts
Normal file
5
packages/slate/src/editor/edges.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const edges: EditorInterface['edges'] = (editor, at) => {
|
||||
return [Editor.start(editor, at), Editor.end(editor, at)]
|
||||
}
|
12
packages/slate/src/editor/element-read-only.ts
Normal file
12
packages/slate/src/editor/element-read-only.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const elementReadOnly: EditorInterface['elementReadOnly'] = (
|
||||
editor,
|
||||
options = {}
|
||||
) => {
|
||||
return Editor.above(editor, {
|
||||
...options,
|
||||
match: n => Element.isElement(n) && Editor.isElementReadOnly(editor, n),
|
||||
})
|
||||
}
|
5
packages/slate/src/editor/end.ts
Normal file
5
packages/slate/src/editor/end.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const end: EditorInterface['end'] = (editor, at) => {
|
||||
return Editor.point(editor, at, { edge: 'end' })
|
||||
}
|
6
packages/slate/src/editor/first.ts
Normal file
6
packages/slate/src/editor/first.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const first: EditorInterface['first'] = (editor, at) => {
|
||||
const path = Editor.path(editor, at, { edge: 'start' })
|
||||
return Editor.node(editor, path)
|
||||
}
|
7
packages/slate/src/editor/fragment.ts
Normal file
7
packages/slate/src/editor/fragment.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Node } from '../interfaces/node'
|
||||
|
||||
export const fragment: EditorInterface['fragment'] = (editor, at) => {
|
||||
const range = Editor.range(editor, at)
|
||||
return Node.fragment(editor, range)
|
||||
}
|
9
packages/slate/src/editor/get-void.ts
Normal file
9
packages/slate/src/editor/get-void.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Element } from '../interfaces/element'
|
||||
|
||||
export const getVoid: EditorInterface['void'] = (editor, options = {}) => {
|
||||
return Editor.above(editor, {
|
||||
...options,
|
||||
match: n => Element.isElement(n) && Editor.isVoid(editor, n),
|
||||
})
|
||||
}
|
8
packages/slate/src/editor/has-blocks.ts
Normal file
8
packages/slate/src/editor/has-blocks.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Element } from '../interfaces/element'
|
||||
|
||||
export const hasBlocks: EditorInterface['hasBlocks'] = (editor, element) => {
|
||||
return element.children.some(
|
||||
n => Element.isElement(n) && Editor.isBlock(editor, n)
|
||||
)
|
||||
}
|
8
packages/slate/src/editor/has-inlines.ts
Normal file
8
packages/slate/src/editor/has-inlines.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Text } from '../interfaces/text'
|
||||
|
||||
export const hasInlines: EditorInterface['hasInlines'] = (editor, element) => {
|
||||
return element.children.some(
|
||||
n => Text.isText(n) || Editor.isInline(editor, n)
|
||||
)
|
||||
}
|
6
packages/slate/src/editor/has-path.ts
Normal file
6
packages/slate/src/editor/has-path.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
import { Node } from '../interfaces/node'
|
||||
|
||||
export const hasPath: EditorInterface['hasPath'] = (editor, path) => {
|
||||
return Node.has(editor, path)
|
||||
}
|
6
packages/slate/src/editor/has-texts.ts
Normal file
6
packages/slate/src/editor/has-texts.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
import { Text } from '../interfaces/text'
|
||||
|
||||
export const hasTexts: EditorInterface['hasTexts'] = (editor, element) => {
|
||||
return element.children.every(n => Text.isText(n))
|
||||
}
|
54
packages/slate/src/editor/index.ts
Normal file
54
packages/slate/src/editor/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export * from './above'
|
||||
export * from './add-mark'
|
||||
export * from './after'
|
||||
export * from './before'
|
||||
export * from './delete-backward'
|
||||
export * from './delete-forward'
|
||||
export * from './delete-fragment'
|
||||
export * from './edges'
|
||||
export * from './element-read-only'
|
||||
export * from './end'
|
||||
export * from './first'
|
||||
export * from './fragment'
|
||||
export * from './get-void'
|
||||
export * from './has-blocks'
|
||||
export * from './has-inlines'
|
||||
export * from './has-path'
|
||||
export * from './has-texts'
|
||||
export * from './insert-break'
|
||||
export * from './insert-node'
|
||||
export * from './insert-soft-break'
|
||||
export * from './insert-text'
|
||||
export * from './is-block'
|
||||
export * from './is-edge'
|
||||
export * from './is-editor'
|
||||
export * from './is-empty'
|
||||
export * from './is-end'
|
||||
export * from './is-normalizing'
|
||||
export * from './is-start'
|
||||
export * from './last'
|
||||
export * from './leaf'
|
||||
export * from './levels'
|
||||
export * from './marks'
|
||||
export * from './next'
|
||||
export * from './node'
|
||||
export * from './nodes'
|
||||
export * from './normalize'
|
||||
export * from './parent'
|
||||
export * from './path-ref'
|
||||
export * from './path-refs'
|
||||
export * from './path'
|
||||
export * from './point-ref'
|
||||
export * from './point-refs'
|
||||
export * from './point'
|
||||
export * from './positions'
|
||||
export * from './previous'
|
||||
export * from './range-ref'
|
||||
export * from './range-refs'
|
||||
export * from './range'
|
||||
export * from './remove-mark'
|
||||
export * from './set-normalizing'
|
||||
export * from './start'
|
||||
export * from './string'
|
||||
export * from './unhang-range'
|
||||
export * from './without-normalizing'
|
6
packages/slate/src/editor/insert-break.ts
Normal file
6
packages/slate/src/editor/insert-break.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const insertBreak: EditorInterface['insertBreak'] = editor => {
|
||||
Transforms.splitNodes(editor, { always: true })
|
||||
}
|
6
packages/slate/src/editor/insert-node.ts
Normal file
6
packages/slate/src/editor/insert-node.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const insertNode: EditorInterface['insertNode'] = (editor, node) => {
|
||||
Transforms.insertNodes(editor, node)
|
||||
}
|
6
packages/slate/src/editor/insert-soft-break.ts
Normal file
6
packages/slate/src/editor/insert-soft-break.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const insertSoftBreak: EditorInterface['insertSoftBreak'] = editor => {
|
||||
Transforms.splitNodes(editor, { always: true })
|
||||
}
|
21
packages/slate/src/editor/insert-text.ts
Normal file
21
packages/slate/src/editor/insert-text.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const insertText: EditorInterface['insertText'] = (
|
||||
editor,
|
||||
text,
|
||||
options = {}
|
||||
) => {
|
||||
const { selection, marks } = editor
|
||||
|
||||
if (selection) {
|
||||
if (marks) {
|
||||
const node = { text, ...marks }
|
||||
Transforms.insertNodes(editor, node)
|
||||
} else {
|
||||
Transforms.insertText(editor, text, options)
|
||||
}
|
||||
|
||||
editor.marks = null
|
||||
}
|
||||
}
|
5
packages/slate/src/editor/is-block.ts
Normal file
5
packages/slate/src/editor/is-block.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const isBlock: EditorInterface['isBlock'] = (editor, value) => {
|
||||
return !editor.isInline(value)
|
||||
}
|
5
packages/slate/src/editor/is-edge.ts
Normal file
5
packages/slate/src/editor/is-edge.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const isEdge: EditorInterface['isEdge'] = (editor, point, at) => {
|
||||
return Editor.isStart(editor, point, at) || Editor.isEnd(editor, point, at)
|
||||
}
|
44
packages/slate/src/editor/is-editor.ts
Normal file
44
packages/slate/src/editor/is-editor.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Node } from '../interfaces/node'
|
||||
import { Operation } from '../interfaces/operation'
|
||||
|
||||
const IS_EDITOR_CACHE = new WeakMap<object, boolean>()
|
||||
|
||||
export const isEditor: EditorInterface['isEditor'] = (
|
||||
value: any
|
||||
): value is Editor => {
|
||||
const cachedIsEditor = IS_EDITOR_CACHE.get(value)
|
||||
if (cachedIsEditor !== undefined) {
|
||||
return cachedIsEditor
|
||||
}
|
||||
|
||||
if (!isPlainObject(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isEditor =
|
||||
typeof value.addMark === 'function' &&
|
||||
typeof value.apply === 'function' &&
|
||||
typeof value.deleteFragment === 'function' &&
|
||||
typeof value.insertBreak === 'function' &&
|
||||
typeof value.insertSoftBreak === 'function' &&
|
||||
typeof value.insertFragment === 'function' &&
|
||||
typeof value.insertNode === 'function' &&
|
||||
typeof value.insertText === 'function' &&
|
||||
typeof value.isElementReadOnly === 'function' &&
|
||||
typeof value.isInline === 'function' &&
|
||||
typeof value.isSelectable === 'function' &&
|
||||
typeof value.isVoid === 'function' &&
|
||||
typeof value.normalizeNode === 'function' &&
|
||||
typeof value.onChange === 'function' &&
|
||||
typeof value.removeMark === 'function' &&
|
||||
typeof value.getDirtyPaths === 'function' &&
|
||||
(value.marks === null || isPlainObject(value.marks)) &&
|
||||
(value.selection === null || Range.isRange(value.selection)) &&
|
||||
Node.isNodeList(value.children) &&
|
||||
Operation.isOperationList(value.operations)
|
||||
IS_EDITOR_CACHE.set(value, isEditor)
|
||||
return isEditor
|
||||
}
|
14
packages/slate/src/editor/is-empty.ts
Normal file
14
packages/slate/src/editor/is-empty.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
import { Text } from '../interfaces/text'
|
||||
|
||||
export const isEmpty: EditorInterface['isEmpty'] = (editor, element) => {
|
||||
const { children } = element
|
||||
const [first] = children
|
||||
return (
|
||||
children.length === 0 ||
|
||||
(children.length === 1 &&
|
||||
Text.isText(first) &&
|
||||
first.text === '' &&
|
||||
!editor.isVoid(element))
|
||||
)
|
||||
}
|
7
packages/slate/src/editor/is-end.ts
Normal file
7
packages/slate/src/editor/is-end.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Point } from '../interfaces/point'
|
||||
|
||||
export const isEnd: EditorInterface['isEnd'] = (editor, point, at) => {
|
||||
const end = Editor.end(editor, at)
|
||||
return Point.equals(point, end)
|
||||
}
|
7
packages/slate/src/editor/is-normalizing.ts
Normal file
7
packages/slate/src/editor/is-normalizing.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
import { NORMALIZING } from '../utils/weak-maps'
|
||||
|
||||
export const isNormalizing: EditorInterface['isNormalizing'] = editor => {
|
||||
const isNormalizing = NORMALIZING.get(editor)
|
||||
return isNormalizing === undefined ? true : isNormalizing
|
||||
}
|
12
packages/slate/src/editor/is-start.ts
Normal file
12
packages/slate/src/editor/is-start.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Point } from '../interfaces/point'
|
||||
|
||||
export const isStart: EditorInterface['isStart'] = (editor, point, at) => {
|
||||
// PERF: If the offset isn't `0` we know it's not the start.
|
||||
if (point.offset !== 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const start = Editor.start(editor, at)
|
||||
return Point.equals(point, start)
|
||||
}
|
6
packages/slate/src/editor/last.ts
Normal file
6
packages/slate/src/editor/last.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const last: EditorInterface['last'] = (editor, at) => {
|
||||
const path = Editor.path(editor, at, { edge: 'end' })
|
||||
return Editor.node(editor, path)
|
||||
}
|
8
packages/slate/src/editor/leaf.ts
Normal file
8
packages/slate/src/editor/leaf.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Node } from '../interfaces/node'
|
||||
|
||||
export const leaf: EditorInterface['leaf'] = (editor, at, options = {}) => {
|
||||
const path = Editor.path(editor, at, options)
|
||||
const node = Node.leaf(editor, path)
|
||||
return [node, path]
|
||||
}
|
40
packages/slate/src/editor/levels.ts
Normal file
40
packages/slate/src/editor/levels.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Node, NodeEntry } from '../interfaces/node'
|
||||
import { Editor, EditorLevelsOptions } from '../interfaces/editor'
|
||||
import { Element } from '../interfaces/element'
|
||||
|
||||
export function* levels<T extends Node>(
|
||||
editor: Editor,
|
||||
options: EditorLevelsOptions<T> = {}
|
||||
): Generator<NodeEntry<T>, void, undefined> {
|
||||
const { at = editor.selection, reverse = false, voids = false } = options
|
||||
let { match } = options
|
||||
|
||||
if (match == null) {
|
||||
match = () => true
|
||||
}
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
const levels: NodeEntry<T>[] = []
|
||||
const path = Editor.path(editor, at)
|
||||
|
||||
for (const [n, p] of Node.levels(editor, path)) {
|
||||
if (!match(n, p)) {
|
||||
continue
|
||||
}
|
||||
|
||||
levels.push([n, p])
|
||||
|
||||
if (!voids && Element.isElement(n) && Editor.isVoid(editor, n)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (reverse) {
|
||||
levels.reverse()
|
||||
}
|
||||
|
||||
yield* levels
|
||||
}
|
61
packages/slate/src/editor/marks.ts
Normal file
61
packages/slate/src/editor/marks.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { NodeEntry } from '../interfaces/node'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Text } from '../interfaces/text'
|
||||
import { Element } from '../interfaces/element'
|
||||
|
||||
export const marks: EditorInterface['marks'] = (editor, options = {}) => {
|
||||
const { marks, selection } = editor
|
||||
|
||||
if (!selection) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (marks) {
|
||||
return marks
|
||||
}
|
||||
|
||||
if (Range.isExpanded(selection)) {
|
||||
const [match] = Editor.nodes(editor, { match: Text.isText })
|
||||
|
||||
if (match) {
|
||||
const [node] = match as NodeEntry<Text>
|
||||
const { text, ...rest } = node
|
||||
return rest
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const { anchor } = selection
|
||||
const { path } = anchor
|
||||
let [node] = Editor.leaf(editor, path)
|
||||
|
||||
if (anchor.offset === 0) {
|
||||
const prev = Editor.previous(editor, { at: path, match: Text.isText })
|
||||
const markedVoid = Editor.above(editor, {
|
||||
match: n =>
|
||||
Element.isElement(n) &&
|
||||
Editor.isVoid(editor, n) &&
|
||||
editor.markableVoid(n),
|
||||
})
|
||||
if (!markedVoid) {
|
||||
const block = Editor.above(editor, {
|
||||
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
|
||||
})
|
||||
|
||||
if (prev && block) {
|
||||
const [prevNode, prevPath] = prev
|
||||
const [, blockPath] = block
|
||||
|
||||
if (Path.isAncestor(blockPath, prevPath)) {
|
||||
node = prevNode as Text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { text, ...rest } = node
|
||||
return rest
|
||||
}
|
36
packages/slate/src/editor/next.ts
Normal file
36
packages/slate/src/editor/next.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Span } from '../interfaces/location'
|
||||
import { Path } from '../interfaces/path'
|
||||
|
||||
export const next: EditorInterface['next'] = (editor, options = {}) => {
|
||||
const { mode = 'lowest', voids = false } = options
|
||||
let { match, at = editor.selection } = options
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
const pointAfterLocation = Editor.after(editor, at, { voids })
|
||||
|
||||
if (!pointAfterLocation) return
|
||||
|
||||
const [, to] = Editor.last(editor, [])
|
||||
|
||||
const span: Span = [pointAfterLocation.path, to]
|
||||
|
||||
if (Path.isPath(at) && at.length === 0) {
|
||||
throw new Error(`Cannot get the next node from the root node!`)
|
||||
}
|
||||
|
||||
if (match == null) {
|
||||
if (Path.isPath(at)) {
|
||||
const [parent] = Editor.parent(editor, at)
|
||||
match = n => parent.children.includes(n)
|
||||
} else {
|
||||
match = () => true
|
||||
}
|
||||
}
|
||||
|
||||
const [next] = Editor.nodes(editor, { at: span, match, mode, voids })
|
||||
return next
|
||||
}
|
8
packages/slate/src/editor/node.ts
Normal file
8
packages/slate/src/editor/node.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Node } from '../interfaces/node'
|
||||
|
||||
export const node: EditorInterface['node'] = (editor, at, options = {}) => {
|
||||
const path = Editor.path(editor, at, options)
|
||||
const node = Node.get(editor, path)
|
||||
return [node, path]
|
||||
}
|
124
packages/slate/src/editor/nodes.ts
Normal file
124
packages/slate/src/editor/nodes.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Node, NodeEntry } from '../interfaces/node'
|
||||
import { Editor, EditorNodesOptions } from '../interfaces/editor'
|
||||
import { Span } from '../interfaces/location'
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Text } from '../interfaces/text'
|
||||
|
||||
export function* nodes<T extends Node>(
|
||||
editor: Editor,
|
||||
options: EditorNodesOptions<T> = {}
|
||||
): Generator<NodeEntry<T>, void, undefined> {
|
||||
const {
|
||||
at = editor.selection,
|
||||
mode = 'all',
|
||||
universal = false,
|
||||
reverse = false,
|
||||
voids = false,
|
||||
ignoreNonSelectable = false,
|
||||
} = options
|
||||
let { match } = options
|
||||
|
||||
if (!match) {
|
||||
match = () => true
|
||||
}
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
let from
|
||||
let to
|
||||
|
||||
if (Span.isSpan(at)) {
|
||||
from = at[0]
|
||||
to = at[1]
|
||||
} else {
|
||||
const first = Editor.path(editor, at, { edge: 'start' })
|
||||
const last = Editor.path(editor, at, { edge: 'end' })
|
||||
from = reverse ? last : first
|
||||
to = reverse ? first : last
|
||||
}
|
||||
|
||||
const nodeEntries = Node.nodes(editor, {
|
||||
reverse,
|
||||
from,
|
||||
to,
|
||||
pass: ([node]) => {
|
||||
if (!Element.isElement(node)) return false
|
||||
if (
|
||||
!voids &&
|
||||
(Editor.isVoid(editor, node) || Editor.isElementReadOnly(editor, node))
|
||||
)
|
||||
return true
|
||||
if (ignoreNonSelectable && !Editor.isSelectable(editor, node)) return true
|
||||
return false
|
||||
},
|
||||
})
|
||||
|
||||
const matches: NodeEntry<T>[] = []
|
||||
let hit: NodeEntry<T> | undefined
|
||||
|
||||
for (const [node, path] of nodeEntries) {
|
||||
if (
|
||||
ignoreNonSelectable &&
|
||||
Element.isElement(node) &&
|
||||
!Editor.isSelectable(editor, node)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const isLower = hit && Path.compare(path, hit[1]) === 0
|
||||
|
||||
// In highest mode any node lower than the last hit is not a match.
|
||||
if (mode === 'highest' && isLower) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!match(node, path)) {
|
||||
// If we've arrived at a leaf text node that is not lower than the last
|
||||
// hit, then we've found a branch that doesn't include a match, which
|
||||
// means the match is not universal.
|
||||
if (universal && !isLower && Text.isText(node)) {
|
||||
return
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a match and it's lower than the last, update the hit.
|
||||
if (mode === 'lowest' && isLower) {
|
||||
hit = [node, path]
|
||||
continue
|
||||
}
|
||||
|
||||
// In lowest mode we emit the last hit, once it's guaranteed lowest.
|
||||
const emit: NodeEntry<T> | undefined =
|
||||
mode === 'lowest' ? hit : [node, path]
|
||||
|
||||
if (emit) {
|
||||
if (universal) {
|
||||
matches.push(emit)
|
||||
} else {
|
||||
yield emit
|
||||
}
|
||||
}
|
||||
|
||||
hit = [node, path]
|
||||
}
|
||||
|
||||
// Since lowest is always emitting one behind, catch up at the end.
|
||||
if (mode === 'lowest' && hit) {
|
||||
if (universal) {
|
||||
matches.push(hit)
|
||||
} else {
|
||||
yield hit
|
||||
}
|
||||
}
|
||||
|
||||
// Universal defers to ensure that the match occurs in every branch, so we
|
||||
// yield all of the matches after iterating.
|
||||
if (universal) {
|
||||
yield* matches
|
||||
}
|
||||
}
|
93
packages/slate/src/editor/normalize.ts
Normal file
93
packages/slate/src/editor/normalize.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { DIRTY_PATH_KEYS, DIRTY_PATHS } from '../utils/weak-maps'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Node } from '../interfaces/node'
|
||||
import { Element } from '../interfaces/element'
|
||||
|
||||
export const normalize: EditorInterface['normalize'] = (
|
||||
editor,
|
||||
options = {}
|
||||
) => {
|
||||
const { force = false, operation } = options
|
||||
const getDirtyPaths = (editor: Editor) => {
|
||||
return DIRTY_PATHS.get(editor) || []
|
||||
}
|
||||
|
||||
const getDirtyPathKeys = (editor: Editor) => {
|
||||
return DIRTY_PATH_KEYS.get(editor) || new Set()
|
||||
}
|
||||
|
||||
const popDirtyPath = (editor: Editor): Path => {
|
||||
const path = getDirtyPaths(editor).pop()!
|
||||
const key = path.join(',')
|
||||
getDirtyPathKeys(editor).delete(key)
|
||||
return path
|
||||
}
|
||||
|
||||
if (!Editor.isNormalizing(editor)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (force) {
|
||||
const allPaths = Array.from(Node.nodes(editor), ([, p]) => p)
|
||||
const allPathKeys = new Set(allPaths.map(p => p.join(',')))
|
||||
DIRTY_PATHS.set(editor, allPaths)
|
||||
DIRTY_PATH_KEYS.set(editor, allPathKeys)
|
||||
}
|
||||
|
||||
if (getDirtyPaths(editor).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
/*
|
||||
Fix dirty elements with no children.
|
||||
editor.normalizeNode() does fix this, but some normalization fixes also require it to work.
|
||||
Running an initial pass avoids the catch-22 race condition.
|
||||
*/
|
||||
for (const dirtyPath of getDirtyPaths(editor)) {
|
||||
if (Node.has(editor, dirtyPath)) {
|
||||
const entry = Editor.node(editor, dirtyPath)
|
||||
const [node, _] = entry
|
||||
|
||||
/*
|
||||
The default normalizer inserts an empty text node in this scenario, but it can be customised.
|
||||
So there is some risk here.
|
||||
|
||||
As long as the normalizer only inserts child nodes for this case it is safe to do in any order;
|
||||
by definition adding children to an empty node can't cause other paths to change.
|
||||
*/
|
||||
if (Element.isElement(node) && node.children.length === 0) {
|
||||
editor.normalizeNode(entry, { operation })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dirtyPaths = getDirtyPaths(editor)
|
||||
const initialDirtyPathsLength = dirtyPaths.length
|
||||
let iteration = 0
|
||||
|
||||
while (dirtyPaths.length !== 0) {
|
||||
if (
|
||||
!editor.shouldNormalize({
|
||||
dirtyPaths,
|
||||
iteration,
|
||||
initialDirtyPathsLength,
|
||||
operation,
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const dirtyPath = popDirtyPath(editor)
|
||||
|
||||
// If the node doesn't exist in the tree, it does not need to be normalized.
|
||||
if (Node.has(editor, dirtyPath)) {
|
||||
const entry = Editor.node(editor, dirtyPath)
|
||||
editor.normalizeNode(entry, { operation })
|
||||
}
|
||||
iteration++
|
||||
dirtyPaths = getDirtyPaths(editor)
|
||||
}
|
||||
})
|
||||
}
|
10
packages/slate/src/editor/parent.ts
Normal file
10
packages/slate/src/editor/parent.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Ancestor, NodeEntry } from '../interfaces/node'
|
||||
|
||||
export const parent: EditorInterface['parent'] = (editor, at, options = {}) => {
|
||||
const path = Editor.path(editor, at, options)
|
||||
const parentPath = Path.parent(path)
|
||||
const entry = Editor.node(editor, parentPath)
|
||||
return entry as NodeEntry<Ancestor>
|
||||
}
|
25
packages/slate/src/editor/path-ref.ts
Normal file
25
packages/slate/src/editor/path-ref.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { PathRef } from '../interfaces/path-ref'
|
||||
|
||||
export const pathRef: EditorInterface['pathRef'] = (
|
||||
editor,
|
||||
path,
|
||||
options = {}
|
||||
) => {
|
||||
const { affinity = 'forward' } = options
|
||||
const ref: PathRef = {
|
||||
current: path,
|
||||
affinity,
|
||||
unref() {
|
||||
const { current } = ref
|
||||
const pathRefs = Editor.pathRefs(editor)
|
||||
pathRefs.delete(ref)
|
||||
ref.current = null
|
||||
return current
|
||||
},
|
||||
}
|
||||
|
||||
const refs = Editor.pathRefs(editor)
|
||||
refs.add(ref)
|
||||
return ref
|
||||
}
|
13
packages/slate/src/editor/path-refs.ts
Normal file
13
packages/slate/src/editor/path-refs.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
import { PATH_REFS } from '../utils/weak-maps'
|
||||
|
||||
export const pathRefs: EditorInterface['pathRefs'] = editor => {
|
||||
let refs = PATH_REFS.get(editor)
|
||||
|
||||
if (!refs) {
|
||||
refs = new Set()
|
||||
PATH_REFS.set(editor, refs)
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
35
packages/slate/src/editor/path.ts
Normal file
35
packages/slate/src/editor/path.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { EditorInterface, Node, Path, Point, Range } from '../interfaces'
|
||||
|
||||
export const path: EditorInterface['path'] = (editor, at, options = {}) => {
|
||||
const { depth, edge } = options
|
||||
|
||||
if (Path.isPath(at)) {
|
||||
if (edge === 'start') {
|
||||
const [, firstPath] = Node.first(editor, at)
|
||||
at = firstPath
|
||||
} else if (edge === 'end') {
|
||||
const [, lastPath] = Node.last(editor, at)
|
||||
at = lastPath
|
||||
}
|
||||
}
|
||||
|
||||
if (Range.isRange(at)) {
|
||||
if (edge === 'start') {
|
||||
at = Range.start(at)
|
||||
} else if (edge === 'end') {
|
||||
at = Range.end(at)
|
||||
} else {
|
||||
at = Path.common(at.anchor.path, at.focus.path)
|
||||
}
|
||||
}
|
||||
|
||||
if (Point.isPoint(at)) {
|
||||
at = at.path
|
||||
}
|
||||
|
||||
if (depth != null) {
|
||||
at = at.slice(0, depth)
|
||||
}
|
||||
|
||||
return at
|
||||
}
|
25
packages/slate/src/editor/point-ref.ts
Normal file
25
packages/slate/src/editor/point-ref.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { PointRef } from '../interfaces/point-ref'
|
||||
|
||||
export const pointRef: EditorInterface['pointRef'] = (
|
||||
editor,
|
||||
point,
|
||||
options = {}
|
||||
) => {
|
||||
const { affinity = 'forward' } = options
|
||||
const ref: PointRef = {
|
||||
current: point,
|
||||
affinity,
|
||||
unref() {
|
||||
const { current } = ref
|
||||
const pointRefs = Editor.pointRefs(editor)
|
||||
pointRefs.delete(ref)
|
||||
ref.current = null
|
||||
return current
|
||||
},
|
||||
}
|
||||
|
||||
const refs = Editor.pointRefs(editor)
|
||||
refs.add(ref)
|
||||
return ref
|
||||
}
|
13
packages/slate/src/editor/point-refs.ts
Normal file
13
packages/slate/src/editor/point-refs.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
import { POINT_REFS } from '../utils/weak-maps'
|
||||
|
||||
export const pointRefs: EditorInterface['pointRefs'] = editor => {
|
||||
let refs = POINT_REFS.get(editor)
|
||||
|
||||
if (!refs) {
|
||||
refs = new Set()
|
||||
POINT_REFS.set(editor, refs)
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
38
packages/slate/src/editor/point.ts
Normal file
38
packages/slate/src/editor/point.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Node } from '../interfaces/node'
|
||||
import { Text } from '../interfaces/text'
|
||||
import { Range } from '../interfaces/range'
|
||||
|
||||
export const point: EditorInterface['point'] = (editor, at, options = {}) => {
|
||||
const { edge = 'start' } = options
|
||||
|
||||
if (Path.isPath(at)) {
|
||||
let path
|
||||
|
||||
if (edge === 'end') {
|
||||
const [, lastPath] = Node.last(editor, at)
|
||||
path = lastPath
|
||||
} else {
|
||||
const [, firstPath] = Node.first(editor, at)
|
||||
path = firstPath
|
||||
}
|
||||
|
||||
const node = Node.get(editor, path)
|
||||
|
||||
if (!Text.isText(node)) {
|
||||
throw new Error(
|
||||
`Cannot get the ${edge} point in the node at path [${at}] because it has no ${edge} text node.`
|
||||
)
|
||||
}
|
||||
|
||||
return { path, offset: edge === 'end' ? node.text.length : 0 }
|
||||
}
|
||||
|
||||
if (Range.isRange(at)) {
|
||||
const [start, end] = Range.edges(at)
|
||||
return edge === 'start' ? start : end
|
||||
}
|
||||
|
||||
return at
|
||||
}
|
190
packages/slate/src/editor/positions.ts
Normal file
190
packages/slate/src/editor/positions.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { Editor, EditorPositionsOptions } from '../interfaces/editor'
|
||||
import { Point } from '../interfaces/point'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Text } from '../interfaces/text'
|
||||
import {
|
||||
getCharacterDistance,
|
||||
getWordDistance,
|
||||
splitByCharacterDistance,
|
||||
} from '../utils/string'
|
||||
|
||||
export function* positions(
|
||||
editor: Editor,
|
||||
options: EditorPositionsOptions = {}
|
||||
): Generator<Point, void, undefined> {
|
||||
const {
|
||||
at = editor.selection,
|
||||
unit = 'offset',
|
||||
reverse = false,
|
||||
voids = false,
|
||||
ignoreNonSelectable = false,
|
||||
} = options
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm notes:
|
||||
*
|
||||
* Each step `distance` is dynamic depending on the underlying text
|
||||
* and the `unit` specified. Each step, e.g., a line or word, may
|
||||
* span multiple text nodes, so we iterate through the text both on
|
||||
* two levels in step-sync:
|
||||
*
|
||||
* `leafText` stores the text on a text leaf level, and is advanced
|
||||
* through using the counters `leafTextOffset` and `leafTextRemaining`.
|
||||
*
|
||||
* `blockText` stores the text on a block level, and is shortened
|
||||
* by `distance` every time it is advanced.
|
||||
*
|
||||
* We only maintain a window of one blockText and one leafText because
|
||||
* a block node always appears before all of its leaf nodes.
|
||||
*/
|
||||
|
||||
const range = Editor.range(editor, at)
|
||||
const [start, end] = Range.edges(range)
|
||||
const first = reverse ? end : start
|
||||
let isNewBlock = false
|
||||
let blockText = ''
|
||||
let distance = 0 // Distance for leafText to catch up to blockText.
|
||||
let leafTextRemaining = 0
|
||||
let leafTextOffset = 0
|
||||
|
||||
// Iterate through all nodes in range, grabbing entire textual content
|
||||
// of block nodes in blockText, and text nodes in leafText.
|
||||
// Exploits the fact that nodes are sequenced in such a way that we first
|
||||
// encounter the block node, then all of its text nodes, so when iterating
|
||||
// through the blockText and leafText we just need to remember a window of
|
||||
// one block node and leaf node, respectively.
|
||||
for (const [node, path] of Editor.nodes(editor, {
|
||||
at,
|
||||
reverse,
|
||||
voids,
|
||||
ignoreNonSelectable,
|
||||
})) {
|
||||
/*
|
||||
* ELEMENT NODE - Yield position(s) for voids, collect blockText for blocks
|
||||
*/
|
||||
if (Element.isElement(node)) {
|
||||
// Void nodes are a special case, so by default we will always
|
||||
// yield their first point. If the `voids` option is set to true,
|
||||
// then we will iterate over their content.
|
||||
if (!voids && (editor.isVoid(node) || editor.isElementReadOnly(node))) {
|
||||
yield Editor.start(editor, path)
|
||||
continue
|
||||
}
|
||||
|
||||
// Inline element nodes are ignored as they don't themselves
|
||||
// contribute to `blockText` or `leafText` - their parent and
|
||||
// children do.
|
||||
if (editor.isInline(node)) continue
|
||||
|
||||
// Block element node - set `blockText` to its text content.
|
||||
if (Editor.hasInlines(editor, node)) {
|
||||
// We always exhaust block nodes before encountering a new one:
|
||||
// console.assert(blockText === '',
|
||||
// `blockText='${blockText}' - `+
|
||||
// `not exhausted before new block node`, path)
|
||||
|
||||
// Ensure range considered is capped to `range`, in the
|
||||
// start/end edge cases where block extends beyond range.
|
||||
// Equivalent to this, but presumably more performant:
|
||||
// blockRange = Editor.range(editor, ...Editor.edges(editor, path))
|
||||
// blockRange = Range.intersection(range, blockRange) // intersect
|
||||
// blockText = Editor.string(editor, blockRange, { voids })
|
||||
const e = Path.isAncestor(path, end.path)
|
||||
? end
|
||||
: Editor.end(editor, path)
|
||||
const s = Path.isAncestor(path, start.path)
|
||||
? start
|
||||
: Editor.start(editor, path)
|
||||
|
||||
blockText = Editor.string(editor, { anchor: s, focus: e }, { voids })
|
||||
isNewBlock = true
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* TEXT LEAF NODE - Iterate through text content, yielding
|
||||
* positions every `distance` offset according to `unit`.
|
||||
*/
|
||||
if (Text.isText(node)) {
|
||||
const isFirst = Path.equals(path, first.path)
|
||||
|
||||
// Proof that we always exhaust text nodes before encountering a new one:
|
||||
// console.assert(leafTextRemaining <= 0,
|
||||
// `leafTextRemaining=${leafTextRemaining} - `+
|
||||
// `not exhausted before new leaf text node`, path)
|
||||
|
||||
// Reset `leafText` counters for new text node.
|
||||
if (isFirst) {
|
||||
leafTextRemaining = reverse
|
||||
? first.offset
|
||||
: node.text.length - first.offset
|
||||
leafTextOffset = first.offset // Works for reverse too.
|
||||
} else {
|
||||
leafTextRemaining = node.text.length
|
||||
leafTextOffset = reverse ? leafTextRemaining : 0
|
||||
}
|
||||
|
||||
// Yield position at the start of node (potentially).
|
||||
if (isFirst || isNewBlock || unit === 'offset') {
|
||||
yield { path, offset: leafTextOffset }
|
||||
isNewBlock = false
|
||||
}
|
||||
|
||||
// Yield positions every (dynamically calculated) `distance` offset.
|
||||
while (true) {
|
||||
// If `leafText` has caught up with `blockText` (distance=0),
|
||||
// and if blockText is exhausted, break to get another block node,
|
||||
// otherwise advance blockText forward by the new `distance`.
|
||||
if (distance === 0) {
|
||||
if (blockText === '') break
|
||||
distance = calcDistance(blockText, unit, reverse)
|
||||
// Split the string at the previously found distance and use the
|
||||
// remaining string for the next iteration.
|
||||
blockText = splitByCharacterDistance(blockText, distance, reverse)[1]
|
||||
}
|
||||
|
||||
// Advance `leafText` by the current `distance`.
|
||||
leafTextOffset = reverse
|
||||
? leafTextOffset - distance
|
||||
: leafTextOffset + distance
|
||||
leafTextRemaining = leafTextRemaining - distance
|
||||
|
||||
// If `leafText` is exhausted, break to get a new leaf node
|
||||
// and set distance to the overflow amount, so we'll (maybe)
|
||||
// catch up to blockText in the next leaf text node.
|
||||
if (leafTextRemaining < 0) {
|
||||
distance = -leafTextRemaining
|
||||
break
|
||||
}
|
||||
|
||||
// Successfully walked `distance` offsets through `leafText`
|
||||
// to catch up with `blockText`, so we can reset `distance`
|
||||
// and yield this position in this node.
|
||||
distance = 0
|
||||
yield { path, offset: leafTextOffset }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Proof that upon completion, we've exahusted both leaf and block text:
|
||||
// console.assert(leafTextRemaining <= 0, "leafText wasn't exhausted")
|
||||
// console.assert(blockText === '', "blockText wasn't exhausted")
|
||||
|
||||
// Helper:
|
||||
// Return the distance in offsets for a step of size `unit` on given string.
|
||||
function calcDistance(text: string, unit: string, reverse?: boolean) {
|
||||
if (unit === 'character') {
|
||||
return getCharacterDistance(text, reverse)
|
||||
} else if (unit === 'word') {
|
||||
return getWordDistance(text, reverse)
|
||||
} else if (unit === 'line' || unit === 'block') {
|
||||
return text.length
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
47
packages/slate/src/editor/previous.ts
Normal file
47
packages/slate/src/editor/previous.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Span } from '../interfaces/location'
|
||||
import { Path } from '../interfaces/path'
|
||||
|
||||
export const previous: EditorInterface['previous'] = (editor, options = {}) => {
|
||||
const { mode = 'lowest', voids = false } = options
|
||||
let { match, at = editor.selection } = options
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
const pointBeforeLocation = Editor.before(editor, at, { voids })
|
||||
|
||||
if (!pointBeforeLocation) {
|
||||
return
|
||||
}
|
||||
|
||||
const [, to] = Editor.first(editor, [])
|
||||
|
||||
// The search location is from the start of the document to the path of
|
||||
// the point before the location passed in
|
||||
const span: Span = [pointBeforeLocation.path, to]
|
||||
|
||||
if (Path.isPath(at) && at.length === 0) {
|
||||
throw new Error(`Cannot get the previous node from the root node!`)
|
||||
}
|
||||
|
||||
if (match == null) {
|
||||
if (Path.isPath(at)) {
|
||||
const [parent] = Editor.parent(editor, at)
|
||||
match = n => parent.children.includes(n)
|
||||
} else {
|
||||
match = () => true
|
||||
}
|
||||
}
|
||||
|
||||
const [previous] = Editor.nodes(editor, {
|
||||
reverse: true,
|
||||
at: span,
|
||||
match,
|
||||
mode,
|
||||
voids,
|
||||
})
|
||||
|
||||
return previous
|
||||
}
|
25
packages/slate/src/editor/range-ref.ts
Normal file
25
packages/slate/src/editor/range-ref.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { RangeRef } from '../interfaces/range-ref'
|
||||
|
||||
export const rangeRef: EditorInterface['rangeRef'] = (
|
||||
editor,
|
||||
range,
|
||||
options = {}
|
||||
) => {
|
||||
const { affinity = 'forward' } = options
|
||||
const ref: RangeRef = {
|
||||
current: range,
|
||||
affinity,
|
||||
unref() {
|
||||
const { current } = ref
|
||||
const rangeRefs = Editor.rangeRefs(editor)
|
||||
rangeRefs.delete(ref)
|
||||
ref.current = null
|
||||
return current
|
||||
},
|
||||
}
|
||||
|
||||
const refs = Editor.rangeRefs(editor)
|
||||
refs.add(ref)
|
||||
return ref
|
||||
}
|
13
packages/slate/src/editor/range-refs.ts
Normal file
13
packages/slate/src/editor/range-refs.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
import { RANGE_REFS } from '../utils/weak-maps'
|
||||
|
||||
export const rangeRefs: EditorInterface['rangeRefs'] = editor => {
|
||||
let refs = RANGE_REFS.get(editor)
|
||||
|
||||
if (!refs) {
|
||||
refs = new Set()
|
||||
RANGE_REFS.set(editor, refs)
|
||||
}
|
||||
|
||||
return refs
|
||||
}
|
12
packages/slate/src/editor/range.ts
Normal file
12
packages/slate/src/editor/range.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Range } from '../interfaces/range'
|
||||
|
||||
export const range: EditorInterface['range'] = (editor, at, to) => {
|
||||
if (Range.isRange(at) && !to) {
|
||||
return at
|
||||
}
|
||||
|
||||
const start = Editor.start(editor, at)
|
||||
const end = Editor.end(editor, to || at)
|
||||
return { anchor: start, focus: end }
|
||||
}
|
45
packages/slate/src/editor/remove-mark.ts
Normal file
45
packages/slate/src/editor/remove-mark.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Node } from '../interfaces/node'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Text } from '../interfaces/text'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { FLUSHING } from '../utils/weak-maps'
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const removeMark: EditorInterface['removeMark'] = (editor, key) => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection) {
|
||||
const match = (node: Node, path: Path) => {
|
||||
if (!Text.isText(node)) {
|
||||
return false // marks can only be applied to text
|
||||
}
|
||||
const [parentNode, parentPath] = Editor.parent(editor, path)
|
||||
return !editor.isVoid(parentNode) || editor.markableVoid(parentNode)
|
||||
}
|
||||
const expandedSelection = Range.isExpanded(selection)
|
||||
let markAcceptingVoidSelected = false
|
||||
if (!expandedSelection) {
|
||||
const [selectedNode, selectedPath] = Editor.node(editor, selection)
|
||||
if (selectedNode && match(selectedNode, selectedPath)) {
|
||||
const [parentNode] = Editor.parent(editor, selectedPath)
|
||||
markAcceptingVoidSelected =
|
||||
parentNode && editor.markableVoid(parentNode)
|
||||
}
|
||||
}
|
||||
if (expandedSelection || markAcceptingVoidSelected) {
|
||||
Transforms.unsetNodes(editor, key, {
|
||||
match,
|
||||
split: true,
|
||||
voids: true,
|
||||
})
|
||||
} else {
|
||||
const marks = { ...(Editor.marks(editor) || {}) }
|
||||
delete marks[key]
|
||||
editor.marks = marks
|
||||
if (!FLUSHING.get(editor)) {
|
||||
editor.onChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
packages/slate/src/editor/set-normalizing.ts
Normal file
9
packages/slate/src/editor/set-normalizing.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { EditorInterface } from '../interfaces/editor'
|
||||
import { NORMALIZING } from '../utils/weak-maps'
|
||||
|
||||
export const setNormalizing: EditorInterface['setNormalizing'] = (
|
||||
editor,
|
||||
isNormalizing
|
||||
) => {
|
||||
NORMALIZING.set(editor, isNormalizing)
|
||||
}
|
5
packages/slate/src/editor/start.ts
Normal file
5
packages/slate/src/editor/start.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const start: EditorInterface['start'] = (editor, at) => {
|
||||
return Editor.point(editor, at, { edge: 'start' })
|
||||
}
|
31
packages/slate/src/editor/string.ts
Normal file
31
packages/slate/src/editor/string.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Text } from '../interfaces/text'
|
||||
import { Path } from '../interfaces/path'
|
||||
|
||||
export const string: EditorInterface['string'] = (editor, at, options = {}) => {
|
||||
const { voids = false } = options
|
||||
const range = Editor.range(editor, at)
|
||||
const [start, end] = Range.edges(range)
|
||||
let text = ''
|
||||
|
||||
for (const [node, path] of Editor.nodes(editor, {
|
||||
at: range,
|
||||
match: Text.isText,
|
||||
voids,
|
||||
})) {
|
||||
let t = node.text
|
||||
|
||||
if (Path.equals(path, end.path)) {
|
||||
t = t.slice(0, end.offset)
|
||||
}
|
||||
|
||||
if (Path.equals(path, start.path)) {
|
||||
t = t.slice(start.offset)
|
||||
}
|
||||
|
||||
text += t
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
53
packages/slate/src/editor/unhang-range.ts
Normal file
53
packages/slate/src/editor/unhang-range.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Text } from '../interfaces/text'
|
||||
|
||||
export const unhangRange: EditorInterface['unhangRange'] = (
|
||||
editor,
|
||||
range,
|
||||
options = {}
|
||||
) => {
|
||||
const { voids = false } = options
|
||||
let [start, end] = Range.edges(range)
|
||||
|
||||
// PERF: exit early if we can guarantee that the range isn't hanging.
|
||||
if (
|
||||
start.offset !== 0 ||
|
||||
end.offset !== 0 ||
|
||||
Range.isCollapsed(range) ||
|
||||
Path.hasPrevious(end.path)
|
||||
) {
|
||||
return range
|
||||
}
|
||||
|
||||
const endBlock = Editor.above(editor, {
|
||||
at: end,
|
||||
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
|
||||
voids,
|
||||
})
|
||||
const blockPath = endBlock ? endBlock[1] : []
|
||||
const first = Editor.start(editor, start)
|
||||
const before = { anchor: first, focus: end }
|
||||
let skip = true
|
||||
|
||||
for (const [node, path] of Editor.nodes(editor, {
|
||||
at: before,
|
||||
match: Text.isText,
|
||||
reverse: true,
|
||||
voids,
|
||||
})) {
|
||||
if (skip) {
|
||||
skip = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (node.text !== '' || Path.isBefore(path, blockPath)) {
|
||||
end = { path, offset: node.text.length }
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return { anchor: start, focus: end }
|
||||
}
|
15
packages/slate/src/editor/without-normalizing.ts
Normal file
15
packages/slate/src/editor/without-normalizing.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
|
||||
export const withoutNormalizing: EditorInterface['withoutNormalizing'] = (
|
||||
editor,
|
||||
fn
|
||||
) => {
|
||||
const value = Editor.isNormalizing(editor)
|
||||
Editor.setNormalizing(editor, false)
|
||||
try {
|
||||
fn()
|
||||
} finally {
|
||||
Editor.setNormalizing(editor, value)
|
||||
}
|
||||
Editor.normalize(editor)
|
||||
}
|
@@ -1,16 +1,8 @@
|
||||
export * from './core'
|
||||
export * from './create-editor'
|
||||
export * from './interfaces/custom-types'
|
||||
export * from './interfaces/editor'
|
||||
export * from './interfaces/element'
|
||||
export * from './interfaces/location'
|
||||
export * from './interfaces/node'
|
||||
export * from './interfaces/operation'
|
||||
export * from './interfaces/path'
|
||||
export * from './interfaces/path-ref'
|
||||
export * from './interfaces/point'
|
||||
export * from './interfaces/point-ref'
|
||||
export * from './interfaces/range'
|
||||
export * from './interfaces/range-ref'
|
||||
export * from './interfaces/scrubber'
|
||||
export * from './interfaces/text'
|
||||
export * from './transforms'
|
||||
export * from './editor'
|
||||
export * from './interfaces'
|
||||
export * from './transforms-node'
|
||||
export * from './transforms-selection'
|
||||
export * from './transforms-text'
|
||||
export * from './types'
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { Editor, Node, Path, Descendant, ExtendedType, Ancestor } from '..'
|
||||
import { Ancestor, Descendant, Editor, ExtendedType, Node, Path } from '..'
|
||||
|
||||
/**
|
||||
* `Element` objects are a type of node in a Slate document that contain other
|
||||
@@ -14,15 +14,42 @@ export interface BaseElement {
|
||||
export type Element = ExtendedType<'Element', BaseElement>
|
||||
|
||||
export interface ElementInterface {
|
||||
/**
|
||||
* Check if a value implements the 'Ancestor' interface.
|
||||
*/
|
||||
isAncestor: (value: any) => value is Ancestor
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Element` interface.
|
||||
*/
|
||||
isElement: (value: any) => value is Element
|
||||
|
||||
/**
|
||||
* Check if a value is an array of `Element` objects.
|
||||
*/
|
||||
isElementList: (value: any) => value is Element[]
|
||||
|
||||
/**
|
||||
* Check if a set of props is a partial of Element.
|
||||
*/
|
||||
isElementProps: (props: any) => props is Partial<Element>
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Element` interface and has elementKey with selected value.
|
||||
* Default it check to `type` key value
|
||||
*/
|
||||
isElementType: <T extends Element>(
|
||||
value: any,
|
||||
elementVal: string,
|
||||
elementKey?: string
|
||||
) => value is T
|
||||
|
||||
/**
|
||||
* Check if an element matches set of properties.
|
||||
*
|
||||
* Note: this checks custom properties, and it does not ensure that any
|
||||
* children are equivalent.
|
||||
*/
|
||||
matches: (element: Element, props: Partial<Element>) => boolean
|
||||
}
|
||||
|
||||
@@ -39,40 +66,20 @@ const isElement = (value: any): value is Element => {
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Element: ElementInterface = {
|
||||
/**
|
||||
* Check if a value implements the 'Ancestor' interface.
|
||||
*/
|
||||
|
||||
isAncestor(value: any): value is Ancestor {
|
||||
return isPlainObject(value) && Node.isNodeList(value.children)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Element` interface.
|
||||
*/
|
||||
|
||||
isElement,
|
||||
/**
|
||||
* Check if a value is an array of `Element` objects.
|
||||
*/
|
||||
|
||||
isElementList(value: any): value is Element[] {
|
||||
return Array.isArray(value) && value.every(val => Element.isElement(val))
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a set of props is a partial of Element.
|
||||
*/
|
||||
|
||||
isElementProps(props: any): props is Partial<Element> {
|
||||
return (props as Partial<Element>).children !== undefined
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Element` interface and has elementKey with selected value.
|
||||
* Default it check to `type` key value
|
||||
*/
|
||||
|
||||
isElementType: <T extends Element>(
|
||||
value: any,
|
||||
elementVal: string,
|
||||
@@ -81,13 +88,6 @@ export const Element: ElementInterface = {
|
||||
return isElement(value) && value[elementKey] === elementVal
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an element matches set of properties.
|
||||
*
|
||||
* Note: this checks custom properties, and it does not ensure that any
|
||||
* children are equivalent.
|
||||
*/
|
||||
|
||||
matches(element: Element, props: Partial<Element>): boolean {
|
||||
for (const key in props) {
|
||||
if (key === 'children') {
|
||||
@@ -107,5 +107,4 @@ export const Element: ElementInterface = {
|
||||
* `ElementEntry` objects refer to an `Element` and the `Path` where it can be
|
||||
* found inside a root node.
|
||||
*/
|
||||
|
||||
export type ElementEntry = [Element, Path]
|
||||
|
14
packages/slate/src/interfaces/index.ts
Normal file
14
packages/slate/src/interfaces/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export * from './editor'
|
||||
export * from './element'
|
||||
export * from './location'
|
||||
export * from './node'
|
||||
export * from './operation'
|
||||
export * from './path-ref'
|
||||
export * from './path'
|
||||
export * from './point-ref'
|
||||
export * from './point'
|
||||
export * from './range-ref'
|
||||
export * from './range'
|
||||
export * from './scrubber'
|
||||
export * from './text'
|
||||
export * from './transforms/index'
|
@@ -12,15 +12,14 @@ import { Path, Point, Range } from '..'
|
||||
export type Location = Path | Point | Range
|
||||
|
||||
export interface LocationInterface {
|
||||
/**
|
||||
* Check if a value implements the `Location` interface.
|
||||
*/
|
||||
isLocation: (value: any) => value is Location
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Location: LocationInterface = {
|
||||
/**
|
||||
* Check if a value implements the `Location` interface.
|
||||
*/
|
||||
|
||||
isLocation(value: any): value is Location {
|
||||
return Path.isPath(value) || Point.isPoint(value) || Range.isRange(value)
|
||||
},
|
||||
@@ -34,15 +33,14 @@ export const Location: LocationInterface = {
|
||||
export type Span = [Path, Path]
|
||||
|
||||
export interface SpanInterface {
|
||||
/**
|
||||
* Check if a value implements the `Span` interface.
|
||||
*/
|
||||
isSpan: (value: any) => value is Span
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Span: SpanInterface = {
|
||||
/**
|
||||
* Check if a value implements the `Span` interface.
|
||||
*/
|
||||
|
||||
isSpan(value: any): value is Span {
|
||||
return (
|
||||
Array.isArray(value) && value.length === 2 && value.every(Path.isPath)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { produce } from 'immer'
|
||||
import { Editor, Path, Range, Text, Scrubber } from '..'
|
||||
import { Editor, Path, Range, Scrubber, Text } from '..'
|
||||
import { Element, ElementEntry } from './element'
|
||||
|
||||
/**
|
||||
@@ -51,49 +51,155 @@ export interface NodeTextsOptions {
|
||||
}
|
||||
|
||||
export interface NodeInterface {
|
||||
/**
|
||||
* Get the node at a specific path, asserting that it's an ancestor node.
|
||||
*/
|
||||
ancestor: (root: Node, path: Path) => Ancestor
|
||||
|
||||
/**
|
||||
* Return a generator of all the ancestor nodes above a specific path.
|
||||
*
|
||||
* By default the order is top-down, from highest to lowest ancestor in
|
||||
* the tree, but you can pass the `reverse: true` option to go bottom-up.
|
||||
*/
|
||||
ancestors: (
|
||||
root: Node,
|
||||
path: Path,
|
||||
options?: NodeAncestorsOptions
|
||||
) => Generator<NodeEntry<Ancestor>, void, undefined>
|
||||
|
||||
/**
|
||||
* Get the child of a node at a specific index.
|
||||
*/
|
||||
child: (root: Node, index: number) => Descendant
|
||||
|
||||
/**
|
||||
* Iterate over the children of a node at a specific path.
|
||||
*/
|
||||
children: (
|
||||
root: Node,
|
||||
path: Path,
|
||||
options?: NodeChildrenOptions
|
||||
) => Generator<NodeEntry<Descendant>, void, undefined>
|
||||
|
||||
/**
|
||||
* Get an entry for the common ancesetor node of two paths.
|
||||
*/
|
||||
common: (root: Node, path: Path, another: Path) => NodeEntry
|
||||
|
||||
/**
|
||||
* Get the node at a specific path, asserting that it's a descendant node.
|
||||
*/
|
||||
descendant: (root: Node, path: Path) => Descendant
|
||||
|
||||
/**
|
||||
* Return a generator of all the descendant node entries inside a root node.
|
||||
*/
|
||||
descendants: (
|
||||
root: Node,
|
||||
options?: NodeDescendantsOptions
|
||||
) => Generator<NodeEntry<Descendant>, void, undefined>
|
||||
|
||||
/**
|
||||
* Return a generator of all the element nodes inside a root node. Each iteration
|
||||
* will return an `ElementEntry` tuple consisting of `[Element, Path]`. If the
|
||||
* root node is an element it will be included in the iteration as well.
|
||||
*/
|
||||
elements: (
|
||||
root: Node,
|
||||
options?: NodeElementsOptions
|
||||
) => Generator<ElementEntry, void, undefined>
|
||||
|
||||
/**
|
||||
* Extract props from a Node.
|
||||
*/
|
||||
extractProps: (node: Node) => NodeProps
|
||||
|
||||
/**
|
||||
* Get the first node entry in a root node from a path.
|
||||
*/
|
||||
first: (root: Node, path: Path) => NodeEntry
|
||||
|
||||
/**
|
||||
* Get the sliced fragment represented by a range inside a root node.
|
||||
*/
|
||||
fragment: (root: Node, range: Range) => Descendant[]
|
||||
|
||||
/**
|
||||
* Get the descendant node referred to by a specific path. If the path is an
|
||||
* empty array, it refers to the root node itself.
|
||||
*/
|
||||
get: (root: Node, path: Path) => Node
|
||||
|
||||
/**
|
||||
* Check if a descendant node exists at a specific path.
|
||||
*/
|
||||
has: (root: Node, path: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Node` interface.
|
||||
*/
|
||||
isNode: (value: any) => value is Node
|
||||
|
||||
/**
|
||||
* Check if a value is a list of `Node` objects.
|
||||
*/
|
||||
isNodeList: (value: any) => value is Node[]
|
||||
|
||||
/**
|
||||
* Get the last node entry in a root node from a path.
|
||||
*/
|
||||
last: (root: Node, path: Path) => NodeEntry
|
||||
|
||||
/**
|
||||
* Get the node at a specific path, ensuring it's a leaf text node.
|
||||
*/
|
||||
leaf: (root: Node, path: Path) => Text
|
||||
|
||||
/**
|
||||
* Return a generator of the in a branch of the tree, from a specific path.
|
||||
*
|
||||
* By default the order is top-down, from highest to lowest node in the tree,
|
||||
* but you can pass the `reverse: true` option to go bottom-up.
|
||||
*/
|
||||
levels: (
|
||||
root: Node,
|
||||
path: Path,
|
||||
options?: NodeLevelsOptions
|
||||
) => Generator<NodeEntry, void, undefined>
|
||||
|
||||
/**
|
||||
* Check if a node matches a set of props.
|
||||
*/
|
||||
matches: (node: Node, props: Partial<Node>) => boolean
|
||||
|
||||
/**
|
||||
* Return a generator of all the node entries of a root node. Each entry is
|
||||
* returned as a `[Node, Path]` tuple, with the path referring to the node's
|
||||
* position inside the root node.
|
||||
*/
|
||||
nodes: (
|
||||
root: Node,
|
||||
options?: NodeNodesOptions
|
||||
) => Generator<NodeEntry, void, undefined>
|
||||
|
||||
/**
|
||||
* Get the parent of a node at a specific path.
|
||||
*/
|
||||
parent: (root: Node, path: Path) => Ancestor
|
||||
|
||||
/**
|
||||
* Get the concatenated text string of a node's content.
|
||||
*
|
||||
* Note that this will not include spaces or line breaks between block nodes.
|
||||
* It is not a user-facing string, but a string for performing offset-related
|
||||
* computations for a node.
|
||||
*/
|
||||
string: (node: Node) => string
|
||||
|
||||
/**
|
||||
* Return a generator of all leaf text nodes in a root node.
|
||||
*/
|
||||
texts: (
|
||||
root: Node,
|
||||
options?: NodeTextsOptions
|
||||
@@ -104,10 +210,6 @@ const IS_NODE_LIST_CACHE = new WeakMap<any[], boolean>()
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Node: NodeInterface = {
|
||||
/**
|
||||
* Get the node at a specific path, asserting that it's an ancestor node.
|
||||
*/
|
||||
|
||||
ancestor(root: Node, path: Path): Ancestor {
|
||||
const node = Node.get(root, path)
|
||||
|
||||
@@ -122,13 +224,6 @@ export const Node: NodeInterface = {
|
||||
return node
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a generator of all the ancestor nodes above a specific path.
|
||||
*
|
||||
* By default the order is top-down, from highest to lowest ancestor in
|
||||
* the tree, but you can pass the `reverse: true` option to go bottom-up.
|
||||
*/
|
||||
|
||||
*ancestors(
|
||||
root: Node,
|
||||
path: Path,
|
||||
@@ -141,10 +236,6 @@ export const Node: NodeInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the child of a node at a specific index.
|
||||
*/
|
||||
|
||||
child(root: Node, index: number): Descendant {
|
||||
if (Text.isText(root)) {
|
||||
throw new Error(
|
||||
@@ -165,10 +256,6 @@ export const Node: NodeInterface = {
|
||||
return c
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterate over the children of a node at a specific path.
|
||||
*/
|
||||
|
||||
*children(
|
||||
root: Node,
|
||||
path: Path,
|
||||
@@ -187,20 +274,12 @@ export const Node: NodeInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an entry for the common ancesetor node of two paths.
|
||||
*/
|
||||
|
||||
common(root: Node, path: Path, another: Path): NodeEntry {
|
||||
const p = Path.common(path, another)
|
||||
const n = Node.get(root, p)
|
||||
return [n, p]
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the node at a specific path, asserting that it's a descendant node.
|
||||
*/
|
||||
|
||||
descendant(root: Node, path: Path): Descendant {
|
||||
const node = Node.get(root, path)
|
||||
|
||||
@@ -215,10 +294,6 @@ export const Node: NodeInterface = {
|
||||
return node
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a generator of all the descendant node entries inside a root node.
|
||||
*/
|
||||
|
||||
*descendants(
|
||||
root: Node,
|
||||
options: NodeDescendantsOptions = {}
|
||||
@@ -232,12 +307,6 @@ export const Node: NodeInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a generator of all the element nodes inside a root node. Each iteration
|
||||
* will return an `ElementEntry` tuple consisting of `[Element, Path]`. If the
|
||||
* root node is an element it will be included in the iteration as well.
|
||||
*/
|
||||
|
||||
*elements(
|
||||
root: Node,
|
||||
options: NodeElementsOptions = {}
|
||||
@@ -249,10 +318,6 @@ export const Node: NodeInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Extract props from a Node.
|
||||
*/
|
||||
|
||||
extractProps(node: Node): NodeProps {
|
||||
if (Element.isAncestor(node)) {
|
||||
const { children, ...properties } = node
|
||||
@@ -265,10 +330,6 @@ export const Node: NodeInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the first node entry in a root node from a path.
|
||||
*/
|
||||
|
||||
first(root: Node, path: Path): NodeEntry {
|
||||
const p = path.slice()
|
||||
let n = Node.get(root, p)
|
||||
@@ -285,10 +346,6 @@ export const Node: NodeInterface = {
|
||||
return [n, p]
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the sliced fragment represented by a range inside a root node.
|
||||
*/
|
||||
|
||||
fragment(root: Node, range: Range): Descendant[] {
|
||||
if (Text.isText(root)) {
|
||||
throw new Error(
|
||||
@@ -331,11 +388,6 @@ export const Node: NodeInterface = {
|
||||
return newRoot.children
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the descendant node referred to by a specific path. If the path is an
|
||||
* empty array, it refers to the root node itself.
|
||||
*/
|
||||
|
||||
get(root: Node, path: Path): Node {
|
||||
let node = root
|
||||
|
||||
@@ -356,10 +408,6 @@ export const Node: NodeInterface = {
|
||||
return node
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a descendant node exists at a specific path.
|
||||
*/
|
||||
|
||||
has(root: Node, path: Path): boolean {
|
||||
let node = root
|
||||
|
||||
@@ -376,20 +424,12 @@ export const Node: NodeInterface = {
|
||||
return true
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Node` interface.
|
||||
*/
|
||||
|
||||
isNode(value: any): value is Node {
|
||||
return (
|
||||
Text.isText(value) || Element.isElement(value) || Editor.isEditor(value)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a list of `Node` objects.
|
||||
*/
|
||||
|
||||
isNodeList(value: any): value is Node[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return false
|
||||
@@ -403,10 +443,6 @@ export const Node: NodeInterface = {
|
||||
return isNodeList
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the last node entry in a root node from a path.
|
||||
*/
|
||||
|
||||
last(root: Node, path: Path): NodeEntry {
|
||||
const p = path.slice()
|
||||
let n = Node.get(root, p)
|
||||
@@ -424,10 +460,6 @@ export const Node: NodeInterface = {
|
||||
return [n, p]
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the node at a specific path, ensuring it's a leaf text node.
|
||||
*/
|
||||
|
||||
leaf(root: Node, path: Path): Text {
|
||||
const node = Node.get(root, path)
|
||||
|
||||
@@ -442,13 +474,6 @@ export const Node: NodeInterface = {
|
||||
return node
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a generator of the in a branch of the tree, from a specific path.
|
||||
*
|
||||
* By default the order is top-down, from highest to lowest node in the tree,
|
||||
* but you can pass the `reverse: true` option to go bottom-up.
|
||||
*/
|
||||
|
||||
*levels(
|
||||
root: Node,
|
||||
path: Path,
|
||||
@@ -460,10 +485,6 @@ export const Node: NodeInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a node matches a set of props.
|
||||
*/
|
||||
|
||||
matches(node: Node, props: Partial<Node>): boolean {
|
||||
return (
|
||||
(Element.isElement(node) &&
|
||||
@@ -475,12 +496,6 @@ export const Node: NodeInterface = {
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a generator of all the node entries of a root node. Each entry is
|
||||
* returned as a `[Node, Path]` tuple, with the path referring to the node's
|
||||
* position inside the root node.
|
||||
*/
|
||||
|
||||
*nodes(
|
||||
root: Node,
|
||||
options: NodeNodesOptions = {}
|
||||
@@ -550,10 +565,6 @@ export const Node: NodeInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the parent of a node at a specific path.
|
||||
*/
|
||||
|
||||
parent(root: Node, path: Path): Ancestor {
|
||||
const parentPath = Path.parent(path)
|
||||
const p = Node.get(root, parentPath)
|
||||
@@ -567,14 +578,6 @@ export const Node: NodeInterface = {
|
||||
return p
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the concatenated text string of a node's content.
|
||||
*
|
||||
* Note that this will not include spaces or line breaks between block nodes.
|
||||
* It is not a user-facing string, but a string for performing offset-related
|
||||
* computations for a node.
|
||||
*/
|
||||
|
||||
string(node: Node): string {
|
||||
if (Text.isText(node)) {
|
||||
return node.text
|
||||
@@ -583,10 +586,6 @@ export const Node: NodeInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a generator of all leaf text nodes in a root node.
|
||||
*/
|
||||
|
||||
*texts(
|
||||
root: Node,
|
||||
options: NodeTextsOptions = {}
|
||||
|
@@ -139,28 +139,44 @@ export type BaseOperation = NodeOperation | SelectionOperation | TextOperation
|
||||
export type Operation = ExtendedType<'Operation', BaseOperation>
|
||||
|
||||
export interface OperationInterface {
|
||||
/**
|
||||
* Check if a value is a `NodeOperation` object.
|
||||
*/
|
||||
isNodeOperation: (value: any) => value is NodeOperation
|
||||
|
||||
/**
|
||||
* Check if a value is an `Operation` object.
|
||||
*/
|
||||
isOperation: (value: any) => value is Operation
|
||||
|
||||
/**
|
||||
* Check if a value is a list of `Operation` objects.
|
||||
*/
|
||||
isOperationList: (value: any) => value is Operation[]
|
||||
|
||||
/**
|
||||
* Check if a value is a `SelectionOperation` object.
|
||||
*/
|
||||
isSelectionOperation: (value: any) => value is SelectionOperation
|
||||
|
||||
/**
|
||||
* Check if a value is a `TextOperation` object.
|
||||
*/
|
||||
isTextOperation: (value: any) => value is TextOperation
|
||||
|
||||
/**
|
||||
* Invert an operation, returning a new operation that will exactly undo the
|
||||
* original when applied.
|
||||
*/
|
||||
inverse: (op: Operation) => Operation
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Operation: OperationInterface = {
|
||||
/**
|
||||
* Check if a value is a `NodeOperation` object.
|
||||
*/
|
||||
|
||||
isNodeOperation(value: any): value is NodeOperation {
|
||||
return Operation.isOperation(value) && value.type.endsWith('_node')
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is an `Operation` object.
|
||||
*/
|
||||
|
||||
isOperation(value: any): value is Operation {
|
||||
if (!isPlainObject(value)) {
|
||||
return false
|
||||
@@ -215,37 +231,20 @@ export const Operation: OperationInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a list of `Operation` objects.
|
||||
*/
|
||||
|
||||
isOperationList(value: any): value is Operation[] {
|
||||
return (
|
||||
Array.isArray(value) && value.every(val => Operation.isOperation(val))
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a `SelectionOperation` object.
|
||||
*/
|
||||
|
||||
isSelectionOperation(value: any): value is SelectionOperation {
|
||||
return Operation.isOperation(value) && value.type.endsWith('_selection')
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a `TextOperation` object.
|
||||
*/
|
||||
|
||||
isTextOperation(value: any): value is TextOperation {
|
||||
return Operation.isOperation(value) && value.type.endsWith('_text')
|
||||
},
|
||||
|
||||
/**
|
||||
* Invert an operation, returning a new operation that will exactly undo the
|
||||
* original when applied.
|
||||
*/
|
||||
|
||||
inverse(op: Operation): Operation {
|
||||
switch (op.type) {
|
||||
case 'insert_node': {
|
||||
|
@@ -13,15 +13,14 @@ export interface PathRef {
|
||||
}
|
||||
|
||||
export interface PathRefInterface {
|
||||
/**
|
||||
* Transform the path ref's current value by an operation.
|
||||
*/
|
||||
transform: (ref: PathRef, op: Operation) => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const PathRef: PathRefInterface = {
|
||||
/**
|
||||
* Transform the path ref's current value by an operation.
|
||||
*/
|
||||
|
||||
transform(ref: PathRef, op: Operation): void {
|
||||
const { current, affinity } = ref
|
||||
|
||||
|
@@ -2,11 +2,11 @@ import {
|
||||
InsertNodeOperation,
|
||||
MergeNodeOperation,
|
||||
MoveNodeOperation,
|
||||
Operation,
|
||||
RemoveNodeOperation,
|
||||
SplitNodeOperation,
|
||||
Operation,
|
||||
} from '..'
|
||||
import { TextDirection } from './types'
|
||||
import { TextDirection } from '../types/types'
|
||||
|
||||
/**
|
||||
* `Path` arrays are a list of indexes that describe a node's exact position in
|
||||
@@ -29,25 +29,120 @@ export interface PathTransformOptions {
|
||||
}
|
||||
|
||||
export interface PathInterface {
|
||||
/**
|
||||
* Get a list of ancestor paths for a given path.
|
||||
*
|
||||
* The paths are sorted from shallowest to deepest ancestor. However, if the
|
||||
* `reverse: true` option is passed, they are reversed.
|
||||
*/
|
||||
ancestors: (path: Path, options?: PathAncestorsOptions) => Path[]
|
||||
|
||||
/**
|
||||
* Get the common ancestor path of two paths.
|
||||
*/
|
||||
common: (path: Path, another: Path) => Path
|
||||
|
||||
/**
|
||||
* Compare a path to another, returning an integer indicating whether the path
|
||||
* was before, at, or after the other.
|
||||
*
|
||||
* Note: Two paths of unequal length can still receive a `0` result if one is
|
||||
* directly above or below the other. If you want exact matching, use
|
||||
* [[Path.equals]] instead.
|
||||
*/
|
||||
compare: (path: Path, another: Path) => -1 | 0 | 1
|
||||
|
||||
/**
|
||||
* Check if a path ends after one of the indexes in another.
|
||||
*/
|
||||
endsAfter: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a path ends at one of the indexes in another.
|
||||
*/
|
||||
endsAt: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a path ends before one of the indexes in another.
|
||||
*/
|
||||
endsBefore: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a path is exactly equal to another.
|
||||
*/
|
||||
equals: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if the path of previous sibling node exists
|
||||
*/
|
||||
hasPrevious: (path: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a path is after another.
|
||||
*/
|
||||
isAfter: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a path is an ancestor of another.
|
||||
*/
|
||||
isAncestor: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a path is before another.
|
||||
*/
|
||||
isBefore: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a path is a child of another.
|
||||
*/
|
||||
isChild: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a path is equal to or an ancestor of another.
|
||||
*/
|
||||
isCommon: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a path is a descendant of another.
|
||||
*/
|
||||
isDescendant: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check if a path is the parent of another.
|
||||
*/
|
||||
isParent: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Check is a value implements the `Path` interface.
|
||||
*/
|
||||
isPath: (value: any) => value is Path
|
||||
|
||||
/**
|
||||
* Check if a path is a sibling of another.
|
||||
*/
|
||||
isSibling: (path: Path, another: Path) => boolean
|
||||
|
||||
/**
|
||||
* Get a list of paths at every level down to a path. Note: this is the same
|
||||
* as `Path.ancestors`, but including the path itself.
|
||||
*
|
||||
* The paths are sorted from shallowest to deepest. However, if the `reverse:
|
||||
* true` option is passed, they are reversed.
|
||||
*/
|
||||
levels: (path: Path, options?: PathLevelsOptions) => Path[]
|
||||
|
||||
/**
|
||||
* Given a path, get the path to the next sibling node.
|
||||
*/
|
||||
next: (path: Path) => Path
|
||||
|
||||
/**
|
||||
* Returns whether this operation can affect paths or not. Used as an
|
||||
* optimization when updating dirty paths during normalization
|
||||
*
|
||||
* NOTE: This *must* be kept in sync with the implementation of 'transform'
|
||||
* below
|
||||
*/
|
||||
operationCanTransformPath: (
|
||||
operation: Operation
|
||||
) => operation is
|
||||
@@ -56,9 +151,25 @@ export interface PathInterface {
|
||||
| MergeNodeOperation
|
||||
| SplitNodeOperation
|
||||
| MoveNodeOperation
|
||||
|
||||
/**
|
||||
* Given a path, return a new path referring to the parent node above it.
|
||||
*/
|
||||
parent: (path: Path) => Path
|
||||
|
||||
/**
|
||||
* Given a path, get the path to the previous sibling node.
|
||||
*/
|
||||
previous: (path: Path) => Path
|
||||
|
||||
/**
|
||||
* Get a path relative to an ancestor.
|
||||
*/
|
||||
relative: (path: Path, ancestor: Path) => Path
|
||||
|
||||
/**
|
||||
* Transform a path by an operation.
|
||||
*/
|
||||
transform: (
|
||||
path: Path,
|
||||
operation: Operation,
|
||||
@@ -68,13 +179,6 @@ export interface PathInterface {
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Path: PathInterface = {
|
||||
/**
|
||||
* Get a list of ancestor paths for a given path.
|
||||
*
|
||||
* The paths are sorted from shallowest to deepest ancestor. However, if the
|
||||
* `reverse: true` option is passed, they are reversed.
|
||||
*/
|
||||
|
||||
ancestors(path: Path, options: PathAncestorsOptions = {}): Path[] {
|
||||
const { reverse = false } = options
|
||||
let paths = Path.levels(path, options)
|
||||
@@ -88,10 +192,6 @@ export const Path: PathInterface = {
|
||||
return paths
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the common ancestor path of two paths.
|
||||
*/
|
||||
|
||||
common(path: Path, another: Path): Path {
|
||||
const common: Path = []
|
||||
|
||||
@@ -109,15 +209,6 @@ export const Path: PathInterface = {
|
||||
return common
|
||||
},
|
||||
|
||||
/**
|
||||
* Compare a path to another, returning an integer indicating whether the path
|
||||
* was before, at, or after the other.
|
||||
*
|
||||
* Note: Two paths of unequal length can still receive a `0` result if one is
|
||||
* directly above or below the other. If you want exact matching, use
|
||||
* [[Path.equals]] instead.
|
||||
*/
|
||||
|
||||
compare(path: Path, another: Path): -1 | 0 | 1 {
|
||||
const min = Math.min(path.length, another.length)
|
||||
|
||||
@@ -129,10 +220,6 @@ export const Path: PathInterface = {
|
||||
return 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path ends after one of the indexes in another.
|
||||
*/
|
||||
|
||||
endsAfter(path: Path, another: Path): boolean {
|
||||
const i = path.length - 1
|
||||
const as = path.slice(0, i)
|
||||
@@ -142,10 +229,6 @@ export const Path: PathInterface = {
|
||||
return Path.equals(as, bs) && av > bv
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path ends at one of the indexes in another.
|
||||
*/
|
||||
|
||||
endsAt(path: Path, another: Path): boolean {
|
||||
const i = path.length
|
||||
const as = path.slice(0, i)
|
||||
@@ -153,10 +236,6 @@ export const Path: PathInterface = {
|
||||
return Path.equals(as, bs)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path ends before one of the indexes in another.
|
||||
*/
|
||||
|
||||
endsBefore(path: Path, another: Path): boolean {
|
||||
const i = path.length - 1
|
||||
const as = path.slice(0, i)
|
||||
@@ -166,88 +245,48 @@ export const Path: PathInterface = {
|
||||
return Path.equals(as, bs) && av < bv
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path is exactly equal to another.
|
||||
*/
|
||||
|
||||
equals(path: Path, another: Path): boolean {
|
||||
return (
|
||||
path.length === another.length && path.every((n, i) => n === another[i])
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the path of previous sibling node exists
|
||||
*/
|
||||
|
||||
hasPrevious(path: Path): boolean {
|
||||
return path[path.length - 1] > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path is after another.
|
||||
*/
|
||||
|
||||
isAfter(path: Path, another: Path): boolean {
|
||||
return Path.compare(path, another) === 1
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path is an ancestor of another.
|
||||
*/
|
||||
|
||||
isAncestor(path: Path, another: Path): boolean {
|
||||
return path.length < another.length && Path.compare(path, another) === 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path is before another.
|
||||
*/
|
||||
|
||||
isBefore(path: Path, another: Path): boolean {
|
||||
return Path.compare(path, another) === -1
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path is a child of another.
|
||||
*/
|
||||
|
||||
isChild(path: Path, another: Path): boolean {
|
||||
return (
|
||||
path.length === another.length + 1 && Path.compare(path, another) === 0
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path is equal to or an ancestor of another.
|
||||
*/
|
||||
|
||||
isCommon(path: Path, another: Path): boolean {
|
||||
return path.length <= another.length && Path.compare(path, another) === 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path is a descendant of another.
|
||||
*/
|
||||
|
||||
isDescendant(path: Path, another: Path): boolean {
|
||||
return path.length > another.length && Path.compare(path, another) === 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path is the parent of another.
|
||||
*/
|
||||
|
||||
isParent(path: Path, another: Path): boolean {
|
||||
return (
|
||||
path.length + 1 === another.length && Path.compare(path, another) === 0
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check is a value implements the `Path` interface.
|
||||
*/
|
||||
|
||||
isPath(value: any): value is Path {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
@@ -255,10 +294,6 @@ export const Path: PathInterface = {
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a path is a sibling of another.
|
||||
*/
|
||||
|
||||
isSibling(path: Path, another: Path): boolean {
|
||||
if (path.length !== another.length) {
|
||||
return false
|
||||
@@ -271,14 +306,6 @@ export const Path: PathInterface = {
|
||||
return al !== bl && Path.equals(as, bs)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a list of paths at every level down to a path. Note: this is the same
|
||||
* as `Path.ancestors`, but including the path itself.
|
||||
*
|
||||
* The paths are sorted from shallowest to deepest. However, if the `reverse:
|
||||
* true` option is passed, they are reversed.
|
||||
*/
|
||||
|
||||
levels(path: Path, options: PathLevelsOptions = {}): Path[] {
|
||||
const { reverse = false } = options
|
||||
const list: Path[] = []
|
||||
@@ -294,10 +321,6 @@ export const Path: PathInterface = {
|
||||
return list
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a path, get the path to the next sibling node.
|
||||
*/
|
||||
|
||||
next(path: Path): Path {
|
||||
if (path.length === 0) {
|
||||
throw new Error(
|
||||
@@ -309,13 +332,6 @@ export const Path: PathInterface = {
|
||||
return path.slice(0, -1).concat(last + 1)
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether this operation can affect paths or not. Used as an
|
||||
* optimization when updating dirty paths during normalization
|
||||
*
|
||||
* NOTE: This *must* be kept in sync with the implementation of 'transform'
|
||||
* below
|
||||
*/
|
||||
operationCanTransformPath(
|
||||
operation: Operation
|
||||
): operation is
|
||||
@@ -336,10 +352,6 @@ export const Path: PathInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a path, return a new path referring to the parent node above it.
|
||||
*/
|
||||
|
||||
parent(path: Path): Path {
|
||||
if (path.length === 0) {
|
||||
throw new Error(`Cannot get the parent path of the root path [${path}].`)
|
||||
@@ -348,10 +360,6 @@ export const Path: PathInterface = {
|
||||
return path.slice(0, -1)
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a path, get the path to the previous sibling node.
|
||||
*/
|
||||
|
||||
previous(path: Path): Path {
|
||||
if (path.length === 0) {
|
||||
throw new Error(
|
||||
@@ -370,10 +378,6 @@ export const Path: PathInterface = {
|
||||
return path.slice(0, -1).concat(last - 1)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a path relative to an ancestor.
|
||||
*/
|
||||
|
||||
relative(path: Path, ancestor: Path): Path {
|
||||
if (!Path.isAncestor(ancestor, path) && !Path.equals(path, ancestor)) {
|
||||
throw new Error(
|
||||
@@ -384,10 +388,6 @@ export const Path: PathInterface = {
|
||||
return path.slice(ancestor.length)
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform a path by an operation.
|
||||
*/
|
||||
|
||||
transform(
|
||||
path: Path | null,
|
||||
operation: Operation,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Operation, Point } from '..'
|
||||
import { TextDirection } from './types'
|
||||
import { TextDirection } from '../types/types'
|
||||
|
||||
/**
|
||||
* `PointRef` objects keep a specific point in a document synced over time as new
|
||||
@@ -14,15 +14,14 @@ export interface PointRef {
|
||||
}
|
||||
|
||||
export interface PointRefInterface {
|
||||
/**
|
||||
* Transform the point ref's current value by an operation.
|
||||
*/
|
||||
transform: (ref: PointRef, op: Operation) => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const PointRef: PointRefInterface = {
|
||||
/**
|
||||
* Transform the point ref's current value by an operation.
|
||||
*/
|
||||
|
||||
transform(ref: PointRef, op: Operation): void {
|
||||
const { current, affinity } = ref
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { produce } from 'immer'
|
||||
import { ExtendedType, Operation, Path } from '..'
|
||||
import { TextDirection } from './types'
|
||||
import { TextDirection } from '../types/types'
|
||||
|
||||
/**
|
||||
* `Point` objects refer to a specific location in a text node in a Slate
|
||||
@@ -22,11 +22,35 @@ export interface PointTransformOptions {
|
||||
}
|
||||
|
||||
export interface PointInterface {
|
||||
/**
|
||||
* Compare a point to another, returning an integer indicating whether the
|
||||
* point was before, at, or after the other.
|
||||
*/
|
||||
compare: (point: Point, another: Point) => -1 | 0 | 1
|
||||
|
||||
/**
|
||||
* Check if a point is after another.
|
||||
*/
|
||||
isAfter: (point: Point, another: Point) => boolean
|
||||
|
||||
/**
|
||||
* Check if a point is before another.
|
||||
*/
|
||||
isBefore: (point: Point, another: Point) => boolean
|
||||
|
||||
/**
|
||||
* Check if a point is exactly equal to another.
|
||||
*/
|
||||
equals: (point: Point, another: Point) => boolean
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Point` interface.
|
||||
*/
|
||||
isPoint: (value: any) => value is Point
|
||||
|
||||
/**
|
||||
* Transform a point by an operation.
|
||||
*/
|
||||
transform: (
|
||||
point: Point,
|
||||
op: Operation,
|
||||
@@ -36,11 +60,6 @@ export interface PointInterface {
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Point: PointInterface = {
|
||||
/**
|
||||
* Compare a point to another, returning an integer indicating whether the
|
||||
* point was before, at, or after the other.
|
||||
*/
|
||||
|
||||
compare(point: Point, another: Point): -1 | 0 | 1 {
|
||||
const result = Path.compare(point.path, another.path)
|
||||
|
||||
@@ -53,26 +72,14 @@ export const Point: PointInterface = {
|
||||
return result
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a point is after another.
|
||||
*/
|
||||
|
||||
isAfter(point: Point, another: Point): boolean {
|
||||
return Point.compare(point, another) === 1
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a point is before another.
|
||||
*/
|
||||
|
||||
isBefore(point: Point, another: Point): boolean {
|
||||
return Point.compare(point, another) === -1
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a point is exactly equal to another.
|
||||
*/
|
||||
|
||||
equals(point: Point, another: Point): boolean {
|
||||
// PERF: ensure the offsets are equal first since they are cheaper to check.
|
||||
return (
|
||||
@@ -80,10 +87,6 @@ export const Point: PointInterface = {
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Point` interface.
|
||||
*/
|
||||
|
||||
isPoint(value: any): value is Point {
|
||||
return (
|
||||
isPlainObject(value) &&
|
||||
@@ -92,10 +95,6 @@ export const Point: PointInterface = {
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform a point by an operation.
|
||||
*/
|
||||
|
||||
transform(
|
||||
point: Point | null,
|
||||
op: Operation,
|
||||
|
@@ -13,15 +13,14 @@ export interface RangeRef {
|
||||
}
|
||||
|
||||
export interface RangeRefInterface {
|
||||
/**
|
||||
* Transform the range ref's current value by an operation.
|
||||
*/
|
||||
transform: (ref: RangeRef, op: Operation) => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const RangeRef: RangeRefInterface = {
|
||||
/**
|
||||
* Transform the range ref's current value by an operation.
|
||||
*/
|
||||
|
||||
transform(ref: RangeRef, op: Operation): void {
|
||||
const { current, affinity } = ref
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { produce } from 'immer'
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { ExtendedType, Operation, Path, Point, PointEntry } from '..'
|
||||
import { RangeDirection } from './types'
|
||||
import { RangeDirection } from '../types/types'
|
||||
|
||||
/**
|
||||
* `Range` objects are a set of points that refer to a specific span of a Slate
|
||||
@@ -25,18 +25,76 @@ export interface RangeTransformOptions {
|
||||
}
|
||||
|
||||
export interface RangeInterface {
|
||||
/**
|
||||
* Get the start and end points of a range, in the order in which they appear
|
||||
* in the document.
|
||||
*/
|
||||
edges: (range: Range, options?: RangeEdgesOptions) => [Point, Point]
|
||||
|
||||
/**
|
||||
* Get the end point of a range.
|
||||
*/
|
||||
end: (range: Range) => Point
|
||||
|
||||
/**
|
||||
* Check if a range is exactly equal to another.
|
||||
*/
|
||||
equals: (range: Range, another: Range) => boolean
|
||||
|
||||
/**
|
||||
* Check if a range includes a path, a point or part of another range.
|
||||
*/
|
||||
includes: (range: Range, target: Path | Point | Range) => boolean
|
||||
|
||||
/**
|
||||
* Get the intersection of a range with another.
|
||||
*/
|
||||
intersection: (range: Range, another: Range) => Range | null
|
||||
|
||||
/**
|
||||
* Check if a range is backward, meaning that its anchor point appears in the
|
||||
* document _after_ its focus point.
|
||||
*/
|
||||
isBackward: (range: Range) => boolean
|
||||
|
||||
/**
|
||||
* Check if a range is collapsed, meaning that both its anchor and focus
|
||||
* points refer to the exact same position in the document.
|
||||
*/
|
||||
isCollapsed: (range: Range) => boolean
|
||||
|
||||
/**
|
||||
* Check if a range is expanded.
|
||||
*
|
||||
* This is the opposite of [[Range.isCollapsed]] and is provided for legibility.
|
||||
*/
|
||||
isExpanded: (range: Range) => boolean
|
||||
|
||||
/**
|
||||
* Check if a range is forward.
|
||||
*
|
||||
* This is the opposite of [[Range.isBackward]] and is provided for legibility.
|
||||
*/
|
||||
isForward: (range: Range) => boolean
|
||||
|
||||
/**
|
||||
* Check if a value implements the [[Range]] interface.
|
||||
*/
|
||||
isRange: (value: any) => value is Range
|
||||
|
||||
/**
|
||||
* Iterate through all of the point entries in a range.
|
||||
*/
|
||||
points: (range: Range) => Generator<PointEntry, void, undefined>
|
||||
|
||||
/**
|
||||
* Get the start point of a range.
|
||||
*/
|
||||
start: (range: Range) => Point
|
||||
|
||||
/**
|
||||
* Transform a range by an operation.
|
||||
*/
|
||||
transform: (
|
||||
range: Range,
|
||||
op: Operation,
|
||||
@@ -46,11 +104,6 @@ export interface RangeInterface {
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Range: RangeInterface = {
|
||||
/**
|
||||
* Get the start and end points of a range, in the order in which they appear
|
||||
* in the document.
|
||||
*/
|
||||
|
||||
edges(range: Range, options: RangeEdgesOptions = {}): [Point, Point] {
|
||||
const { reverse = false } = options
|
||||
const { anchor, focus } = range
|
||||
@@ -59,19 +112,11 @@ export const Range: RangeInterface = {
|
||||
: [focus, anchor]
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the end point of a range.
|
||||
*/
|
||||
|
||||
end(range: Range): Point {
|
||||
const [, end] = Range.edges(range)
|
||||
return end
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a range is exactly equal to another.
|
||||
*/
|
||||
|
||||
equals(range: Range, another: Range): boolean {
|
||||
return (
|
||||
Point.equals(range.anchor, another.anchor) &&
|
||||
@@ -79,10 +124,6 @@ export const Range: RangeInterface = {
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a range includes a path, a point or part of another range.
|
||||
*/
|
||||
|
||||
includes(range: Range, target: Path | Point | Range): boolean {
|
||||
if (Range.isRange(target)) {
|
||||
if (
|
||||
@@ -112,10 +153,6 @@ export const Range: RangeInterface = {
|
||||
return isAfterStart && isBeforeEnd
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the intersection of a range with another.
|
||||
*/
|
||||
|
||||
intersection(range: Range, another: Range): Range | null {
|
||||
const { anchor, focus, ...rest } = range
|
||||
const [s1, e1] = Range.edges(range)
|
||||
@@ -130,50 +167,24 @@ export const Range: RangeInterface = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a range is backward, meaning that its anchor point appears in the
|
||||
* document _after_ its focus point.
|
||||
*/
|
||||
|
||||
isBackward(range: Range): boolean {
|
||||
const { anchor, focus } = range
|
||||
return Point.isAfter(anchor, focus)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a range is collapsed, meaning that both its anchor and focus
|
||||
* points refer to the exact same position in the document.
|
||||
*/
|
||||
|
||||
isCollapsed(range: Range): boolean {
|
||||
const { anchor, focus } = range
|
||||
return Point.equals(anchor, focus)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a range is expanded.
|
||||
*
|
||||
* This is the opposite of [[Range.isCollapsed]] and is provided for legibility.
|
||||
*/
|
||||
|
||||
isExpanded(range: Range): boolean {
|
||||
return !Range.isCollapsed(range)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a range is forward.
|
||||
*
|
||||
* This is the opposite of [[Range.isBackward]] and is provided for legibility.
|
||||
*/
|
||||
|
||||
isForward(range: Range): boolean {
|
||||
return !Range.isBackward(range)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value implements the [[Range]] interface.
|
||||
*/
|
||||
|
||||
isRange(value: any): value is Range {
|
||||
return (
|
||||
isPlainObject(value) &&
|
||||
@@ -182,28 +193,16 @@ export const Range: RangeInterface = {
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterate through all of the point entries in a range.
|
||||
*/
|
||||
|
||||
*points(range: Range): Generator<PointEntry, void, undefined> {
|
||||
yield [range.anchor, 'anchor']
|
||||
yield [range.focus, 'focus']
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the start point of a range.
|
||||
*/
|
||||
|
||||
start(range: Range): Point {
|
||||
const [start] = Range.edges(range)
|
||||
return start
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform a range by an operation.
|
||||
*/
|
||||
|
||||
transform(
|
||||
range: Range | null,
|
||||
op: Operation,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { Range } from '..'
|
||||
import { ExtendedType } from './custom-types'
|
||||
import { ExtendedType } from '../types/custom-types'
|
||||
import { isDeepEqual } from '../utils/deep-equal'
|
||||
|
||||
/**
|
||||
@@ -20,22 +20,45 @@ export interface TextEqualsOptions {
|
||||
}
|
||||
|
||||
export interface TextInterface {
|
||||
equals: (text: Text, another: Text, options?: TextEqualsOptions) => boolean
|
||||
isText: (value: any) => value is Text
|
||||
isTextList: (value: any) => value is Text[]
|
||||
isTextProps: (props: any) => props is Partial<Text>
|
||||
matches: (text: Text, props: Partial<Text>) => boolean
|
||||
decorations: (node: Text, decorations: Range[]) => Text[]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Text: TextInterface = {
|
||||
/**
|
||||
* Check if two text nodes are equal.
|
||||
*
|
||||
* When loose is set, the text is not compared. This is
|
||||
* used to check whether sibling text nodes can be merged.
|
||||
*/
|
||||
equals: (text: Text, another: Text, options?: TextEqualsOptions) => boolean
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Text` interface.
|
||||
*/
|
||||
isText: (value: any) => value is Text
|
||||
|
||||
/**
|
||||
* Check if a value is a list of `Text` objects.
|
||||
*/
|
||||
isTextList: (value: any) => value is Text[]
|
||||
|
||||
/**
|
||||
* Check if some props are a partial of Text.
|
||||
*/
|
||||
isTextProps: (props: any) => props is Partial<Text>
|
||||
|
||||
/**
|
||||
* Check if an text matches set of properties.
|
||||
*
|
||||
* Note: this is for matching custom properties, and it does not ensure that
|
||||
* the `text` property are two nodes equal.
|
||||
*/
|
||||
matches: (text: Text, props: Partial<Text>) => boolean
|
||||
|
||||
/**
|
||||
* Get the leaves for a text node given decorations.
|
||||
*/
|
||||
decorations: (node: Text, decorations: Range[]) => Text[]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Text: TextInterface = {
|
||||
equals(text: Text, another: Text, options: TextEqualsOptions = {}): boolean {
|
||||
const { loose = false } = options
|
||||
|
||||
@@ -51,37 +74,18 @@ export const Text: TextInterface = {
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Text` interface.
|
||||
*/
|
||||
|
||||
isText(value: any): value is Text {
|
||||
return isPlainObject(value) && typeof value.text === 'string'
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a value is a list of `Text` objects.
|
||||
*/
|
||||
|
||||
isTextList(value: any): value is Text[] {
|
||||
return Array.isArray(value) && value.every(val => Text.isText(val))
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if some props are a partial of Text.
|
||||
*/
|
||||
|
||||
isTextProps(props: any): props is Partial<Text> {
|
||||
return (props as Partial<Text>).text !== undefined
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an text matches set of properties.
|
||||
*
|
||||
* Note: this is for matching custom properties, and it does not ensure that
|
||||
* the `text` property are two nodes equal.
|
||||
*/
|
||||
|
||||
matches(text: Text, props: Partial<Text>): boolean {
|
||||
for (const key in props) {
|
||||
if (key === 'text') {
|
||||
@@ -96,10 +100,6 @@ export const Text: TextInterface = {
|
||||
return true
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the leaves for a text node given decorations.
|
||||
*/
|
||||
|
||||
decorations(node: Text, decorations: Range[]): Text[] {
|
||||
let leaves: Text[] = [{ ...node }]
|
||||
|
||||
|
@@ -13,9 +13,12 @@ import {
|
||||
Scrubber,
|
||||
Selection,
|
||||
Text,
|
||||
} from '..'
|
||||
} from '../../index'
|
||||
|
||||
export interface GeneralTransforms {
|
||||
/**
|
||||
* Transform the editor by an operation.
|
||||
*/
|
||||
transform: (editor: Editor, op: Operation) => void
|
||||
}
|
||||
|
||||
@@ -315,10 +318,6 @@ const applyToDraft = (editor: Editor, selection: Selection, op: Operation) => {
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const GeneralTransforms: GeneralTransforms = {
|
||||
/**
|
||||
* Transform the editor by an operation.
|
||||
*/
|
||||
|
||||
transform(editor: Editor, op: Operation): void {
|
||||
editor.children = createDraft(editor.children)
|
||||
let selection = editor.selection && createDraft(editor.selection)
|
192
packages/slate/src/interfaces/transforms/node.ts
Normal file
192
packages/slate/src/interfaces/transforms/node.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Editor, Element, Location, Node, Path } from '../../index'
|
||||
import { NodeMatch, PropsCompare, PropsMerge } from '../editor'
|
||||
import { MaximizeMode, RangeMode } from '../../types/types'
|
||||
|
||||
export interface NodeTransforms {
|
||||
/**
|
||||
* Insert nodes at a specific location in the Editor.
|
||||
*/
|
||||
insertNodes: <T extends Node>(
|
||||
editor: Editor,
|
||||
nodes: Node | Node[],
|
||||
options?: {
|
||||
at?: Location
|
||||
match?: NodeMatch<T>
|
||||
mode?: RangeMode
|
||||
hanging?: boolean
|
||||
select?: boolean
|
||||
voids?: boolean
|
||||
}
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Lift nodes at a specific location upwards in the document tree, splitting
|
||||
* their parent in two if necessary.
|
||||
*/
|
||||
liftNodes: <T extends Node>(
|
||||
editor: Editor,
|
||||
options?: {
|
||||
at?: Location
|
||||
match?: NodeMatch<T>
|
||||
mode?: MaximizeMode
|
||||
voids?: boolean
|
||||
}
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Merge a node at a location with the previous node of the same depth,
|
||||
* removing any empty containing nodes after the merge if necessary.
|
||||
*/
|
||||
mergeNodes: <T extends Node>(
|
||||
editor: Editor,
|
||||
options?: {
|
||||
at?: Location
|
||||
match?: NodeMatch<T>
|
||||
mode?: RangeMode
|
||||
hanging?: boolean
|
||||
voids?: boolean
|
||||
}
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Move the nodes at a location to a new location.
|
||||
*/
|
||||
moveNodes: <T extends Node>(
|
||||
editor: Editor,
|
||||
options: {
|
||||
at?: Location
|
||||
match?: NodeMatch<T>
|
||||
mode?: MaximizeMode
|
||||
to: Path
|
||||
voids?: boolean
|
||||
}
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Remove the nodes at a specific location in the document.
|
||||
*/
|
||||
removeNodes: <T extends Node>(
|
||||
editor: Editor,
|
||||
options?: {
|
||||
at?: Location
|
||||
match?: NodeMatch<T>
|
||||
mode?: RangeMode
|
||||
hanging?: boolean
|
||||
voids?: boolean
|
||||
}
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Set new properties on the nodes at a location.
|
||||
*/
|
||||
setNodes: <T extends Node>(
|
||||
editor: Editor,
|
||||
props: Partial<T>,
|
||||
options?: {
|
||||
at?: Location
|
||||
match?: NodeMatch<T>
|
||||
mode?: MaximizeMode
|
||||
hanging?: boolean
|
||||
split?: boolean
|
||||
voids?: boolean
|
||||
compare?: PropsCompare
|
||||
merge?: PropsMerge
|
||||
}
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Split the nodes at a specific location.
|
||||
*/
|
||||
splitNodes: <T extends Node>(
|
||||
editor: Editor,
|
||||
options?: {
|
||||
at?: Location
|
||||
match?: NodeMatch<T>
|
||||
mode?: RangeMode
|
||||
always?: boolean
|
||||
height?: number
|
||||
voids?: boolean
|
||||
}
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Unset properties on the nodes at a location.
|
||||
*/
|
||||
unsetNodes: <T extends Node>(
|
||||
editor: Editor,
|
||||
props: string | string[],
|
||||
options?: {
|
||||
at?: Location
|
||||
match?: NodeMatch<T>
|
||||
mode?: MaximizeMode
|
||||
hanging?: boolean
|
||||
split?: boolean
|
||||
voids?: boolean
|
||||
}
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Unwrap the nodes at a location from a parent node, splitting the parent if
|
||||
* necessary to ensure that only the content in the range is unwrapped.
|
||||
*/
|
||||
unwrapNodes: <T extends Node>(
|
||||
editor: Editor,
|
||||
options?: {
|
||||
at?: Location
|
||||
match?: NodeMatch<T>
|
||||
mode?: MaximizeMode
|
||||
split?: boolean
|
||||
voids?: boolean
|
||||
}
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Wrap the nodes at a location in a new container node, splitting the edges
|
||||
* of the range first to ensure that only the content in the range is wrapped.
|
||||
*/
|
||||
wrapNodes: <T extends Node>(
|
||||
editor: Editor,
|
||||
element: Element,
|
||||
options?: {
|
||||
at?: Location
|
||||
match?: NodeMatch<T>
|
||||
mode?: MaximizeMode
|
||||
split?: boolean
|
||||
voids?: boolean
|
||||
}
|
||||
) => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const NodeTransforms: NodeTransforms = {
|
||||
insertNodes(editor, nodes, options) {
|
||||
editor.insertNodes(nodes, options)
|
||||
},
|
||||
liftNodes(editor, options) {
|
||||
editor.liftNodes(options)
|
||||
},
|
||||
mergeNodes(editor, options) {
|
||||
editor.mergeNodes(options)
|
||||
},
|
||||
moveNodes(editor, options) {
|
||||
editor.moveNodes(options)
|
||||
},
|
||||
removeNodes(editor, options) {
|
||||
editor.removeNodes(options)
|
||||
},
|
||||
setNodes(editor, props, options) {
|
||||
editor.setNodes(props, options)
|
||||
},
|
||||
splitNodes(editor, options) {
|
||||
editor.splitNodes(options)
|
||||
},
|
||||
unsetNodes(editor, props, options) {
|
||||
editor.unsetNodes(props, options)
|
||||
},
|
||||
unwrapNodes(editor, options) {
|
||||
editor.unwrapNodes(options)
|
||||
},
|
||||
wrapNodes(editor, element, options) {
|
||||
editor.wrapNodes(element, options)
|
||||
},
|
||||
}
|
75
packages/slate/src/interfaces/transforms/selection.ts
Normal file
75
packages/slate/src/interfaces/transforms/selection.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Editor, Location, Point, Range } from '../../index'
|
||||
import { MoveUnit, SelectionEdge } from '../../types/types'
|
||||
|
||||
export interface SelectionCollapseOptions {
|
||||
edge?: SelectionEdge
|
||||
}
|
||||
|
||||
export interface SelectionMoveOptions {
|
||||
distance?: number
|
||||
unit?: MoveUnit
|
||||
reverse?: boolean
|
||||
edge?: SelectionEdge
|
||||
}
|
||||
|
||||
export interface SelectionSetPointOptions {
|
||||
edge?: SelectionEdge
|
||||
}
|
||||
|
||||
export interface SelectionTransforms {
|
||||
/**
|
||||
* Collapse the selection.
|
||||
*/
|
||||
collapse: (editor: Editor, options?: SelectionCollapseOptions) => void
|
||||
|
||||
/**
|
||||
* Unset the selection.
|
||||
*/
|
||||
deselect: (editor: Editor) => void
|
||||
|
||||
/**
|
||||
* Move the selection's point forward or backward.
|
||||
*/
|
||||
move: (editor: Editor, options?: SelectionMoveOptions) => void
|
||||
|
||||
/**
|
||||
* Set the selection to a new value.
|
||||
*/
|
||||
select: (editor: Editor, target: Location) => void
|
||||
|
||||
/**
|
||||
* Set new properties on one of the selection's points.
|
||||
*/
|
||||
setPoint: (
|
||||
editor: Editor,
|
||||
props: Partial<Point>,
|
||||
options?: SelectionSetPointOptions
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Set new properties on the selection.
|
||||
*/
|
||||
setSelection: (editor: Editor, props: Partial<Range>) => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const SelectionTransforms: SelectionTransforms = {
|
||||
collapse(editor, options) {
|
||||
editor.collapse(options)
|
||||
},
|
||||
deselect(editor) {
|
||||
editor.deselect()
|
||||
},
|
||||
move(editor, options) {
|
||||
editor.move(options)
|
||||
},
|
||||
select(editor, target) {
|
||||
editor.select(target)
|
||||
},
|
||||
setPoint(editor, props, options) {
|
||||
editor.setPoint(props, options)
|
||||
},
|
||||
setSelection(editor, props) {
|
||||
editor.setSelection(props)
|
||||
},
|
||||
}
|
106
packages/slate/src/interfaces/transforms/text.ts
Normal file
106
packages/slate/src/interfaces/transforms/text.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Editor, Location, Node, Path, Range, Transforms } from '../../index'
|
||||
import { TextUnit } from '../../types/types'
|
||||
|
||||
export interface TextDeleteOptions {
|
||||
at?: Location
|
||||
distance?: number
|
||||
unit?: TextUnit
|
||||
reverse?: boolean
|
||||
hanging?: boolean
|
||||
voids?: boolean
|
||||
}
|
||||
|
||||
export interface TextInsertFragmentOptions {
|
||||
at?: Location
|
||||
hanging?: boolean
|
||||
voids?: boolean
|
||||
}
|
||||
|
||||
export interface TextInsertTextOptions {
|
||||
at?: Location
|
||||
voids?: boolean
|
||||
}
|
||||
|
||||
export interface TextTransforms {
|
||||
/**
|
||||
* Delete content in the editor.
|
||||
*/
|
||||
delete: (editor: Editor, options?: TextDeleteOptions) => void
|
||||
|
||||
/**
|
||||
* Insert a fragment at a specific location in the editor.
|
||||
*/
|
||||
insertFragment: (
|
||||
editor: Editor,
|
||||
fragment: Node[],
|
||||
options?: TextInsertFragmentOptions
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Insert a string of text in the Editor.
|
||||
*/
|
||||
insertText: (
|
||||
editor: Editor,
|
||||
text: string,
|
||||
options?: TextInsertTextOptions
|
||||
) => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const TextTransforms: TextTransforms = {
|
||||
delete(editor, options) {
|
||||
editor.delete(options)
|
||||
},
|
||||
insertFragment(editor, fragment, options) {
|
||||
editor.insertFragment(fragment, options)
|
||||
},
|
||||
insertText(
|
||||
editor: Editor,
|
||||
text: string,
|
||||
options: TextInsertTextOptions = {}
|
||||
): void {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
const { voids = false } = options
|
||||
let { at = editor.selection } = options
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Path.isPath(at)) {
|
||||
at = Editor.range(editor, at)
|
||||
}
|
||||
|
||||
if (Range.isRange(at)) {
|
||||
if (Range.isCollapsed(at)) {
|
||||
at = at.anchor
|
||||
} else {
|
||||
const end = Range.end(at)
|
||||
if (!voids && Editor.void(editor, { at: end })) {
|
||||
return
|
||||
}
|
||||
const start = Range.start(at)
|
||||
const startRef = Editor.pointRef(editor, start)
|
||||
const endRef = Editor.pointRef(editor, end)
|
||||
Transforms.delete(editor, { at, voids })
|
||||
const startPoint = startRef.unref()
|
||||
const endPoint = endRef.unref()
|
||||
|
||||
at = startPoint || endPoint!
|
||||
Transforms.setSelection(editor, { anchor: at, focus: at })
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(!voids && Editor.void(editor, { at })) ||
|
||||
Editor.elementReadOnly(editor, { at })
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const { path, offset } = at
|
||||
if (text.length > 0)
|
||||
editor.apply({ type: 'insert_text', path, offset, text })
|
||||
})
|
||||
},
|
||||
}
|
10
packages/slate/src/transforms-node/index.ts
Normal file
10
packages/slate/src/transforms-node/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from './insert-nodes'
|
||||
export * from './lift-nodes'
|
||||
export * from './merge-nodes'
|
||||
export * from './move-nodes'
|
||||
export * from './remove-nodes'
|
||||
export * from './set-nodes'
|
||||
export * from './split-nodes'
|
||||
export * from './unset-nodes'
|
||||
export * from './unwrap-nodes'
|
||||
export * from './wrap-nodes'
|
117
packages/slate/src/transforms-node/insert-nodes.ts
Normal file
117
packages/slate/src/transforms-node/insert-nodes.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { NodeTransforms } from '../interfaces/transforms/node'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
import { Node } from '../interfaces/node'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { Point } from '../interfaces/point'
|
||||
import { Text } from '../interfaces/text'
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Path } from '../interfaces/path'
|
||||
|
||||
export const insertNodes: NodeTransforms['insertNodes'] = (
|
||||
editor,
|
||||
nodes,
|
||||
options = {}
|
||||
) => {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
const { hanging = false, voids = false, mode = 'lowest' } = options
|
||||
let { at, match, select } = options
|
||||
|
||||
if (Node.isNode(nodes)) {
|
||||
nodes = [nodes]
|
||||
}
|
||||
|
||||
if (nodes.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const [node] = nodes
|
||||
|
||||
// By default, use the selection as the target location. But if there is
|
||||
// no selection, insert at the end of the document since that is such a
|
||||
// common use case when inserting from a non-selected state.
|
||||
if (!at) {
|
||||
if (editor.selection) {
|
||||
at = editor.selection
|
||||
} else if (editor.children.length > 0) {
|
||||
at = Editor.end(editor, [])
|
||||
} else {
|
||||
at = [0]
|
||||
}
|
||||
|
||||
select = true
|
||||
}
|
||||
|
||||
if (select == null) {
|
||||
select = false
|
||||
}
|
||||
|
||||
if (Range.isRange(at)) {
|
||||
if (!hanging) {
|
||||
at = Editor.unhangRange(editor, at, { voids })
|
||||
}
|
||||
|
||||
if (Range.isCollapsed(at)) {
|
||||
at = at.anchor
|
||||
} else {
|
||||
const [, end] = Range.edges(at)
|
||||
const pointRef = Editor.pointRef(editor, end)
|
||||
Transforms.delete(editor, { at })
|
||||
at = pointRef.unref()!
|
||||
}
|
||||
}
|
||||
|
||||
if (Point.isPoint(at)) {
|
||||
if (match == null) {
|
||||
if (Text.isText(node)) {
|
||||
match = n => Text.isText(n)
|
||||
} else if (editor.isInline(node)) {
|
||||
match = n => Text.isText(n) || Editor.isInline(editor, n)
|
||||
} else {
|
||||
match = n => Element.isElement(n) && Editor.isBlock(editor, n)
|
||||
}
|
||||
}
|
||||
|
||||
const [entry] = Editor.nodes(editor, {
|
||||
at: at.path,
|
||||
match,
|
||||
mode,
|
||||
voids,
|
||||
})
|
||||
|
||||
if (entry) {
|
||||
const [, matchPath] = entry
|
||||
const pathRef = Editor.pathRef(editor, matchPath)
|
||||
const isAtEnd = Editor.isEnd(editor, at, matchPath)
|
||||
Transforms.splitNodes(editor, { at, match, mode, voids })
|
||||
const path = pathRef.unref()!
|
||||
at = isAtEnd ? Path.next(path) : path
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const parentPath = Path.parent(at)
|
||||
let index = at[at.length - 1]
|
||||
|
||||
if (!voids && Editor.void(editor, { at: parentPath })) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
const path = parentPath.concat(index)
|
||||
index++
|
||||
editor.apply({ type: 'insert_node', path, node })
|
||||
at = Path.next(at)
|
||||
}
|
||||
at = Path.previous(at)
|
||||
|
||||
if (select) {
|
||||
const point = Editor.end(editor, at)
|
||||
|
||||
if (point) {
|
||||
Transforms.select(editor, point)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
61
packages/slate/src/transforms-node/lift-nodes.ts
Normal file
61
packages/slate/src/transforms-node/lift-nodes.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { NodeTransforms } from '../interfaces/transforms/node'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { matchPath } from '../utils/match-path'
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Ancestor, NodeEntry } from '../interfaces/node'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
|
||||
export const liftNodes: NodeTransforms['liftNodes'] = (
|
||||
editor,
|
||||
options = {}
|
||||
) => {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
const { at = editor.selection, mode = 'lowest', voids = false } = options
|
||||
let { match } = options
|
||||
|
||||
if (match == null) {
|
||||
match = Path.isPath(at)
|
||||
? matchPath(editor, at)
|
||||
: n => Element.isElement(n) && Editor.isBlock(editor, n)
|
||||
}
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
const matches = Editor.nodes(editor, { at, match, mode, voids })
|
||||
const pathRefs = Array.from(matches, ([, p]) => Editor.pathRef(editor, p))
|
||||
|
||||
for (const pathRef of pathRefs) {
|
||||
const path = pathRef.unref()!
|
||||
|
||||
if (path.length < 2) {
|
||||
throw new Error(
|
||||
`Cannot lift node at a path [${path}] because it has a depth of less than \`2\`.`
|
||||
)
|
||||
}
|
||||
|
||||
const parentNodeEntry = Editor.node(editor, Path.parent(path))
|
||||
const [parent, parentPath] = parentNodeEntry as NodeEntry<Ancestor>
|
||||
const index = path[path.length - 1]
|
||||
const { length } = parent.children
|
||||
|
||||
if (length === 1) {
|
||||
const toPath = Path.next(parentPath)
|
||||
Transforms.moveNodes(editor, { at: path, to: toPath, voids })
|
||||
Transforms.removeNodes(editor, { at: parentPath, voids })
|
||||
} else if (index === 0) {
|
||||
Transforms.moveNodes(editor, { at: path, to: parentPath, voids })
|
||||
} else if (index === length - 1) {
|
||||
const toPath = Path.next(parentPath)
|
||||
Transforms.moveNodes(editor, { at: path, to: toPath, voids })
|
||||
} else {
|
||||
const splitPath = Path.next(path)
|
||||
const toPath = Path.next(parentPath)
|
||||
Transforms.splitNodes(editor, { at: splitPath, voids })
|
||||
Transforms.moveNodes(editor, { at: path, to: toPath, voids })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
156
packages/slate/src/transforms-node/merge-nodes.ts
Normal file
156
packages/slate/src/transforms-node/merge-nodes.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { NodeTransforms } from '../interfaces/transforms/node'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { Text } from '../interfaces/text'
|
||||
import { Scrubber } from '../interfaces/scrubber'
|
||||
import { Node } from '../interfaces/node'
|
||||
|
||||
const hasSingleChildNest = (editor: Editor, node: Node): boolean => {
|
||||
if (Element.isElement(node)) {
|
||||
const element = node as Element
|
||||
if (Editor.isVoid(editor, node)) {
|
||||
return true
|
||||
} else if (element.children.length === 1) {
|
||||
return hasSingleChildNest(editor, element.children[0])
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else if (Editor.isEditor(node)) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const mergeNodes: NodeTransforms['mergeNodes'] = (
|
||||
editor,
|
||||
options = {}
|
||||
) => {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
let { match, at = editor.selection } = options
|
||||
const { hanging = false, voids = false, mode = 'lowest' } = options
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
if (match == null) {
|
||||
if (Path.isPath(at)) {
|
||||
const [parent] = Editor.parent(editor, at)
|
||||
match = n => parent.children.includes(n)
|
||||
} else {
|
||||
match = n => Element.isElement(n) && Editor.isBlock(editor, n)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hanging && Range.isRange(at)) {
|
||||
at = Editor.unhangRange(editor, at, { voids })
|
||||
}
|
||||
|
||||
if (Range.isRange(at)) {
|
||||
if (Range.isCollapsed(at)) {
|
||||
at = at.anchor
|
||||
} else {
|
||||
const [, end] = Range.edges(at)
|
||||
const pointRef = Editor.pointRef(editor, end)
|
||||
Transforms.delete(editor, { at })
|
||||
at = pointRef.unref()!
|
||||
|
||||
if (options.at == null) {
|
||||
Transforms.select(editor, at)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [current] = Editor.nodes(editor, { at, match, voids, mode })
|
||||
const prev = Editor.previous(editor, { at, match, voids, mode })
|
||||
|
||||
if (!current || !prev) {
|
||||
return
|
||||
}
|
||||
|
||||
const [node, path] = current
|
||||
const [prevNode, prevPath] = prev
|
||||
|
||||
if (path.length === 0 || prevPath.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const newPath = Path.next(prevPath)
|
||||
const commonPath = Path.common(path, prevPath)
|
||||
const isPreviousSibling = Path.isSibling(path, prevPath)
|
||||
const levels = Array.from(Editor.levels(editor, { at: path }), ([n]) => n)
|
||||
.slice(commonPath.length)
|
||||
.slice(0, -1)
|
||||
|
||||
// Determine if the merge will leave an ancestor of the path empty as a
|
||||
// result, in which case we'll want to remove it after merging.
|
||||
const emptyAncestor = Editor.above(editor, {
|
||||
at: path,
|
||||
mode: 'highest',
|
||||
match: n => levels.includes(n) && hasSingleChildNest(editor, n),
|
||||
})
|
||||
|
||||
const emptyRef = emptyAncestor && Editor.pathRef(editor, emptyAncestor[1])
|
||||
let properties
|
||||
let position
|
||||
|
||||
// Ensure that the nodes are equivalent, and figure out what the position
|
||||
// and extra properties of the merge will be.
|
||||
if (Text.isText(node) && Text.isText(prevNode)) {
|
||||
const { text, ...rest } = node
|
||||
position = prevNode.text.length
|
||||
properties = rest as Partial<Text>
|
||||
} else if (Element.isElement(node) && Element.isElement(prevNode)) {
|
||||
const { children, ...rest } = node
|
||||
position = prevNode.children.length
|
||||
properties = rest as Partial<Element>
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot merge the node at path [${path}] with the previous sibling because it is not the same kind: ${Scrubber.stringify(
|
||||
node
|
||||
)} ${Scrubber.stringify(prevNode)}`
|
||||
)
|
||||
}
|
||||
|
||||
// If the node isn't already the next sibling of the previous node, move
|
||||
// it so that it is before merging.
|
||||
if (!isPreviousSibling) {
|
||||
Transforms.moveNodes(editor, { at: path, to: newPath, voids })
|
||||
}
|
||||
|
||||
// If there was going to be an empty ancestor of the node that was merged,
|
||||
// we remove it from the tree.
|
||||
if (emptyRef) {
|
||||
Transforms.removeNodes(editor, { at: emptyRef.current!, voids })
|
||||
}
|
||||
|
||||
// If the target node that we're merging with is empty, remove it instead
|
||||
// of merging the two. This is a common rich text editor behavior to
|
||||
// prevent losing formatting when deleting entire nodes when you have a
|
||||
// hanging selection.
|
||||
// if prevNode is first child in parent,don't remove it.
|
||||
if (
|
||||
(Element.isElement(prevNode) && Editor.isEmpty(editor, prevNode)) ||
|
||||
(Text.isText(prevNode) &&
|
||||
prevNode.text === '' &&
|
||||
prevPath[prevPath.length - 1] !== 0)
|
||||
) {
|
||||
Transforms.removeNodes(editor, { at: prevPath, voids })
|
||||
} else {
|
||||
editor.apply({
|
||||
type: 'merge_node',
|
||||
path: newPath,
|
||||
position,
|
||||
properties,
|
||||
})
|
||||
}
|
||||
|
||||
if (emptyRef) {
|
||||
emptyRef.unref()
|
||||
}
|
||||
})
|
||||
}
|
53
packages/slate/src/transforms-node/move-nodes.ts
Normal file
53
packages/slate/src/transforms-node/move-nodes.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { NodeTransforms } from '../interfaces/transforms/node'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { matchPath } from '../utils/match-path'
|
||||
import { Element } from '../interfaces/element'
|
||||
|
||||
export const moveNodes: NodeTransforms['moveNodes'] = (editor, options) => {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
const {
|
||||
to,
|
||||
at = editor.selection,
|
||||
mode = 'lowest',
|
||||
voids = false,
|
||||
} = options
|
||||
let { match } = options
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
if (match == null) {
|
||||
match = Path.isPath(at)
|
||||
? matchPath(editor, at)
|
||||
: n => Element.isElement(n) && Editor.isBlock(editor, n)
|
||||
}
|
||||
|
||||
const toRef = Editor.pathRef(editor, to)
|
||||
const targets = Editor.nodes(editor, { at, match, mode, voids })
|
||||
const pathRefs = Array.from(targets, ([, p]) => Editor.pathRef(editor, p))
|
||||
|
||||
for (const pathRef of pathRefs) {
|
||||
const path = pathRef.unref()!
|
||||
const newPath = toRef.current!
|
||||
|
||||
if (path.length !== 0) {
|
||||
editor.apply({ type: 'move_node', path, newPath })
|
||||
}
|
||||
|
||||
if (
|
||||
toRef.current &&
|
||||
Path.isSibling(newPath, path) &&
|
||||
Path.isAfter(newPath, path)
|
||||
) {
|
||||
// When performing a sibling move to a later index, the path at the destination is shifted
|
||||
// to before the insertion point instead of after. To ensure our group of nodes are inserted
|
||||
// in the correct order we increment toRef to account for that
|
||||
toRef.current = Path.next(toRef.current)
|
||||
}
|
||||
}
|
||||
|
||||
toRef.unref()
|
||||
})
|
||||
}
|
42
packages/slate/src/transforms-node/remove-nodes.ts
Normal file
42
packages/slate/src/transforms-node/remove-nodes.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NodeTransforms } from '../interfaces/transforms/node'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { matchPath } from '../utils/match-path'
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Range } from '../interfaces/range'
|
||||
|
||||
export const removeNodes: NodeTransforms['removeNodes'] = (
|
||||
editor,
|
||||
options = {}
|
||||
) => {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
const { hanging = false, voids = false, mode = 'lowest' } = options
|
||||
let { at = editor.selection, match } = options
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
if (match == null) {
|
||||
match = Path.isPath(at)
|
||||
? matchPath(editor, at)
|
||||
: n => Element.isElement(n) && Editor.isBlock(editor, n)
|
||||
}
|
||||
|
||||
if (!hanging && Range.isRange(at)) {
|
||||
at = Editor.unhangRange(editor, at, { voids })
|
||||
}
|
||||
|
||||
const depths = Editor.nodes(editor, { at, match, mode, voids })
|
||||
const pathRefs = Array.from(depths, ([, p]) => Editor.pathRef(editor, p))
|
||||
|
||||
for (const pathRef of pathRefs) {
|
||||
const path = pathRef.unref()!
|
||||
|
||||
if (path) {
|
||||
const [node] = Editor.node(editor, path)
|
||||
editor.apply({ type: 'remove_node', path, node })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
121
packages/slate/src/transforms-node/set-nodes.ts
Normal file
121
packages/slate/src/transforms-node/set-nodes.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { NodeTransforms } from '../interfaces/transforms/node'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { matchPath } from '../utils/match-path'
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { Node } from '../interfaces/node'
|
||||
|
||||
export const setNodes: NodeTransforms['setNodes'] = (
|
||||
editor,
|
||||
props: Partial<Node>,
|
||||
options = {}
|
||||
) => {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
let { match, at = editor.selection, compare, merge } = options
|
||||
const {
|
||||
hanging = false,
|
||||
mode = 'lowest',
|
||||
split = false,
|
||||
voids = false,
|
||||
} = options
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
if (match == null) {
|
||||
match = Path.isPath(at)
|
||||
? matchPath(editor, at)
|
||||
: n => Element.isElement(n) && Editor.isBlock(editor, n)
|
||||
}
|
||||
|
||||
if (!hanging && Range.isRange(at)) {
|
||||
at = Editor.unhangRange(editor, at, { voids })
|
||||
}
|
||||
|
||||
if (split && Range.isRange(at)) {
|
||||
if (
|
||||
Range.isCollapsed(at) &&
|
||||
Editor.leaf(editor, at.anchor)[0].text.length > 0
|
||||
) {
|
||||
// If the range is collapsed in a non-empty node and 'split' is true, there's nothing to
|
||||
// set that won't get normalized away
|
||||
return
|
||||
}
|
||||
const rangeRef = Editor.rangeRef(editor, at, { affinity: 'inward' })
|
||||
const [start, end] = Range.edges(at)
|
||||
const splitMode = mode === 'lowest' ? 'lowest' : 'highest'
|
||||
const endAtEndOfNode = Editor.isEnd(editor, end, end.path)
|
||||
Transforms.splitNodes(editor, {
|
||||
at: end,
|
||||
match,
|
||||
mode: splitMode,
|
||||
voids,
|
||||
always: !endAtEndOfNode,
|
||||
})
|
||||
const startAtStartOfNode = Editor.isStart(editor, start, start.path)
|
||||
Transforms.splitNodes(editor, {
|
||||
at: start,
|
||||
match,
|
||||
mode: splitMode,
|
||||
voids,
|
||||
always: !startAtStartOfNode,
|
||||
})
|
||||
at = rangeRef.unref()!
|
||||
|
||||
if (options.at == null) {
|
||||
Transforms.select(editor, at)
|
||||
}
|
||||
}
|
||||
|
||||
if (!compare) {
|
||||
compare = (prop, nodeProp) => prop !== nodeProp
|
||||
}
|
||||
|
||||
for (const [node, path] of Editor.nodes(editor, {
|
||||
at,
|
||||
match,
|
||||
mode,
|
||||
voids,
|
||||
})) {
|
||||
const properties: Partial<Node> = {}
|
||||
const newProperties: Partial<Node> = {}
|
||||
|
||||
// You can't set properties on the editor node.
|
||||
if (path.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
let hasChanges = false
|
||||
|
||||
for (const k in props) {
|
||||
if (k === 'children' || k === 'text') {
|
||||
continue
|
||||
}
|
||||
|
||||
if (compare(props[k], node[k])) {
|
||||
hasChanges = true
|
||||
// Omit new properties from the old properties list
|
||||
if (node.hasOwnProperty(k)) properties[k] = node[k]
|
||||
// Omit properties that have been removed from the new properties list
|
||||
if (merge) {
|
||||
if (props[k] != null) newProperties[k] = merge(node[k], props[k])
|
||||
} else {
|
||||
if (props[k] != null) newProperties[k] = props[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
editor.apply({
|
||||
type: 'set_node',
|
||||
path,
|
||||
properties,
|
||||
newProperties,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
140
packages/slate/src/transforms-node/split-nodes.ts
Normal file
140
packages/slate/src/transforms-node/split-nodes.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { NodeTransforms } from '../interfaces/transforms/node'
|
||||
import { Editor } from '../interfaces/editor'
|
||||
import { Element } from '../interfaces/element'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Path } from '../interfaces/path'
|
||||
import { PointRef } from '../interfaces/point-ref'
|
||||
import { Transforms } from '../interfaces/transforms'
|
||||
import { Node } from '../interfaces/node'
|
||||
import { Point } from '../interfaces/point'
|
||||
|
||||
/**
|
||||
* Convert a range into a point by deleting it's content.
|
||||
*/
|
||||
const deleteRange = (editor: Editor, range: Range): Point | null => {
|
||||
if (Range.isCollapsed(range)) {
|
||||
return range.anchor
|
||||
} else {
|
||||
const [, end] = Range.edges(range)
|
||||
const pointRef = Editor.pointRef(editor, end)
|
||||
Transforms.delete(editor, { at: range })
|
||||
return pointRef.unref()
|
||||
}
|
||||
}
|
||||
|
||||
export const splitNodes: NodeTransforms['splitNodes'] = (
|
||||
editor,
|
||||
options = {}
|
||||
) => {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
const { mode = 'lowest', voids = false } = options
|
||||
let { match, at = editor.selection, height = 0, always = false } = options
|
||||
|
||||
if (match == null) {
|
||||
match = n => Element.isElement(n) && Editor.isBlock(editor, n)
|
||||
}
|
||||
|
||||
if (Range.isRange(at)) {
|
||||
at = deleteRange(editor, at)
|
||||
}
|
||||
|
||||
// If the target is a path, the default height-skipping and position
|
||||
// counters need to account for us potentially splitting at a non-leaf.
|
||||
if (Path.isPath(at)) {
|
||||
const path = at
|
||||
const point = Editor.point(editor, path)
|
||||
const [parent] = Editor.parent(editor, path)
|
||||
match = n => n === parent
|
||||
height = point.path.length - path.length + 1
|
||||
at = point
|
||||
always = true
|
||||
}
|
||||
|
||||
if (!at) {
|
||||
return
|
||||
}
|
||||
|
||||
const beforeRef = Editor.pointRef(editor, at, {
|
||||
affinity: 'backward',
|
||||
})
|
||||
let afterRef: PointRef | undefined
|
||||
try {
|
||||
const [highest] = Editor.nodes(editor, { at, match, mode, voids })
|
||||
|
||||
if (!highest) {
|
||||
return
|
||||
}
|
||||
|
||||
const voidMatch = Editor.void(editor, { at, mode: 'highest' })
|
||||
const nudge = 0
|
||||
|
||||
if (!voids && voidMatch) {
|
||||
const [voidNode, voidPath] = voidMatch
|
||||
|
||||
if (Element.isElement(voidNode) && editor.isInline(voidNode)) {
|
||||
let after = Editor.after(editor, voidPath)
|
||||
|
||||
if (!after) {
|
||||
const text = { text: '' }
|
||||
const afterPath = Path.next(voidPath)
|
||||
Transforms.insertNodes(editor, text, { at: afterPath, voids })
|
||||
after = Editor.point(editor, afterPath)!
|
||||
}
|
||||
|
||||
at = after
|
||||
always = true
|
||||
}
|
||||
|
||||
const siblingHeight = at.path.length - voidPath.length
|
||||
height = siblingHeight + 1
|
||||
always = true
|
||||
}
|
||||
|
||||
afterRef = Editor.pointRef(editor, at)
|
||||
const depth = at.path.length - height
|
||||
const [, highestPath] = highest
|
||||
const lowestPath = at.path.slice(0, depth)
|
||||
let position = height === 0 ? at.offset : at.path[depth] + nudge
|
||||
|
||||
for (const [node, path] of Editor.levels(editor, {
|
||||
at: lowestPath,
|
||||
reverse: true,
|
||||
voids,
|
||||
})) {
|
||||
let split = false
|
||||
|
||||
if (
|
||||
path.length < highestPath.length ||
|
||||
path.length === 0 ||
|
||||
(!voids && Element.isElement(node) && Editor.isVoid(editor, node))
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
const point = beforeRef.current!
|
||||
const isEnd = Editor.isEnd(editor, point, path)
|
||||
|
||||
if (always || !beforeRef || !Editor.isEdge(editor, point, path)) {
|
||||
split = true
|
||||
const properties = Node.extractProps(node)
|
||||
editor.apply({
|
||||
type: 'split_node',
|
||||
path,
|
||||
position,
|
||||
properties,
|
||||
})
|
||||
}
|
||||
|
||||
position = path[path.length - 1] + (split || isEnd ? 1 : 0)
|
||||
}
|
||||
|
||||
if (options.at == null) {
|
||||
const point = afterRef.current || Editor.end(editor, [])
|
||||
Transforms.select(editor, point)
|
||||
}
|
||||
} finally {
|
||||
beforeRef.unref()
|
||||
afterRef?.unref()
|
||||
}
|
||||
})
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user