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

View File

@@ -1,6 +1,6 @@
# Location # Location
The `Location` interface is a union of the ways to refer to a specific location in a Slate document: paths, points or ranges. Methods will often accept a `Location` instead of requiring only a `Path`, `Point` or `Range`. The `Location` interface is a union of the ways to refer to a specific location in a Slate document: paths, points or ranges. Methods will often accept a `Location` instead of requiring only a `Path`, `Point` or `Range`.
```typescript ```typescript
type Location = Path | Point | Range type Location = Path | Point | Range
@@ -26,9 +26,9 @@ type Path = number[]
```typescript ```typescript
interface Point { interface Point {
path: Path path: Path
offset: number offset: number
[key: string]: unknown [key: string]: unknown
} }
``` ```
@@ -66,9 +66,9 @@ Options: `{affinity?: 'forward' | 'backward' | null}`
```typescript ```typescript
interface Range { interface Range {
anchor: Point anchor: Point
focus: Point focus: Point
[key: string]: unknown [key: string]: unknown
} }
``` ```
@@ -96,7 +96,7 @@ Get the intersection of one `range` with `another`.
###### `Range.isBackward(range: Range): boolean` ###### `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` ###### `Range.isCollapsed(range: Range): boolean`
@@ -126,5 +126,4 @@ Get the start point of a `range`
Transform a `range` by an `op`. Transform a `range` by an `op`.
Options: `{affinity: 'forward' | 'backward' | Options: `{affinity: 'forward' | 'backward' | 'outward' | 'inward' | null}`
'outward' | 'inward' | null}`

View File

@@ -1,6 +1,6 @@
# Node # Node
The `Node` union type represents all of the different types of nodes that occur in a Slate document tree. The `Node` union type represents all of the different types of nodes that occur in a Slate document tree.
```typescript ```typescript
type Node = Editor | Element | Text type Node = Editor | Element | Text
@@ -57,7 +57,7 @@ Get the first node entry in a root node from a `path`.
###### `Node.fragment(root: Node, range: Range): Descendant[]` ###### `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` ###### `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>` ###### `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}` Options: `{reverse?: boolean}`
@@ -236,8 +236,8 @@ Check if an element matches a set of `props`. Note: This checks custom propertie
```typescript ```typescript
interface Text { interface Text {
text: string, text: string
[key: string]: unknown [key: string]: unknown
} }
``` ```

View File

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

View File

@@ -8,28 +8,28 @@ Transforms that operate on nodes.
###### NodeOptions ###### NodeOptions
All transforms listed below support a parameter `options`. This includes options specific to the transform, and general `NodeOptions` to specify the place in the document that the transform is applied to. All transforms listed below support a parameter `options`. This includes options specific to the transform, and general `NodeOptions` to specify the place in the document that the transform is applied to.
```typescript ```typescript
interface NodeOptions { interface NodeOptions {
at?: Location at?: Location
match?: (node: Node) => boolean match?: (node: Node) => boolean
mode?: 'highest' | 'lowest' mode?: 'highest' | 'lowest'
voids?: boolean voids?: boolean
} }
``` ```
###### `Transforms.insertNodes(editor: Editor, nodes: Node | Node[], options?)` ###### `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}`. Options supported: `NodeOptions & {hanging?: boolean, select?: boolean}`.
###### `Transforms.removeNodes(editor: Editor, options?)` ###### `Transforms.removeNodes(editor: Editor, options?)`
Remove nodes at the specified location in the document. If no location is specified, remove the nodes in the selection. Remove nodes at the specified location in the document. If no location is specified, remove the nodes in the selection.
Options supported: `NodeOptions & {hanging?: boolean}` Options supported: `NodeOptions & {hanging?: boolean}`
###### `Transforms.mergeNodes(editor: Editor, options?)` ###### `Transforms.mergeNodes(editor: Editor, options?)`
@@ -41,7 +41,7 @@ Options supported: `NodeOptions & {hanging?: boolean}`
Split nodes at the specified location. If no location is specified, split the selection. Split nodes at the specified location. If no location is specified, split the selection.
Options supported: `NodeOptions & {height?: number, always?: boolean}` Options supported: `NodeOptions & {height?: number, always?: boolean}`
###### `Transforms.wrapNodes(editor: Editor, element: Element, options?)` ###### `Transforms.wrapNodes(editor: Editor, element: Element, options?)`
@@ -75,7 +75,7 @@ Options supported: `NodeOptions`. For `options.mode`, `'all'` is also supported.
###### `Transforms.moveNodes(editor: Editor, options)` ###### `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. 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. 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)` ###### `Transforms.select(editor: Editor, target: Location)`

View File

@@ -1,3 +1,3 @@
# Slate Hyperscript # Slate Hyperscript
This package contains a hyperscript helper for creating Slate documents with JSX! This package contains a hyperscript helper for creating Slate documents with JSX!

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. Once you've installed Slate, you'll need to import it.
```jsx ```jsx
// Import React dependencies. // Import React dependencies.
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from 'react'
// Import the Slate editor factory. // Import the Slate editor factory.
import { createEditor } from 'slate' import { createEditor } from 'slate'
@@ -70,7 +69,11 @@ const App = () => {
const [value, setValue] = useState([]) const [value, setValue] = useState([])
// Render the Slate context. // Render the Slate context.
return ( 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([]) const [value, setValue] = useState([])
return ( return (
// Add the editable component inside the context. // 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 /> <Editable />
</Slate> </Slate>
) )
@@ -114,7 +121,11 @@ const App = () => {
]) ])
return ( return (
<Slate editor={editor} value={value} onChange={newValue => setValue(newValue)}> <Slate
editor={editor}
value={value}
onChange={newValue => setValue(newValue)}
>
<Editable /> <Editable />
</Slate> </Slate>
) )

View File

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

View File

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

View File

@@ -20,6 +20,7 @@
"serve": "cd ./site && next", "serve": "cd ./site && next",
"start": "npm-run-all --parallel --print-label watch serve", "start": "npm-run-all --parallel --print-label watch serve",
"test": "mocha --require ./config/babel/register.cjs ./packages/*/test/index.js", "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", "test:inspect": "yarn test --inspect-brk",
"watch": "yarn build:rollup --watch" "watch": "yarn build:rollup --watch"
}, },
@@ -87,4 +88,4 @@
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.4",
"typescript": "^3.7.2" "typescript": "^3.7.2"
} }
} }

View File

@@ -13,7 +13,7 @@ export const MERGING = new WeakMap<Editor, boolean | undefined>()
* `HistoryEditor` contains helpers for history-enabled editors. * `HistoryEditor` contains helpers for history-enabled editors.
*/ */
export interface HistoryEditor extends Editor { export type HistoryEditor = Editor & {
history: History history: History
undo: () => void undo: () => void
redo: () => void redo: () => void
@@ -25,7 +25,7 @@ export const HistoryEditor = {
*/ */
isHistoryEditor(value: any): value is 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 selection: Partial<Range> = {}
const editor = makeEditor() const editor = makeEditor()
Object.assign(editor, attributes) 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 // Search the document's texts to see if any of them have tokens associated
// that need incorporated into the selection. // 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": { "devDependencies": {
"slate": "^0.59.0", "slate": "^0.59.0",
"slate-history": "^0.59.0",
"slate-hyperscript": "^0.59.0" "slate-hyperscript": "^0.59.0"
}, },
"peerDependencies": { "peerDependencies": {

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import React, { useMemo, useState, useCallback, useEffect } from 'react' 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 { ReactEditor } from '../plugin/react-editor'
import { FocusedContext } from '../hooks/use-focused' import { FocusedContext } from '../hooks/use-focused'
@@ -14,10 +14,9 @@ import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps'
export const Slate = (props: { export const Slate = (props: {
editor: ReactEditor editor: ReactEditor
value: Node[] value: Descendant[]
children: React.ReactNode children: React.ReactNode
onChange: (value: Node[]) => void onChange: (value: Descendant[]) => void
[key: string]: unknown
}) => { }) => {
const { editor, children, onChange, value, ...rest } = props const { editor, children, onChange, value, ...rest } = props
const [key, setKey] = useState(0) 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 DOMRange = globalThis.Range
import DOMSelection = globalThis.Selection import DOMSelection = globalThis.Selection
import DOMStaticRange = globalThis.StaticRange import DOMStaticRange = globalThis.StaticRange
export { export {
DOMNode, DOMNode,
DOMComment, DOMComment,

View File

@@ -1 +1,3 @@
This package contains the core logic of Slate. Feel free to poke around to learn more! 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. * Get the "dirty" paths generated from an operation.
*/ */
const getDirtyPaths = (op: Operation) => { const getDirtyPaths = (op: Operation): Path[] => {
switch (op.type) { switch (op.type) {
case 'insert_text': case 'insert_text':
case 'remove_text': case 'remove_text':

View File

@@ -11,4 +11,5 @@ export * from './interfaces/point-ref'
export * from './interfaces/range' export * from './interfaces/range'
export * from './interfaces/range-ref' export * from './interfaces/range-ref'
export * from './interfaces/text' export * from './interfaces/text'
export * from './interfaces/custom-types'
export * from './transforms' 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 { import {
Ancestor, Ancestor,
Descendant, ExtendedType,
Element,
Location, Location,
Node, Node,
NodeEntry, NodeEntry,
@@ -27,18 +26,23 @@ import {
RANGE_REFS, RANGE_REFS,
} from '../utils/weak-maps' } from '../utils/weak-maps'
import { getWordDistance, getCharacterDistance } from '../utils/string' 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 * 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. * by plugins that wish to add their own helpers and implement new behaviors.
*/ */
export interface Editor { export interface BaseEditor {
children: Node[] children: Descendant[]
selection: Range | null selection: Selection
operations: Operation[] operations: Operation[]
marks: Record<string, any> | null marks: Omit<Text, 'text'> | null
[key: string]: unknown
// Schema-specific node behaviors. // Schema-specific node behaviors.
isInline: (element: Element) => boolean isInline: (element: Element) => boolean
@@ -60,7 +64,208 @@ export interface Editor {
removeMark: (key: string) => void 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. * 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. * 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 const { marks, selection } = editor
if (!selection) { if (!selection) {
@@ -559,7 +764,7 @@ export const Editor = {
* Get the matching node in the branch of the document after a location. * Get the matching node in the branch of the document after a location.
*/ */
next<T extends Node>( next<T extends Descendant>(
editor: Editor, editor: Editor,
options: { options: {
at?: Location at?: Location
@@ -736,7 +941,7 @@ export const Editor = {
options: { options: {
force?: boolean force?: boolean
} = {} } = {}
) { ): void {
const { force = false } = options const { force = false } = options
const getDirtyPaths = (editor: Editor) => { const getDirtyPaths = (editor: Editor) => {
return DIRTY_PATHS.get(editor) || [] return DIRTY_PATHS.get(editor) || []

View File

@@ -1,5 +1,5 @@
import isPlainObject from 'is-plain-object' 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 * `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. * depending on the Slate editor's configuration.
*/ */
export interface Element { export interface BaseElement {
children: Node[] children: Descendant[]
[key: string]: unknown
} }
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. * 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. * 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 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. * Check if a value implements the `Location` interface.
*/ */
@@ -28,7 +32,11 @@ export const Location = {
export type Span = [Path, Path] 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. * Check if a value implements the `Span` interface.
*/ */

View File

@@ -1,14 +1,93 @@
import { produce } from 'immer' 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 * The `Node` union type represents all of the different types of nodes that
* occur in a Slate document tree. * 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. * 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. * 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 return newRoot.children
@@ -354,8 +449,12 @@ export const Node = {
matches(node: Node, props: Partial<Node>): boolean { matches(node: Node, props: Partial<Node>): boolean {
return ( return (
(Element.isElement(node) && Element.matches(node, props)) || (Element.isElement(node) &&
(Text.isText(node) && Text.matches(node, props)) 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] 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' import isPlainObject from 'is-plain-object'
export type InsertNodeOperation = { export type BaseInsertNodeOperation = {
type: 'insert_node' type: 'insert_node'
path: Path path: Path
node: Node node: Node
[key: string]: unknown
} }
export type InsertTextOperation = { export type InsertNodeOperation = ExtendedType<
'InsertNodeOperation',
BaseInsertNodeOperation
>
export type BaseInsertTextOperation = {
type: 'insert_text' type: 'insert_text'
path: Path path: Path
offset: number offset: number
text: string text: string
[key: string]: unknown
} }
export type MergeNodeOperation = { export type InsertTextOperation = ExtendedType<
'InsertTextOperation',
BaseInsertTextOperation
>
export type BaseMergeNodeOperation = {
type: 'merge_node' type: 'merge_node'
path: Path path: Path
position: number position: number
properties: Partial<Node> properties: Partial<Node>
[key: string]: unknown
} }
export type MoveNodeOperation = { export type MergeNodeOperation = ExtendedType<
'MergeNodeOperation',
BaseMergeNodeOperation
>
export type BaseMoveNodeOperation = {
type: 'move_node' type: 'move_node'
path: Path path: Path
newPath: Path newPath: Path
[key: string]: unknown
} }
export type RemoveNodeOperation = { export type MoveNodeOperation = ExtendedType<
'MoveNodeOperation',
BaseMoveNodeOperation
>
export type BaseRemoveNodeOperation = {
type: 'remove_node' type: 'remove_node'
path: Path path: Path
node: Node node: Node
[key: string]: unknown
} }
export type RemoveTextOperation = { export type RemoveNodeOperation = ExtendedType<
'RemoveNodeOperation',
BaseRemoveNodeOperation
>
export type BaseRemoveTextOperation = {
type: 'remove_text' type: 'remove_text'
path: Path path: Path
offset: number offset: number
text: string text: string
[key: string]: unknown
} }
export type SetNodeOperation = { export type RemoveTextOperation = ExtendedType<
'RemoveTextOperation',
BaseRemoveTextOperation
>
export type BaseSetNodeOperation = {
type: 'set_node' type: 'set_node'
path: Path path: Path
properties: Partial<Node> properties: Partial<Node>
newProperties: Partial<Node> newProperties: Partial<Node>
[key: string]: unknown
} }
export type SetSelectionOperation = export type SetNodeOperation = ExtendedType<
'SetNodeOperation',
BaseSetNodeOperation
>
export type BaseSetSelectionOperation =
| { | {
type: 'set_selection' type: 'set_selection'
[key: string]: unknown
properties: null properties: null
newProperties: Range newProperties: Range
} }
| { | {
type: 'set_selection' type: 'set_selection'
[key: string]: unknown
properties: Partial<Range> properties: Partial<Range>
newProperties: Partial<Range> newProperties: Partial<Range>
} }
| { | {
type: 'set_selection' type: 'set_selection'
[key: string]: unknown
properties: Range properties: Range
newProperties: null newProperties: null
} }
export type SplitNodeOperation = { export type SetSelectionOperation = ExtendedType<
'SetSelectionOperation',
BaseSetSelectionOperation
>
export type BaseSplitNodeOperation = {
type: 'split_node' type: 'split_node'
path: Path path: Path
position: number position: number
properties: Partial<Node> properties: Partial<Node>
[key: string]: unknown
} }
export type SplitNodeOperation = ExtendedType<
'SplitNodeOperation',
BaseSplitNodeOperation
>
export type NodeOperation = export type NodeOperation =
| InsertNodeOperation | InsertNodeOperation
| MergeNodeOperation | MergeNodeOperation
@@ -103,7 +137,16 @@ export type TextOperation = InsertTextOperation | RemoveTextOperation
export type Operation = NodeOperation | SelectionOperation | TextOperation 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. * Check of a value is a `NodeOperation` object.
*/ */

View File

@@ -12,7 +12,11 @@ export interface PathRef {
unref(): Path | null 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. * Transform the path ref's current value by an operation.
*/ */

View File

@@ -9,7 +9,41 @@ import { Operation } from '..'
export type Path = number[] 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. * Get a list of ancestor paths for a given path.
* *

View File

@@ -12,7 +12,11 @@ export interface PointRef {
unref(): Point | null 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. * Transform the point ref's current value by an operation.
*/ */

View File

@@ -1,6 +1,6 @@
import isPlainObject from 'is-plain-object' import isPlainObject from 'is-plain-object'
import { produce } from 'immer' 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 * `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. * only refer to `Text` nodes.
*/ */
export interface Point { export interface BasePoint {
path: Path path: Path
offset: number 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 * Compare a point to another, returning an integer indicating whether the
* point was before, at, or after the other. * point was before, at, or after the other.

View File

@@ -12,7 +12,11 @@ export interface RangeRef {
unref(): Range | null 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. * Transform the range ref's current value by an operation.
*/ */

View File

@@ -1,6 +1,6 @@
import { produce } from 'immer' import { produce } from 'immer'
import isPlainObject from 'is-plain-object' 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 * `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. * multiple nodes.
*/ */
export interface Range { export interface BaseRange {
anchor: Point anchor: Point
focus: 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 * Get the start and end points of a range, in the order in which they appear
* in the document. * in the document.

View File

@@ -1,5 +1,6 @@
import isPlainObject from 'is-plain-object' import isPlainObject from 'is-plain-object'
import { Range } from '..' import { Range } from '..'
import { ExtendedType } from './custom-types'
/** /**
* `Text` objects represent the nodes that contain the actual text content of a * `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. * nodes in the document tree as they cannot contain any children.
*/ */
export interface Text { export interface BaseText {
text: string 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. * 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])) 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. * Check if an text matches set of properties.
* *

View File

@@ -13,12 +13,16 @@ import {
Ancestor, Ancestor,
} from '..' } from '..'
export const GeneralTransforms = { export interface GeneralTransforms {
transform: (editor: Editor, op: Operation) => void
}
export const GeneralTransforms: GeneralTransforms = {
/** /**
* Transform the editor by an operation. * Transform the editor by an operation.
*/ */
transform(editor: Editor, op: Operation) { transform(editor: Editor, op: Operation): void {
editor.children = createDraft(editor.children) editor.children = createDraft(editor.children)
let selection = editor.selection && createDraft(editor.selection) 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) { if (selection) {
editor.selection = isDraft(selection) editor.selection = isDraft(selection)

View File

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

View File

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

View File

@@ -11,7 +11,38 @@ import {
Transforms, Transforms,
} from '..' } 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. * Delete content in the editor.
*/ */
@@ -26,7 +57,7 @@ export const TextTransforms = {
hanging?: boolean hanging?: boolean
voids?: boolean voids?: boolean
} = {} } = {}
) { ): void {
Editor.withoutNormalizing(editor, () => { Editor.withoutNormalizing(editor, () => {
const { const {
reverse = false, reverse = false,
@@ -196,7 +227,7 @@ export const TextTransforms = {
hanging?: boolean hanging?: boolean
voids?: boolean voids?: boolean
} = {} } = {}
) { ): void {
Editor.withoutNormalizing(editor, () => { Editor.withoutNormalizing(editor, () => {
const { hanging = false, voids = false } = options const { hanging = false, voids = false } = options
let { at = editor.selection } = options let { at = editor.selection } = options
@@ -410,7 +441,7 @@ export const TextTransforms = {
at?: Location at?: Location
voids?: boolean voids?: boolean
} = {} } = {}
) { ): void {
Editor.withoutNormalizing(editor, () => { Editor.withoutNormalizing(editor, () => {
const { voids = false } = options const { voids = false } = options
let { at = editor.selection } = 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 => { export const test = value => {
return Element.isElement(value) return Element.isElement(value)
} }
export const output = false 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, useReadOnly,
ReactEditor, ReactEditor,
} from 'slate-react' } 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 { css } from 'emotion'
import { withHistory } from 'slate-history' 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 CheckListsExample = () => {
const [value, setValue] = useState<Node[]>(initialValue) const [value, setValue] = useState<Descendant[]>(initialValue)
const renderElement = useCallback(props => <Element {...props} />, []) const renderElement = useCallback(props => <Element {...props} />, [])
const editor = useMemo( const editor = useMemo(
() => withChecklists(withHistory(withReact(createEditor()))), () => withChecklists(withHistory(withReact(createEditor()))),
@@ -39,7 +94,10 @@ const withChecklists = editor => {
if (selection && Range.isCollapsed(selection)) { if (selection && Range.isCollapsed(selection)) {
const [match] = Editor.nodes(editor, { 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) { if (match) {
@@ -47,11 +105,15 @@ const withChecklists = editor => {
const start = Editor.start(editor, path) const start = Editor.start(editor, path)
if (Point.equals(selection.anchor, start)) { if (Point.equals(selection.anchor, start)) {
Transforms.setNodes( const newProperties: Partial<SlateElement> = {
editor, type: 'paragraph',
{ type: 'paragraph' }, }
{ match: n => n.type === 'check-list-item' } Transforms.setNodes(editor, newProperties, {
) match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n.type === 'check-list-item',
})
return return
} }
} }
@@ -102,11 +164,10 @@ const CheckListItemElement = ({ attributes, children, element }) => {
checked={checked} checked={checked}
onChange={event => { onChange={event => {
const path = ReactEditor.findPath(editor, element) const path = ReactEditor.findPath(editor, element)
Transforms.setNodes( const newProperties: Partial<SlateElement> = {
editor, checked: event.target.checked,
{ checked: event.target.checked }, }
{ at: path } Transforms.setNodes(editor, newProperties, { at: path })
)
}} }}
/> />
</span> </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 export default CheckListsExample

View File

@@ -5,7 +5,14 @@ import 'prismjs/components/prism-sql'
import 'prismjs/components/prism-java' import 'prismjs/components/prism-java'
import React, { useState, useCallback, useMemo } from 'react' import React, { useState, useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-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 { withHistory } from 'slate-history'
import { css } from 'emotion' 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 React, { useState, useMemo } from 'react'
import { Transforms, createEditor, Node } from 'slate' import { Transforms, createEditor, Node, Element as SlateElement } from 'slate'
import { import {
Slate, Slate,
Editable, Editable,
@@ -67,7 +67,10 @@ const VideoElement = ({ attributes, children, element }) => {
url={url} url={url}
onChange={val => { onChange={val => {
const path = ReactEditor.findPath(editor, element) 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> </div>

View File

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

View File

@@ -1,7 +1,14 @@
import React, { useState, useMemo } from 'react' import React, { useState, useMemo } from 'react'
import isUrl from 'is-url' import isUrl from 'is-url'
import { Slate, Editable, withReact, useSlate } from 'slate-react' 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 { withHistory } from 'slate-history'
import { Button, Icon, Toolbar } from '../components' import { Button, Icon, Toolbar } from '../components'
@@ -61,12 +68,18 @@ const insertLink = (editor, url) => {
} }
const isLinkActive = editor => { 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 return !!link
} }
const unwrapLink = editor => { 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) => { const wrapLink = (editor, url) => {

View File

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

View File

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

View File

@@ -1,6 +1,13 @@
import React, { useState, useCallback, useMemo } from 'react' import React, { useState, useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-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' import { withHistory } from 'slate-history'
const TablesExample = () => { const TablesExample = () => {
@@ -26,7 +33,10 @@ const withTables = editor => {
if (selection && Range.isCollapsed(selection)) { if (selection && Range.isCollapsed(selection)) {
const [cell] = Editor.nodes(editor, { 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) { if (cell) {
@@ -47,7 +57,10 @@ const withTables = editor => {
if (selection && Range.isCollapsed(selection)) { if (selection && Range.isCollapsed(selection)) {
const [cell] = Editor.nodes(editor, { 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) { if (cell) {
@@ -67,7 +80,12 @@ const withTables = editor => {
const { selection } = editor const { selection } = editor
if (selection) { 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) { if (table) {
return return

View File

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

5002
yarn.lock

File diff suppressed because it is too large Load Diff