mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-01-16 13:09:31 +01: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:
parent
7e1608018b
commit
9a21251270
6
.changeset/brown-suns-smile.md
Normal file
6
.changeset/brown-suns-smile.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
'slate-react': minor
|
||||
'slate-dom': minor
|
||||
---
|
||||
|
||||
Split out slate-dom package
|
@ -12,6 +12,7 @@ import { startCase } from 'lodash'
|
||||
import Core from '../../packages/slate/package.json'
|
||||
import History from '../../packages/slate-history/package.json'
|
||||
import Hyperscript from '../../packages/slate-hyperscript/package.json'
|
||||
import DOM from '../../packages/slate-dom/package.json'
|
||||
import React from '../../packages/slate-react/package.json'
|
||||
|
||||
/**
|
||||
@ -203,5 +204,6 @@ export default [
|
||||
...factory(Core),
|
||||
...factory(History),
|
||||
...factory(Hyperscript),
|
||||
...factory(DOM),
|
||||
...factory(React),
|
||||
]
|
||||
|
64
packages/slate-dom/package.json
Normal file
64
packages/slate-dom/package.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "slate-dom",
|
||||
"description": "Tools for building completely customizable richtext editors with React.",
|
||||
"version": "0.110.2",
|
||||
"license": "MIT",
|
||||
"repository": "git://github.com/ianstormtaylor/slate.git",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"umd": "dist/slate-dom.js",
|
||||
"umdMin": "dist/slate-dom.min.js",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"dependencies": {
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"direction": "^1.0.4",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"scroll-into-view-if-needed": "^3.1.0",
|
||||
"tiny-invariant": "1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"@types/is-hotkey": "^0.1.8",
|
||||
"@types/jest": "29.5.6",
|
||||
"@types/jsdom": "^21.1.4",
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@types/resize-observer-browser": "^0.1.8",
|
||||
"slate": "^0.110.2",
|
||||
"slate-hyperscript": "^0.100.0",
|
||||
"source-map-loader": "^4.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"slate": ">=0.99.0"
|
||||
},
|
||||
"umdGlobals": {
|
||||
"slate": "Slate"
|
||||
},
|
||||
"keywords": [
|
||||
"canvas",
|
||||
"contenteditable",
|
||||
"docs",
|
||||
"document",
|
||||
"edit",
|
||||
"editor",
|
||||
"editable",
|
||||
"html",
|
||||
"immutable",
|
||||
"markdown",
|
||||
"medium",
|
||||
"paper",
|
||||
"react",
|
||||
"rich",
|
||||
"richtext",
|
||||
"richtext",
|
||||
"slate",
|
||||
"text",
|
||||
"wysiwyg",
|
||||
"wysiwym"
|
||||
]
|
||||
}
|
45
packages/slate-dom/src/custom-types.ts
Normal file
45
packages/slate-dom/src/custom-types.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { BaseRange, BaseText } from 'slate'
|
||||
import { DOMEditor } from './plugin/dom-editor'
|
||||
|
||||
declare module 'slate' {
|
||||
interface CustomTypes {
|
||||
Editor: DOMEditor
|
||||
Text: BaseText & {
|
||||
placeholder?: string
|
||||
onPlaceholderResize?: (node: HTMLElement | null) => void
|
||||
// FIXME: is unknown correct here?
|
||||
[key: string]: unknown
|
||||
}
|
||||
Range: BaseRange & {
|
||||
placeholder?: string
|
||||
onPlaceholderResize?: (node: HTMLElement | null) => void
|
||||
// FIXME: is unknown correct here?
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
MSStream: boolean
|
||||
}
|
||||
interface DocumentOrShadowRoot {
|
||||
getSelection(): Selection | null
|
||||
}
|
||||
|
||||
interface CaretPosition {
|
||||
readonly offsetNode: Node
|
||||
readonly offset: number
|
||||
getClientRect(): DOMRect | null
|
||||
}
|
||||
|
||||
interface Document {
|
||||
caretPositionFromPoint(x: number, y: number): CaretPosition | null
|
||||
}
|
||||
|
||||
interface Node {
|
||||
getRootNode(options?: GetRootNodeOptions): Document | ShadowRoot
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
89
packages/slate-dom/src/index.ts
Normal file
89
packages/slate-dom/src/index.ts
Normal file
@ -0,0 +1,89 @@
|
||||
// Plugin
|
||||
export { DOMEditor, type DOMEditorInterface } from './plugin/dom-editor'
|
||||
export { withDOM } from './plugin/with-dom'
|
||||
|
||||
// Utils
|
||||
export { TRIPLE_CLICK } from './utils/constants'
|
||||
|
||||
export {
|
||||
applyStringDiff,
|
||||
mergeStringDiffs,
|
||||
normalizePoint,
|
||||
normalizeRange,
|
||||
normalizeStringDiff,
|
||||
StringDiff,
|
||||
targetRange,
|
||||
TextDiff,
|
||||
verifyDiffState,
|
||||
} from './utils/diff-text'
|
||||
|
||||
export {
|
||||
DOMElement,
|
||||
DOMNode,
|
||||
DOMPoint,
|
||||
DOMRange,
|
||||
DOMSelection,
|
||||
DOMStaticRange,
|
||||
DOMText,
|
||||
getActiveElement,
|
||||
getDefaultView,
|
||||
getSelection,
|
||||
hasShadowRoot,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isDOMElement,
|
||||
isDOMNode,
|
||||
isDOMSelection,
|
||||
isPlainTextOnlyPaste,
|
||||
isTrackedMutation,
|
||||
normalizeDOMPoint,
|
||||
} from './utils/dom'
|
||||
|
||||
export {
|
||||
CAN_USE_DOM,
|
||||
HAS_BEFORE_INPUT_SUPPORT,
|
||||
IS_ANDROID,
|
||||
IS_CHROME,
|
||||
IS_FIREFOX,
|
||||
IS_FIREFOX_LEGACY,
|
||||
IS_IOS,
|
||||
IS_WEBKIT,
|
||||
IS_UC_MOBILE,
|
||||
IS_WECHATBROWSER,
|
||||
} from './utils/environment'
|
||||
|
||||
export { default as Hotkeys } from './utils/hotkeys'
|
||||
|
||||
export { Key } from './utils/key'
|
||||
|
||||
export {
|
||||
isElementDecorationsEqual,
|
||||
isTextDecorationsEqual,
|
||||
} from './utils/range-list'
|
||||
|
||||
export {
|
||||
EDITOR_TO_ELEMENT,
|
||||
EDITOR_TO_FORCE_RENDER,
|
||||
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_PLACEHOLDER_ELEMENT,
|
||||
EDITOR_TO_SCHEDULE_FLUSH,
|
||||
EDITOR_TO_USER_MARKS,
|
||||
EDITOR_TO_USER_SELECTION,
|
||||
EDITOR_TO_WINDOW,
|
||||
ELEMENT_TO_NODE,
|
||||
IS_COMPOSING,
|
||||
IS_FOCUSED,
|
||||
IS_NODE_MAP_DIRTY,
|
||||
IS_READ_ONLY,
|
||||
MARK_PLACEHOLDER_SYMBOL,
|
||||
NODE_TO_ELEMENT,
|
||||
NODE_TO_INDEX,
|
||||
NODE_TO_KEY,
|
||||
NODE_TO_PARENT,
|
||||
PLACEHOLDER_SYMBOL,
|
||||
} from './utils/weak-maps'
|
1075
packages/slate-dom/src/plugin/dom-editor.ts
Normal file
1075
packages/slate-dom/src/plugin/dom-editor.ts
Normal file
File diff suppressed because it is too large
Load Diff
382
packages/slate-dom/src/plugin/with-dom.ts
Normal file
382
packages/slate-dom/src/plugin/with-dom.ts
Normal file
@ -0,0 +1,382 @@
|
||||
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 { DOMEditor } from './dom-editor'
|
||||
|
||||
/**
|
||||
* `withDOM` adds DOM specific behaviors to the editor.
|
||||
*
|
||||
* If you are using TypeScript, you must extend Slate's CustomTypes to use
|
||||
* this plugin.
|
||||
*
|
||||
* See https://docs.slatejs.org/concepts/11-typescript to learn how.
|
||||
*/
|
||||
|
||||
export const withDOM = <T extends BaseEditor>(
|
||||
editor: T,
|
||||
clipboardFormatKey = 'x-slate-fragment'
|
||||
): T & DOMEditor => {
|
||||
const e = editor as T & DOMEditor
|
||||
const { apply, onChange, deleteBackward, addMark, removeMark } = e
|
||||
|
||||
// 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.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 = DOMEditor.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 = DOMEditor.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 = DOMEditor.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
|
||||
}
|
||||
|
||||
e.onChange = options => {
|
||||
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 = DOMEditor.findKey(e, n)
|
||||
matches.push([p, key])
|
||||
}
|
||||
return matches
|
||||
}
|
@ -12,7 +12,7 @@ import DOMText = globalThis.Text
|
||||
import DOMRange = globalThis.Range
|
||||
import DOMSelection = globalThis.Selection
|
||||
import DOMStaticRange = globalThis.StaticRange
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
import { DOMEditor } from '../plugin/dom-editor'
|
||||
|
||||
export {
|
||||
DOMNode,
|
||||
@ -289,7 +289,7 @@ export const getSelection = (root: Document | ShadowRoot): Selection | null => {
|
||||
*/
|
||||
|
||||
export const isTrackedMutation = (
|
||||
editor: ReactEditor,
|
||||
editor: DOMEditor,
|
||||
mutation: MutationRecord,
|
||||
batch: MutationRecord[]
|
||||
): boolean => {
|
||||
@ -298,9 +298,9 @@ export const isTrackedMutation = (
|
||||
return false
|
||||
}
|
||||
|
||||
const { document } = ReactEditor.getWindow(editor)
|
||||
const { document } = DOMEditor.getWindow(editor)
|
||||
if (document.contains(target)) {
|
||||
return ReactEditor.hasDOMNode(editor, target, { editable: true })
|
||||
return DOMEditor.hasDOMNode(editor, target, { editable: true })
|
||||
}
|
||||
|
||||
const parentMutation = batch.find(({ addedNodes, removedNodes }) => {
|
83
packages/slate-dom/src/utils/environment.ts
Normal file
83
packages/slate-dom/src/utils/environment.ts
Normal file
@ -0,0 +1,83 @@
|
||||
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'
|
@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { Editor, Range } from 'slate'
|
||||
import { ReactEditor } from '../plugin/react-editor'
|
||||
import { DOMEditor } from '../plugin/dom-editor'
|
||||
|
||||
const doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => {
|
||||
const middle = (compareRect.top + compareRect.bottom) / 2
|
||||
@ -11,13 +11,9 @@ const doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => {
|
||||
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()
|
||||
const areRangesSameLine = (editor: DOMEditor, range1: Range, range2: Range) => {
|
||||
const rect1 = DOMEditor.toDOMRange(editor, range1).getBoundingClientRect()
|
||||
const rect2 = DOMEditor.toDOMRange(editor, range2).getBoundingClientRect()
|
||||
|
||||
return doRectsIntersect(rect1, rect2) && doRectsIntersect(rect2, rect1)
|
||||
}
|
||||
@ -31,7 +27,7 @@ const areRangesSameLine = (
|
||||
* @returns {Range} A valid portion of the parentRange which is one a single line
|
||||
*/
|
||||
export const findCurrentLineRange = (
|
||||
editor: ReactEditor,
|
||||
editor: DOMEditor,
|
||||
parentRange: Range
|
||||
): Range => {
|
||||
const parentRangeBoundary = Editor.range(editor, Range.end(parentRange))
|
@ -1,8 +1,18 @@
|
||||
import { Ancestor, Editor, Node, Operation, Range, RangeRef, Text } from 'slate'
|
||||
import { Action } from '../hooks/android-input-manager/android-input-manager'
|
||||
import {
|
||||
Ancestor,
|
||||
Editor,
|
||||
Node,
|
||||
Operation,
|
||||
Point,
|
||||
Range,
|
||||
RangeRef,
|
||||
Text,
|
||||
} from 'slate'
|
||||
import { TextDiff } from './diff-text'
|
||||
import { Key } from './key'
|
||||
|
||||
export type Action = { at?: Point | Range; run: () => void }
|
||||
|
||||
/**
|
||||
* 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.
|
9
packages/slate-dom/tsconfig.json
Normal file
9
packages/slate-dom/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../config/typescript/tsconfig.json",
|
||||
"include": ["src/**/*"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib"
|
||||
},
|
||||
"references": [{ "path": "../slate" }]
|
||||
}
|
@ -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,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'
|
||||
|
@ -5,5 +5,12 @@
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib"
|
||||
},
|
||||
"references": [{ "path": "../slate" }]
|
||||
"references": [
|
||||
{
|
||||
"path": "../slate"
|
||||
},
|
||||
{
|
||||
"path": "../slate-dom"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
27
yarn.lock
27
yarn.lock
@ -13258,6 +13258,31 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slate-dom@npm:^0.110.2, slate-dom@workspace:packages/slate-dom":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "slate-dom@workspace:packages/slate-dom"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.23.2"
|
||||
"@juggle/resize-observer": "npm:^3.4.0"
|
||||
"@types/is-hotkey": "npm:^0.1.8"
|
||||
"@types/jest": "npm:29.5.6"
|
||||
"@types/jsdom": "npm:^21.1.4"
|
||||
"@types/lodash": "npm:^4.14.200"
|
||||
"@types/resize-observer-browser": "npm:^0.1.8"
|
||||
direction: "npm:^1.0.4"
|
||||
is-hotkey: "npm:^0.2.0"
|
||||
is-plain-object: "npm:^5.0.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
scroll-into-view-if-needed: "npm:^3.1.0"
|
||||
slate: "npm:^0.110.2"
|
||||
slate-hyperscript: "npm:^0.100.0"
|
||||
source-map-loader: "npm:^4.0.1"
|
||||
tiny-invariant: "npm:1.3.1"
|
||||
peerDependencies:
|
||||
slate: ">=0.99.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"slate-history@workspace:*, slate-history@workspace:packages/slate-history":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "slate-history@workspace:packages/slate-history"
|
||||
@ -13391,6 +13416,7 @@ __metadata:
|
||||
react-dom: "npm:^18.2.0"
|
||||
scroll-into-view-if-needed: "npm:^3.1.0"
|
||||
slate: "npm:^0.110.2"
|
||||
slate-dom: "npm:^0.110.2"
|
||||
slate-hyperscript: "npm:^0.100.0"
|
||||
source-map-loader: "npm:^4.0.1"
|
||||
tiny-invariant: "npm:1.3.1"
|
||||
@ -13398,6 +13424,7 @@ __metadata:
|
||||
react: ">=18.2.0"
|
||||
react-dom: ">=18.2.0"
|
||||
slate: ">=0.99.0"
|
||||
slate-dom: ">=0.110.2"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user