diff --git a/.eslintrc b/.eslintrc index 4e03bc478e..52d68ca1da 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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": [ { @@ -131,4 +179,4 @@ } } ] -} +} \ No newline at end of file diff --git a/docs/api/locations.md b/docs/api/locations.md index e7758925a7..49bdd72b54 100644 --- a/docs/api/locations.md +++ b/docs/api/locations.md @@ -1,6 +1,6 @@ # 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 type Location = Path | Point | Range @@ -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}` diff --git a/docs/api/nodes.md b/docs/api/nodes.md index b3b9b3e489..6d8881ac55 100644 --- a/docs/api/nodes.md +++ b/docs/api/nodes.md @@ -1,6 +1,6 @@ # 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 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[]` -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` -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 } ``` diff --git a/docs/api/refs.md b/docs/api/refs.md index 84c6cd6fd6..1f9332d46b 100644 --- a/docs/api/refs.md +++ b/docs/api/refs.md @@ -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 } ``` diff --git a/docs/api/transforms.md b/docs/api/transforms.md index aee4cf74e5..34308a050b 100644 --- a/docs/api/transforms.md +++ b/docs/api/transforms.md @@ -8,28 +8,28 @@ Transforms that operate on nodes. ###### 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 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}`. +Options supported: `NodeOptions & {hanging?: boolean, select?: boolean}`. ###### `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?)` @@ -41,7 +41,7 @@ Options supported: `NodeOptions & {hanging?: boolean}` 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?)` @@ -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)` diff --git a/docs/libraries/slate-hyperscript.md b/docs/libraries/slate-hyperscript.md index db81fb0f6e..4a7afbb98b 100644 --- a/docs/libraries/slate-hyperscript.md +++ b/docs/libraries/slate-hyperscript.md @@ -1,3 +1,3 @@ # Slate Hyperscript -This package contains a hyperscript helper for creating Slate documents with JSX! \ No newline at end of file +This package contains a hyperscript helper for creating Slate documents with JSX! diff --git a/docs/libraries/slate-react.md b/docs/libraries/slate-react.md index 1fc56c056c..537aa79b24 100644 --- a/docs/libraries/slate-react.md +++ b/docs/libraries/slate-react.md @@ -18,11 +18,11 @@ React components for rendering Slate editors The main Slate editor. -###### `DefaultElement(props: RenderElementProps)` +###### `DefaultElement(props: RenderElementProps)` The default element renderer. -###### `DefaultLeaf(props: RenderLeafProps)` +###### `DefaultLeaf(props: RenderLeafProps)` The default custom leaf renderer. @@ -137,4 +137,3 @@ Adds React and DOM specific behaviors to the editor. ## Utils Private convenience modules - diff --git a/docs/walkthroughs/01-installing-slate.md b/docs/walkthroughs/01-installing-slate.md index 9ed264f0e8..3471fe895f 100644 --- a/docs/walkthroughs/01-installing-slate.md +++ b/docs/walkthroughs/01-installing-slate.md @@ -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 ( - setValue(newValue)} /> + setValue(newValue)} + /> ) } ``` @@ -89,7 +92,11 @@ const App = () => { const [value, setValue] = useState([]) return ( // Add the editable component inside the context. - setValue(newValue)}> + setValue(newValue)} + > ) @@ -114,7 +121,11 @@ const App = () => { ]) return ( - setValue(newValue)}> + setValue(newValue)} + > ) diff --git a/docs/walkthroughs/02-adding-event-handlers.md b/docs/walkthroughs/02-adding-event-handlers.md index d3078cc47a..00246ac8bd 100644 --- a/docs/walkthroughs/02-adding-event-handlers.md +++ b/docs/walkthroughs/02-adding-event-handlers.md @@ -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') } }} /> diff --git a/docs/walkthroughs/03-defining-custom-elements.md b/docs/walkthroughs/03-defining-custom-elements.md index 05848397f7..8db481832e 100644 --- a/docs/walkthroughs/03-defining-custom-elements.md +++ b/docs/walkthroughs/03-defining-custom-elements.md @@ -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') } }} /> diff --git a/package.json b/package.json index 719843dc3e..2419ad33c3 100644 --- a/package.json +++ b/package.json @@ -88,4 +88,4 @@ "source-map-loader": "^0.2.4", "typescript": "^3.7.2" } -} +} \ No newline at end of file diff --git a/packages/slate-history/src/history-editor.ts b/packages/slate-history/src/history-editor.ts index 28f9f2e7dc..8342d291c3 100644 --- a/packages/slate-history/src/history-editor.ts +++ b/packages/slate-history/src/history-editor.ts @@ -13,7 +13,7 @@ export const MERGING = new WeakMap() * `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) }, /** diff --git a/packages/slate-history/test/jsx.d.ts b/packages/slate-history/test/jsx.d.ts new file mode 100644 index 0000000000..de69552d4c --- /dev/null +++ b/packages/slate-history/test/jsx.d.ts @@ -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 + } +} diff --git a/packages/slate-hyperscript/src/creators.ts b/packages/slate-hyperscript/src/creators.ts index 32d4112447..d0ef89e3e8 100644 --- a/packages/slate-hyperscript/src/creators.ts +++ b/packages/slate-hyperscript/src/creators.ts @@ -237,7 +237,7 @@ export function createEditor( const selection: Partial = {} 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. diff --git a/packages/slate-hyperscript/test/jsx.d.ts b/packages/slate-hyperscript/test/jsx.d.ts new file mode 100644 index 0000000000..de69552d4c --- /dev/null +++ b/packages/slate-hyperscript/test/jsx.d.ts @@ -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 + } +} diff --git a/packages/slate-react/package.json b/packages/slate-react/package.json index 237b1af703..4dba4277d4 100644 --- a/packages/slate-react/package.json +++ b/packages/slate-react/package.json @@ -24,6 +24,7 @@ }, "devDependencies": { "slate": "^0.58.4", + "slate-history": "^0.58.4", "slate-hyperscript": "^0.58.4" }, "peerDependencies": { diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index df6a86e813..8ae2d3b947 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -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' @@ -751,7 +752,7 @@ export const Editable = (props: EditableProps) => { if (Hotkeys.isRedo(nativeEvent)) { event.preventDefault() - if (typeof editor.redo === 'function') { + if (HistoryEditor.isHistoryEditor(editor)) { editor.redo() } @@ -761,7 +762,7 @@ export const Editable = (props: EditableProps) => { if (Hotkeys.isUndo(nativeEvent)) { event.preventDefault() - if (typeof editor.undo === 'function') { + if (HistoryEditor.isHistoryEditor(editor)) { editor.undo() } diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index defbb25351..a6e6f86f51 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -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} {children} @@ -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 {children} diff --git a/packages/slate-react/src/components/slate.tsx b/packages/slate-react/src/components/slate.tsx index 82b3caa0ca..a8018c09fb 100644 --- a/packages/slate-react/src/components/slate.tsx +++ b/packages/slate-react/src/components/slate.tsx @@ -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) diff --git a/packages/slate-react/src/custom-types.d.ts b/packages/slate-react/src/custom-types.d.ts new file mode 100644 index 0000000000..8510eb9be6 --- /dev/null +++ b/packages/slate-react/src/custom-types.d.ts @@ -0,0 +1,12 @@ +import { CustomTypes } from 'slate' + +declare module 'slate' { + interface CustomTypes { + Text: { + placeholder: string + } + Range: { + placeholder?: string + } + } +} diff --git a/packages/slate-react/src/utils/dom.ts b/packages/slate-react/src/utils/dom.ts index 0736bd4d1a..3bb5ceeea9 100644 --- a/packages/slate-react/src/utils/dom.ts +++ b/packages/slate-react/src/utils/dom.ts @@ -12,6 +12,7 @@ import DOMText = globalThis.Text import DOMRange = globalThis.Range import DOMSelection = globalThis.Selection import DOMStaticRange = globalThis.StaticRange + export { DOMNode, DOMComment, diff --git a/packages/slate/src/index.ts b/packages/slate/src/index.ts index 0f898a7751..2088071146 100755 --- a/packages/slate/src/index.ts +++ b/packages/slate/src/index.ts @@ -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' diff --git a/packages/slate/src/interfaces/custom-types.ts b/packages/slate/src/interfaces/custom-types.ts new file mode 100644 index 0000000000..d85d64324b --- /dev/null +++ b/packages/slate/src/interfaces/custom-types.ts @@ -0,0 +1,11 @@ +/** + * Extendable Custom Types Interface + */ + +export interface CustomTypes { + [key: string]: unknown +} + +export type ExtendedType = unknown extends CustomTypes[K] + ? B + : B & CustomTypes[K] diff --git a/packages/slate/src/interfaces/editor.ts b/packages/slate/src/interfaces/editor.ts index 7340e60326..a9ef14487f 100755 --- a/packages/slate/src/interfaces/editor.ts +++ b/packages/slate/src/interfaces/editor.ts @@ -4,8 +4,7 @@ import { reverse as reverseText } from 'esrever' import { Ancestor, - Descendant, - Element, + ExtendedType, Location, Node, NodeEntry, @@ -27,18 +26,19 @@ import { RANGE_REFS, } from '../utils/weak-maps' import { getWordDistance, getCharacterDistance } from '../utils/string' +import { Descendant } from './node' +import { Element } from './element' /** * 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[] +export interface BaseEditor { + children: Descendant[] selection: Range | null operations: Operation[] - marks: Record | null - [key: string]: unknown + marks: Omit | null // Schema-specific node behaviors. isInline: (element: Element) => boolean @@ -60,6 +60,8 @@ export interface Editor { removeMark: (key: string) => void } +export type Editor = ExtendedType<'Editor', BaseEditor> + export const Editor = { /** * Get the ancestor above a location in the document. @@ -554,7 +556,7 @@ export const Editor = { * Get the matching node in the branch of the document after a location. */ - next( + next( editor: Editor, options: { at?: Location diff --git a/packages/slate/src/interfaces/element.ts b/packages/slate/src/interfaces/element.ts index 34361e8c2a..d7b6b93f9b 100755 --- a/packages/slate/src/interfaces/element.ts +++ b/packages/slate/src/interfaces/element.ts @@ -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,20 @@ 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 type Element = ExtendedType<'Element', BaseElement> export const Element = { + /** + * 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 +44,14 @@ export const Element = { ) }, + /** + * Check if a set of props is a partial of Element. + */ + + isElementProps(props: any): props is Partial { + return (props as Partial).children !== undefined + }, + /** * Check if an element matches set of properties. * diff --git a/packages/slate/src/interfaces/node.ts b/packages/slate/src/interfaces/node.ts index ab5794d992..d762f3d5be 100755 --- a/packages/slate/src/interfaces/node.ts +++ b/packages/slate/src/interfaces/node.ts @@ -1,12 +1,15 @@ 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 = { /** @@ -164,6 +167,22 @@ export const Node = { } }, + /** + * Extract props from a Node. + */ + + extractProps(node: Node): Partial { + 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 +241,7 @@ export const Node = { } } - delete r.selection + if (Editor.isEditor(r)) delete r.selection }) return newRoot.children @@ -354,8 +373,12 @@ export const Node = { matches(node: Node, props: Partial): 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)) ) }, diff --git a/packages/slate/src/interfaces/operation.ts b/packages/slate/src/interfaces/operation.ts index 87b4c3a0cc..7205918a73 100755 --- a/packages/slate/src/interfaces/operation.ts +++ b/packages/slate/src/interfaces/operation.ts @@ -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 - [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 newProperties: Partial - [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 newProperties: Partial } | { 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 - [key: string]: unknown } +export type SplitNodeOperation = ExtendedType< + 'SplitNodeOperation', + BaseSplitNodeOperation +> + export type NodeOperation = | InsertNodeOperation | MergeNodeOperation diff --git a/packages/slate/src/interfaces/point.ts b/packages/slate/src/interfaces/point.ts index 971b6486cf..29f818f975 100755 --- a/packages/slate/src/interfaces/point.ts +++ b/packages/slate/src/interfaces/point.ts @@ -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,12 +9,13 @@ import { Operation, Path } from '..' * only refer to `Text` nodes. */ -export interface Point { +export interface BasePoint { path: Path offset: number - [key: string]: unknown } +export type Point = ExtendedType<'Point', BasePoint> + export const Point = { /** * Compare a point to another, returning an integer indicating whether the diff --git a/packages/slate/src/interfaces/range.ts b/packages/slate/src/interfaces/range.ts index 76f5494c7a..706633fa3f 100755 --- a/packages/slate/src/interfaces/range.ts +++ b/packages/slate/src/interfaces/range.ts @@ -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,12 +8,13 @@ import { Operation, Path, Point, PointEntry } from '..' * multiple nodes. */ -export interface Range { +export interface BaseRange { anchor: Point focus: Point - [key: string]: unknown } +export type Range = ExtendedType<'Range', BaseRange> + export const Range = { /** * Get the start and end points of a range, in the order in which they appear diff --git a/packages/slate/src/interfaces/text.ts b/packages/slate/src/interfaces/text.ts index 96eb750c4c..69dd2ba72e 100755 --- a/packages/slate/src/interfaces/text.ts +++ b/packages/slate/src/interfaces/text.ts @@ -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,11 +8,12 @@ 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 type Text = ExtendedType<'Text', BaseText> + export const Text = { /** * Check if two text nodes are equal. @@ -63,6 +65,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 { + return (props as Partial).text !== undefined + }, + /** * Check if an text matches set of properties. * diff --git a/packages/slate/src/transforms/general.ts b/packages/slate/src/transforms/general.ts index 5b170dfd46..411dfdd507 100755 --- a/packages/slate/src/transforms/general.ts +++ b/packages/slate/src/transforms/general.ts @@ -272,7 +272,7 @@ export const GeneralTransforms = { } } - editor.children = finishDraft(editor.children) as Node[] + editor.children = finishDraft(editor.children) as Descendant[] if (selection) { editor.selection = isDraft(selection) diff --git a/packages/slate/src/transforms/index.ts b/packages/slate/src/transforms/index.ts index ad7975bb15..ce59a7ab5a 100644 --- a/packages/slate/src/transforms/index.ts +++ b/packages/slate/src/transforms/index.ts @@ -3,9 +3,16 @@ import { NodeTransforms } from './node' import { SelectionTransforms } from './selection' import { TextTransforms } from './text' -export const Transforms = { - ...GeneralTransforms, - ...NodeTransforms, - ...SelectionTransforms, - ...TextTransforms, -} +type TransformsType = typeof GeneralTransforms & + typeof NodeTransforms & + typeof SelectionTransforms & + typeof TextTransforms + +const Transforms: TransformsType = Object.assign( + GeneralTransforms, + NodeTransforms, + SelectionTransforms, + TextTransforms +) + +export { Transforms } diff --git a/packages/slate/src/transforms/node.ts b/packages/slate/src/transforms/node.ts index 2d4084158f..148275cb68 100644 --- a/packages/slate/src/transforms/node.ts +++ b/packages/slate/src/transforms/node.ts @@ -1,7 +1,7 @@ import { Editor, - Element, Location, + Descendant, Node, Path, Point, @@ -11,6 +11,7 @@ import { NodeEntry, Ancestor, } from '..' +import { Element } from '../interfaces/element' export const NodeTransforms = { /** @@ -631,7 +632,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, @@ -720,7 +721,7 @@ export const NodeTransforms = { for (const pathRef of pathRefs) { const path = pathRef.unref()! - const [node] = Editor.node(editor, path) as NodeEntry + const [node] = Editor.node(editor, path) let range = Editor.range(editor, path) if (split && rangeRef) { @@ -729,7 +730,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, }) } @@ -823,7 +824,7 @@ export const NodeTransforms = { const range = Editor.range(editor, firstPath, lastPath) const commonNodeEntry = Editor.node(editor, commonPath) - const [commonNode] = commonNodeEntry as NodeEntry + const [commonNode] = commonNodeEntry const depth = commonPath.length + 1 const wrapperPath = Path.next(lastPath.slice(0, depth)) const wrapper = { ...element, children: [] } @@ -831,7 +832,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, }) diff --git a/packages/slate/test/interfaces/CustomTypes/boldText-false.tsx b/packages/slate/test/interfaces/CustomTypes/boldText-false.tsx new file mode 100644 index 0000000000..d152c9bfbb --- /dev/null +++ b/packages/slate/test/interfaces/CustomTypes/boldText-false.tsx @@ -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 diff --git a/packages/slate/test/interfaces/CustomTypes/boldText-true.tsx b/packages/slate/test/interfaces/CustomTypes/boldText-true.tsx new file mode 100644 index 0000000000..9fa528eebc --- /dev/null +++ b/packages/slate/test/interfaces/CustomTypes/boldText-true.tsx @@ -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 diff --git a/packages/slate/test/interfaces/CustomTypes/custom-types.ts b/packages/slate/test/interfaces/CustomTypes/custom-types.ts new file mode 100644 index 0000000000..caddc00822 --- /dev/null +++ b/packages/slate/test/interfaces/CustomTypes/custom-types.ts @@ -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 + } +} diff --git a/packages/slate/test/interfaces/CustomTypes/customText-false.tsx b/packages/slate/test/interfaces/CustomTypes/customText-false.tsx new file mode 100644 index 0000000000..c1cff79c73 --- /dev/null +++ b/packages/slate/test/interfaces/CustomTypes/customText-false.tsx @@ -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 diff --git a/packages/slate/test/interfaces/CustomTypes/customText-true.tsx b/packages/slate/test/interfaces/CustomTypes/customText-true.tsx new file mode 100644 index 0000000000..dc801f15c2 --- /dev/null +++ b/packages/slate/test/interfaces/CustomTypes/customText-true.tsx @@ -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 diff --git a/packages/slate/test/interfaces/CustomTypes/headingElement-false.tsx b/packages/slate/test/interfaces/CustomTypes/headingElement-false.tsx new file mode 100644 index 0000000000..196c235c32 --- /dev/null +++ b/packages/slate/test/interfaces/CustomTypes/headingElement-false.tsx @@ -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 diff --git a/packages/slate/test/interfaces/CustomTypes/headingElement-true.tsx b/packages/slate/test/interfaces/CustomTypes/headingElement-true.tsx new file mode 100644 index 0000000000..633138a0fc --- /dev/null +++ b/packages/slate/test/interfaces/CustomTypes/headingElement-true.tsx @@ -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 diff --git a/packages/slate/test/interfaces/CustomTypes/type-guards.ts b/packages/slate/test/interfaces/CustomTypes/type-guards.ts new file mode 100644 index 0000000000..3a898a567b --- /dev/null +++ b/packages/slate/test/interfaces/CustomTypes/type-guards.ts @@ -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' diff --git a/packages/slate/test/interfaces/Element/isElement/boolean.tsx b/packages/slate/test/interfaces/Element/isElement/boolean.tsx index 822c40a9b5..ad65825481 100644 --- a/packages/slate/test/interfaces/Element/isElement/boolean.tsx +++ b/packages/slate/test/interfaces/Element/isElement/boolean.tsx @@ -4,4 +4,5 @@ export const input = true export const test = value => { return Element.isElement(value) } + export const output = false diff --git a/packages/slate/test/jsx.d.ts b/packages/slate/test/jsx.d.ts new file mode 100644 index 0000000000..de69552d4c --- /dev/null +++ b/packages/slate/test/jsx.d.ts @@ -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 + } +} diff --git a/site/examples/check-lists.tsx b/site/examples/check-lists.tsx index ec55e59b64..fd5bb537d3 100644 --- a/site/examples/check-lists.tsx +++ b/site/examples/check-lists.tsx @@ -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(initialValue) + const [value, setValue] = useState(initialValue) const renderElement = useCallback(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 = { + 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 = { + checked: event.target.checked, + } + Transforms.setNodes(editor, newProperties, { at: path }) }} /> @@ -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 diff --git a/site/examples/code-highlighting.tsx b/site/examples/code-highlighting.tsx index 6669ed050c..a93f1fc264 100644 --- a/site/examples/code-highlighting.tsx +++ b/site/examples/code-highlighting.tsx @@ -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' diff --git a/site/examples/custom-types.d.ts b/site/examples/custom-types.d.ts new file mode 100644 index 0000000000..7764a72b86 --- /dev/null +++ b/site/examples/custom-types.d.ts @@ -0,0 +1,17 @@ +import { Text, 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 diff --git a/site/examples/embeds.tsx b/site/examples/embeds.tsx index c7eccc05ca..4e7bbea3d2 100644 --- a/site/examples/embeds.tsx +++ b/site/examples/embeds.tsx @@ -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 = { + url: val, + } + Transforms.setNodes(editor, newProperties, { at: path }) }} /> diff --git a/site/examples/forced-layout.tsx b/site/examples/forced-layout.tsx index f10e2bd81a..0bcebd3523 100644 --- a/site/examples/forced-layout.tsx +++ b/site/examples/forced-layout.tsx @@ -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 = { type } + Transforms.setNodes(editor, newProperties, { at: childPath }) } } } diff --git a/site/examples/links.tsx b/site/examples/links.tsx index e5b1201fe3..7b091f2078 100644 --- a/site/examples/links.tsx +++ b/site/examples/links.tsx @@ -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) => { diff --git a/site/examples/markdown-shortcuts.tsx b/site/examples/markdown-shortcuts.tsx index 68ad98140e..11da5a143d 100644 --- a/site/examples/markdown-shortcuts.tsx +++ b/site/examples/markdown-shortcuts.tsx @@ -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 = { + 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 = { + 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, }) } diff --git a/site/examples/richtext.tsx b/site/examples/richtext.tsx index 8a6d5e8200..3e333ee7b0 100644 --- a/site/examples/richtext.tsx +++ b/site/examples/richtext.tsx @@ -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 = { 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 diff --git a/site/examples/tables.tsx b/site/examples/tables.tsx index 5b3250fcb3..f85a78b397 100644 --- a/site/examples/tables.tsx +++ b/site/examples/tables.tsx @@ -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 diff --git a/support/fixtures.js b/support/fixtures.js index a379b2c136..27507de07c 100644 --- a/support/fixtures.js +++ b/support/fixtures.js @@ -25,7 +25,8 @@ export const fixtures = (...args) => { } if ( stat.isFile() && - (file.endsWith('.js') || file.endsWith('.ts') || file.endsWith('.tsx')) && + (file.endsWith('.js') || + file.endsWith('.tsx')) && !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 +35,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) {