diff --git a/.changeset/lazy-cougars-doubt.md b/.changeset/lazy-cougars-doubt.md new file mode 100644 index 0000000000..415c11b045 --- /dev/null +++ b/.changeset/lazy-cougars-doubt.md @@ -0,0 +1,5 @@ +--- +slate: minor +--- + +**The `Node.extractProps` utility is now `Node.props`.** This is just to stay consistent with the rest of the API that uses terse names for getters since they're used so frequently when writing transforms/normalizations. diff --git a/.changeset/light-horses-wink.md b/.changeset/light-horses-wink.md new file mode 100644 index 0000000000..57a7d4a704 --- /dev/null +++ b/.changeset/light-horses-wink.md @@ -0,0 +1,5 @@ +--- +slate: minor +--- + +**Remove `Element.isElementProps` and `Text.isTextProps`.** These didn't actually do what they promised to do, since they were really check if something was a base `Element` or base `Text` node, and not just a valid props object. So this removes them to not cause confusion. If you need to check if something is a valid props object you will need to implement it in userland because Slate doesn't have awareness of what is valid for you. diff --git a/.changeset/perfect-candles-judge.md b/.changeset/perfect-candles-judge.md new file mode 100644 index 0000000000..241e0fb5ec --- /dev/null +++ b/.changeset/perfect-candles-judge.md @@ -0,0 +1,5 @@ +--- +slate: minor +--- + +**The `Element.isAncestor` method is now `Node.isAncestor`.** This is done to stay consistent, where any node-related helpers that aren't specifically scoped to `Editor`, `Element`, or `Text` nodes are stored in the more general `Node` namespace instead. diff --git a/.eslintrc b/.eslintrc index 52d68ca1da..c93e11e42c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,12 +5,7 @@ "prettier/@typescript-eslint", "prettier/react" ], - "plugins": [ - "@typescript-eslint", - "import", - "react", - "prettier" - ], + "plugins": ["@typescript-eslint", "import", "react", "prettier"], "parser": "@typescript-eslint/parser", "parserOptions": { "sourceType": "module", @@ -21,12 +16,7 @@ }, "ignorePatterns": ["**/next-env.d.ts"], "settings": { - "import/extensions": [ - ".js", - ".ts", - ".jsx", - ".tsx" - ], + "import/extensions": [".js", ".ts", ".jsx", ".tsx"], "react": { "version": "detect" } @@ -45,10 +35,7 @@ "allowKeywords": true } ], - "eqeqeq": [ - "error", - "smart" - ], + "eqeqeq": ["error", "smart"], "import/default": "error", "import/export": "error", "import/first": "error", @@ -65,7 +52,6 @@ "import/no-mutable-exports": "error", "import/no-named-as-default": "error", "import/no-named-as-default-member": "error", - "import/no-unresolved": "error", "linebreak-style": "error", "no-array-constructor": "error", "no-class-assign": "error", @@ -89,7 +75,6 @@ "no-new-object": "error", "no-new-symbol": "error", "no-path-concat": "error", - "no-redeclare": "error", "no-regex-spaces": "error", "no-sequences": "error", "no-tabs": "error", @@ -106,10 +91,7 @@ "no-var": "error", "no-void": "error", "no-with": "error", - "object-shorthand": [ - "error", - "always" - ], + "object-shorthand": ["error", "always"], "prefer-arrow-callback": "error", "prefer-const": [ "error", @@ -123,10 +105,7 @@ "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", @@ -145,9 +124,7 @@ "error", "always", { - "exceptions": [ - "-" - ] + "exceptions": ["-"] } ], "use-isnan": "error", @@ -161,14 +138,8 @@ } ], "valid-typeof": "error", - "yield-star-spacing": [ - "error", - "after" - ], - "yoda": [ - "error", - "never" - ] + "yield-star-spacing": ["error", "after"], + "yoda": ["error", "never"] }, "overrides": [ { @@ -179,4 +150,4 @@ } } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index d4f16e5488..420bb4cea4 100644 --- a/package.json +++ b/package.json @@ -53,8 +53,8 @@ "@types/node": "^12.12.14", "@types/react": "^16.9.13", "@types/react-dom": "^16.9.4", - "@typescript-eslint/eslint-plugin": "^2.9.0", - "@typescript-eslint/parser": "^2.9.0", + "@typescript-eslint/eslint-plugin": "^4.21.0", + "@typescript-eslint/parser": "^4.21.0", "babel-eslint": "^10.0.3", "babel-plugin-dev-expression": "^0.2.2", "babel-plugin-module-resolver": "^3.1.1", @@ -62,15 +62,15 @@ "emotion": "^10.0.9", "eslint": "^6.7.1", "eslint-config-prettier": "^6.7.0", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-prettier": "^3.1.1", - "eslint-plugin-react": "^7.16.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-react": "^7.23.1", "faker": "^4.1.0", "image-extensions": "^1.1.0", "is-hotkey": "^0.1.6", "is-url": "^1.2.2", "lerna": "^3.19.0", - "lint-staged": ">=10", + "lint-staged": "^10.5.4", "lodash": "^4.17.4", "mocha": "^6.2.0", "next": "^9.4.0", @@ -99,7 +99,7 @@ "slate-hyperscript": "*", "slate-react": "*", "source-map-loader": "^0.2.4", - "typescript": "3.9.7" + "typescript": "^4.2.3" }, "simple-git-hooks": { "pre-commit": "yarn lint-staged" diff --git a/packages/slate-history/src/history-editor.ts b/packages/slate-history/src/history-editor.ts index 6b0bb0a875..11dd16cc89 100644 --- a/packages/slate-history/src/history-editor.ts +++ b/packages/slate-history/src/history-editor.ts @@ -1,19 +1,19 @@ -import { BaseEditor, Editor } from 'slate' +import { Editor, Value } from 'slate' import { History } from './history' /** * Weakmaps for attaching state to the editor. */ -export const HISTORY = new WeakMap() -export const SAVING = new WeakMap() -export const MERGING = new WeakMap() +export const HISTORY = new WeakMap, History>() +export const SAVING = new WeakMap, boolean | undefined>() +export const MERGING = new WeakMap, boolean | undefined>() /** * `HistoryEditor` contains helpers for history-enabled editors. */ -export interface HistoryEditor extends BaseEditor { +export type HistoryEditor = Editor & { history: History undo: () => void redo: () => void @@ -24,7 +24,7 @@ export const HistoryEditor = { * Check if a value is a `HistoryEditor` object. */ - isHistoryEditor(value: any): value is HistoryEditor { + isHistoryEditor(value: any): value is HistoryEditor { return History.isHistory(value.history) && Editor.isEditor(value) }, @@ -32,7 +32,7 @@ export const HistoryEditor = { * Get the merge flag's current value. */ - isMerging(editor: HistoryEditor): boolean | undefined { + isMerging(editor: HistoryEditor): boolean | undefined { return MERGING.get(editor) }, @@ -40,7 +40,7 @@ export const HistoryEditor = { * Get the saving flag's current value. */ - isSaving(editor: HistoryEditor): boolean | undefined { + isSaving(editor: HistoryEditor): boolean | undefined { return SAVING.get(editor) }, @@ -48,7 +48,7 @@ export const HistoryEditor = { * Redo to the previous saved state. */ - redo(editor: HistoryEditor): void { + redo(editor: HistoryEditor): void { editor.redo() }, @@ -56,7 +56,7 @@ export const HistoryEditor = { * Undo to the previous saved state. */ - undo(editor: HistoryEditor): void { + undo(editor: HistoryEditor): void { editor.undo() }, @@ -65,7 +65,10 @@ export const HistoryEditor = { * the new operations into previous save point in the history. */ - withoutMerging(editor: HistoryEditor, fn: () => void): void { + withoutMerging( + editor: HistoryEditor, + fn: () => void + ): void { const prev = HistoryEditor.isMerging(editor) MERGING.set(editor, false) fn() @@ -77,7 +80,10 @@ export const HistoryEditor = { * their operations into the history. */ - withoutSaving(editor: HistoryEditor, fn: () => void): void { + withoutSaving( + editor: HistoryEditor, + fn: () => void + ): void { const prev = HistoryEditor.isSaving(editor) SAVING.set(editor, false) fn() diff --git a/packages/slate-history/src/with-history.ts b/packages/slate-history/src/with-history.ts index 89af48c68e..f6bf65d99c 100644 --- a/packages/slate-history/src/with-history.ts +++ b/packages/slate-history/src/with-history.ts @@ -1,4 +1,4 @@ -import { Editor, Operation, Path } from 'slate' +import { Editor, Operation, Path, Value } from 'slate' import { HistoryEditor } from './history-editor' @@ -12,8 +12,10 @@ import { HistoryEditor } from './history-editor' * See https://docs.slatejs.org/concepts/11-typescript to learn how. */ -export const withHistory = (editor: T) => { - const e = editor as T & HistoryEditor +export const withHistory = >( + editor: E +): E & HistoryEditor => { + const e = (editor as unknown) as E & HistoryEditor const { apply } = e e.history = { undos: [], redos: [] } diff --git a/packages/slate-hyperscript/src/creators.ts b/packages/slate-hyperscript/src/creators.ts index d0ef89e3e8..6759ff6153 100644 --- a/packages/slate-hyperscript/src/creators.ts +++ b/packages/slate-hyperscript/src/creators.ts @@ -6,6 +6,7 @@ import { Text, Editor, createEditor as makeEditor, + Value, } from 'slate' import { AnchorToken, @@ -221,7 +222,7 @@ export function createEditor( tagName: string, attributes: { [key: string]: any }, children: any[] -): Editor { +): Editor { const otherChildren: any[] = [] let selectionChild: Range | undefined diff --git a/packages/slate-react/package.json b/packages/slate-react/package.json index 1289383ff3..e484d43d2b 100644 --- a/packages/slate-react/package.json +++ b/packages/slate-react/package.json @@ -21,7 +21,8 @@ "is-plain-object": "^3.0.0", "lodash": "^4.17.4", "scroll-into-view-if-needed": "^2.2.20", - "tiny-invariant": "1.0.6" + "tiny-invariant": "^1.0.6", + "tiny-warning": "^1.0.3" }, "devDependencies": { "slate": "^0.62.0", diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 5f5e564121..6b8ca30bfb 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -5,9 +5,12 @@ import { NodeEntry, Node, Range, - Text, Transforms, Path, + ElementOf, + TextOf, + NodeOf, + Value, } from 'slate' import getDirection from 'direction' import { HistoryEditor } from 'slate-history' @@ -28,6 +31,7 @@ import { ReadOnlyContext } from '../hooks/use-read-only' import { useSlate } from '../hooks/use-slate' import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect' import { DecorateContext } from '../hooks/use-decorate' +import { PLACEHOLDER_SYMBOL } from '../utils/symbols' import { DOMElement, DOMNode, @@ -35,7 +39,6 @@ import { getDefaultView, isDOMElement, isDOMNode, - DOMStaticRange, isPlainTextOnlyPaste, } from '../utils/dom' import { @@ -44,7 +47,6 @@ import { IS_READ_ONLY, NODE_TO_ELEMENT, IS_FOCUSED, - PLACEHOLDER_SYMBOL, EDITOR_TO_WINDOW, } from '../utils/weak-maps' @@ -60,9 +62,9 @@ const HAS_BEFORE_INPUT_SUPPORT = !( * `RenderElementProps` are passed to the `renderElement` handler. */ -export interface RenderElementProps { +export interface RenderElementProps { children: any - element: Element + element: ElementOf> attributes: { 'data-slate-node': 'element' 'data-slate-inline'?: true @@ -72,32 +74,40 @@ export interface RenderElementProps { } } +export type RenderElementFn = ( + props: RenderElementProps +) => JSX.Element + /** * `RenderLeafProps` are passed to the `renderLeaf` handler. */ -export interface RenderLeafProps { +export interface RenderLeafProps { children: any - leaf: Text - text: Text + leaf: TextOf> + text: TextOf> attributes: { 'data-slate-leaf': true } } +export type RenderLeafFn = ( + props: RenderLeafProps +) => JSX.Element + /** * `EditableProps` are passed to the `` component. */ -export type EditableProps = { - decorate?: (entry: NodeEntry) => Range[] +export type EditableProps = { + decorate?: (entry: NodeEntry>>) => Range[] onDOMBeforeInput?: (event: InputEvent) => void placeholder?: string readOnly?: boolean role?: string style?: React.CSSProperties - renderElement?: (props: RenderElementProps) => JSX.Element - renderLeaf?: (props: RenderLeafProps) => JSX.Element + renderElement?: RenderElementFn + renderLeaf?: RenderLeafFn as?: React.ElementType } & React.TextareaHTMLAttributes @@ -105,7 +115,7 @@ export type EditableProps = { * Editable. */ -export const Editable = (props: EditableProps) => { +export const Editable = (props: EditableProps) => { const { autoFocus, decorate = defaultDecorate, @@ -118,7 +128,7 @@ export const Editable = (props: EditableProps) => { as: Component = 'div', ...attributes } = props - const editor = useSlate() + const editor = useSlate() const ref = useRef(null) // Update internal state on each render. @@ -188,7 +198,7 @@ export const Editable = (props: EditableProps) => { // then its children might just change - DOM responds to it on its own // but Slate's value is not being updated through any operation // and thus it doesn't transform selection on its own - if (selection && !ReactEditor.hasRange(editor, selection)) { + if (selection && !Range.exists(selection, editor)) { editor.selection = ReactEditor.toSlateRange(editor, domSelection) return } @@ -223,6 +233,7 @@ export const Editable = (props: EditableProps) => { scrollMode: 'if-needed', boundary: el, }) + // @ts-ignore delete leafEl.getBoundingClientRect } else { domSelection.removeAllRanges() @@ -471,7 +482,7 @@ export const Editable = (props: EditableProps) => { } }, [onDOMSelectionChange]) - const decorations = decorate([editor, []]) + const decorations = decorate([editor as NodeOf>, []]) if ( placeholder && @@ -480,17 +491,19 @@ export const Editable = (props: EditableProps) => { Node.string(editor) === '' ) { const start = Editor.start(editor, []) - decorations.push({ - [PLACEHOLDER_SYMBOL]: true, + const range = { + [PLACEHOLDER_SYMBOL]: placeholder, placeholder, anchor: start, focus: start, - }) + } + + decorations.push(range) } return ( - + { event.preventDefault() if (HistoryEditor.isHistoryEditor(editor)) { - editor.redo() + ;(editor as any).redo() } return @@ -837,7 +850,7 @@ export const Editable = (props: EditableProps) => { event.preventDefault() if (HistoryEditor.isHistoryEditor(editor)) { - editor.undo() + ;(editor as any).undo() } return @@ -1039,7 +1052,9 @@ export const Editable = (props: EditableProps) => { {useChildren({ decorations, node: editor, + // @ts-ignore renderElement, + // @ts-ignore renderLeaf, selection: editor.selection, })} @@ -1055,29 +1070,12 @@ export const Editable = (props: EditableProps) => { const defaultDecorate: (entry: NodeEntry) => Range[] = () => [] -/** - * Check if two DOM range objects are equal. - */ - -const isRangeEqual = (a: DOMRange, b: DOMRange) => { - return ( - (a.startContainer === b.startContainer && - a.startOffset === b.startOffset && - a.endContainer === b.endContainer && - a.endOffset === b.endOffset) || - (a.startContainer === b.endContainer && - a.startOffset === b.endOffset && - a.endContainer === b.startContainer && - a.endOffset === b.startOffset) - ) -} - /** * Check if the target is in the editor. */ -const hasTarget = ( - editor: ReactEditor, +const hasTarget = ( + editor: ReactEditor, target: EventTarget | null ): target is DOMNode => { return isDOMNode(target) && ReactEditor.hasDOMNode(editor, target) @@ -1087,8 +1085,8 @@ const hasTarget = ( * Check if the target is editable and in the editor. */ -const hasEditableTarget = ( - editor: ReactEditor, +const hasEditableTarget = ( + editor: ReactEditor, target: EventTarget | null ): target is DOMNode => { return ( @@ -1101,8 +1099,8 @@ const hasEditableTarget = ( * Check if the target is inside void and in the editor. */ -const isTargetInsideVoid = ( - editor: ReactEditor, +const isTargetInsideVoid = ( + editor: ReactEditor, target: EventTarget | null ): boolean => { const slateNode = diff --git a/packages/slate-react/src/components/element.tsx b/packages/slate-react/src/components/element.tsx index 0c68fd2bb9..6b2a6af4fe 100644 --- a/packages/slate-react/src/components/element.tsx +++ b/packages/slate-react/src/components/element.tsx @@ -1,6 +1,6 @@ import React, { useRef } from 'react' import getDirection from 'direction' -import { Editor, Node, Range, NodeEntry, Element as SlateElement } from 'slate' +import { Editor, Node, Range, Element as SlateElement, Value } from 'slate' import Text from './text' import useChildren from '../hooks/use-children' @@ -15,23 +15,23 @@ import { KEY_TO_ELEMENT, } from '../utils/weak-maps' import { isDecoratorRangeListEqual } from '../utils/range-list' -import { RenderElementProps, RenderLeafProps } from './editable' +import { RenderElementFn, RenderElementProps, RenderLeafFn } from './editable' /** * Element. */ -const Element = (props: { +const Element: React.FC<{ decorations: Range[] element: SlateElement - renderElement?: (props: RenderElementProps) => JSX.Element - renderLeaf?: (props: RenderLeafProps) => JSX.Element + renderElement?: RenderElementFn + renderLeaf?: RenderLeafFn selection: Range | null -}) => { +}> = props => { const { decorations, element, - renderElement = (p: RenderElementProps) => , + renderElement = (p: RenderElementProps) => , renderLeaf, selection, } = props @@ -142,7 +142,7 @@ const MemoizedElement = React.memo(Element, (prev, next) => { * The default element renderer. */ -export const DefaultElement = (props: RenderElementProps) => { +export const DefaultElement: React.FC> = props => { const { attributes, children, element } = props const editor = useSlateStatic() const Tag = editor.isInline(element) ? 'span' : 'div' diff --git a/packages/slate-react/src/components/leaf.tsx b/packages/slate-react/src/components/leaf.tsx index 21e064af86..57cd7ee79f 100644 --- a/packages/slate-react/src/components/leaf.tsx +++ b/packages/slate-react/src/components/leaf.tsx @@ -1,26 +1,26 @@ import React, { useRef, useEffect } from 'react' -import { Element, Text } from 'slate' +import { Element, Text, Value } from 'slate' import String from './string' -import { PLACEHOLDER_SYMBOL } from '../utils/weak-maps' -import { RenderLeafProps } from './editable' +import { PLACEHOLDER_SYMBOL } from '../utils/symbols' +import { RenderLeafFn, RenderLeafProps } from './editable' /** * Individual leaves in a text node with unique formatting. */ -const Leaf = (props: { +const Leaf: React.FC<{ isLast: boolean leaf: Text parent: Element - renderLeaf?: (props: RenderLeafProps) => JSX.Element + renderLeaf?: RenderLeafFn text: Text -}) => { +}> = props => { const { leaf, isLast, text, parent, - renderLeaf = (props: RenderLeafProps) => , + renderLeaf = (props: RenderLeafProps) => , } = props const placeholderRef = useRef(null) @@ -66,7 +66,7 @@ const Leaf = (props: { position: 'absolute', }} > - {leaf.placeholder} + {leaf.placeholder as string} {children} @@ -97,7 +97,7 @@ const MemoizedLeaf = React.memo(Leaf, (prev, next) => { ) }) -export const DefaultLeaf = (props: RenderLeafProps) => { +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 6eb5dbd632..6e7c2ac4e8 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 { Editor, Node, Element, Descendant } from 'slate' +import { Editor, Node, Value, ValueOf } from 'slate' import invariant from 'tiny-invariant' import { ReactEditor } from '../plugin/react-editor' @@ -14,15 +14,15 @@ import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect * is a mutable singleton so it won't ever register as "changed" otherwise. */ -export const Slate = (props: { - editor: ReactEditor - value: Descendant[] +export const Slate = (props: { + editor: ReactEditor + value: V children: React.ReactNode - onChange: (value: Descendant[]) => void + onChange: (value: V) => void }) => { const { editor, children, onChange, value, ...rest } = props const [key, setKey] = useState(0) - const context: [ReactEditor] = useMemo(() => { + const context: [ReactEditor] = useMemo(() => { invariant( Node.isNodeList(value), `[Slate] value is invalid! Expected a list of elements but got: ${JSON.stringify( diff --git a/packages/slate-react/src/components/string.tsx b/packages/slate-react/src/components/string.tsx index 0a9228d957..0083384370 100644 --- a/packages/slate-react/src/components/string.tsx +++ b/packages/slate-react/src/components/string.tsx @@ -7,12 +7,12 @@ import { ReactEditor, useSlateStatic } from '..' * Leaf content strings. */ -const String = (props: { +const String: React.FC<{ isLast: boolean leaf: Text parent: Element text: Text -}) => { +}> = props => { const { isLast, leaf, parent, text } = props const editor = useSlateStatic() const path = ReactEditor.findPath(editor, text) diff --git a/packages/slate-react/src/components/text.tsx b/packages/slate-react/src/components/text.tsx index b83283f902..a75fa98970 100644 --- a/packages/slate-react/src/components/text.tsx +++ b/packages/slate-react/src/components/text.tsx @@ -1,9 +1,9 @@ import React, { useRef } from 'react' -import { Range, Element, Text as SlateText } from 'slate' +import { Range, Element, Text as SlateText, Value } from 'slate' import Leaf from './leaf' import { ReactEditor, useSlateStatic } from '..' -import { RenderLeafProps } from './editable' +import { RenderLeafFn } from './editable' import { useIsomorphicLayoutEffect } from '../hooks/use-isomorphic-layout-effect' import { KEY_TO_ELEMENT, @@ -16,13 +16,13 @@ import { isDecoratorRangeListEqual } from '../utils/range-list' * Text. */ -const Text = (props: { +const Text: React.FC<{ decorations: Range[] isLast: boolean parent: Element - renderLeaf?: (props: RenderLeafProps) => JSX.Element + renderLeaf?: RenderLeafFn text: SlateText -}) => { +}> = props => { const { decorations, isLast, parent, renderLeaf, text } = props const editor = useSlateStatic() const ref = useRef(null) diff --git a/packages/slate-react/src/custom-types.ts b/packages/slate-react/src/custom-types.ts deleted file mode 100644 index c073693362..0000000000 --- a/packages/slate-react/src/custom-types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BaseRange, BaseText } from 'slate' -import { ReactEditor } from './plugin/react-editor' - -declare module 'slate' { - interface CustomTypes { - Editor: ReactEditor - Text: BaseText & { - placeholder: string - } - Range: BaseRange & { - placeholder?: string - } - } -} diff --git a/packages/slate-react/src/hooks/use-children.tsx b/packages/slate-react/src/hooks/use-children.tsx index ae3f3dbbf1..86e80b3a60 100644 --- a/packages/slate-react/src/hooks/use-children.tsx +++ b/packages/slate-react/src/hooks/use-children.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Editor, Range, Element, NodeEntry, Ancestor, Descendant } from 'slate' +import { Editor, Range, Element, Ancestor, Descendant, Value } from 'slate' import ElementComponent from '../components/element' import TextComponent from '../components/text' @@ -7,7 +7,7 @@ import { ReactEditor } from '..' import { useSlateStatic } from './use-slate-static' import { useDecorate } from './use-decorate' import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps' -import { RenderElementProps, RenderLeafProps } from '../components/editable' +import { RenderElementFn, RenderLeafFn } from '../components/editable' /** * Children. @@ -16,8 +16,8 @@ import { RenderElementProps, RenderLeafProps } from '../components/editable' const useChildren = (props: { decorations: Range[] node: Ancestor - renderElement?: (props: RenderElementProps) => JSX.Element - renderLeaf?: (props: RenderLeafProps) => JSX.Element + renderElement?: RenderElementFn + renderLeaf?: RenderLeafFn selection: Range | null }) => { const { decorations, node, renderElement, renderLeaf, selection } = props diff --git a/packages/slate-react/src/hooks/use-decorate.ts b/packages/slate-react/src/hooks/use-decorate.ts index a7eb43a8e6..6f37c514f4 100644 --- a/packages/slate-react/src/hooks/use-decorate.ts +++ b/packages/slate-react/src/hooks/use-decorate.ts @@ -1,5 +1,5 @@ import { createContext, useContext } from 'react' -import { Range, NodeEntry } from 'slate' +import { Range, NodeEntry, NodeOf, Editor, Value } from 'slate' /** * A React context for sharing the `decorate` prop of the editable. @@ -13,6 +13,8 @@ export const DecorateContext = createContext<(entry: NodeEntry) => Range[]>( * Get the current `decorate` prop of the editable. */ -export const useDecorate = (): ((entry: NodeEntry) => Range[]) => { +export const useDecorate = (): (( + entry: NodeEntry>> +) => Range[]) => { return useContext(DecorateContext) } diff --git a/packages/slate-react/src/hooks/use-editor.tsx b/packages/slate-react/src/hooks/use-editor.tsx index d287da3d68..12508a7c41 100644 --- a/packages/slate-react/src/hooks/use-editor.tsx +++ b/packages/slate-react/src/hooks/use-editor.tsx @@ -1,13 +1,20 @@ import { useContext } from 'react' +import warning from 'tiny-warning' import { EditorContext } from './use-slate-static' /** * Get the current editor object from the React context. - * @deprecated Use useSlateStatic instead. + * + * @deprecated Use `useSlateStatic` instead. */ export const useEditor = () => { + warning( + true, + 'slate@0.60 - The `useEditor` hook has been renamed to `useSlateStatic`.' + ) + const editor = useContext(EditorContext) if (!editor) { diff --git a/packages/slate-react/src/hooks/use-slate-static.tsx b/packages/slate-react/src/hooks/use-slate-static.tsx index 4ced7c2d56..93a2e30b16 100644 --- a/packages/slate-react/src/hooks/use-slate-static.tsx +++ b/packages/slate-react/src/hooks/use-slate-static.tsx @@ -1,18 +1,18 @@ import { createContext, useContext } from 'react' +import { Value } from 'slate' import { ReactEditor } from '../plugin/react-editor' -import { Editor } from 'slate' /** * A React context for sharing the editor object. */ -export const EditorContext = createContext(null) +export const EditorContext = createContext | null>(null) /** * Get the current editor object from the React context. */ -export const useSlateStatic = (): Editor => { +export const useSlateStatic = (): ReactEditor => { const editor = useContext(EditorContext) if (!editor) { @@ -21,5 +21,5 @@ export const useSlateStatic = (): Editor => { ) } - return editor + return editor as ReactEditor } diff --git a/packages/slate-react/src/hooks/use-slate.tsx b/packages/slate-react/src/hooks/use-slate.tsx index 8e5736380c..cca41a54f1 100644 --- a/packages/slate-react/src/hooks/use-slate.tsx +++ b/packages/slate-react/src/hooks/use-slate.tsx @@ -1,5 +1,5 @@ import { createContext, useContext } from 'react' -import { Editor } from 'slate' +import { Value } from 'slate' import { ReactEditor } from '../plugin/react-editor' /** @@ -7,13 +7,13 @@ import { ReactEditor } from '../plugin/react-editor' * context whenever changes occur. */ -export const SlateContext = createContext<[ReactEditor] | null>(null) +export const SlateContext = createContext<[ReactEditor] | null>(null) /** * Get the current editor object from the React context. */ -export const useSlate = (): Editor => { +export const useSlate = (): ReactEditor => { const context = useContext(SlateContext) if (!context) { @@ -23,5 +23,5 @@ export const useSlate = (): Editor => { } const [editor] = context - return editor + return editor as ReactEditor } diff --git a/packages/slate-react/src/index.ts b/packages/slate-react/src/index.ts index 1b0974be16..4e5725d97c 100644 --- a/packages/slate-react/src/index.ts +++ b/packages/slate-react/src/index.ts @@ -1,8 +1,9 @@ // Components export { + Editable, + EditableProps, RenderElementProps, RenderLeafProps, - Editable, } from './components/editable' export { DefaultElement } from './components/element' export { DefaultLeaf } from './components/leaf' diff --git a/packages/slate-react/src/plugin/react-editor.ts b/packages/slate-react/src/plugin/react-editor.ts index 915e58ce8c..c3494efa70 100644 --- a/packages/slate-react/src/plugin/react-editor.ts +++ b/packages/slate-react/src/plugin/react-editor.ts @@ -1,4 +1,4 @@ -import { Editor, Node, Path, Point, Range, Transforms, BaseEditor } from 'slate' +import { Editor, Node, Path, Point, Range, Transforms, Value } from 'slate' import { Key } from '../utils/key' import { @@ -30,10 +30,9 @@ import { IS_CHROME } from '../utils/environment' * A React and DOM-specific version of the `Editor` interface. */ -export interface ReactEditor extends BaseEditor { +export type ReactEditor = Editor & { insertData: (data: DataTransfer) => void setFragmentData: (data: DataTransfer) => void - hasRange: (editor: ReactEditor, range: Range) => boolean } export const ReactEditor = { @@ -41,7 +40,7 @@ export const ReactEditor = { * Return the host window of the current editor. */ - getWindow(editor: ReactEditor): Window { + getWindow(editor: ReactEditor): Window { const window = EDITOR_TO_WINDOW.get(editor) if (!window) { throw new Error('Unable to find a host window element for this editor') @@ -53,7 +52,7 @@ export const ReactEditor = { * Find a key for a Slate node. */ - findKey(editor: ReactEditor, node: Node): Key { + findKey(editor: ReactEditor, node: Node): Key { let key = NODE_TO_KEY.get(node) if (!key) { @@ -68,7 +67,7 @@ export const ReactEditor = { * Find the path of Slate node. */ - findPath(editor: ReactEditor, node: Node): Path { + findPath(editor: ReactEditor, node: Node): Path { const path: Path = [] let child = node @@ -102,7 +101,9 @@ export const ReactEditor = { * Find the DOM node that implements DocumentOrShadowRoot for the editor. */ - findDocumentOrShadowRoot(editor: ReactEditor): Document | ShadowRoot { + findDocumentOrShadowRoot( + editor: ReactEditor + ): Document | ShadowRoot { const el = ReactEditor.toDOMNode(editor, editor) const root = el.getRootNode() @@ -125,7 +126,7 @@ export const ReactEditor = { * Check if the editor is focused. */ - isFocused(editor: ReactEditor): boolean { + isFocused(editor: ReactEditor): boolean { return !!IS_FOCUSED.get(editor) }, @@ -133,7 +134,7 @@ export const ReactEditor = { * Check if the editor is in read-only mode. */ - isReadOnly(editor: ReactEditor): boolean { + isReadOnly(editor: ReactEditor): boolean { return !!IS_READ_ONLY.get(editor) }, @@ -141,7 +142,7 @@ export const ReactEditor = { * Blur the editor. */ - blur(editor: ReactEditor): void { + blur(editor: ReactEditor): void { const el = ReactEditor.toDOMNode(editor, editor) const root = ReactEditor.findDocumentOrShadowRoot(editor) IS_FOCUSED.set(editor, false) @@ -155,7 +156,7 @@ export const ReactEditor = { * Focus the editor. */ - focus(editor: ReactEditor): void { + focus(editor: ReactEditor): void { const el = ReactEditor.toDOMNode(editor, editor) const root = ReactEditor.findDocumentOrShadowRoot(editor) IS_FOCUSED.set(editor, true) @@ -169,7 +170,7 @@ export const ReactEditor = { * Deselect the editor. */ - deselect(editor: ReactEditor): void { + deselect(editor: ReactEditor): void { const el = ReactEditor.toDOMNode(editor, editor) const { selection } = editor const root = ReactEditor.findDocumentOrShadowRoot(editor) @@ -188,8 +189,8 @@ export const ReactEditor = { * Check if a DOM node is within the editor. */ - hasDOMNode( - editor: ReactEditor, + hasDOMNode( + editor: ReactEditor, target: DOMNode, options: { editable?: boolean } = {} ): boolean { @@ -229,7 +230,10 @@ export const ReactEditor = { * Insert data from a `DataTransfer` into the editor. */ - insertData(editor: ReactEditor, data: DataTransfer): void { + insertData( + editor: ReactEditor, + data: DataTransfer + ): void { editor.insertData(data) }, @@ -237,7 +241,10 @@ export const ReactEditor = { * Sets data from the currently selected fragment on a `DataTransfer`. */ - setFragmentData(editor: ReactEditor, data: DataTransfer): void { + setFragmentData( + editor: ReactEditor, + data: DataTransfer + ): void { editor.setFragmentData(data) }, @@ -245,7 +252,7 @@ export const ReactEditor = { * Find the native DOM element from a Slate node. */ - toDOMNode(editor: ReactEditor, node: Node): HTMLElement { + toDOMNode(editor: ReactEditor, node: Node): HTMLElement { const domNode = Editor.isEditor(node) ? EDITOR_TO_ELEMENT.get(editor) : KEY_TO_ELEMENT.get(ReactEditor.findKey(editor, node)) @@ -263,7 +270,7 @@ export const ReactEditor = { * Find a native DOM selection point from a Slate point. */ - toDOMPoint(editor: ReactEditor, point: Point): DOMPoint { + toDOMPoint(editor: ReactEditor, point: Point): DOMPoint { const [node] = Editor.node(editor, point.path) const el = ReactEditor.toDOMNode(editor, node) let domPoint: DOMPoint | undefined @@ -320,7 +327,7 @@ export const ReactEditor = { * according to https://dom.spec.whatwg.org/#concept-range-bp-set. */ - toDOMRange(editor: ReactEditor, range: Range): DOMRange { + toDOMRange(editor: ReactEditor, range: Range): DOMRange { const { anchor, focus } = range const isBackward = Range.isBackward(range) const domAnchor = ReactEditor.toDOMPoint(editor, anchor) @@ -354,7 +361,7 @@ export const ReactEditor = { * Find a Slate node from a native DOM `element`. */ - toSlateNode(editor: ReactEditor, domNode: DOMNode): Node { + toSlateNode(editor: ReactEditor, domNode: DOMNode): Node { let domEl = isDOMElement(domNode) ? domNode : domNode.parentElement if (domEl && !domEl.hasAttribute('data-slate-node')) { @@ -374,7 +381,7 @@ export const ReactEditor = { * Get the target range from a DOM `event`. */ - findEventRange(editor: ReactEditor, event: any): Range { + findEventRange(editor: ReactEditor, event: any): Range { if ('nativeEvent' in event) { event = event.nativeEvent } @@ -440,7 +447,10 @@ export const ReactEditor = { * Find a Slate point from a DOM selection's `domNode` and `domOffset`. */ - toSlatePoint(editor: ReactEditor, domPoint: DOMPoint): Point { + toSlatePoint( + editor: ReactEditor, + domPoint: DOMPoint + ): Point { const [nearestNode, nearestOffset] = normalizeDOMPoint(domPoint) const parentNode = nearestNode.parentNode as DOMElement let textNode: DOMElement | null = null @@ -530,8 +540,8 @@ export const ReactEditor = { * Find a Slate range from a DOM range or selection. */ - toSlateRange( - editor: ReactEditor, + toSlateRange( + editor: ReactEditor, domRange: DOMRange | DOMStaticRange | DOMSelection ): Range { const el = isDOMSelection(domRange) @@ -587,11 +597,4 @@ export const ReactEditor = { return { anchor, focus } }, - - hasRange(editor: ReactEditor, range: Range): boolean { - const { anchor, focus } = range - return ( - Editor.hasPath(editor, anchor.path) && Editor.hasPath(editor, focus.path) - ) - }, } diff --git a/packages/slate-react/src/plugin/with-react.ts b/packages/slate-react/src/plugin/with-react.ts index 48ad82f7ee..5848623c6e 100644 --- a/packages/slate-react/src/plugin/with-react.ts +++ b/packages/slate-react/src/plugin/with-react.ts @@ -1,5 +1,5 @@ import ReactDOM from 'react-dom' -import { Editor, Node, Path, Operation, Transforms, Range } from 'slate' +import { Editor, Node, Path, Operation, Transforms, Range, Value } from 'slate' import { ReactEditor } from './react-editor' import { Key } from '../utils/key' @@ -16,8 +16,10 @@ import { findCurrentLineRange } from '../utils/lines' * See https://docs.slatejs.org/concepts/11-typescript to learn how. */ -export const withReact = (editor: T) => { - const e = editor as T & ReactEditor +export const withReact = >( + editor: E +): E & ReactEditor => { + const e = (editor as unknown) as E & ReactEditor const { apply, onChange, deleteBackward } = e e.deleteBackward = unit => { diff --git a/packages/slate-react/src/utils/lines.ts b/packages/slate-react/src/utils/lines.ts index 960d77a559..ae1fe48f28 100644 --- a/packages/slate-react/src/utils/lines.ts +++ b/packages/slate-react/src/utils/lines.ts @@ -2,7 +2,7 @@ * Utilities for single-line deletion */ -import { Range, Editor } from 'slate' +import { Range, Editor, Value } from 'slate' import { ReactEditor } from '..' const doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => { @@ -11,8 +11,8 @@ const doRectsIntersect = (rect: DOMRect, compareRect: DOMRect) => { return rect.top <= middle && rect.bottom >= middle } -const areRangesSameLine = ( - editor: ReactEditor, +const areRangesSameLine = ( + editor: ReactEditor, range1: Range, range2: Range ) => { @@ -30,8 +30,8 @@ const areRangesSameLine = ( * @param {Range} parentRange The parent range to compare against * @returns {Range} A valid portion of the parentRange which is one a single line */ -export const findCurrentLineRange = ( - editor: ReactEditor, +export const findCurrentLineRange = ( + editor: ReactEditor, parentRange: Range ): Range => { const parentRangeBoundary = Editor.range(editor, Range.end(parentRange)) diff --git a/packages/slate-react/src/utils/range-list.ts b/packages/slate-react/src/utils/range-list.ts index 71aff06979..47b486cdda 100644 --- a/packages/slate-react/src/utils/range-list.ts +++ b/packages/slate-react/src/utils/range-list.ts @@ -1,5 +1,5 @@ import { Range } from 'slate' -import { PLACEHOLDER_SYMBOL } from './weak-maps' +import { PLACEHOLDER_SYMBOL } from './symbols' export const shallowCompare = (obj1: {}, obj2: {}) => Object.keys(obj1).length === Object.keys(obj2).length && diff --git a/packages/slate-react/src/utils/symbols.ts b/packages/slate-react/src/utils/symbols.ts new file mode 100644 index 0000000000..73a9a86711 --- /dev/null +++ b/packages/slate-react/src/utils/symbols.ts @@ -0,0 +1,5 @@ +/** + * A symbol for associate decorated ranges and leaves with placeholders. + */ + +export const PLACEHOLDER_SYMBOL = (Symbol('placeholder') as unknown) as string diff --git a/packages/slate-react/src/utils/weak-maps.ts b/packages/slate-react/src/utils/weak-maps.ts index c2fa295ad9..56bae3a314 100644 --- a/packages/slate-react/src/utils/weak-maps.ts +++ b/packages/slate-react/src/utils/weak-maps.ts @@ -1,7 +1,10 @@ -import { Node, Ancestor, Editor, Range } from 'slate' +import { Node, Ancestor, Value } from 'slate' +import { ReactEditor } from '..' import { Key } from './key' +type E = ReactEditor + /** * Two weak maps that allow us rebuild a path given a node. They are populated * at render time such that after a render occurs we can always backtrack. @@ -14,9 +17,9 @@ export const NODE_TO_PARENT: WeakMap = new WeakMap() * Weak maps that allow us to go between Slate nodes and DOM nodes. These * are used to resolve DOM event-related logic into Slate actions. */ -export const EDITOR_TO_WINDOW: WeakMap = new WeakMap() -export const EDITOR_TO_ELEMENT: WeakMap = new WeakMap() -export const EDITOR_TO_PLACEHOLDER: WeakMap = new WeakMap() +export const EDITOR_TO_WINDOW: WeakMap = new WeakMap() +export const EDITOR_TO_ELEMENT: WeakMap = new WeakMap() +export const EDITOR_TO_PLACEHOLDER: WeakMap = new WeakMap() export const ELEMENT_TO_NODE: WeakMap = new WeakMap() export const KEY_TO_ELEMENT: WeakMap = new WeakMap() export const NODE_TO_ELEMENT: WeakMap = new WeakMap() @@ -26,19 +29,13 @@ export const NODE_TO_KEY: WeakMap = new WeakMap() * Weak maps for storing editor-related state. */ -export const IS_READ_ONLY: WeakMap = new WeakMap() -export const IS_FOCUSED: WeakMap = new WeakMap() -export const IS_DRAGGING: WeakMap = new WeakMap() -export const IS_CLICKING: WeakMap = new WeakMap() +export const IS_READ_ONLY: WeakMap = new WeakMap() +export const IS_FOCUSED: WeakMap = new WeakMap() +export const IS_DRAGGING: WeakMap = new WeakMap() +export const IS_CLICKING: WeakMap = new WeakMap() /** * Weak map for associating the context `onChange` context with the plugin. */ -export const EDITOR_TO_ON_CHANGE = new WeakMap void>() - -/** - * Symbols. - */ - -export const PLACEHOLDER_SYMBOL = (Symbol('placeholder') as unknown) as string +export const EDITOR_TO_ON_CHANGE = new WeakMap void>() diff --git a/packages/slate/src/create-editor.ts b/packages/slate/src/create-editor.ts index aa436ca966..e66e318e3a 100755 --- a/packages/slate/src/create-editor.ts +++ b/packages/slate/src/create-editor.ts @@ -1,3 +1,4 @@ +import { DIRTY_PATHS, FLUSHING } from './utils/weak-maps' import { Descendant, Editor, @@ -12,16 +13,19 @@ import { RangeRef, Text, Transforms, + Value, + MarksOf, + ElementOf, + TextOf, } from './' -import { DIRTY_PATHS, FLUSHING } from './utils/weak-maps' /** * Create a new Slate `Editor` object. */ -export const createEditor = (): Editor => { - const editor: Editor = { - children: [], +export const createEditor = (): Editor => { + const editor: Editor = { + children: ([] as unknown) as V, operations: [], selection: null, marks: null, @@ -94,11 +98,10 @@ export const createEditor = (): Editor => { if (selection) { if (Range.isExpanded(selection)) { - Transforms.setNodes( - editor, - { [key]: value }, - { match: Text.isText, split: true } - ) + Transforms.setNodes(editor, { [key]: value } as any, { + match: Text.isText, + split: true, + }) } else { const marks = { ...(Editor.marks(editor) || {}), @@ -139,8 +142,10 @@ export const createEditor = (): Editor => { const { selection } = editor if (selection) { - return Node.fragment(editor, selection) + const fragment = Node.fragment(editor, selection) + return fragment } + return [] }, @@ -148,11 +153,20 @@ export const createEditor = (): Editor => { Transforms.splitNodes(editor, { always: true }) }, - insertFragment: (fragment: Node[]) => { + // @ts-ignore + insertFragment: ( + fragment: Array> | TextOf>> + ) => { Transforms.insertFragment(editor, fragment) }, - insertNode: (node: Node) => { + // @ts-ignore + insertNode: ( + node: + | ElementOf> + | TextOf> + | Array> | TextOf>> + ) => { Transforms.insertNodes(editor, node) }, @@ -182,7 +196,7 @@ export const createEditor = (): Editor => { } if (marks) { - const node = { text, ...marks } + const node = { text, ...marks } as any Transforms.insertNodes(editor, node) } else { Transforms.insertText(editor, text) @@ -202,7 +216,7 @@ export const createEditor = (): Editor => { // Ensure that block and inline nodes have at least one text child. if (Element.isElement(node) && node.children.length === 0) { - const child = { text: '' } + const child = { text: '' } as any Transforms.insertNodes(editor, child, { at: path.concat(0), voids: true, @@ -214,10 +228,10 @@ export const createEditor = (): Editor => { const shouldHaveInlines = Editor.isEditor(node) ? false : Element.isElement(node) && - (editor.isInline(node) || + (editor.isInline(node as any) || node.children.length === 0 || Text.isText(node.children[0]) || - editor.isInline(node.children[0])) + editor.isInline((node as any).children[0])) // Since we'll be applying operations while iterating, keep track of an // index that accounts for any added/removed nodes. @@ -229,7 +243,7 @@ export const createEditor = (): Editor => { const isLast = i === node.children.length - 1 const isInlineOrText = Text.isText(child) || - (Element.isElement(child) && editor.isInline(child)) + (Element.isElement(child) && editor.isInline(child as any)) // Only allow block nodes in the top-level children and parent blocks // that only contain block nodes. Similarly, only allow inline nodes in @@ -240,16 +254,16 @@ export const createEditor = (): Editor => { n-- } else if (Element.isElement(child)) { // Ensure that inline nodes are surrounded by text nodes. - if (editor.isInline(child)) { + if (editor.isInline(child as any)) { if (prev == null || !Text.isText(prev)) { - const newChild = { text: '' } + const newChild = { text: '' } as any Transforms.insertNodes(editor, newChild, { at: path.concat(n), voids: true, }) n++ } else if (isLast) { - const newChild = { text: '' } + const newChild = { text: '' } as any Transforms.insertNodes(editor, newChild, { at: path.concat(n + 1), voids: true, diff --git a/packages/slate/src/index.ts b/packages/slate/src/index.ts old mode 100755 new mode 100644 index 2088071146..54bc3a245c --- a/packages/slate/src/index.ts +++ b/packages/slate/src/index.ts @@ -1,15 +1,4 @@ -export * from './create-editor' -export * from './interfaces/editor' -export * from './interfaces/element' -export * from './interfaces/location' -export * from './interfaces/node' -export * from './interfaces/operation' -export * from './interfaces/path' -export * from './interfaces/path-ref' -export * from './interfaces/point' -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' +import * as Slate from './slate' + +export * from './slate' +export { Slate } diff --git a/packages/slate/src/interfaces/custom-types.ts b/packages/slate/src/interfaces/custom-types.ts deleted file mode 100644 index 397112a05e..0000000000 --- a/packages/slate/src/interfaces/custom-types.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Extendable Custom Types Interface - */ - -type ExtendableTypes = - | 'Editor' - | 'Element' - | 'Text' - | 'Selection' - | 'Range' - | 'Point' - | 'InsertNodeOperation' - | 'InsertTextOperation' - | 'MergeNodeOperation' - | 'MoveNodeOperation' - | 'RemoveNodeOperation' - | 'RemoveTextOperation' - | 'SetNodeOperation' - | 'SetSelectionOperation' - | 'SplitNodeOperation' - -export interface CustomTypes { - [key: string]: unknown -} - -export type ExtendedType< - K extends ExtendableTypes, - B -> = unknown extends CustomTypes[K] ? B : CustomTypes[K] diff --git a/packages/slate/src/interfaces/editor.ts b/packages/slate/src/interfaces/editor.ts index c5f9a0afdd..0c59edfd61 100755 --- a/packages/slate/src/interfaces/editor.ts +++ b/packages/slate/src/interfaces/editor.ts @@ -1,9 +1,6 @@ import isPlainObject from 'is-plain-object' import { reverse as reverseText } from 'esrever' - import { - Ancestor, - ExtendedType, Location, Node, NodeEntry, @@ -16,6 +13,13 @@ import { RangeRef, Span, Text, + AncestorOf, + ElementOf, + NodeOf, + MarksOf, + TextOf, + Element, + NodeMatch, } from '..' import { DIRTY_PATHS, @@ -25,23 +29,22 @@ import { RANGE_REFS, } from '../utils/weak-maps' import { getWordDistance, getCharacterDistance } from '../utils/string' -import { Descendant } from './node' -import { Element } from './element' -export type BaseSelection = Range | null +const IS_EDITOR_CACHE = new WeakMap() -export type Selection = ExtendedType<'Selection', BaseSelection> +export type Value = 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 BaseEditor { - children: Descendant[] +export type Editor = { + children: V selection: Selection operations: Operation[] - marks: Omit | null + marks: Record | null + [key: string]: unknown // Schema-specific node behaviors. isInline: (element: Element) => boolean @@ -55,246 +58,28 @@ export interface BaseEditor { deleteBackward: (unit: 'character' | 'word' | 'line' | 'block') => void deleteForward: (unit: 'character' | 'word' | 'line' | 'block') => void deleteFragment: (direction?: 'forward' | 'backward') => void - getFragment: () => Descendant[] + getFragment: () => Array insertBreak: () => void - insertFragment: (fragment: Node[]) => void - insertNode: (node: Node) => void + insertFragment: (fragment: Array) => void + insertNode: (node: Element | Text | Array) => void insertText: (text: string) => void removeMark: (key: string) => void } -export type Editor = ExtendedType<'Editor', BaseEditor> - -export interface EditorInterface { - above: ( - editor: Editor, - options?: { - at?: Location - match?: NodeMatch - mode?: 'highest' | 'lowest' - voids?: boolean - } - ) => NodeEntry | undefined - addMark: (editor: Editor, key: string, value: any) => void - after: ( - editor: Editor, - at: Location, - options?: { - distance?: number - unit?: 'offset' | 'character' | 'word' | 'line' | 'block' - voids?: boolean - } - ) => Point | undefined - before: ( - editor: Editor, - at: Location, - options?: { - distance?: number - unit?: 'offset' | 'character' | 'word' | 'line' | 'block' - voids?: boolean - } - ) => 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, - options?: { - direction?: 'forward' | 'backward' - } - ) => 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 - hasPath: (editor: Editor, path: Path) => 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 - levels: ( - editor: Editor, - options?: { - at?: Location - match?: NodeMatch - reverse?: boolean - voids?: boolean - } - ) => Generator, void, undefined> - marks: (editor: Editor) => Omit | null - next: ( - editor: Editor, - options?: { - at?: Location - match?: NodeMatch - mode?: 'all' | 'highest' | 'lowest' - voids?: boolean - } - ) => NodeEntry | undefined - node: ( - editor: Editor, - at: Location, - options?: { - depth?: number - edge?: 'start' | 'end' - } - ) => NodeEntry - nodes: ( - editor: Editor, - options?: { - at?: Location | Span - match?: NodeMatch - mode?: 'all' | 'highest' | 'lowest' - universal?: boolean - reverse?: boolean - voids?: boolean - } - ) => Generator, void, undefined> - normalize: ( - editor: Editor, - options?: { - force?: boolean - } - ) => void - parent: ( - editor: Editor, - at: Location, - options?: { - depth?: number - edge?: 'start' | 'end' - } - ) => NodeEntry - 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 - 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 - positions: ( - editor: Editor, - options?: { - at?: Location - unit?: 'offset' | 'character' | 'word' | 'line' | 'block' - reverse?: boolean - voids?: boolean - } - ) => Generator - previous: ( - editor: Editor, - options?: { - at?: Location - match?: NodeMatch - mode?: 'all' | 'highest' | 'lowest' - voids?: boolean - } - ) => NodeEntry | 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 - removeMark: (editor: Editor, key: string) => void - start: (editor: Editor, at: Location) => Point - string: ( - editor: Editor, - at: Location, - options?: { - voids?: boolean - } - ) => string - unhangRange: ( - editor: Editor, - range: Range, - options?: { - voids?: boolean - } - ) => Range - void: ( - editor: Editor, - options?: { - at?: Location - mode?: 'highest' | 'lowest' - voids?: boolean - } - ) => NodeEntry | undefined - withoutNormalizing: (editor: Editor, fn: () => void) => void -} - -const IS_EDITOR_CACHE = new WeakMap() - -export const Editor: EditorInterface = { +export const Editor = { /** * Get the ancestor above a location in the document. */ - above( - editor: Editor, + above( + editor: Editor, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'highest' | 'lowest' voids?: boolean } = {} - ): NodeEntry | undefined { + ): NodeEntry>> | undefined { const { voids = false, mode = 'lowest', @@ -328,7 +113,15 @@ export const Editor: EditorInterface = { * `editor.marks` property instead, and applied when text is inserted next. */ - addMark(editor: Editor, key: string, value: any): void { + addMark< + V extends Value, + M extends MarksOf>, + K extends keyof M & string + >( + editor: Editor, + key: {} extends M ? string : K, + value: {} extends M ? unknown : M[K] + ): void { editor.addMark(key, value) }, @@ -336,8 +129,8 @@ export const Editor: EditorInterface = { * Get the point after a location. */ - after( - editor: Editor, + after( + editor: Editor, at: Location, options: { distance?: number @@ -374,8 +167,8 @@ export const Editor: EditorInterface = { * Get the point before a location. */ - before( - editor: Editor, + before( + editor: Editor, at: Location, options: { distance?: number @@ -413,8 +206,8 @@ export const Editor: EditorInterface = { * Delete content in the editor backward from the current selection. */ - deleteBackward( - editor: Editor, + deleteBackward( + editor: Editor, options: { unit?: 'character' | 'word' | 'line' | 'block' } = {} @@ -427,8 +220,8 @@ export const Editor: EditorInterface = { * Delete content in the editor forward from the current selection. */ - deleteForward( - editor: Editor, + deleteForward( + editor: Editor, options: { unit?: 'character' | 'word' | 'line' | 'block' } = {} @@ -441,8 +234,8 @@ export const Editor: EditorInterface = { * Delete the content in the current selection. */ - deleteFragment( - editor: Editor, + deleteFragment( + editor: Editor, options: { direction?: 'forward' | 'backward' } = {} @@ -455,7 +248,7 @@ export const Editor: EditorInterface = { * Get the start and end points of a location. */ - edges(editor: Editor, at: Location): [Point, Point] { + edges(editor: Editor, at: Location): [Point, Point] { return [Editor.start(editor, at), Editor.end(editor, at)] }, @@ -463,7 +256,7 @@ export const Editor: EditorInterface = { * Get the end point of a location. */ - end(editor: Editor, at: Location): Point { + end(editor: Editor, at: Location): Point { return Editor.point(editor, at, { edge: 'end' }) }, @@ -471,25 +264,33 @@ export const Editor: EditorInterface = { * Get the first node at a location. */ - first(editor: Editor, at: Location): NodeEntry { + first( + editor: Editor, + at: Location + ): NodeEntry>> { const path = Editor.path(editor, at, { edge: 'start' }) - return Editor.node(editor, path) + const first = Editor.node(editor, path) + return first }, /** * Get the fragment at a location. */ - fragment(editor: Editor, at: Location): Descendant[] { + fragment( + editor: Editor, + at: Location + ): Array> | TextOf>> { const range = Editor.range(editor, at) const fragment = Node.fragment(editor, range) return fragment }, + /** * Check if a node has block children. */ - hasBlocks(editor: Editor, element: Element): boolean { + hasBlocks(editor: Editor, element: Element): boolean { return element.children.some(n => Editor.isBlock(editor, n)) }, @@ -497,7 +298,7 @@ export const Editor: EditorInterface = { * Check if a node has inline and text children. */ - hasInlines(editor: Editor, element: Element): boolean { + hasInlines(editor: Editor, element: Element): boolean { return element.children.some( n => Text.isText(n) || Editor.isInline(editor, n) ) @@ -507,7 +308,7 @@ export const Editor: EditorInterface = { * Check if a node has text children. */ - hasTexts(editor: Editor, element: Element): boolean { + hasTexts(editor: Editor, element: Element): boolean { return element.children.every(n => Text.isText(n)) }, @@ -517,7 +318,7 @@ export const Editor: EditorInterface = { * If the selection is currently expanded, it will be deleted first. */ - insertBreak(editor: Editor): void { + insertBreak(editor: Editor): void { editor.insertBreak() }, @@ -527,7 +328,10 @@ export const Editor: EditorInterface = { * If the selection is currently expanded, it will be deleted first. */ - insertFragment(editor: Editor, fragment: Node[]): void { + insertFragment( + editor: Editor, + fragment: Array> | TextOf>> + ): void { editor.insertFragment(fragment) }, @@ -537,7 +341,13 @@ export const Editor: EditorInterface = { * If the selection is currently expanded, it will be deleted first. */ - insertNode(editor: Editor, node: Node): void { + insertNode( + editor: Editor, + node: + | ElementOf> + | TextOf> + | Array> | TextOf>> + ): void { editor.insertNode(node) }, @@ -547,7 +357,7 @@ export const Editor: EditorInterface = { * If the selection is currently expanded, it will be deleted first. */ - insertText(editor: Editor, text: string): void { + insertText(editor: Editor, text: string): void { editor.insertText(text) }, @@ -555,7 +365,7 @@ export const Editor: EditorInterface = { * Check if a value is a block `Element` object. */ - isBlock(editor: Editor, value: any): value is Element { + isBlock(editor: Editor, value: any): value is Element { return Element.isElement(value) && !editor.isInline(value) }, @@ -563,7 +373,7 @@ export const Editor: EditorInterface = { * Check if a value is an `Editor` object. */ - isEditor(value: any): value is Editor { + isEditor(value: any): value is Editor { if (!isPlainObject(value)) return false const cachedIsEditor = IS_EDITOR_CACHE.get(value) if (cachedIsEditor !== undefined) { @@ -596,7 +406,11 @@ export const Editor: EditorInterface = { * Check if a point is the end point of a location. */ - isEnd(editor: Editor, point: Point, at: Location): boolean { + isEnd( + editor: Editor, + point: Point, + at: Location + ): boolean { const end = Editor.end(editor, at) return Point.equals(point, end) }, @@ -605,7 +419,11 @@ export const Editor: EditorInterface = { * Check if a point is an edge of a location. */ - isEdge(editor: Editor, point: Point, at: Location): boolean { + isEdge( + editor: Editor, + point: Point, + at: Location + ): boolean { return Editor.isStart(editor, point, at) || Editor.isEnd(editor, point, at) }, @@ -613,7 +431,10 @@ export const Editor: EditorInterface = { * Check if an element is empty, accounting for void nodes. */ - isEmpty(editor: Editor, element: Element): boolean { + isEmpty( + editor: Editor, + element: ElementOf> + ): boolean { const { children } = element const [first] = children return ( @@ -629,15 +450,15 @@ export const Editor: EditorInterface = { * Check if a value is an inline `Element` object. */ - isInline(editor: Editor, value: any): value is Element { - return Element.isElement(value) && editor.isInline(value) + isInline(editor: Editor, value: any): value is Element { + return Element.isElement(value) && editor.isInline(value as any) }, /** * Check if the editor is currently normalizing after each operation. */ - isNormalizing(editor: Editor): boolean { + isNormalizing(editor: Editor): boolean { const isNormalizing = NORMALIZING.get(editor) return isNormalizing === undefined ? true : isNormalizing }, @@ -646,7 +467,11 @@ export const Editor: EditorInterface = { * Check if a point is the start point of a location. */ - isStart(editor: Editor, point: Point, at: Location): boolean { + isStart( + editor: Editor, + point: Point, + at: Location + ): boolean { // PERF: If the offset isn't `0` we know it's not the start. if (point.offset !== 0) { return false @@ -660,31 +485,35 @@ export const Editor: EditorInterface = { * Check if a value is a void `Element` object. */ - isVoid(editor: Editor, value: any): value is Element { - return Element.isElement(value) && editor.isVoid(value) + isVoid(editor: Editor, value: any): value is Element { + return Element.isElement(value) && editor.isVoid(value as any) }, /** * Get the last node at a location. */ - last(editor: Editor, at: Location): NodeEntry { + last( + editor: Editor, + at: Location + ): NodeEntry>> { const path = Editor.path(editor, at, { edge: 'end' }) - return Editor.node(editor, path) + const node = Editor.node(editor, path) + return node }, /** * Get the leaf text node at a location. */ - leaf( - editor: Editor, + leaf( + editor: Editor, at: Location, options: { depth?: number edge?: 'start' | 'end' } = {} - ): NodeEntry { + ): NodeEntry>> { const path = Editor.path(editor, at, options) const node = Node.leaf(editor, path) return [node, path] @@ -694,15 +523,15 @@ export const Editor: EditorInterface = { * Iterate through all of the levels at a location. */ - *levels( - editor: Editor, + *levels( + editor: Editor, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> reverse?: boolean voids?: boolean } = {} - ): Generator, void, undefined> { + ): Generator>>, void, undefined> { const { at = editor.selection, reverse = false, voids = false } = options let { match } = options @@ -714,7 +543,7 @@ export const Editor: EditorInterface = { return } - const levels: NodeEntry[] = [] + const levels: NodeEntry>>[] = [] const path = Editor.path(editor, at) for (const [n, p] of Node.levels(editor, path)) { @@ -740,15 +569,17 @@ export const Editor: EditorInterface = { * Get the marks that would be added to text at the current selection. */ - marks(editor: Editor): Omit | null { - const { marks, selection } = editor + marks( + editor: Editor + ): Partial>> | null { + let { marks, selection } = editor if (!selection) { return null } if (marks) { - return marks + return editor.marks as any } if (Range.isExpanded(selection)) { @@ -756,8 +587,8 @@ export const Editor: EditorInterface = { if (match) { const [node] = match as NodeEntry - const { text, ...rest } = node - return rest + marks = Node.props(node) + return marks as any } else { return {} } @@ -778,28 +609,28 @@ export const Editor: EditorInterface = { const [, blockPath] = block if (Path.isAncestor(blockPath, prevPath)) { - node = prevNode as Text + node = prevNode as TextOf> } } } - const { text, ...rest } = node - return rest + marks = Node.props(node) + return marks as any }, /** * Get the matching node in the branch of the document after a location. */ - next( - editor: Editor, + next( + editor: Editor, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'all' | 'highest' | 'lowest' voids?: boolean } = {} - ): NodeEntry | undefined { + ): NodeEntry>> | undefined { const { mode = 'lowest', voids = false } = options let { match, at = editor.selection } = options @@ -822,7 +653,7 @@ export const Editor: EditorInterface = { if (match == null) { if (Path.isPath(at)) { const [parent] = Editor.parent(editor, at) - match = n => parent.children.includes(n) + match = n => parent.children.includes(n as any) } else { match = () => true } @@ -836,14 +667,14 @@ export const Editor: EditorInterface = { * Get the node at a location. */ - node( - editor: Editor, + node( + editor: Editor, at: Location, options: { depth?: number edge?: 'start' | 'end' } = {} - ): NodeEntry { + ): NodeEntry>> { const path = Editor.path(editor, at, options) const node = Node.get(editor, path) return [node, path] @@ -853,17 +684,17 @@ export const Editor: EditorInterface = { * Iterate through all of the nodes in the Editor. */ - *nodes( - editor: Editor, + *nodes( + editor: Editor, options: { at?: Location | Span - match?: NodeMatch + match?: NodeMatch>> mode?: 'all' | 'highest' | 'lowest' universal?: boolean reverse?: boolean voids?: boolean } = {} - ): Generator, void, undefined> { + ): Generator>>, void, undefined> { const { at = editor.selection, mode = 'all', @@ -901,8 +732,8 @@ export const Editor: EditorInterface = { pass: ([n]) => (voids ? false : Editor.isVoid(editor, n)), }) - const matches: NodeEntry[] = [] - let hit: NodeEntry | undefined + const matches: NodeEntry>>[] = [] + let hit: NodeEntry>> | undefined for (const [node, path] of nodeEntries) { const isLower = hit && Path.compare(path, hit[1]) === 0 @@ -930,7 +761,7 @@ export const Editor: EditorInterface = { } // In lowest mode we emit the last hit, once it's guaranteed lowest. - const emit: NodeEntry | undefined = + const emit: NodeEntry>> | undefined = mode === 'lowest' ? hit : [node, path] if (emit) { @@ -959,18 +790,19 @@ export const Editor: EditorInterface = { yield* matches } }, + /** * Normalize any dirty objects in the editor. */ - normalize( - editor: Editor, + normalize( + editor: Editor, options: { force?: boolean } = {} ): void { const { force = false } = options - const getDirtyPaths = (editor: Editor) => { + const getDirtyPaths = (editor: Editor) => { return DIRTY_PATHS.get(editor) || [] } @@ -1014,26 +846,26 @@ export const Editor: EditorInterface = { * Get the parent node of a location. */ - parent( - editor: Editor, + parent( + editor: Editor, at: Location, options: { depth?: number edge?: 'start' | 'end' } = {} - ): NodeEntry { + ): NodeEntry>> { const path = Editor.path(editor, at, options) const parentPath = Path.parent(path) const entry = Editor.node(editor, parentPath) - return entry as NodeEntry + return entry as NodeEntry>> }, /** * Get the path of a location. */ - path( - editor: Editor, + path( + editor: Editor, at: Location, options: { depth?: number @@ -1073,17 +905,13 @@ export const Editor: EditorInterface = { return at }, - hasPath(editor: Editor, path: Path): boolean { - return Node.has(editor, path) - }, - /** * Create a mutable ref for a `Path` object, which will stay in sync as new * operations are applied to the editor. */ - pathRef( - editor: Editor, + pathRef( + editor: Editor, path: Path, options: { affinity?: 'backward' | 'forward' | null @@ -1111,7 +939,7 @@ export const Editor: EditorInterface = { * Get the set of currently tracked path refs of the editor. */ - pathRefs(editor: Editor): Set { + pathRefs(editor: Editor): Set { let refs = PATH_REFS.get(editor) if (!refs) { @@ -1126,8 +954,8 @@ export const Editor: EditorInterface = { * Get the start or end point of a location. */ - point( - editor: Editor, + point( + editor: Editor, at: Location, options: { edge?: 'start' | 'end' @@ -1170,8 +998,8 @@ export const Editor: EditorInterface = { * operations are applied to the editor. */ - pointRef( - editor: Editor, + pointRef( + editor: Editor, point: Point, options: { affinity?: 'backward' | 'forward' | null @@ -1199,7 +1027,7 @@ export const Editor: EditorInterface = { * Get the set of currently tracked point refs of the editor. */ - pointRefs(editor: Editor): Set { + pointRefs(editor: Editor): Set { let refs = POINT_REFS.get(editor) if (!refs) { @@ -1223,8 +1051,8 @@ export const Editor: EditorInterface = { * voids option, then iteration will occur. */ - *positions( - editor: Editor, + *positions( + editor: Editor, options: { at?: Location unit?: 'offset' | 'character' | 'word' | 'line' | 'block' @@ -1281,12 +1109,12 @@ export const Editor: EditorInterface = { // Void nodes are a special case, so by default we will always // yield their first point. If the voids option is set to true, // then we will iterate over their content - if (!voids && editor.isVoid(node)) { + if (!voids && editor.isVoid(node as ElementOf>)) { yield Editor.start(editor, path) continue } - if (editor.isInline(node)) { + if (editor.isInline(node as ElementOf>)) { continue } @@ -1344,15 +1172,15 @@ export const Editor: EditorInterface = { * Get the matching node in the branch of the document before a location. */ - previous( - editor: Editor, + previous( + editor: Editor, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'all' | 'highest' | 'lowest' voids?: boolean } = {} - ): NodeEntry | undefined { + ): NodeEntry>> | undefined { const { mode = 'lowest', voids = false } = options let { match, at = editor.selection } = options @@ -1379,7 +1207,7 @@ export const Editor: EditorInterface = { if (match == null) { if (Path.isPath(at)) { const [parent] = Editor.parent(editor, at) - match = n => parent.children.includes(n) + match = n => parent.children.includes(n as any) } else { match = () => true } @@ -1400,7 +1228,11 @@ export const Editor: EditorInterface = { * Get a range of a location. */ - range(editor: Editor, at: Location, to?: Location): Range { + range( + editor: Editor, + at: Location, + to?: Location + ): Range { if (Range.isRange(at) && !to) { return at } @@ -1415,8 +1247,8 @@ export const Editor: EditorInterface = { * operations are applied to the editor. */ - rangeRef( - editor: Editor, + rangeRef( + editor: Editor, range: Range, options: { affinity?: 'backward' | 'forward' | 'outward' | 'inward' | null @@ -1444,7 +1276,7 @@ export const Editor: EditorInterface = { * Get the set of currently tracked range refs of the editor. */ - rangeRefs(editor: Editor): Set { + rangeRefs(editor: Editor): Set { let refs = RANGE_REFS.get(editor) if (!refs) { @@ -1463,7 +1295,10 @@ export const Editor: EditorInterface = { * `editor.marks` and applied to the text inserted next. */ - removeMark(editor: Editor, key: string): void { + removeMark>>( + editor: Editor, + key: {} extends M ? string : keyof M & string + ): void { editor.removeMark(key) }, @@ -1471,7 +1306,7 @@ export const Editor: EditorInterface = { * Get the start point of a location. */ - start(editor: Editor, at: Location): Point { + start(editor: Editor, at: Location): Point { return Editor.point(editor, at, { edge: 'start' }) }, @@ -1482,8 +1317,8 @@ export const Editor: EditorInterface = { * string, regardless of content, unless you pass in true for the voids option */ - string( - editor: Editor, + string( + editor: Editor, at: Location, options: { voids?: boolean @@ -1519,8 +1354,8 @@ export const Editor: EditorInterface = { * Convert a range into a non-hanging one. */ - unhangRange( - editor: Editor, + unhangRange( + editor: Editor, range: Range, options: { voids?: boolean @@ -1567,25 +1402,29 @@ export const Editor: EditorInterface = { * Match a void node in the current branch of the editor. */ - void( - editor: Editor, + void( + editor: Editor, options: { at?: Location mode?: 'highest' | 'lowest' voids?: boolean } = {} - ): NodeEntry | undefined { - return Editor.above(editor, { + ): NodeEntry>> | undefined { + const entry = Editor.above(editor, { ...options, match: n => Editor.isVoid(editor, n), }) + + if (entry) { + return entry as NodeEntry>> + } }, /** * Call a function, deferring normalization until after it completes. */ - withoutNormalizing(editor: Editor, fn: () => void): void { + withoutNormalizing(editor: Editor, fn: () => void): void { const value = Editor.isNormalizing(editor) NORMALIZING.set(editor, false) fn() @@ -1595,9 +1434,14 @@ export const Editor: EditorInterface = { } /** - * A helper type for narrowing matched nodes with a predicate. + * The `Selection` interface is a special type of `Range` that can contain extra + * properties, and can be `null` to mean no selection exists in the editor. + */ + +export type Selection = Range | null + +/** + * A helper type for getting the value of an editor. */ -export type NodeMatch = - | ((node: Node, path: Path) => node is T) - | ((node: Node, path: Path) => boolean) +export type ValueOf> = E['children'] diff --git a/packages/slate/src/interfaces/element.ts b/packages/slate/src/interfaces/element.ts index e3b1097c8e..17feff1db6 100755 --- a/packages/slate/src/interfaces/element.ts +++ b/packages/slate/src/interfaces/element.ts @@ -1,35 +1,43 @@ import isPlainObject from 'is-plain-object' -import { Editor, Node, Path, Descendant, ExtendedType, Ancestor } from '..' +import { Editor, Node, Path, Text, Value } from '..' +import { DescendantOf, NodeOf } from './node' /** - * `Element` objects are a type of node in a Slate document that contain other - * element nodes or text nodes. They can be either "blocks" or "inlines" - * depending on the Slate editor's configuration. + * `ElementEntry` objects refer to an `Element` and the `Path` where it can be + * found inside a root node. */ -export interface BaseElement { - children: Descendant[] -} +export type ElementEntry = [Element, Path] -export type Element = ExtendedType<'Element', BaseElement> +/** + * A utility type to get all the element nodes type from a root node. + */ -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 - matches: (element: Element, props: Partial) => boolean -} +export type ElementOf = Editor extends N + ? Element + : Element extends N + ? Element + : N extends Editor + ? Extract | ElementOf + : N extends Element + ? + | N + | Extract + | ElementOf + : never -export const Element: ElementInterface = { - /** - * Check if a value implements the 'Ancestor' interface. - */ +/** + * `Element` objects are a type of node in a Slate document that contain other + * element nodes or text nodes. They can be either "blocks" or "inlines" + * depending on the Slate editor's configuration. + */ - isAncestor(value: any): value is Ancestor { - return isPlainObject(value) && Node.isNodeList(value.children) - }, +export interface Element { + children: Array + [key: string]: unknown +} +export const Element = { /** * Check if a value implements the `Element` interface. */ @@ -50,14 +58,6 @@ export const Element: ElementInterface = { return Array.isArray(value) && value.every(val => Element.isElement(val)) }, - /** - * 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. * @@ -65,7 +65,7 @@ export const Element: ElementInterface = { * children are equivalent. */ - matches(element: Element, props: Partial): boolean { + matches(element: Element, props: object): boolean { for (const key in props) { if (key === 'children') { continue @@ -79,10 +79,3 @@ export const Element: ElementInterface = { return true }, } - -/** - * `ElementEntry` objects refer to an `Element` and the `Path` where it can be - * found inside a root node. - */ - -export type ElementEntry = [Element, Path] diff --git a/packages/slate/src/interfaces/location.ts b/packages/slate/src/interfaces/location.ts index 712e4f49d6..a1537c1ddf 100755 --- a/packages/slate/src/interfaces/location.ts +++ b/packages/slate/src/interfaces/location.ts @@ -11,11 +11,7 @@ import { Path, Point, Range } from '..' export type Location = Path | Point | Range -export interface LocationInterface { - isLocation: (value: any) => value is Location -} - -export const Location: LocationInterface = { +export const Location = { /** * Check if a value implements the `Location` interface. */ @@ -32,11 +28,7 @@ export const Location: LocationInterface = { export type Span = [Path, Path] -export interface SpanInterface { - isSpan: (value: any) => value is Span -} - -export const Span: SpanInterface = { +export const Span = { /** * Check if a value implements the `Span` interface. */ diff --git a/packages/slate/src/interfaces/node.ts b/packages/slate/src/interfaces/node.ts index e12a3a7640..09450b7199 100755 --- a/packages/slate/src/interfaces/node.ts +++ b/packages/slate/src/interfaces/node.ts @@ -1,99 +1,30 @@ import { produce } from 'immer' -import { Editor, Path, Range, Text } from '..' -import { Element, ElementEntry } from './element' +import isPlainObject from 'is-plain-object' +import { + Editor, + Path, + Range, + Text, + Element, + ElementOf, + TextOf, + Value, +} from '..' +const IS_NODE_LIST_CACHE = new WeakMap() /** * The `Node` union type represents all of the different types of nodes that * occur in a Slate document tree. */ -export type BaseNode = Editor | Element | Text -export type Node = Editor | Element | Text - -export interface NodeInterface { - ancestor: (root: Node, path: Path) => Ancestor - ancestors: ( - root: Node, - path: Path, - options?: { - reverse?: boolean - } - ) => Generator, void, undefined> - child: (root: Node, index: number) => Descendant - children: ( - root: Node, - path: Path, - options?: { - reverse?: boolean - } - ) => Generator, 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, void, undefined> - elements: ( - root: Node, - options?: { - from?: Path - to?: Path - reverse?: boolean - pass?: (node: NodeEntry) => boolean - } - ) => Generator - 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 - matches: (node: Node, props: Partial) => boolean - nodes: ( - root: Node, - options?: { - from?: Path - to?: Path - reverse?: boolean - pass?: (entry: NodeEntry) => boolean - } - ) => Generator - 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, void, undefined> -} - -const IS_NODE_LIST_CACHE = new WeakMap() +export type Node = Editor | Element | Text -export const Node: NodeInterface = { +export const Node = { /** * Get the node at a specific path, asserting that it's an ancestor node. */ - ancestor(root: Node, path: Path): Ancestor { + ancestor(root: N, path: Path): AncestorOf { const node = Node.get(root, path) if (Text.isText(node)) { @@ -102,7 +33,7 @@ export const Node: NodeInterface = { ) } - return node + return node as AncestorOf }, /** @@ -112,17 +43,16 @@ export const Node: NodeInterface = { * the tree, but you can pass the `reverse: true` option to go top-down. */ - *ancestors( - root: Node, + *ancestors( + root: N, path: Path, options: { reverse?: boolean } = {} - ): Generator, void, undefined> { + ): Generator>, void, undefined> { for (const p of Path.ancestors(path, options)) { const n = Node.ancestor(root, p) - const entry: NodeEntry = [n, p] - yield entry + yield [n, p] as NodeEntry> } }, @@ -130,14 +60,14 @@ export const Node: NodeInterface = { * Get the child of a node at a specific index. */ - child(root: Node, index: number): Descendant { + child(root: N, index: I): ChildOf { if (Text.isText(root)) { throw new Error( `Cannot get the child of a text node: ${JSON.stringify(root)}` ) } - const c = root.children[index] as Descendant + const c = (root as AncestorOf).children[index] as ChildOf if (c == null) { throw new Error( @@ -154,13 +84,13 @@ export const Node: NodeInterface = { * Iterate over the children of a node at a specific path. */ - *children( - root: Node, + *children( + root: N, path: Path, options: { reverse?: boolean } = {} - ): Generator, void, undefined> { + ): Generator>, void, undefined> { const { reverse = false } = options const ancestor = Node.ancestor(root, path) const { children } = ancestor @@ -169,7 +99,7 @@ export const Node: NodeInterface = { while (reverse ? index >= 0 : index < children.length) { const child = Node.child(ancestor, index) const childPath = path.concat(index) - yield [child, childPath] + yield [child, childPath] as NodeEntry> index = reverse ? index - 1 : index + 1 } }, @@ -178,7 +108,11 @@ export const Node: NodeInterface = { * Get an entry for the common ancesetor node of two paths. */ - common(root: Node, path: Path, another: Path): NodeEntry { + common( + root: N, + path: Path, + another: Path + ): NodeEntry> { const p = Path.common(path, another) const n = Node.get(root, p) return [n, p] @@ -188,7 +122,7 @@ export const Node: NodeInterface = { * Get the node at a specific path, asserting that it's a descendant node. */ - descendant(root: Node, path: Path): Descendant { + descendant(root: N, path: Path): DescendantOf { const node = Node.get(root, path) if (Editor.isEditor(node)) { @@ -197,27 +131,30 @@ export const Node: NodeInterface = { ) } - return node + return node as DescendantOf }, /** * Return a generator of all the descendant node entries inside a root node. */ - *descendants( - root: Node, + *descendants( + root: N, options: { from?: Path to?: Path reverse?: boolean - pass?: (node: NodeEntry) => boolean + pass?: (node: NodeEntry>) => boolean } = {} - ): Generator, void, undefined> { - for (const [node, path] of Node.nodes(root, options)) { + ): Generator>, void, undefined> { + const { pass, ...rest } = options + + for (const [node, path] of Node.nodes(root, rest)) { if (path.length !== 0) { - // NOTE: we have to coerce here because checking the path's length does - // guarantee that `node` is not a `Editor`, but TypeScript doesn't know. - yield [node, path] as NodeEntry + const e = [node, path] as NodeEntry> + if (pass == null || pass(e)) { + yield e + } } } }, @@ -228,51 +165,40 @@ export const Node: NodeInterface = { * root node is an element it will be included in the iteration as well. */ - *elements( - root: Node, + *elements( + root: N, options: { from?: Path to?: Path reverse?: boolean - pass?: (node: NodeEntry) => boolean + pass?: (node: NodeEntry>) => boolean } = {} - ): Generator { - for (const [node, path] of Node.nodes(root, options)) { + ): Generator>, void, undefined> { + const { pass, ...rest } = options + + for (const [node, path] of Node.nodes(root, rest)) { if (Element.isElement(node)) { - yield [node, path] + const e = [node, path] as NodeEntry> + if (pass == null || pass(e)) { + yield e + } } } }, - /** - * 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. */ - first(root: Node, path: Path): NodeEntry { + first(root: N, path: Path): NodeEntry> { const p = path.slice() let n = Node.get(root, p) while (n) { - if (Text.isText(n) || n.children.length === 0) { + if (Text.isText(n) || (n as AncestorOf).children.length === 0) { break } else { - n = n.children[0] + n = Node.child(n, 0) as NodeOf p.push(0) } } @@ -284,7 +210,10 @@ export const Node: NodeInterface = { * Get the sliced fragment represented by a range inside a root node. */ - fragment(root: Node, range: Range): Descendant[] { + fragment( + root: N, + range: Range + ): Array | TextOf> { if (Text.isText(root)) { throw new Error( `Cannot get a fragment starting from a root text node: ${JSON.stringify( @@ -293,7 +222,11 @@ export const Node: NodeInterface = { ) } - const newRoot = produce({ children: root.children }, r => { + const children = (root as any).children as Array | TextOf> + const newRoot = produce({ children }, draft => { + // HACK: get TypeScript not to complain about the draft being special. + const r = (draft as any) as Ancestor + const [start, end] = Range.edges(range) const nodeEntries = Node.nodes(r, { reverse: true, @@ -304,7 +237,7 @@ export const Node: NodeInterface = { if (!Range.includes(range, path)) { const parent = Node.parent(r, path) const index = path[path.length - 1] - parent.children.splice(index, 1) + ;(parent as Ancestor).children.splice(index, 1) } if (Path.equals(path, end.path)) { @@ -313,7 +246,7 @@ export const Node: NodeInterface = { } if (Path.equals(path, start.path)) { - const leaf = Node.leaf(r, path) + const leaf = Node.leaf(r, path) as Text leaf.text = leaf.text.slice(start.offset) } } @@ -331,13 +264,14 @@ export const Node: NodeInterface = { * empty array, it refers to the root node itself. */ - get(root: Node, path: Path): Node { - let node = root + get(root: N, path: Path): NodeOf { + // @ts-ignore + let node: NodeOf = root for (let i = 0; i < path.length; i++) { const p = path[i] - if (Text.isText(node) || !node.children[p]) { + if (Text.isText(node) || !(node as Ancestor).children[p]) { throw new Error( `Cannot find a descendant at path [${path}] in node: ${JSON.stringify( root @@ -345,7 +279,7 @@ export const Node: NodeInterface = { ) } - node = node.children[p] + node = (node as Ancestor).children[p] as NodeOf } return node @@ -371,6 +305,14 @@ export const Node: NodeInterface = { return true }, + /** + * 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 `Node` interface. */ @@ -402,16 +344,16 @@ export const Node: NodeInterface = { * Get the last node entry in a root node from a path. */ - last(root: Node, path: Path): NodeEntry { + last(root: N, path: Path): NodeEntry> { const p = path.slice() let n = Node.get(root, p) while (n) { - if (Text.isText(n) || n.children.length === 0) { + if (Text.isText(n) || (n as Ancestor).children.length === 0) { break } else { - const i = n.children.length - 1 - n = n.children[i] + const i = (n as Ancestor).children.length - 1 + n = (n as Ancestor).children[i] as NodeOf p.push(i) } } @@ -423,7 +365,7 @@ export const Node: NodeInterface = { * Get the node at a specific path, ensuring it's a leaf text node. */ - leaf(root: Node, path: Path): Text { + leaf(root: N, path: Path): TextOf { const node = Node.get(root, path) if (!Text.isText(node)) { @@ -432,7 +374,7 @@ export const Node: NodeInterface = { ) } - return node + return node as TextOf }, /** @@ -442,13 +384,13 @@ export const Node: NodeInterface = { * but you can pass the `reverse: true` option to go bottom-up. */ - *levels( - root: Node, + *levels( + root: N, path: Path, options: { reverse?: boolean } = {} - ): Generator { + ): Generator>, void, undefined> { for (const p of Path.levels(path, options)) { const n = Node.get(root, p) yield [n, p] @@ -459,14 +401,10 @@ export const Node: NodeInterface = { * Check if a node matches a set of props. */ - matches(node: Node, props: Partial): boolean { + matches(node: Node, props: object): boolean { return ( - (Element.isElement(node) && - Element.isElementProps(props) && - Element.matches(node, props)) || - (Text.isText(node) && - Text.isTextProps(props) && - Text.matches(node, props)) + (Element.isElement(node) && Element.matches(node, props)) || + (Text.isText(node) && Text.matches(node, props)) ) }, @@ -476,20 +414,21 @@ export const Node: NodeInterface = { * position inside the root node. */ - *nodes( - root: Node, + *nodes( + root: N, options: { from?: Path to?: Path reverse?: boolean - pass?: (entry: NodeEntry) => boolean + pass?: (entry: NodeEntry>) => boolean } = {} - ): Generator { + ): Generator>, void, undefined> { const { pass, reverse = false } = options const { from = [], to } = options const visited = new Set() + // @ts-ignore + let n: NodeOf = root let p: Path = [] - let n = root while (true) { if (to && (reverse ? Path.isBefore(p, to) : Path.isAfter(p, to))) { @@ -504,11 +443,11 @@ export const Node: NodeInterface = { if ( !visited.has(n) && !Text.isText(n) && - n.children.length !== 0 && + (n as Ancestor).children.length !== 0 && (pass == null || pass([n, p]) === false) ) { visited.add(n) - let nextIndex = reverse ? n.children.length - 1 : 0 + let nextIndex = reverse ? (n as Ancestor).children.length - 1 : 0 if (Path.isAncestor(p, from)) { nextIndex = from[p.length] @@ -554,7 +493,7 @@ export const Node: NodeInterface = { * Get the parent of a node at a specific path. */ - parent(root: Node, path: Path): Ancestor { + parent(root: N, path: Path): AncestorOf { const parentPath = Path.parent(path) const p = Node.get(root, parentPath) @@ -564,7 +503,21 @@ export const Node: NodeInterface = { ) } - return p + return p as AncestorOf + }, + + /** + * Extract the custom properties from a node. + */ + + props(node: N): NodeProps { + if (Text.isText(node)) { + const { text, ...props } = node + return props as NodeProps + } else { + const { children, ...props } = node as AncestorOf + return (props as unknown) as NodeProps + } }, /** @@ -587,30 +540,54 @@ export const Node: NodeInterface = { * Return a generator of all leaf text nodes in a root node. */ - *texts( - root: Node, + *texts( + root: N, options: { from?: Path to?: Path reverse?: boolean - pass?: (node: NodeEntry) => boolean + pass?: (node: NodeEntry>) => boolean } = {} - ): Generator, void, undefined> { + ): Generator>, void, undefined> { for (const [node, path] of Node.nodes(root, options)) { if (Text.isText(node)) { - yield [node, path] + yield [node as TextOf, path] } } }, } /** - * The `Descendant` union type represents nodes that are descendants in the - * tree. It is returned as a convenience in certain cases to narrow a value - * further than the more generic `Node` union. + * `NodeEntry` objects are returned when iterating over the nodes in a Slate + * document tree. They consist of the node and its `Path` relative to the root + * node in the document. */ -export type Descendant = Element | Text +export type NodeEntry = [N, Path] + +/** + * A utility type to get all the node types from a root node type. + */ + +export type NodeOf = N | ElementOf | TextOf + +/** + * Convenience type for returning the props of a node. + */ + +export type NodeProps = N extends Editor + ? Omit + : N extends Element + ? Omit + : Omit + +/** + * A helper type for narrowing matched nodes with a predicate. + */ + +export type NodeMatch = + | ((node: Node, path: Path) => node is T) + | ((node: Node, path: Path) => boolean) /** * The `Ancestor` union type represents nodes that are ancestors in the tree. @@ -618,20 +595,49 @@ export type Descendant = Element | Text * than the more generic `Node` union. */ -export type Ancestor = Editor | Element +export type Ancestor = Editor | Element /** - * `NodeEntry` objects are returned when iterating over the nodes in a Slate - * document tree. They consist of the node and its `Path` relative to the root - * node in the document. + * A utility type to get all the ancestor node types from a root node type. */ -export type NodeEntry = [T, Path] +export type AncestorOf = Editor extends N + ? Editor | Element + : Element extends N + ? Element + : N extends Editor + ? N | N['children'][number] | ElementOf + : N extends Element + ? N | ElementOf + : never /** - * Convenience type for returning the props of a node. + * The `Descendant` union type represents nodes that are descendants in the + * tree. It is returned as a convenience in certain cases to narrow a value + * further than the more generic `Node` union. */ -export type NodeProps = - | Omit - | Omit - | Omit + +export type Descendant = Element | Text + +/** + * A utility type to get all the descendant node types from a root node type. + */ + +export type DescendantOf = N extends Editor + ? ElementOf | TextOf + : N extends Element + ? ElementOf | TextOf + : never + +/** + * A utility type to get the child node types from a root node type. + */ + +export type ChildOf< + N extends Node, + I extends number = number +> = N extends Editor + ? N['children'][I] + : N extends Element + ? N['children'][I] + : never diff --git a/packages/slate/src/interfaces/operation.ts b/packages/slate/src/interfaces/operation.ts index e97431af82..48829df4d2 100755 --- a/packages/slate/src/interfaces/operation.ts +++ b/packages/slate/src/interfaces/operation.ts @@ -1,121 +1,87 @@ -import { ExtendedType, Node, Path, Range } from '..' import isPlainObject from 'is-plain-object' +import { Node, Path, Range } from '..' -export type BaseInsertNodeOperation = { +export type InsertNodeOperation = { type: 'insert_node' path: Path node: Node + [key: string]: unknown } -export type InsertNodeOperation = ExtendedType< - 'InsertNodeOperation', - BaseInsertNodeOperation -> - -export type BaseInsertTextOperation = { +export type InsertTextOperation = { type: 'insert_text' path: Path offset: number text: string + [key: string]: unknown } -export type InsertTextOperation = ExtendedType< - 'InsertTextOperation', - BaseInsertTextOperation -> - -export type BaseMergeNodeOperation = { +export type MergeNodeOperation = { type: 'merge_node' path: Path position: number - properties: Partial + properties: object + [key: string]: unknown } -export type MergeNodeOperation = ExtendedType< - 'MergeNodeOperation', - BaseMergeNodeOperation -> - -export type BaseMoveNodeOperation = { +export type MoveNodeOperation = { type: 'move_node' path: Path newPath: Path + [key: string]: unknown } -export type MoveNodeOperation = ExtendedType< - 'MoveNodeOperation', - BaseMoveNodeOperation -> - -export type BaseRemoveNodeOperation = { +export type RemoveNodeOperation = { type: 'remove_node' path: Path node: Node + [key: string]: unknown } -export type RemoveNodeOperation = ExtendedType< - 'RemoveNodeOperation', - BaseRemoveNodeOperation -> - -export type BaseRemoveTextOperation = { +export type RemoveTextOperation = { type: 'remove_text' path: Path offset: number text: string + [key: string]: unknown } -export type RemoveTextOperation = ExtendedType< - 'RemoveTextOperation', - BaseRemoveTextOperation -> - -export type BaseSetNodeOperation = { +export type SetNodeOperation = { type: 'set_node' path: Path - properties: Partial - newProperties: Partial + properties: object + newProperties: object + [key: string]: unknown } -export type SetNodeOperation = ExtendedType< - 'SetNodeOperation', - BaseSetNodeOperation -> - -export type BaseSetSelectionOperation = +export type SetSelectionOperation = | { type: 'set_selection' properties: null newProperties: Range + [key: string]: unknown } | { type: 'set_selection' properties: Partial newProperties: Partial + [key: string]: unknown } | { type: 'set_selection' properties: Range newProperties: null + [key: string]: unknown } -export type SetSelectionOperation = ExtendedType< - 'SetSelectionOperation', - BaseSetSelectionOperation -> - -export type BaseSplitNodeOperation = { +export type SplitNodeOperation = { type: 'split_node' path: Path position: number - properties: Partial + properties: object + [key: string]: unknown } -export type SplitNodeOperation = ExtendedType< - 'SplitNodeOperation', - BaseSplitNodeOperation -> - export type NodeOperation = | InsertNodeOperation | MergeNodeOperation @@ -137,16 +103,7 @@ export type TextOperation = InsertTextOperation | RemoveTextOperation export type Operation = NodeOperation | SelectionOperation | TextOperation -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 = { +export const Operation = { /** * Check of a value is a `NodeOperation` object. */ @@ -255,7 +212,8 @@ export const Operation: OperationInterface = { } case 'merge_node': { - return { ...op, type: 'split_node', path: Path.previous(op.path) } + const { path } = op + return { ...op, type: 'split_node', path: Path.previous(path) } } case 'move_node': { @@ -317,7 +275,8 @@ export const Operation: OperationInterface = { } case 'split_node': { - return { ...op, type: 'merge_node', path: Path.next(op.path) } + const { path } = op + return { ...op, type: 'merge_node', path: Path.next(path) } } } }, diff --git a/packages/slate/src/interfaces/path-ref.ts b/packages/slate/src/interfaces/path-ref.ts index 3c14fa28b6..50bd20f11c 100644 --- a/packages/slate/src/interfaces/path-ref.ts +++ b/packages/slate/src/interfaces/path-ref.ts @@ -6,17 +6,13 @@ import { Operation, Path } from '..' * at any time for the up-to-date path value. */ -export interface PathRef { +export type PathRef = { current: Path | null affinity: 'forward' | 'backward' | null unref(): Path | null } -export interface PathRefInterface { - transform: (ref: PathRef, op: Operation) => void -} - -export const PathRef: PathRefInterface = { +export const PathRef = { /** * Transform the path ref's current value by an operation. */ diff --git a/packages/slate/src/interfaces/path.ts b/packages/slate/src/interfaces/path.ts index 9e20dde245..bb0f83312f 100755 --- a/packages/slate/src/interfaces/path.ts +++ b/packages/slate/src/interfaces/path.ts @@ -1,5 +1,5 @@ import { produce } from 'immer' -import { Operation } from '..' +import { Operation, Node } from '..' /** * `Path` arrays are a list of indexes that describe a node's exact position in @@ -9,42 +9,7 @@ import { Operation } from '..' export type Path = number[] -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 - hasPrevious: (path: 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 = { +export const Path = { /** * Get a list of ancestor paths for a given path. * @@ -153,6 +118,14 @@ export const Path: PathInterface = { ) }, + /** + * Check if a path exists in a root node. + */ + + exists(path: Path, root: Node): boolean { + return Node.has(root, path) + }, + /** * Check if the path of previous sibling node exists */ diff --git a/packages/slate/src/interfaces/point-ref.ts b/packages/slate/src/interfaces/point-ref.ts index 95222b9715..f71bbb3818 100644 --- a/packages/slate/src/interfaces/point-ref.ts +++ b/packages/slate/src/interfaces/point-ref.ts @@ -6,17 +6,13 @@ import { Operation, Point } from '..' * at any time for the up-to-date point value. */ -export interface PointRef { +export type PointRef = { current: Point | null affinity: 'forward' | 'backward' | null unref(): Point | null } -export interface PointRefInterface { - transform: (ref: PointRef, op: Operation) => void -} - -export const PointRef: PointRefInterface = { +export const PointRef = { /** * Transform the point ref's current value by an operation. */ diff --git a/packages/slate/src/interfaces/point.ts b/packages/slate/src/interfaces/point.ts index 80576a198d..cebe956e18 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 { ExtendedType, Operation, Path } from '..' +import { Operation, Node, Path, Text } from '..' /** * `Point` objects refer to a specific location in a text node in a Slate @@ -9,27 +9,13 @@ import { ExtendedType, Operation, Path } from '..' * only refer to `Text` nodes. */ -export interface BasePoint { +export type Point = { path: Path offset: number + [key: string]: unknown } -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 = { +export const Point = { /** * Compare a point to another, returning an integer indicating whether the * point was before, at, or after the other. @@ -74,6 +60,22 @@ export const Point: PointInterface = { ) }, + /** + * Check if a point exists in a root node. + */ + + exists(point: Point, root: Node): boolean { + const { path, offset } = point + const desc = Node.get(root, path) + + if (desc == null || !Text.isText(desc)) { + return false + } + + const { text } = desc + return offset <= text.length + }, + /** * Check if a value implements the `Point` interface. */ diff --git a/packages/slate/src/interfaces/range-ref.ts b/packages/slate/src/interfaces/range-ref.ts index 7f7e4a0f93..e8069eacc9 100644 --- a/packages/slate/src/interfaces/range-ref.ts +++ b/packages/slate/src/interfaces/range-ref.ts @@ -6,17 +6,13 @@ import { Operation, Range } from '..' * at any time for the up-to-date range value. */ -export interface RangeRef { +export type RangeRef = { current: Range | null affinity: 'forward' | 'backward' | 'outward' | 'inward' | null unref(): Range | null } -export interface RangeRefInterface { - transform: (ref: RangeRef, op: Operation) => void -} - -export const RangeRef: RangeRefInterface = { +export const RangeRef = { /** * Transform the range ref's current value by an operation. */ diff --git a/packages/slate/src/interfaces/range.ts b/packages/slate/src/interfaces/range.ts index f1e268bb33..b7c927c7fa 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 { ExtendedType, Operation, Path, Point, PointEntry } from '..' +import { Operation, Node, Path, Point, PointEntry } from '..' /** * `Range` objects are a set of points that refer to a specific span of a Slate @@ -8,41 +8,13 @@ import { ExtendedType, Operation, Path, Point, PointEntry } from '..' * multiple nodes. */ -export interface BaseRange { +export type Range = { anchor: Point focus: Point + [key: string]: unknown } -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 - start: (range: Range) => Point - transform: ( - range: Range, - op: Operation, - options?: { - affinity?: 'forward' | 'backward' | 'outward' | 'inward' | null - } - ) => Range | null -} - -export const Range: RangeInterface = { +export const Range = { /** * Get the start and end points of a range, in the order in which they appear * in the document. @@ -81,6 +53,15 @@ export const Range: RangeInterface = { ) }, + /** + * Check if a range exists in a root node. + */ + + exists(range: Range, root: Node): boolean { + const { anchor, focus } = range + return Point.exists(anchor, root) && Point.exists(focus, root) + }, + /** * Check if a range includes a path, a point or part of another range. */ diff --git a/packages/slate/src/interfaces/text.ts b/packages/slate/src/interfaces/text.ts index d9cfc244bd..9e404e46cf 100755 --- a/packages/slate/src/interfaces/text.ts +++ b/packages/slate/src/interfaces/text.ts @@ -1,8 +1,24 @@ import isPlainObject from 'is-plain-object' import isEqual from 'lodash/isEqual' import omit from 'lodash/omit' -import { Range } from '..' -import { ExtendedType } from './custom-types' +import { Range, Editor, Element, Node, Value, NodeProps } from '..' +import { Simplify, UnionToIntersection } from '../utils/types' + +/** + * A utility type to get all the text node types from a root node type. + */ + +export type TextOf = Editor extends N + ? Text + : Element extends N + ? Text + : N extends Editor + ? TextOf + : N extends Element + ? Extract | TextOf + : N extends Text + ? N + : never /** * `Text` objects represent the nodes that contain the actual text content of a @@ -10,22 +26,12 @@ import { ExtendedType } from './custom-types' * nodes in the document tree as they cannot contain any children. */ -export interface BaseText { +export interface Text { text: string + [key: string]: unknown } -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 - matches: (text: Text, props: Partial) => boolean - decorations: (node: Text, decorations: Range[]) => Text[] -} - -export const Text: TextInterface = { +export const Text = { /** * Check if two text nodes are equal. */ @@ -36,7 +42,6 @@ export const Text: TextInterface = { options: { loose?: boolean } = {} ): boolean { const { loose = false } = options - return isEqual( loose ? omit(text, 'text') : text, loose ? omit(another, 'text') : another @@ -59,14 +64,6 @@ export const Text: TextInterface = { return Array.isArray(value) && value.every(val => Text.isText(val)) }, - /** - * 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. * @@ -74,7 +71,7 @@ export const Text: TextInterface = { * the `text` property are two nodes equal. */ - matches(text: Text, props: Partial): boolean { + matches(text: T, props: object): boolean { for (const key in props) { if (key === 'text') { continue @@ -92,8 +89,8 @@ export const Text: TextInterface = { * Get the leaves for a text node given decorations. */ - decorations(node: Text, decorations: Range[]): Text[] { - let leaves: Text[] = [{ ...node }] + decorations(node: T, decorations: Range[]): T[] { + let leaves: T[] = [{ ...node }] for (const dec of decorations) { const { anchor, focus, ...rest } = dec @@ -163,3 +160,17 @@ export const Text: TextInterface = { return leaves }, } + +/** + * A utility type to get all the mark types from a root node type. + */ + +// export type MarksOf = NodeProps> | {} + +export type MarksOf = Simplify< + UnionToIntersection>> +> + +export type MarkKeysOf = {} extends MarksOf + ? unknown + : keyof MarksOf diff --git a/packages/slate/src/slate.ts b/packages/slate/src/slate.ts new file mode 100755 index 0000000000..0f898a7751 --- /dev/null +++ b/packages/slate/src/slate.ts @@ -0,0 +1,14 @@ +export * from './create-editor' +export * from './interfaces/editor' +export * from './interfaces/element' +export * from './interfaces/location' +export * from './interfaces/node' +export * from './interfaces/operation' +export * from './interfaces/path' +export * from './interfaces/path-ref' +export * from './interfaces/point' +export * from './interfaces/point-ref' +export * from './interfaces/range' +export * from './interfaces/range-ref' +export * from './interfaces/text' +export * from './transforms' diff --git a/packages/slate/src/transforms/general.ts b/packages/slate/src/transforms/general.ts index a3d17a4ec8..c514514295 100755 --- a/packages/slate/src/transforms/general.ts +++ b/packages/slate/src/transforms/general.ts @@ -11,19 +11,18 @@ import { NodeEntry, Path, Ancestor, + Value, + AncestorOf, } from '..' +import { ElementOf } from '../interfaces/element' -export interface GeneralTransforms { - transform: (editor: Editor, op: Operation) => void -} - -export const GeneralTransforms: GeneralTransforms = { +export const GeneralTransforms = { /** * Transform the editor by an operation. */ - transform(editor: Editor, op: Operation): void { - editor.children = createDraft(editor.children) + transform(editor: Editor, op: Operation): void { + editor.children = createDraft(editor.children) as V let selection = editor.selection && createDraft(editor.selection) switch (op.type) { @@ -69,6 +68,7 @@ export const GeneralTransforms: GeneralTransforms = { if (Text.isText(node) && Text.isText(prev)) { prev.text += node.text } else if (!Text.isText(node) && !Text.isText(prev)) { + // @ts-ignore prev.children.push(...node.children) } else { throw new Error( @@ -200,6 +200,7 @@ export const GeneralTransforms: GeneralTransforms = { if (value == null) { delete node[key] } else { + // @ts-ignore node[key] = value } } @@ -266,9 +267,12 @@ export const GeneralTransforms: GeneralTransforms = { text: after, } } else { - const before = node.children.slice(0, position) - const after = node.children.slice(position) - node.children = before + const before = (node as AncestorOf>).children.slice( + 0, + position + ) + const after = (node as AncestorOf>).children.slice(position) + ;(node as ElementOf>).children = before newNode = { ...(properties as Partial), @@ -288,7 +292,7 @@ export const GeneralTransforms: GeneralTransforms = { } } - editor.children = finishDraft(editor.children) + editor.children = finishDraft(editor.children) as V if (selection) { editor.selection = isDraft(selection) diff --git a/packages/slate/src/transforms/index.ts b/packages/slate/src/transforms/index.ts index aa137bf794..ad7975bb15 100644 --- a/packages/slate/src/transforms/index.ts +++ b/packages/slate/src/transforms/index.ts @@ -3,10 +3,7 @@ import { NodeTransforms } from './node' import { SelectionTransforms } from './selection' import { TextTransforms } from './text' -export const Transforms: GeneralTransforms & - NodeTransforms & - SelectionTransforms & - TextTransforms = { +export const Transforms = { ...GeneralTransforms, ...NodeTransforms, ...SelectionTransforms, diff --git a/packages/slate/src/transforms/node.ts b/packages/slate/src/transforms/node.ts index 4fac582a8d..f6d1abb81c 100644 --- a/packages/slate/src/transforms/node.ts +++ b/packages/slate/src/transforms/node.ts @@ -10,129 +10,27 @@ import { Transforms, NodeEntry, Ancestor, + NodeMatch, + NodeOf, + ElementOf, + TextOf, + Value, } from '..' -import { NodeMatch } from '../interfaces/editor' -export interface NodeTransforms { - insertNodes: ( - editor: Editor, - nodes: Node | Node[], - options?: { - at?: Location - match?: NodeMatch - mode?: 'highest' | 'lowest' - hanging?: boolean - select?: boolean - voids?: boolean - } - ) => void - liftNodes: ( - editor: Editor, - options?: { - at?: Location - match?: NodeMatch - mode?: 'all' | 'highest' | 'lowest' - voids?: boolean - } - ) => void - mergeNodes: ( - editor: Editor, - options?: { - at?: Location - match?: NodeMatch - mode?: 'highest' | 'lowest' - hanging?: boolean - voids?: boolean - } - ) => void - moveNodes: ( - editor: Editor, - options: { - at?: Location - match?: NodeMatch - mode?: 'all' | 'highest' | 'lowest' - to: Path - voids?: boolean - } - ) => void - removeNodes: ( - editor: Editor, - options?: { - at?: Location - match?: NodeMatch - mode?: 'highest' | 'lowest' - hanging?: boolean - voids?: boolean - } - ) => void - setNodes: ( - editor: Editor, - props: Partial, - options?: { - at?: Location - match?: NodeMatch - mode?: 'all' | 'highest' | 'lowest' - hanging?: boolean - split?: boolean - voids?: boolean - } - ) => void - splitNodes: ( - editor: Editor, - options?: { - at?: Location - match?: NodeMatch - mode?: 'highest' | 'lowest' - always?: boolean - height?: number - voids?: boolean - } - ) => void - unsetNodes: ( - editor: Editor, - props: string | string[], - options?: { - at?: Location - match?: NodeMatch - mode?: 'all' | 'highest' | 'lowest' - split?: boolean - voids?: boolean - } - ) => void - unwrapNodes: ( - editor: Editor, - options?: { - at?: Location - match?: NodeMatch - mode?: 'all' | 'highest' | 'lowest' - split?: boolean - voids?: boolean - } - ) => void - wrapNodes: ( - editor: Editor, - element: Element, - options?: { - at?: Location - match?: NodeMatch - mode?: 'all' | 'highest' | 'lowest' - split?: boolean - voids?: boolean - } - ) => void -} - -export const NodeTransforms: NodeTransforms = { +export const NodeTransforms = { /** * Insert nodes at a specific location in the Editor. */ - insertNodes( - editor: Editor, - nodes: Node | Node[], + insertNodes( + editor: Editor, + nodes: + | ElementOf> + | TextOf> + | Array> | TextOf>>, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'highest' | 'lowest' hanging?: boolean select?: boolean @@ -143,8 +41,8 @@ export const NodeTransforms: NodeTransforms = { const { hanging = false, voids = false, mode = 'lowest' } = options let { at, match, select } = options - if (Node.isNode(nodes)) { - nodes = [nodes] + if (!Array.isArray(nodes)) { + nodes = [nodes] as Array> | TextOf>> } if (nodes.length === 0) { @@ -191,7 +89,7 @@ export const NodeTransforms: NodeTransforms = { if (match == null) { if (Text.isText(node)) { match = n => Text.isText(n) - } else if (editor.isInline(node)) { + } else if (editor.isInline(node as ElementOf>)) { match = n => Text.isText(n) || Editor.isInline(editor, n) } else { match = n => Editor.isBlock(editor, n) @@ -245,11 +143,11 @@ export const NodeTransforms: NodeTransforms = { * their parent in two if necessary. */ - liftNodes( - editor: Editor, + liftNodes( + editor: Editor, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'all' | 'highest' | 'lowest' voids?: boolean } = {} @@ -309,11 +207,11 @@ export const NodeTransforms: NodeTransforms = { * removing any empty containing nodes after the merge if necessary. */ - mergeNodes( - editor: Editor, + mergeNodes( + editor: Editor, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'highest' | 'lowest' hanging?: boolean voids?: boolean @@ -330,7 +228,7 @@ export const NodeTransforms: NodeTransforms = { if (match == null) { if (Path.isPath(at)) { const [parent] = Editor.parent(editor, at) - match = n => parent.children.includes(n) + match = n => parent.children.includes(n as any) } else { match = n => Editor.isBlock(editor, n) } @@ -381,7 +279,7 @@ export const NodeTransforms: NodeTransforms = { const emptyAncestor = Editor.above(editor, { at: path, mode: 'highest', - match: n => levels.includes(n) && hasSingleChildNest(editor, n), + match: n => levels.includes(n as any) && hasSingleChildNest(editor, n), }) const emptyRef = emptyAncestor && Editor.pathRef(editor, emptyAncestor[1]) @@ -395,8 +293,8 @@ export const NodeTransforms: NodeTransforms = { position = prevNode.text.length properties = rest as Partial } else if (Element.isElement(node) && Element.isElement(prevNode)) { - const { children, ...rest } = node - position = prevNode.children.length + const { children, ...rest } = node as ElementOf> + position = (prevNode as ElementOf>).children.length properties = rest as Partial } else { throw new Error( @@ -423,7 +321,8 @@ export const NodeTransforms: NodeTransforms = { // prevent losing formatting when deleting entire nodes when you have a // hanging selection. if ( - (Element.isElement(prevNode) && Editor.isEmpty(editor, prevNode)) || + (Element.isElement(prevNode) && + Editor.isEmpty(editor, prevNode as ElementOf>)) || (Text.isText(prevNode) && prevNode.text === '') ) { Transforms.removeNodes(editor, { at: prevPath, voids }) @@ -446,11 +345,11 @@ export const NodeTransforms: NodeTransforms = { * Move the nodes at a location to a new location. */ - moveNodes( - editor: Editor, + moveNodes( + editor: Editor, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'all' | 'highest' | 'lowest' to: Path voids?: boolean @@ -507,11 +406,11 @@ export const NodeTransforms: NodeTransforms = { * Remove the nodes at a specific location in the document. */ - removeNodes( - editor: Editor, + removeNodes( + editor: Editor, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'highest' | 'lowest' hanging?: boolean voids?: boolean @@ -553,12 +452,12 @@ export const NodeTransforms: NodeTransforms = { * Set new properties on the nodes at a location. */ - setNodes( - editor: Editor, - props: Partial, + setNodes( + editor: Editor, + props: Partial>>, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'all' | 'highest' | 'lowest' hanging?: boolean split?: boolean @@ -617,8 +516,8 @@ export const NodeTransforms: NodeTransforms = { mode, voids, })) { - const properties: Partial = {} - const newProperties: Partial = {} + const properties = {} + const newProperties = {} // You can't set properties on the editor node. if (path.length === 0) { @@ -626,6 +525,7 @@ export const NodeTransforms: NodeTransforms = { } for (const k in props) { + // @ts-ignore if (k === 'children' || k === 'text') { continue } @@ -652,11 +552,11 @@ export const NodeTransforms: NodeTransforms = { * Split the nodes at a specific location. */ - splitNodes( - editor: Editor, + splitNodes( + editor: Editor, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'highest' | 'lowest' always?: boolean height?: number @@ -712,7 +612,10 @@ export const NodeTransforms: NodeTransforms = { if (!after) { const text = { text: '' } const afterPath = Path.next(voidPath) - Transforms.insertNodes(editor, text, { at: afterPath, voids }) + Transforms.insertNodes(editor, text as any, { + at: afterPath, + voids, + }) after = Editor.point(editor, afterPath)! } @@ -751,7 +654,7 @@ export const NodeTransforms: NodeTransforms = { if (always || !beforeRef || !Editor.isEdge(editor, point, path)) { split = true - const properties = Node.extractProps(node) + const properties = Node.props(node as Element) editor.apply({ type: 'split_node', path, @@ -777,12 +680,12 @@ export const NodeTransforms: NodeTransforms = { * Unset properties on the nodes at a location. */ - unsetNodes( - editor: Editor, + unsetNodes( + editor: Editor, props: string | string[], options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'all' | 'highest' | 'lowest' split?: boolean voids?: boolean @@ -806,11 +709,11 @@ export const NodeTransforms: NodeTransforms = { * necessary to ensure that only the content in the range is unwrapped. */ - unwrapNodes( - editor: Editor, + unwrapNodes( + editor: Editor, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'all' | 'highest' | 'lowest' split?: boolean voids?: boolean @@ -849,7 +752,7 @@ export const NodeTransforms: NodeTransforms = { Transforms.liftNodes(editor, { at: range, - match: n => Element.isAncestor(node) && node.children.includes(n), + match: n => Node.isAncestor(node) && node.children.includes(n as any), voids, }) } @@ -865,12 +768,12 @@ export const NodeTransforms: NodeTransforms = { * of the range first to ensure that only the content in the range is wrapped. */ - wrapNodes( - editor: Editor, - element: Element, + wrapNodes( + editor: Editor, + element: ElementOf>, options: { at?: Location - match?: NodeMatch + match?: NodeMatch>> mode?: 'all' | 'highest' | 'lowest' split?: boolean voids?: boolean @@ -947,12 +850,15 @@ export const NodeTransforms: NodeTransforms = { const depth = commonPath.length + 1 const wrapperPath = Path.next(lastPath.slice(0, depth)) const wrapper = { ...element, children: [] } - Transforms.insertNodes(editor, wrapper, { at: wrapperPath, voids }) - + Transforms.insertNodes(editor, wrapper as any, { + at: wrapperPath, + voids, + }) Transforms.moveNodes(editor, { at: range, match: n => - Element.isAncestor(commonNode) && commonNode.children.includes(n), + Node.isAncestor(commonNode) && + commonNode.children.includes(n as any), to: wrapperPath.concat(0), voids, }) @@ -962,7 +868,10 @@ export const NodeTransforms: NodeTransforms = { }, } -const hasSingleChildNest = (editor: Editor, node: Node): boolean => { +const hasSingleChildNest = ( + editor: Editor, + node: Node +): boolean => { if (Element.isElement(node)) { const element = node as Element if (Editor.isVoid(editor, node)) { @@ -983,7 +892,10 @@ const hasSingleChildNest = (editor: Editor, node: Node): boolean => { * Convert a range into a point by deleting it's content. */ -const deleteRange = (editor: Editor, range: Range): Point | null => { +const deleteRange = ( + editor: Editor, + range: Range +): Point | null => { if (Range.isCollapsed(range)) { return range.anchor } else { @@ -994,7 +906,10 @@ const deleteRange = (editor: Editor, range: Range): Point | null => { } } -const matchPath = (editor: Editor, path: Path): ((node: Node) => boolean) => { +const matchPath = ( + editor: Editor, + path: Path +): ((node: Node) => boolean) => { const [node] = Editor.node(editor, path) return n => n === node } diff --git a/packages/slate/src/transforms/selection.ts b/packages/slate/src/transforms/selection.ts index d82dffc54f..94eb27a730 100755 --- a/packages/slate/src/transforms/selection.ts +++ b/packages/slate/src/transforms/selection.ts @@ -1,40 +1,12 @@ -import { Editor, Location, Point, Range, Transforms } from '..' +import { Editor, Location, Point, Range, Transforms, Value } from '..' -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, - options?: { - edge?: 'anchor' | 'focus' | 'start' | 'end' - } - ) => void - setSelection: (editor: Editor, props: Partial) => void -} - -export const SelectionTransforms: SelectionTransforms = { +export const SelectionTransforms = { /** * Collapse the selection. */ - collapse( - editor: Editor, + collapse( + editor: Editor, options: { edge?: 'anchor' | 'focus' | 'start' | 'end' } = {} @@ -61,7 +33,7 @@ export const SelectionTransforms: SelectionTransforms = { * Unset the selection. */ - deselect(editor: Editor): void { + deselect(editor: Editor): void { const { selection } = editor if (selection) { @@ -77,8 +49,8 @@ export const SelectionTransforms: SelectionTransforms = { * Move the selection's point forward or backward. */ - move( - editor: Editor, + move( + editor: Editor, options: { distance?: number unit?: 'offset' | 'character' | 'word' | 'line' @@ -133,7 +105,7 @@ export const SelectionTransforms: SelectionTransforms = { * Set the selection to a new value. */ - select(editor: Editor, target: Location): void { + select(editor: Editor, target: Location): void { const { selection } = editor target = Editor.range(editor, target) @@ -161,8 +133,8 @@ export const SelectionTransforms: SelectionTransforms = { * Set new properties on one of the selection's points. */ - setPoint( - editor: Editor, + setPoint( + editor: Editor, props: Partial, options: { edge?: 'anchor' | 'focus' | 'start' | 'end' @@ -195,7 +167,10 @@ export const SelectionTransforms: SelectionTransforms = { * Set new properties on the selection. */ - setSelection(editor: Editor, props: Partial): void { + setSelection( + editor: Editor, + props: Partial + ): void { const { selection } = editor const oldProps: Partial | null = {} const newProps: Partial = {} diff --git a/packages/slate/src/transforms/text.ts b/packages/slate/src/transforms/text.ts index 1d982b7ecb..650d26b7ad 100644 --- a/packages/slate/src/transforms/text.ts +++ b/packages/slate/src/transforms/text.ts @@ -9,46 +9,18 @@ import { Point, Range, Transforms, + Value, } from '..' +import { ElementOf } from '../interfaces/element' +import { TextOf } from '../interfaces/text' -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 = { +export const TextTransforms = { /** * Delete content in the editor. */ - delete( - editor: Editor, + delete( + editor: Editor, options: { at?: Location distance?: number @@ -226,9 +198,9 @@ export const TextTransforms: TextTransforms = { * Insert a fragment at a specific location in the editor. */ - insertFragment( - editor: Editor, - fragment: Node[], + insertFragment( + editor: Editor, + fragment: Array> | TextOf>>, options: { at?: Location hanging?: boolean @@ -302,17 +274,21 @@ export const TextTransforms: TextTransforms = { const isBlockEnd = Editor.isEnd(editor, at, blockPath) const mergeStart = !isBlockStart || (isBlockStart && isBlockEnd) const mergeEnd = !isBlockEnd - const [, firstPath] = Node.first({ children: fragment }, []) - const [, lastPath] = Node.last({ children: fragment }, []) - - const matches: NodeEntry[] = [] - const matcher = ([n, p]: NodeEntry) => { + const [, firstPath] = Node.first({ children: fragment } as any, []) + const [, lastPath] = Node.last({ children: fragment } as any, []) + + const matches: NodeEntry< + Array> | TextOf>>[number] + >[] = [] + const matcher = ([n, p]: NodeEntry< + Array> | TextOf>>[number] + >) => { if ( mergeStart && Path.isAncestor(p, firstPath) && Element.isElement(n) && - !editor.isVoid(n) && - !editor.isInline(n) + !editor.isVoid(n as any) && + !editor.isInline(n as any) ) { return false } @@ -321,8 +297,8 @@ export const TextTransforms: TextTransforms = { mergeEnd && Path.isAncestor(p, lastPath) && Element.isElement(n) && - !editor.isVoid(n) && - !editor.isInline(n) + !editor.isVoid(n as any) && + !editor.isInline(n as any) ) { return false } @@ -330,12 +306,17 @@ export const TextTransforms: TextTransforms = { return true } - for (const entry of Node.nodes( - { children: fragment }, - { pass: matcher } - )) { - if (entry[1].length > 0 && matcher(entry)) { - matches.push(entry) + const root = { children: fragment } + + for (const entry of Node.nodes(root as any, { + pass: matcher as any, + })) { + if (entry[1].length > 0 && matcher(entry as any)) { + matches.push( + entry as NodeEntry< + Array> | TextOf>>[number] + > + ) } } @@ -402,7 +383,7 @@ export const TextTransforms: TextTransforms = { voids, }) - Transforms.insertNodes(editor, middles, { + Transforms.insertNodes(editor, middles, { at: middleRef.current!, match: n => Editor.isBlock(editor, n), mode: 'lowest', @@ -441,8 +422,8 @@ export const TextTransforms: TextTransforms = { * Insert a string of text in the Editor. */ - insertText( - editor: Editor, + insertText( + editor: Editor, text: string, options: { at?: Location diff --git a/packages/slate/src/utils/types.ts b/packages/slate/src/utils/types.ts new file mode 100644 index 0000000000..bfee4a3538 --- /dev/null +++ b/packages/slate/src/utils/types.ts @@ -0,0 +1,17 @@ +/** + * Simplify a complex type expression into a single object. + */ + +export type Simplify = T extends any[] | Date + ? T + : { [K in keyof T]: T[K] } & {} + +/** + * Turn a union type into an intersection. + */ + +export type UnionToIntersection = (U extends any +? (k: U) => void +: never) extends (k: infer I) => void + ? I + : never diff --git a/packages/slate/src/utils/weak-maps.ts b/packages/slate/src/utils/weak-maps.ts index 829f578703..6ef5637382 100644 --- a/packages/slate/src/utils/weak-maps.ts +++ b/packages/slate/src/utils/weak-maps.ts @@ -1,8 +1,8 @@ import { Editor, Path, PathRef, PointRef, RangeRef } from '..' -export const DIRTY_PATHS: WeakMap = new WeakMap() -export const FLUSHING: WeakMap = new WeakMap() -export const NORMALIZING: WeakMap = new WeakMap() -export const PATH_REFS: WeakMap> = new WeakMap() -export const POINT_REFS: WeakMap> = new WeakMap() -export const RANGE_REFS: WeakMap> = new WeakMap() +export const DIRTY_PATHS: WeakMap, Path[]> = new WeakMap() +export const FLUSHING: WeakMap, boolean> = new WeakMap() +export const NORMALIZING: WeakMap, boolean> = new WeakMap() +export const PATH_REFS: WeakMap, Set> = new WeakMap() +export const POINT_REFS: WeakMap, Set> = new WeakMap() +export const RANGE_REFS: WeakMap, Set> = new WeakMap() diff --git a/packages/slate/test/transforms/setNodes/block/block-across.tsx b/packages/slate/test/transforms/setNodes/block/block-across.tsx index 61d33a12f5..4cd4e5ebbf 100644 --- a/packages/slate/test/transforms/setNodes/block/block-across.tsx +++ b/packages/slate/test/transforms/setNodes/block/block-across.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isBlock(editor, n) } ) } @@ -23,11 +23,11 @@ export const input = ( ) export const output = ( - + word - + a nother diff --git a/packages/slate/test/transforms/setNodes/block/block-hanging.tsx b/packages/slate/test/transforms/setNodes/block/block-hanging.tsx index 26155e20f8..117ed4f205 100644 --- a/packages/slate/test/transforms/setNodes/block/block-hanging.tsx +++ b/packages/slate/test/transforms/setNodes/block/block-hanging.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isBlock(editor, n) } ) } @@ -23,7 +23,7 @@ export const input = ( ) export const output = ( - + word diff --git a/packages/slate/test/transforms/setNodes/block/block-nested.tsx b/packages/slate/test/transforms/setNodes/block/block-nested.tsx index c26978c363..161f135ae6 100644 --- a/packages/slate/test/transforms/setNodes/block/block-nested.tsx +++ b/packages/slate/test/transforms/setNodes/block/block-nested.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isBlock(editor, n) } ) } @@ -22,7 +22,7 @@ export const input = ( export const output = ( - + word diff --git a/packages/slate/test/transforms/setNodes/block/block-void.tsx b/packages/slate/test/transforms/setNodes/block/block-void.tsx index a889d93f44..588ea4e488 100644 --- a/packages/slate/test/transforms/setNodes/block/block-void.tsx +++ b/packages/slate/test/transforms/setNodes/block/block-void.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isBlock(editor, n) } ) } @@ -19,7 +19,7 @@ export const input = ( ) export const output = ( - + word diff --git a/packages/slate/test/transforms/setNodes/block/block.tsx b/packages/slate/test/transforms/setNodes/block/block.tsx index 5450ae5f4c..d481a22d8a 100644 --- a/packages/slate/test/transforms/setNodes/block/block.tsx +++ b/packages/slate/test/transforms/setNodes/block/block.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isBlock(editor, n) } ) } @@ -19,7 +19,7 @@ export const input = ( ) export const output = ( - + word diff --git a/packages/slate/test/transforms/setNodes/inline/inline-across.tsx b/packages/slate/test/transforms/setNodes/inline/inline-across.tsx index 4efca3d85f..dc0a300edf 100644 --- a/packages/slate/test/transforms/setNodes/inline/inline-across.tsx +++ b/packages/slate/test/transforms/setNodes/inline/inline-across.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isInline(editor, n) } ) } @@ -33,7 +33,7 @@ export const output = ( - + word @@ -41,7 +41,7 @@ export const output = ( - + another diff --git a/packages/slate/test/transforms/setNodes/inline/inline-block-hanging.tsx b/packages/slate/test/transforms/setNodes/inline/inline-block-hanging.tsx index 0ff4ce4b89..94940c64e1 100644 --- a/packages/slate/test/transforms/setNodes/inline/inline-block-hanging.tsx +++ b/packages/slate/test/transforms/setNodes/inline/inline-block-hanging.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isInline(editor, n) } ) } @@ -33,7 +33,7 @@ export const output = ( - + word diff --git a/packages/slate/test/transforms/setNodes/inline/inline-hanging.tsx b/packages/slate/test/transforms/setNodes/inline/inline-hanging.tsx index 31f8dd1aed..6451c326b6 100644 --- a/packages/slate/test/transforms/setNodes/inline/inline-hanging.tsx +++ b/packages/slate/test/transforms/setNodes/inline/inline-hanging.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isInline(editor, n) } ) } @@ -25,7 +25,7 @@ export const output = ( - + word diff --git a/packages/slate/test/transforms/setNodes/inline/inline-nested.tsx b/packages/slate/test/transforms/setNodes/inline/inline-nested.tsx index fbdd8c99a4..a19be01205 100644 --- a/packages/slate/test/transforms/setNodes/inline/inline-nested.tsx +++ b/packages/slate/test/transforms/setNodes/inline/inline-nested.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isInline(editor, n) } ) } @@ -31,7 +31,7 @@ export const output = ( - + word diff --git a/packages/slate/test/transforms/setNodes/inline/inline-void.tsx b/packages/slate/test/transforms/setNodes/inline/inline-void.tsx index 6efecf97f4..d208d61c43 100644 --- a/packages/slate/test/transforms/setNodes/inline/inline-void.tsx +++ b/packages/slate/test/transforms/setNodes/inline/inline-void.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isInline(editor, n) } ) } @@ -25,7 +25,7 @@ export const output = ( - + word diff --git a/packages/slate/test/transforms/setNodes/inline/inline.tsx b/packages/slate/test/transforms/setNodes/inline/inline.tsx index d02657f434..a8b9ecea6f 100644 --- a/packages/slate/test/transforms/setNodes/inline/inline.tsx +++ b/packages/slate/test/transforms/setNodes/inline/inline.tsx @@ -5,7 +5,7 @@ import { jsx } from '../../..' export const run = editor => { Transforms.setNodes( editor, - { key: true }, + { a: true }, { match: n => Editor.isInline(editor, n) } ) } @@ -25,7 +25,7 @@ export const output = ( - + word diff --git a/packages/slate/test/transforms/setNodes/split/text-remove.tsx b/packages/slate/test/transforms/setNodes/split/text-remove.tsx index 9786e33c50..39515d06f4 100644 --- a/packages/slate/test/transforms/setNodes/split/text-remove.tsx +++ b/packages/slate/test/transforms/setNodes/split/text-remove.tsx @@ -3,16 +3,12 @@ import { Transforms, Text } from 'slate' import { jsx } from '../../..' export const run = editor => { - Transforms.setNodes( - editor, - { key: null }, - { match: Text.isText, split: true } - ) + Transforms.setNodes(editor, { a: null }, { match: Text.isText, split: true }) } export const input = ( - + w or d @@ -23,13 +19,13 @@ export const input = ( export const output = ( - w + w or - d + d ) diff --git a/packages/slate/test/transforms/setNodes/split/text.tsx b/packages/slate/test/transforms/setNodes/split/text.tsx index 9962110742..41aa277ea8 100644 --- a/packages/slate/test/transforms/setNodes/split/text.tsx +++ b/packages/slate/test/transforms/setNodes/split/text.tsx @@ -3,11 +3,7 @@ import { Transforms, Text } from 'slate' import { jsx } from '../../..' export const run = editor => { - Transforms.setNodes( - editor, - { key: true }, - { match: Text.isText, split: true } - ) + Transforms.setNodes(editor, { a: true }, { match: Text.isText, split: true }) } export const input = ( @@ -22,7 +18,7 @@ export const output = ( w - + or diff --git a/packages/slate/test/transforms/setNodes/text/block-across.tsx b/packages/slate/test/transforms/setNodes/text/block-across.tsx index 5e1b903a08..1344ad1bfc 100644 --- a/packages/slate/test/transforms/setNodes/text/block-across.tsx +++ b/packages/slate/test/transforms/setNodes/text/block-across.tsx @@ -3,7 +3,7 @@ import { Transforms, Text } from 'slate' import { jsx } from '../../..' export const run = editor => { - Transforms.setNodes(editor, { key: true }, { match: Text.isText }) + Transforms.setNodes(editor, { a: true }, { match: Text.isText }) } export const input = ( @@ -20,13 +20,13 @@ export const input = ( export const output = ( - + word - + a nother diff --git a/packages/slate/test/transforms/setNodes/text/text.tsx b/packages/slate/test/transforms/setNodes/text/text.tsx index eb65b81b24..88bebaf37a 100644 --- a/packages/slate/test/transforms/setNodes/text/text.tsx +++ b/packages/slate/test/transforms/setNodes/text/text.tsx @@ -3,7 +3,7 @@ import { Transforms, Text } from 'slate' import { jsx } from '../../..' export const run = editor => { - Transforms.setNodes(editor, { key: true }, { match: Text.isText }) + Transforms.setNodes(editor, { a: true }, { match: Text.isText }) } export const input = ( @@ -16,7 +16,7 @@ export const input = ( export const output = ( - + word diff --git a/packages/slate/test/transforms/setNodes/voids-true/block.tsx b/packages/slate/test/transforms/setNodes/voids-true/block.tsx index 10e2600255..0458fc1d8e 100644 --- a/packages/slate/test/transforms/setNodes/voids-true/block.tsx +++ b/packages/slate/test/transforms/setNodes/voids-true/block.tsx @@ -8,12 +8,12 @@ export const input = ( ) export const run = editor => { - Transforms.setNodes(editor, { key: true }, { at: [0, 0], voids: true }) + Transforms.setNodes(editor, { a: true }, { at: [0, 0], voids: true }) } export const output = ( - word + word ) diff --git a/packages/slate/test/transforms/unsetNodes/text/text.tsx b/packages/slate/test/transforms/unsetNodes/text/text.tsx index 306af936b6..effa786961 100644 --- a/packages/slate/test/transforms/unsetNodes/text/text.tsx +++ b/packages/slate/test/transforms/unsetNodes/text/text.tsx @@ -3,12 +3,12 @@ import { Transforms, Text } from 'slate' import { jsx } from '../../..' export const run = editor => { - Transforms.unsetNodes(editor, 'key', { match: Text.isText }) + Transforms.unsetNodes(editor, 'a', { match: Text.isText }) } export const input = ( - + word diff --git a/site/examples/check-lists.tsx b/site/examples/check-lists.tsx index 9cf074ce74..48a43a9392 100644 --- a/site/examples/check-lists.tsx +++ b/site/examples/check-lists.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useCallback } from 'react' +import React, { useState, useMemo } from 'react' import { Slate, Editable, @@ -13,61 +13,14 @@ import { Range, Point, createEditor, - Descendant, - Element as SlateElement, + Element, + Value, } 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 renderElement = useCallback(props => , []) + const [value, setValue] = useState(initialValue) const editor = useMemo( () => withChecklists(withHistory(withReact(createEditor()))), [] @@ -95,24 +48,24 @@ const withChecklists = editor => { const [match] = Editor.nodes(editor, { match: n => !Editor.isEditor(n) && - SlateElement.isElement(n) && + Element.isElement(n) && n.type === 'check-list-item', }) if (match) { const [, path] = match const start = Editor.start(editor, path) - if (Point.equals(selection.anchor, start)) { - const newProperties: Partial = { - type: 'paragraph', - } - Transforms.setNodes(editor, newProperties, { - match: n => - !Editor.isEditor(n) && - SlateElement.isElement(n) && - n.type === 'check-list-item', - }) + Transforms.setNodes( + editor, + { type: 'paragraph' }, + { + match: n => + !Editor.isEditor(n) && + Element.isElement(n) && + n.type === 'check-list-item', + } + ) return } } @@ -124,9 +77,8 @@ const withChecklists = editor => { return editor } -const Element = props => { +const renderElement = props => { const { attributes, children, element } = props - switch (element.type) { case 'check-list-item': return @@ -146,7 +98,6 @@ const CheckListItemElement = ({ attributes, children, element }) => { display: flex; flex-direction: row; align-items: center; - & + & { margin-top: 0; } @@ -163,10 +114,11 @@ const CheckListItemElement = ({ attributes, children, element }) => { checked={checked} onChange={event => { const path = ReactEditor.findPath(editor, element) - const newProperties: Partial = { - checked: event.target.checked, - } - Transforms.setNodes(editor, newProperties, { at: path }) + Transforms.setNodes( + editor, + { checked: event.target.checked }, + { at: path } + ) }} /> @@ -177,7 +129,6 @@ const CheckListItemElement = ({ attributes, children, element }) => { flex: 1; opacity: ${checked ? 0.666 : 1}; text-decoration: ${checked ? 'none' : 'line-through'}; - &:focus { outline: none; } @@ -189,4 +140,50 @@ const CheckListItemElement = ({ attributes, children, element }) => { ) } +const initialValue: Value = [ + { + 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!' }], + }, +] + export default CheckListsExample diff --git a/site/examples/code-highlighting.tsx b/site/examples/code-highlighting.tsx index b2ad12a83b..70ec85aecf 100644 --- a/site/examples/code-highlighting.tsx +++ b/site/examples/code-highlighting.tsx @@ -1,21 +1,19 @@ import Prism from 'prismjs' -import 'prismjs/components/prism-python' -import 'prismjs/components/prism-php' -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, Element as SlateElement, Descendant } from 'slate' +import { Text, createEditor, Value } from 'slate' import { withHistory } from 'slate-history' import { css } from 'emotion' +import 'prismjs/components/prism-python' +import 'prismjs/components/prism-php' +import 'prismjs/components/prism-sql' +import 'prismjs/components/prism-java' + const CodeHighlightingExample = () => { - const [value, setValue] = useState(initialValue) + const [value, setValue] = useState(initialValue) const [language, setLanguage] = useState('html') - const renderLeaf = useCallback(props => , []) const editor = useMemo(() => withHistory(withReact(createEditor())), []) - - // decorate function depends on the language selected const decorate = useCallback( ([node, path]) => { const ranges = [] @@ -87,8 +85,7 @@ const getLength = token => { } } -// different token types, styles found on Prismjs website -const Leaf = ({ attributes, children, leaf }) => { +const renderLeaf = ({ attributes, children, leaf }) => { return ( { ${leaf.comment && css` color: slategray; - `} + `} ${(leaf.operator || leaf.url) && css` @@ -142,9 +139,9 @@ const Leaf = ({ attributes, children, leaf }) => { ) } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { - type: 'paragraph', + type: 'code', children: [ { text: '

Hi!

', @@ -153,7 +150,7 @@ const initialValue: Descendant[] = [ }, ] -// modifications and additions to prism library +export default CodeHighlightingExample Prism.languages.python = Prism.languages.extend('python', {}) Prism.languages.insertBefore('python', 'prolog', { @@ -234,5 +231,3 @@ Prism.languages.markdown.bold.inside.italic = Prism.util.clone( Prism.languages.markdown.italic ) Prism.languages.markdown.italic.inside.bold = Prism.util.clone(Prism.languages.markdown.bold); // prettier-ignore - -export default CodeHighlightingExample diff --git a/site/examples/custom-types.d.ts b/site/examples/custom-types.d.ts deleted file mode 100644 index 38b775fae3..0000000000 --- a/site/examples/custom-types.d.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - Text, - createEditor, - Node, - Element, - Editor, - Descendant, - BaseEditor, -} from 'slate' -import { ReactEditor } from 'slate-react' -import { HistoryEditor } from 'slate-history' - -export type BlockQuoteElement = { type: 'block-quote'; children: Descendant[] } - -export type BulletedListElement = { - type: 'bulleted-list' - children: Descendant[] -} - -export type CheckListItemElement = { - type: 'check-list-item' - checked: boolean - children: Descendant[] -} - -export type EditableVoidElement = { - type: 'editable-void' - children: EmptyText[] -} - -export type HeadingElement = { type: 'heading'; children: Descendant[] } - -export type HeadingTwoElement = { type: 'heading-two'; children: Descendant[] } - -export type ImageElement = { - type: 'image' - url: string - children: EmptyText[] -} - -export type LinkElement = { type: 'link'; url: string; children: Descendant[] } - -export type ListItemElement = { type: 'list-item'; children: Descendant[] } - -export type MentionElement = { - type: 'mention' - character: string - children: CustomText[] -} - -export type ParagraphElement = { type: 'paragraph'; children: Descendant[] } - -export type TableElement = { type: 'table'; children: TableRow[] } - -export type TableCellElement = { type: 'table-cell'; children: CustomText[] } - -export type TableRowElement = { type: 'table-row'; children: TableCell[] } - -export type TitleElement = { type: 'title'; children: Descendant[] } - -export type VideoElement = { type: 'video'; url: string; children: EmptyText[] } - -type CustomElement = - | BlockQuoteElement - | BulletedListElement - | CheckListItemElement - | EditableVoidElement - | HeadingElement - | HeadingTwoElement - | ImageElement - | LinkElement - | ListItemElement - | MentionElement - | ParagraphElement - | TableElement - | TableRowElement - | TableCellElement - | TitleElement - | VideoElement - -export type CustomText = { - bold?: boolean - italic?: boolean - code?: boolean - text: string -} - -export type EmptyText = { - text: string -} - -export type CustomEditor = BaseEditor & ReactEditor & HistoryEditor - -declare module 'slate' { - interface CustomTypes { - Editor: CustomEditor - Element: CustomElement - Text: CustomText | EmptyText - } -} diff --git a/site/examples/editable-voids.tsx b/site/examples/editable-voids.tsx index 45031e3363..d662c71db0 100644 --- a/site/examples/editable-voids.tsx +++ b/site/examples/editable-voids.tsx @@ -1,28 +1,25 @@ import React, { useState, useMemo } from 'react' -import { Transforms, createEditor, Descendant } from 'slate' +import { Transforms, createEditor, Value } from 'slate' import { Slate, Editable, useSlateStatic, withReact } from 'slate-react' import { withHistory } from 'slate-history' import { css } from 'emotion' - import RichTextEditor from './richtext' import { Button, Icon, Toolbar } from '../components' -import { EditableVoidElement } from './custom-types' const EditableVoidsExample = () => { - const [value, setValue] = useState(initialValue) + const [value, setValue] = useState(initialValue) const editor = useMemo( () => withEditableVoids(withHistory(withReact(createEditor()))), [] ) return ( - + setValue(value)}> - } + renderElement={renderElement} placeholder="Enter some text..." /> @@ -40,17 +37,14 @@ const withEditableVoids = editor => { } const insertEditableVoid = editor => { - const text = { text: '' } - const voidNode: EditableVoidElement = { + Transforms.insertNodes(editor, { type: 'editable-void', - children: [text], - } - Transforms.insertNodes(editor, voidNode) + children: [{ text: '' }], + }) } -const Element = props => { +const renderElement = props => { const { attributes, children, element } = props - switch (element.type) { case 'editable-void': return @@ -59,13 +53,8 @@ const Element = props => { } } -const unsetWidthStyle = css` - width: unset; -` - -const EditableVoid = ({ attributes, children, element }) => { +const EditableVoid = ({ attributes, children }) => { const [inputValue, setInputValue] = useState('') - return ( // Need contentEditable=false or Firefox has issues with certain input types.
@@ -88,7 +77,9 @@ const EditableVoid = ({ attributes, children, element }) => { />

Left or right handed:

{ Left
{ ) } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { type: 'paragraph', children: [ diff --git a/site/examples/embeds.tsx b/site/examples/embeds.tsx index 561aa9c1a0..7c970975fa 100644 --- a/site/examples/embeds.tsx +++ b/site/examples/embeds.tsx @@ -1,10 +1,5 @@ import React, { useState, useMemo } from 'react' -import { - Transforms, - createEditor, - Element as SlateElement, - Descendant, -} from 'slate' +import { Transforms, createEditor, Value } from 'slate' import { Slate, Editable, @@ -14,12 +9,12 @@ import { } from 'slate-react' const EmbedsExample = () => { - const [value, setValue] = useState(initialValue) + const [value, setValue] = useState(initialValue) const editor = useMemo(() => withEmbeds(withReact(createEditor())), []) return ( setValue(value)}> } + renderElement={renderElement} placeholder="Enter some text..." /> @@ -32,7 +27,7 @@ const withEmbeds = editor => { return editor } -const Element = props => { +const renderElement = props => { const { attributes, children, element } = props switch (element.type) { case 'video': @@ -70,10 +65,7 @@ const VideoElement = ({ attributes, children, element }) => { url={url} onChange={val => { const path = ReactEditor.findPath(editor, element) - const newProperties: Partial = { - url: val, - } - Transforms.setNodes(editor, newProperties, { at: path }) + Transforms.setNodes(editor, { url: val }, { at: path }) }} />
@@ -101,7 +93,7 @@ const UrlInput = ({ url, onChange }) => { ) } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { type: 'paragraph', children: [ diff --git a/site/examples/forced-layout.tsx b/site/examples/forced-layout.tsx index 7d2c5f6a4d..78676099d5 100644 --- a/site/examples/forced-layout.tsx +++ b/site/examples/forced-layout.tsx @@ -1,59 +1,15 @@ -import React, { useState, useCallback, useMemo } from 'react' +import React, { useState, useMemo } from 'react' import { Slate, Editable, withReact } from 'slate-react' -import { - Transforms, - createEditor, - Node, - Element as SlateElement, - Descendant, -} from 'slate' +import { Transforms, Element, createEditor, Node, Value } from 'slate' import { withHistory } from 'slate-history' -import { ParagraphElement, TitleElement } from './custom-types' - -const withLayout = editor => { - const { normalizeNode } = editor - - editor.normalizeNode = ([node, path]) => { - if (path.length === 0) { - if (editor.children.length < 1) { - const title: TitleElement = { - type: 'title', - children: [{ text: 'Untitled' }], - } - Transforms.insertNodes(editor, title, { at: path.concat(0) }) - } - - if (editor.children.length < 2) { - const paragraph: ParagraphElement = { - type: 'paragraph', - children: [{ text: '' }], - } - Transforms.insertNodes(editor, paragraph, { at: path.concat(1) }) - } - - for (const [child, childPath] of Node.children(editor, path)) { - const type = childPath[0] === 0 ? 'title' : 'paragraph' - - if (SlateElement.isElement(child) && child.type !== type) { - const newProperties: Partial = { type } - Transforms.setNodes(editor, newProperties, { at: childPath }) - } - } - } - - return normalizeNode([node, path]) - } - - return editor -} const ForcedLayoutExample = () => { - const [value, setValue] = useState(initialValue) - const renderElement = useCallback(props => , []) + const [value, setValue] = useState(initialValue) const editor = useMemo( () => withLayout(withHistory(withReact(createEditor()))), [] ) + return ( setValue(value)}> { ) } -const Element = ({ attributes, children, element }) => { +const renderElement = ({ attributes, children, element }) => { switch (element.type) { - case 'title': + case 'heading-two': return

{children}

case 'paragraph': return

{children}

} } -const initialValue: Descendant[] = [ +const withLayout = editor => { + const { normalizeNode } = editor + + editor.normalizeNode = ([node, path]) => { + if (path.length === 0) { + if (editor.children.length < 1) { + Transforms.insertNodes( + editor, + { type: 'heading-two', children: [{ text: 'Untitled' }] }, + { at: path.concat(0) } + ) + } + + if (editor.children.length < 2) { + Transforms.insertNodes( + editor, + { type: 'paragraph', children: [{ text: '' }] }, + { at: path.concat(1) } + ) + } + + for (const [child, childPath] of Node.children(editor, path)) { + const type = childPath[0] === 0 ? 'heading-two' : 'paragraph' + if (Element.isElement(child) && child.type !== type) { + Transforms.setNodes(editor, { type }, { at: childPath }) + } + } + } + + return normalizeNode([node, path]) + } + + return editor +} + +const initialValue: Value = [ { - type: 'title', + type: 'heading-two', children: [{ text: 'Enforce Your Layout!' }], }, { diff --git a/site/examples/hovering-toolbar.tsx b/site/examples/hovering-toolbar.tsx index 23258923d8..b1aa3a7d21 100644 --- a/site/examples/hovering-toolbar.tsx +++ b/site/examples/hovering-toolbar.tsx @@ -1,28 +1,19 @@ import React, { useState, useMemo, useRef, useEffect } from 'react' import { Slate, Editable, ReactEditor, withReact, useSlate } from 'slate-react' -import { - Editor, - Transforms, - Text, - createEditor, - Element, - Descendant, -} from 'slate' +import { Editor, Transforms, Text, createEditor, Value } from 'slate' import { css } from 'emotion' import { withHistory } from 'slate-history' - import { Button, Icon, Menu, Portal } from '../components' import { Range } from 'slate' const HoveringMenuExample = () => { - const [value, setValue] = useState(initialValue) + const [value, setValue] = useState(initialValue) const editor = useMemo(() => withHistory(withReact(createEditor())), []) - return ( setValue(value)}> } + renderLeaf={renderLeaf} placeholder="Enter some text..." onDOMBeforeInput={(event: InputEvent) => { event.preventDefault() @@ -40,6 +31,22 @@ const HoveringMenuExample = () => { ) } +const renderLeaf = ({ attributes, children, leaf }) => { + if (leaf.bold) { + children = {children} + } + + if (leaf.italic) { + children = {children} + } + + if (leaf.underlined) { + children = {children} + } + + return {children} +} + const toggleFormat = (editor, format) => { const isActive = isFormatActive(editor, format) Transforms.setNodes( @@ -57,22 +64,6 @@ const isFormatActive = (editor, format) => { return !!match } -const Leaf = ({ attributes, children, leaf }) => { - if (leaf.bold) { - children = {children} - } - - if (leaf.italic) { - children = {children} - } - - if (leaf.underlined) { - children = {children} - } - - return {children} -} - const HoveringToolbar = () => { const ref = useRef() const editor = useSlate() @@ -80,10 +71,7 @@ const HoveringToolbar = () => { useEffect(() => { const el = ref.current const { selection } = editor - - if (!el) { - return - } + if (!el) return if ( !selection || @@ -147,7 +135,7 @@ const FormatButton = ({ format, icon }) => { ) } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { type: 'paragraph', children: [ @@ -165,7 +153,10 @@ const initialValue: Descendant[] = [ type: 'paragraph', children: [ { text: 'Try it out yourself! Just ' }, - { text: 'select any piece of text and the menu will appear', bold: true }, + { + text: 'select any piece of text and the menu will appear', + bold: true, + }, { text: '.' }, ], }, diff --git a/site/examples/huge-document.tsx b/site/examples/huge-document.tsx index 20746e9990..90da5a1b43 100644 --- a/site/examples/huge-document.tsx +++ b/site/examples/huge-document.tsx @@ -1,15 +1,15 @@ -import React, { useState, useMemo, useCallback } from 'react' +import React, { useState, useMemo } from 'react' import faker from 'faker' -import { createEditor, Descendant } from 'slate' +import { createEditor } from 'slate' import { Slate, Editable, withReact } from 'slate-react' const HEADINGS = 100 const PARAGRAPHS = 7 -const initialValue: Descendant[] = [] +const initialValue = [] for (let h = 0; h < HEADINGS; h++) { initialValue.push({ - type: 'heading', + type: 'heading-one', children: [{ text: faker.lorem.sentence() }], }) @@ -22,8 +22,7 @@ for (let h = 0; h < HEADINGS; h++) { } const HugeDocumentExample = () => { - const [value, setValue] = useState(initialValue) - const renderElement = useCallback(props => , []) + const [value, setValue] = useState(initialValue) const editor = useMemo(() => withReact(createEditor()), []) return ( setValue(value)}> @@ -32,9 +31,9 @@ const HugeDocumentExample = () => { ) } -const Element = ({ attributes, children, element }) => { +const renderElement = ({ attributes, children, element }) => { switch (element.type) { - case 'heading': + case 'heading-one': return

{children}

default: return

{children}

diff --git a/site/examples/iframe.tsx b/site/examples/iframe.tsx index e6cd922b4b..823c25d431 100644 --- a/site/examples/iframe.tsx +++ b/site/examples/iframe.tsx @@ -1,10 +1,9 @@ -import React, { useCallback, useMemo, useState } from 'react' +import React, { useMemo, useState } from 'react' import { createPortal } from 'react-dom' import isHotkey from 'is-hotkey' import { Editable, withReact, useSlate, Slate, ReactEditor } from 'slate-react' -import { Editor, createEditor, Descendant } from 'slate' +import { Editor, createEditor, Value } from 'slate' import { withHistory } from 'slate-history' - import { Button, Icon, Toolbar } from '../components' const HOTKEYS = { @@ -14,17 +13,9 @@ const HOTKEYS = { 'mod+`': 'code', } -const IFrameExample = () => { - const [value, setValue] = useState(initialValue) - const renderElement = useCallback( - ({ attributes, children }) =>

{children}

, - [] - ) - const renderLeaf = useCallback(props => , []) +const IframeExample = () => { + const [value, setValue] = useState(initialValue) const editor = useMemo(() => withHistory(withReact(createEditor())), []) - - const handleBlur = useCallback(() => ReactEditor.deselect(editor), [editor]) - return ( setValue(value)}> @@ -33,9 +24,8 @@ const IFrameExample = () => { - + ) } @@ -69,7 +59,7 @@ const isMarkActive = (editor, format) => { return marks ? marks[format] === true : false } -const Leaf = ({ attributes, children, leaf }) => { +const renderLeaf = ({ attributes, children, leaf }) => { if (leaf.bold) { children = {children} } @@ -104,7 +94,7 @@ const MarkButton = ({ format, icon }) => { ) } -const IFrame = ({ children, ...props }) => { +const Iframe = ({ children, ...props }) => { const [contentRef, setContentRef] = useState(null) const mountNode = contentRef && @@ -117,7 +107,7 @@ const IFrame = ({ children, ...props }) => { ) } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { type: 'paragraph', children: [ @@ -155,4 +145,4 @@ const initialValue: Descendant[] = [ }, ] -export default IFrameExample +export default IframeExample diff --git a/site/examples/images.tsx b/site/examples/images.tsx index 24e4bc8646..00e36f701d 100644 --- a/site/examples/images.tsx +++ b/site/examples/images.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo } from 'react' import imageExtensions from 'image-extensions' import isUrl from 'is-url' -import { Transforms, createEditor, Descendant } from 'slate' +import { Transforms, createEditor, Value } from 'slate' import { Slate, Editable, @@ -12,12 +12,10 @@ import { } from 'slate-react' import { withHistory } from 'slate-history' import { css } from 'emotion' - import { Button, Icon, Toolbar } from '../components' -import { ImageElement } from './custom-types' const ImagesExample = () => { - const [value, setValue] = useState(initialValue) + const [value, setValue] = useState(initialValue) const editor = useMemo( () => withImages(withHistory(withReact(createEditor()))), [] @@ -29,7 +27,7 @@ const ImagesExample = () => { } + renderElement={renderElement} placeholder="Enter some text..." />
@@ -72,14 +70,15 @@ const withImages = editor => { } const insertImage = (editor, url) => { - const text = { text: '' } - const image: ImageElement = { type: 'image', url, children: [text] } - Transforms.insertNodes(editor, image) + Transforms.insertNodes(editor, { + type: 'image', + url, + children: [{ text: '' }], + }) } -const Element = props => { +const renderElement = props => { const { attributes, children, element } = props - switch (element.type) { case 'image': return @@ -135,7 +134,7 @@ const isImageUrl = url => { return imageExtensions.includes(ext) } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { type: 'paragraph', children: [ diff --git a/site/examples/links.tsx b/site/examples/links.tsx index ab66fd2d75..0a556c7b79 100644 --- a/site/examples/links.tsx +++ b/site/examples/links.tsx @@ -1,21 +1,12 @@ import React, { useState, useMemo } from 'react' import isUrl from 'is-url' import { Slate, Editable, withReact, useSlate } from 'slate-react' -import { - Transforms, - Editor, - Range, - createEditor, - Element as SlateElement, - Descendant, -} from 'slate' +import { Transforms, Editor, Range, createEditor, Element, Value } from 'slate' import { withHistory } from 'slate-history' -import { LinkElement } from './custom-types' - import { Button, Icon, Toolbar } from '../components' -const LinkExample = () => { - const [value, setValue] = useState(initialValue) +export default function() { + const [value, setValue] = useState(initialValue) const editor = useMemo( () => withLinks(withHistory(withReact(createEditor()))), [] @@ -25,10 +16,9 @@ const LinkExample = () => { setValue(value)}> - } + renderElement={renderElement} placeholder="Enter some text..." /> @@ -52,7 +42,6 @@ const withLinks = editor => { editor.insertData = data => { const text = data.getData('text/plain') - if (text && isUrl(text)) { wrapLink(editor, text) } else { @@ -72,7 +61,7 @@ const insertLink = (editor, url) => { const isLinkActive = editor => { const [link] = Editor.nodes(editor, { match: n => - !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'link', + !Editor.isEditor(n) && Element.isElement(n) && n.type === 'link', }) return !!link } @@ -80,7 +69,7 @@ const isLinkActive = editor => { const unwrapLink = editor => { Transforms.unwrapNodes(editor, { match: n => - !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'link', + !Editor.isEditor(n) && Element.isElement(n) && n.type === 'link', }) } @@ -91,7 +80,7 @@ const wrapLink = (editor, url) => { const { selection } = editor const isCollapsed = selection && Range.isCollapsed(selection) - const link: LinkElement = { + const link = { type: 'link', url, children: isCollapsed ? [{ text: url }] : [], @@ -105,14 +94,10 @@ const wrapLink = (editor, url) => { } } -const Element = ({ attributes, children, element }) => { +const renderElement = ({ attributes, children, element }) => { switch (element.type) { case 'link': - return ( - - {children} - - ) + return default: return

{children}

} @@ -135,24 +120,7 @@ const LinkButton = () => { ) } -const RemoveLinkButton = () => { - const editor = useSlate() - - return ( - - ) -} - -const initialValue: Descendant[] = [ +const initialValue: Value = [ { type: 'paragraph', children: [ @@ -164,9 +132,7 @@ const initialValue: Descendant[] = [ url: 'https://en.wikipedia.org/wiki/Hypertext', children: [{ text: 'hyperlinks' }], }, - { - text: '!', - }, + { text: '!' }, ], }, { @@ -179,5 +145,3 @@ const initialValue: Descendant[] = [ ], }, ] - -export default LinkExample diff --git a/site/examples/markdown-preview.tsx b/site/examples/markdown-preview.tsx index 0d6703f8fa..eb2791678d 100644 --- a/site/examples/markdown-preview.tsx +++ b/site/examples/markdown-preview.tsx @@ -1,55 +1,13 @@ import Prism from 'prismjs' -import React, { useState, useCallback, useMemo } from 'react' +import React, { useState, useMemo } from 'react' import { Slate, Editable, withReact } from 'slate-react' -import { Text, createEditor, Element, Descendant } from 'slate' +import { Text, createEditor, Value } from 'slate' import { withHistory } from 'slate-history' import { css } from 'emotion' -// eslint-disable-next-line -;Prism.languages.markdown=Prism.languages.extend("markup",{}),Prism.languages.insertBefore("markdown","prolog",{blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},code:[{pattern:/^(?: {4}|\t).+/m,alias:"keyword"},{pattern:/``.+?``|`[^`\n]+`/,alias:"keyword"}],title:[{pattern:/\w+.*(?:\r?\n|\r)(?:==+|--+)/,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#+.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:/(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^\*\*|^__|\*\*$|__$/}},italic:{pattern:/(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^[*_]|[*_]$/}},url:{pattern:/!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/,inside:{variable:{pattern:/(!?\[)[^\]]+(?=\]$)/,lookbehind:!0},string:{pattern:/"(?:\\.|[^"\\])*"(?=\)$)/}}}}),Prism.languages.markdown.bold.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.italic.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.bold.inside.italic=Prism.util.clone(Prism.languages.markdown.italic),Prism.languages.markdown.italic.inside.bold=Prism.util.clone(Prism.languages.markdown.bold); // prettier-ignore - const MarkdownPreviewExample = () => { - const [value, setValue] = useState(initialValue) - const renderLeaf = useCallback(props => , []) + const [value, setValue] = useState(initialValue) const editor = useMemo(() => withHistory(withReact(createEditor())), []) - const decorate = useCallback(([node, path]) => { - const ranges = [] - - if (!Text.isText(node)) { - return ranges - } - - const getLength = token => { - if (typeof token === 'string') { - return token.length - } else if (typeof token.content === 'string') { - return token.content.length - } else { - return token.content.reduce((l, t) => l + getLength(t), 0) - } - } - - const tokens = Prism.tokenize(node.text, Prism.languages.markdown) - let start = 0 - - for (const token of tokens) { - const length = getLength(token) - const end = start + length - - if (typeof token !== 'string') { - ranges.push({ - [token.type]: true, - anchor: { path, offset: start }, - focus: { path, offset: end }, - }) - } - - start = end - } - - return ranges - }, []) - return ( setValue(value)}> { ) } -const Leaf = ({ attributes, children, leaf }) => { +const decorate = ([node, path]) => { + const ranges = [] + + if (!Text.isText(node)) { + return ranges + } + + const getLength = token => { + if (typeof token === 'string') { + return token.length + } else if (typeof token.content === 'string') { + return token.content.length + } else { + return token.content.reduce((l, t) => l + getLength(t), 0) + } + } + + const tokens = Prism.tokenize(node.text, Prism.languages.markdown) + let start = 0 + + for (const token of tokens) { + const length = getLength(token) + const end = start + length + + if (typeof token !== 'string') { + ranges.push({ + [token.type]: true, + anchor: { path, offset: start }, + focus: { path, offset: end }, + }) + } + + start = end + } + + return ranges +} + +const renderLeaf = ({ attributes, children, leaf }) => { return ( { ) } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { type: 'paragraph', children: [ @@ -130,3 +126,6 @@ const initialValue: Descendant[] = [ ] export default MarkdownPreviewExample + +// eslint-disable-next-line +;Prism.languages.markdown=Prism.languages.extend("markup",{}),Prism.languages.insertBefore("markdown","prolog",{blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},code:[{pattern:/^(?: {4}|\t).+/m,alias:"keyword"},{pattern:/``.+?``|`[^`\n]+`/,alias:"keyword"}],title:[{pattern:/\w+.*(?:\r?\n|\r)(?:==+|--+)/,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#+.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:/(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^\*\*|^__|\*\*$|__$/}},italic:{pattern:/(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^[*_]|[*_]$/}},url:{pattern:/!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/,inside:{variable:{pattern:/(!?\[)[^\]]+(?=\]$)/,lookbehind:!0},string:{pattern:/"(?:\\.|[^"\\])*"(?=\)$)/}}}}),Prism.languages.markdown.bold.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.italic.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.bold.inside.italic=Prism.util.clone(Prism.languages.markdown.italic),Prism.languages.markdown.italic.inside.bold=Prism.util.clone(Prism.languages.markdown.bold); // prettier-ignore diff --git a/site/examples/markdown-shortcuts.tsx b/site/examples/markdown-shortcuts.tsx index ffecace2bb..36533b4428 100644 --- a/site/examples/markdown-shortcuts.tsx +++ b/site/examples/markdown-shortcuts.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo } from 'react' +import React, { useState, useMemo } from 'react' import { Slate, Editable, withReact } from 'slate-react' import { Editor, @@ -6,17 +6,16 @@ import { Range, Point, createEditor, - Element as SlateElement, - Descendant, + Element, + Value, } from 'slate' import { withHistory } from 'slate-history' -import { BulletedListElement } from './custom-types' const SHORTCUTS = { '*': 'list-item', '-': 'list-item', '+': 'list-item', - '>': 'block-quote', + '>': 'quote', '#': 'heading-one', '##': 'heading-two', '###': 'heading-three', @@ -26,8 +25,7 @@ const SHORTCUTS = { } const MarkdownShortcutsExample = () => { - const [value, setValue] = useState(initialValue) - const renderElement = useCallback(props => , []) + const [value, setValue] = useState(initialValue) const editor = useMemo( () => withShortcuts(withReact(withHistory(createEditor()))), [] @@ -64,24 +62,23 @@ const withShortcuts = editor => { if (type) { Transforms.select(editor, range) Transforms.delete(editor) - const newProperties: Partial = { - type, - } - Transforms.setNodes(editor, newProperties, { - match: n => Editor.isBlock(editor, n), - }) + Transforms.setNodes( + editor, + { type }, + { match: n => Editor.isBlock(editor, n) } + ) if (type === 'list-item') { - const list: BulletedListElement = { - type: 'bulleted-list', - children: [], - } - Transforms.wrapNodes(editor, list, { - match: n => - !Editor.isEditor(n) && - SlateElement.isElement(n) && - n.type === 'list-item', - }) + Transforms.wrapNodes( + editor, + { type: 'bulleted-list', children: [] }, + { + match: n => + !Editor.isEditor(n) && + Element.isElement(n) && + n.type === 'list-item', + } + ) } return @@ -105,20 +102,17 @@ const withShortcuts = editor => { if ( !Editor.isEditor(block) && - SlateElement.isElement(block) && + Element.isElement(block) && block.type !== 'paragraph' && Point.equals(selection.anchor, start) ) { - const newProperties: Partial = { - type: 'paragraph', - } - Transforms.setNodes(editor, newProperties) + Transforms.setNodes(editor, { type: 'paragraph' }) if (block.type === 'list-item') { Transforms.unwrapNodes(editor, { match: n => !Editor.isEditor(n) && - SlateElement.isElement(n) && + Element.isElement(n) && n.type === 'bulleted-list', split: true, }) @@ -135,9 +129,9 @@ const withShortcuts = editor => { return editor } -const Element = ({ attributes, children, element }) => { +const renderElement = ({ attributes, children, element }) => { switch (element.type) { - case 'block-quote': + case 'quote': return
{children}
case 'bulleted-list': return
    {children}
@@ -160,7 +154,7 @@ const Element = ({ attributes, children, element }) => { } } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { type: 'paragraph', children: [ @@ -171,7 +165,7 @@ const initialValue: Descendant[] = [ ], }, { - type: 'block-quote', + type: 'quote', children: [{ text: 'A wise quote.' }], }, { diff --git a/site/examples/mentions.tsx b/site/examples/mentions.tsx index dac03f7b27..b58443d2e7 100644 --- a/site/examples/mentions.tsx +++ b/site/examples/mentions.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useCallback, useRef, useEffect, useState } from 'react' -import { Editor, Transforms, Range, createEditor, Descendant } from 'slate' +import { Editor, Transforms, Range, createEditor, Value } from 'slate' import { withHistory } from 'slate-history' import { Slate, @@ -11,15 +11,13 @@ import { } from 'slate-react' import { Portal } from '../components' -import { MentionElement } from './custom-types' const MentionExample = () => { const ref = useRef() - const [value, setValue] = useState(initialValue) + const [value, setValue] = useState(initialValue) const [target, setTarget] = useState() const [index, setIndex] = useState(0) const [search, setSearch] = useState('') - const renderElement = useCallback(props => , []) const editor = useMemo( () => withMentions(withReact(withHistory(createEditor()))), [] @@ -155,16 +153,16 @@ const withMentions = editor => { } const insertMention = (editor, character) => { - const mention: MentionElement = { + Transforms.insertNodes(editor, { type: 'mention', character, children: [{ text: '' }], - } - Transforms.insertNodes(editor, mention) + }) + Transforms.move(editor) } -const Element = props => { +const renderElement = props => { const { attributes, children, element } = props switch (element.type) { case 'mention': @@ -198,7 +196,7 @@ const Mention = ({ attributes, children, element }) => { ) } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { type: 'paragraph', children: [ diff --git a/site/examples/paste-html.tsx b/site/examples/paste-html.tsx index 9f60071397..e128e77b0c 100644 --- a/site/examples/paste-html.tsx +++ b/site/examples/paste-html.tsx @@ -1,6 +1,6 @@ -import React, { useState, useCallback, useMemo } from 'react' +import React, { useState, useMemo } from 'react' import { jsx } from 'slate-hyperscript' -import { Transforms, createEditor, Descendant } from 'slate' +import { Transforms, createEditor, Value } from 'slate' import { withHistory } from 'slate-history' import { css } from 'emotion' import { @@ -80,9 +80,7 @@ export const deserialize = el => { } const PasteHtmlExample = () => { - const [value, setValue] = useState(initialValue) - const renderElement = useCallback(props => , []) - const renderLeaf = useCallback(props => , []) + const [value, setValue] = useState(initialValue) const editor = useMemo( () => withHtml(withReact(withHistory(createEditor()))), [] @@ -125,7 +123,7 @@ const withHtml = editor => { return editor } -const Element = props => { +const renderElement = props => { const { attributes, children, element } = props switch (element.type) { @@ -187,7 +185,7 @@ const ImageElement = ({ attributes, children, element }) => { ) } -const Leaf = ({ attributes, children, leaf }) => { +const renderLeaf = ({ attributes, children, leaf }) => { if (leaf.bold) { children = {children} } @@ -211,7 +209,7 @@ const Leaf = ({ attributes, children, leaf }) => { return {children} } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { type: 'paragraph', children: [ diff --git a/site/examples/plaintext.tsx b/site/examples/plaintext.tsx index 9ba20dda7f..b9776502f1 100644 --- a/site/examples/plaintext.tsx +++ b/site/examples/plaintext.tsx @@ -1,10 +1,10 @@ import React, { useState, useMemo } from 'react' -import { createEditor, Descendant } from 'slate' +import { createEditor, Value } from 'slate' import { Slate, Editable, withReact } from 'slate-react' import { withHistory } from 'slate-history' const PlainTextExample = () => { - const [value, setValue] = useState(initialValue) + const [value, setValue] = useState(initialValue) const editor = useMemo(() => withHistory(withReact(createEditor())), []) return ( setValue(value)}> @@ -13,9 +13,8 @@ const PlainTextExample = () => { ) } -const initialValue: Descendant[] = [ +const initialValue: Value = [ { - type: 'paragraph', children: [ { text: 'This is editable plain text, just like a