1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-08 00:06:37 +02:00

Custom TypeScript Types (#3835)

This PR adds better TypeScript types into Slate and is based on the proposal here: https://github.com/ianstormtaylor/slate/issues/3725

* Extend Slate's types like Element and Text

* Supports type discrimination (ie. if an element has type === "table" then we get a reduced set of properties)

* added custom types

* files

* more extensions

* files

* changed fixtures

* changes eslint file

* changed element.children to descendant

* updated types

* more type changes

* changed a lot of typing, still getting building errors

* extended text type in slate-react

* removed type assertions

* Clean up of custom types and a couple uneeded comments.

* Rename headingElement-true.tsx.tsx to headingElement-true.tsx

* moved basetext and baselement

* Update packages/slate/src/interfaces/text.ts

Co-authored-by: Brent Farese <25846953+BrentFarese@users.noreply.github.com>

* Fix some type issues with core functions.

* Clean up text and element files.

* Convert other types to extended types.

* Change the type of editor.marks to the appropriate type.

* Add version 100.0.0 to package.json

* Revert "Add version 100.0.0 to package.json"

This reverts commit 329e44e43d.

* added custom types

* files

* more extensions

* files

* changed fixtures

* changes eslint file

* changed element.children to descendant

* updated types

* more type changes

* changed a lot of typing, still getting building errors

* extended text type in slate-react

* removed type assertions

* Clean up of custom types and a couple uneeded comments.

* Rename headingElement-true.tsx.tsx to headingElement-true.tsx

* moved basetext and baselement

* Update packages/slate/src/interfaces/text.ts

Co-authored-by: Brent Farese <25846953+BrentFarese@users.noreply.github.com>

* Fix some type issues with core functions.

* Clean up text and element files.

* Convert other types to extended types.

* Change the type of editor.marks to the appropriate type.

* Run linter.

* Remove key:string uknown from the base types.

* Clean up types after removing key:string unknown.

* Lint and prettier fixes.

* Implement custom-types

Co-authored-by: mdmjg <mdj308@nyu.edu>

* added custom types to examples

* reset yarn lock

* added ts to fixtures

* examples custom types

* Working fix

* ts-thesunny-try

* Extract interface types.

* Fix minor return type in create-editor.

* Fix the typing issue with Location having compile time CustomTypes

* Extract types for Transforms.

* Update README.

* Fix dependency on slate-history in slate-react

Co-authored-by: mdmjg <mdj308@nyu.edu>
Co-authored-by: Brent Farese <brentfarese@gmail.com>
Co-authored-by: Brent Farese <25846953+BrentFarese@users.noreply.github.com>
Co-authored-by: Tim Buckley <timothypbuckley@gmail.com>
This commit is contained in:
Sunny Hirai
2020-11-24 12:30:06 -08:00
committed by GitHub
parent a5f4170162
commit 08275f68f3
61 changed files with 3111 additions and 3387 deletions

View File

@@ -5,7 +5,12 @@
"prettier/@typescript-eslint",
"prettier/react"
],
"plugins": ["@typescript-eslint", "import", "react", "prettier"],
"plugins": [
"@typescript-eslint",
"import",
"react",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
@@ -16,7 +21,12 @@
},
"ignorePatterns": ["**/next-env.d.ts"],
"settings": {
"import/extensions": [".js", ".ts", ".jsx", ".tsx"],
"import/extensions": [
".js",
".ts",
".jsx",
".tsx"
],
"react": {
"version": "detect"
}
@@ -29,8 +39,16 @@
},
"rules": {
"constructor-super": "error",
"dot-notation": ["error", { "allowKeywords": true }],
"eqeqeq": ["error", "smart"],
"dot-notation": [
"error",
{
"allowKeywords": true
}
],
"eqeqeq": [
"error",
"smart"
],
"import/default": "error",
"import/export": "error",
"import/first": "error",
@@ -40,7 +58,9 @@
"import/no-deprecated": "error",
"import/no-extraneous-dependencies": [
"error",
{ "peerDependencies": true }
{
"peerDependencies": true
}
],
"import/no-mutable-exports": "error",
"import/no-named-as-default": "error",
@@ -86,18 +106,27 @@
"no-var": "error",
"no-void": "error",
"no-with": "error",
"object-shorthand": ["error", "always"],
"object-shorthand": [
"error",
"always"
],
"prefer-arrow-callback": "error",
"prefer-const": [
"error",
{ "destructuring": "all", "ignoreReadBeforeAssign": true }
{
"destructuring": "all",
"ignoreReadBeforeAssign": true
}
],
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"prettier/prettier": "error",
"radix": "error",
"react/jsx-boolean-value": ["error", "never"],
"react/jsx-boolean-value": [
"error",
"never"
],
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": "error",
"react/jsx-no-undef": "error",
@@ -112,15 +141,34 @@
"react/react-in-jsx-scope": "error",
"react/self-closing-comp": "error",
"react/sort-prop-types": "error",
"spaced-comment": ["error", "always", { "exceptions": ["-"] }],
"spaced-comment": [
"error",
"always",
{
"exceptions": [
"-"
]
}
],
"use-isnan": "error",
"valid-jsdoc": [
"error",
{ "prefer": { "return": "returns" }, "requireReturn": false }
{
"prefer": {
"return": "returns"
},
"requireReturn": false
}
],
"valid-typeof": "error",
"yield-star-spacing": ["error", "after"],
"yoda": ["error", "never"]
"yield-star-spacing": [
"error",
"after"
],
"yoda": [
"error",
"never"
]
},
"overrides": [
{

View File

@@ -26,9 +26,9 @@ type Path = number[]
```typescript
interface Point {
path: Path
offset: number
[key: string]: unknown
path: Path
offset: number
[key: string]: unknown
}
```
@@ -66,9 +66,9 @@ Options: `{affinity?: 'forward' | 'backward' | null}`
```typescript
interface Range {
anchor: Point
focus: Point
[key: string]: unknown
anchor: Point
focus: Point
[key: string]: unknown
}
```
@@ -96,7 +96,7 @@ Get the intersection of one `range` with `another`.
###### `Range.isBackward(range: Range): boolean`
Check if a `range` is backward, meaning that its anchor point appears *after* its focus point in the document.
Check if a `range` is backward, meaning that its anchor point appears _after_ its focus point in the document.
###### `Range.isCollapsed(range: Range): boolean`
@@ -126,5 +126,4 @@ Get the start point of a `range`
Transform a `range` by an `op`.
Options: `{affinity: 'forward' | 'backward' |
'outward' | 'inward' | null}`
Options: `{affinity: 'forward' | 'backward' | 'outward' | 'inward' | null}`

View File

@@ -57,7 +57,7 @@ Get the first node entry in a root node from a `path`.
###### `Node.fragment(root: Node, range: Range): Descendant[]`
Get the sliced fragment represented by the `range`.
Get the sliced fragment represented by the `range`.
###### `Node.get(root: Node, path: Path): Node`
@@ -85,7 +85,7 @@ Get the node at a specific `path`, ensuring it's a leaf text node. If the node i
###### `Node.levels(root: Node, path: Path, options?): Generator<NodeEntry>`
Return a generator of the nodes in a branch of the tree, from a specific `path`. By default, the order is top-down, from the lowest to the highest node in the tree, but you can pass the `reverse: true` option to go bottom-up.
Return a generator of the nodes in a branch of the tree, from a specific `path`. By default, the order is top-down, from the lowest to the highest node in the tree, but you can pass the `reverse: true` option to go bottom-up.
Options: `{reverse?: boolean}`
@@ -236,8 +236,8 @@ Check if an element matches a set of `props`. Note: This checks custom propertie
```typescript
interface Text {
text: string,
[key: string]: unknown
text: string
[key: string]: unknown
}
```

View File

@@ -8,9 +8,9 @@
```typescript
interface PointRef {
current: Point | null
affinity: 'forward' | 'backward' | null
unref(): Point | null
current: Point | null
affinity: 'forward' | 'backward' | null
unref(): Point | null
}
```
@@ -26,9 +26,9 @@ Transform the point refs current value by an `op`.
```typescript
interface RangeRef {
current: Range | null
affinity: 'forward' | 'backward' | 'outward' | 'inward' | null
unref(): Range | null
current: Range | null
affinity: 'forward' | 'backward' | 'outward' | 'inward' | null
unref(): Range | null
}
```

View File

@@ -12,16 +12,16 @@ All transforms listed below support a parameter `options`. This includes options
```typescript
interface NodeOptions {
at?: Location
match?: (node: Node) => boolean
mode?: 'highest' | 'lowest'
voids?: boolean
at?: Location
match?: (node: Node) => boolean
mode?: 'highest' | 'lowest'
voids?: boolean
}
```
###### `Transforms.insertNodes(editor: Editor, nodes: Node | Node[], options?)`
Insert `nodes` at the specified location in the document. If no location is specified, insert at the current selection. If there is no selection, insert at the end of the document.
Insert `nodes` at the specified location in the document. If no location is specified, insert at the current selection. If there is no selection, insert at the end of the document.
Options supported: `NodeOptions & {hanging?: boolean, select?: boolean}`.
@@ -75,7 +75,7 @@ Options supported: `NodeOptions`. For `options.mode`, `'all'` is also supported.
###### `Transforms.moveNodes(editor: Editor, options)`
Move the nodes from an origin to a destination. A destination must be specified in the `options`. If no origin is specified, move the selection.
Move the nodes from an origin to a destination. A destination must be specified in the `options`. If no origin is specified, move the selection.
Options supported: `NodeOptions & {to: Path}`. For `options.mode`, `'all'` is also supported.
@@ -87,7 +87,7 @@ Transforms that operate on the document's selection.
Collapse the selection to a single point.
Options: `{edge?: 'anchor' | 'focus' | 'start' | 'end'}`
Options: `{edge?: 'anchor' | 'focus' | 'start' | 'end'}`
###### `Transforms.select(editor: Editor, target: Location)`

View File

@@ -16,10 +16,9 @@ _Note, if you'd rather use a pre-bundled version of Slate, you can `yarn add sla
Once you've installed Slate, you'll need to import it.
```jsx
// Import React dependencies.
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from 'react'
// Import the Slate editor factory.
import { createEditor } from 'slate'
@@ -70,7 +69,11 @@ const App = () => {
const [value, setValue] = useState([])
// Render the Slate context.
return (
<Slate editor={editor} value={value} onChange={newValue => setValue(newValue)} />
<Slate
editor={editor}
value={value}
onChange={newValue => setValue(newValue)}
/>
)
}
```
@@ -89,7 +92,11 @@ const App = () => {
const [value, setValue] = useState([])
return (
// Add the editable component inside the context.
<Slate editor={editor} value={value} onChange={newValue => setValue(newValue)}>
<Slate
editor={editor}
value={value}
onChange={newValue => setValue(newValue)}
>
<Editable />
</Slate>
)
@@ -114,7 +121,11 @@ const App = () => {
])
return (
<Slate editor={editor} value={value} onChange={newValue => setValue(newValue)}>
<Slate
editor={editor}
value={value}
onChange={newValue => setValue(newValue)}
>
<Editable />
</Slate>
)

View File

@@ -75,7 +75,7 @@ const App = () => {
// Prevent the ampersand character from being inserted.
event.preventDefault()
// Execute the `insertText` method when the event occurs.
editor.insertText("and")
editor.insertText('and')
}
}}
/>

View File

@@ -22,7 +22,7 @@ const App = () => {
onKeyDown={event => {
if (event.key === '&') {
event.preventDefault()
editor.insertText("and")
editor.insertText('and')
}
}}
/>
@@ -93,7 +93,7 @@ const App = () => {
onKeyDown={event => {
if (event.key === '&') {
event.preventDefault()
editor.insertText("and")
editor.insertText('and')
}
}}
/>

View File

@@ -20,6 +20,7 @@
"serve": "cd ./site && next",
"start": "npm-run-all --parallel --print-label watch serve",
"test": "mocha --require ./config/babel/register.cjs ./packages/*/test/index.js",
"test:custom": "mocha --require ./config/babel/register.cjs ./packages/slate/test/index.js",
"test:inspect": "yarn test --inspect-brk",
"watch": "yarn build:rollup --watch"
},

View File

@@ -13,7 +13,7 @@ export const MERGING = new WeakMap<Editor, boolean | undefined>()
* `HistoryEditor` contains helpers for history-enabled editors.
*/
export interface HistoryEditor extends Editor {
export type HistoryEditor = Editor & {
history: History
undo: () => void
redo: () => void
@@ -25,7 +25,7 @@ export const HistoryEditor = {
*/
isHistoryEditor(value: any): value is HistoryEditor {
return Editor.isEditor(value) && History.isHistory(value.history)
return History.isHistory(value.history) && Editor.isEditor(value)
},
/**

6
packages/slate-history/test/jsx.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
// This allows tests to include Slate Nodes written in JSX without TypeScript complaining.
declare namespace jsx.JSX {
interface IntrinsicElements {
[elemName: string]: any // eslint-disable-line
}
}

View File

@@ -237,7 +237,7 @@ export function createEditor(
const selection: Partial<Range> = {}
const editor = makeEditor()
Object.assign(editor, attributes)
editor.children = descendants
editor.children = descendants as Element[]
// Search the document's texts to see if any of them have tokens associated
// that need incorporated into the selection.

View File

@@ -0,0 +1,6 @@
// This allows tests to include Slate Nodes written in JSX without TypeScript complaining.
declare namespace jsx.JSX {
interface IntrinsicElements {
[elemName: string]: any // eslint-disable-line
}
}

View File

@@ -24,6 +24,7 @@
},
"devDependencies": {
"slate": "^0.59.0",
"slate-history": "^0.59.0",
"slate-hyperscript": "^0.59.0"
},
"peerDependencies": {

View File

@@ -9,6 +9,7 @@ import {
Transforms,
Path,
} from 'slate'
import { HistoryEditor } from 'slate-history'
import throttle from 'lodash/throttle'
import scrollIntoView from 'scroll-into-view-if-needed'
@@ -766,7 +767,7 @@ export const Editable = (props: EditableProps) => {
if (Hotkeys.isRedo(nativeEvent)) {
event.preventDefault()
if (typeof editor.redo === 'function') {
if (HistoryEditor.isHistoryEditor(editor)) {
editor.redo()
}
@@ -776,7 +777,7 @@ export const Editable = (props: EditableProps) => {
if (Hotkeys.isUndo(nativeEvent)) {
event.preventDefault()
if (typeof editor.undo === 'function') {
if (HistoryEditor.isHistoryEditor(editor)) {
editor.undo()
}

View File

@@ -1,6 +1,5 @@
import React from 'react'
import { Text, Element } from 'slate'
import { Element, Text } from 'slate'
import String from './string'
import { PLACEHOLDER_SYMBOL } from '../utils/weak-maps'
import { RenderLeafProps } from './editable'
@@ -46,7 +45,7 @@ const Leaf = (props: {
textDecoration: 'none',
}}
>
{leaf.placeholder as React.ReactNode}
{leaf.placeholder}
</span>
{children}
</React.Fragment>
@@ -75,10 +74,6 @@ const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
)
})
/**
* The default custom leaf renderer.
*/
export const DefaultLeaf = (props: RenderLeafProps) => {
const { attributes, children } = props
return <span {...attributes}>{children}</span>

View File

@@ -1,5 +1,5 @@
import React, { useMemo, useState, useCallback, useEffect } from 'react'
import { Node } from 'slate'
import { Node, Element, Descendant } from 'slate'
import { ReactEditor } from '../plugin/react-editor'
import { FocusedContext } from '../hooks/use-focused'
@@ -14,10 +14,9 @@ import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps'
export const Slate = (props: {
editor: ReactEditor
value: Node[]
value: Descendant[]
children: React.ReactNode
onChange: (value: Node[]) => void
[key: string]: unknown
onChange: (value: Descendant[]) => void
}) => {
const { editor, children, onChange, value, ...rest } = props
const [key, setKey] = useState(0)

View File

@@ -0,0 +1,12 @@
import { CustomTypes } from 'slate'
declare module 'slate' {
interface CustomTypes {
Text: {
placeholder: string
}
Range: {
placeholder?: string
}
}
}

View File

@@ -12,6 +12,7 @@ import DOMText = globalThis.Text
import DOMRange = globalThis.Range
import DOMSelection = globalThis.Selection
import DOMStaticRange = globalThis.StaticRange
export {
DOMNode,
DOMComment,

View File

@@ -1 +1,3 @@
This package contains the core logic of Slate. Feel free to poke around to learn more!
Note: A number of source files contain extracted types for `Interfaces` or `Transforms`. This is done currently to enable custom type extensions as found in `packages/src/interfaces/custom-types.ts`.

View File

@@ -307,7 +307,7 @@ export const createEditor = (): Editor => {
* Get the "dirty" paths generated from an operation.
*/
const getDirtyPaths = (op: Operation) => {
const getDirtyPaths = (op: Operation): Path[] => {
switch (op.type) {
case 'insert_text':
case 'remove_text':

View File

@@ -11,4 +11,5 @@ export * from './interfaces/point-ref'
export * from './interfaces/range'
export * from './interfaces/range-ref'
export * from './interfaces/text'
export * from './interfaces/custom-types'
export * from './transforms'

View File

@@ -0,0 +1,11 @@
/**
* Extendable Custom Types Interface
*/
export interface CustomTypes {
[key: string]: unknown
}
export type ExtendedType<K extends string, B> = unknown extends CustomTypes[K]
? B
: B & CustomTypes[K]

View File

@@ -4,8 +4,7 @@ import { reverse as reverseText } from 'esrever'
import {
Ancestor,
Descendant,
Element,
ExtendedType,
Location,
Node,
NodeEntry,
@@ -27,18 +26,23 @@ import {
RANGE_REFS,
} from '../utils/weak-maps'
import { getWordDistance, getCharacterDistance } from '../utils/string'
import { Descendant } from './node'
import { Element } from './element'
export type BaseSelection = Range | null
export type Selection = ExtendedType<'Selection', BaseSelection>
/**
* 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
export interface BaseEditor {
children: Descendant[]
selection: Selection
operations: Operation[]
marks: Record<string, any> | null
[key: string]: unknown
marks: Omit<Text, 'text'> | null
// Schema-specific node behaviors.
isInline: (element: Element) => boolean
@@ -60,7 +64,208 @@ export interface Editor {
removeMark: (key: string) => void
}
export const Editor = {
export type Editor = ExtendedType<'Editor', BaseEditor>
export interface EditorInterface {
above: <T extends Ancestor>(
editor: Editor,
options?: {
at?: Location
match?: NodeMatch<T>
mode?: 'highest' | 'lowest'
voids?: boolean
}
) => NodeEntry<T> | undefined
addMark: (editor: Editor, key: string, value: any) => void
after: (
editor: Editor,
at: Location,
options?: {
distance?: number
unit?: 'offset' | 'character' | 'word' | 'line' | 'block'
}
) => Point | undefined
before: (
editor: Editor,
at: Location,
options?: {
distance?: number
unit?: 'offset' | 'character' | 'word' | 'line' | 'block'
}
) => Point | undefined
deleteBackward: (
editor: Editor,
options?: {
unit?: 'character' | 'word' | 'line' | 'block'
}
) => void
deleteForward: (
editor: Editor,
options?: {
unit?: 'character' | 'word' | 'line' | 'block'
}
) => void
deleteFragment: (editor: Editor) => void
edges: (editor: Editor, at: Location) => [Point, Point]
end: (editor: Editor, at: Location) => Point
first: (editor: Editor, at: Location) => NodeEntry
fragment: (editor: Editor, at: Location) => Descendant[]
hasBlocks: (editor: Editor, element: Element) => boolean
hasInlines: (editor: Editor, element: Element) => boolean
hasTexts: (editor: Editor, element: Element) => boolean
insertBreak: (editor: Editor) => void
insertFragment: (editor: Editor, fragment: Node[]) => void
insertNode: (editor: Editor, node: Node) => void
insertText: (editor: Editor, text: string) => void
isBlock: (editor: Editor, value: any) => value is Element
isEditor: (value: any) => value is Editor
isEnd: (editor: Editor, point: Point, at: Location) => boolean
isEdge: (editor: Editor, point: Point, at: Location) => boolean
isEmpty: (editor: Editor, element: Element) => boolean
isInline: (editor: Editor, value: any) => value is Element
isNormalizing: (editor: Editor) => boolean
isStart: (editor: Editor, point: Point, at: Location) => boolean
isVoid: (editor: Editor, value: any) => value is Element
last: (editor: Editor, at: Location) => NodeEntry
leaf: (
editor: Editor,
at: Location,
options?: {
depth?: number
edge?: 'start' | 'end'
}
) => NodeEntry<Text>
levels: <T extends Node>(
editor: Editor,
options?: {
at?: Location
match?: NodeMatch<T>
reverse?: boolean
voids?: boolean
}
) => Generator<NodeEntry<T>, void, undefined>
marks: (editor: Editor) => Omit<Text, 'text'> | null
next: <T extends Descendant>(
editor: Editor,
options?: {
at?: Location
match?: NodeMatch<T>
mode?: 'all' | 'highest' | 'lowest'
voids?: boolean
}
) => NodeEntry<T> | undefined
node: (
editor: Editor,
at: Location,
options?: {
depth?: number
edge?: 'start' | 'end'
}
) => NodeEntry
nodes: <T extends Node>(
editor: Editor,
options?: {
at?: Location | Span
match?: NodeMatch<T>
mode?: 'all' | 'highest' | 'lowest'
universal?: boolean
reverse?: boolean
voids?: boolean
}
) => Generator<NodeEntry<T>, void, undefined>
normalize: (
editor: Editor,
options?: {
force?: boolean
}
) => void
parent: (
editor: Editor,
at: Location,
options?: {
depth?: number
edge?: 'start' | 'end'
}
) => NodeEntry<Ancestor>
path: (
editor: Editor,
at: Location,
options?: {
depth?: number
edge?: 'start' | 'end'
}
) => Path
pathRef: (
editor: Editor,
path: Path,
options?: {
affinity?: 'backward' | 'forward' | null
}
) => PathRef
pathRefs: (editor: Editor) => Set<PathRef>
point: (
editor: Editor,
at: Location,
options?: {
edge?: 'start' | 'end'
}
) => Point
pointRef: (
editor: Editor,
point: Point,
options?: {
affinity?: 'backward' | 'forward' | null
}
) => PointRef
pointRefs: (editor: Editor) => Set<PointRef>
positions: (
editor: Editor,
options?: {
at?: Location
unit?: 'offset' | 'character' | 'word' | 'line' | 'block'
reverse?: boolean
}
) => Generator<Point, void, undefined>
previous: <T extends Node>(
editor: Editor,
options?: {
at?: Location
match?: NodeMatch<T>
mode?: 'all' | 'highest' | 'lowest'
voids?: boolean
}
) => NodeEntry<T> | undefined
range: (editor: Editor, at: Location, to?: Location) => Range
rangeRef: (
editor: Editor,
range: Range,
options?: {
affinity?: 'backward' | 'forward' | 'outward' | 'inward' | null
}
) => RangeRef
rangeRefs: (editor: Editor) => Set<RangeRef>
removeMark: (editor: Editor, key: string) => void
start: (editor: Editor, at: Location) => Point
string: (editor: Editor, at: Location) => string
unhangRange: (
editor: Editor,
range: Range,
options?: {
voids?: boolean
}
) => Range
void: (
editor: Editor,
options?: {
at?: Location
mode?: 'highest' | 'lowest'
voids?: boolean
}
) => NodeEntry<Element> | undefined
withoutNormalizing: (editor: Editor, fn: () => void) => void
}
export const Editor: EditorInterface = {
/**
* Get the ancestor above a location in the document.
*/
@@ -508,7 +713,7 @@ export const Editor = {
* Get the marks that would be added to text at the current selection.
*/
marks(editor: Editor): Record<string, any> | null {
marks(editor: Editor): Omit<Text, 'text'> | null {
const { marks, selection } = editor
if (!selection) {
@@ -559,7 +764,7 @@ export const Editor = {
* Get the matching node in the branch of the document after a location.
*/
next<T extends Node>(
next<T extends Descendant>(
editor: Editor,
options: {
at?: Location
@@ -736,7 +941,7 @@ export const Editor = {
options: {
force?: boolean
} = {}
) {
): void {
const { force = false } = options
const getDirtyPaths = (editor: Editor) => {
return DIRTY_PATHS.get(editor) || []

View File

@@ -1,5 +1,5 @@
import isPlainObject from 'is-plain-object'
import { Editor, Node, Path } from '..'
import { Editor, Node, Path, Descendant, ExtendedType, Ancestor } from '..'
/**
* `Element` objects are a type of node in a Slate document that contain other
@@ -7,12 +7,29 @@ import { Editor, Node, Path } from '..'
* depending on the Slate editor's configuration.
*/
export interface Element {
children: Node[]
[key: string]: unknown
export interface BaseElement {
children: Descendant[]
}
export const Element = {
export type Element = ExtendedType<'Element', BaseElement>
export interface ElementInterface {
isAncestor: (value: any) => value is Ancestor
isElement: (value: any) => value is Element
isElementList: (value: any) => value is Element[]
isElementProps: (props: any) => props is Partial<Element>
matches: (element: Element, props: Partial<Element>) => boolean
}
export const Element: ElementInterface = {
/**
* Check if a value implements the 'Ancestor' interface.
*/
isAncestor(value: any): value is Ancestor {
return isPlainObject(value) && Node.isNodeList(value.children)
},
/**
* Check if a value implements the `Element` interface.
*/
@@ -36,6 +53,14 @@ export const Element = {
)
},
/**
* Check if a set of props is a partial of Element.
*/
isElementProps(props: any): props is Partial<Element> {
return (props as Partial<Element>).children !== undefined
},
/**
* Check if an element matches set of properties.
*

View File

@@ -11,7 +11,11 @@ import { Path, Point, Range } from '..'
export type Location = Path | Point | Range
export const Location = {
export interface LocationInterface {
isLocation: (value: any) => value is Location
}
export const Location: LocationInterface = {
/**
* Check if a value implements the `Location` interface.
*/
@@ -28,7 +32,11 @@ export const Location = {
export type Span = [Path, Path]
export const Span = {
export interface SpanInterface {
isSpan: (value: any) => value is Span
}
export const Span: SpanInterface = {
/**
* Check if a value implements the `Span` interface.
*/

View File

@@ -1,14 +1,93 @@
import { produce } from 'immer'
import { Editor, Element, ElementEntry, Path, Range, Text } from '..'
import { Editor, Path, Range, Text } from '..'
import { Element, ElementEntry } from './element'
import { ExtendedType } from './custom-types'
/**
* The `Node` union type represents all of the different types of nodes that
* occur in a Slate document tree.
*/
export type Node = Editor | Element | Text
export type BaseNode = Editor | Element | Text
export type Node = ExtendedType<'Node', BaseNode>
export const Node = {
export interface NodeInterface {
ancestor: (root: Node, path: Path) => Ancestor
ancestors: (
root: Node,
path: Path,
options?: {
reverse?: boolean
}
) => Generator<NodeEntry<Ancestor>, void, undefined>
child: (root: Node, index: number) => Descendant
children: (
root: Node,
path: Path,
options?: {
reverse?: boolean
}
) => Generator<NodeEntry<Descendant>, void, undefined>
common: (root: Node, path: Path, another: Path) => NodeEntry
descendant: (root: Node, path: Path) => Descendant
descendants: (
root: Node,
options?: {
from?: Path
to?: Path
reverse?: boolean
pass?: (node: NodeEntry) => boolean
}
) => Generator<NodeEntry<Descendant>, void, undefined>
elements: (
root: Node,
options?: {
from?: Path
to?: Path
reverse?: boolean
pass?: (node: NodeEntry) => boolean
}
) => Generator<ElementEntry, void, undefined>
extractProps: (node: Node) => NodeProps
first: (root: Node, path: Path) => NodeEntry
fragment: (root: Node, range: Range) => Descendant[]
get: (root: Node, path: Path) => Node
has: (root: Node, path: Path) => boolean
isNode: (value: any) => value is Node
isNodeList: (value: any) => value is Node[]
last: (root: Node, path: Path) => NodeEntry
leaf: (root: Node, path: Path) => Text
levels: (
root: Node,
path: Path,
options?: {
reverse?: boolean
}
) => Generator<NodeEntry, void, undefined>
matches: (node: Node, props: Partial<Node>) => boolean
nodes: (
root: Node,
options?: {
from?: Path
to?: Path
reverse?: boolean
pass?: (entry: NodeEntry) => boolean
}
) => Generator<NodeEntry, void, undefined>
parent: (root: Node, path: Path) => Ancestor
string: (node: Node) => string
texts: (
root: Node,
options?: {
from?: Path
to?: Path
reverse?: boolean
pass?: (node: NodeEntry) => boolean
}
) => Generator<NodeEntry<Text>, void, undefined>
}
export const Node: NodeInterface = {
/**
* Get the node at a specific path, asserting that it's an ancestor node.
*/
@@ -164,6 +243,22 @@ export const Node = {
}
},
/**
* Extract props from a Node.
*/
extractProps(node: Node): NodeProps {
if (Element.isAncestor(node)) {
const { children, ...properties } = node
return properties
} else {
const { text, ...properties } = node
return properties
}
},
/**
* Get the first node entry in a root node from a path.
*/
@@ -222,7 +317,7 @@ export const Node = {
}
}
delete r.selection
if (Editor.isEditor(r)) delete r.selection
})
return newRoot.children
@@ -354,8 +449,12 @@ export const Node = {
matches(node: Node, props: Partial<Node>): boolean {
return (
(Element.isElement(node) && Element.matches(node, props)) ||
(Text.isText(node) && Text.matches(node, props))
(Element.isElement(node) &&
Element.isElementProps(props) &&
Element.matches(node, props)) ||
(Text.isText(node) &&
Text.isTextProps(props) &&
Text.matches(node, props))
)
},
@@ -516,3 +615,11 @@ export type Ancestor = Editor | Element
*/
export type NodeEntry<T extends Node = Node> = [T, Path]
/**
* Convenience type for returning the props of a node.
*/
export type NodeProps =
| Omit<Editor, 'children'>
| Omit<Element, 'children'>
| Omit<Text, 'text'>

View File

@@ -1,87 +1,121 @@
import { Node, Path, Range } from '..'
import { ExtendedType, Node, Path, Range } from '..'
import isPlainObject from 'is-plain-object'
export type InsertNodeOperation = {
export type BaseInsertNodeOperation = {
type: 'insert_node'
path: Path
node: Node
[key: string]: unknown
}
export type InsertTextOperation = {
export type InsertNodeOperation = ExtendedType<
'InsertNodeOperation',
BaseInsertNodeOperation
>
export type BaseInsertTextOperation = {
type: 'insert_text'
path: Path
offset: number
text: string
[key: string]: unknown
}
export type MergeNodeOperation = {
export type InsertTextOperation = ExtendedType<
'InsertTextOperation',
BaseInsertTextOperation
>
export type BaseMergeNodeOperation = {
type: 'merge_node'
path: Path
position: number
properties: Partial<Node>
[key: string]: unknown
}
export type MoveNodeOperation = {
export type MergeNodeOperation = ExtendedType<
'MergeNodeOperation',
BaseMergeNodeOperation
>
export type BaseMoveNodeOperation = {
type: 'move_node'
path: Path
newPath: Path
[key: string]: unknown
}
export type RemoveNodeOperation = {
export type MoveNodeOperation = ExtendedType<
'MoveNodeOperation',
BaseMoveNodeOperation
>
export type BaseRemoveNodeOperation = {
type: 'remove_node'
path: Path
node: Node
[key: string]: unknown
}
export type RemoveTextOperation = {
export type RemoveNodeOperation = ExtendedType<
'RemoveNodeOperation',
BaseRemoveNodeOperation
>
export type BaseRemoveTextOperation = {
type: 'remove_text'
path: Path
offset: number
text: string
[key: string]: unknown
}
export type SetNodeOperation = {
export type RemoveTextOperation = ExtendedType<
'RemoveTextOperation',
BaseRemoveTextOperation
>
export type BaseSetNodeOperation = {
type: 'set_node'
path: Path
properties: Partial<Node>
newProperties: Partial<Node>
[key: string]: unknown
}
export type SetSelectionOperation =
export type SetNodeOperation = ExtendedType<
'SetNodeOperation',
BaseSetNodeOperation
>
export type BaseSetSelectionOperation =
| {
type: 'set_selection'
[key: string]: unknown
properties: null
newProperties: Range
}
| {
type: 'set_selection'
[key: string]: unknown
properties: Partial<Range>
newProperties: Partial<Range>
}
| {
type: 'set_selection'
[key: string]: unknown
properties: Range
newProperties: null
}
export type SplitNodeOperation = {
export type SetSelectionOperation = ExtendedType<
'SetSelectionOperation',
BaseSetSelectionOperation
>
export type BaseSplitNodeOperation = {
type: 'split_node'
path: Path
position: number
properties: Partial<Node>
[key: string]: unknown
}
export type SplitNodeOperation = ExtendedType<
'SplitNodeOperation',
BaseSplitNodeOperation
>
export type NodeOperation =
| InsertNodeOperation
| MergeNodeOperation
@@ -103,7 +137,16 @@ export type TextOperation = InsertTextOperation | RemoveTextOperation
export type Operation = NodeOperation | SelectionOperation | TextOperation
export const Operation = {
export interface OperationInterface {
isNodeOperation: (value: any) => value is NodeOperation
isOperation: (value: any) => value is Operation
isOperationList: (value: any) => value is Operation[]
isSelectionOperation: (value: any) => value is SelectionOperation
isTextOperation: (value: any) => value is TextOperation
inverse: (op: Operation) => Operation
}
export const Operation: OperationInterface = {
/**
* Check of a value is a `NodeOperation` object.
*/

View File

@@ -12,7 +12,11 @@ export interface PathRef {
unref(): Path | null
}
export const PathRef = {
export interface PathRefInterface {
transform: (ref: PathRef, op: Operation) => void
}
export const PathRef: PathRefInterface = {
/**
* Transform the path ref's current value by an operation.
*/

View File

@@ -9,7 +9,41 @@ import { Operation } from '..'
export type Path = number[]
export const Path = {
export interface PathInterface {
ancestors: (path: Path, options?: { reverse?: boolean }) => Path[]
common: (path: Path, another: Path) => Path
compare: (path: Path, another: Path) => -1 | 0 | 1
endsAfter: (path: Path, another: Path) => boolean
endsAt: (path: Path, another: Path) => boolean
endsBefore: (path: Path, another: Path) => boolean
equals: (path: Path, another: Path) => boolean
isAfter: (path: Path, another: Path) => boolean
isAncestor: (path: Path, another: Path) => boolean
isBefore: (path: Path, another: Path) => boolean
isChild: (path: Path, another: Path) => boolean
isCommon: (path: Path, another: Path) => boolean
isDescendant: (path: Path, another: Path) => boolean
isParent: (path: Path, another: Path) => boolean
isPath: (value: any) => value is Path
isSibling: (path: Path, another: Path) => boolean
levels: (
path: Path,
options?: {
reverse?: boolean
}
) => Path[]
next: (path: Path) => Path
parent: (path: Path) => Path
previous: (path: Path) => Path
relative: (path: Path, ancestor: Path) => Path
transform: (
path: Path,
operation: Operation,
options?: { affinity?: 'forward' | 'backward' | null }
) => Path | null
}
export const Path: PathInterface = {
/**
* Get a list of ancestor paths for a given path.
*

View File

@@ -12,7 +12,11 @@ export interface PointRef {
unref(): Point | null
}
export const PointRef = {
export interface PointRefInterface {
transform: (ref: PointRef, op: Operation) => void
}
export const PointRef: PointRefInterface = {
/**
* Transform the point ref's current value by an operation.
*/

View File

@@ -1,6 +1,6 @@
import isPlainObject from 'is-plain-object'
import { produce } from 'immer'
import { Operation, Path } from '..'
import { ExtendedType, Operation, Path } from '..'
/**
* `Point` objects refer to a specific location in a text node in a Slate
@@ -9,13 +9,27 @@ import { Operation, Path } from '..'
* only refer to `Text` nodes.
*/
export interface Point {
export interface BasePoint {
path: Path
offset: number
[key: string]: unknown
}
export const Point = {
export type Point = ExtendedType<'Point', BasePoint>
export interface PointInterface {
compare: (point: Point, another: Point) => -1 | 0 | 1
isAfter: (point: Point, another: Point) => boolean
isBefore: (point: Point, another: Point) => boolean
equals: (point: Point, another: Point) => boolean
isPoint: (value: any) => value is Point
transform: (
point: Point,
op: Operation,
options?: { affinity?: 'forward' | 'backward' | null }
) => Point | null
}
export const Point: PointInterface = {
/**
* Compare a point to another, returning an integer indicating whether the
* point was before, at, or after the other.

View File

@@ -12,7 +12,11 @@ export interface RangeRef {
unref(): Range | null
}
export const RangeRef = {
export interface RangeRefInterface {
transform: (ref: RangeRef, op: Operation) => void
}
export const RangeRef: RangeRefInterface = {
/**
* Transform the range ref's current value by an operation.
*/

View File

@@ -1,6 +1,6 @@
import { produce } from 'immer'
import isPlainObject from 'is-plain-object'
import { Operation, Path, Point, PointEntry } from '..'
import { ExtendedType, Operation, Path, Point, PointEntry } from '..'
/**
* `Range` objects are a set of points that refer to a specific span of a Slate
@@ -8,13 +8,41 @@ import { Operation, Path, Point, PointEntry } from '..'
* multiple nodes.
*/
export interface Range {
export interface BaseRange {
anchor: Point
focus: Point
[key: string]: unknown
}
export const Range = {
export type Range = ExtendedType<'Range', BaseRange>
export interface RangeInterface {
edges: (
range: Range,
options?: {
reverse?: boolean
}
) => [Point, Point]
end: (range: Range) => Point
equals: (range: Range, another: Range) => boolean
includes: (range: Range, target: Path | Point | Range) => boolean
intersection: (range: Range, another: Range) => Range | null
isBackward: (range: Range) => boolean
isCollapsed: (range: Range) => boolean
isExpanded: (range: Range) => boolean
isForward: (range: Range) => boolean
isRange: (value: any) => value is Range
points: (range: Range) => Generator<PointEntry, void, undefined>
start: (range: Range) => Point
transform: (
range: Range,
op: Operation,
options?: {
affinity?: 'forward' | 'backward' | 'outward' | 'inward' | null
}
) => Range | null
}
export const Range: RangeInterface = {
/**
* Get the start and end points of a range, in the order in which they appear
* in the document.

View File

@@ -1,5 +1,6 @@
import isPlainObject from 'is-plain-object'
import { Range } from '..'
import { ExtendedType } from './custom-types'
/**
* `Text` objects represent the nodes that contain the actual text content of a
@@ -7,12 +8,22 @@ import { Range } from '..'
* nodes in the document tree as they cannot contain any children.
*/
export interface Text {
export interface BaseText {
text: string
[key: string]: unknown
}
export const Text = {
export type Text = ExtendedType<'Text', BaseText>
export interface TextInterface {
equals: (text: Text, another: Text, options?: { loose?: boolean }) => boolean
isText: (value: any) => value is Text
isTextList: (value: any) => value is Text[]
isTextProps: (props: any) => props is Partial<Text>
matches: (text: Text, props: Partial<Text>) => boolean
decorations: (node: Text, decorations: Range[]) => Text[]
}
export const Text: TextInterface = {
/**
* Check if two text nodes are equal.
*/
@@ -63,6 +74,14 @@ export const Text = {
return Array.isArray(value) && (value.length === 0 || Text.isText(value[0]))
},
/**
* Check if some props are a partial of Text.
*/
isTextProps(props: any): props is Partial<Text> {
return (props as Partial<Text>).text !== undefined
},
/**
* Check if an text matches set of properties.
*

View File

@@ -13,12 +13,16 @@ import {
Ancestor,
} from '..'
export const GeneralTransforms = {
export interface GeneralTransforms {
transform: (editor: Editor, op: Operation) => void
}
export const GeneralTransforms: GeneralTransforms = {
/**
* Transform the editor by an operation.
*/
transform(editor: Editor, op: Operation) {
transform(editor: Editor, op: Operation): void {
editor.children = createDraft(editor.children)
let selection = editor.selection && createDraft(editor.selection)
@@ -272,7 +276,7 @@ export const GeneralTransforms = {
}
}
editor.children = finishDraft(editor.children) as Node[]
editor.children = finishDraft(editor.children)
if (selection) {
editor.selection = isDraft(selection)

View File

@@ -12,7 +12,116 @@ import {
Ancestor,
} from '..'
export const NodeTransforms = {
export interface NodeTransforms {
insertNodes: (
editor: Editor,
nodes: Node | Node[],
options?: {
at?: Location
match?: (node: Node) => boolean
mode?: 'highest' | 'lowest'
hanging?: boolean
select?: boolean
voids?: boolean
}
) => void
liftNodes: (
editor: Editor,
options?: {
at?: Location
match?: (node: Node) => boolean
mode?: 'all' | 'highest' | 'lowest'
voids?: boolean
}
) => void
mergeNodes: (
editor: Editor,
options?: {
at?: Location
match?: (node: Node) => boolean
mode?: 'highest' | 'lowest'
hanging?: boolean
voids?: boolean
}
) => void
moveNodes: (
editor: Editor,
options: {
at?: Location
match?: (node: Node) => boolean
mode?: 'all' | 'highest' | 'lowest'
to: Path
voids?: boolean
}
) => void
removeNodes: (
editor: Editor,
options?: {
at?: Location
match?: (node: Node) => boolean
mode?: 'highest' | 'lowest'
hanging?: boolean
voids?: boolean
}
) => void
setNodes: (
editor: Editor,
props: Partial<Node>,
options?: {
at?: Location
match?: (node: Node) => boolean
mode?: 'all' | 'highest' | 'lowest'
hanging?: boolean
split?: boolean
voids?: boolean
}
) => void
splitNodes: (
editor: Editor,
options?: {
at?: Location
match?: (node: Node) => boolean
mode?: 'highest' | 'lowest'
always?: boolean
height?: number
voids?: boolean
}
) => void
unsetNodes: (
editor: Editor,
props: string | string[],
options?: {
at?: Location
match?: (node: Node) => boolean
mode?: 'all' | 'highest' | 'lowest'
split?: boolean
voids?: boolean
}
) => void
unwrapNodes: (
editor: Editor,
options?: {
at?: Location
match?: (node: Node) => boolean
mode?: 'all' | 'highest' | 'lowest'
split?: boolean
voids?: boolean
}
) => void
wrapNodes: (
editor: Editor,
element: Element,
options?: {
at?: Location
match?: (node: Node) => boolean
mode?: 'all' | 'highest' | 'lowest'
split?: boolean
voids?: boolean
}
) => void
}
export const NodeTransforms: NodeTransforms = {
/**
* Insert nodes at a specific location in the Editor.
*/
@@ -28,7 +137,7 @@ export const NodeTransforms = {
select?: boolean
voids?: boolean
} = {}
) {
): void {
Editor.withoutNormalizing(editor, () => {
const { hanging = false, voids = false, mode = 'lowest' } = options
let { at, match, select } = options
@@ -143,7 +252,7 @@ export const NodeTransforms = {
mode?: 'all' | 'highest' | 'lowest'
voids?: boolean
} = {}
) {
): void {
Editor.withoutNormalizing(editor, () => {
const { at = editor.selection, mode = 'lowest', voids = false } = options
let { match } = options
@@ -208,7 +317,7 @@ export const NodeTransforms = {
hanging?: boolean
voids?: boolean
} = {}
) {
): void {
Editor.withoutNormalizing(editor, () => {
let { match, at = editor.selection } = options
const { hanging = false, voids = false, mode = 'lowest' } = options
@@ -346,7 +455,7 @@ export const NodeTransforms = {
to: Path
voids?: boolean
}
) {
): void {
Editor.withoutNormalizing(editor, () => {
const {
to,
@@ -396,7 +505,7 @@ export const NodeTransforms = {
hanging?: boolean
voids?: boolean
} = {}
) {
): void {
Editor.withoutNormalizing(editor, () => {
const { hanging = false, voids = false, mode = 'lowest' } = options
let { at = editor.selection, match } = options
@@ -444,7 +553,7 @@ export const NodeTransforms = {
split?: boolean
voids?: boolean
} = {}
) {
): void {
Editor.withoutNormalizing(editor, () => {
let { match, at = editor.selection } = options
const {
@@ -542,7 +651,7 @@ export const NodeTransforms = {
height?: number
voids?: boolean
} = {}
) {
): void {
Editor.withoutNormalizing(editor, () => {
const { mode = 'lowest', voids = false } = options
let { match, at = editor.selection, height = 0, always = false } = options
@@ -631,7 +740,7 @@ export const NodeTransforms = {
if (always || !beforeRef || !Editor.isEdge(editor, point, path)) {
split = true
const { text, children, ...properties } = node
const properties = Node.extractProps(node)
editor.apply({
type: 'split_node',
path,
@@ -667,7 +776,7 @@ export const NodeTransforms = {
split?: boolean
voids?: boolean
} = {}
) {
): void {
if (!Array.isArray(props)) {
props = [props]
}
@@ -694,8 +803,8 @@ export const NodeTransforms = {
mode?: 'all' | 'highest' | 'lowest'
split?: boolean
voids?: boolean
}
) {
} = {}
): void {
Editor.withoutNormalizing(editor, () => {
const { mode = 'lowest', split = false, voids = false } = options
let { at = editor.selection, match } = options
@@ -720,7 +829,7 @@ export const NodeTransforms = {
for (const pathRef of pathRefs) {
const path = pathRef.unref()!
const [node] = Editor.node(editor, path) as NodeEntry<Ancestor>
const [node] = Editor.node(editor, path)
let range = Editor.range(editor, path)
if (split && rangeRef) {
@@ -729,7 +838,7 @@ export const NodeTransforms = {
Transforms.liftNodes(editor, {
at: range,
match: n => node.children.includes(n),
match: n => Element.isAncestor(node) && node.children.includes(n),
voids,
})
}
@@ -755,7 +864,7 @@ export const NodeTransforms = {
split?: boolean
voids?: boolean
} = {}
) {
): void {
Editor.withoutNormalizing(editor, () => {
const { mode = 'lowest', split = false, voids = false } = options
let { match, at = editor.selection } = options
@@ -823,7 +932,7 @@ export const NodeTransforms = {
const range = Editor.range(editor, firstPath, lastPath)
const commonNodeEntry = Editor.node(editor, commonPath)
const [commonNode] = commonNodeEntry as NodeEntry<Ancestor>
const [commonNode] = commonNodeEntry
const depth = commonPath.length + 1
const wrapperPath = Path.next(lastPath.slice(0, depth))
const wrapper = { ...element, children: [] }
@@ -831,7 +940,8 @@ export const NodeTransforms = {
Transforms.moveNodes(editor, {
at: range,
match: n => commonNode.children.includes(n),
match: n =>
Element.isAncestor(commonNode) && commonNode.children.includes(n),
to: wrapperPath.concat(0),
voids,
})

View File

@@ -1,6 +1,34 @@
import { Editor, Location, Point, Range, Transforms } from '..'
export const SelectionTransforms = {
export interface SelectionTransforms {
collapse: (
editor: Editor,
options?: {
edge?: 'anchor' | 'focus' | 'start' | 'end'
}
) => void
deselect: (editor: Editor) => void
move: (
editor: Editor,
options?: {
distance?: number
unit?: 'offset' | 'character' | 'word' | 'line'
reverse?: boolean
edge?: 'anchor' | 'focus' | 'start' | 'end'
}
) => void
select: (editor: Editor, target: Location) => void
setPoint: (
editor: Editor,
props: Partial<Point>,
options?: {
edge?: 'anchor' | 'focus' | 'start' | 'end'
}
) => void
setSelection: (editor: Editor, props: Partial<Range>) => void
}
export const SelectionTransforms: SelectionTransforms = {
/**
* Collapse the selection.
*/
@@ -10,7 +38,7 @@ export const SelectionTransforms = {
options: {
edge?: 'anchor' | 'focus' | 'start' | 'end'
} = {}
) {
): void {
const { edge = 'anchor' } = options
const { selection } = editor
@@ -33,7 +61,7 @@ export const SelectionTransforms = {
* Unset the selection.
*/
deselect(editor: Editor) {
deselect(editor: Editor): void {
const { selection } = editor
if (selection) {
@@ -57,7 +85,7 @@ export const SelectionTransforms = {
reverse?: boolean
edge?: 'anchor' | 'focus' | 'start' | 'end'
} = {}
) {
): void {
const { selection } = editor
const { distance = 1, unit = 'character', reverse = false } = options
let { edge = null } = options
@@ -105,7 +133,7 @@ export const SelectionTransforms = {
* Set the selection to a new value.
*/
select(editor: Editor, target: Location) {
select(editor: Editor, target: Location): void {
const { selection } = editor
target = Editor.range(editor, target)
@@ -138,8 +166,8 @@ export const SelectionTransforms = {
props: Partial<Point>,
options: {
edge?: 'anchor' | 'focus' | 'start' | 'end'
}
) {
} = {}
): void {
const { selection } = editor
let { edge = 'both' } = options
@@ -167,7 +195,7 @@ export const SelectionTransforms = {
* Set new properties on the selection.
*/
setSelection(editor: Editor, props: Partial<Range>) {
setSelection(editor: Editor, props: Partial<Range>): void {
const { selection } = editor
const oldProps: Partial<Range> | null = {}
const newProps: Partial<Range> = {}

View File

@@ -11,7 +11,38 @@ import {
Transforms,
} from '..'
export const TextTransforms = {
export interface TextTransforms {
delete: (
editor: Editor,
options?: {
at?: Location
distance?: number
unit?: 'character' | 'word' | 'line' | 'block'
reverse?: boolean
hanging?: boolean
voids?: boolean
}
) => void
insertFragment: (
editor: Editor,
fragment: Node[],
options?: {
at?: Location
hanging?: boolean
voids?: boolean
}
) => void
insertText: (
editor: Editor,
text: string,
options?: {
at?: Location
voids?: boolean
}
) => void
}
export const TextTransforms: TextTransforms = {
/**
* Delete content in the editor.
*/
@@ -26,7 +57,7 @@ export const TextTransforms = {
hanging?: boolean
voids?: boolean
} = {}
) {
): void {
Editor.withoutNormalizing(editor, () => {
const {
reverse = false,
@@ -196,7 +227,7 @@ export const TextTransforms = {
hanging?: boolean
voids?: boolean
} = {}
) {
): void {
Editor.withoutNormalizing(editor, () => {
const { hanging = false, voids = false } = options
let { at = editor.selection } = options
@@ -410,7 +441,7 @@ export const TextTransforms = {
at?: Location
voids?: boolean
} = {}
) {
): void {
Editor.withoutNormalizing(editor, () => {
const { voids = false } = options
let { at = editor.selection } = options

View File

@@ -0,0 +1,11 @@
import { Text } from 'slate'
import { isBoldText } from './type-guards'
export const input: Text = {
placeholder: 'heading',
text: 'mytext',
}
export const test = isBoldText
export const output = false

View File

@@ -0,0 +1,12 @@
// show that regular methods that are imported work as expected
import { Text } from 'slate'
import { isBoldText } from './type-guards'
export const input: Text = {
bold: true,
text: 'mytext',
}
export const test = isBoldText
export const output = true

View File

@@ -0,0 +1,30 @@
import { Descendant, Element, Text, CustomTypes } from 'slate'
export interface HeadingElement {
type: 'heading'
level: number
children: Descendant[]
}
export interface ListItemElement {
type: 'list-item'
depth: number
children: Descendant[]
}
export interface CustomText {
placeholder: string
text: string
}
export interface BoldCustomText {
bold: boolean
text: string
}
declare module 'slate' {
interface CustomTypes {
Element: HeadingElement | ListItemElement
Text: CustomText | BoldCustomText
}
}

View File

@@ -0,0 +1,11 @@
import { Text } from 'slate'
import { isCustomText } from './type-guards'
export const input: Text = {
bold: true,
text: 'mytext',
}
export const test = isCustomText
export const output = false

View File

@@ -0,0 +1,11 @@
import { Text } from 'slate'
import { isCustomText } from './type-guards'
export const input: Text = {
placeholder: 'mystring',
text: 'mytext',
}
export const test = isCustomText
export const output = true

View File

@@ -0,0 +1,12 @@
import { Element } from 'slate'
import { isHeadingElement } from './type-guards'
export const input: Element = {
type: 'list-item',
depth: 5,
children: [],
}
export const test = isHeadingElement
export const output = false

View File

@@ -0,0 +1,12 @@
import { Element } from 'slate'
import { isHeadingElement } from './type-guards'
export const input: Element = {
type: 'heading',
level: 5,
children: [],
}
export const test = isHeadingElement
export const output = true

View File

@@ -0,0 +1,11 @@
import { Element, Text } from 'slate'
import { BoldCustomText, CustomText, HeadingElement } from './custom-types'
export const isBoldText = (text: Text): text is BoldCustomText =>
!!(text as BoldCustomText).bold
export const isCustomText = (text: Text): text is CustomText =>
!!(text as CustomText).placeholder
export const isHeadingElement = (element: Element): element is HeadingElement =>
element.type === 'heading'

View File

@@ -4,4 +4,5 @@ export const input = true
export const test = value => {
return Element.isElement(value)
}
export const output = false

6
packages/slate/test/jsx.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
// This allows tests to include Slate Nodes written in JSX without TypeScript complaining.
declare namespace jsx.JSX {
interface IntrinsicElements {
[elemName: string]: any // eslint-disable-line
}
}

View File

@@ -7,12 +7,67 @@ import {
useReadOnly,
ReactEditor,
} from 'slate-react'
import { Node, Editor, Transforms, Range, Point, createEditor } from 'slate'
import {
Node,
Editor,
Transforms,
Range,
Point,
createEditor,
Descendant,
Element as SlateElement,
} from 'slate'
import { css } from 'emotion'
import { withHistory } from 'slate-history'
const initialValue: Descendant[] = [
{
type: 'paragraph',
children: [
{
text:
'With Slate you can build complex block types that have their own embedded content and behaviors, like rendering checkboxes inside check list items!',
},
],
},
{
type: 'check-list-item',
checked: true,
children: [{ text: 'Slide to the left.' }],
},
{
type: 'check-list-item',
checked: true,
children: [{ text: 'Slide to the right.' }],
},
{
type: 'check-list-item',
checked: false,
children: [{ text: 'Criss-cross.' }],
},
{
type: 'check-list-item',
checked: true,
children: [{ text: 'Criss-cross!' }],
},
{
type: 'check-list-item',
checked: false,
children: [{ text: 'Cha cha real smooth…' }],
},
{
type: 'check-list-item',
checked: false,
children: [{ text: "Let's go to work!" }],
},
{
type: 'paragraph',
children: [{ text: 'Try it out for yourself!' }],
},
]
const CheckListsExample = () => {
const [value, setValue] = useState<Node[]>(initialValue)
const [value, setValue] = useState<Descendant[]>(initialValue)
const renderElement = useCallback(props => <Element {...props} />, [])
const editor = useMemo(
() => withChecklists(withHistory(withReact(createEditor()))),
@@ -39,7 +94,10 @@ const withChecklists = editor => {
if (selection && Range.isCollapsed(selection)) {
const [match] = Editor.nodes(editor, {
match: n => n.type === 'check-list-item',
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n.type === 'check-list-item',
})
if (match) {
@@ -47,11 +105,15 @@ const withChecklists = editor => {
const start = Editor.start(editor, path)
if (Point.equals(selection.anchor, start)) {
Transforms.setNodes(
editor,
{ type: 'paragraph' },
{ match: n => n.type === 'check-list-item' }
)
const newProperties: Partial<SlateElement> = {
type: 'paragraph',
}
Transforms.setNodes(editor, newProperties, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n.type === 'check-list-item',
})
return
}
}
@@ -102,11 +164,10 @@ const CheckListItemElement = ({ attributes, children, element }) => {
checked={checked}
onChange={event => {
const path = ReactEditor.findPath(editor, element)
Transforms.setNodes(
editor,
{ checked: event.target.checked },
{ at: path }
)
const newProperties: Partial<SlateElement> = {
checked: event.target.checked,
}
Transforms.setNodes(editor, newProperties, { at: path })
}}
/>
</span>
@@ -129,48 +190,4 @@ const CheckListItemElement = ({ attributes, children, element }) => {
)
}
const initialValue = [
{
children: [
{
text:
'With Slate you can build complex block types that have their own embedded content and behaviors, like rendering checkboxes inside check list items!',
},
],
},
{
type: 'check-list-item',
checked: true,
children: [{ text: 'Slide to the left.' }],
},
{
type: 'check-list-item',
checked: true,
children: [{ text: 'Slide to the right.' }],
},
{
type: 'check-list-item',
checked: false,
children: [{ text: 'Criss-cross.' }],
},
{
type: 'check-list-item',
checked: true,
children: [{ text: 'Criss-cross!' }],
},
{
type: 'check-list-item',
checked: false,
children: [{ text: 'Cha cha real smooth…' }],
},
{
type: 'check-list-item',
checked: false,
children: [{ text: "Let's go to work!" }],
},
{
children: [{ text: 'Try it out for yourself!' }],
},
]
export default CheckListsExample

View File

@@ -5,7 +5,14 @@ import 'prismjs/components/prism-sql'
import 'prismjs/components/prism-java'
import React, { useState, useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Text, createEditor, Node } from 'slate'
import {
Text,
createEditor,
Node,
Element as SlateElement,
BaseEditor,
Descendant,
} from 'slate'
import { withHistory } from 'slate-history'
import { css } from 'emotion'

18
site/examples/custom-types.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
import { Text, createEditor, Node, Element, Editor, Descendant } from 'slate'
declare module 'slate' {
interface CustomTypes {
Element: CustomElement
Node: CustomNode
}
}
interface CustomElement {
type?: string
checked?: boolean
url?: string
children: Descendant[]
}
type CustomNode = Editor | CustomElement | Text

View File

@@ -1,5 +1,5 @@
import React, { useState, useMemo } from 'react'
import { Transforms, createEditor, Node } from 'slate'
import { Transforms, createEditor, Node, Element as SlateElement } from 'slate'
import {
Slate,
Editable,
@@ -67,7 +67,10 @@ const VideoElement = ({ attributes, children, element }) => {
url={url}
onChange={val => {
const path = ReactEditor.findPath(editor, element)
Transforms.setNodes(editor, { url: val }, { at: path })
const newProperties: Partial<SlateElement> = {
url: val,
}
Transforms.setNodes(editor, newProperties, { at: path })
}}
/>
</div>

View File

@@ -1,6 +1,6 @@
import React, { useState, useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Transforms, createEditor, Node } from 'slate'
import { Transforms, createEditor, Node, Element as SlateElement } from 'slate'
import { withHistory } from 'slate-history'
const withLayout = editor => {
@@ -21,8 +21,9 @@ const withLayout = editor => {
for (const [child, childPath] of Node.children(editor, path)) {
const type = childPath[0] === 0 ? 'title' : 'paragraph'
if (child.type !== type) {
Transforms.setNodes(editor, { type }, { at: childPath })
if (SlateElement.isElement(child) && child.type !== type) {
const newProperties: Partial<SlateElement> = { type }
Transforms.setNodes(editor, newProperties, { at: childPath })
}
}
}

View File

@@ -1,7 +1,14 @@
import React, { useState, useMemo } from 'react'
import isUrl from 'is-url'
import { Slate, Editable, withReact, useSlate } from 'slate-react'
import { Node, Transforms, Editor, Range, createEditor } from 'slate'
import {
Node,
Transforms,
Editor,
Range,
createEditor,
Element as SlateElement,
} from 'slate'
import { withHistory } from 'slate-history'
import { Button, Icon, Toolbar } from '../components'
@@ -61,12 +68,18 @@ const insertLink = (editor, url) => {
}
const isLinkActive = editor => {
const [link] = Editor.nodes(editor, { match: n => n.type === 'link' })
const [link] = Editor.nodes(editor, {
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'link',
})
return !!link
}
const unwrapLink = editor => {
Transforms.unwrapNodes(editor, { match: n => n.type === 'link' })
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'link',
})
}
const wrapLink = (editor, url) => {

View File

@@ -1,6 +1,14 @@
import React, { useState, useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Node, Editor, Transforms, Range, Point, createEditor } from 'slate'
import {
Node,
Editor,
Transforms,
Range,
Point,
createEditor,
Element as SlateElement,
} from 'slate'
import { withHistory } from 'slate-history'
const SHORTCUTS = {
@@ -55,16 +63,20 @@ const withShortcuts = editor => {
if (type) {
Transforms.select(editor, range)
Transforms.delete(editor)
Transforms.setNodes(
editor,
{ type },
{ match: n => Editor.isBlock(editor, n) }
)
const newProperties: Partial<SlateElement> = {
type,
}
Transforms.setNodes(editor, newProperties, {
match: n => Editor.isBlock(editor, n),
})
if (type === 'list-item') {
const list = { type: 'bulleted-list', children: [] }
Transforms.wrapNodes(editor, list, {
match: n => n.type === 'list-item',
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n.type === 'list-item',
})
}
@@ -88,14 +100,22 @@ const withShortcuts = editor => {
const start = Editor.start(editor, path)
if (
!Editor.isEditor(block) &&
SlateElement.isElement(block) &&
block.type !== 'paragraph' &&
Point.equals(selection.anchor, start)
) {
Transforms.setNodes(editor, { type: 'paragraph' })
const newProperties: Partial<SlateElement> = {
type: 'paragraph',
}
Transforms.setNodes(editor, newProperties)
if (block.type === 'list-item') {
Transforms.unwrapNodes(editor, {
match: n => n.type === 'bulleted-list',
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n.type === 'bulleted-list',
split: true,
})
}

View File

@@ -1,7 +1,13 @@
import React, { useCallback, useMemo, useState } from 'react'
import isHotkey from 'is-hotkey'
import { Editable, withReact, useSlate, Slate } from 'slate-react'
import { Editor, Transforms, createEditor, Node } from 'slate'
import {
Editor,
Transforms,
createEditor,
Node,
Element as SlateElement,
} from 'slate'
import { withHistory } from 'slate-history'
import { Button, Icon, Toolbar } from '../components'
@@ -59,13 +65,16 @@ const toggleBlock = (editor, format) => {
const isList = LIST_TYPES.includes(format)
Transforms.unwrapNodes(editor, {
match: n => LIST_TYPES.includes(n.type as string),
match: n =>
LIST_TYPES.includes(
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type
),
split: true,
})
Transforms.setNodes(editor, {
const newProperties: Partial<SlateElement> = {
type: isActive ? 'paragraph' : isList ? 'list-item' : format,
})
}
Transforms.setNodes(editor, newProperties)
if (!isActive && isList) {
const block = { type: format, children: [] }
@@ -85,7 +94,8 @@ const toggleMark = (editor, format) => {
const isBlockActive = (editor, format) => {
const [match] = Editor.nodes(editor, {
match: n => n.type === format,
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === format,
})
return !!match

View File

@@ -1,6 +1,13 @@
import React, { useState, useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Editor, Range, Point, Node, createEditor } from 'slate'
import {
Editor,
Range,
Point,
Node,
createEditor,
Element as SlateElement,
} from 'slate'
import { withHistory } from 'slate-history'
const TablesExample = () => {
@@ -26,7 +33,10 @@ const withTables = editor => {
if (selection && Range.isCollapsed(selection)) {
const [cell] = Editor.nodes(editor, {
match: n => n.type === 'table-cell',
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n.type === 'table-cell',
})
if (cell) {
@@ -47,7 +57,10 @@ const withTables = editor => {
if (selection && Range.isCollapsed(selection)) {
const [cell] = Editor.nodes(editor, {
match: n => n.type === 'table-cell',
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n.type === 'table-cell',
})
if (cell) {
@@ -67,7 +80,12 @@ const withTables = editor => {
const { selection } = editor
if (selection) {
const [table] = Editor.nodes(editor, { match: n => n.type === 'table' })
const [table] = Editor.nodes(editor, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n.type === 'table',
})
if (table) {
return

View File

@@ -25,7 +25,11 @@ export const fixtures = (...args) => {
}
if (
stat.isFile() &&
(file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.tsx')) &&
(file.endsWith('.js') ||
file.endsWith('.tsx') ||
file.endsWith('.ts')) &&
!file.endsWith('custom-types.ts') &&
!file.endsWith('type-guards.ts') &&
!file.startsWith('.') &&
// Ignoring `index.js` files allows us to use the fixtures directly
// from the top-level directory itself, instead of only children.
@@ -34,7 +38,7 @@ export const fixtures = (...args) => {
const name = basename(file, extname(file))
// This needs to be a non-arrow function to use `this.skip()`.
it(`${name} `, function () {
it(`${name} `, function() {
const module = require(p)
if (module.skip) {

5002
yarn.lock

File diff suppressed because it is too large Load Diff