mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-01-16 21:18:39 +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 Core from '../../packages/slate/package.json'
|
||||||
import History from '../../packages/slate-history/package.json'
|
import History from '../../packages/slate-history/package.json'
|
||||||
import Hyperscript from '../../packages/slate-hyperscript/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'
|
import React from '../../packages/slate-react/package.json'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -203,5 +204,6 @@ export default [
|
|||||||
...factory(Core),
|
...factory(Core),
|
||||||
...factory(History),
|
...factory(History),
|
||||||
...factory(Hyperscript),
|
...factory(Hyperscript),
|
||||||
|
...factory(DOM),
|
||||||
...factory(React),
|
...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 DOMRange = globalThis.Range
|
||||||
import DOMSelection = globalThis.Selection
|
import DOMSelection = globalThis.Selection
|
||||||
import DOMStaticRange = globalThis.StaticRange
|
import DOMStaticRange = globalThis.StaticRange
|
||||||
import { ReactEditor } from '../plugin/react-editor'
|
import { DOMEditor } from '../plugin/dom-editor'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DOMNode,
|
DOMNode,
|
||||||
@ -289,7 +289,7 @@ export const getSelection = (root: Document | ShadowRoot): Selection | null => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const isTrackedMutation = (
|
export const isTrackedMutation = (
|
||||||
editor: ReactEditor,
|
editor: DOMEditor,
|
||||||
mutation: MutationRecord,
|
mutation: MutationRecord,
|
||||||
batch: MutationRecord[]
|
batch: MutationRecord[]
|
||||||
): boolean => {
|
): boolean => {
|
||||||
@ -298,9 +298,9 @@ export const isTrackedMutation = (
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const { document } = ReactEditor.getWindow(editor)
|
const { document } = DOMEditor.getWindow(editor)
|
||||||
if (document.contains(target)) {
|
if (document.contains(target)) {
|
||||||
return ReactEditor.hasDOMNode(editor, target, { editable: true })
|
return DOMEditor.hasDOMNode(editor, target, { editable: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentMutation = batch.find(({ addedNodes, removedNodes }) => {
|
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 { Editor, Range } from 'slate'
|
||||||
import { ReactEditor } from '../plugin/react-editor'
|
import { DOMEditor } from '../plugin/dom-editor'
|
||||||
|
|
||||||
const doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => {
|
const doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => {
|
||||||
const middle = (compareRect.top + compareRect.bottom) / 2
|
const middle = (compareRect.top + compareRect.bottom) / 2
|
||||||
@ -11,13 +11,9 @@ const doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => {
|
|||||||
return rect.top <= middle && rect.bottom >= middle
|
return rect.top <= middle && rect.bottom >= middle
|
||||||
}
|
}
|
||||||
|
|
||||||
const areRangesSameLine = (
|
const areRangesSameLine = (editor: DOMEditor, range1: Range, range2: Range) => {
|
||||||
editor: ReactEditor,
|
const rect1 = DOMEditor.toDOMRange(editor, range1).getBoundingClientRect()
|
||||||
range1: Range,
|
const rect2 = DOMEditor.toDOMRange(editor, range2).getBoundingClientRect()
|
||||||
range2: Range
|
|
||||||
) => {
|
|
||||||
const rect1 = ReactEditor.toDOMRange(editor, range1).getBoundingClientRect()
|
|
||||||
const rect2 = ReactEditor.toDOMRange(editor, range2).getBoundingClientRect()
|
|
||||||
|
|
||||||
return doRectsIntersect(rect1, rect2) && doRectsIntersect(rect2, rect1)
|
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
|
* @returns {Range} A valid portion of the parentRange which is one a single line
|
||||||
*/
|
*/
|
||||||
export const findCurrentLineRange = (
|
export const findCurrentLineRange = (
|
||||||
editor: ReactEditor,
|
editor: DOMEditor,
|
||||||
parentRange: Range
|
parentRange: Range
|
||||||
): Range => {
|
): Range => {
|
||||||
const parentRangeBoundary = Editor.range(editor, Range.end(parentRange))
|
const parentRangeBoundary = Editor.range(editor, Range.end(parentRange))
|
@ -1,8 +1,18 @@
|
|||||||
import { Ancestor, Editor, Node, Operation, Range, RangeRef, Text } from 'slate'
|
import {
|
||||||
import { Action } from '../hooks/android-input-manager/android-input-manager'
|
Ancestor,
|
||||||
|
Editor,
|
||||||
|
Node,
|
||||||
|
Operation,
|
||||||
|
Point,
|
||||||
|
Range,
|
||||||
|
RangeRef,
|
||||||
|
Text,
|
||||||
|
} from 'slate'
|
||||||
import { TextDiff } from './diff-text'
|
import { TextDiff } from './diff-text'
|
||||||
import { Key } from './key'
|
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
|
* 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.
|
* 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": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"slate": "^0.110.2",
|
"slate": "^0.110.2",
|
||||||
|
"slate-dom": "^0.110.2",
|
||||||
"slate-hyperscript": "^0.100.0",
|
"slate-hyperscript": "^0.100.0",
|
||||||
"source-map-loader": "^4.0.1"
|
"source-map-loader": "^4.0.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18.2.0",
|
"react": ">=18.2.0",
|
||||||
"react-dom": ">=18.2.0",
|
"react-dom": ">=18.2.0",
|
||||||
"slate": ">=0.99.0"
|
"slate": ">=0.99.0",
|
||||||
|
"slate-dom": ">=0.110.2"
|
||||||
},
|
},
|
||||||
"umdGlobals": {
|
"umdGlobals": {
|
||||||
"react": "React",
|
"react": "React",
|
||||||
|
@ -31,7 +31,7 @@ import { ReadOnlyContext } from '../hooks/use-read-only'
|
|||||||
import { useSlate } from '../hooks/use-slate'
|
import { useSlate } from '../hooks/use-slate'
|
||||||
import { useTrackUserInput } from '../hooks/use-track-user-input'
|
import { useTrackUserInput } from '../hooks/use-track-user-input'
|
||||||
import { ReactEditor } from '../plugin/react-editor'
|
import { ReactEditor } from '../plugin/react-editor'
|
||||||
import { TRIPLE_CLICK } from '../utils/constants'
|
import { TRIPLE_CLICK } from 'slate-dom'
|
||||||
import {
|
import {
|
||||||
DOMElement,
|
DOMElement,
|
||||||
DOMRange,
|
DOMRange,
|
||||||
@ -42,7 +42,7 @@ import {
|
|||||||
isDOMElement,
|
isDOMElement,
|
||||||
isDOMNode,
|
isDOMNode,
|
||||||
isPlainTextOnlyPaste,
|
isPlainTextOnlyPaste,
|
||||||
} from '../utils/dom'
|
} from 'slate-dom'
|
||||||
import {
|
import {
|
||||||
CAN_USE_DOM,
|
CAN_USE_DOM,
|
||||||
HAS_BEFORE_INPUT_SUPPORT,
|
HAS_BEFORE_INPUT_SUPPORT,
|
||||||
@ -54,8 +54,8 @@ import {
|
|||||||
IS_WEBKIT,
|
IS_WEBKIT,
|
||||||
IS_UC_MOBILE,
|
IS_UC_MOBILE,
|
||||||
IS_WECHATBROWSER,
|
IS_WECHATBROWSER,
|
||||||
} from '../utils/environment'
|
} from 'slate-dom'
|
||||||
import Hotkeys from '../utils/hotkeys'
|
import { Hotkeys } from 'slate-dom'
|
||||||
import {
|
import {
|
||||||
IS_NODE_MAP_DIRTY,
|
IS_NODE_MAP_DIRTY,
|
||||||
EDITOR_TO_ELEMENT,
|
EDITOR_TO_ELEMENT,
|
||||||
@ -71,7 +71,7 @@ import {
|
|||||||
MARK_PLACEHOLDER_SYMBOL,
|
MARK_PLACEHOLDER_SYMBOL,
|
||||||
NODE_TO_ELEMENT,
|
NODE_TO_ELEMENT,
|
||||||
PLACEHOLDER_SYMBOL,
|
PLACEHOLDER_SYMBOL,
|
||||||
} from '../utils/weak-maps'
|
} from 'slate-dom'
|
||||||
import { RestoreDOM } from './restore-dom/restore-dom'
|
import { RestoreDOM } from './restore-dom/restore-dom'
|
||||||
import { AndroidInputManager } from '../hooks/android-input-manager/android-input-manager'
|
import { AndroidInputManager } from '../hooks/android-input-manager/android-input-manager'
|
||||||
import { ComposingContext } from '../hooks/use-composing'
|
import { ComposingContext } from '../hooks/use-composing'
|
||||||
|
@ -4,14 +4,14 @@ import { JSX } from 'react'
|
|||||||
import { Editor, Element as SlateElement, Node, Range } from 'slate'
|
import { Editor, Element as SlateElement, Node, Range } from 'slate'
|
||||||
import { ReactEditor, useReadOnly, useSlateStatic } from '..'
|
import { ReactEditor, useReadOnly, useSlateStatic } from '..'
|
||||||
import useChildren from '../hooks/use-children'
|
import useChildren from '../hooks/use-children'
|
||||||
import { isElementDecorationsEqual } from '../utils/range-list'
|
import { isElementDecorationsEqual } from 'slate-dom'
|
||||||
import {
|
import {
|
||||||
EDITOR_TO_KEY_TO_ELEMENT,
|
EDITOR_TO_KEY_TO_ELEMENT,
|
||||||
ELEMENT_TO_NODE,
|
ELEMENT_TO_NODE,
|
||||||
NODE_TO_ELEMENT,
|
NODE_TO_ELEMENT,
|
||||||
NODE_TO_INDEX,
|
NODE_TO_INDEX,
|
||||||
NODE_TO_PARENT,
|
NODE_TO_PARENT,
|
||||||
} from '../utils/weak-maps'
|
} from 'slate-dom'
|
||||||
import {
|
import {
|
||||||
RenderElementProps,
|
RenderElementProps,
|
||||||
RenderLeafProps,
|
RenderLeafProps,
|
||||||
|
@ -13,10 +13,10 @@ import {
|
|||||||
PLACEHOLDER_SYMBOL,
|
PLACEHOLDER_SYMBOL,
|
||||||
EDITOR_TO_PLACEHOLDER_ELEMENT,
|
EDITOR_TO_PLACEHOLDER_ELEMENT,
|
||||||
EDITOR_TO_FORCE_RENDER,
|
EDITOR_TO_FORCE_RENDER,
|
||||||
} from '../utils/weak-maps'
|
} from 'slate-dom'
|
||||||
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
|
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
|
||||||
import { useSlateStatic } from '../hooks/use-slate-static'
|
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.
|
// Delay the placeholder on Android to prevent the keyboard from closing.
|
||||||
// (https://github.com/ianstormtaylor/slate/pull/5368)
|
// (https://github.com/ianstormtaylor/slate/pull/5368)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { RefObject } from 'react'
|
import { RefObject } from 'react'
|
||||||
import { ReactEditor } from '../../plugin/react-editor'
|
import { ReactEditor } from '../../plugin/react-editor'
|
||||||
import { isTrackedMutation } from '../../utils/dom'
|
import { isTrackedMutation } from 'slate-dom'
|
||||||
|
|
||||||
export type RestoreDOMManager = {
|
export type RestoreDOMManager = {
|
||||||
registerMutations: (mutations: MutationRecord[]) => void
|
registerMutations: (mutations: MutationRecord[]) => void
|
||||||
|
@ -6,7 +6,7 @@ import React, {
|
|||||||
RefObject,
|
RefObject,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { EditorContext } from '../../hooks/use-slate-static'
|
import { EditorContext } from '../../hooks/use-slate-static'
|
||||||
import { IS_ANDROID } from '../../utils/environment'
|
import { IS_ANDROID } from 'slate-dom'
|
||||||
import {
|
import {
|
||||||
createRestoreDomManager,
|
createRestoreDomManager,
|
||||||
RestoreDOMManager,
|
RestoreDOMManager,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { Descendant, Editor, Node, Operation, Scrubber, Selection } from 'slate'
|
import { Descendant, Editor, Node, Operation, Scrubber, Selection } from 'slate'
|
||||||
|
import { EDITOR_TO_ON_CHANGE } from 'slate-dom'
|
||||||
import { FocusedContext } from '../hooks/use-focused'
|
import { FocusedContext } from '../hooks/use-focused'
|
||||||
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
|
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
|
||||||
import { SlateContext, SlateContextValue } from '../hooks/use-slate'
|
import { SlateContext, SlateContextValue } from '../hooks/use-slate'
|
||||||
@ -10,7 +11,6 @@ import {
|
|||||||
import { EditorContext } from '../hooks/use-slate-static'
|
import { EditorContext } from '../hooks/use-slate-static'
|
||||||
import { ReactEditor } from '../plugin/react-editor'
|
import { ReactEditor } from '../plugin/react-editor'
|
||||||
import { REACT_MAJOR_VERSION } from '../utils/environment'
|
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
|
* 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 { ReactEditor, useSlateStatic } from '..'
|
||||||
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
|
import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect'
|
||||||
import { IS_ANDROID, IS_IOS } from '../utils/environment'
|
import { IS_ANDROID, IS_IOS } from 'slate-dom'
|
||||||
import { MARK_PLACEHOLDER_SYMBOL } from '../utils/weak-maps'
|
import { MARK_PLACEHOLDER_SYMBOL } from 'slate-dom'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Leaf content strings.
|
* Leaf content strings.
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React, { useCallback, useRef } from 'react'
|
import React, { useCallback, useRef } from 'react'
|
||||||
import { Element, Range, Text as SlateText } from 'slate'
|
import { Element, Range, Text as SlateText } from 'slate'
|
||||||
import { ReactEditor, useSlateStatic } from '..'
|
import { ReactEditor, useSlateStatic } from '..'
|
||||||
import { isTextDecorationsEqual } from '../utils/range-list'
|
import { isTextDecorationsEqual } from 'slate-dom'
|
||||||
import {
|
import {
|
||||||
EDITOR_TO_KEY_TO_ELEMENT,
|
EDITOR_TO_KEY_TO_ELEMENT,
|
||||||
ELEMENT_TO_NODE,
|
ELEMENT_TO_NODE,
|
||||||
NODE_TO_ELEMENT,
|
NODE_TO_ELEMENT,
|
||||||
} from '../utils/weak-maps'
|
} from 'slate-dom'
|
||||||
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
|
import { RenderLeafProps, RenderPlaceholderProps } from './editable'
|
||||||
import Leaf from './leaf'
|
import Leaf from './leaf'
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@ import {
|
|||||||
targetRange,
|
targetRange,
|
||||||
TextDiff,
|
TextDiff,
|
||||||
verifyDiffState,
|
verifyDiffState,
|
||||||
} from '../../utils/diff-text'
|
} from 'slate-dom'
|
||||||
import { isDOMSelection, isTrackedMutation } from '../../utils/dom'
|
import { isDOMSelection, isTrackedMutation } from 'slate-dom'
|
||||||
import {
|
import {
|
||||||
EDITOR_TO_FORCE_RENDER,
|
EDITOR_TO_FORCE_RENDER,
|
||||||
EDITOR_TO_PENDING_ACTION,
|
EDITOR_TO_PENDING_ACTION,
|
||||||
@ -23,7 +23,7 @@ import {
|
|||||||
EDITOR_TO_USER_MARKS,
|
EDITOR_TO_USER_MARKS,
|
||||||
IS_COMPOSING,
|
IS_COMPOSING,
|
||||||
IS_NODE_MAP_DIRTY,
|
IS_NODE_MAP_DIRTY,
|
||||||
} from '../../utils/weak-maps'
|
} from 'slate-dom'
|
||||||
|
|
||||||
export type Action = { at?: Point | Range; run: () => void }
|
export type Action = { at?: Point | Range; run: () => void }
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { RefObject, useState } from 'react'
|
import { RefObject, useState } from 'react'
|
||||||
import { useSlateStatic } from '../use-slate-static'
|
import { useSlateStatic } from '../use-slate-static'
|
||||||
import { IS_ANDROID } from '../../utils/environment'
|
import { IS_ANDROID } from 'slate-dom'
|
||||||
import { EDITOR_TO_SCHEDULE_FLUSH } from '../../utils/weak-maps'
|
import { EDITOR_TO_SCHEDULE_FLUSH } from 'slate-dom'
|
||||||
import {
|
import {
|
||||||
createAndroidInputManager,
|
createAndroidInputManager,
|
||||||
CreateAndroidInputManagerOptions,
|
CreateAndroidInputManagerOptions,
|
||||||
|
@ -9,11 +9,7 @@ import {
|
|||||||
import ElementComponent from '../components/element'
|
import ElementComponent from '../components/element'
|
||||||
import TextComponent from '../components/text'
|
import TextComponent from '../components/text'
|
||||||
import { ReactEditor } from '../plugin/react-editor'
|
import { ReactEditor } from '../plugin/react-editor'
|
||||||
import {
|
import { IS_NODE_MAP_DIRTY, NODE_TO_INDEX, NODE_TO_PARENT } from 'slate-dom'
|
||||||
IS_NODE_MAP_DIRTY,
|
|
||||||
NODE_TO_INDEX,
|
|
||||||
NODE_TO_PARENT,
|
|
||||||
} from '../utils/weak-maps'
|
|
||||||
import { useDecorate } from './use-decorate'
|
import { useDecorate } from './use-decorate'
|
||||||
import { SelectedContext } from './use-selected'
|
import { SelectedContext } from './use-selected'
|
||||||
import { useSlateStatic } from './use-slate-static'
|
import { useSlateStatic } from './use-slate-static'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useLayoutEffect, useEffect } from 'react'
|
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
|
* 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'
|
export { withReact } from './plugin/with-react'
|
||||||
|
|
||||||
// Utils
|
// 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 ReactDOM from 'react-dom'
|
||||||
import {
|
import { BaseEditor } from 'slate'
|
||||||
BaseEditor,
|
import { withDOM } from 'slate-dom'
|
||||||
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 { ReactEditor } from './react-editor'
|
import { ReactEditor } from './react-editor'
|
||||||
import { REACT_MAJOR_VERSION } from '../utils/environment'
|
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.
|
* See https://docs.slatejs.org/concepts/11-typescript to learn how.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const withReact = <T extends BaseEditor>(
|
export const withReact = <T extends BaseEditor>(
|
||||||
editor: T,
|
editor: T,
|
||||||
clipboardFormatKey = 'x-slate-fragment'
|
clipboardFormatKey = 'x-slate-fragment'
|
||||||
): T & ReactEditor => {
|
): T & ReactEditor => {
|
||||||
const e = editor as T & ReactEditor
|
let e = editor as T & ReactEditor
|
||||||
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
|
e = withDOM(e, clipboardFormatKey)
|
||||||
// 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) => {
|
const { onChange } = e
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
e.onChange = options => {
|
e.onChange = options => {
|
||||||
// COMPAT: React < 18 doesn't batch `setState` hook calls, which means
|
// COMPAT: React < 18 doesn't batch `setState` hook calls, which means
|
||||||
@ -373,24 +34,9 @@ export const withReact = <T extends BaseEditor>(
|
|||||||
: (callback: () => void) => callback()
|
: (callback: () => void) => callback()
|
||||||
|
|
||||||
maybeBatchUpdates(() => {
|
maybeBatchUpdates(() => {
|
||||||
const onContextChange = EDITOR_TO_ON_CHANGE.get(e)
|
|
||||||
|
|
||||||
if (onContextChange) {
|
|
||||||
onContextChange(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(options)
|
onChange(options)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return e
|
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'
|
import React from 'react'
|
||||||
|
|
||||||
export const REACT_MAJOR_VERSION = parseInt(React.version.split('.')[0], 10)
|
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",
|
"rootDir": "./src",
|
||||||
"outDir": "./lib"
|
"outDir": "./lib"
|
||||||
},
|
},
|
||||||
"references": [{ "path": "../slate" }]
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../slate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../slate-dom"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
27
yarn.lock
27
yarn.lock
@ -13258,6 +13258,31 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"slate-history@workspace:*, slate-history@workspace:packages/slate-history":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "slate-history@workspace:packages/slate-history"
|
resolution: "slate-history@workspace:packages/slate-history"
|
||||||
@ -13391,6 +13416,7 @@ __metadata:
|
|||||||
react-dom: "npm:^18.2.0"
|
react-dom: "npm:^18.2.0"
|
||||||
scroll-into-view-if-needed: "npm:^3.1.0"
|
scroll-into-view-if-needed: "npm:^3.1.0"
|
||||||
slate: "npm:^0.110.2"
|
slate: "npm:^0.110.2"
|
||||||
|
slate-dom: "npm:^0.110.2"
|
||||||
slate-hyperscript: "npm:^0.100.0"
|
slate-hyperscript: "npm:^0.100.0"
|
||||||
source-map-loader: "npm:^4.0.1"
|
source-map-loader: "npm:^4.0.1"
|
||||||
tiny-invariant: "npm:1.3.1"
|
tiny-invariant: "npm:1.3.1"
|
||||||
@ -13398,6 +13424,7 @@ __metadata:
|
|||||||
react: ">=18.2.0"
|
react: ">=18.2.0"
|
||||||
react-dom: ">=18.2.0"
|
react-dom: ">=18.2.0"
|
||||||
slate: ">=0.99.0"
|
slate: ">=0.99.0"
|
||||||
|
slate-dom: ">=0.110.2"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user