1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-29 09:59:48 +02:00

refactor: Split out slate dom package (#5734)

* Copied some things from slate-react into new react-dom package

* Refactor slate-react to use slate-dom

* Fixed failing tests

* Created changeset

* Ran fix:prettier

* Fixed name

* Removed duplicate code

* Fixed import

* Restored linting rule

* Bumped slate-dom version

* Bumped slate dependency version

* Added export of IS_NODE_MAP_DIRTY after rebase
This commit is contained in:
Brian Ingles
2024-10-31 09:20:34 -05:00
committed by GitHub
parent 7e1608018b
commit 9a21251270
37 changed files with 1847 additions and 1559 deletions

View File

@@ -35,13 +35,15 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"slate": "^0.110.2",
"slate-dom": "^0.110.2",
"slate-hyperscript": "^0.100.0",
"source-map-loader": "^4.0.1"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0",
"slate": ">=0.99.0"
"slate": ">=0.99.0",
"slate-dom": ">=0.110.2"
},
"umdGlobals": {
"react": "React",

View File

@@ -31,7 +31,7 @@ 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 { TRIPLE_CLICK } from 'slate-dom'
import {
DOMElement,
DOMRange,
@@ -42,7 +42,7 @@ import {
isDOMElement,
isDOMNode,
isPlainTextOnlyPaste,
} from '../utils/dom'
} from 'slate-dom'
import {
CAN_USE_DOM,
HAS_BEFORE_INPUT_SUPPORT,
@@ -54,8 +54,8 @@ import {
IS_WEBKIT,
IS_UC_MOBILE,
IS_WECHATBROWSER,
} from '../utils/environment'
import Hotkeys from '../utils/hotkeys'
} from 'slate-dom'
import { Hotkeys } from 'slate-dom'
import {
IS_NODE_MAP_DIRTY,
EDITOR_TO_ELEMENT,
@@ -71,7 +71,7 @@ import {
MARK_PLACEHOLDER_SYMBOL,
NODE_TO_ELEMENT,
PLACEHOLDER_SYMBOL,
} from '../utils/weak-maps'
} from 'slate-dom'
import { RestoreDOM } from './restore-dom/restore-dom'
import { AndroidInputManager } from '../hooks/android-input-manager/android-input-manager'
import { ComposingContext } from '../hooks/use-composing'

View File

@@ -4,14 +4,14 @@ import { JSX } from 'react'
import { Editor, Element as SlateElement, Node, Range } from 'slate'
import { ReactEditor, useReadOnly, useSlateStatic } from '..'
import useChildren from '../hooks/use-children'
import { isElementDecorationsEqual } from '../utils/range-list'
import { isElementDecorationsEqual } from 'slate-dom'
import {
EDITOR_TO_KEY_TO_ELEMENT,
ELEMENT_TO_NODE,
NODE_TO_ELEMENT,
NODE_TO_INDEX,
NODE_TO_PARENT,
} from '../utils/weak-maps'
} from 'slate-dom'
import {
RenderElementProps,
RenderLeafProps,

View File

@@ -13,10 +13,10 @@ import {
PLACEHOLDER_SYMBOL,
EDITOR_TO_PLACEHOLDER_ELEMENT,
EDITOR_TO_FORCE_RENDER,
} from '../utils/weak-maps'
} from 'slate-dom'
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
import { useSlateStatic } from '../hooks/use-slate-static'
import { IS_WEBKIT, IS_ANDROID } from '../utils/environment'
import { IS_WEBKIT, IS_ANDROID } from 'slate-dom'
// Delay the placeholder on Android to prevent the keyboard from closing.
// (https://github.com/ianstormtaylor/slate/pull/5368)

View File

@@ -1,6 +1,6 @@
import { RefObject } from 'react'
import { ReactEditor } from '../../plugin/react-editor'
import { isTrackedMutation } from '../../utils/dom'
import { isTrackedMutation } from 'slate-dom'
export type RestoreDOMManager = {
registerMutations: (mutations: MutationRecord[]) => void

View File

@@ -6,7 +6,7 @@ import React, {
RefObject,
} from 'react'
import { EditorContext } from '../../hooks/use-slate-static'
import { IS_ANDROID } from '../../utils/environment'
import { IS_ANDROID } from 'slate-dom'
import {
createRestoreDomManager,
RestoreDOMManager,

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react'
import { Descendant, Editor, Node, Operation, Scrubber, Selection } from 'slate'
import { EDITOR_TO_ON_CHANGE } from 'slate-dom'
import { FocusedContext } from '../hooks/use-focused'
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
import { SlateContext, SlateContextValue } from '../hooks/use-slate'
@@ -10,7 +11,6 @@ import {
import { EditorContext } from '../hooks/use-slate-static'
import { ReactEditor } from '../plugin/react-editor'
import { REACT_MAJOR_VERSION } from '../utils/environment'
import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps'
/**
* A wrapper around the provider to handle `onChange` events, because the editor

View File

@@ -3,8 +3,8 @@ import { Editor, Text, Path, Element, Node } from 'slate'
import { ReactEditor, useSlateStatic } from '..'
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
import { IS_ANDROID, IS_IOS } from '../utils/environment'
import { MARK_PLACEHOLDER_SYMBOL } from '../utils/weak-maps'
import { IS_ANDROID, IS_IOS } from 'slate-dom'
import { MARK_PLACEHOLDER_SYMBOL } from 'slate-dom'
/**
* Leaf content strings.

View File

@@ -1,12 +1,12 @@
import React, { useCallback, useRef } from 'react'
import { Element, Range, Text as SlateText } from 'slate'
import { ReactEditor, useSlateStatic } from '..'
import { isTextDecorationsEqual } from '../utils/range-list'
import { isTextDecorationsEqual } from 'slate-dom'
import {
EDITOR_TO_KEY_TO_ELEMENT,
ELEMENT_TO_NODE,
NODE_TO_ELEMENT,
} from '../utils/weak-maps'
} from 'slate-dom'
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
import Leaf from './leaf'

View File

@@ -11,8 +11,8 @@ import {
targetRange,
TextDiff,
verifyDiffState,
} from '../../utils/diff-text'
import { isDOMSelection, isTrackedMutation } from '../../utils/dom'
} from 'slate-dom'
import { isDOMSelection, isTrackedMutation } from 'slate-dom'
import {
EDITOR_TO_FORCE_RENDER,
EDITOR_TO_PENDING_ACTION,
@@ -23,7 +23,7 @@ import {
EDITOR_TO_USER_MARKS,
IS_COMPOSING,
IS_NODE_MAP_DIRTY,
} from '../../utils/weak-maps'
} from 'slate-dom'
export type Action = { at?: Point | Range; run: () => void }

View File

@@ -1,7 +1,7 @@
import { RefObject, useState } from 'react'
import { useSlateStatic } from '../use-slate-static'
import { IS_ANDROID } from '../../utils/environment'
import { EDITOR_TO_SCHEDULE_FLUSH } from '../../utils/weak-maps'
import { IS_ANDROID } from 'slate-dom'
import { EDITOR_TO_SCHEDULE_FLUSH } from 'slate-dom'
import {
createAndroidInputManager,
CreateAndroidInputManagerOptions,

View File

@@ -9,11 +9,7 @@ import {
import ElementComponent from '../components/element'
import TextComponent from '../components/text'
import { ReactEditor } from '../plugin/react-editor'
import {
IS_NODE_MAP_DIRTY,
NODE_TO_INDEX,
NODE_TO_PARENT,
} from '../utils/weak-maps'
import { IS_NODE_MAP_DIRTY, NODE_TO_INDEX, NODE_TO_PARENT } from 'slate-dom'
import { useDecorate } from './use-decorate'
import { SelectedContext } from './use-selected'
import { useSlateStatic } from './use-slate-static'

View File

@@ -1,5 +1,5 @@
import { useLayoutEffect, useEffect } from 'react'
import { CAN_USE_DOM } from '../utils/environment'
import { CAN_USE_DOM } from 'slate-dom'
/**
* Prevent warning on SSR by falling back to useEffect when DOM isn't available

View File

@@ -27,4 +27,4 @@ export { ReactEditor } from './plugin/react-editor'
export { withReact } from './plugin/with-react'
// Utils
export { NODE_TO_INDEX, NODE_TO_PARENT } from './utils/weak-maps'
export { NODE_TO_INDEX, NODE_TO_PARENT } from 'slate-dom'

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,6 @@
import ReactDOM from 'react-dom'
import {
BaseEditor,
Editor,
Element,
Node,
Operation,
Path,
PathRef,
Point,
Range,
Transforms,
} from 'slate'
import {
TextDiff,
transformPendingPoint,
transformPendingRange,
transformTextDiff,
} from '../utils/diff-text'
import {
getPlainText,
getSlateFragmentAttribute,
isDOMText,
} from '../utils/dom'
import { Key } from '../utils/key'
import { findCurrentLineRange } from '../utils/lines'
import {
IS_NODE_MAP_DIRTY,
EDITOR_TO_KEY_TO_ELEMENT,
EDITOR_TO_ON_CHANGE,
EDITOR_TO_PENDING_ACTION,
EDITOR_TO_PENDING_DIFFS,
EDITOR_TO_PENDING_INSERTION_MARKS,
EDITOR_TO_PENDING_SELECTION,
EDITOR_TO_SCHEDULE_FLUSH,
EDITOR_TO_USER_MARKS,
EDITOR_TO_USER_SELECTION,
NODE_TO_KEY,
} from '../utils/weak-maps'
import { BaseEditor } from 'slate'
import { withDOM } from 'slate-dom'
import { ReactEditor } from './react-editor'
import { REACT_MAJOR_VERSION } from '../utils/environment'
@@ -48,318 +12,15 @@ import { REACT_MAJOR_VERSION } from '../utils/environment'
*
* See https://docs.slatejs.org/concepts/11-typescript to learn how.
*/
export const withReact = <T extends BaseEditor>(
editor: T,
clipboardFormatKey = 'x-slate-fragment'
): T & ReactEditor => {
const e = editor as T & ReactEditor
const { apply, onChange, deleteBackward, addMark, removeMark } = e
let e = editor as T & ReactEditor
// The WeakMap which maps a key to a specific HTMLElement must be scoped to the editor instance to
// avoid collisions between editors in the DOM that share the same value.
EDITOR_TO_KEY_TO_ELEMENT.set(e, new WeakMap())
e = withDOM(e, clipboardFormatKey)
e.addMark = (key, value) => {
EDITOR_TO_SCHEDULE_FLUSH.get(e)?.()
if (
!EDITOR_TO_PENDING_INSERTION_MARKS.get(e) &&
EDITOR_TO_PENDING_DIFFS.get(e)?.length
) {
// Ensure the current pending diffs originating from changes before the addMark
// are applied with the current formatting
EDITOR_TO_PENDING_INSERTION_MARKS.set(e, null)
}
EDITOR_TO_USER_MARKS.delete(e)
addMark(key, value)
}
e.removeMark = key => {
if (
!EDITOR_TO_PENDING_INSERTION_MARKS.get(e) &&
EDITOR_TO_PENDING_DIFFS.get(e)?.length
) {
// Ensure the current pending diffs originating from changes before the addMark
// are applied with the current formatting
EDITOR_TO_PENDING_INSERTION_MARKS.set(e, null)
}
EDITOR_TO_USER_MARKS.delete(e)
removeMark(key)
}
e.deleteBackward = unit => {
if (unit !== 'line') {
return deleteBackward(unit)
}
if (e.selection && Range.isCollapsed(e.selection)) {
const parentBlockEntry = Editor.above(e, {
match: n => Element.isElement(n) && Editor.isBlock(e, n),
at: e.selection,
})
if (parentBlockEntry) {
const [, parentBlockPath] = parentBlockEntry
const parentElementRange = Editor.range(
e,
parentBlockPath,
e.selection.anchor
)
const currentLineRange = findCurrentLineRange(e, parentElementRange)
if (!Range.isCollapsed(currentLineRange)) {
Transforms.delete(e, { at: currentLineRange })
}
}
}
}
// This attempts to reset the NODE_TO_KEY entry to the correct value
// as apply() changes the object reference and hence invalidates the NODE_TO_KEY entry
e.apply = (op: Operation) => {
const matches: [Path, Key][] = []
const pathRefMatches: [PathRef, Key][] = []
const pendingDiffs = EDITOR_TO_PENDING_DIFFS.get(e)
if (pendingDiffs?.length) {
const transformed = pendingDiffs
.map(textDiff => transformTextDiff(textDiff, op))
.filter(Boolean) as TextDiff[]
EDITOR_TO_PENDING_DIFFS.set(e, transformed)
}
const pendingSelection = EDITOR_TO_PENDING_SELECTION.get(e)
if (pendingSelection) {
EDITOR_TO_PENDING_SELECTION.set(
e,
transformPendingRange(e, pendingSelection, op)
)
}
const pendingAction = EDITOR_TO_PENDING_ACTION.get(e)
if (pendingAction?.at) {
const at = Point.isPoint(pendingAction?.at)
? transformPendingPoint(e, pendingAction.at, op)
: transformPendingRange(e, pendingAction.at, op)
EDITOR_TO_PENDING_ACTION.set(e, at ? { ...pendingAction, at } : null)
}
switch (op.type) {
case 'insert_text':
case 'remove_text':
case 'set_node':
case 'split_node': {
matches.push(...getMatches(e, op.path))
break
}
case 'set_selection': {
// Selection was manually set, don't restore the user selection after the change.
EDITOR_TO_USER_SELECTION.get(e)?.unref()
EDITOR_TO_USER_SELECTION.delete(e)
break
}
case 'insert_node':
case 'remove_node': {
matches.push(...getMatches(e, Path.parent(op.path)))
break
}
case 'merge_node': {
const prevPath = Path.previous(op.path)
matches.push(...getMatches(e, prevPath))
break
}
case 'move_node': {
const commonPath = Path.common(
Path.parent(op.path),
Path.parent(op.newPath)
)
matches.push(...getMatches(e, commonPath))
let changedPath: Path
if (Path.isBefore(op.path, op.newPath)) {
matches.push(...getMatches(e, Path.parent(op.path)))
changedPath = op.newPath
} else {
matches.push(...getMatches(e, Path.parent(op.newPath)))
changedPath = op.path
}
const changedNode = Node.get(editor, Path.parent(changedPath))
const changedNodeKey = ReactEditor.findKey(e, changedNode)
const changedPathRef = Editor.pathRef(e, Path.parent(changedPath))
pathRefMatches.push([changedPathRef, changedNodeKey])
break
}
}
apply(op)
switch (op.type) {
case 'insert_node':
case 'remove_node':
case 'merge_node':
case 'move_node':
case 'split_node': {
IS_NODE_MAP_DIRTY.set(e, true)
}
}
for (const [path, key] of matches) {
const [node] = Editor.node(e, path)
NODE_TO_KEY.set(node, key)
}
for (const [pathRef, key] of pathRefMatches) {
if (pathRef.current) {
const [node] = Editor.node(e, pathRef.current)
NODE_TO_KEY.set(node, key)
}
pathRef.unref()
}
}
e.setFragmentData = (data: Pick<DataTransfer, 'getData' | 'setData'>) => {
const { selection } = e
if (!selection) {
return
}
const [start, end] = Range.edges(selection)
const startVoid = Editor.void(e, { at: start.path })
const endVoid = Editor.void(e, { at: end.path })
if (Range.isCollapsed(selection) && !startVoid) {
return
}
// Create a fake selection so that we can add a Base64-encoded copy of the
// fragment to the HTML, to decode on future pastes.
const domRange = ReactEditor.toDOMRange(e, selection)
let contents = domRange.cloneContents()
let attach = contents.childNodes[0] as HTMLElement
// Make sure attach is non-empty, since empty nodes will not get copied.
contents.childNodes.forEach(node => {
if (node.textContent && node.textContent.trim() !== '') {
attach = node as HTMLElement
}
})
// COMPAT: If the end node is a void node, we need to move the end of the
// range from the void node's spacer span, to the end of the void node's
// content, since the spacer is before void's content in the DOM.
if (endVoid) {
const [voidNode] = endVoid
const r = domRange.cloneRange()
const domNode = ReactEditor.toDOMNode(e, voidNode)
r.setEndAfter(domNode)
contents = r.cloneContents()
}
// COMPAT: If the start node is a void node, we need to attach the encoded
// fragment to the void node's content node instead of the spacer, because
// attaching it to empty `<div>/<span>` nodes will end up having it erased by
// most browsers. (2018/04/27)
if (startVoid) {
attach = contents.querySelector('[data-slate-spacer]')! as HTMLElement
}
// Remove any zero-width space spans from the cloned DOM so that they don't
// show up elsewhere when pasted.
Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach(
zw => {
const isNewline = zw.getAttribute('data-slate-zero-width') === 'n'
zw.textContent = isNewline ? '\n' : ''
}
)
// Set a `data-slate-fragment` attribute on a non-empty node, so it shows up
// in the HTML, and can be used for intra-Slate pasting. If it's a text
// node, wrap it in a `<span>` so we have something to set an attribute on.
if (isDOMText(attach)) {
const span = attach.ownerDocument.createElement('span')
// COMPAT: In Chrome and Safari, if we don't add the `white-space` style
// then leading and trailing spaces will be ignored. (2017/09/21)
span.style.whiteSpace = 'pre'
span.appendChild(attach)
contents.appendChild(span)
attach = span
}
const fragment = e.getFragment()
const string = JSON.stringify(fragment)
const encoded = window.btoa(encodeURIComponent(string))
attach.setAttribute('data-slate-fragment', encoded)
data.setData(`application/${clipboardFormatKey}`, encoded)
// Add the content to a <div> so that we can get its inner HTML.
const div = contents.ownerDocument.createElement('div')
div.appendChild(contents)
div.setAttribute('hidden', 'true')
contents.ownerDocument.body.appendChild(div)
data.setData('text/html', div.innerHTML)
data.setData('text/plain', getPlainText(div))
contents.ownerDocument.body.removeChild(div)
return data
}
e.insertData = (data: DataTransfer) => {
if (!e.insertFragmentData(data)) {
e.insertTextData(data)
}
}
e.insertFragmentData = (data: DataTransfer): boolean => {
/**
* Checking copied fragment from application/x-slate-fragment or data-slate-fragment
*/
const fragment =
data.getData(`application/${clipboardFormatKey}`) ||
getSlateFragmentAttribute(data)
if (fragment) {
const decoded = decodeURIComponent(window.atob(fragment))
const parsed = JSON.parse(decoded) as Node[]
e.insertFragment(parsed)
return true
}
return false
}
e.insertTextData = (data: DataTransfer): boolean => {
const text = data.getData('text/plain')
if (text) {
const lines = text.split(/\r\n|\r|\n/)
let split = false
for (const line of lines) {
if (split) {
Transforms.splitNodes(e, { always: true })
}
e.insertText(line)
split = true
}
return true
}
return false
}
const { onChange } = e
e.onChange = options => {
// COMPAT: React < 18 doesn't batch `setState` hook calls, which means
@@ -373,24 +34,9 @@ export const withReact = <T extends BaseEditor>(
: (callback: () => void) => callback()
maybeBatchUpdates(() => {
const onContextChange = EDITOR_TO_ON_CHANGE.get(e)
if (onContextChange) {
onContextChange(options)
}
onChange(options)
})
}
return e
}
const getMatches = (e: Editor, path: Path) => {
const matches: [Path, Key][] = []
for (const [n, p] of Editor.levels(e, { at: path })) {
const key = ReactEditor.findKey(e, n)
matches.push([p, key])
}
return matches
}

View File

@@ -1 +0,0 @@
export const TRIPLE_CLICK = 3

View File

@@ -1,426 +0,0 @@
import {
Editor,
Node,
Operation,
Path,
Point,
Range,
Text,
Element,
} from 'slate'
import { EDITOR_TO_PENDING_DIFFS } from './weak-maps'
export type StringDiff = {
start: number
end: number
text: string
}
export type TextDiff = {
id: number
path: Path
diff: StringDiff
}
/**
* Check whether a text diff was applied in a way we can perform the pending action on /
* recover the pending selection.
*/
export function verifyDiffState(editor: Editor, textDiff: TextDiff): boolean {
const { path, diff } = textDiff
if (!Editor.hasPath(editor, path)) {
return false
}
const node = Node.get(editor, path)
if (!Text.isText(node)) {
return false
}
if (diff.start !== node.text.length || diff.text.length === 0) {
return (
node.text.slice(diff.start, diff.start + diff.text.length) === diff.text
)
}
const nextPath = Path.next(path)
if (!Editor.hasPath(editor, nextPath)) {
return false
}
const nextNode = Node.get(editor, nextPath)
return Text.isText(nextNode) && nextNode.text.startsWith(diff.text)
}
export function applyStringDiff(text: string, ...diffs: StringDiff[]) {
return diffs.reduce(
(text, diff) =>
text.slice(0, diff.start) + diff.text + text.slice(diff.end),
text
)
}
function longestCommonPrefixLength(str: string, another: string) {
const length = Math.min(str.length, another.length)
for (let i = 0; i < length; i++) {
if (str.charAt(i) !== another.charAt(i)) {
return i
}
}
return length
}
function longestCommonSuffixLength(
str: string,
another: string,
max: number
): number {
const length = Math.min(str.length, another.length, max)
for (let i = 0; i < length; i++) {
if (
str.charAt(str.length - i - 1) !== another.charAt(another.length - i - 1)
) {
return i
}
}
return length
}
/**
* Remove redundant changes from the diff so that it spans the minimal possible range
*/
export function normalizeStringDiff(targetText: string, diff: StringDiff) {
const { start, end, text } = diff
const removedText = targetText.slice(start, end)
const prefixLength = longestCommonPrefixLength(removedText, text)
const max = Math.min(
removedText.length - prefixLength,
text.length - prefixLength
)
const suffixLength = longestCommonSuffixLength(removedText, text, max)
const normalized: StringDiff = {
start: start + prefixLength,
end: end - suffixLength,
text: text.slice(prefixLength, text.length - suffixLength),
}
if (normalized.start === normalized.end && normalized.text.length === 0) {
return null
}
return normalized
}
/**
* Return a string diff that is equivalent to applying b after a spanning the range of
* both changes
*/
export function mergeStringDiffs(
targetText: string,
a: StringDiff,
b: StringDiff
): StringDiff | null {
const start = Math.min(a.start, b.start)
const overlap = Math.max(
0,
Math.min(a.start + a.text.length, b.end) - b.start
)
const applied = applyStringDiff(targetText, a, b)
const sliceEnd = Math.max(
b.start + b.text.length,
a.start +
a.text.length +
(a.start + a.text.length > b.start ? b.text.length : 0) -
overlap
)
const text = applied.slice(start, sliceEnd)
const end = Math.max(a.end, b.end - a.text.length + (a.end - a.start))
return normalizeStringDiff(targetText, { start, end, text })
}
/**
* Get the slate range the text diff spans.
*/
export function targetRange(textDiff: TextDiff): Range {
const { path, diff } = textDiff
return {
anchor: { path, offset: diff.start },
focus: { path, offset: diff.end },
}
}
/**
* Normalize a 'pending point' a.k.a a point based on the dom state before applying
* the pending diffs. Since the pending diffs might have been inserted with different
* marks we have to 'walk' the offset from the starting position to ensure we still
* have a valid point inside the document
*/
export function normalizePoint(editor: Editor, point: Point): Point | null {
let { path, offset } = point
if (!Editor.hasPath(editor, path)) {
return null
}
let leaf = Node.get(editor, path)
if (!Text.isText(leaf)) {
return null
}
const parentBlock = Editor.above(editor, {
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
at: path,
})
if (!parentBlock) {
return null
}
while (offset > leaf.text.length) {
const entry = Editor.next(editor, { at: path, match: Text.isText })
if (!entry || !Path.isDescendant(entry[1], parentBlock[1])) {
return null
}
offset -= leaf.text.length
leaf = entry[0]
path = entry[1]
}
return { path, offset }
}
/**
* Normalize a 'pending selection' to ensure it's valid in the current document state.
*/
export function normalizeRange(editor: Editor, range: Range): Range | null {
const anchor = normalizePoint(editor, range.anchor)
if (!anchor) {
return null
}
if (Range.isCollapsed(range)) {
return { anchor, focus: anchor }
}
const focus = normalizePoint(editor, range.focus)
if (!focus) {
return null
}
return { anchor, focus }
}
export function transformPendingPoint(
editor: Editor,
point: Point,
op: Operation
): Point | null {
const pendingDiffs = EDITOR_TO_PENDING_DIFFS.get(editor)
const textDiff = pendingDiffs?.find(({ path }) =>
Path.equals(path, point.path)
)
if (!textDiff || point.offset <= textDiff.diff.start) {
return Point.transform(point, op, { affinity: 'backward' })
}
const { diff } = textDiff
// Point references location inside the diff => transform the point based on the location
// the diff will be applied to and add the offset inside the diff.
if (point.offset <= diff.start + diff.text.length) {
const anchor = { path: point.path, offset: diff.start }
const transformed = Point.transform(anchor, op, {
affinity: 'backward',
})
if (!transformed) {
return null
}
return {
path: transformed.path,
offset: transformed.offset + point.offset - diff.start,
}
}
// Point references location after the diff
const anchor = {
path: point.path,
offset: point.offset - diff.text.length + diff.end - diff.start,
}
const transformed = Point.transform(anchor, op, {
affinity: 'backward',
})
if (!transformed) {
return null
}
if (
op.type === 'split_node' &&
Path.equals(op.path, point.path) &&
anchor.offset < op.position &&
diff.start < op.position
) {
return transformed
}
return {
path: transformed.path,
offset: transformed.offset + diff.text.length - diff.end + diff.start,
}
}
export function transformPendingRange(
editor: Editor,
range: Range,
op: Operation
): Range | null {
const anchor = transformPendingPoint(editor, range.anchor, op)
if (!anchor) {
return null
}
if (Range.isCollapsed(range)) {
return { anchor, focus: anchor }
}
const focus = transformPendingPoint(editor, range.focus, op)
if (!focus) {
return null
}
return { anchor, focus }
}
export function transformTextDiff(
textDiff: TextDiff,
op: Operation
): TextDiff | null {
const { path, diff, id } = textDiff
switch (op.type) {
case 'insert_text': {
if (!Path.equals(op.path, path) || op.offset >= diff.end) {
return textDiff
}
if (op.offset <= diff.start) {
return {
diff: {
start: op.text.length + diff.start,
end: op.text.length + diff.end,
text: diff.text,
},
id,
path,
}
}
return {
diff: {
start: diff.start,
end: diff.end + op.text.length,
text: diff.text,
},
id,
path,
}
}
case 'remove_text': {
if (!Path.equals(op.path, path) || op.offset >= diff.end) {
return textDiff
}
if (op.offset + op.text.length <= diff.start) {
return {
diff: {
start: diff.start - op.text.length,
end: diff.end - op.text.length,
text: diff.text,
},
id,
path,
}
}
return {
diff: {
start: diff.start,
end: diff.end - op.text.length,
text: diff.text,
},
id,
path,
}
}
case 'split_node': {
if (!Path.equals(op.path, path) || op.position >= diff.end) {
return {
diff,
id,
path: Path.transform(path, op, { affinity: 'backward' })!,
}
}
if (op.position > diff.start) {
return {
diff: {
start: diff.start,
end: Math.min(op.position, diff.end),
text: diff.text,
},
id,
path,
}
}
return {
diff: {
start: diff.start - op.position,
end: diff.end - op.position,
text: diff.text,
},
id,
path: Path.transform(path, op, { affinity: 'forward' })!,
}
}
case 'merge_node': {
if (!Path.equals(op.path, path)) {
return {
diff,
id,
path: Path.transform(path, op)!,
}
}
return {
diff: {
start: diff.start + op.position,
end: diff.end + op.position,
text: diff.text,
},
id,
path: Path.transform(path, op)!,
}
}
}
const newPath = Path.transform(path, op)
if (!newPath) {
return null
}
return {
diff,
path: newPath,
id,
}
}

View File

@@ -1,357 +0,0 @@
/**
* Types.
*/
// COMPAT: This is required to prevent TypeScript aliases from doing some very
// weird things for Slate's types with the same name as globals. (2019/11/27)
// https://github.com/microsoft/TypeScript/issues/35002
import DOMNode = globalThis.Node
import DOMComment = globalThis.Comment
import DOMElement = globalThis.Element
import DOMText = globalThis.Text
import DOMRange = globalThis.Range
import DOMSelection = globalThis.Selection
import DOMStaticRange = globalThis.StaticRange
import { ReactEditor } from '../plugin/react-editor'
export {
DOMNode,
DOMComment,
DOMElement,
DOMText,
DOMRange,
DOMSelection,
DOMStaticRange,
}
declare global {
interface Window {
Selection: (typeof Selection)['constructor']
DataTransfer: (typeof DataTransfer)['constructor']
Node: (typeof Node)['constructor']
}
}
export type DOMPoint = [Node, number]
/**
* Returns the host window of a DOM node
*/
export const getDefaultView = (value: any): Window | null => {
return (
(value && value.ownerDocument && value.ownerDocument.defaultView) || null
)
}
/**
* Check if a DOM node is a comment node.
*/
export const isDOMComment = (value: any): value is DOMComment => {
return isDOMNode(value) && value.nodeType === 8
}
/**
* Check if a DOM node is an element node.
*/
export const isDOMElement = (value: any): value is DOMElement => {
return isDOMNode(value) && value.nodeType === 1
}
/**
* Check if a value is a DOM node.
*/
export const isDOMNode = (value: any): value is DOMNode => {
const window = getDefaultView(value)
return !!window && value instanceof window.Node
}
/**
* Check if a value is a DOM selection.
*/
export const isDOMSelection = (value: any): value is DOMSelection => {
const window = value && value.anchorNode && getDefaultView(value.anchorNode)
return !!window && value instanceof window.Selection
}
/**
* Check if a DOM node is an element node.
*/
export const isDOMText = (value: any): value is DOMText => {
return isDOMNode(value) && value.nodeType === 3
}
/**
* Checks whether a paste event is a plaintext-only event.
*/
export const isPlainTextOnlyPaste = (event: ClipboardEvent) => {
return (
event.clipboardData &&
event.clipboardData.getData('text/plain') !== '' &&
event.clipboardData.types.length === 1
)
}
/**
* Normalize a DOM point so that it always refers to a text node.
*/
export const normalizeDOMPoint = (domPoint: DOMPoint): DOMPoint => {
let [node, offset] = domPoint
// If it's an element node, its offset refers to the index of its children
// including comment nodes, so try to find the right text child node.
if (isDOMElement(node) && node.childNodes.length) {
let isLast = offset === node.childNodes.length
let index = isLast ? offset - 1 : offset
;[node, index] = getEditableChildAndIndex(
node,
index,
isLast ? 'backward' : 'forward'
)
// If the editable child found is in front of input offset, we instead seek to its end
isLast = index < offset
// If the node has children, traverse until we have a leaf node. Leaf nodes
// can be either text nodes, or other void DOM nodes.
while (isDOMElement(node) && node.childNodes.length) {
const i = isLast ? node.childNodes.length - 1 : 0
node = getEditableChild(node, i, isLast ? 'backward' : 'forward')
}
// Determine the new offset inside the text node.
offset = isLast && node.textContent != null ? node.textContent.length : 0
}
// Return the node and offset.
return [node, offset]
}
/**
* Determines whether the active element is nested within a shadowRoot
*/
export const hasShadowRoot = (node: Node | null) => {
let parent = node && node.parentNode
while (parent) {
if (parent.toString() === '[object ShadowRoot]') {
return true
}
parent = parent.parentNode
}
return false
}
/**
* Get the nearest editable child and index at `index` in a `parent`, preferring
* `direction`.
*/
export const getEditableChildAndIndex = (
parent: DOMElement,
index: number,
direction: 'forward' | 'backward'
): [DOMNode, number] => {
const { childNodes } = parent
let child = childNodes[index]
let i = index
let triedForward = false
let triedBackward = false
// While the child is a comment node, or an element node with no children,
// keep iterating to find a sibling non-void, non-comment node.
while (
isDOMComment(child) ||
(isDOMElement(child) && child.childNodes.length === 0) ||
(isDOMElement(child) && child.getAttribute('contenteditable') === 'false')
) {
if (triedForward && triedBackward) {
break
}
if (i >= childNodes.length) {
triedForward = true
i = index - 1
direction = 'backward'
continue
}
if (i < 0) {
triedBackward = true
i = index + 1
direction = 'forward'
continue
}
child = childNodes[i]
index = i
i += direction === 'forward' ? 1 : -1
}
return [child, index]
}
/**
* Get the nearest editable child at `index` in a `parent`, preferring
* `direction`.
*/
export const getEditableChild = (
parent: DOMElement,
index: number,
direction: 'forward' | 'backward'
): DOMNode => {
const [child] = getEditableChildAndIndex(parent, index, direction)
return child
}
/**
* Get a plaintext representation of the content of a node, accounting for block
* elements which get a newline appended.
*
* The domNode must be attached to the DOM.
*/
export const getPlainText = (domNode: DOMNode) => {
let text = ''
if (isDOMText(domNode) && domNode.nodeValue) {
return domNode.nodeValue
}
if (isDOMElement(domNode)) {
for (const childNode of Array.from(domNode.childNodes)) {
text += getPlainText(childNode)
}
const display = getComputedStyle(domNode).getPropertyValue('display')
if (display === 'block' || display === 'list' || domNode.tagName === 'BR') {
text += '\n'
}
}
return text
}
/**
* Get x-slate-fragment attribute from data-slate-fragment
*/
const catchSlateFragment = /data-slate-fragment="(.+?)"/m
export const getSlateFragmentAttribute = (
dataTransfer: DataTransfer
): string | void => {
const htmlData = dataTransfer.getData('text/html')
const [, fragment] = htmlData.match(catchSlateFragment) || []
return fragment
}
/**
* Get the x-slate-fragment attribute that exist in text/html data
* and append it to the DataTransfer object
*/
export const getClipboardData = (
dataTransfer: DataTransfer,
clipboardFormatKey = 'x-slate-fragment'
): DataTransfer => {
if (!dataTransfer.getData(`application/${clipboardFormatKey}`)) {
const fragment = getSlateFragmentAttribute(dataTransfer)
if (fragment) {
const clipboardData = new DataTransfer()
dataTransfer.types.forEach(type => {
clipboardData.setData(type, dataTransfer.getData(type))
})
clipboardData.setData(`application/${clipboardFormatKey}`, fragment)
return clipboardData
}
}
return dataTransfer
}
/**
* Get the dom selection from Shadow Root if possible, otherwise from the document
*/
export const getSelection = (root: Document | ShadowRoot): Selection | null => {
if (root.getSelection != null) {
return root.getSelection()
}
return document.getSelection()
}
/**
* Check whether a mutation originates from a editable element inside the editor.
*/
export const isTrackedMutation = (
editor: ReactEditor,
mutation: MutationRecord,
batch: MutationRecord[]
): boolean => {
const { target } = mutation
if (isDOMElement(target) && target.matches('[contentEditable="false"]')) {
return false
}
const { document } = ReactEditor.getWindow(editor)
if (document.contains(target)) {
return ReactEditor.hasDOMNode(editor, target, { editable: true })
}
const parentMutation = batch.find(({ addedNodes, removedNodes }) => {
for (const node of addedNodes) {
if (node === target || node.contains(target)) {
return true
}
}
for (const node of removedNodes) {
if (node === target || node.contains(target)) {
return true
}
}
})
if (!parentMutation || parentMutation === mutation) {
return false
}
// Target add/remove is tracked. Track the mutation if we track the parent mutation.
return isTrackedMutation(editor, parentMutation, batch)
}
/**
* Retrieves the deepest active element in the DOM, considering nested shadow DOMs.
*/
export const getActiveElement = () => {
let activeElement = document.activeElement
while (activeElement?.shadowRoot && activeElement.shadowRoot?.activeElement) {
activeElement = activeElement?.shadowRoot?.activeElement
}
return activeElement
}
/**
* @returns `true` if `otherNode` is before `node` in the document; otherwise, `false`.
*/
export const isBefore = (node: DOMNode, otherNode: DOMNode): boolean =>
Boolean(
node.compareDocumentPosition(otherNode) &
DOMNode.DOCUMENT_POSITION_PRECEDING
)
/**
* @returns `true` if `otherNode` is after `node` in the document; otherwise, `false`.
*/
export const isAfter = (node: DOMNode, otherNode: DOMNode): boolean =>
Boolean(
node.compareDocumentPosition(otherNode) &
DOMNode.DOCUMENT_POSITION_FOLLOWING
)

View File

@@ -1,87 +1,3 @@
import React from 'react'
export const REACT_MAJOR_VERSION = parseInt(React.version.split('.')[0], 10)
export const IS_IOS =
typeof navigator !== 'undefined' &&
typeof window !== 'undefined' &&
/iPad|iPhone|iPod/.test(navigator.userAgent) &&
!window.MSStream
export const IS_APPLE =
typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent)
export const IS_ANDROID =
typeof navigator !== 'undefined' && /Android/.test(navigator.userAgent)
export const IS_FIREFOX =
typeof navigator !== 'undefined' &&
/^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent)
export const IS_WEBKIT =
typeof navigator !== 'undefined' &&
/AppleWebKit(?!.*Chrome)/i.test(navigator.userAgent)
// "modern" Edge was released at 79.x
export const IS_EDGE_LEGACY =
typeof navigator !== 'undefined' &&
/Edge?\/(?:[0-6][0-9]|[0-7][0-8])(?:\.)/i.test(navigator.userAgent)
export const IS_CHROME =
typeof navigator !== 'undefined' && /Chrome/i.test(navigator.userAgent)
// Native `beforeInput` events don't work well with react on Chrome 75
// and older, Chrome 76+ can use `beforeInput` though.
export const IS_CHROME_LEGACY =
typeof navigator !== 'undefined' &&
/Chrome?\/(?:[0-7][0-5]|[0-6][0-9])(?:\.)/i.test(navigator.userAgent)
export const IS_ANDROID_CHROME_LEGACY =
IS_ANDROID &&
typeof navigator !== 'undefined' &&
/Chrome?\/(?:[0-5]?\d)(?:\.)/i.test(navigator.userAgent)
// Firefox did not support `beforeInput` until `v87`.
export const IS_FIREFOX_LEGACY =
typeof navigator !== 'undefined' &&
/^(?!.*Seamonkey)(?=.*Firefox\/(?:[0-7][0-9]|[0-8][0-6])(?:\.)).*/i.test(
navigator.userAgent
)
// UC mobile browser
export const IS_UC_MOBILE =
typeof navigator !== 'undefined' && /.*UCBrowser/.test(navigator.userAgent)
// Wechat browser (not including mac wechat)
export const IS_WECHATBROWSER =
typeof navigator !== 'undefined' &&
/.*Wechat/.test(navigator.userAgent) &&
!/.*MacWechat/.test(navigator.userAgent) // avoid lookbehind (buggy in safari < 16.4)
// Check if DOM is available as React does internally.
// https://github.com/facebook/react/blob/master/packages/shared/ExecutionEnvironment.js
export const CAN_USE_DOM = !!(
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'
)
// Check if the browser is Safari and older than 17
export const IS_SAFARI_LEGACY =
typeof navigator !== 'undefined' &&
/Safari/.test(navigator.userAgent) &&
/Version\/(\d+)/.test(navigator.userAgent) &&
(navigator.userAgent.match(/Version\/(\d+)/)?.[1]
? parseInt(navigator.userAgent.match(/Version\/(\d+)/)?.[1]!, 10) < 17
: false)
// COMPAT: Firefox/Edge Legacy don't support the `beforeinput` event
// Chrome Legacy doesn't support `beforeinput` correctly
export const HAS_BEFORE_INPUT_SUPPORT =
(!IS_CHROME_LEGACY || !IS_ANDROID_CHROME_LEGACY) &&
!IS_EDGE_LEGACY &&
// globalThis is undefined in older browsers
typeof globalThis !== 'undefined' &&
globalThis.InputEvent &&
// @ts-ignore The `getTargetRanges` property isn't recognized.
typeof globalThis.InputEvent.prototype.getTargetRanges === 'function'

View File

@@ -1,97 +0,0 @@
import { isHotkey } from 'is-hotkey'
import { IS_APPLE } from './environment'
/**
* Hotkey mappings for each platform.
*/
const HOTKEYS = {
bold: 'mod+b',
compose: ['down', 'left', 'right', 'up', 'backspace', 'enter'],
moveBackward: 'left',
moveForward: 'right',
moveWordBackward: 'ctrl+left',
moveWordForward: 'ctrl+right',
deleteBackward: 'shift?+backspace',
deleteForward: 'shift?+delete',
extendBackward: 'shift+left',
extendForward: 'shift+right',
italic: 'mod+i',
insertSoftBreak: 'shift+enter',
splitBlock: 'enter',
undo: 'mod+z',
}
const APPLE_HOTKEYS = {
moveLineBackward: 'opt+up',
moveLineForward: 'opt+down',
moveWordBackward: 'opt+left',
moveWordForward: 'opt+right',
deleteBackward: ['ctrl+backspace', 'ctrl+h'],
deleteForward: ['ctrl+delete', 'ctrl+d'],
deleteLineBackward: 'cmd+shift?+backspace',
deleteLineForward: ['cmd+shift?+delete', 'ctrl+k'],
deleteWordBackward: 'opt+shift?+backspace',
deleteWordForward: 'opt+shift?+delete',
extendLineBackward: 'opt+shift+up',
extendLineForward: 'opt+shift+down',
redo: 'cmd+shift+z',
transposeCharacter: 'ctrl+t',
}
const WINDOWS_HOTKEYS = {
deleteWordBackward: 'ctrl+shift?+backspace',
deleteWordForward: 'ctrl+shift?+delete',
redo: ['ctrl+y', 'ctrl+shift+z'],
}
/**
* Create a platform-aware hotkey checker.
*/
const create = (key: string) => {
const generic = HOTKEYS[<keyof typeof HOTKEYS>key]
const apple = APPLE_HOTKEYS[<keyof typeof APPLE_HOTKEYS>key]
const windows = WINDOWS_HOTKEYS[<keyof typeof WINDOWS_HOTKEYS>key]
const isGeneric = generic && isHotkey(generic)
const isApple = apple && isHotkey(apple)
const isWindows = windows && isHotkey(windows)
return (event: KeyboardEvent) => {
if (isGeneric && isGeneric(event)) return true
if (IS_APPLE && isApple && isApple(event)) return true
if (!IS_APPLE && isWindows && isWindows(event)) return true
return false
}
}
/**
* Hotkeys.
*/
export default {
isBold: create('bold'),
isCompose: create('compose'),
isMoveBackward: create('moveBackward'),
isMoveForward: create('moveForward'),
isDeleteBackward: create('deleteBackward'),
isDeleteForward: create('deleteForward'),
isDeleteLineBackward: create('deleteLineBackward'),
isDeleteLineForward: create('deleteLineForward'),
isDeleteWordBackward: create('deleteWordBackward'),
isDeleteWordForward: create('deleteWordForward'),
isExtendBackward: create('extendBackward'),
isExtendForward: create('extendForward'),
isExtendLineBackward: create('extendLineBackward'),
isExtendLineForward: create('extendLineForward'),
isItalic: create('italic'),
isMoveLineBackward: create('moveLineBackward'),
isMoveLineForward: create('moveLineForward'),
isMoveWordBackward: create('moveWordBackward'),
isMoveWordForward: create('moveWordForward'),
isRedo: create('redo'),
isSoftBreak: create('insertSoftBreak'),
isSplitBlock: create('splitBlock'),
isTransposeCharacter: create('transposeCharacter'),
isUndo: create('undo'),
}

View File

@@ -1,18 +0,0 @@
/**
* An auto-incrementing identifier for keys.
*/
let n = 0
/**
* A class that keeps track of a key string. We use a full class here because we
* want to be able to use them as keys in `WeakMap` objects.
*/
export class Key {
id: string
constructor() {
this.id = `${n++}`
}
}

View File

@@ -1,79 +0,0 @@
/**
* Utilities for single-line deletion
*/
import { Editor, Range } from 'slate'
import { ReactEditor } from '../plugin/react-editor'
const doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => {
const middle = (compareRect.top + compareRect.bottom) / 2
return rect.top <= middle && rect.bottom >= middle
}
const areRangesSameLine = (
editor: ReactEditor,
range1: Range,
range2: Range
) => {
const rect1 = ReactEditor.toDOMRange(editor, range1).getBoundingClientRect()
const rect2 = ReactEditor.toDOMRange(editor, range2).getBoundingClientRect()
return doRectsIntersect(rect1, rect2) && doRectsIntersect(rect2, rect1)
}
/**
* A helper utility that returns the end portion of a `Range`
* which is located on a single line.
*
* @param {Editor} editor The editor object to compare against
* @param {Range} parentRange The parent range to compare against
* @returns {Range} A valid portion of the parentRange which is one a single line
*/
export const findCurrentLineRange = (
editor: ReactEditor,
parentRange: Range
): Range => {
const parentRangeBoundary = Editor.range(editor, Range.end(parentRange))
const positions = Array.from(Editor.positions(editor, { at: parentRange }))
let left = 0
let right = positions.length
let middle = Math.floor(right / 2)
if (
areRangesSameLine(
editor,
Editor.range(editor, positions[left]),
parentRangeBoundary
)
) {
return Editor.range(editor, positions[left], parentRangeBoundary)
}
if (positions.length < 2) {
return Editor.range(
editor,
positions[positions.length - 1],
parentRangeBoundary
)
}
while (middle !== positions.length && middle !== left) {
if (
areRangesSameLine(
editor,
Editor.range(editor, positions[middle]),
parentRangeBoundary
)
) {
right = middle
} else {
left = middle
}
middle = Math.floor((left + right) / 2)
}
return Editor.range(editor, positions[right], parentRangeBoundary)
}

View File

@@ -1,82 +0,0 @@
import { Range } from 'slate'
import { PLACEHOLDER_SYMBOL } from './weak-maps'
export const shallowCompare = (
obj1: { [key: string]: unknown },
obj2: { [key: string]: unknown }
) =>
Object.keys(obj1).length === Object.keys(obj2).length &&
Object.keys(obj1).every(
key => obj2.hasOwnProperty(key) && obj1[key] === obj2[key]
)
const isDecorationFlagsEqual = (range: Range, other: Range) => {
const { anchor: rangeAnchor, focus: rangeFocus, ...rangeOwnProps } = range
const { anchor: otherAnchor, focus: otherFocus, ...otherOwnProps } = other
return (
range[PLACEHOLDER_SYMBOL] === other[PLACEHOLDER_SYMBOL] &&
shallowCompare(rangeOwnProps, otherOwnProps)
)
}
/**
* Check if a list of decorator ranges are equal to another.
*
* PERF: this requires the two lists to also have the ranges inside them in the
* same order, but this is an okay constraint for us since decorations are
* kept in order, and the odd case where they aren't is okay to re-render for.
*/
export const isElementDecorationsEqual = (
list: Range[],
another: Range[]
): boolean => {
if (list.length !== another.length) {
return false
}
for (let i = 0; i < list.length; i++) {
const range = list[i]
const other = another[i]
if (!Range.equals(range, other) || !isDecorationFlagsEqual(range, other)) {
return false
}
}
return true
}
/**
* Check if a list of decorator ranges are equal to another.
*
* PERF: this requires the two lists to also have the ranges inside them in the
* same order, but this is an okay constraint for us since decorations are
* kept in order, and the odd case where they aren't is okay to re-render for.
*/
export const isTextDecorationsEqual = (
list: Range[],
another: Range[]
): boolean => {
if (list.length !== another.length) {
return false
}
for (let i = 0; i < list.length; i++) {
const range = list[i]
const other = another[i]
// compare only offsets because paths doesn't matter for text
if (
range.anchor.offset !== other.anchor.offset ||
range.focus.offset !== other.focus.offset ||
!isDecorationFlagsEqual(range, other)
) {
return false
}
}
return true
}

View File

@@ -1,3 +0,0 @@
export type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => infer R
? (...args: P) => R
: never

View File

@@ -1,88 +0,0 @@
import { Ancestor, Editor, Node, Operation, Range, RangeRef, Text } from 'slate'
import { Action } from '../hooks/android-input-manager/android-input-manager'
import { TextDiff } from './diff-text'
import { Key } from './key'
/**
* Two weak maps that allow us rebuild a path given a node. They are populated
* at render time such that after a render occurs we can always backtrack.
*/
export const IS_NODE_MAP_DIRTY: WeakMap<Editor, boolean> = new WeakMap()
export const NODE_TO_INDEX: WeakMap<Node, number> = new WeakMap()
export const NODE_TO_PARENT: WeakMap<Node, Ancestor> = new WeakMap()
/**
* Weak maps that allow us to go between Slate nodes and DOM nodes. These
* are used to resolve DOM event-related logic into Slate actions.
*/
export const EDITOR_TO_WINDOW: WeakMap<Editor, Window> = new WeakMap()
export const EDITOR_TO_ELEMENT: WeakMap<Editor, HTMLElement> = new WeakMap()
export const EDITOR_TO_PLACEHOLDER: WeakMap<Editor, string> = new WeakMap()
export const EDITOR_TO_PLACEHOLDER_ELEMENT: WeakMap<Editor, HTMLElement> =
new WeakMap()
export const ELEMENT_TO_NODE: WeakMap<HTMLElement, Node> = new WeakMap()
export const NODE_TO_ELEMENT: WeakMap<Node, HTMLElement> = new WeakMap()
export const NODE_TO_KEY: WeakMap<Node, Key> = new WeakMap()
export const EDITOR_TO_KEY_TO_ELEMENT: WeakMap<
Editor,
WeakMap<Key, HTMLElement>
> = new WeakMap()
/**
* Weak maps for storing editor-related state.
*/
export const IS_READ_ONLY: WeakMap<Editor, boolean> = new WeakMap()
export const IS_FOCUSED: WeakMap<Editor, boolean> = new WeakMap()
export const IS_COMPOSING: WeakMap<Editor, boolean> = new WeakMap()
export const EDITOR_TO_USER_SELECTION: WeakMap<Editor, RangeRef | null> =
new WeakMap()
/**
* Weak map for associating the context `onChange` context with the plugin.
*/
export const EDITOR_TO_ON_CHANGE = new WeakMap<
Editor,
(options?: { operation?: Operation }) => void
>()
/**
* Weak maps for saving pending state on composition stage.
*/
export const EDITOR_TO_SCHEDULE_FLUSH: WeakMap<Editor, () => void> =
new WeakMap()
export const EDITOR_TO_PENDING_INSERTION_MARKS: WeakMap<
Editor,
Partial<Text> | null
> = new WeakMap()
export const EDITOR_TO_USER_MARKS: WeakMap<Editor, Partial<Text> | null> =
new WeakMap()
/**
* Android input handling specific weak-maps
*/
export const EDITOR_TO_PENDING_DIFFS: WeakMap<Editor, TextDiff[]> =
new WeakMap()
export const EDITOR_TO_PENDING_ACTION: WeakMap<Editor, Action | null> =
new WeakMap()
export const EDITOR_TO_PENDING_SELECTION: WeakMap<Editor, Range | null> =
new WeakMap()
export const EDITOR_TO_FORCE_RENDER: WeakMap<Editor, () => void> = new WeakMap()
/**
* Symbols.
*/
export const PLACEHOLDER_SYMBOL = Symbol('placeholder') as unknown as string
export const MARK_PLACEHOLDER_SYMBOL = Symbol(
'mark-placeholder'
) as unknown as string

View File

@@ -5,5 +5,12 @@
"rootDir": "./src",
"outDir": "./lib"
},
"references": [{ "path": "../slate" }]
"references": [
{
"path": "../slate"
},
{
"path": "../slate-dom"
}
]
}