1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-01-17 21:49:20 +01:00

Remove commands (#3351)

* remove commands in favor of editor-level functions

* update examples

* fix lint
This commit is contained in:
Ian Storm Taylor 2019-12-18 15:00:42 -05:00 committed by GitHub
parent c2d7905e19
commit 0bbe121d76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
578 changed files with 3532 additions and 3370 deletions

View File

@ -103,13 +103,13 @@ Another special group of helper functions exposed on the `Editor` interface are
```js
// Insert an element node at a specific path.
Editor.insertNodes(editor, [element], { at: path })
Transforms.insertNodes(editor, [element], { at: path })
// Split the nodes in half at a specific point.
Editor.splitNodes(editor, { at: point })
Transforms.splitNodes(editor, { at: point })
// Add a quote format to all the block nodes in the selection.
Editor.setNodes(editor, { type: 'quote' })
Transforms.setNodes(editor, { type: 'quote' })
```
The editor-specific helpers are the ones you'll use most often when working with Slate editors, so it pays to become very familiar with them.

View File

@ -14,7 +14,7 @@ const withImages = editor => {
if (command.type === 'insert_image') {
const { url } = command
const element = { type: 'image', url, children: [{ text: '' }] }
Editor.insertNodes(editor, element)
Transforms.insertNodes(editor, element)
} else {
exec(command)
}

View File

@ -41,7 +41,7 @@ const withParagraphs = editor => {
if (Element.isElement(node) && node.type === 'paragraph') {
for (const [child, childPath] of Node.children(editor, path)) {
if (Element.isElement(child) && !editor.isInline(child)) {
Editor.unwrapNodes(editor, { at: childPath })
Transforms.unwrapNodes(editor, { at: childPath })
return
}
}
@ -67,7 +67,7 @@ If you check the example above again, you'll notice the `return` statement:
```js
if (Element.isElement(child) && !editor.isInline(child)) {
Editor.unwrapNodes(editor, { at: childPath })
Transforms.unwrapNodes(editor, { at: childPath })
return
}
```
@ -135,7 +135,7 @@ const withLinks = editor => {
node.type === 'link' &&
typeof node.url !== 'string'
) {
Editor.setNodes(editor, { url: null }, { at: path })
Transforms.setNodes(editor, { url: null }, { at: path })
return
}

View File

@ -147,7 +147,7 @@ const App = () => {
// Prevent the "`" from being inserted by default.
event.preventDefault()
// Otherwise, set the currently selected blocks type to "code".
Editor.setNodes(
Transforms.setNodes(
editor,
{ type: 'code' },
{ match: n => Editor.isBlock(editor, n) }
@ -208,7 +208,7 @@ const App = () => {
match: n => n.type === 'code',
})
// Toggle the block type depending on whether there's already a match.
Editor.setNodes(
Transforms.setNodes(
editor,
{ type: match ? 'paragraph' : 'code' },
{ match: n => Editor.isBlock(editor, n) }

View File

@ -36,7 +36,7 @@ const App = () => {
const [match] = Editor.nodes(editor, {
match: n => n.type === 'code',
})
Editor.setNodes(
Transforms.setNodes(
editor,
{ type: match ? 'paragraph' : 'code' },
{ match: n => Editor.isBlock(editor, n) }
@ -86,7 +86,7 @@ const App = () => {
const [match] = Editor.nodes(editor, {
match: n => n.type === 'code',
})
Editor.setNodes(
Transforms.setNodes(
editor,
{ type: match ? 'paragraph' : 'code' },
{ match: n => Editor.isBlock(editor, n) }
@ -97,7 +97,7 @@ const App = () => {
// When "B" is pressed, bold the text in the selection.
case 'b': {
event.preventDefault()
Editor.setNodes(
Transforms.setNodes(
editor,
{ bold: true },
// Apply it to text nodes, and split the text node up if the
@ -177,7 +177,7 @@ const App = () => {
const [match] = Editor.nodes(editor, {
match: n => n.type === 'code',
})
Editor.setNodes(
Transforms.setNodes(
editor,
{ type: match ? null : 'code' },
{ match: n => Editor.isBlock(editor, n) }
@ -187,7 +187,7 @@ const App = () => {
case 'b': {
event.preventDefault()
Editor.setNodes(
Transforms.setNodes(
editor,
{ bold: true },
{ match: n => Text.isText(n), split: true }

View File

@ -49,7 +49,7 @@ const App = () => {
const [match] = Editor.nodes(editor, {
match: n => n.type === 'code',
})
Editor.setNodes(
Transforms.setNodes(
editor,
{ type: match ? null : 'code' },
{ match: n => Editor.isBlock(editor, n) }
@ -59,7 +59,7 @@ const App = () => {
case 'b': {
event.preventDefault()
Editor.setNodes(
Transforms.setNodes(
editor,
{ bold: true },
{ match: n => Text.isText(n), split: true }
@ -124,7 +124,7 @@ const App = () => {
match: n => n.type === 'code',
})
const isCodeActive = !!match
Editor.setNodes(
Transforms.setNodes(
editor,
{ type: isCodeActive ? null : 'code' },
{ match: n => Editor.isBlock(editor, n) }
@ -134,7 +134,7 @@ const App = () => {
case 'b': {
event.preventDefault()
Editor.setNodes(
Transforms.setNodes(
editor,
{ bold: true },
{ match: n => Text.isText(n), split: true }
@ -161,7 +161,7 @@ const withCustom = editor => {
// Define a command to toggle the bold formatting.
if (command.type === 'toggle_bold_mark') {
const isActive = CustomEditor.isBoldMarkActive(editor)
Editor.setNodes(
Transforms.setNodes(
editor,
{ bold: isActive ? null : true },
{ match: n => Text.isText(n), split: true }
@ -171,7 +171,7 @@ const withCustom = editor => {
// Define a command to toggle the code block formatting.
else if (command.type === 'toggle_code_block') {
const isActive = CustomEditor.isCodeBlockActive(editor)
Editor.setNodes(
Transforms.setNodes(
editor,
{ type: isActive ? null : 'code' },
{ match: n => Editor.isBlock(editor, n) }

View File

@ -1,29 +0,0 @@
import { Command } from 'slate'
export interface RedoCommand {
type: 'redo'
}
export interface UndoCommand {
type: 'undo'
}
export type HistoryCommand = RedoCommand | UndoCommand
export const HistoryCommand = {
/**
* Check if a value is a `HistoryCommand` object.
*/
isHistoryCommand(value: any): value is HistoryCommand {
if (Command.isCommand(value)) {
switch (value.type) {
case 'redo':
case 'undo':
return true
}
}
return false
},
}

View File

@ -15,6 +15,8 @@ export const MERGING = new WeakMap<Editor, boolean | undefined>()
export interface HistoryEditor extends Editor {
history: History
undo: () => void
redo: () => void
}
export const HistoryEditor = {
@ -30,7 +32,7 @@ export const HistoryEditor = {
* Get the merge flag's current value.
*/
isMerging(editor: Editor): boolean | undefined {
isMerging(editor: HistoryEditor): boolean | undefined {
return MERGING.get(editor)
},
@ -38,16 +40,32 @@ export const HistoryEditor = {
* Get the saving flag's current value.
*/
isSaving(editor: Editor): boolean | undefined {
isSaving(editor: HistoryEditor): boolean | undefined {
return SAVING.get(editor)
},
/**
* Redo to the previous saved state.
*/
redo(editor: HistoryEditor): void {
editor.redo()
},
/**
* Undo to the previous saved state.
*/
undo(editor: HistoryEditor): void {
editor.undo()
},
/**
* Apply a series of changes inside a synchronous `fn`, without merging any of
* the new operations into previous save point in the history.
*/
withoutMerging(editor: Editor, fn: () => void): void {
withoutMerging(editor: HistoryEditor, fn: () => void): void {
const prev = HistoryEditor.isMerging(editor)
MERGING.set(editor, false)
fn()
@ -59,7 +77,7 @@ export const HistoryEditor = {
* their operations into the history.
*/
withoutSaving(editor: Editor, fn: () => void): void {
withoutSaving(editor: HistoryEditor, fn: () => void): void {
const prev = HistoryEditor.isSaving(editor)
SAVING.set(editor, false)
fn()

View File

@ -1,4 +1,3 @@
export * from './history'
export * from './history-command'
export * from './history-editor'
export * from './with-history'

View File

@ -1,6 +1,5 @@
import { Editor, Command, Operation, Path } from 'slate'
import { Editor, Operation, Path } from 'slate'
import { HistoryCommand } from './history-command'
import { HistoryEditor } from './history-editor'
/**
@ -9,114 +8,109 @@ import { HistoryEditor } from './history-editor'
*/
export const withHistory = (editor: Editor): HistoryEditor => {
const { apply, exec } = editor
editor.history = { undos: [], redos: [] }
const e = editor as HistoryEditor
const { apply } = e
e.history = { undos: [], redos: [] }
editor.exec = (command: Command) => {
if (
HistoryEditor.isHistoryEditor(editor) &&
HistoryCommand.isHistoryCommand(command)
) {
const { history } = editor
const { undos, redos } = history
e.redo = () => {
const { history } = e
const { redos } = history
if (command.type === 'redo' && redos.length > 0) {
const batch = redos[redos.length - 1]
if (redos.length > 0) {
const batch = redos[redos.length - 1]
HistoryEditor.withoutSaving(editor, () => {
Editor.withoutNormalizing(editor, () => {
for (const op of batch) {
editor.apply(op)
}
})
HistoryEditor.withoutSaving(e, () => {
Editor.withoutNormalizing(e, () => {
for (const op of batch) {
e.apply(op)
}
})
})
history.redos.pop()
history.undos.push(batch)
return
}
if (command.type === 'undo' && undos.length > 0) {
const batch = undos[undos.length - 1]
HistoryEditor.withoutSaving(editor, () => {
Editor.withoutNormalizing(editor, () => {
const inverseOps = batch.map(Operation.inverse).reverse()
for (const op of inverseOps) {
// If the final operation is deselecting the editor, skip it. This is
if (
op === inverseOps[inverseOps.length - 1] &&
op.type === 'set_selection' &&
op.newProperties == null
) {
continue
} else {
editor.apply(op)
}
}
})
})
history.redos.push(batch)
history.undos.pop()
return
}
history.redos.pop()
history.undos.push(batch)
}
exec(command)
}
editor.apply = (op: Operation) => {
if (HistoryEditor.isHistoryEditor(editor)) {
const { operations, history } = editor
const { undos } = history
const lastBatch = undos[undos.length - 1]
const lastOp = lastBatch && lastBatch[lastBatch.length - 1]
const overwrite = shouldOverwrite(op, lastOp)
let save = HistoryEditor.isSaving(editor)
let merge = HistoryEditor.isMerging(editor)
e.undo = () => {
const { history } = e
const { undos } = history
if (save == null) {
save = shouldSave(op, lastOp)
if (undos.length > 0) {
const batch = undos[undos.length - 1]
HistoryEditor.withoutSaving(e, () => {
Editor.withoutNormalizing(e, () => {
const inverseOps = batch.map(Operation.inverse).reverse()
for (const op of inverseOps) {
// If the final operation is deselecting the editor, skip it. This is
if (
op === inverseOps[inverseOps.length - 1] &&
op.type === 'set_selection' &&
op.newProperties == null
) {
continue
} else {
e.apply(op)
}
}
})
})
history.redos.push(batch)
history.undos.pop()
}
}
e.apply = (op: Operation) => {
const { operations, history } = e
const { undos } = history
const lastBatch = undos[undos.length - 1]
const lastOp = lastBatch && lastBatch[lastBatch.length - 1]
const overwrite = shouldOverwrite(op, lastOp)
let save = HistoryEditor.isSaving(e)
let merge = HistoryEditor.isMerging(e)
if (save == null) {
save = shouldSave(op, lastOp)
}
if (save) {
if (merge == null) {
if (lastBatch == null) {
merge = false
} else if (operations.length !== 0) {
merge = true
} else {
merge = shouldMerge(op, lastOp) || overwrite
}
}
if (save) {
if (merge == null) {
if (lastBatch == null) {
merge = false
} else if (operations.length !== 0) {
merge = true
} else {
merge = shouldMerge(op, lastOp) || overwrite
}
if (lastBatch && merge) {
if (overwrite) {
lastBatch.pop()
}
if (lastBatch && merge) {
if (overwrite) {
lastBatch.pop()
}
lastBatch.push(op)
} else {
const batch = [op]
undos.push(batch)
}
lastBatch.push(op)
} else {
const batch = [op]
undos.push(batch)
}
while (undos.length > 100) {
undos.shift()
}
while (undos.length > 100) {
undos.shift()
}
if (shouldClear(op)) {
history.redos = []
}
if (shouldClear(op)) {
history.redos = []
}
}
apply(op)
}
return editor as HistoryEditor
return e
}
/**

View File

@ -8,7 +8,7 @@ describe('slate-history', () => {
const { input, run, output } = module
const editor = withTest(withHistory(input))
run(editor)
editor.exec({ type: 'undo' })
editor.undo()
assert.deepEqual(editor.children, output.children)
assert.deepEqual(editor.selection, output.selection)
})

View File

@ -3,7 +3,7 @@
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'delete_backward' })
editor.deleteBackward()
}
export const input = (

View File

@ -3,7 +3,7 @@
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'delete_backward' })
editor.deleteBackward()
}
export const input = (

View File

@ -1,10 +1,10 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { Transforms } from 'slate'
import { jsx } from '../..'
export const run = editor => {
Editor.delete(editor)
Transforms.delete(editor)
}
export const input = (

View File

@ -1,10 +1,10 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { Transforms } from 'slate'
import { jsx } from '../..'
export const run = editor => {
Editor.delete(editor)
Transforms.delete(editor)
}
export const input = (

View File

@ -1,10 +1,10 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { Transforms } from 'slate'
import { jsx } from '../..'
export const run = editor => {
Editor.delete(editor)
Transforms.delete(editor)
}
export const input = (

View File

@ -4,7 +4,7 @@ import { Editor } from 'slate'
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'insert_break' })
editor.insertBreak()
}
export const input = (

View File

@ -22,7 +22,7 @@ const fragment = (
)
export const run = editor => {
editor.exec({ type: 'insert_fragment', fragment })
editor.insertFragment(fragment)
}
export const input = (
@ -38,3 +38,5 @@ export const input = (
)
export const output = input
export const skip = true

View File

@ -3,7 +3,7 @@
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'insert_text', text: 'text' })
editor.insertText('text')
}
export const input = (

View File

@ -3,9 +3,9 @@
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'insert_text', text: 't' })
editor.exec({ type: 'insert_text', text: 'w' })
editor.exec({ type: 'insert_text', text: 'o' })
editor.insertText('t')
editor.insertText('w')
editor.insertText('o')
}
export const input = (

View File

@ -1,14 +1,14 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { Transforms } from 'slate'
import { jsx } from '../..'
export const run = editor => {
editor.exec({ type: 'insert_text', text: 't' })
Editor.move(editor, { reverse: true })
editor.exec({ type: 'insert_text', text: 'w' })
Editor.move(editor, { reverse: true })
editor.exec({ type: 'insert_text', text: 'o' })
editor.insertText('t')
Transforms.move(editor, { reverse: true })
editor.insertText('w')
Transforms.move(editor, { reverse: true })
editor.insertText('o')
}
export const input = (

View File

@ -1,5 +1,13 @@
import React, { useEffect, useRef, useMemo, useCallback } from 'react'
import { Editor, Element, NodeEntry, Node, Range, Text } from 'slate'
import {
Editor,
Element,
NodeEntry,
Node,
Range,
Text,
Transforms,
} from 'slate'
import debounce from 'debounce'
import scrollIntoView from 'scroll-into-view-if-needed'
@ -252,7 +260,7 @@ export const Editable = (props: EditableProps) => {
const range = ReactEditor.toSlateRange(editor, targetRange)
if (!selection || !Range.equals(selection, range)) {
Editor.select(editor, range)
Transforms.select(editor, range)
}
}
}
@ -264,7 +272,7 @@ export const Editable = (props: EditableProps) => {
Range.isExpanded(selection) &&
type.startsWith('delete')
) {
editor.exec({ type: 'delete_fragment' })
Editor.deleteFragment(editor)
return
}
@ -272,60 +280,60 @@ export const Editable = (props: EditableProps) => {
case 'deleteByComposition':
case 'deleteByCut':
case 'deleteByDrag': {
editor.exec({ type: 'delete_fragment' })
Editor.deleteFragment(editor)
break
}
case 'deleteContent':
case 'deleteContentForward': {
editor.exec({ type: 'delete_forward', unit: 'character' })
Editor.deleteForward(editor)
break
}
case 'deleteContentBackward': {
editor.exec({ type: 'delete_backward', unit: 'character' })
Editor.deleteBackward(editor)
break
}
case 'deleteEntireSoftLine': {
editor.exec({ type: 'delete_backward', unit: 'line' })
editor.exec({ type: 'delete_forward', unit: 'line' })
Editor.deleteBackward(editor, { unit: 'line' })
Editor.deleteForward(editor, { unit: 'line' })
break
}
case 'deleteHardLineBackward': {
editor.exec({ type: 'delete_backward', unit: 'block' })
Editor.deleteBackward(editor, { unit: 'block' })
break
}
case 'deleteSoftLineBackward': {
editor.exec({ type: 'delete_backward', unit: 'line' })
Editor.deleteBackward(editor, { unit: 'line' })
break
}
case 'deleteHardLineForward': {
editor.exec({ type: 'delete_forward', unit: 'block' })
Editor.deleteForward(editor, { unit: 'block' })
break
}
case 'deleteSoftLineForward': {
editor.exec({ type: 'delete_forward', unit: 'line' })
Editor.deleteForward(editor, { unit: 'line' })
break
}
case 'deleteWordBackward': {
editor.exec({ type: 'delete_backward', unit: 'word' })
Editor.deleteBackward(editor, { unit: 'word' })
break
}
case 'deleteWordForward': {
editor.exec({ type: 'delete_forward', unit: 'word' })
Editor.deleteForward(editor, { unit: 'word' })
break
}
case 'insertLineBreak':
case 'insertParagraph': {
editor.exec({ type: 'insert_break' })
Editor.insertBreak(editor)
break
}
@ -336,9 +344,9 @@ export const Editable = (props: EditableProps) => {
case 'insertReplacementText':
case 'insertText': {
if (data instanceof DataTransfer) {
editor.exec({ type: 'insert_data', data })
ReactEditor.insertData(editor, data)
} else if (typeof data === 'string') {
editor.exec({ type: 'insert_text', text: data })
Editor.insertText(editor, data)
}
break
@ -378,9 +386,9 @@ export const Editable = (props: EditableProps) => {
hasEditableTarget(editor, domRange.endContainer)
) {
const range = ReactEditor.toSlateRange(editor, domRange)
Editor.select(editor, range)
Transforms.select(editor, range)
} else {
Editor.deselect(editor)
Transforms.deselect(editor)
}
}
}, 100),
@ -441,7 +449,7 @@ export const Editable = (props: EditableProps) => {
if (IS_FIREFOX && !readOnly) {
event.preventDefault()
const text = (event as any).data as string
editor.exec({ type: 'insert_text', text })
Editor.insertText(editor, text)
}
},
[readOnly]
@ -517,7 +525,7 @@ export const Editable = (props: EditableProps) => {
if (Editor.void(editor, { at: start })) {
const range = Editor.range(editor, start)
Editor.select(editor, range)
Transforms.select(editor, range)
}
}
},
@ -536,7 +544,7 @@ export const Editable = (props: EditableProps) => {
// type that we need. So instead, insert whenever a composition
// ends since it will already have been committed to the DOM.
if (!IS_SAFARI && !IS_FIREFOX && event.data) {
editor.exec({ type: 'insert_text', text: event.data })
Editor.insertText(editor, event.data)
}
}
},
@ -577,7 +585,7 @@ export const Editable = (props: EditableProps) => {
const { selection } = editor
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
Editor.deleteFragment(editor)
}
}
},
@ -615,7 +623,7 @@ export const Editable = (props: EditableProps) => {
// so that it shows up in the selection's fragment.
if (voidMatch) {
const range = Editor.range(editor, path)
Editor.select(editor, range)
Transforms.select(editor, range)
}
setFragmentData(event.dataTransfer, editor)
@ -641,8 +649,8 @@ export const Editable = (props: EditableProps) => {
event.preventDefault()
const range = ReactEditor.findEventRange(editor, event)
const data = event.dataTransfer
Editor.select(editor, range)
editor.exec({ type: 'insert_data', data })
Transforms.select(editor, range)
ReactEditor.insertData(editor, data)
}
}
},
@ -688,13 +696,21 @@ export const Editable = (props: EditableProps) => {
// hotkeys ourselves. (2019/11/06)
if (Hotkeys.isRedo(nativeEvent)) {
event.preventDefault()
editor.exec({ type: 'redo' })
if (editor.undo) {
editor.undo()
}
return
}
if (Hotkeys.isUndo(nativeEvent)) {
event.preventDefault()
editor.exec({ type: 'undo' })
if (editor.redo) {
editor.redo()
}
return
}
@ -704,19 +720,19 @@ export const Editable = (props: EditableProps) => {
// (2017/10/17)
if (Hotkeys.isMoveLineBackward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'line', reverse: true })
Transforms.move(editor, { unit: 'line', reverse: true })
return
}
if (Hotkeys.isMoveLineForward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'line' })
Transforms.move(editor, { unit: 'line' })
return
}
if (Hotkeys.isExtendLineBackward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, {
Transforms.move(editor, {
unit: 'line',
edge: 'focus',
reverse: true,
@ -726,7 +742,7 @@ export const Editable = (props: EditableProps) => {
if (Hotkeys.isExtendLineForward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'line', edge: 'focus' })
Transforms.move(editor, { unit: 'line', edge: 'focus' })
return
}
@ -739,9 +755,9 @@ export const Editable = (props: EditableProps) => {
event.preventDefault()
if (selection && Range.isCollapsed(selection)) {
Editor.move(editor, { reverse: true })
Transforms.move(editor, { reverse: true })
} else {
Editor.collapse(editor, { edge: 'start' })
Transforms.collapse(editor, { edge: 'start' })
}
return
@ -751,9 +767,9 @@ export const Editable = (props: EditableProps) => {
event.preventDefault()
if (selection && Range.isCollapsed(selection)) {
Editor.move(editor)
Transforms.move(editor)
} else {
Editor.collapse(editor, { edge: 'end' })
Transforms.collapse(editor, { edge: 'end' })
}
return
@ -761,13 +777,13 @@ export const Editable = (props: EditableProps) => {
if (Hotkeys.isMoveWordBackward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'word', reverse: true })
Transforms.move(editor, { unit: 'word', reverse: true })
return
}
if (Hotkeys.isMoveWordForward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'word' })
Transforms.move(editor, { unit: 'word' })
return
}
@ -788,7 +804,7 @@ export const Editable = (props: EditableProps) => {
if (Hotkeys.isSplitBlock(nativeEvent)) {
event.preventDefault()
editor.exec({ type: 'insert_break' })
Editor.insertBreak(editor)
return
}
@ -796,9 +812,9 @@ export const Editable = (props: EditableProps) => {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
Editor.deleteFragment(editor)
} else {
editor.exec({ type: 'delete_backward', unit: 'character' })
Editor.deleteBackward(editor)
}
return
@ -808,9 +824,9 @@ export const Editable = (props: EditableProps) => {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
Editor.deleteFragment(editor)
} else {
editor.exec({ type: 'delete_forward', unit: 'character' })
Editor.deleteForward(editor)
}
return
@ -820,9 +836,9 @@ export const Editable = (props: EditableProps) => {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
Editor.deleteFragment(editor)
} else {
editor.exec({ type: 'delete_backward', unit: 'line' })
Editor.deleteBackward(editor, { unit: 'line' })
}
return
@ -832,9 +848,9 @@ export const Editable = (props: EditableProps) => {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
Editor.deleteFragment(editor)
} else {
editor.exec({ type: 'delete_forward', unit: 'line' })
Editor.deleteForward(editor, { unit: 'line' })
}
return
@ -844,9 +860,9 @@ export const Editable = (props: EditableProps) => {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
Editor.deleteFragment(editor)
} else {
editor.exec({ type: 'delete_backward', unit: 'word' })
Editor.deleteBackward(editor, { unit: 'word' })
}
return
@ -856,9 +872,9 @@ export const Editable = (props: EditableProps) => {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
Editor.deleteFragment(editor)
} else {
editor.exec({ type: 'delete_forward', unit: 'word' })
Editor.deleteForward(editor, { unit: 'word' })
}
return
@ -879,10 +895,7 @@ export const Editable = (props: EditableProps) => {
!isEventHandled(event, attributes.onPaste)
) {
event.preventDefault()
editor.exec({
type: 'insert_data',
data: event.clipboardData,
})
ReactEditor.insertData(editor, event.clipboardData)
}
},
[readOnly, attributes.onPaste]
@ -984,7 +997,10 @@ const isDOMEventHandled = (event: Event, handler?: (event: Event) => void) => {
* Set the currently selected fragment to the clipboard.
*/
const setFragmentData = (dataTransfer: DataTransfer, editor: Editor): void => {
const setFragmentData = (
dataTransfer: DataTransfer,
editor: ReactEditor
): void => {
const { selection } = editor
if (!selection) {

View File

@ -1,5 +1,5 @@
import React, { useMemo, useState, useCallback } from 'react'
import { Editor, Node, Range } from 'slate'
import { Node } from 'slate'
import { ReactEditor } from '../plugin/react-editor'
import { FocusedContext } from '../hooks/use-focused'
@ -13,7 +13,7 @@ import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps'
*/
export const Slate = (props: {
editor: Editor
editor: ReactEditor
value: Node[]
children: React.ReactNode
onChange: (value: Node[]) => void
@ -21,7 +21,7 @@ export const Slate = (props: {
}) => {
const { editor, children, onChange, value, ...rest } = props
const [key, setKey] = useState(0)
const context: [Editor] = useMemo(() => {
const context: [ReactEditor] = useMemo(() => {
editor.children = value
Object.assign(editor, rest)
return [editor]

View File

@ -1,14 +1,15 @@
import { Editor } from 'slate'
import { createContext, useContext } from 'react'
import { ReactEditor } from '../plugin/react-editor'
/**
* A React context for sharing the `Editor` class.
* A React context for sharing the editor object.
*/
export const EditorContext = createContext<Editor | null>(null)
export const EditorContext = createContext<ReactEditor | null>(null)
/**
* Get the current `Editor` class that the component lives under.
* Get the current editor object from the React context.
*/
export const useEditor = () => {

View File

@ -1,15 +1,16 @@
import { Editor } from 'slate'
import { createContext, useContext } from 'react'
import { ReactEditor } from '../plugin/react-editor'
/**
* A React context for sharing the `Editor` class, in a way that re-renders the
* A React context for sharing the editor object, in a way that re-renders the
* context whenever changes occur.
*/
export const SlateContext = createContext<[Editor] | null>(null)
export const SlateContext = createContext<[ReactEditor] | null>(null)
/**
* Get the current `Editor` class that the component lives under.
* Get the current editor object from the React context.
*/
export const useSlate = () => {

View File

@ -16,6 +16,5 @@ export { useSelected } from './hooks/use-selected'
export { useSlate } from './hooks/use-slate'
// Plugin
export { InsertDataCommand, ReactCommand } from './plugin/react-command'
export { ReactEditor } from './plugin/react-editor'
export { withReact } from './plugin/with-react'

View File

@ -1,33 +0,0 @@
import { Command } from 'slate'
/**
* The `InsertDataCommand` inserts content from a `DataTransfer` object.
*/
export interface InsertDataCommand {
type: 'insert_data'
data: DataTransfer
}
/**
* The `ReactCommand` union for all commands that the React plugins defines.
*/
export type ReactCommand = InsertDataCommand
export const ReactCommand = {
/**
* Check if a value is a `ReactCommand` object.
*/
isReactCommand(value: any): value is InsertDataCommand {
if (Command.isCommand(value)) {
switch (value.type) {
case 'insert_data':
return value.data instanceof DataTransfer
}
}
return false
},
}

View File

@ -1,4 +1,4 @@
import { Editor, Element, Node, Path, Point, Range } from 'slate'
import { Editor, Node, Path, Point, Range, Transforms } from 'slate'
import { Key } from '../utils/key'
import {
@ -22,7 +22,13 @@ import {
normalizeDOMPoint,
} from '../utils/dom'
export interface ReactEditor extends Editor {}
/**
* A React and DOM-specific version of the `Editor` interface.
*/
export interface ReactEditor extends Editor {
insertData: (data: DataTransfer) => void
}
export const ReactEditor = {
/**
@ -129,7 +135,7 @@ export const ReactEditor = {
}
if (selection) {
Editor.deselect(editor)
Transforms.deselect(editor)
}
},
@ -170,6 +176,14 @@ export const ReactEditor = {
)
},
/**
* Insert data from a `DataTransfer` into the editor.
*/
insertData(editor: ReactEditor, data: DataTransfer): void {
editor.insertData(data)
},
/**
* Find the native DOM element from a Slate node.
*/

View File

@ -1,8 +1,7 @@
import ReactDOM from 'react-dom'
import { Editor, Node, Path, Operation, Command } from 'slate'
import { Editor, Node, Path, Operation, Transforms } from 'slate'
import { ReactEditor } from './react-editor'
import { ReactCommand } from './react-command'
import { Key } from '../utils/key'
import { EDITOR_TO_ON_CHANGE, NODE_TO_KEY } from '../utils/weak-maps'
@ -10,18 +9,19 @@ import { EDITOR_TO_ON_CHANGE, NODE_TO_KEY } from '../utils/weak-maps'
* `withReact` adds React and DOM specific behaviors to the editor.
*/
export const withReact = (editor: Editor): Editor => {
const { apply, exec, onChange } = editor
export const withReact = (editor: Editor): ReactEditor => {
const e = editor as ReactEditor
const { apply, onChange } = e
editor.apply = (op: Operation) => {
e.apply = (op: Operation) => {
const matches: [Path, Key][] = []
switch (op.type) {
case 'insert_text':
case 'remove_text':
case 'set_node': {
for (const [node, path] of Editor.levels(editor, { at: op.path })) {
const key = ReactEditor.findKey(editor, node)
for (const [node, path] of Editor.levels(e, { at: op.path })) {
const key = ReactEditor.findKey(e, node)
matches.push([path, key])
}
@ -32,10 +32,10 @@ export const withReact = (editor: Editor): Editor => {
case 'remove_node':
case 'merge_node':
case 'split_node': {
for (const [node, path] of Editor.levels(editor, {
for (const [node, path] of Editor.levels(e, {
at: Path.parent(op.path),
})) {
const key = ReactEditor.findKey(editor, node)
const key = ReactEditor.findKey(e, node)
matches.push([path, key])
}
@ -51,53 +51,45 @@ export const withReact = (editor: Editor): Editor => {
apply(op)
for (const [path, key] of matches) {
const [node] = Editor.node(editor, path)
const [node] = Editor.node(e, path)
NODE_TO_KEY.set(node, key)
}
}
editor.exec = (command: Command) => {
if (
ReactCommand.isReactCommand(command) &&
command.type === 'insert_data'
) {
const { data } = command
const fragment = data.getData('application/x-slate-fragment')
e.insertData = (data: DataTransfer) => {
const fragment = data.getData('application/x-slate-fragment')
if (fragment) {
const decoded = decodeURIComponent(window.atob(fragment))
const parsed = JSON.parse(decoded) as Node[]
Editor.insertFragment(editor, parsed)
return
}
if (fragment) {
const decoded = decodeURIComponent(window.atob(fragment))
const parsed = JSON.parse(decoded) as Node[]
Transforms.insertFragment(e, parsed)
return
}
const text = data.getData('text/plain')
const text = data.getData('text/plain')
if (text) {
const lines = text.split('\n')
let split = false
if (text) {
const lines = text.split('\n')
let split = false
for (const line of lines) {
if (split) {
Editor.splitNodes(editor)
}
Editor.insertText(editor, line)
split = true
for (const line of lines) {
if (split) {
Transforms.splitNodes(e)
}
Transforms.insertText(e, line)
split = true
}
} else {
exec(command)
}
}
editor.onChange = () => {
e.onChange = () => {
// COMPAT: React doesn't batch `setState` hook calls, which means that the
// children and selection can get out of sync for one render pass. So we
// have to use this unstable API to ensure it batches them. (2019/12/03)
// https://github.com/facebook/react/issues/14259#issuecomment-439702367
ReactDOM.unstable_batchedUpdates(() => {
const onContextChange = EDITOR_TO_ON_CHANGE.get(editor)
const onContextChange = EDITOR_TO_ON_CHANGE.get(e)
if (onContextChange) {
onContextChange()
@ -107,5 +99,5 @@ export const withReact = (editor: Editor): Editor => {
})
}
return editor
return e
}

View File

@ -1,6 +1,4 @@
import {
Command,
CoreCommand,
Descendant,
Editor,
Element,
@ -13,10 +11,9 @@ import {
Range,
RangeRef,
Text,
} from '.'
import { DIRTY_PATHS } from './interfaces/editor/transforms/general'
const FLUSHING: WeakMap<Editor, boolean> = new WeakMap()
Transforms,
} from './'
import { DIRTY_PATHS, FLUSHING } from './utils/weak-maps'
/**
* Create a new Slate `Editor` object.
@ -31,6 +28,7 @@ export const createEditor = (): Editor => {
isInline: () => false,
isVoid: () => false,
onChange: () => {},
apply: (op: Operation) => {
for (const ref of Editor.pathRefs(editor)) {
PathRef.transform(ref, op)
@ -90,131 +88,101 @@ export const createEditor = (): Editor => {
})
}
},
exec: (command: Command) => {
if (CoreCommand.isCoreCommand(command)) {
const { selection } = editor
switch (command.type) {
case 'add_mark': {
if (selection) {
const { key, value } = command
addMark: (key: string, value: any) => {
const { selection } = editor
if (Range.isExpanded(selection)) {
Editor.setNodes(
editor,
{ [key]: value },
{ match: Text.isText, split: true }
)
} else {
const marks = {
...(Editor.marks(editor) || {}),
[key]: value,
}
editor.marks = marks
editor.onChange()
}
}
break
if (selection) {
if (Range.isExpanded(selection)) {
Transforms.setNodes(
editor,
{ [key]: value },
{ match: Text.isText, split: true }
)
} else {
const marks = {
...(Editor.marks(editor) || {}),
[key]: value,
}
case 'delete_backward': {
if (selection && Range.isCollapsed(selection)) {
Editor.delete(editor, { unit: command.unit, reverse: true })
}
break
}
case 'delete_forward': {
if (selection && Range.isCollapsed(selection)) {
Editor.delete(editor, { unit: command.unit })
}
break
}
case 'delete_fragment': {
if (selection && Range.isExpanded(selection)) {
Editor.delete(editor)
}
break
}
case 'insert_break': {
Editor.splitNodes(editor, { always: true })
break
}
case 'insert_fragment': {
Editor.insertFragment(editor, command.fragment)
break
}
case 'insert_node': {
Editor.insertNodes(editor, command.node)
break
}
case 'insert_text': {
if (selection) {
// If the cursor is at the end of an inline, move it outside of
// the inline before inserting
if (Range.isCollapsed(selection)) {
const inline = Editor.above(editor, {
match: n => Editor.isInline(editor, n),
mode: 'highest',
})
if (inline) {
const [, inlinePath] = inline
if (Editor.isEnd(editor, selection.anchor, inlinePath)) {
const point = Editor.after(editor, inlinePath)!
Editor.setSelection(editor, { anchor: point, focus: point })
}
}
}
const { marks } = editor
const { text } = command
if (marks) {
const node = { text, ...marks }
Editor.insertNodes(editor, node)
} else {
Editor.insertText(editor, text)
}
editor.marks = null
}
break
}
case 'remove_mark': {
if (selection) {
const { key } = command
if (Range.isExpanded(selection)) {
Editor.unsetNodes(editor, key, {
match: Text.isText,
split: true,
})
} else {
const marks = { ...(Editor.marks(editor) || {}) }
delete marks[key]
editor.marks = marks
editor.onChange()
}
}
break
}
editor.marks = marks
editor.onChange()
}
}
},
deleteBackward: (unit: 'character' | 'word' | 'line' | 'block') => {
const { selection } = editor
if (selection && Range.isCollapsed(selection)) {
Transforms.delete(editor, { unit, reverse: true })
}
},
deleteForward: (unit: 'character' | 'word' | 'line' | 'block') => {
const { selection } = editor
if (selection && Range.isCollapsed(selection)) {
Transforms.delete(editor, { unit })
}
},
deleteFragment: () => {
const { selection } = editor
if (selection && Range.isExpanded(selection)) {
Transforms.delete(editor)
}
},
insertBreak: () => {
Transforms.splitNodes(editor, { always: true })
},
insertFragment: (fragment: Node[]) => {
Transforms.insertFragment(editor, fragment)
},
insertNode: (node: Node) => {
Transforms.insertNodes(editor, node)
},
insertText: (text: string) => {
const { selection, marks } = editor
if (selection) {
// If the cursor is at the end of an inline, move it outside of
// the inline before inserting
if (Range.isCollapsed(selection)) {
const inline = Editor.above(editor, {
match: n => Editor.isInline(editor, n),
mode: 'highest',
})
if (inline) {
const [, inlinePath] = inline
if (Editor.isEnd(editor, selection.anchor, inlinePath)) {
const point = Editor.after(editor, inlinePath)!
Transforms.setSelection(editor, {
anchor: point,
focus: point,
})
}
}
}
if (marks) {
const node = { text, ...marks }
Transforms.insertNodes(editor, node)
} else {
Transforms.insertText(editor, text)
}
editor.marks = null
}
},
normalizeNode: (entry: NodeEntry) => {
const [node, path] = entry
@ -226,7 +194,10 @@ export const createEditor = (): Editor => {
// Ensure that block and inline nodes have at least one text child.
if (Element.isElement(node) && node.children.length === 0) {
const child = { text: '' }
Editor.insertNodes(editor, child, { at: path.concat(0), voids: true })
Transforms.insertNodes(editor, child, {
at: path.concat(0),
voids: true,
})
return
}
@ -256,21 +227,21 @@ export const createEditor = (): Editor => {
// other inline nodes, or parent blocks that only contain inlines and
// text.
if (isInlineOrText !== shouldHaveInlines) {
Editor.removeNodes(editor, { at: path.concat(n), voids: true })
Transforms.removeNodes(editor, { at: path.concat(n), voids: true })
n--
} else if (Element.isElement(child)) {
// Ensure that inline nodes are surrounded by text nodes.
if (editor.isInline(child)) {
if (prev == null || !Text.isText(prev)) {
const newChild = { text: '' }
Editor.insertNodes(editor, newChild, {
Transforms.insertNodes(editor, newChild, {
at: path.concat(n),
voids: true,
})
n++
} else if (isLast) {
const newChild = { text: '' }
Editor.insertNodes(editor, newChild, {
Transforms.insertNodes(editor, newChild, {
at: path.concat(n + 1),
voids: true,
})
@ -281,22 +252,43 @@ export const createEditor = (): Editor => {
// Merge adjacent text nodes that are empty or match.
if (prev != null && Text.isText(prev)) {
if (Text.equals(child, prev, { loose: true })) {
Editor.mergeNodes(editor, { at: path.concat(n), voids: true })
Transforms.mergeNodes(editor, { at: path.concat(n), voids: true })
n--
} else if (prev.text === '') {
Editor.removeNodes(editor, {
Transforms.removeNodes(editor, {
at: path.concat(n - 1),
voids: true,
})
n--
} else if (isLast && child.text === '') {
Editor.removeNodes(editor, { at: path.concat(n), voids: true })
Transforms.removeNodes(editor, {
at: path.concat(n),
voids: true,
})
n--
}
}
}
}
},
removeMark: (key: string) => {
const { selection } = editor
if (selection) {
if (Range.isExpanded(selection)) {
Transforms.unsetNodes(editor, key, {
match: Text.isText,
split: true,
})
} else {
const marks = { ...(Editor.marks(editor) || {}) }
delete marks[key]
editor.marks = marks
editor.onChange()
}
}
},
}
return editor

View File

@ -1,5 +1,4 @@
export * from './create-editor'
export * from './interfaces/command'
export * from './interfaces/editor'
export * from './interfaces/element'
export * from './interfaces/location'
@ -12,3 +11,4 @@ export * from './interfaces/point-ref'
export * from './interfaces/range'
export * from './interfaces/range-ref'
export * from './interfaces/text'
export * from './transforms'

View File

@ -1,153 +0,0 @@
import isPlainObject from 'is-plain-object'
import { Node } from '..'
/**
* `Command` objects represent an action that a user is taking on the editor.
* They capture the semantic "intent" of a user while they edit a document.
*/
export interface Command {
type: string
[key: string]: any
}
export const Command = {
/**
* Check if a value is a `Command` object.
*/
isCommand(value: any): value is Command {
return isPlainObject(value) && typeof value.type === 'string'
},
}
/**
* The `AddMarkCommand` adds properties to the text nodes in the selection.
*/
export interface AddMarkCommand {
type: 'add_mark'
key: string
value: any
}
/**
* The `DeleteBackwardCommand` delete's content backward, meaning before the
* current selection, by a specific `unit` of distance.
*/
export interface DeleteBackwardCommand {
type: 'delete_backward'
unit: 'character' | 'word' | 'line' | 'block'
}
/**
* The `DeleteForwardCommand` delete's content forward, meaning after the
* current selection, by a specific `unit` of distance.
*/
export interface DeleteForwardCommand {
type: 'delete_forward'
unit: 'character' | 'word' | 'line' | 'block'
}
/**
* The `DeleteFragmentCommand` delete's the content of the current selection.
*/
export interface DeleteFragmentCommand {
type: 'delete_fragment'
}
/**
* The `InsertBreakCommand` breaks a block in two at the current selection.
*/
export interface InsertBreakCommand {
type: 'insert_break'
}
/**
* The `InsertFragmentCommand` inserts a list of nodes at the current selection.
*/
export interface InsertFragmentCommand {
type: 'insert_fragment'
fragment: Node[]
}
/**
* The `InsertNodeCommand` inserts a node at the current selection.
*/
export interface InsertNodeCommand {
type: 'insert_node'
node: Node
}
/**
* The `InsertTextCommand` inserts a string of text at the current selection.
*/
export interface InsertTextCommand {
type: 'insert_text'
text: string
}
/**
* The `RemoveMarkCommand` removes properties from text nodes in the selection.
*/
export interface RemoveMarkCommand {
type: 'remove_mark'
key: string
}
/**
* The `CoreCommand` union is a set of all of the commands that are recognized
* by Slate's "core" out of the box.
*/
export type CoreCommand =
| AddMarkCommand
| DeleteBackwardCommand
| DeleteForwardCommand
| DeleteFragmentCommand
| InsertBreakCommand
| InsertFragmentCommand
| InsertNodeCommand
| InsertTextCommand
| RemoveMarkCommand
export const CoreCommand = {
/**
* Check if a value is a `CoreCommand` object.
*/
isCoreCommand(value: any): value is CoreCommand {
if (Command.isCommand(value)) {
switch (value.type) {
case 'add_mark':
return typeof value.key === 'string' && value.value != null
case 'delete_backward':
return typeof value.unit === 'string'
case 'delete_forward':
return typeof value.unit === 'string'
case 'delete_fragment':
return true
case 'insert_break':
return true
case 'insert_fragment':
return Node.isNodeList(value.fragment)
case 'insert_node':
return Node.isNode(value.node)
case 'insert_text':
return typeof value.text === 'string'
case 'remove_mark':
return typeof value.key === 'string'
}
}
return false
},
}

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +0,0 @@
import { Command, Element, Operation, Range, Node, NodeEntry } from '../..'
import { ElementQueries } from './queries/element'
import { GeneralTransforms } from './transforms/general'
import { GeneralQueries } from './queries/general'
import { LocationQueries } from './queries/location'
import { NodeTransforms } from './transforms/node'
import { RangeQueries } from './queries/range'
import { SelectionTransforms } from './transforms/selection'
import { TextTransforms } from './transforms/text'
/**
* The `Editor` interface stores all the state of a Slate editor. It is extended
* by plugins that wish to add their own helpers and implement new behaviors.
*/
export interface Editor {
children: Node[]
selection: Range | null
operations: Operation[]
marks: Record<string, any> | null
apply: (operation: Operation) => void
exec: (command: Command) => void
isInline: (element: Element) => boolean
isVoid: (element: Element) => boolean
normalizeNode: (entry: NodeEntry) => void
onChange: () => void
[key: string]: any
}
export const Editor = {
...ElementQueries,
...GeneralQueries,
...GeneralTransforms,
...LocationQueries,
...NodeTransforms,
...RangeQueries,
...SelectionTransforms,
...TextTransforms,
}

View File

@ -1,45 +0,0 @@
import { Editor, Element, Text } from '../../..'
export const ElementQueries = {
/**
* Check if a node has block children.
*/
hasBlocks(editor: Editor, element: Element): boolean {
return element.children.some(n => Editor.isBlock(editor, n))
},
/**
* Check if a node has inline and text children.
*/
hasInlines(editor: Editor, element: Element): boolean {
return element.children.some(
n => Text.isText(n) || Editor.isInline(editor, n)
)
},
/**
* Check if a node has text children.
*/
hasTexts(editor: Editor, element: Element): boolean {
return element.children.every(n => Text.isText(n))
},
/**
* Check if an element is empty, accounting for void nodes.
*/
isEmpty(editor: Editor, element: Element): boolean {
const { children } = element
const [first] = children
return (
children.length === 0 ||
(children.length === 1 &&
Text.isText(first) &&
first.text === '' &&
!editor.isVoid(element))
)
},
}

View File

@ -1,273 +0,0 @@
import isPlainObject from 'is-plain-object'
import {
Editor,
Operation,
Path,
Point,
Text,
PathRef,
PointRef,
Element,
NodeEntry,
Range,
RangeRef,
Node,
} from '../../..'
export const NORMALIZING: WeakMap<Editor, boolean> = new WeakMap()
export const PATH_REFS: WeakMap<Editor, Set<PathRef>> = new WeakMap()
export const POINT_REFS: WeakMap<Editor, Set<PointRef>> = new WeakMap()
export const RANGE_REFS: WeakMap<Editor, Set<RangeRef>> = new WeakMap()
export const GeneralQueries = {
/**
* Check if a value is a block `Element` object.
*/
isBlock(editor: Editor, value: any): value is Element {
return Element.isElement(value) && !editor.isInline(value)
},
/**
* Check if a value is an `Editor` object.
*/
isEditor(value: any): value is Editor {
return (
isPlainObject(value) &&
typeof value.apply === 'function' &&
typeof value.exec === 'function' &&
typeof value.isInline === 'function' &&
typeof value.isVoid === 'function' &&
typeof value.normalizeNode === 'function' &&
typeof value.onChange === 'function' &&
(value.marks === null || isPlainObject(value.marks)) &&
(value.selection === null || Range.isRange(value.selection)) &&
Node.isNodeList(value.children) &&
Operation.isOperationList(value.operations)
)
},
/**
* Check if a value is an inline `Element` object.
*/
isInline(editor: Editor, value: any): value is Element {
return Element.isElement(value) && editor.isInline(value)
},
/**
* Check if the editor is currently normalizing after each operation.
*/
isNormalizing(editor: Editor): boolean {
const isNormalizing = NORMALIZING.get(editor)
return isNormalizing === undefined ? true : isNormalizing
},
/**
* Check if a value is a void `Element` object.
*/
isVoid(editor: Editor, value: any): value is Element {
return Element.isElement(value) && editor.isVoid(value)
},
/**
* Get the marks that would be added to text at the current selection.
*/
marks(editor: Editor): Record<string, any> | null {
const { marks, selection } = editor
if (!selection) {
return null
}
if (marks) {
return marks
}
if (Range.isExpanded(selection)) {
const [match] = Editor.nodes(editor, { match: Text.isText })
if (match) {
const [node] = match as NodeEntry<Text>
const { text, ...rest } = node
return rest
} else {
return {}
}
}
const { anchor } = selection
const { path } = anchor
let [node] = Editor.leaf(editor, path)
if (anchor.offset === 0) {
const prev = Editor.previous(editor, { at: path, match: Text.isText })
const block = Editor.above(editor, {
match: n => Editor.isBlock(editor, n),
})
if (prev && block) {
const [prevNode, prevPath] = prev
const [, blockPath] = block
if (Path.isAncestor(blockPath, prevPath)) {
node = prevNode as Text
}
}
}
const { text, ...rest } = node
return rest
},
/**
* Create a mutable ref for a `Path` object, which will stay in sync as new
* operations are applied to the editor.
*/
pathRef(
editor: Editor,
path: Path,
options: {
affinity?: 'backward' | 'forward' | null
} = {}
): PathRef {
const { affinity = 'forward' } = options
const ref: PathRef = {
current: path,
affinity,
unref() {
const { current } = ref
const pathRefs = Editor.pathRefs(editor)
pathRefs.delete(ref)
ref.current = null
return current
},
}
const refs = Editor.pathRefs(editor)
refs.add(ref)
return ref
},
/**
* Get the set of currently tracked path refs of the editor.
*/
pathRefs(editor: Editor): Set<PathRef> {
let refs = PATH_REFS.get(editor)
if (!refs) {
refs = new Set()
PATH_REFS.set(editor, refs)
}
return refs
},
/**
* Create a mutable ref for a `Point` object, which will stay in sync as new
* operations are applied to the editor.
*/
pointRef(
editor: Editor,
point: Point,
options: {
affinity?: 'backward' | 'forward' | null
} = {}
): PointRef {
const { affinity = 'forward' } = options
const ref: PointRef = {
current: point,
affinity,
unref() {
const { current } = ref
const pointRefs = Editor.pointRefs(editor)
pointRefs.delete(ref)
ref.current = null
return current
},
}
const refs = Editor.pointRefs(editor)
refs.add(ref)
return ref
},
/**
* Get the set of currently tracked point refs of the editor.
*/
pointRefs(editor: Editor): Set<PointRef> {
let refs = POINT_REFS.get(editor)
if (!refs) {
refs = new Set()
POINT_REFS.set(editor, refs)
}
return refs
},
/**
* Create a mutable ref for a `Range` object, which will stay in sync as new
* operations are applied to the editor.
*/
rangeRef(
editor: Editor,
range: Range,
options: {
affinity?: 'backward' | 'forward' | 'outward' | 'inward' | null
} = {}
): RangeRef {
const { affinity = 'forward' } = options
const ref: RangeRef = {
current: range,
affinity,
unref() {
const { current } = ref
const rangeRefs = Editor.rangeRefs(editor)
rangeRefs.delete(ref)
ref.current = null
return current
},
}
const refs = Editor.rangeRefs(editor)
refs.add(ref)
return ref
},
/**
* Get the set of currently tracked range refs of the editor.
*/
rangeRefs(editor: Editor): Set<RangeRef> {
let refs = RANGE_REFS.get(editor)
if (!refs) {
refs = new Set()
RANGE_REFS.set(editor, refs)
}
return refs
},
/**
* Call a function, deferring normalization until after it completes.
*/
withoutNormalizing(editor: Editor, fn: () => void): void {
const value = Editor.isNormalizing(editor)
NORMALIZING.set(editor, false)
fn()
NORMALIZING.set(editor, value)
Editor.normalize(editor)
},
}

View File

@ -1,993 +0,0 @@
import { reverse as reverseText } from 'esrever'
import {
Ancestor,
Descendant,
Editor,
Element,
Location,
Node,
NodeEntry,
Path,
Point,
Range,
Span,
Text,
} from '../../..'
export const LocationQueries = {
/**
* Get the ancestor above a location in the document.
*/
above<T extends Ancestor>(
editor: Editor,
options: {
at?: Location
match?: NodeMatch<T>
mode?: 'highest' | 'lowest'
voids?: boolean
} = {}
): NodeEntry<T> | undefined {
const {
voids = false,
mode = 'lowest',
at = editor.selection,
match,
} = options
if (!at) {
return
}
const path = Editor.path(editor, at)
const reverse = mode === 'lowest'
for (const [n, p] of Editor.levels(editor, {
at: path,
voids,
match,
reverse,
})) {
if (!Text.isText(n) && !Path.equals(path, p)) {
return [n, p]
}
}
},
/**
* Get the point after a location.
*/
after(
editor: Editor,
at: Location,
options: {
distance?: number
unit?: 'offset' | 'character' | 'word' | 'line' | 'block'
} = {}
): Point | undefined {
const anchor = Editor.point(editor, at, { edge: 'end' })
const focus = Editor.end(editor, [])
const range = { anchor, focus }
const { distance = 1 } = options
let d = 0
let target
for (const p of Editor.positions(editor, { ...options, at: range })) {
if (d > distance) {
break
}
if (d !== 0) {
target = p
}
d++
}
return target
},
/**
* Get the point before a location.
*/
before(
editor: Editor,
at: Location,
options: {
distance?: number
unit?: 'offset' | 'character' | 'word' | 'line' | 'block'
} = {}
): Point | undefined {
const anchor = Editor.start(editor, [])
const focus = Editor.point(editor, at, { edge: 'start' })
const range = { anchor, focus }
const { distance = 1 } = options
let d = 0
let target
for (const p of Editor.positions(editor, {
...options,
at: range,
reverse: true,
})) {
if (d > distance) {
break
}
if (d !== 0) {
target = p
}
d++
}
return target
},
/**
* Get the start and end points of a location.
*/
edges(editor: Editor, at: Location): [Point, Point] {
return [Editor.start(editor, at), Editor.end(editor, at)]
},
/**
* Get the end point of a location.
*/
end(editor: Editor, at: Location): Point {
return Editor.point(editor, at, { edge: 'end' })
},
/**
* Get the first node at a location.
*/
first(editor: Editor, at: Location): NodeEntry {
const path = Editor.path(editor, at, { edge: 'start' })
return Editor.node(editor, path)
},
/**
* Get the fragment at a location.
*/
fragment(editor: Editor, at: Location): Descendant[] {
const range = Editor.range(editor, at)
const fragment = Node.fragment(editor, range)
return fragment
},
/**
* Check if a point is the end point of a location.
*/
isEnd(editor: Editor, point: Point, at: Location): boolean {
const end = Editor.end(editor, at)
return Point.equals(point, end)
},
/**
* Check if a point is an edge of a location.
*/
isEdge(editor: Editor, point: Point, at: Location): boolean {
return Editor.isStart(editor, point, at) || Editor.isEnd(editor, point, at)
},
/**
* Check if a point is the start point of a location.
*/
isStart(editor: Editor, point: Point, at: Location): boolean {
// PERF: If the offset isn't `0` we know it's not the start.
if (point.offset !== 0) {
return false
}
const start = Editor.start(editor, at)
return Point.equals(point, start)
},
/**
* Get the last node at a location.
*/
last(editor: Editor, at: Location): NodeEntry {
const path = Editor.path(editor, at, { edge: 'end' })
return Editor.node(editor, path)
},
/**
* Get the leaf text node at a location.
*/
leaf(
editor: Editor,
at: Location,
options: {
depth?: number
edge?: 'start' | 'end'
} = {}
): NodeEntry<Text> {
const path = Editor.path(editor, at, options)
const node = Node.leaf(editor, path)
return [node, path]
},
/**
* Iterate through all of the levels at a location.
*/
*levels<T extends Node>(
editor: Editor,
options: {
at?: Location
match?: NodeMatch<T>
reverse?: boolean
voids?: boolean
} = {}
): Iterable<NodeEntry<T>> {
const { at = editor.selection, reverse = false, voids = false } = options
let { match } = options
if (match == null) {
match = () => true
}
if (!at) {
return
}
const levels: NodeEntry<T>[] = []
const path = Editor.path(editor, at)
for (const [n, p] of Node.levels(editor, path)) {
if (!match(n)) {
continue
}
levels.push([n, p])
if (!voids && Editor.isVoid(editor, n)) {
break
}
}
if (reverse) {
levels.reverse()
}
yield* levels
},
/**
* Get the matching node in the branch of the document after a location.
*/
next<T extends Node>(
editor: Editor,
options: {
at?: Location
match?: NodeMatch<T>
mode?: 'all' | 'highest' | 'lowest'
voids?: boolean
} = {}
): NodeEntry<T> | undefined {
const { mode = 'lowest', voids = false } = options
let { match, at = editor.selection } = options
if (!at) {
return
}
const [, from] = Editor.last(editor, at)
const [, to] = Editor.last(editor, [])
const span: Span = [from, to]
if (Path.isPath(at) && at.length === 0) {
throw new Error(`Cannot get the next node from the root node!`)
}
if (match == null) {
if (Path.isPath(at)) {
const [parent] = Editor.parent(editor, at)
match = n => parent.children.includes(n)
} else {
match = () => true
}
}
const [, next] = Editor.nodes(editor, { at: span, match, mode, voids })
return next
},
/**
* Get the node at a location.
*/
node(
editor: Editor,
at: Location,
options: {
depth?: number
edge?: 'start' | 'end'
} = {}
): NodeEntry {
const path = Editor.path(editor, at, options)
const node = Node.get(editor, path)
return [node, path]
},
/**
* Iterate through all of the nodes in the Editor.
*/
*nodes<T extends Node>(
editor: Editor,
options: {
at?: Location | Span
match?: NodeMatch<T>
mode?: 'all' | 'highest' | 'lowest'
universal?: boolean
reverse?: boolean
voids?: boolean
} = {}
): Iterable<NodeEntry<T>> {
const {
at = editor.selection,
mode = 'all',
universal = false,
reverse = false,
voids = false,
} = options
let { match } = options
if (!match) {
match = () => true
}
if (!at) {
return
}
let from
let to
if (Span.isSpan(at)) {
from = at[0]
to = at[1]
} else {
const first = Editor.path(editor, at, { edge: 'start' })
const last = Editor.path(editor, at, { edge: 'end' })
from = reverse ? last : first
to = reverse ? first : last
}
const iterable = Node.nodes(editor, {
reverse,
from,
to,
pass: ([n]) => (voids ? false : Editor.isVoid(editor, n)),
})
const matches: NodeEntry<T>[] = []
let hit: NodeEntry<T> | undefined
for (const [node, path] of iterable) {
const isLower = hit && Path.compare(path, hit[1]) === 0
// In highest mode any node lower than the last hit is not a match.
if (mode === 'highest' && isLower) {
continue
}
if (!match(node)) {
// If we've arrived at a leaf text node that is not lower than the last
// hit, then we've found a branch that doesn't include a match, which
// means the match is not universal.
if (universal && !isLower && Text.isText(node)) {
return
} else {
continue
}
}
// If there's a match and it's lower than the last, update the hit.
if (mode === 'lowest' && isLower) {
hit = [node, path]
continue
}
// In lowest mode we emit the last hit, once it's guaranteed lowest.
const emit: NodeEntry<T> | undefined =
mode === 'lowest' ? hit : [node, path]
if (emit) {
if (universal) {
matches.push(emit)
} else {
yield emit
}
}
hit = [node, path]
}
// Since lowest is always emitting one behind, catch up at the end.
if (mode === 'lowest' && hit) {
if (universal) {
matches.push(hit)
} else {
yield hit
}
}
// Universal defers to ensure that the match occurs in every branch, so we
// yield all of the matches after iterating.
if (universal) {
yield* matches
}
},
/**
* Get the parent node of a location.
*/
parent(
editor: Editor,
at: Location,
options: {
depth?: number
edge?: 'start' | 'end'
} = {}
): NodeEntry<Ancestor> {
const path = Editor.path(editor, at, options)
const parentPath = Path.parent(path)
const entry = Editor.node(editor, parentPath)
return entry as NodeEntry<Ancestor>
},
/**
* Get the path of a location.
*/
path(
editor: Editor,
at: Location,
options: {
depth?: number
edge?: 'start' | 'end'
} = {}
): Path {
const { depth, edge } = options
if (Path.isPath(at)) {
if (edge === 'start') {
const [, firstPath] = Node.first(editor, at)
at = firstPath
} else if (edge === 'end') {
const [, lastPath] = Node.last(editor, at)
at = lastPath
}
}
if (Range.isRange(at)) {
if (edge === 'start') {
at = Range.start(at)
} else if (edge === 'end') {
at = Range.end(at)
} else {
at = Path.common(at.anchor.path, at.focus.path)
}
}
if (Point.isPoint(at)) {
at = at.path
}
if (depth != null) {
at = at.slice(0, depth)
}
return at
},
/**
* Get the start or end point of a location.
*/
point(
editor: Editor,
at: Location,
options: {
edge?: 'start' | 'end'
} = {}
): Point {
const { edge = 'start' } = options
if (Path.isPath(at)) {
let path
if (edge === 'end') {
const [, lastPath] = Node.last(editor, at)
path = lastPath
} else {
const [, firstPath] = Node.first(editor, at)
path = firstPath
}
const node = Node.get(editor, path)
if (!Text.isText(node)) {
throw new Error(
`Cannot get the ${edge} point in the node at path [${at}] because it has no ${edge} text node.`
)
}
return { path, offset: edge === 'end' ? node.text.length : 0 }
}
if (Range.isRange(at)) {
const [start, end] = Range.edges(at)
return edge === 'start' ? start : end
}
return at
},
/**
* Iterate through all of the positions in the document where a `Point` can be
* placed.
*
* By default it will move forward by individual offsets at a time, but you
* can pass the `unit: 'character'` option to moved forward one character, word,
* or line at at time.
*
* Note: void nodes are treated as a single point, and iteration will not
* happen inside their content.
*/
*positions(
editor: Editor,
options: {
at?: Location
unit?: 'offset' | 'character' | 'word' | 'line' | 'block'
reverse?: boolean
} = {}
): Iterable<Point> {
const { at = editor.selection, unit = 'offset', reverse = false } = options
if (!at) {
return
}
const range = Editor.range(editor, at)
const [start, end] = Range.edges(range)
const first = reverse ? end : start
let string = ''
let available = 0
let offset = 0
let distance: number | null = null
let isNewBlock = false
const advance = () => {
if (distance == null) {
if (unit === 'character') {
distance = getCharacterDistance(string)
} else if (unit === 'word') {
distance = getWordDistance(string)
} else if (unit === 'line' || unit === 'block') {
distance = string.length
} else {
distance = 1
}
string = string.slice(distance)
}
// Add or substract the offset.
offset = reverse ? offset - distance : offset + distance
// Subtract the distance traveled from the available text.
available = available - distance!
// If the available had room to spare, reset the distance so that it will
// advance again next time. Otherwise, set it to the overflow amount.
distance = available >= 0 ? null : 0 - available
}
for (const [node, path] of Editor.nodes(editor, { at, reverse })) {
if (Element.isElement(node)) {
// Void nodes are a special case, since we don't want to iterate over
// their content. We instead always just yield their first point.
if (editor.isVoid(node)) {
yield Editor.start(editor, path)
continue
}
if (editor.isInline(node)) {
continue
}
if (Editor.hasInlines(editor, node)) {
const e = Path.isAncestor(path, end.path)
? end
: Editor.end(editor, path)
const s = Path.isAncestor(path, start.path)
? start
: Editor.start(editor, path)
const text = Editor.string(editor, { anchor: s, focus: e })
string = reverse ? reverseText(text) : text
isNewBlock = true
}
}
if (Text.isText(node)) {
const isFirst = Path.equals(path, first.path)
available = node.text.length
offset = reverse ? available : 0
if (isFirst) {
available = reverse ? first.offset : available - first.offset
offset = first.offset
}
if (isFirst || isNewBlock || unit === 'offset') {
yield { path, offset }
}
while (true) {
// If there's no more string, continue to the next block.
if (string === '') {
break
} else {
advance()
}
// If the available space hasn't overflow, we have another point to
// yield in the current text node.
if (available >= 0) {
yield { path, offset }
} else {
break
}
}
isNewBlock = false
}
}
},
/**
* Get the matching node in the branch of the document before a location.
*/
previous<T extends Node>(
editor: Editor,
options: {
at?: Location
match?: NodeMatch<T>
mode?: 'all' | 'highest' | 'lowest'
voids?: boolean
} = {}
): NodeEntry<T> | undefined {
const { mode = 'lowest', voids = false } = options
let { match, at = editor.selection } = options
if (!at) {
return
}
const [, from] = Editor.first(editor, at)
const [, to] = Editor.first(editor, [])
const span: Span = [from, to]
if (Path.isPath(at) && at.length === 0) {
throw new Error(`Cannot get the previous node from the root node!`)
}
if (match == null) {
if (Path.isPath(at)) {
const [parent] = Editor.parent(editor, at)
match = n => parent.children.includes(n)
} else {
match = () => true
}
}
const [, previous] = Editor.nodes(editor, {
reverse: true,
at: span,
match,
mode,
voids,
})
return previous
},
/**
* Get a range of a location.
*/
range(editor: Editor, at: Location, to?: Location): Range {
if (Range.isRange(at) && !to) {
return at
}
const start = Editor.start(editor, at)
const end = Editor.end(editor, to || at)
return { anchor: start, focus: end }
},
/**
* Get the start point of a location.
*/
start(editor: Editor, at: Location): Point {
return Editor.point(editor, at, { edge: 'start' })
},
/**
* Get the text string content of a location.
*
* Note: the text of void nodes is presumed to be an empty string, regardless
* of what their actual content is.
*/
string(editor: Editor, at: Location): string {
const range = Editor.range(editor, at)
const [start, end] = Range.edges(range)
let text = ''
for (const [node, path] of Editor.nodes(editor, {
at: range,
match: Text.isText,
})) {
let t = node.text
if (Path.equals(path, end.path)) {
t = t.slice(0, end.offset)
}
if (Path.equals(path, start.path)) {
t = t.slice(start.offset)
}
text += t
}
return text
},
/**
* Match a void node in the current branch of the editor.
*/
void(
editor: Editor,
options: {
at?: Location
mode?: 'highest' | 'lowest'
voids?: boolean
} = {}
): NodeEntry<Element> | undefined {
return Editor.above(editor, {
...options,
match: n => Editor.isVoid(editor, n),
})
},
}
/**
* Constants for string distance checking.
*/
const SPACE = /\s/
const PUNCTUATION = /[\u0021-\u0023\u0025-\u002A\u002C-\u002F\u003A\u003B\u003F\u0040\u005B-\u005D\u005F\u007B\u007D\u00A1\u00A7\u00AB\u00B6\u00B7\u00BB\u00BF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E3B\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]/
const CHAMELEON = /['\u2018\u2019]/
const SURROGATE_START = 0xd800
const SURROGATE_END = 0xdfff
const ZERO_WIDTH_JOINER = 0x200d
/**
* Check if a character is a word character. The `remaining` argument is used
* because sometimes you must read subsequent characters to truly determine it.
*/
const isWordCharacter = (char: string, remaining: string): boolean => {
if (SPACE.test(char)) {
return false
}
// Chameleons count as word characters as long as they're in a word, so
// recurse to see if the next one is a word character or not.
if (CHAMELEON.test(char)) {
let next = remaining.charAt(0)
const length = getCharacterDistance(next)
next = remaining.slice(0, length)
const rest = remaining.slice(length)
if (isWordCharacter(next, rest)) {
return true
}
}
if (PUNCTUATION.test(char)) {
return false
}
return true
}
/**
* Get the distance to the end of the first character in a string of text.
*/
const getCharacterDistance = (text: string): number => {
let offset = 0
// prev types:
// SURR: surrogate pair
// MOD: modifier (technically also surrogate pair)
// ZWJ: zero width joiner
// VAR: variation selector
// BMP: sequenceable character from basic multilingual plane
let prev: 'SURR' | 'MOD' | 'ZWJ' | 'VAR' | 'BMP' | null = null
let charCode = text.charCodeAt(0)
while (charCode) {
if (isSurrogate(charCode)) {
const modifier = isModifier(charCode, text, offset)
// Early returns are the heart of this function, where we decide if previous and current
// codepoints should form a single character (in terms of how many of them should selection
// jump over).
if (prev === 'SURR' || prev === 'BMP') {
break
}
offset += 2
prev = modifier ? 'MOD' : 'SURR'
charCode = text.charCodeAt(offset)
// Absolutely fine to `continue` without any checks because if `charCode` is NaN (which
// is the case when out of `text` range), next `while` loop won"t execute and we"re done.
continue
}
if (charCode === ZERO_WIDTH_JOINER) {
offset += 1
prev = 'ZWJ'
charCode = text.charCodeAt(offset)
continue
}
if (isBMPEmoji(charCode)) {
if (prev && prev !== 'ZWJ' && prev !== 'VAR') {
break
}
offset += 1
prev = 'BMP'
charCode = text.charCodeAt(offset)
continue
}
if (isVariationSelector(charCode)) {
if (prev && prev !== 'ZWJ') {
break
}
offset += 1
prev = 'VAR'
charCode = text.charCodeAt(offset)
continue
}
// Modifier 'groups up' with what ever character is before that (even whitespace), need to
// look ahead.
if (prev === 'MOD') {
offset += 1
break
}
// If while loop ever gets here, we're done (e.g latin chars).
break
}
return offset || 1
}
/**
* Get the distance to the end of the first word in a string of text.
*/
const getWordDistance = (text: string): number => {
let length = 0
let i = 0
let started = false
let char
while ((char = text.charAt(i))) {
const l = getCharacterDistance(char)
char = text.slice(i, i + l)
const rest = text.slice(i + l)
if (isWordCharacter(char, rest)) {
started = true
length += l
} else if (!started) {
length += l
} else {
break
}
i += l
}
return length
}
/**
* Determines if `code` is a surrogate
*/
const isSurrogate = (code: number): boolean =>
SURROGATE_START <= code && code <= SURROGATE_END
/**
* Does `code` form Modifier with next one.
*
* https://emojipedia.org/modifiers/
*/
const isModifier = (code: number, text: string, offset: number): boolean => {
if (code === 0xd83c) {
const next = text.charCodeAt(offset + 1)
return next <= 0xdfff && next >= 0xdffb
}
return false
}
/**
* Is `code` a Variation Selector.
*
* https://codepoints.net/variation_selectors
*/
const isVariationSelector = (code: number): boolean => {
return code <= 0xfe0f && code >= 0xfe00
}
/**
* Is `code` one of the BMP codes used in emoji sequences.
*
* https://emojipedia.org/emoji-zwj-sequences/
*/
const isBMPEmoji = (code: number): boolean => {
// This requires tiny bit of maintanance, better ideas?
// Fortunately it only happens if new Unicode Standard
// is released. Fails gracefully if upkeep lags behind,
// same way Slate previously behaved with all emojis.
return (
code === 0x2764 || // heart (❤)
code === 0x2642 || // male (♂)
code === 0x2640 || // female (♀)
code === 0x2620 || // scull (☠)
code === 0x2695 || // medical (⚕)
code === 0x2708 || // plane (✈️)
code === 0x25ef // large circle (◯)
)
}
/**
* A helper type for narrowing matched nodes with a predicate.
*/
type NodeMatch<T extends Node> =
| ((node: Node) => node is T)
| ((node: Node) => boolean)

View File

@ -1,51 +0,0 @@
import { Editor, Text, Path, Range } from '../../..'
export const RangeQueries = {
/**
* Convert a range into a non-hanging one.
*/
unhangRange(
editor: Editor,
range: Range,
options: {
voids?: boolean
} = {}
): Range {
const { voids = false } = options
let [start, end] = Range.edges(range)
// PERF: exit early if we can guarantee that the range isn't hanging.
if (start.offset !== 0 || end.offset !== 0 || Range.isCollapsed(range)) {
return range
}
const endBlock = Editor.above(editor, {
at: end,
match: n => Editor.isBlock(editor, n),
})
const blockPath = endBlock ? endBlock[1] : []
const first = Editor.start(editor, [])
const before = { anchor: first, focus: end }
let skip = true
for (const [node, path] of Editor.nodes(editor, {
at: before,
match: Text.isText,
reverse: true,
voids,
})) {
if (skip) {
skip = false
continue
}
if (node.text !== '' || Path.isBefore(path, blockPath)) {
end = { path, offset: node.text.length }
break
}
}
return { anchor: start, focus: end }
},
}

View File

@ -10,55 +10,10 @@ import {
Descendant,
NodeEntry,
Path,
} from '../../..'
export const DIRTY_PATHS: WeakMap<Editor, Path[]> = new WeakMap()
Transforms,
} from '..'
export const GeneralTransforms = {
/**
* Normalize any dirty objects in the editor.
*/
normalize(
editor: Editor,
options: {
force?: boolean
} = {}
) {
const { force = false } = options
if (!Editor.isNormalizing(editor)) {
return
}
if (force) {
const allPaths = Array.from(Node.nodes(editor), ([, p]) => p)
DIRTY_PATHS.set(editor, allPaths)
}
if (getDirtyPaths(editor).length === 0) {
return
}
Editor.withoutNormalizing(editor, () => {
const max = getDirtyPaths(editor).length * 42 // HACK: better way?
let m = 0
while (getDirtyPaths(editor).length !== 0) {
if (m > max) {
throw new Error(`
Could not completely normalize the editor after ${max} iterations! This is usually due to incorrect normalization logic that leaves a node in an invalid state.
`)
}
const path = getDirtyPaths(editor).pop()!
const entry = Editor.node(editor, path)
editor.normalizeNode(entry)
m++
}
})
},
/**
* Transform the editor by an operation.
*/
@ -328,7 +283,3 @@ export const GeneralTransforms = {
}
},
}
const getDirtyPaths = (editor: Editor) => {
return DIRTY_PATHS.get(editor) || []
}

View File

@ -0,0 +1,11 @@
import { GeneralTransforms } from './general'
import { NodeTransforms } from './node'
import { SelectionTransforms } from './selection'
import { TextTransforms } from './text'
export const Transforms = {
...GeneralTransforms,
...NodeTransforms,
...SelectionTransforms,
...TextTransforms,
}

View File

@ -7,7 +7,8 @@ import {
Point,
Range,
Text,
} from '../../..'
Transforms,
} from '..'
export const NodeTransforms = {
/**
@ -69,7 +70,7 @@ export const NodeTransforms = {
} else {
const [, end] = Range.edges(at)
const pointRef = Editor.pointRef(editor, end)
Editor.delete(editor, { at })
Transforms.delete(editor, { at })
at = pointRef.unref()!
}
}
@ -96,7 +97,7 @@ export const NodeTransforms = {
const [, matchPath] = entry
const pathRef = Editor.pathRef(editor, matchPath)
const isAtEnd = Editor.isEnd(editor, at, matchPath)
Editor.splitNodes(editor, { at, match, mode, voids })
Transforms.splitNodes(editor, { at, match, mode, voids })
const path = pathRef.unref()!
at = isAtEnd ? Path.next(path) : path
} else {
@ -121,7 +122,7 @@ export const NodeTransforms = {
const point = Editor.end(editor, at)
if (point) {
Editor.select(editor, point)
Transforms.select(editor, point)
}
}
})
@ -173,18 +174,18 @@ export const NodeTransforms = {
if (length === 1) {
const toPath = Path.next(parentPath)
Editor.moveNodes(editor, { at: path, to: toPath, voids })
Editor.removeNodes(editor, { at: parentPath, voids })
Transforms.moveNodes(editor, { at: path, to: toPath, voids })
Transforms.removeNodes(editor, { at: parentPath, voids })
} else if (index === 0) {
Editor.moveNodes(editor, { at: path, to: parentPath, voids })
Transforms.moveNodes(editor, { at: path, to: parentPath, voids })
} else if (index === length - 1) {
const toPath = Path.next(parentPath)
Editor.moveNodes(editor, { at: path, to: toPath, voids })
Transforms.moveNodes(editor, { at: path, to: toPath, voids })
} else {
const splitPath = Path.next(path)
const toPath = Path.next(parentPath)
Editor.splitNodes(editor, { at: splitPath, voids })
Editor.moveNodes(editor, { at: path, to: toPath, voids })
Transforms.splitNodes(editor, { at: splitPath, voids })
Transforms.moveNodes(editor, { at: path, to: toPath, voids })
}
}
})
@ -232,11 +233,11 @@ export const NodeTransforms = {
} else {
const [, end] = Range.edges(at)
const pointRef = Editor.pointRef(editor, end)
Editor.delete(editor, { at })
Transforms.delete(editor, { at })
at = pointRef.unref()!
if (options.at == null) {
Editor.select(editor, at)
Transforms.select(editor, at)
}
}
}
@ -296,13 +297,13 @@ export const NodeTransforms = {
// If the node isn't already the next sibling of the previous node, move
// it so that it is before merging.
if (!isPreviousSibling) {
Editor.moveNodes(editor, { at: path, to: newPath, voids })
Transforms.moveNodes(editor, { at: path, to: newPath, voids })
}
// If there was going to be an empty ancestor of the node that was merged,
// we remove it from the tree.
if (emptyRef) {
Editor.removeNodes(editor, { at: emptyRef.current!, voids })
Transforms.removeNodes(editor, { at: emptyRef.current!, voids })
}
// If the target node that we're merging with is empty, remove it instead
@ -313,7 +314,7 @@ export const NodeTransforms = {
(Element.isElement(prevNode) && Editor.isEmpty(editor, prevNode)) ||
(Text.isText(prevNode) && prevNode.text === '')
) {
Editor.removeNodes(editor, { at: prevPath, voids })
Transforms.removeNodes(editor, { at: prevPath, voids })
} else {
editor.apply({
type: 'merge_node',
@ -469,12 +470,22 @@ export const NodeTransforms = {
const rangeRef = Editor.rangeRef(editor, at, { affinity: 'inward' })
const [start, end] = Range.edges(at)
const splitMode = mode === 'lowest' ? 'lowest' : 'highest'
Editor.splitNodes(editor, { at: end, match, mode: splitMode, voids })
Editor.splitNodes(editor, { at: start, match, mode: splitMode, voids })
Transforms.splitNodes(editor, {
at: end,
match,
mode: splitMode,
voids,
})
Transforms.splitNodes(editor, {
at: start,
match,
mode: splitMode,
voids,
})
at = rangeRef.unref()!
if (options.at == null) {
Editor.select(editor, at)
Transforms.select(editor, at)
}
}
@ -579,7 +590,7 @@ export const NodeTransforms = {
if (!after) {
const text = { text: '' }
const afterPath = Path.next(voidPath)
Editor.insertNodes(editor, text, { at: afterPath, voids })
Transforms.insertNodes(editor, text, { at: afterPath, voids })
after = Editor.point(editor, afterPath)!
}
@ -635,7 +646,7 @@ export const NodeTransforms = {
if (options.at == null) {
const point = afterRef.current || Editor.end(editor, [])
Editor.select(editor, point)
Transforms.select(editor, point)
}
beforeRef.unref()
@ -668,7 +679,7 @@ export const NodeTransforms = {
obj[key] = null
}
Editor.setNodes(editor, obj, options)
Transforms.setNodes(editor, obj, options)
},
/**
@ -717,7 +728,7 @@ export const NodeTransforms = {
range = Range.intersection(rangeRef.current!, range)!
}
Editor.liftNodes(editor, {
Transforms.liftNodes(editor, {
at: range,
match: n => node.children.includes(n),
voids,
@ -769,12 +780,12 @@ export const NodeTransforms = {
const rangeRef = Editor.rangeRef(editor, at, {
affinity: 'inward',
})
Editor.splitNodes(editor, { at: end, match, voids })
Editor.splitNodes(editor, { at: start, match, voids })
Transforms.splitNodes(editor, { at: end, match, voids })
Transforms.splitNodes(editor, { at: start, match, voids })
at = rangeRef.unref()!
if (options.at == null) {
Editor.select(editor, at)
Transforms.select(editor, at)
}
}
@ -816,9 +827,9 @@ export const NodeTransforms = {
const depth = commonPath.length + 1
const wrapperPath = Path.next(lastPath.slice(0, depth))
const wrapper = { ...element, children: [] }
Editor.insertNodes(editor, wrapper, { at: wrapperPath, voids })
Transforms.insertNodes(editor, wrapper, { at: wrapperPath, voids })
Editor.moveNodes(editor, {
Transforms.moveNodes(editor, {
at: range,
match: n => commonNode.children.includes(n),
to: wrapperPath.concat(0),
@ -840,7 +851,7 @@ const deleteRange = (editor: Editor, range: Range): Point | null => {
} else {
const [, end] = Range.edges(range)
const pointRef = Editor.pointRef(editor, end)
Editor.delete(editor, { at: range })
Transforms.delete(editor, { at: range })
return pointRef.unref()
}
}

View File

@ -1,4 +1,4 @@
import { Editor, Location, Point, Range } from '../../..'
import { Editor, Location, Point, Range, Transforms } from '..'
export const SelectionTransforms = {
/**
@ -17,15 +17,15 @@ export const SelectionTransforms = {
if (!selection) {
return
} else if (edge === 'anchor') {
Editor.select(editor, selection.anchor)
Transforms.select(editor, selection.anchor)
} else if (edge === 'focus') {
Editor.select(editor, selection.focus)
Transforms.select(editor, selection.focus)
} else if (edge === 'start') {
const [start] = Range.edges(selection)
Editor.select(editor, start)
Transforms.select(editor, start)
} else if (edge === 'end') {
const [, end] = Range.edges(selection)
Editor.select(editor, end)
Transforms.select(editor, end)
}
},
@ -98,7 +98,7 @@ export const SelectionTransforms = {
}
}
Editor.setSelection(editor, props)
Transforms.setSelection(editor, props)
},
/**
@ -110,7 +110,7 @@ export const SelectionTransforms = {
target = Editor.range(editor, target)
if (selection) {
Editor.setSelection(editor, target)
Transforms.setSelection(editor, target)
return
}
@ -160,9 +160,9 @@ export const SelectionTransforms = {
const newPoint = Object.assign(point, props)
if (edge === 'anchor') {
Editor.setSelection(editor, { anchor: newPoint })
Transforms.setSelection(editor, { anchor: newPoint })
} else {
Editor.setSelection(editor, { focus: newPoint })
Transforms.setSelection(editor, { focus: newPoint })
}
},

View File

@ -8,7 +8,8 @@ import {
Text,
Point,
Range,
} from '../../..'
Transforms,
} from '..'
export const TextTransforms = {
/**
@ -60,7 +61,7 @@ export const TextTransforms = {
}
if (Path.isPath(at)) {
Editor.removeNodes(editor, { at, voids })
Transforms.removeNodes(editor, { at, voids })
return
}
@ -150,7 +151,7 @@ export const TextTransforms = {
for (const pathRef of pathRefs) {
const path = pathRef.unref()!
Editor.removeNodes(editor, { at: path, voids })
Transforms.removeNodes(editor, { at: path, voids })
}
if (!endVoid) {
@ -168,7 +169,7 @@ export const TextTransforms = {
endRef.current &&
startRef.current
) {
Editor.mergeNodes(editor, {
Transforms.mergeNodes(editor, {
at: endRef.current,
hanging: true,
voids,
@ -178,7 +179,7 @@ export const TextTransforms = {
const point = endRef.unref() || startRef.unref()
if (options.at == null && point) {
Editor.select(editor, point)
Transforms.select(editor, point)
}
})
},
@ -221,7 +222,7 @@ export const TextTransforms = {
}
const pointRef = Editor.pointRef(editor, end)
Editor.delete(editor, { at })
Transforms.delete(editor, { at })
at = pointRef.unref()!
}
} else if (Path.isPath(at)) {
@ -339,7 +340,7 @@ export const TextTransforms = {
isInlineEnd ? Path.next(inlinePath) : inlinePath
)
Editor.splitNodes(editor, {
Transforms.splitNodes(editor, {
at,
match: n =>
hasBlocks
@ -356,21 +357,21 @@ export const TextTransforms = {
: inlinePath
)
Editor.insertNodes(editor, starts, {
Transforms.insertNodes(editor, starts, {
at: startRef.current!,
match: n => Text.isText(n) || Editor.isInline(editor, n),
mode: 'highest',
voids,
})
Editor.insertNodes(editor, middles, {
Transforms.insertNodes(editor, middles, {
at: middleRef.current!,
match: n => Editor.isBlock(editor, n),
mode: 'lowest',
voids,
})
Editor.insertNodes(editor, ends, {
Transforms.insertNodes(editor, ends, {
at: endRef.current!,
match: n => Text.isText(n) || Editor.isInline(editor, n),
mode: 'highest',
@ -389,7 +390,7 @@ export const TextTransforms = {
}
const end = Editor.end(editor, path)
Editor.select(editor, end)
Transforms.select(editor, end)
}
startRef.unref()
@ -433,9 +434,9 @@ export const TextTransforms = {
}
const pointRef = Editor.pointRef(editor, end)
Editor.delete(editor, { at, voids })
Transforms.delete(editor, { at, voids })
at = pointRef.unref()!
Editor.setSelection(editor, { anchor: at, focus: at })
Transforms.setSelection(editor, { anchor: at, focus: at })
}
}

View File

@ -0,0 +1,200 @@
/**
* Constants for string distance checking.
*/
const SPACE = /\s/
const PUNCTUATION = /[\u0021-\u0023\u0025-\u002A\u002C-\u002F\u003A\u003B\u003F\u0040\u005B-\u005D\u005F\u007B\u007D\u00A1\u00A7\u00AB\u00B6\u00B7\u00BB\u00BF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E3B\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]/
const CHAMELEON = /['\u2018\u2019]/
const SURROGATE_START = 0xd800
const SURROGATE_END = 0xdfff
const ZERO_WIDTH_JOINER = 0x200d
/**
* Get the distance to the end of the first character in a string of text.
*/
export const getCharacterDistance = (text: string): number => {
let offset = 0
// prev types:
// SURR: surrogate pair
// MOD: modifier (technically also surrogate pair)
// ZWJ: zero width joiner
// VAR: variation selector
// BMP: sequenceable character from basic multilingual plane
let prev: 'SURR' | 'MOD' | 'ZWJ' | 'VAR' | 'BMP' | null = null
let charCode = text.charCodeAt(0)
while (charCode) {
if (isSurrogate(charCode)) {
const modifier = isModifier(charCode, text, offset)
// Early returns are the heart of this function, where we decide if previous and current
// codepoints should form a single character (in terms of how many of them should selection
// jump over).
if (prev === 'SURR' || prev === 'BMP') {
break
}
offset += 2
prev = modifier ? 'MOD' : 'SURR'
charCode = text.charCodeAt(offset)
// Absolutely fine to `continue` without any checks because if `charCode` is NaN (which
// is the case when out of `text` range), next `while` loop won"t execute and we"re done.
continue
}
if (charCode === ZERO_WIDTH_JOINER) {
offset += 1
prev = 'ZWJ'
charCode = text.charCodeAt(offset)
continue
}
if (isBMPEmoji(charCode)) {
if (prev && prev !== 'ZWJ' && prev !== 'VAR') {
break
}
offset += 1
prev = 'BMP'
charCode = text.charCodeAt(offset)
continue
}
if (isVariationSelector(charCode)) {
if (prev && prev !== 'ZWJ') {
break
}
offset += 1
prev = 'VAR'
charCode = text.charCodeAt(offset)
continue
}
// Modifier 'groups up' with what ever character is before that (even whitespace), need to
// look ahead.
if (prev === 'MOD') {
offset += 1
break
}
// If while loop ever gets here, we're done (e.g latin chars).
break
}
return offset || 1
}
/**
* Get the distance to the end of the first word in a string of text.
*/
export const getWordDistance = (text: string): number => {
let length = 0
let i = 0
let started = false
let char
while ((char = text.charAt(i))) {
const l = getCharacterDistance(char)
char = text.slice(i, i + l)
const rest = text.slice(i + l)
if (isWordCharacter(char, rest)) {
started = true
length += l
} else if (!started) {
length += l
} else {
break
}
i += l
}
return length
}
/**
* Check if a character is a word character. The `remaining` argument is used
* because sometimes you must read subsequent characters to truly determine it.
*/
const isWordCharacter = (char: string, remaining: string): boolean => {
if (SPACE.test(char)) {
return false
}
// Chameleons count as word characters as long as they're in a word, so
// recurse to see if the next one is a word character or not.
if (CHAMELEON.test(char)) {
let next = remaining.charAt(0)
const length = getCharacterDistance(next)
next = remaining.slice(0, length)
const rest = remaining.slice(length)
if (isWordCharacter(next, rest)) {
return true
}
}
if (PUNCTUATION.test(char)) {
return false
}
return true
}
/**
* Determines if `code` is a surrogate
*/
const isSurrogate = (code: number): boolean =>
SURROGATE_START <= code && code <= SURROGATE_END
/**
* Does `code` form Modifier with next one.
*
* https://emojipedia.org/modifiers/
*/
const isModifier = (code: number, text: string, offset: number): boolean => {
if (code === 0xd83c) {
const next = text.charCodeAt(offset + 1)
return next <= 0xdfff && next >= 0xdffb
}
return false
}
/**
* Is `code` a Variation Selector.
*
* https://codepoints.net/variation_selectors
*/
const isVariationSelector = (code: number): boolean => {
return code <= 0xfe0f && code >= 0xfe00
}
/**
* Is `code` one of the BMP codes used in emoji sequences.
*
* https://emojipedia.org/emoji-zwj-sequences/
*/
const isBMPEmoji = (code: number): boolean => {
// This requires tiny bit of maintanance, better ideas?
// Fortunately it only happens if new Unicode Standard
// is released. Fails gracefully if upkeep lags behind,
// same way Slate previously behaved with all emojis.
return (
code === 0x2764 || // heart (❤)
code === 0x2642 || // male (♂)
code === 0x2640 || // female (♀)
code === 0x2620 || // scull (☠)
code === 0x2695 || // medical (⚕)
code === 0x2708 || // plane (✈️)
code === 0x25ef // large circle (◯)
)
}

View File

@ -0,0 +1,8 @@
import { Editor, Path, PathRef, PointRef, RangeRef } from '..'
export const DIRTY_PATHS: WeakMap<Editor, Path[]> = new WeakMap()
export const FLUSHING: WeakMap<Editor, boolean> = new WeakMap()
export const NORMALIZING: WeakMap<Editor, boolean> = new WeakMap()
export const PATH_REFS: WeakMap<Editor, Set<PathRef>> = new WeakMap()
export const POINT_REFS: WeakMap<Editor, Set<PointRef>> = new WeakMap()
export const RANGE_REFS: WeakMap<Editor, Set<RangeRef>> = new WeakMap()

View File

@ -5,7 +5,12 @@ import { createHyperscript } from 'slate-hyperscript'
describe('slate', () => {
fixtures(__dirname, 'interfaces', ({ module }) => {
const { input, test, output } = module
let { input, test, output } = module
if (Editor.isEditor(input)) {
input = withTest(input)
}
const result = test(input)
assert.deepEqual(result, output)
})
@ -32,13 +37,6 @@ describe('slate', () => {
assert.deepEqual(editor.selection, output.selection)
})
fixtures(__dirname, 'queries', ({ module }) => {
const { input, run, output } = module
const editor = withTest(input)
const result = run(editor)
assert.deepEqual(result, output)
})
fixtures(__dirname, 'transforms', ({ module }) => {
const { input, run, output } = module
const editor = withTest(input)

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.above(editor, {
at: [0, 0, 0],
match: n => Editor.isBlock(editor, n),

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.above(editor, {
at: [0, 0, 0],
match: n => Editor.isBlock(editor, n),

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.above(editor, {
at: [0, 1, 0],
match: n => Editor.isInline(editor, n),

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -10,7 +10,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.after(editor, [1, 0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -10,7 +10,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.after(editor, [0, 0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.after(editor, { path: [0, 0], offset: 1 })
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -10,7 +10,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.after(editor, {
anchor: { path: [0, 0], offset: 1 },
focus: { path: [1, 0], offset: 2 },

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -10,7 +10,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.before(editor, [1, 0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.before(editor, { path: [0, 0], offset: 1 })
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -10,7 +10,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.before(editor, {
anchor: { path: [0, 0], offset: 1 },
focus: { path: [0, 1], offset: 2 },

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -10,7 +10,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.before(editor, [0, 0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.edges(editor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.edges(editor, { path: [0, 0], offset: 1 })
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.edges(editor, {
anchor: { path: [0, 0], offset: 1 },
focus: { path: [0, 0], offset: 3 },

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.end(editor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.end(editor, { path: [0, 0], offset: 1 })
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
return Editor.end(editor, {
anchor: { path: [0, 0], offset: 1 },
focus: { path: [0, 0], offset: 2 },

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.hasBlocks(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.hasBlocks(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -15,7 +15,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.hasBlocks(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.hasBlocks(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.hasInlines(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.hasInlines(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -15,7 +15,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.hasInlines(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.hasInlines(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.hasTexts(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.hasTexts(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -15,7 +15,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.hasTexts(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.hasTexts(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.isBlock(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.isBlock(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -12,7 +12,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const { anchor } = editor.selection
return Editor.isEdge(editor, anchor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -12,7 +12,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const { anchor } = editor.selection
return Editor.isEdge(editor, anchor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -12,7 +12,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const { anchor } = editor.selection
return Editor.isEdge(editor, anchor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.isEmpty(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.isEmpty(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.isEmpty(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.isEmpty(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -15,7 +15,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.isEmpty(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -13,7 +13,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.isEmpty(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.isEmpty(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -15,7 +15,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.isEmpty(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -12,7 +12,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const { anchor } = editor.selection
return Editor.isEnd(editor, anchor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -12,7 +12,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const { anchor } = editor.selection
return Editor.isEnd(editor, anchor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -12,7 +12,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const { anchor } = editor.selection
return Editor.isEnd(editor, anchor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.isInline(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.isInline(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -12,7 +12,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const { anchor } = editor.selection
return Editor.isStart(editor, anchor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -12,7 +12,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const { anchor } = editor.selection
return Editor.isStart(editor, anchor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -12,7 +12,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const { anchor } = editor.selection
return Editor.isStart(editor, anchor, [0])
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.isVoid(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -9,7 +9,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const block = editor.children[0]
return Editor.isVoid(editor, block)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.isVoid(editor, inline)
}

View File

@ -1,7 +1,7 @@
/** @jsx jsx */
import { Editor } from 'slate'
import { jsx } from '../..'
import { jsx } from '../../..'
export const input = (
<editor>
@ -11,7 +11,7 @@ export const input = (
</editor>
)
export const run = editor => {
export const test = editor => {
const inline = editor.children[0].children[1]
return Editor.isVoid(editor, inline)
}

Some files were not shown because too many files have changed in this diff Show More