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:
@@ -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",
|
||||
|
@@ -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'
|
||||
|
@@ -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,
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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'
|
||||
|
||||
|
@@ -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 }
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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'
|
||||
|
@@ -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
|
||||
|
@@ -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
@@ -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
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
export const TRIPLE_CLICK = 3
|
@@ -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,
|
||||
}
|
||||
}
|
@@ -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
|
||||
)
|
@@ -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'
|
||||
|
@@ -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'),
|
||||
}
|
@@ -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++}`
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
export type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => infer R
|
||||
? (...args: P) => R
|
||||
: never
|
@@ -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
|
@@ -5,5 +5,12 @@
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib"
|
||||
},
|
||||
"references": [{ "path": "../slate" }]
|
||||
"references": [
|
||||
{
|
||||
"path": "../slate"
|
||||
},
|
||||
{
|
||||
"path": "../slate-dom"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user