diff --git a/package-lock.json b/package-lock.json index 8d2f76dfc..fb071cc12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32391,14 +32391,14 @@ }, "packages/slate-commons": { "name": "@prezly/slate-commons", - "version": "0.29.1", + "version": "0.31.1", "license": "MIT", "dependencies": { - "@prezly/slate-types": "^0.29.1", + "@prezly/slate-types": "^0.31.1", "uuid": "^8.3.0" }, "devDependencies": { - "@prezly/slate-hyperscript": "^0.29.1", + "@prezly/slate-hyperscript": "^0.31.1", "@types/uuid": "^8.3.0" }, "peerDependencies": { @@ -32410,7 +32410,7 @@ }, "packages/slate-editor": { "name": "@prezly/slate-editor", - "version": "0.29.1", + "version": "0.31.1", "license": "MIT", "dependencies": { "@popperjs/core": "^2.6.0", @@ -32419,10 +32419,10 @@ "@prezly/linear-partition": "^1.0.2", "@prezly/progress-promise": "^1.0.1", "@prezly/sdk": "^6.12.1", - "@prezly/slate-commons": "^0.29.1", - "@prezly/slate-hyperscript": "^0.29.1", - "@prezly/slate-lists": "^0.29.1", - "@prezly/slate-types": "^0.29.1", + "@prezly/slate-commons": "^0.31.1", + "@prezly/slate-hyperscript": "^0.31.1", + "@prezly/slate-lists": "^0.31.1", + "@prezly/slate-types": "^0.31.1", "@prezly/uploadcare": "^1.1.2", "@prezly/uploadcare-widget": "^3.16.1", "@udecode/plate-core": "^9.0.0", @@ -32491,7 +32491,7 @@ }, "packages/slate-hyperscript": { "name": "@prezly/slate-hyperscript", - "version": "0.29.1", + "version": "0.31.1", "license": "MIT", "dependencies": { "is-plain-object": "^5.0.0" @@ -32511,16 +32511,16 @@ }, "packages/slate-lists": { "name": "@prezly/slate-lists", - "version": "0.29.1", + "version": "0.31.1", "license": "MIT", "dependencies": { - "@prezly/slate-commons": "^0.29.1", - "@prezly/slate-types": "^0.29.1", + "@prezly/slate-commons": "^0.31.0", + "is-hotkey": "^0.2.0", "uuid": "^8.3.0" }, "devDependencies": { - "@prezly/slate-hyperscript": "^0.29.1", - "@types/uuid": "^8.3.0" + "@types/uuid": "^8.3.0", + "slate-hyperscript": "^0.67.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0", @@ -32528,9 +32528,35 @@ "slate-react": "^0.71.0" } }, + "packages/slate-lists/node_modules/@prezly/slate-types": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/@prezly/slate-types/-/slate-types-0.30.0.tgz", + "integrity": "sha512-CyRABw3j5N9C9OEqIy2sFtgp4RgAgGuzSO8/RqzSwh388ohFo8HD1Iw6IlRbqjK5UhlcOZo9JV6XKeKHXZQx6w==", + "dependencies": { + "@prezly/sdk": "^6.12.1", + "@prezly/uploads": "^0.2.1", + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": "~0.71.0" + } + }, + "packages/slate-lists/node_modules/is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==" + }, + "packages/slate-lists/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "packages/slate-types": { "name": "@prezly/slate-types", - "version": "0.29.1", + "version": "0.31.1", "license": "MIT", "dependencies": { "@prezly/sdk": "^6.12.1", @@ -36306,8 +36332,8 @@ "@prezly/slate-commons": { "version": "file:packages/slate-commons", "requires": { - "@prezly/slate-hyperscript": "^0.29.1", - "@prezly/slate-types": "^0.29.1", + "@prezly/slate-hyperscript": "^0.31.1", + "@prezly/slate-types": "^0.31.1", "@types/uuid": "^8.3.0", "uuid": "^8.3.0" } @@ -36322,10 +36348,10 @@ "@prezly/linear-partition": "^1.0.2", "@prezly/progress-promise": "^1.0.1", "@prezly/sdk": "^6.12.1", - "@prezly/slate-commons": "^0.29.1", - "@prezly/slate-hyperscript": "^0.29.1", - "@prezly/slate-lists": "^0.29.1", - "@prezly/slate-types": "^0.29.1", + "@prezly/slate-commons": "^0.31.1", + "@prezly/slate-hyperscript": "^0.31.1", + "@prezly/slate-lists": "^0.31.1", + "@prezly/slate-types": "^0.31.1", "@prezly/uploadcare": "^1.1.2", "@prezly/uploadcare-widget": "^3.16.1", "@storybook/addon-actions": "^6.4.17", @@ -36401,11 +36427,32 @@ "@prezly/slate-lists": { "version": "file:packages/slate-lists", "requires": { - "@prezly/slate-commons": "^0.29.1", - "@prezly/slate-hyperscript": "^0.29.1", - "@prezly/slate-types": "^0.29.1", + "@prezly/slate-commons": "^0.31.0", "@types/uuid": "^8.3.0", + "is-hotkey": "^0.2.0", + "slate-hyperscript": "^0.67.0", "uuid": "^8.3.0" + }, + "dependencies": { + "@prezly/slate-types": { + "version": "https://registry.npmjs.org/@prezly/slate-types/-/slate-types-0.30.0.tgz", + "integrity": "sha512-CyRABw3j5N9C9OEqIy2sFtgp4RgAgGuzSO8/RqzSwh388ohFo8HD1Iw6IlRbqjK5UhlcOZo9JV6XKeKHXZQx6w==", + "requires": { + "@prezly/sdk": "^6.12.1", + "@prezly/uploads": "^0.2.1", + "is-plain-object": "^5.0.0" + } + }, + "is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==" + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + } } }, "@prezly/slate-types": { diff --git a/packages/slate-commons/src/EditableWithExtensions.tsx b/packages/slate-commons/src/EditableWithExtensions.tsx index 63c50a295..5bf25593d 100644 --- a/packages/slate-commons/src/EditableWithExtensions.tsx +++ b/packages/slate-commons/src/EditableWithExtensions.tsx @@ -1,7 +1,9 @@ /* eslint-disable react-hooks/exhaustive-deps */ import type { FunctionComponent } from 'react'; import React, { useCallback, useMemo } from 'react'; -import { Editable, useSlateStatic } from 'slate-react'; +import type { Editor } from 'slate'; +import type { ReactEditor } from 'slate-react'; +import { Editable } from 'slate-react'; import { combineDecorate, @@ -22,6 +24,7 @@ import type { export interface Props { decorate?: Decorate; + editor: Editor & ReactEditor; /** * Each extension fields will be combined by role. * @@ -67,6 +70,7 @@ export interface Props { export const EditableWithExtensions: FunctionComponent = ({ decorate, + editor, extensions = [], onDOMBeforeInput: onDOMBeforeInputList = [], onDOMBeforeInputDeps = [], @@ -78,8 +82,6 @@ export const EditableWithExtensions: FunctionComponent = ({ renderLeafDeps = [], ...props }) => { - const editor = useSlateStatic(); - const combinedDecorate: Decorate = useMemo( function () { const decorateFns = createExtensionsDecorators(editor, extensions); diff --git a/packages/slate-commons/src/commands/focus.ts b/packages/slate-commons/src/commands/focus.ts index f998a6bd0..a360bfcbc 100644 --- a/packages/slate-commons/src/commands/focus.ts +++ b/packages/slate-commons/src/commands/focus.ts @@ -3,7 +3,7 @@ import { ReactEditor } from 'slate-react'; import { moveCursorToEndOfDocument } from './moveCursorToEndOfDocument'; -export function focus(editor: Editor): void { +export function focus(editor: Editor & ReactEditor): void { ReactEditor.focus(editor); moveCursorToEndOfDocument(editor); } diff --git a/packages/slate-commons/src/commands/getCurrentDomNode.ts b/packages/slate-commons/src/commands/getCurrentDomNode.ts index 706b3511f..7553f5513 100644 --- a/packages/slate-commons/src/commands/getCurrentDomNode.ts +++ b/packages/slate-commons/src/commands/getCurrentDomNode.ts @@ -1,9 +1,10 @@ import type { Editor } from 'slate'; +import type { ReactEditor } from 'slate-react'; import { getCurrentNodeEntry } from './getCurrentNodeEntry'; import { toDomNode } from './toDomNode'; -export function getCurrentDomNode(editor: Editor): HTMLElement | null { +export function getCurrentDomNode(editor: Editor & ReactEditor): HTMLElement | null { const [currentNode] = getCurrentNodeEntry(editor) || []; if (!currentNode) { diff --git a/packages/slate-commons/src/commands/insertNodes.test.tsx b/packages/slate-commons/src/commands/insertNodes.test.tsx index 3eec604ca..00d247959 100644 --- a/packages/slate-commons/src/commands/insertNodes.test.tsx +++ b/packages/slate-commons/src/commands/insertNodes.test.tsx @@ -1,11 +1,9 @@ /** @jsx jsx */ -import type { LinkNode } from '@prezly/slate-types'; -import { PARAGRAPH_NODE_TYPE } from '@prezly/slate-types'; -import type { Editor } from 'slate'; +import type { Editor, Node } from 'slate'; import { jsx } from '../jsx'; -import { createEditor, INLINE_ELEMENT, INLINE_VOID_ELEMENT, VOID_ELEMENT } from '../test-utils'; +import { createEditor } from '../test-utils'; import { insertNodes } from './insertNodes'; @@ -88,12 +86,14 @@ describe('insertNodes', () => { ) as unknown as Editor; - insertNodes(editor, [ - { - children: [{ text: 'dolor' }], - type: PARAGRAPH_NODE_TYPE, - }, - ]); + insertNodes( + editor, + nodes([ + + dolor + , + ]), + ); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -129,12 +129,11 @@ describe('insertNodes', () => { insertNodes( editor, - [ - { - children: [{ text: 'dolor' }], - type: PARAGRAPH_NODE_TYPE, - }, - ], + nodes([ + + dolor + , + ]), { ensureEmptyParagraphAfter: true }, ); @@ -172,16 +171,14 @@ describe('insertNodes', () => { insertNodes( editor, - [ - { - children: [{ text: 'dolor' }], - type: PARAGRAPH_NODE_TYPE, - }, - { - children: [{ text: '' }], - type: PARAGRAPH_NODE_TYPE, - }, - ], + nodes([ + + dolor + , + + + , + ]), { ensureEmptyParagraphAfter: true }, ); @@ -219,19 +216,19 @@ describe('insertNodes', () => { ) as unknown as Editor; - insertNodes(editor, [ - { text: 'xxx' }, - { - type: INLINE_ELEMENT, - children: [{ text: 'yyy' }], - href: 'https://example.com', - } as LinkNode, - { - type: PARAGRAPH_NODE_TYPE, - children: [{ text: 'dolor' }], - }, - { text: 'zzz' }, - ]); + insertNodes( + editor, + nodes([ + xxx, + + yyy + , + + dolor + , + zzz, + ]), + ); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -259,12 +256,14 @@ describe('insertNodes', () => { ) as unknown as Editor; - insertNodes(editor, [ - { - type: PARAGRAPH_NODE_TYPE, - children: [{ text: 'dolor' }], - }, - ]); + insertNodes( + editor, + nodes([ + + dolor + , + ]), + ); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -299,20 +298,20 @@ describe('insertNodes', () => { ) as unknown as Editor; - insertNodes(editor, [ - { text: 'xxx' }, - { - type: INLINE_ELEMENT, - children: [{ text: 'yyy' }], - href: 'https://example.com', - } as LinkNode, - { text: 'zzz' }, - { - children: [{ text: 'dolor' }], - type: PARAGRAPH_NODE_TYPE, - }, - { text: 'aaa' }, - ]); + insertNodes( + editor, + nodes([ + xxx, + + yyy + , + zzz, + + dolor + , + aaa, + ]), + ); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -347,15 +346,16 @@ describe('insertNodes', () => { ) as unknown as Editor; - insertNodes(editor, [ - { text: 'xxx' }, - { - type: INLINE_ELEMENT, - children: [{ text: 'yyy' }], - href: 'https://example.com', - } as LinkNode, - { text: 'zzz' }, - ]); + insertNodes( + editor, + nodes([ + xxx, + + yyy + , + zzz, + ]), + ); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -388,22 +388,12 @@ describe('insertNodes', () => { ) as unknown as Editor; insertNodes(editor, [ - { - children: [{ text: '' }], - type: VOID_ELEMENT, - }, - { - text: 'lorem', - }, - { - bold: true, - text: ' ', - }, - { - bold: true, - text: 'ipsum', - }, - ]); + + + , + lorem, + {' ipsum'}, + ] as unknown as Node[]); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -438,30 +428,25 @@ describe('insertNodes', () => { ) as unknown as Editor; - insertNodes(editor, [ - { - children: [{ text: '' }], - type: VOID_ELEMENT, - }, - { - text: 'lorem', - }, - { - type: INLINE_VOID_ELEMENT, - href: 'https://example.com', - children: [{ text: 'ipsum' }], - } as LinkNode, - { - bold: true, - text: ' ', - }, - { - bold: true, - text: 'dolor', - }, - ]); + insertNodes( + editor, + nodes([ + + + , + lorem, + + ipsum + , + {' dolor'}, + ]), + ); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); }); + +function nodes(nodes: JSX.IntrinsicElements[keyof JSX.IntrinsicElements][]): Node[] { + return nodes as unknown as Node[]; +} diff --git a/packages/slate-commons/src/commands/isMarkActive.test.tsx b/packages/slate-commons/src/commands/isMarkActive.test.tsx index 99d89aa0a..3f1682439 100644 --- a/packages/slate-commons/src/commands/isMarkActive.test.tsx +++ b/packages/slate-commons/src/commands/isMarkActive.test.tsx @@ -1,20 +1,19 @@ /** @jsx jsx */ -import type { Editor } from 'slate'; +import type { BaseText, Editor } from 'slate'; import { jsx } from '../jsx'; import { isMarkActive } from './isMarkActive'; -const EXAMPLE_MARK_1 = 'bold'; -const EXAMPLE_MARK_2 = 'underlined'; +type StyledText = BaseText & { bold?: boolean; underlined?: boolean }; describe('isMarkActive', () => { it('Returns "true" when mark is active', () => { const editor = ( - + lorem ipsum @@ -22,15 +21,15 @@ describe('isMarkActive', () => { ) as unknown as Editor; - expect(isMarkActive(editor, EXAMPLE_MARK_1)).toBe(true); + expect(isMarkActive(editor, 'bold')).toBe(true); }); it('Returns "false" when mark is inactive', () => { const editor = ( - lorem ipsum - + lorem ipsum + lorem ipsum @@ -38,7 +37,7 @@ describe('isMarkActive', () => { ) as unknown as Editor; - expect(isMarkActive(editor, EXAMPLE_MARK_1)).toBe(false); + expect(isMarkActive(editor, 'bold')).toBe(false); }); it('Returns "false" when there is no selection', () => { @@ -50,6 +49,6 @@ describe('isMarkActive', () => { ) as unknown as Editor; - expect(isMarkActive(editor, EXAMPLE_MARK_1)).toBe(false); + expect(isMarkActive(editor, 'bold')).toBe(false); }); }); diff --git a/packages/slate-commons/src/commands/isMarkActive.ts b/packages/slate-commons/src/commands/isMarkActive.ts index 8e6b360e4..319dd9133 100644 --- a/packages/slate-commons/src/commands/isMarkActive.ts +++ b/packages/slate-commons/src/commands/isMarkActive.ts @@ -1,7 +1,7 @@ import type { Text } from 'slate'; import { Editor } from 'slate'; -export function isMarkActive(editor: Editor, mark: keyof Omit): boolean { - const marks = Editor.marks(editor); +export function isMarkActive(editor: Editor, mark: keyof Omit): boolean { + const marks = Editor.marks(editor) as Record; return marks ? marks[mark] === true : false; } diff --git a/packages/slate-commons/src/commands/toggleMark.test.tsx b/packages/slate-commons/src/commands/toggleMark.test.tsx index 0b92ccbd8..141e1d4c1 100644 --- a/packages/slate-commons/src/commands/toggleMark.test.tsx +++ b/packages/slate-commons/src/commands/toggleMark.test.tsx @@ -1,12 +1,14 @@ /** @jsx jsx */ -import type { Editor } from 'slate'; +import type { BaseText, Editor } from 'slate'; import { jsx } from '../jsx'; import { toggleMark } from './toggleMark'; -const EXAMPLE_MARK_1 = 'bold'; +interface StyledText extends BaseText { + bold?: boolean; +} describe('toggleMark', () => { it('Adds the mark when it is inactive', () => { @@ -25,7 +27,7 @@ describe('toggleMark', () => { const expected = ( - + lorem ipsum @@ -34,7 +36,7 @@ describe('toggleMark', () => { ) as unknown as Editor; - toggleMark(editor, EXAMPLE_MARK_1); + toggleMark(editor, 'bold'); expect(editor.children).toEqual(expected.children); }); @@ -43,7 +45,7 @@ describe('toggleMark', () => { const editor = ( - + lorem ipsum @@ -64,7 +66,7 @@ describe('toggleMark', () => { ) as unknown as Editor; - toggleMark(editor, EXAMPLE_MARK_1); + toggleMark(editor, 'bold'); expect(editor.children).toEqual(expected.children); }); diff --git a/packages/slate-commons/src/commands/toggleMark.ts b/packages/slate-commons/src/commands/toggleMark.ts index 200e45299..f48974d6f 100644 --- a/packages/slate-commons/src/commands/toggleMark.ts +++ b/packages/slate-commons/src/commands/toggleMark.ts @@ -3,12 +3,16 @@ import { Editor } from 'slate'; import { isMarkActive } from './isMarkActive'; -export function toggleMark(editor: Editor, mark: keyof Omit, force?: boolean): void { +export function toggleMark( + editor: Editor, + mark: keyof Omit, + force?: boolean, +): void { const shouldSet = force ?? !isMarkActive(editor, mark); if (shouldSet) { - Editor.addMark(editor, mark, true); + Editor.addMark(editor, mark as string, true); } else { - Editor.removeMark(editor, mark); + Editor.removeMark(editor, mark as string); } } diff --git a/packages/slate-commons/src/index.ts b/packages/slate-commons/src/index.ts index a2d0b1a7c..46e8abefc 100644 --- a/packages/slate-commons/src/index.ts +++ b/packages/slate-commons/src/index.ts @@ -1,16 +1,3 @@ -import type { ElementNode, TextNode } from '@prezly/slate-types'; -import type { BaseEditor } from 'slate'; -import type { HistoryEditor } from 'slate-history'; -import type { ReactEditor } from 'slate-react'; - -declare module 'slate' { - interface CustomTypes { - Editor: BaseEditor & ReactEditor & HistoryEditor; - Element: ElementNode; - Text: TextNode; - } -} - export { EditableWithExtensions } from './EditableWithExtensions'; export * as EditorCommands from './commands'; export { diff --git a/packages/slate-commons/src/jsx.ts b/packages/slate-commons/src/jsx.ts index 7ab1dfb82..9c14715f5 100644 --- a/packages/slate-commons/src/jsx.ts +++ b/packages/slate-commons/src/jsx.ts @@ -51,7 +51,7 @@ declare global { // using 'h-text' instead of 'text' to avoid collision with React typings, see: // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0182cd9094aa081558a3c4bfc970bbdfb71d891d/types/react/index.d.ts#L3136 'h-text': { - [key: string]: any; // allow marks + [key: string]: any; children?: ReactNode; }; } diff --git a/packages/slate-commons/src/test-utils.ts b/packages/slate-commons/src/test-utils.ts index b7bc416a8..caedaf5cb 100644 --- a/packages/slate-commons/src/test-utils.ts +++ b/packages/slate-commons/src/test-utils.ts @@ -7,6 +7,7 @@ import { LINK_NODE_TYPE, } from '@prezly/slate-types'; import type { Editor } from 'slate'; +import { Element } from 'slate'; export const INLINE_ELEMENT = LINK_NODE_TYPE; export const INLINE_VOID_ELEMENT = LINK_NODE_TYPE; @@ -18,14 +19,14 @@ function withGenericTestElements(editor: T): T { const { isInline, isVoid } = editor; editor.isInline = (element) => - [INLINE_ELEMENT, INLINE_VOID_ELEMENT].includes(element.type as string) - ? true - : isInline(element); + Element.isElementType(element, INLINE_ELEMENT) || + Element.isElementType(element, INLINE_VOID_ELEMENT) || + isInline(element); editor.isVoid = (element) => - [VOID_ELEMENT, INLINE_VOID_ELEMENT].includes(element.type as string) - ? true - : isVoid(element); + Element.isElementType(element, VOID_ELEMENT) || + Element.isElementType(element, INLINE_VOID_ELEMENT) || + isVoid(element); return editor; } diff --git a/packages/slate-commons/src/types/WithOverrides.ts b/packages/slate-commons/src/types/WithOverrides.ts index 305fedea3..4fa3d7284 100644 --- a/packages/slate-commons/src/types/WithOverrides.ts +++ b/packages/slate-commons/src/types/WithOverrides.ts @@ -1,5 +1,3 @@ -import type { BaseEditor } from 'slate'; -import type { HistoryEditor } from 'slate-history'; -import type { ReactEditor } from 'slate-react'; +import type { Editor } from 'slate'; -export type WithOverrides = (editor: T) => T; +export type WithOverrides = (editor: T) => T; diff --git a/packages/slate-editor/src/index.ts b/packages/slate-editor/src/index.ts index 568c265e4..ba860646b 100644 --- a/packages/slate-editor/src/index.ts +++ b/packages/slate-editor/src/index.ts @@ -1,3 +1,4 @@ +import type { ListsEditor } from '@prezly/slate-lists'; import type { ElementNode, TextNode } from '@prezly/slate-types'; import type { BaseEditor } from 'slate'; import type { HistoryEditor } from 'slate-history'; @@ -5,7 +6,7 @@ import type { ReactEditor } from 'slate-react'; declare module 'slate' { interface CustomTypes { - Editor: BaseEditor & ReactEditor & HistoryEditor; + Editor: BaseEditor & ReactEditor & HistoryEditor & ListsEditor; Element: ElementNode; Text: TextNode; } diff --git a/packages/slate-editor/src/modules/editor-v4-autoformat/withAutoformat.ts b/packages/slate-editor/src/modules/editor-v4-autoformat/withAutoformat.ts index c2827943a..4d73a1c0d 100644 --- a/packages/slate-editor/src/modules/editor-v4-autoformat/withAutoformat.ts +++ b/packages/slate-editor/src/modules/editor-v4-autoformat/withAutoformat.ts @@ -1,15 +1,10 @@ import { EditorCommands } from '@prezly/slate-commons'; -import type { BaseEditor } from 'slate'; -import type { HistoryEditor } from 'slate-history'; -import type { ReactEditor } from 'slate-react'; +import type { Editor } from 'slate'; import { autoformatBlock, autoformatMark, autoformatText } from './transforms'; import type { AutoformatRule } from './types'; -export function withAutoformat( - editor: T, - rules: AutoformatRule[], -): T { +export function withAutoformat(editor: T, rules: AutoformatRule[]): T { const { insertText } = editor; const autoformatters = { diff --git a/packages/slate-editor/src/modules/editor-v4-paragraphs/lib/createParagraph.ts b/packages/slate-editor/src/modules/editor-v4-paragraphs/lib/createParagraph.ts index aeb1f48c1..b035ebd89 100644 --- a/packages/slate-editor/src/modules/editor-v4-paragraphs/lib/createParagraph.ts +++ b/packages/slate-editor/src/modules/editor-v4-paragraphs/lib/createParagraph.ts @@ -1,11 +1,11 @@ import type { ParagraphNode } from '@prezly/slate-types'; import { PARAGRAPH_NODE_TYPE } from '@prezly/slate-types'; -export function createParagraph( - children: ParagraphNode['children'] = [{ text: '' }], -): ParagraphNode { +type Props = Partial>; + +export function createParagraph({ children }: Props = {}): ParagraphNode { return { type: PARAGRAPH_NODE_TYPE, - children, + children: children ?? [{ text: '' }], }; } diff --git a/packages/slate-editor/src/modules/editor-v4-paragraphs/lib/parseSerializedElement.ts b/packages/slate-editor/src/modules/editor-v4-paragraphs/lib/parseSerializedElement.ts index f34948ffe..782edf43f 100644 --- a/packages/slate-editor/src/modules/editor-v4-paragraphs/lib/parseSerializedElement.ts +++ b/packages/slate-editor/src/modules/editor-v4-paragraphs/lib/parseSerializedElement.ts @@ -7,7 +7,7 @@ export function parseSerializedElement(serialized: string): ParagraphNode | unde const parsed = JSON.parse(serialized); if (isParagraphNode(parsed)) { - return createParagraph(parsed.children); + return createParagraph(parsed); } return undefined; diff --git a/packages/slate-editor/src/modules/editor-v4-rich-formatting/RichFormattingExtension.tsx b/packages/slate-editor/src/modules/editor-v4-rich-formatting/RichFormattingExtension.tsx index f0aff1dae..fc9316185 100644 --- a/packages/slate-editor/src/modules/editor-v4-rich-formatting/RichFormattingExtension.tsx +++ b/packages/slate-editor/src/modules/editor-v4-rich-formatting/RichFormattingExtension.tsx @@ -6,9 +6,14 @@ import type { RenderElementProps } from 'slate-react'; import { RichTextElement, Text } from './components'; import { RICH_FORMATTING_EXTENSION_ID } from './constants'; import { createDeserialize } from './createDeserialize'; -import { createOnKeyDown } from './createOnKeyDown'; -import { isRichTextElement, normalizeRedundantRichTextAttributes } from './lib'; +import { createOnKeyDownHandler } from './createOnKeyDownHandler'; +import { + isRichTextElement, + normalizeRedundantRichTextAttributes, + withResetRichFormattingOnBreak, +} from './lib'; import { ElementType } from './types'; +import { withListsFormatting } from './withListsFormatting'; interface Parameters { blocks: boolean; @@ -19,7 +24,7 @@ export const RichFormattingExtension = ({ blocks }: Parameters): Extension => ({ deserialize: createDeserialize({ blocks }), inlineTypes: [], normalizers: [normalizeRedundantRichTextAttributes], - onKeyDown: createOnKeyDown({ blocks }), + onKeyDown: createOnKeyDownHandler({ blocks }), renderElement: ({ attributes, children, element }: RenderElementProps) => { if (blocks && isRichTextElement(element)) { return ( @@ -38,4 +43,7 @@ export const RichFormattingExtension = ({ blocks }: Parameters): Extension => ({ ElementType.HEADING_ONE, ElementType.HEADING_TWO, ], + withOverrides(editor) { + return withResetRichFormattingOnBreak(blocks ? withListsFormatting(editor) : editor); + }, }); diff --git a/packages/slate-editor/src/modules/editor-v4-rich-formatting/createOnKeyDown.ts b/packages/slate-editor/src/modules/editor-v4-rich-formatting/createOnKeyDown.ts deleted file mode 100644 index 2fffe881f..000000000 --- a/packages/slate-editor/src/modules/editor-v4-rich-formatting/createOnKeyDown.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { EditorCommands } from '@prezly/slate-commons'; -import { isHotkey } from 'is-hotkey'; -import type { KeyboardEvent } from 'react'; -import { Editor } from 'slate'; -import { ReactEditor } from 'slate-react'; - -import { lists } from './lists'; -import { MarkType } from './types'; - -const MARK_HOTKEYS: { hotkey: string; mark: MarkType }[] = [ - { hotkey: 'mod+b', mark: MarkType.BOLD }, - { hotkey: 'mod+i', mark: MarkType.ITALIC }, - { hotkey: 'mod+u', mark: MarkType.UNDERLINED }, -]; - -function marksOnKeyDown(event: KeyboardEvent, editor: Editor) { - return MARK_HOTKEYS.forEach(({ hotkey, mark }) => { - if (isHotkey(hotkey, event.nativeEvent)) { - event.preventDefault(); - EditorCommands.toggleMark(editor, mark); - } - }); -} - -function listsOnKeyDown(event: KeyboardEvent, editor: Editor) { - const listItemsInSelection = lists.getListItemsInRange(editor, editor.selection); - - // Since we're overriding the default Tab key behavior - // we need to bring back the possibility to blur the editor - // with keyboard. - if (isHotkey('esc', event.nativeEvent)) { - event.preventDefault(); - ReactEditor.blur(editor); - } - - if (isHotkey('tab', event.nativeEvent)) { - event.preventDefault(); - lists.increaseDepth(editor); - } - - if (isHotkey('shift+tab', event.nativeEvent)) { - event.preventDefault(); - lists.decreaseDepth(editor); - } - - if (isHotkey('backspace', event.nativeEvent) && !lists.canDeleteBackward(editor)) { - event.preventDefault(); - lists.decreaseDepth(editor); - } - - if (isHotkey('enter', event.nativeEvent)) { - if (lists.isCursorInEmptyListItem(editor)) { - event.preventDefault(); - lists.decreaseDepth(editor); - } else if (listItemsInSelection.length > 0) { - event.preventDefault(); - lists.splitListItem(editor); - } - } -} - -function softBreakOnKeyDown(event: KeyboardEvent, editor: Editor) { - if (isHotkey('shift+enter', event.nativeEvent) && !event.isDefaultPrevented()) { - event.preventDefault(); - Editor.insertText(editor, '\n'); - } -} - -export function createOnKeyDown(parameters: { blocks: boolean }) { - return (event: KeyboardEvent, editor: Editor) => { - softBreakOnKeyDown(event, editor); - marksOnKeyDown(event, editor); - - if (parameters.blocks) { - listsOnKeyDown(event, editor); - - // Slate does not always trigger normalization when one would expect it to. - // So we want to force it after we perform lists operations, as it fixes - // many unexpected behaviors. - // https://github.com/ianstormtaylor/slate/issues/3758 - Editor.normalize(editor, { force: true }); - } - }; -} diff --git a/packages/slate-editor/src/modules/editor-v4-rich-formatting/createOnKeyDownHandler.ts b/packages/slate-editor/src/modules/editor-v4-rich-formatting/createOnKeyDownHandler.ts new file mode 100644 index 000000000..cd81c6cdc --- /dev/null +++ b/packages/slate-editor/src/modules/editor-v4-rich-formatting/createOnKeyDownHandler.ts @@ -0,0 +1,40 @@ +import { EditorCommands } from '@prezly/slate-commons'; +import { onKeyDown as onListsKeyDown } from '@prezly/slate-lists'; +import { isHotkey } from 'is-hotkey'; +import type { KeyboardEvent } from 'react'; +import { Editor } from 'slate'; + +import { MarkType } from './types'; + +const MARK_HOTKEYS: { hotkey: string; mark: MarkType }[] = [ + { hotkey: 'mod+b', mark: MarkType.BOLD }, + { hotkey: 'mod+i', mark: MarkType.ITALIC }, + { hotkey: 'mod+u', mark: MarkType.UNDERLINED }, +]; + +function marksOnKeyDown(event: KeyboardEvent, editor: Editor) { + return MARK_HOTKEYS.forEach(({ hotkey, mark }) => { + if (isHotkey(hotkey, event.nativeEvent)) { + event.preventDefault(); + EditorCommands.toggleMark(editor, mark); + } + }); +} + +function softBreakOnKeyDown(event: KeyboardEvent, editor: Editor) { + if (isHotkey('shift+enter', event.nativeEvent) && !event.isDefaultPrevented()) { + event.preventDefault(); + Editor.insertText(editor, '\n'); + } +} + +export function createOnKeyDownHandler(parameters: { blocks: boolean }) { + return (event: KeyboardEvent, editor: Editor) => { + softBreakOnKeyDown(event, editor); + marksOnKeyDown(event, editor); + + if (parameters.blocks) { + onListsKeyDown(editor, event); + } + }; +} diff --git a/packages/slate-editor/src/modules/editor-v4-rich-formatting/index.ts b/packages/slate-editor/src/modules/editor-v4-rich-formatting/index.ts index 39be90bc6..21bc63d20 100644 --- a/packages/slate-editor/src/modules/editor-v4-rich-formatting/index.ts +++ b/packages/slate-editor/src/modules/editor-v4-rich-formatting/index.ts @@ -4,4 +4,3 @@ export { isRichTextBlockElement, isRichTextElement, toggleBlock } from './lib'; export { RichFormattingExtension } from './RichFormattingExtension'; export type { RichFormattingExtensionParameters, RichTextElementType } from './types'; export { ElementType, MarkType } from './types'; -export { withRichFormatting } from './withRichFormatting'; diff --git a/packages/slate-editor/src/modules/editor-v4-rich-formatting/lib/toggleBlock.ts b/packages/slate-editor/src/modules/editor-v4-rich-formatting/lib/toggleBlock.ts index 12b8eeb1c..c7ccd9a7e 100644 --- a/packages/slate-editor/src/modules/editor-v4-rich-formatting/lib/toggleBlock.ts +++ b/packages/slate-editor/src/modules/editor-v4-rich-formatting/lib/toggleBlock.ts @@ -1,9 +1,9 @@ import { EditorCommands } from '@prezly/slate-commons'; +import { Lists, ListType } from '@prezly/slate-lists'; import type { ElementNode } from '@prezly/slate-types'; import type { Editor } from 'slate'; import { Transforms } from 'slate'; -import { lists } from '../lists'; import { ElementType } from '../types'; export function toggleBlock(editor: Editor, type: T['type']): void { @@ -13,13 +13,19 @@ export function toggleBlock(editor: Editor, type: T['type return; } - if (type === ElementType.BULLETED_LIST || type === ElementType.NUMBERED_LIST) { - lists.wrapInList(editor, type); - lists.setListType(editor, type); + if (type === ElementType.BULLETED_LIST) { + Lists.wrapInList(editor, ListType.UNORDERED); + Lists.setListType(editor, ListType.UNORDERED); return; } - lists.unwrapList(editor); + if (type === ElementType.NUMBERED_LIST) { + Lists.wrapInList(editor, ListType.ORDERED); + Lists.setListType(editor, ListType.ORDERED); + return; + } + + Lists.unwrapList(editor); if (path && EditorCommands.isCursorInEmptyParagraph(editor, { trim: true })) { EditorCommands.removeChildren(editor, [currentNode, path]); diff --git a/packages/slate-editor/src/modules/editor-v4-rich-formatting/lib/withResetRichFormattingOnBreak.ts b/packages/slate-editor/src/modules/editor-v4-rich-formatting/lib/withResetRichFormattingOnBreak.ts index ccdb2c3f9..4a3c8b62c 100644 --- a/packages/slate-editor/src/modules/editor-v4-rich-formatting/lib/withResetRichFormattingOnBreak.ts +++ b/packages/slate-editor/src/modules/editor-v4-rich-formatting/lib/withResetRichFormattingOnBreak.ts @@ -1,11 +1,9 @@ /* eslint-disable no-param-reassign */ import { EditorCommands } from '@prezly/slate-commons'; -import { isList } from '@prezly/slate-lists'; +import { isListNode } from '@prezly/slate-types'; import type { Editor } from 'slate'; -import { options } from '../lists'; - import { isRichTextBlockElement } from './isRichTextBlockElement'; export function withResetRichFormattingOnBreak(editor: T): T { @@ -21,7 +19,7 @@ export function withResetRichFormattingOnBreak(editor: T): T { if ( isRichTextBlockElement(currentNode) && - !isList(options, currentNode) && + !isListNode(currentNode) && EditorCommands.isSelectionAtBlockEnd(editor) ) { EditorCommands.insertEmptyParagraph(editor); diff --git a/packages/slate-editor/src/modules/editor-v4-rich-formatting/lists.ts b/packages/slate-editor/src/modules/editor-v4-rich-formatting/lists.ts deleted file mode 100644 index 8ed1a8275..000000000 --- a/packages/slate-editor/src/modules/editor-v4-rich-formatting/lists.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ListsOptions } from '@prezly/slate-lists'; -import { Lists } from '@prezly/slate-lists'; -import { PARAGRAPH_NODE_TYPE } from '@prezly/slate-types'; - -import { ElementType } from './types'; - -export const options: ListsOptions = { - defaultBlockType: PARAGRAPH_NODE_TYPE, - listItemTextType: ElementType.LIST_ITEM_TEXT, - listItemType: ElementType.LIST_ITEM, - listTypes: [ElementType.BULLETED_LIST, ElementType.NUMBERED_LIST], - wrappableTypes: [ - PARAGRAPH_NODE_TYPE, - ElementType.BLOCK_QUOTE, - ElementType.HEADING_ONE, - ElementType.HEADING_TWO, - ], -}; - -export const lists = Lists(options); diff --git a/packages/slate-editor/src/modules/editor-v4-rich-formatting/withListsFormatting.ts b/packages/slate-editor/src/modules/editor-v4-rich-formatting/withListsFormatting.ts new file mode 100644 index 000000000..63e2a2815 --- /dev/null +++ b/packages/slate-editor/src/modules/editor-v4-rich-formatting/withListsFormatting.ts @@ -0,0 +1,60 @@ +import type { ListsEditor, ListsSchema } from '@prezly/slate-lists'; +import { ListType, withLists, withListsReact } from '@prezly/slate-lists'; +import { + BULLETED_LIST_NODE_TYPE, + isHeadingNode, + isListItemNode, + isListItemTextNode, + isListNode, + isParagraphNode, + isQuoteNode, + LIST_ITEM_NODE_TYPE, + LIST_ITEM_TEXT_NODE_TYPE, + NUMBERED_LIST_NODE_TYPE, +} from '@prezly/slate-types'; +import type { Editor } from 'slate'; + +import { createParagraph } from '#modules/editor-v4-paragraphs'; + +const SCHEMA: ListsSchema = { + isConvertibleToListTextNode(node) { + return isParagraphNode(node) || isHeadingNode(node) || isQuoteNode(node); + }, + isDefaultTextNode: isParagraphNode, + isListNode(node, type?) { + if (type === ListType.ORDERED) { + return isListNode(node, NUMBERED_LIST_NODE_TYPE); + } + if (type === ListType.UNORDERED) { + return isListNode(node, BULLETED_LIST_NODE_TYPE); + } + return isListNode(node); + }, + isListItemNode, + isListItemTextNode, + createDefaultTextNode(props = {}) { + return createParagraph(props); + }, + createListNode(type: ListType = ListType.UNORDERED, { children } = {}) { + return { + type: type === ListType.ORDERED ? NUMBERED_LIST_NODE_TYPE : BULLETED_LIST_NODE_TYPE, + children: children ?? [this.createListItemNode()], + }; + }, + createListItemNode({ children } = {}) { + return { + type: LIST_ITEM_NODE_TYPE, + children: children ?? [this.createListItemTextNode()], + }; + }, + createListItemTextNode({ children } = {}) { + return { + type: LIST_ITEM_TEXT_NODE_TYPE, + children: children ?? [{ text: '' }], + }; + }, +}; + +export function withListsFormatting(editor: T): T & ListsEditor { + return withListsReact(withLists(SCHEMA)(editor)); +} diff --git a/packages/slate-editor/src/modules/editor-v4-rich-formatting/withRichFormatting.ts b/packages/slate-editor/src/modules/editor-v4-rich-formatting/withRichFormatting.ts deleted file mode 100644 index ff58ff04a..000000000 --- a/packages/slate-editor/src/modules/editor-v4-rich-formatting/withRichFormatting.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable no-param-reassign */ - -import { withLists, withListsReact } from '@prezly/slate-lists'; -import type { Editor } from 'slate'; - -import { withResetRichFormattingOnBreak } from './lib'; -import { options } from './lists'; - -export function withRichFormatting(editor: T): T { - return withResetRichFormattingOnBreak(withListsReact(withLists(options)(editor))); -} diff --git a/packages/slate-editor/src/modules/editor-v4/EditorV4.tsx b/packages/slate-editor/src/modules/editor-v4/EditorV4.tsx index f599f17ae..b2b204ee4 100644 --- a/packages/slate-editor/src/modules/editor-v4/EditorV4.tsx +++ b/packages/slate-editor/src/modules/editor-v4/EditorV4.tsx @@ -353,6 +353,7 @@ const EditorV4: FunctionComponent = (props) => { > As TypeScript interfaces... @@ -60,17 +65,17 @@ import { Node } from 'slate'; interface ListNode { children: ListItemNode[]; - type: 'bulleted-list' | 'numbered-list'; // see ListsOptions to customize this + type: 'bulleted-list' | 'numbered-list'; // depends on your ListsSchema } interface ListItemNode { children: [ListItemTextNode] | [ListItemTextNode, ListNode]; - type: 'list-item'; // see ListsOptions to customize this + type: 'list-item'; // depends on your ListsSchema } interface ListItemTextNode { children: Node[]; // by default everything is allowed here - type: 'list-item-text'; // see ListsOptions to customize this + type: 'list-item-text'; // depends on your ListsSchema } ``` @@ -93,7 +98,8 @@ yarn add @prezly/slate-lists ## User guide -Let's start with a minimal Slate + React example which we will be adding lists support to. Nothing interesting here just yet. +Let's start with a minimal Slate + React example which we will be adding lists support to. +Nothing interesting here just yet. Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-0-initial-state-9gmff?file=/src/MyEditor.tsx @@ -116,29 +122,67 @@ export const MyEditor = () => { }; ``` -### Define [`ListsOptions`](src/types.ts) +### Define [`ListsSchema`](src/types.ts) -First you're going to want to define options that will be passed to the extension. Just create an object matching the [`ListsOptions`](src/types.ts) interface. +First you're going to want to define schema that will be passed to the extension. +Just create an object matching the [`ListsSchema`](src/types.ts) interface. Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-1-define-options-m564b?file=/src/MyEditor.tsx ```diff import { useMemo, useState } from 'react'; - import { createEditor, Node } from 'slate'; +-import { createEditor, Node } from 'slate'; ++import { createEditor, Element, Node } from 'slate'; import { Editable, Slate, withReact } from 'slate-react'; -+import { ListsOptions } from '@prezly/slate-lists'; ++import { type ListsSchema, ListType } from '@prezly/slate-lists'; + -+const options: ListsOptions = { -+ defaultBlockType: 'paragraph', -+ listItemTextType: 'list-item-text', -+ listItemType: 'list-item', -+ listTypes: ['ordered-list', 'unordered-list'], -+ wrappableTypes: ['paragraph'] ++const schema: ListsSchema = { ++ isConvertibleToListTextNode(node) { ++ return Element.isElementType(node, PARAGRAPH_TYPE); ++ }, ++ isDefaultTextNode(node) { ++ return Element.isElementType(node, 'paragraph'); ++ }, ++ isListNode(node, type?) { ++ if (type === ListType.UNORDERED) return Element.isElementType(node, 'bulleted-list'); ++ if (type === ListType.ORDERED) return Element.isElementType(node, 'numbered-list'); ++ return Element.isElementType(node, 'ordered-list') || Element.isElementType(node, 'numbered-list'); ++ }, ++ isListItemNode(node) { ++ return Element.isElementNode(node, 'list-item'); ++ }, ++ isListItemTextNode(node) { ++ return Element.isElementNode(node, 'list-item-text'); ++ }, ++ createDefaultTextNode({ children } = {}) { ++ return { ++ type: 'paragraph', ++ children: children ?? [{ text: '' }], ++ }; ++ }, ++ createListNode({ children } = {}, type = ListType.UNORDERED) { ++ return { ++ type: type === ListType.UNORDERED ? 'bulleted-list' : 'numbered-list', ++ children: children ?? [this.creteListItemNode()], ++ }; ++ }, ++ createListItemNode({ children } = {}) { ++ return { ++ type: 'list-item', ++ children: children ?? [this.createListItemTextNode()], ++ }; ++ }, ++ createListItemTextNode({ children } = {}) { ++ return { ++ type: 'list-item-text', ++ children: children ?? [{ text: '' }], ++ }; ++ }, +}; const initialValue: Node[] = [{ type: 'paragraph', children: [{ text: 'Hello world!' }] }]; - const MyEditor = () => { + export const MyEditor = () => { const [value, setValue] = useState(initialValue); const editor = useMemo(() => withReact(createEditor()), []); @@ -148,29 +192,66 @@ Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-1-define-opt ); }; - - export default MyEditor; ``` ### Use [`withLists`](src/lib/withLists.ts) plugin -[`withLists`](src/lib/withLists.ts) is a [Slate plugin](https://docs.slatejs.org/concepts/07-plugins) that enables [normalizations](https://docs.slatejs.org/concepts/10-normalizing) which enforce [schema](#Schema) constraints and recover from unsupported structures. +[`withLists`](src/lib/withLists.ts) is a [Slate plugin](https://docs.slatejs.org/concepts/07-plugins) +that enables [normalizations](https://docs.slatejs.org/concepts/10-normalizing) +which enforce [schema](#Schema) constraints and recover from unsupported structures. Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-2-use-withlists-plugin-5splt?file=/src/MyEditor.tsx ```diff import { useMemo, useState } from 'react'; import { createEditor, Node } from 'slate'; + import { createEditor, Element, Node } from 'slate'; import { Editable, Slate, withReact } from 'slate-react'; --import { ListsOptions } from '@prezly/slate-lists'; -+import { ListsOptions, withLists } from '@prezly/slate-lists'; - - const options: ListsOptions = { - defaultBlockType: 'paragraph', - listItemTextType: 'list-item-text', - listItemType: 'list-item', - listTypes: ['ordered-list', 'unordered-list'], - wrappableTypes: ['paragraph'], +-import { type ListsSchema, ListType } from '@prezly/slate-lists'; ++import { type ListsSchema, ListType, withLists } from '@prezly/slate-lists'; + + const schema: ListsSchema = { + isConvertibleToListTextNode(node) { + return Element.isElementType(node, PARAGRAPH_TYPE); + }, + isDefaultTextNode(node) { + return Element.isElementType(node, 'paragraph'); + }, + isListNode(node, type?) { + if (type === ListType.UNORDERED) return Element.isElementType(node, 'bulleted-list'); + if (type === ListType.ORDERED) return Element.isElementType(node, 'numbered-list'); + return Element.isElementType(node, 'ordered-list') || Element.isElementType(node, 'numbered-list'); + }, + isListItemNode(node) { + return Element.isElementNode(node, 'list-item'); + }, + isListItemTextNode(node) { + return Element.isElementNode(node, 'list-item-text'); + }, + createDefaultTextNode({ children } = {}) { + return { + type: 'paragraph', + children: children ?? [{ text: '' }], + }; + }, + createListNode({ children } = {}, type = ListType.UNORDERED) { + return { + type: type === ListType.UNORDERED ? 'bulleted-list' : 'numbered-list', + children: children ?? [this.creteListItemNode()], + }; + }, + createListItemNode({ children } = {}) { + return { + type: 'list-item', + children: children ?? [this.createListItemTextNode()], + }; + }, + createListItemTextNode({ children } = {}) { + return { + type: 'list-item-text', + children: children ?? [{ text: '' }], + }; + }, }; const initialValue: Node[] = [{ type: 'paragraph', children: [{ text: 'Hello world!' }] }]; @@ -178,8 +259,10 @@ Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-2-use-withli export const MyEditor = () => { const [value, setValue] = useState(initialValue); - const editor = useMemo(() => withReact(createEditor()), []); -+ const baseEditor = useMemo(() => withReact(createEditor()), []); -+ const editor = useMemo(() => withLists(options)(baseEditor), [baseEditor]); ++ const editor = useMemo(function() { ++ const baseEditor = withReact(createEditor()); ++ return withLists(schema)(baseEditor); ++ }, []); return ( @@ -187,77 +270,78 @@ Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-2-use-withli ); }; - ``` ### Use [`withListsReact`](src/lib/withListsReact.ts) plugin -[`withListsReact`](src/lib/withListsReact.ts) is useful on the client-side - it's a [Slate plugin](https://docs.slatejs.org/concepts/07-plugins) that overrides `editor.setFragmentData`. It enables `Range.prototype.cloneContents` monkey patch to improve copying behavior in some edge cases. +[`withListsReact`](src/lib/withListsReact.ts) is useful on the client-side - +it's a [Slate plugin](https://docs.slatejs.org/concepts/07-plugins) - that overrides `editor.setFragmentData`. +It enables `Range.prototype.cloneContents` monkey patch to improve copying behavior in some edge cases. Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-3-use-withlistsreact-plugin-rgubg?file=/src/MyEditor.tsx ```diff import { useMemo, useState } from 'react'; import { createEditor, Node } from 'slate'; + import { createEditor, Element, Node } from 'slate'; import { Editable, Slate, withReact } from 'slate-react'; --import { ListsOptions, withLists } from '@prezly/slate-lists'; -+import { ListsOptions, withLists, withListsReact } from '@prezly/slate-lists'; - - const options: ListsOptions = { - defaultBlockType: 'paragraph', - listItemTextType: 'list-item-text', - listItemType: 'list-item', - listTypes: ['ordered-list', 'unordered-list'], - wrappableTypes: ['paragraph'], - }; - - const initialValue: Node[] = [{ type: 'paragraph', children: [{ text: 'Hello world!' }] }]; - - const MyEditor = () => { - const [value, setValue] = useState(initialValue); - const baseEditor = useMemo(() => withReact(createEditor()), []); -- const editor = useMemo(() => withLists(options)(baseEditor), [baseEditor]); -+ const editor = useMemo(() => withListsReact(withLists(options)(baseEditor)), [baseEditor]); - - return ( - - - - ); - }; - - export default MyEditor; -``` - -### Use [`Lists`](src/Lists.ts) - -It's time to pass the [`ListsOptions`](src/types.ts) instance to [`Lists`](src/Lists.ts) function. It will create an object (`lists`) with utilities and transforms bound to the options you passed to it. Those are the building blocks you're going to use when adding lists support to your editor. Use them to implement UI controls, keyboard shortcuts, etc. - -Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-4-use-lists-v5fop?file=/src/MyEditor.tsx - -```diff - import { useMemo, useState } from 'react'; - import { createEditor, Node } from 'slate'; - import { Editable, Slate, withReact } from 'slate-react'; --import { ListsOptions, withLists, withListsReact } from '@prezly/slate-lists'; -+import { Lists, ListsOptions, withLists, withListsReact } from '@prezly/slate-lists'; - - const options: ListsOptions = { - defaultBlockType: 'paragraph', - listItemTextType: 'list-item-text', - listItemType: 'list-item', - listTypes: ['ordered-list', 'unordered-list'], - wrappableTypes: ['paragraph'], +-import { type ListsSchema, ListType, withLists } from '@prezly/slate-lists'; ++import { type ListsSchema, ListType, withLists, withListsReact } from '@prezly/slate-lists'; + + const schema: ListsSchema = { + isConvertibleToListTextNode(node) { + return Element.isElementType(node, PARAGRAPH_TYPE); + }, + isDefaultTextNode(node) { + return Element.isElementType(node, 'paragraph'); + }, + isListNode(node, type?) { + if (type === ListType.UNORDERED) return Element.isElementType(node, 'bulleted-list'); + if (type === ListType.ORDERED) return Element.isElementType(node, 'numbered-list'); + return Element.isElementType(node, 'ordered-list') || Element.isElementType(node, 'numbered-list'); + }, + isListItemNode(node) { + return Element.isElementNode(node, 'list-item'); + }, + isListItemTextNode(node) { + return Element.isElementNode(node, 'list-item-text'); + }, + createDefaultTextNode({ children } = {}) { + return { + type: 'paragraph', + children: children ?? [{ text: '' }], + }; + }, + createListNode({ children } = {}, type = ListType.UNORDERED) { + return { + type: type === ListType.UNORDERED ? 'bulleted-list' : 'numbered-list', + children: children ?? [this.creteListItemNode()], + }; + }, + createListItemNode({ children } = {}) { + return { + type: 'list-item', + children: children ?? [this.createListItemTextNode()], + }; + }, + createListItemTextNode({ children } = {}) { + return { + type: 'list-item-text', + children: children ?? [{ text: '' }], + }; + }, }; -+ -+ const lists = Lists(options); const initialValue: Node[] = [{ type: 'paragraph', children: [{ text: 'Hello world!' }] }]; export const MyEditor = () => { const [value, setValue] = useState(initialValue); - const baseEditor = useMemo(() => withReact(createEditor()), []); - const editor = useMemo(() => withListsReact(withLists(options)(baseEditor)), [baseEditor]); + const editor = useMemo(() => withReact(createEditor()), []); + const editor = useMemo(function() { + const baseEditor = withReact(createEditor()); +- return withLists(schema)(baseEditor); ++ return withListsReact(withLists(schema)(baseEditor)); + }, []); return ( @@ -269,7 +353,7 @@ Live example: https://codesandbox.io/s/prezlyslate-lists-user-guide-4-use-lists- ### Good to go -Now you can use the [API exposed on the `lists` instance](#Lists). +Now you can use the [API exposed on the `Lists` functions](#Lists). Be sure to check the [complete usage example](#Demo). @@ -279,83 +363,90 @@ There are JSDocs for all core functionality. Only core API is documented although all utility functions are exposed. Should you ever need anything beyond the core API, please have a look at [`src/index.ts`](src/index.ts) to see what's available. -- [`ListsOptions`](#ListsOptions) +- [`ListsSchema`](#ListsSchema) - [`withLists`](#withLists) - [`withListsReact`](#withListsReact) - [`Lists`](#Lists) -### [`ListsOptions`](src/types.ts) +### [`ListsSchema`](src/types.ts) + +Lists schema wires the Lists plugin to your project-level defined Slate model. +It is designed with 100% customization in mind, not depending on any specific node types, or non-core interfaces. + +| Name | Description | +|-------------------------------|------------------------------------------------------------------------------------------------------------------------| +| `isConvertibleToListTextNode` | Check if a node can be converted to a list item text node. | +| `isDefaultTextNode` | Check if a node is a plain default text node, that list item text node will become when it is unwrapped or normalized. | +| `isListNode` | Check if a node is representing a list. | +| `isListItemNode` | Check if a node is representing a list item. | +| `isListItemTextNode` | Check if a node is representing a list item text. | +| `createDefaultTextNode` | Create a plain default text node. List item text nodes become these when unwrapped or normalized. | +| `createListNode` | Create a new list node of the given type. | +| `createListItemNode` | Create a new list item node. | +| `createListItemTextNode` | Create a new list item text node. | -All options are required. +### [`ListsEditor`](src/types.ts) -| Name | Type | Description | -| ------------------ | ---------- | ----------------------------------------------------------------------------------------------------------- | -| `defaultBlockType` | `string` | Type of the node that `listItemTextType` will become when it is unwrapped or normalized. | -| `listItemTextType` | `string` | Type of the node representing list item text. | -| `listItemType` | `string` | Type of the node representing list item. | -| `listTypes` | `string[]` | Types of nodes representing lists. The first type will be the default type (e.g. when wrapping with lists). | -| `wrappableTypes` | `string[]` | Types of nodes that can be converted into a node representing list item text. | +ListsEditor is an instance of Slate `Editor`, extends with `ListsSchema` methods: + +```ts +type ListsEditor = Editor & ListsSchema; +``` ### [`withLists`](src/lib/withLists.ts) -```tsx +```ts /** * Enables normalizations that enforce schema constraints and recover from unsupported cases. */ -withLists(options: ListsOptions) => ((editor: T) => T) +withLists(schema: ListsSchema) => ((editor: T) => T & ListsEditor) ``` ### [`withListsReact`](src/lib/withListsReact.ts) -```tsx +```ts /** * Enables Range.prototype.cloneContents monkey patch to improve pasting behavior * in few edge cases. */ -withListsReact(editor: T): T +withListsReact(editor: T): T ``` ### [`Lists`](src/Lists.ts) -```tsx -/** - * Creates an API adapter with functions bound to passed options. - */ -Lists(options: ListsOptions) => :ListsApiAdapter: -``` - -Note: `:ListsApiAdapter:` is actually an implicit interface (`ReturnType`). +`Lists` is a namespace export with all list-related editor utility functions. +Most of them require an instance of [`ListsEditor`](#ListsEditor) as the first argument. -Here are its methods: +Here are the functions methods: -```tsx +```ts /** * Returns true when editor.deleteBackward() is safe to call (it won't break the structure). */ -canDeleteBackward(editor: Editor) => boolean +canDeleteBackward(editor: ListsEditor) => boolean /** * Decreases nesting depth of all "list-items" in the current selection. * All "list-items" in the root "list" will become "default" nodes. */ -decreaseDepth(editor: Editor) => void +decreaseDepth(editor: ListsEditor) => void /** * Decreases nesting depth of "list-item" at a given Path. */ -decreaseListItemDepth(editor: Editor, listItemPath: Path) => void +decreaseListItemDepth(editor: ListsEditor, listItemPath: Path) => void /** * Returns all "list-items" in a given Range. * @param at defaults to current selection if not specified */ -getListItemsInRange(editor: Editor, at: Range | null | undefined) => NodeEntry[] +getListItemsInRange(editor: ListsEditor, at: Range | null | undefined) => NodeEntry[] /** * Returns all "lists" in a given Range. * @param at defaults to current selection if not specified */ -getListsInRange(editor: Editor, at: Range | null | undefined) => NodeEntry[] +getListsInRange(editor: ListsEditor, at: Range | null | undefined) => NodeEntry[] /** * Returns the "type" of a given list node. @@ -366,94 +457,79 @@ getListType(node: Node) => string * Returns "list" node nested in "list-item" at a given path. * Returns null if there is no nested "list". */ -getNestedList(editor: Editor, listItemPath: Path) => NodeEntry | null +getNestedList(editor: ListsEditor, listItemPath: Path) => NodeEntry | null /** * Returns parent "list" node of "list-item" at a given path. * Returns null if there is no parent "list". */ -getParentList(editor: Editor, listItemPath: Path) => NodeEntry | null +getParentList(editor: ListsEditor, listItemPath: Path) => NodeEntry | null /** * Returns parent "list-item" node of "list-item" at a given path. * Returns null if there is no parent "list-item". */ -getParentListItem(editor: Editor, listItemPath: Path) => NodeEntry | null +getParentListItem(editor: ListsEditor, listItemPath: Path) => NodeEntry | null /** * Increases nesting depth of all "list-items" in the current selection. * All nodes matching options.wrappableTypes in the selection will be converted to "list-items" and wrapped in a "list". */ -increaseDepth(editor: Editor) => void +increaseDepth(editor: ListsEditor) => void /** * Increases nesting depth of "list-item" at a given Path. */ -increaseListItemDepth(editor: Editor, listItemPath: Path) => void +increaseListItemDepth(editor: ListsEditor, listItemPath: Path) => void /** * Returns true when editor has collapsed selection and the cursor is in an empty "list-item". */ -isCursorInEmptyListItem(editor: Editor) => boolean +isCursorInEmptyListItem(editor: ListsEditor) => boolean /** * Returns true when editor has collapsed selection and the cursor is at the beginning of a "list-item". */ -isCursorAtStartOfListItem(editor: Editor) => boolean - -/** - * Checks whether node.type is an Element matching any of options.listTypes. - */ -isList(node: Node) => node is Element - -/** - * Checks whether node.type is an Element matching options.listItemType. - */ -isListItem(node: Node) => node is Element - -/** - * Checks whether node.type is an Element matching options.listItemTextType. - */ -isListItemText(node: Node) => node is Element +isCursorAtStartOfListItem(editor: ListsEditor) => boolean /** * Returns true if given "list-item" node contains a non-empty "list-item-text" node. */ -listItemContainsText(editor: Editor, node: Node) => boolean +listItemContainsText(editor: ListsEditor, node: Node) => boolean /** * Moves all "list-items" from one "list" to the end of another "list". */ -moveListItemsToAnotherList(editor: Editor, parameters: { at: NodeEntry; to: NodeEntry; }) => void +moveListItemsToAnotherList(editor: ListsEditor, parameters: { at: NodeEntry; to: NodeEntry; }) => void /** * Nests (moves) given "list" in a given "list-item". */ -moveListToListItem(editor: Editor, parameters: { at: NodeEntry; to: NodeEntry; }) => void +moveListToListItem(editor: ListsEditor, parameters: { at: NodeEntry; to: NodeEntry; }) => void /** * Sets "type" of all "list" nodes in the current selection. */ -setListType(editor: Editor, listType: string) => void +setListType(editor: ListsEditor, listType: string) => void /** * Collapses the current selection (by removing everything in it) and if the cursor * ends up in a "list-item" node, it will break that "list-item" into 2 nodes, splitting * the text at the cursor location. */ -splitListItem(editor: Editor) => void +splitListItem(editor: ListsEditor) => void /** * Unwraps all "list-items" in the current selection. * No list be left in the current selection. */ -unwrapList(editor: Editor) => void +unwrapList(editor: ListsEditor) => void /** * All nodes matching options.wrappableTypes in the current selection * will be converted to "list-items" and wrapped in "lists". */ -wrapInList(editor: Editor, listType: string) => void +wrapInList(editor: ListsEditor, listType: ListType) => void ``` ---- diff --git a/packages/slate-lists/package.json b/packages/slate-lists/package.json index 442ed77f9..9243cfc9c 100644 --- a/packages/slate-lists/package.json +++ b/packages/slate-lists/package.json @@ -54,12 +54,12 @@ "slate-react": "^0.71.0" }, "dependencies": { - "@prezly/slate-commons": "^0.31.1", - "@prezly/slate-types": "^0.31.1", + "@prezly/slate-commons": "^0.31.0", + "is-hotkey": "^0.2.0", "uuid": "^8.3.0" }, "devDependencies": { - "@prezly/slate-hyperscript": "^0.31.1", - "@types/uuid": "^8.3.0" + "@types/uuid": "^8.3.0", + "slate-hyperscript": "^0.67.0" } } diff --git a/packages/slate-lists/src/Lists.ts b/packages/slate-lists/src/Lists.ts deleted file mode 100644 index 82b7fddfa..000000000 --- a/packages/slate-lists/src/Lists.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - canDeleteBackward, - decreaseDepth, - decreaseListItemDepth, - getListItemsInRange, - getListsInRange, - getListType, - getNestedList, - getParentList, - getParentListItem, - increaseDepth, - increaseListItemDepth, - isCursorAtStartOfListItem, - isCursorInEmptyListItem, - isList, - isListItem, - isListItemText, - listItemContainsText, - moveListItemsToAnotherList, - moveListToListItem, - setListType, - splitListItem, - unwrapList, - wrapInList, -} from './lib'; -import type { ListsOptions } from './types'; - -/** - * Creates an API adapter with functions bound to passed options. - */ -export function Lists(options: ListsOptions) { - return { - canDeleteBackward: canDeleteBackward.bind(null, options), - decreaseDepth: decreaseDepth.bind(null, options), - decreaseListItemDepth: decreaseListItemDepth.bind(null, options), - getListItemsInRange: getListItemsInRange.bind(null, options), - getListsInRange: getListsInRange.bind(null, options), - getListType: getListType.bind(null, options), - getNestedList: getNestedList.bind(null, options), - getParentList: getParentList.bind(null, options), - getParentListItem: getParentListItem.bind(null, options), - increaseDepth: increaseDepth.bind(null, options), - increaseListItemDepth: increaseListItemDepth.bind(null, options), - isCursorAtStartOfListItem: isCursorAtStartOfListItem.bind(null, options), - isCursorInEmptyListItem: isCursorInEmptyListItem.bind(null, options), - isList: isList.bind(null, options), - isListItem: isListItem.bind(null, options), - isListItemText: isListItemText.bind(null, options), - listItemContainsText: listItemContainsText.bind(null, options), - moveListItemsToAnotherList: moveListItemsToAnotherList.bind(null, options), - moveListToListItem: moveListToListItem.bind(null, options), - setListType: setListType.bind(null, options), - splitListItem: splitListItem.bind(null, options), - unwrapList: unwrapList.bind(null, options), - wrapInList: wrapInList.bind(null, options), - }; -} diff --git a/packages/slate-lists/src/index.ts b/packages/slate-lists/src/index.ts index 3f10af1c7..20996619e 100644 --- a/packages/slate-lists/src/index.ts +++ b/packages/slate-lists/src/index.ts @@ -1,18 +1,6 @@ -import type { ElementNode, TextNode } from '@prezly/slate-types'; -import type { BaseEditor } from 'slate'; -import type { HistoryEditor } from 'slate-history'; -import type { ReactEditor } from 'slate-react'; - -declare module 'slate' { - interface CustomTypes { - Editor: BaseEditor & ReactEditor & HistoryEditor; - Element: ElementNode; - Text: TextNode; - } -} - -export * from './lib'; -export { Lists } from './Lists'; +export * as Lists from './lib'; +export * as Normalizations from './normalizations'; +export { onKeyDown } from './onKeyDown'; export * from './types'; export { withLists } from './withLists'; export { withListsReact } from './withListsReact'; diff --git a/packages/slate-lists/src/jsx.ts b/packages/slate-lists/src/jsx.ts index 748a44c70..29f2294bc 100644 --- a/packages/slate-lists/src/jsx.ts +++ b/packages/slate-lists/src/jsx.ts @@ -1,108 +1,112 @@ -/* eslint-disable @typescript-eslint/no-namespace */ +import type { ComponentType, ReactNode } from 'react'; +import type * as Slate from 'slate'; +import { createEditor, Element } from 'slate'; +import { createEditor as createEditorFactory, createHyperscript } from 'slate-hyperscript'; -import { createHyperscript } from '@prezly/slate-hyperscript'; -import { - BULLETED_LIST_NODE_TYPE, - LIST_ITEM_NODE_TYPE, - LIST_ITEM_TEXT_NODE_TYPE, - NUMBERED_LIST_NODE_TYPE, - PARAGRAPH_NODE_TYPE, -} from '@prezly/slate-types'; -import type { ReactNode } from 'react'; +import type { ListsSchema } from './types'; +import { ListType } from './types'; +import { withLists } from './withLists'; -import { INLINE_ELEMENT, UNWRAPPABLE_ELEMENT } from './test-utils'; +type AllOrNothing = { [key in keyof T]: T[key] } | { [key in keyof T]?: never }; -declare global { - namespace JSX { - // This is copied from "packages/slate-hyperscript/src/index.ts" - // TODO: find a way to not have to copy it and still have type hinting - // when using hyperscript. - // See: https://github.com/prezly/slate/issues/6 - interface IntrinsicElements { - anchor: - | { - offset?: never; - path?: never; - } - | { - offset: number; - path: number[]; - }; - cursor: { - children?: never; - }; - editor: { - children?: ReactNode; - }; - element: { - [key: string]: any; - children?: ReactNode; - type: string; - }; - focus: - | { - offset?: never; - path?: never; - } - | { - offset: number; - path: number[]; - }; - fragment: { - children?: ReactNode; - }; - selection: { - children?: ReactNode; - }; - // using 'h-text' instead of 'text' to avoid collision with React typings, see: - // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0182cd9094aa081558a3c4bfc970bbdfb71d891d/types/react/index.d.ts#L3136 - 'h-text': { - [key: string]: any; // allow marks - children?: ReactNode; - }; - } +export const PARAGRAPH_TYPE = 'paragraph'; +export const LINK_TYPE = 'link'; +export const DIVIDER_TYPE = 'divider'; +export const ORDERED_LIST_TYPE = ListType.ORDERED; +export const UNORDERED_LIST_TYPE = ListType.UNORDERED; +export const LIST_ITEM_TYPE = 'li'; +export const LIST_ITEM_TEXT_TYPE = 'li-text'; + +export const Editor = 'editor' as any as ComponentType; +export const Untyped = 'untyped' as any as ComponentType; +export const Anchor = 'anchor' as any as ComponentType>; +export const Focus = 'focus' as any as ComponentType>; +export const Cursor = 'cursor' as any as ComponentType; +export const Selection = 'selection' as any as ComponentType; +export const Fragment = 'fragment' as any as ComponentType; +export const Text = 'text' as any as ComponentType<{ children?: ReactNode; [mark: string]: any }>; +export const Paragraph = PARAGRAPH_TYPE as any as ComponentType; +export const Link = LINK_TYPE as any as ComponentType<{ href: string; children?: ReactNode }>; +export const Divider = DIVIDER_TYPE as any as ComponentType; +export const OrderedList = ORDERED_LIST_TYPE as any as ComponentType; +export const UnorderedList = UNORDERED_LIST_TYPE as any as ComponentType; +export const ListItem = LIST_ITEM_TYPE as any as ComponentType; +export const ListItemText = LIST_ITEM_TEXT_TYPE as any as ComponentType; + +const INLINE_ELEMENTS = [LINK_TYPE]; +const VOID_ELEMENTS = [DIVIDER_TYPE]; - interface IntrinsicElements { - 'h-element-with-no-type': { - children?: ReactNode; - }; - // it's a "link" in our tests - because we have to pick something - // but it could have been any other inline element - 'h-inline-element': { - children?: ReactNode; - href: string; - }; - 'h-li': { - children?: ReactNode; - }; - 'h-li-text': { - children?: ReactNode; - }; - 'h-ol': { - children?: ReactNode; - }; - 'h-p': { - children?: ReactNode; - }; - 'h-ul': { - children?: ReactNode; - }; - 'h-unwrappable-element': { - children?: ReactNode; - }; +const SCHEMA: ListsSchema = { + isConvertibleToListTextNode(node) { + return Element.isElementType(node, PARAGRAPH_TYPE); + }, + isDefaultTextNode(node) { + return Element.isElementType(node, PARAGRAPH_TYPE); + }, + isListNode(node, type) { + if (type) { + return Element.isElementType(node, type); } - } -} + return ( + Element.isElementType(node, ORDERED_LIST_TYPE) || + Element.isElementType(node, UNORDERED_LIST_TYPE) + ); + }, + isListItemNode(node) { + return Element.isElementType(node, LIST_ITEM_TYPE); + }, + isListItemTextNode(node) { + return Element.isElementType(node, LIST_ITEM_TEXT_TYPE); + }, + createDefaultTextNode(props = {}) { + return { children: [{ text: '' }], ...props, type: PARAGRAPH_TYPE }; + }, + createListNode(type: ListType = ListType.UNORDERED, props = {}) { + return { children: [{ text: '' }], ...props, type }; + }, + createListItemNode(props = {}) { + return { children: [{ text: '' }], ...props, type: LIST_ITEM_TYPE }; + }, + createListItemTextNode(props = {}) { + return { children: [{ text: '' }], ...props, type: LIST_ITEM_TEXT_TYPE }; + }, +}; export const jsx = createHyperscript({ elements: { - 'h-element-with-no-type': {}, - 'h-inline-element': { type: INLINE_ELEMENT }, - 'h-li': { type: LIST_ITEM_NODE_TYPE }, - 'h-li-text': { type: LIST_ITEM_TEXT_NODE_TYPE }, - 'h-ol': { type: NUMBERED_LIST_NODE_TYPE }, - 'h-p': { type: PARAGRAPH_NODE_TYPE }, - 'h-ul': { type: BULLETED_LIST_NODE_TYPE }, - 'h-unwrappable-element': { type: UNWRAPPABLE_ELEMENT }, + untyped: {}, + [PARAGRAPH_TYPE]: { type: PARAGRAPH_TYPE }, + [LINK_TYPE]: { type: LINK_TYPE }, + [DIVIDER_TYPE]: { type: DIVIDER_TYPE }, + [ORDERED_LIST_TYPE]: { type: ORDERED_LIST_TYPE }, + [UNORDERED_LIST_TYPE]: { type: UNORDERED_LIST_TYPE }, + [LIST_ITEM_TYPE]: { type: LIST_ITEM_TYPE }, + [LIST_ITEM_TEXT_TYPE]: { type: LIST_ITEM_TEXT_TYPE }, + }, + creators: { + editor: createEditorFactory(function () { + const decorators = [withInlineElements, withVoidElements, withLists(SCHEMA)]; + + return decorators.reduce((editor, decorate) => decorate(editor), createEditor()); + }), }, }); + +function withVoidElements(editor: T, types: string[] = VOID_ELEMENTS): T { + const { isVoid } = editor; + editor.isVoid = function (node) { + return types.some((type) => Element.isElementType(node, type)) || isVoid(node); + }; + return editor; +} + +function withInlineElements( + editor: T, + types: string[] = INLINE_ELEMENTS, +): T { + const { isInline } = editor; + editor.isInline = function (node) { + return types.some((type) => Element.isElementType(node, type)) || isInline(node); + }; + return editor; +} diff --git a/packages/slate-lists/src/lib/canDeleteBackward.ts b/packages/slate-lists/src/lib/canDeleteBackward.ts index 30f8ad4e2..73c31e650 100644 --- a/packages/slate-lists/src/lib/canDeleteBackward.ts +++ b/packages/slate-lists/src/lib/canDeleteBackward.ts @@ -1,7 +1,6 @@ import { EditorCommands } from '@prezly/slate-commons'; -import type { Editor } from 'slate'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; import { getListItemsInRange } from './getListItemsInRange'; import { getParentListItem } from './getParentListItem'; @@ -10,15 +9,15 @@ import { isCursorAtStartOfListItem } from './isCursorAtStartOfListItem'; /** * Returns true when editor.deleteBackward() is safe to call (it won't break the structure). */ -export function canDeleteBackward(options: ListsOptions, editor: Editor): boolean { - const listItemsInSelection = getListItemsInRange(options, editor, editor.selection); +export function canDeleteBackward(editor: ListsEditor): boolean { + const listItemsInSelection = getListItemsInRange(editor, editor.selection); if (listItemsInSelection.length === 0) { return true; } const [[, listItemPath]] = listItemsInSelection; - const isInNestedList = getParentListItem(options, editor, listItemPath) !== null; + const isInNestedList = getParentListItem(editor, listItemPath) !== null; const isFirstListItem = EditorCommands.getPreviousSibling(editor, listItemPath) === null; - return isInNestedList || !isFirstListItem || !isCursorAtStartOfListItem(options, editor); + return isInNestedList || !isFirstListItem || !isCursorAtStartOfListItem(editor); } diff --git a/packages/slate-lists/src/lib/cloneContentsMonkeyPatch.ts b/packages/slate-lists/src/lib/cloneContentsMonkeyPatch.ts deleted file mode 100644 index f5ec99976..000000000 --- a/packages/slate-lists/src/lib/cloneContentsMonkeyPatch.ts +++ /dev/null @@ -1,64 +0,0 @@ -const originalCloneContents = Range.prototype.cloneContents; - -function wrapInFragment(nodes: (string | Node)[]): DocumentFragment { - const fragment = document.createDocumentFragment(); - fragment.append(...nodes); - return fragment; -} - -function wrapInList(nodes: (string | Node)[], nodeName: 'OL' | 'UL'): HTMLElement { - const listElement = document.createElement(nodeName); - listElement.append(...nodes); - return listElement; -} - -function wrapInLi(nodes: (string | Node)[]): HTMLElement { - const listItemElement = document.createElement('li'); - listItemElement.append(...nodes); - return listItemElement; -} - -export const cloneContentsMonkeyPatch = { - /** - * Activates `Range.prototype.cloneContents` override that ensures in the cloned contents: - * - there are no
  • children elements without parent
  • element - * - there are no
  • elements without parent
      or
        elements - */ - activate() { - Range.prototype.cloneContents = function cloneContents(): DocumentFragment { - const contents = originalCloneContents.apply(this); - - if ( - this.commonAncestorContainer.nodeName === 'OL' || - this.commonAncestorContainer.nodeName === 'UL' - ) { - return wrapInFragment([ - wrapInList([...contents.childNodes], this.commonAncestorContainer.nodeName), - ]); - } - - if ( - this.commonAncestorContainer.nodeName === 'LI' && - this.commonAncestorContainer.parentElement && - (this.commonAncestorContainer.parentElement.nodeName === 'OL' || - this.commonAncestorContainer.parentElement.nodeName === 'UL') - ) { - return wrapInFragment([ - wrapInList( - [wrapInLi([...contents.childNodes])], - this.commonAncestorContainer.parentElement.nodeName, - ), - ]); - } - - return contents; - }; - }, - - /** - * Brings back the native `Range.prototype.cloneContents`. - */ - deactivate() { - Range.prototype.cloneContents = originalCloneContents; - }, -}; diff --git a/packages/slate-lists/src/lib/createDefaultNode.ts b/packages/slate-lists/src/lib/createDefaultNode.ts deleted file mode 100644 index 87aa5aeee..000000000 --- a/packages/slate-lists/src/lib/createDefaultNode.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Element } from 'slate'; - -import type { ListsOptions } from '../types'; - -export function createDefaultNode(options: ListsOptions): Element { - return { - children: [], - // @prezly/slate-lists package should not assume what default block type is. - // It is up to @prezly/slate-lists package user to ensure that they pass correct options - type: options.defaultBlockType as any, - }; -} diff --git a/packages/slate-lists/src/lib/createList.ts b/packages/slate-lists/src/lib/createList.ts deleted file mode 100644 index e65c08cd7..000000000 --- a/packages/slate-lists/src/lib/createList.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ListNode } from '@prezly/slate-types'; - -export function createList(type: string, children: ListNode['children'] = []): ListNode { - return { - children, - type: type as ListNode['type'], - }; -} diff --git a/packages/slate-lists/src/lib/createListItem.ts b/packages/slate-lists/src/lib/createListItem.ts deleted file mode 100644 index 2aba75b86..000000000 --- a/packages/slate-lists/src/lib/createListItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ListItemNode } from '@prezly/slate-types'; - -import type { ListsOptions } from '../types'; - -import { createListItemText } from './createListItemText'; - -export function createListItem( - options: ListsOptions, - children?: ListItemNode['children'], -): ListItemNode { - return { - children: Array.isArray(children) ? children : [createListItemText(options)], - type: options.listItemType as ListItemNode['type'], - }; -} diff --git a/packages/slate-lists/src/lib/createListItemText.ts b/packages/slate-lists/src/lib/createListItemText.ts deleted file mode 100644 index ca01b5bdf..000000000 --- a/packages/slate-lists/src/lib/createListItemText.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ListItemTextNode } from '@prezly/slate-types'; - -import type { ListsOptions } from '../types'; - -export function createListItemText( - options: ListsOptions, - children: ListItemTextNode['children'] = [{ text: '' }], -): ListItemTextNode { - return { - children, - type: options.listItemTextType as ListItemTextNode['type'], - }; -} diff --git a/packages/slate-lists/src/lib/decreaseDepth.test.tsx b/packages/slate-lists/src/lib/decreaseDepth.test.tsx index 30bad5476..7a0c0ea6f 100644 --- a/packages/slate-lists/src/lib/decreaseDepth.test.tsx +++ b/packages/slate-lists/src/lib/decreaseDepth.test.tsx @@ -1,37 +1,49 @@ /** @jsx jsx */ -import type { Editor } from 'slate'; +import { + jsx, + Editor, + OrderedList, + UnorderedList, + ListItem, + ListItemText, + Text, + Paragraph, + Cursor, + Anchor, + Focus, +} from '../jsx'; +import type { ListsEditor } from '../types'; -import { jsx } from '../jsx'; -import { createListsEditor, lists } from '../test-utils'; +import { decreaseDepth } from './decreaseDepth'; describe('decreaseDepth - no selected items', () => { it('Does nothing when there is no selection', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - ) as unknown as Editor; + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; - lists.decreaseDepth(editor); + decreaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -40,307 +52,307 @@ describe('decreaseDepth - no selected items', () => { describe('decreaseDepth - single item selected', () => { it('Converts list item to a paragraph when there is no grandparent list', () => { - const editor = createListsEditor( - - - - - + const editor = ( + + + + + lorem ipsum - - - - - - , - ); + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - + + + lorem ipsum - - - - - ) as unknown as Editor; + + + + + ) as unknown as ListsEditor; - lists.decreaseDepth(editor); + decreaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Moves list-item to the grandparent list', () => { - const editor = createListsEditor( - - - - - lorem - - - - - ipsum - - - - - + const editor = ( + + + + + lorem + + + + + ipsum + + + + + dolor sit amet - - - - - - - - , - ); + + + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem - - - - - ipsum - - - - - - - + + + + + lorem + + + + + ipsum + + + + + + + dolor sit amet - - - - - - - ) as unknown as Editor; + + + + + + + ) as unknown as ListsEditor; - lists.decreaseDepth(editor); + decreaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Moves list-item to the grandparent list and removes the parent list if empty', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - + const editor = ( + + + + + lorem ipsum + + + + + dolor sit amet - - - - - - - - , - ); + + + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - + + + + + lorem ipsum + + + + + dolor sit amet - - - - - - - ) as unknown as Editor; + + + + + + + ) as unknown as ListsEditor; - lists.decreaseDepth(editor); + decreaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Moves list-item to the grandparent list and moves succeeding siblings into a new nested list', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - aaa - - - - - + const editor = ( + + + + + lorem ipsum + + + + + aaa + + + + + dolor sit amet - - - - - - - bbb - - - - - ccc - - - - - - , - ); + + + + + + + bbb + + + + + ccc + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - aaa - - - - - - - + + + + + lorem ipsum + + + + + aaa + + + + + + + dolor sit amet - - - - - - - bbb - - - - - ccc - - - - - - - ) as unknown as Editor; + + + + + + + bbb + + + + + ccc + + + + + + + ) as unknown as ListsEditor; - lists.decreaseDepth(editor); + decreaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Converts list-item into a paragraph, moves it out of the list and moves succeeding siblings into a new list', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - aaa - - - - - - - + const editor = ( + + + + + lorem ipsum + + + + + aaa + + + + + + + dolor sit amet - - - - - - - bbb - - - - - ccc - - - - , - ); + + + + + + + bbb + + + + + ccc + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - aaa - - - - - - - + + + + + lorem ipsum + + + + + aaa + + + + + + + dolor sit amet - - - - - - - bbb - - - - - ccc - - - - - ) as unknown as Editor; + + + + + + + bbb + + + + + ccc + + + + + ) as unknown as ListsEditor; - lists.decreaseDepth(editor); + decreaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -349,191 +361,191 @@ describe('decreaseDepth - single item selected', () => { describe('decreaseDepth - multiple items selected', () => { it('Decreases depth of all list items in selection that have no list items ancesors in selection', () => { - const editor = createListsEditor( - - - - - Nested Lists A - - - - - Nested Lists A1 - - - - - Nested Lists A1a - - - - - Nested Lists A1b - - - - - - - + const editor = ( + + + + + Nested Lists A + + + + + Nested Lists A1 + + + + + Nested Lists A1a + + + + + Nested Lists A1b + + + + + + + Nested - Lists A2 - - - - - - Nested Lists A2a - - - - - Nested Lists A2b - - - - - - - Nested Lists A3 - - - - - - - Nested Lists B - - - - - Nested Lists B1 - - - - - Nested Lists B1a - - - - - + Lists A2 + + + + + + Nested Lists A2a + + + + + Nested Lists A2b + + + + + + + Nested Lists A3 + + + + + + + Nested Lists B + + + + + Nested Lists B1 + + + + + Nested Lists B1a + + + + + Nested Lists - B1b - - - - - - - - Nested Lists B2 - - - - - - - Nested Lists C - - - - , - ); + B1b + + + + + + + + Nested Lists B2 + + + + + + + Nested Lists C + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - Nested Lists A - - - - - Nested Lists A1 - - - - - Nested Lists A1a - - - - - Nested Lists A1b - - - - - - - - - + + + + + Nested Lists A + + + + + Nested Lists A1 + + + + + Nested Lists A1a + + + + + Nested Lists A1b + + + + + + + + + Nested - Lists A2 - - - - - - Nested Lists A2a - - - - - Nested Lists A2b - - - - - - - Nested Lists A3 - - - - - Nested Lists B - - - - - Nested Lists B1 - - - - - Nested Lists B1a - - - - - + Lists A2 + + + + + + Nested Lists A2a + + + + + Nested Lists A2b + + + + + + + Nested Lists A3 + + + + + Nested Lists B + + + + + Nested Lists B1 + + + + + Nested Lists B1a + + + + + Nested Lists - B1b - - - - - - - - Nested Lists B2 - - - - - Nested Lists C - - - - - ) as unknown as Editor; + B1b + + + + + + + + Nested Lists B2 + + + + + Nested Lists C + + + + + ) as unknown as ListsEditor; - lists.decreaseDepth(editor); + decreaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); diff --git a/packages/slate-lists/src/lib/decreaseDepth.ts b/packages/slate-lists/src/lib/decreaseDepth.ts index 104c0face..72111ab24 100644 --- a/packages/slate-lists/src/lib/decreaseDepth.ts +++ b/packages/slate-lists/src/lib/decreaseDepth.ts @@ -1,7 +1,6 @@ import { EditorCommands, nodeIdManager } from '@prezly/slate-commons'; -import type { Editor } from 'slate'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; import { decreaseListItemDepth } from './decreaseListItemDepth'; import { getListItemsInRange } from './getListItemsInRange'; @@ -10,12 +9,12 @@ import { getListItemsInRange } from './getListItemsInRange'; * Decreases nesting depth of all "list-items" in the current selection. * All "list-items" in the root "list" will become "default" nodes. */ -export function decreaseDepth(options: ListsOptions, editor: Editor): void { +export function decreaseDepth(editor: ListsEditor): void { if (!editor.selection) { return; } - const listItemsInRange = getListItemsInRange(options, editor, editor.selection); + const listItemsInRange = getListItemsInRange(editor, editor.selection); const unreachableListItems = EditorCommands.getUnreachableAncestors(listItemsInRange); // When calling `decreaseListItemDepth` the paths and references to "list-items" // can change, so we need a way of marking the "list-items" scheduled for transformation. @@ -33,6 +32,6 @@ export function decreaseDepth(options: ListsOptions, editor: Editor): void { } const [, listItemEntryPath] = listItemEntry; - decreaseListItemDepth(options, editor, listItemEntryPath); + decreaseListItemDepth(editor, listItemEntryPath); }); } diff --git a/packages/slate-lists/src/lib/decreaseListItemDepth.ts b/packages/slate-lists/src/lib/decreaseListItemDepth.ts index 5f57801f9..6d22f74b7 100644 --- a/packages/slate-lists/src/lib/decreaseListItemDepth.ts +++ b/packages/slate-lists/src/lib/decreaseListItemDepth.ts @@ -1,9 +1,9 @@ -import type { Element } from 'slate'; import { Editor, Node, Path, Transforms } from 'slate'; import { NESTED_LIST_PATH_INDEX, TEXT_PATH_INDEX } from '../constants'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; +import { getListType } from './getListType'; import { getParentList } from './getParentList'; import { getParentListItem } from './getParentListItem'; import { increaseListItemDepth } from './increaseListItemDepth'; @@ -11,12 +11,8 @@ import { increaseListItemDepth } from './increaseListItemDepth'; /** * Decreases nesting depth of "list-item" at a given Path. */ -export function decreaseListItemDepth( - options: ListsOptions, - editor: Editor, - listItemPath: Path, -): void { - const parentList = getParentList(options, editor, listItemPath); +export function decreaseListItemDepth(editor: ListsEditor, listItemPath: Path): void { + const parentList = getParentList(editor, listItemPath); if (!parentList) { // It should never happen. @@ -24,7 +20,7 @@ export function decreaseListItemDepth( } const [parentListNode, parentListPath] = parentList; - const parentListItem = getParentListItem(options, editor, listItemPath); + const parentListItem = getParentListItem(editor, listItemPath); const listItemIndex = listItemPath[listItemPath.length - 1]; const previousSiblings = parentListNode.children.slice(0, listItemIndex); const nextSiblings = parentListNode.children.slice(listItemIndex + 1); @@ -35,7 +31,7 @@ export function decreaseListItemDepth( // The next sibling path is always the same, because once we move out the next sibling, // another one will take its place. const nextSiblingPath = [...parentListPath, listItemIndex + 1]; - increaseListItemDepth(options, editor, nextSiblingPath); + increaseListItemDepth(editor, nextSiblingPath); }); Editor.withoutNormalizing(editor, () => { @@ -60,7 +56,7 @@ export function decreaseListItemDepth( if (Node.has(editor, listItemNestedListPath)) { Transforms.setNodes( editor, - { type: parentListNode.type }, + editor.createListNode(getListType(editor, parentListNode)), { at: listItemNestedListPath }, ); Transforms.liftNodes(editor, { at: listItemNestedListPath }); @@ -68,11 +64,9 @@ export function decreaseListItemDepth( } if (Node.has(editor, listItemTextPath)) { - Transforms.setNodes( - editor, - { type: options.defaultBlockType as Element['type'] }, - { at: listItemTextPath }, - ); + Transforms.setNodes(editor, editor.createDefaultTextNode(), { + at: listItemTextPath, + }); Transforms.liftNodes(editor, { at: listItemTextPath }); Transforms.liftNodes(editor, { at: listItemPath }); } diff --git a/packages/slate-lists/src/lib/getListItemsInRange.test.tsx b/packages/slate-lists/src/lib/getListItemsInRange.test.tsx index 69ddd2566..1f5bd0683 100644 --- a/packages/slate-lists/src/lib/getListItemsInRange.test.tsx +++ b/packages/slate-lists/src/lib/getListItemsInRange.test.tsx @@ -1,147 +1,157 @@ /** @jsx jsx */ -import { jsx } from '../jsx'; -import { createListsEditor, options } from '../test-utils'; +import { + jsx, + Editor, + OrderedList, + UnorderedList, + ListItem, + ListItemText, + Text, + Anchor, + Focus, +} from '../jsx'; +import type { ListsEditor } from '../types'; import { getListItemsInRange } from './getListItemsInRange'; describe('getListItemsInRange', () => { it('Returns an empty array when there is no selection', () => { - const editor = createListsEditor( - - - - - Nested Lists A - - - - - Nested Lists A1 - - - - - Nested Lists A1a - - - - - Nested Lists A1b - - - - - - - - - Nested Lists C - - - - , - ); + const editor = ( + + + + + Nested Lists A + + + + + Nested Lists A1 + + + + + Nested Lists A1a + + + + + Nested Lists A1b + + + + + + + + + Nested Lists C + + + + + ) as unknown as ListsEditor; - const listItemsInRange = getListItemsInRange(options, editor, editor.selection); + const listItemsInRange = getListItemsInRange(editor, editor.selection); expect(listItemsInRange).toEqual([]); }); it('Finds all partially selected list items', () => { - const editor = createListsEditor( - - - - - Nested Lists A - - - - - Nested Lists A1 - - - - - Nested Lists A1a - - - - - Nested Lists A1b - - - - - - - + const editor = ( + + + + + Nested Lists A + + + + + Nested Lists A1 + + + + + Nested Lists A1a + + + + + Nested Lists A1b + + + + + + + Nested - Lists A2 - - - - - - Nested Lists A2a - - - - - Nested Lists A2b - - - - - - - Nested Lists A3 - - - - - - - Nested Lists B - - - - - Nested Lists B1 - - - - - Nested Lists B1a - - - - - + Lists A2 + + + + + + Nested Lists A2a + + + + + Nested Lists A2b + + + + + + + Nested Lists A3 + + + + + + + Nested Lists B + + + + + Nested Lists B1 + + + + + Nested Lists B1a + + + + + Nested Lists - B1b - - - - - - - - Nested Lists B2 - - - - - - - Nested Lists C - - - - , - ); + B1b + + + + + + + + Nested Lists B2 + + + + + + + Nested Lists C + + + + + ) as unknown as ListsEditor; - const listItemsInRange = getListItemsInRange(options, editor, editor.selection); + const listItemsInRange = getListItemsInRange(editor, editor.selection); const listItemsPathsInRange = listItemsInRange.map(([, path]) => path); expect(listItemsPathsInRange).toEqual([ diff --git a/packages/slate-lists/src/lib/getListItemsInRange.ts b/packages/slate-lists/src/lib/getListItemsInRange.ts index 1a5134668..a4988e11e 100644 --- a/packages/slate-lists/src/lib/getListItemsInRange.ts +++ b/packages/slate-lists/src/lib/getListItemsInRange.ts @@ -1,19 +1,12 @@ import type { Element, NodeEntry } from 'slate'; import { Editor, Path, Range } from 'slate'; -import type { ListsOptions } from '../types'; - -import { isListItem } from './isListItem'; +import type { ListsEditor } from '../types'; /** * Returns all "list-items" in a given Range. - * @param at defaults to current selection if not specified */ -export function getListItemsInRange( - options: ListsOptions, - editor: Editor, - at: Range | null | undefined, -): NodeEntry[] { +export function getListItemsInRange(editor: ListsEditor, at?: Range | null): NodeEntry[] { if (!at) { return []; } @@ -21,7 +14,7 @@ export function getListItemsInRange( const rangeStartPoint = Range.start(at); const listItemsInSelection = Editor.nodes(editor, { at, - match: (node) => isListItem(options, node), + match: editor.isListItemNode, }); return Array.from(listItemsInSelection).filter(([, path]) => { diff --git a/packages/slate-lists/src/lib/getListType.ts b/packages/slate-lists/src/lib/getListType.ts index de85076e5..b12ac7716 100644 --- a/packages/slate-lists/src/lib/getListType.ts +++ b/packages/slate-lists/src/lib/getListType.ts @@ -1,15 +1,23 @@ +import type { Node } from 'slate'; import { Element } from 'slate'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; +import { ListType } from '../types'; /** * Returns the "type" of a given list node. */ -export function getListType(options: ListsOptions, node: unknown): string { - if (Element.isElement(node)) { - return node.type; +export function getListType(editor: ListsEditor, node: Node): ListType { + const isElement = Element.isElement(node); + + if (isElement && editor.isListNode(node, ListType.ORDERED)) { + return ListType.ORDERED; + } + + if (isElement && editor.isListNode(node, ListType.UNORDERED)) { + return ListType.UNORDERED; } - // It should never happen. - return options.listTypes[0]; + // This should never happen. + return ListType.UNORDERED; } diff --git a/packages/slate-lists/src/lib/getListsInRange.ts b/packages/slate-lists/src/lib/getListsInRange.ts index 3ade0b210..8788dd8b1 100644 --- a/packages/slate-lists/src/lib/getListsInRange.ts +++ b/packages/slate-lists/src/lib/getListsInRange.ts @@ -1,22 +1,20 @@ -import type { Editor, Element, NodeEntry, Range } from 'slate'; +import type { Element, NodeEntry, Range } from 'slate'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; import { getListItemsInRange } from './getListItemsInRange'; import { getParentList } from './getParentList'; /** - * Returns all "lists" in a given Range. - * @param at defaults to current selection if not specified + * Get all lists in the given Range. */ export function getListsInRange( - options: ListsOptions, - editor: Editor, + editor: ListsEditor, at: Range | null | undefined, ): NodeEntry[] { - const listItemsInRange = getListItemsInRange(options, editor, at); + const listItemsInRange = getListItemsInRange(editor, at); const lists = listItemsInRange - .map(([, listItemPath]) => getParentList(options, editor, listItemPath)) + .map(([, listItemPath]) => getParentList(editor, listItemPath)) .filter((list) => list !== null); // TypeScript complains about `null`s even though we filter for them, hence the typecast. return lists as NodeEntry[]; diff --git a/packages/slate-lists/src/lib/getNestedList.ts b/packages/slate-lists/src/lib/getNestedList.ts index 3f5ef0fbe..b07c93524 100644 --- a/packages/slate-lists/src/lib/getNestedList.ts +++ b/packages/slate-lists/src/lib/getNestedList.ts @@ -1,21 +1,15 @@ -import type { Editor, Element, NodeEntry, Path } from 'slate'; -import { Node } from 'slate'; +import type { NodeEntry, Path } from 'slate'; +import { Node, Element } from 'slate'; import { NESTED_LIST_PATH_INDEX } from '../constants'; -import type { ListsOptions } from '../types'; - -import { isList } from './isList'; +import type { ListsEditor } from '../types'; /** * Returns "list" node nested in "list-item" at a given path. * Returns null if there is no nested "list". */ -export function getNestedList( - options: ListsOptions, - editor: Editor, - listItemPath: Path, -): NodeEntry | null { - const nestedListPath = [...listItemPath, NESTED_LIST_PATH_INDEX]; +export function getNestedList(editor: ListsEditor, path: Path): NodeEntry | null { + const nestedListPath = [...path, NESTED_LIST_PATH_INDEX]; if (!Node.has(editor, nestedListPath)) { return null; @@ -23,10 +17,10 @@ export function getNestedList( const nestedList = Node.get(editor, nestedListPath); - if (!isList(options, nestedList)) { + if (Element.isElement(nestedList) && editor.isListNode(nestedList)) { // Sanity check. - return null; + return [nestedList, nestedListPath]; } - return [nestedList, nestedListPath]; + return null; } diff --git a/packages/slate-lists/src/lib/getParentList.ts b/packages/slate-lists/src/lib/getParentList.ts index 20c673fe4..4e7d3c4ea 100644 --- a/packages/slate-lists/src/lib/getParentList.ts +++ b/packages/slate-lists/src/lib/getParentList.ts @@ -1,28 +1,17 @@ -import type { ElementNode } from '@prezly/slate-types'; -import type { NodeEntry, Path } from 'slate'; +import type { Element, NodeEntry, Path } from 'slate'; import { Editor } from 'slate'; -import type { ListsOptions } from '../types'; - -import { isList } from './isList'; +import type { ListsEditor } from '../types'; /** * Returns parent "list" node of "list-item" at a given path. * Returns null if there is no parent "list". */ -export function getParentList( - options: ListsOptions, - editor: Editor, - listItemPath: Path, -): NodeEntry | null { - const parentList = Editor.above(editor, { - at: listItemPath, - match: (node) => isList(options, node), +export function getParentList(editor: ListsEditor, path: Path): NodeEntry | null { + const parentList = Editor.above(editor, { + at: path, + match: (node): node is Element => editor.isListNode(node), }); - if (parentList && isList(options, parentList[0])) { - return parentList; - } - - return null; + return parentList ?? null; } diff --git a/packages/slate-lists/src/lib/getParentListItem.ts b/packages/slate-lists/src/lib/getParentListItem.ts index 2dbca19ce..a6da361b7 100644 --- a/packages/slate-lists/src/lib/getParentListItem.ts +++ b/packages/slate-lists/src/lib/getParentListItem.ts @@ -1,28 +1,17 @@ -import type { ElementNode } from '@prezly/slate-types'; -import type { NodeEntry, Path } from 'slate'; +import type { Element, NodeEntry, Path } from 'slate'; import { Editor } from 'slate'; -import type { ListsOptions } from '../types'; - -import { isListItem } from './isListItem'; +import type { ListsEditor } from '../types'; /** * Returns parent "list-item" node of "list-item" at a given path. * Returns null if there is no parent "list-item". */ -export function getParentListItem( - options: ListsOptions, - editor: Editor, - listItemPath: Path, -): NodeEntry | null { - const parentListItem = Editor.above(editor, { - at: listItemPath, - match: (node) => isListItem(options, node), +export function getParentListItem(editor: ListsEditor, path: Path): NodeEntry | null { + const parentListItem = Editor.above(editor, { + at: path, + match: (node) => editor.isListItemNode(node), }); - if (parentListItem && isListItem(options, parentListItem[0])) { - return parentListItem; - } - - return null; + return parentListItem ?? null; } diff --git a/packages/slate-lists/src/lib/increaseDepth.test.tsx b/packages/slate-lists/src/lib/increaseDepth.test.tsx index 4a6b3c1e6..6a7258f8e 100644 --- a/packages/slate-lists/src/lib/increaseDepth.test.tsx +++ b/packages/slate-lists/src/lib/increaseDepth.test.tsx @@ -1,37 +1,49 @@ /** @jsx jsx */ -import type { Editor } from 'slate'; - -import { jsx } from '../jsx'; -import { createListsEditor, lists } from '../test-utils'; +import { + jsx, + Editor, + UnorderedList, + OrderedList, + ListItem, + ListItemText, + Text, + Paragraph, + Anchor, + Focus, + Cursor, +} from '../jsx'; +import type { ListsEditor } from '../types'; + +import { increaseDepth } from './increaseDepth'; describe('increaseDepth - no selected items', () => { it('Does nothing when there is no selection', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - ) as unknown as Editor; - - lists.increaseDepth(editor); + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; + + increaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -40,196 +52,196 @@ describe('increaseDepth - no selected items', () => { describe('increaseDepth - single item selected', () => { it('Does nothing when there is no preceding sibling list item', () => { - const editor = createListsEditor( - - - - - + const editor = ( + + + + + lorem ipsum - - - - - - , - ); + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - + + + + + lorem ipsum - - - - - - - ) as unknown as Editor; + + + + + + + ) as unknown as ListsEditor; - lists.increaseDepth(editor); + increaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Moves list-item to the child list of a preceding sibling list item', () => { - const editor = createListsEditor( - - - - - lorem - - - - - ipsum - - - - - - - + const editor = ( + + + + + lorem + + + + + ipsum + + + + + + + dolor sit amet - - - - - - , - ); + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem - - - - - ipsum - - - - - + + + + + lorem + + + + + ipsum + + + + + dolor sit amet - - - - - - - - - ) as unknown as Editor; - - lists.increaseDepth(editor); + + + + + + + + + ) as unknown as ListsEditor; + + increaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Creates a child list in preceding sibling list item and moves list-item there', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - + const editor = ( + + + + + lorem ipsum + + + + + dolor sit amet - - - - - - , - ); + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - + + + + + lorem ipsum + + + + + dolor sit amet - - - - - - - - - ) as unknown as Editor; - - lists.increaseDepth(editor); + + + + + + + + + ) as unknown as ListsEditor; + + increaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Creates a child list in preceding sibling list item and moves list-item there, maintaining list type', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - + const editor = ( + + + + + lorem ipsum + + + + + dolor sit amet - - - - - - , - ); + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - + + + + + lorem ipsum + + + + + dolor sit amet - - - - - - - - - ) as unknown as Editor; - - lists.increaseDepth(editor); + + + + + + + + + ) as unknown as ListsEditor; + + increaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -238,258 +250,258 @@ describe('increaseDepth - single item selected', () => { describe('increaseDepth - multiple items selected', () => { it('Increases depth of all indentable list items in selection that have no list items ancestors in selection (A)', () => { - const editor = createListsEditor( - - - - - - + const editor = ( + + + + + + aaa - - - - - - bbb - - - - - + + + + + + bbb + + + + + ccc - - - - - - , - ); + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - - + + + + + + aaa - - - - - - bbb - - - - - + + + + + + bbb + + + + + ccc - - - - - - - - - ) as unknown as Editor; - - lists.increaseDepth(editor); + + + + + + + + + ) as unknown as ListsEditor; + + increaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Increases depth of all indentable list items in selection that have no list items ancestors in selection (B)', () => { - const editor = createListsEditor( - - - - - Nested Lists A - - - - - Nested Lists A1 - - - - - Nested Lists A1a - - - - - Nested Lists A1b - - - - - - - + const editor = ( + + + + + Nested Lists A + + + + + Nested Lists A1 + + + + + Nested Lists A1a + + + + + Nested Lists A1b + + + + + + + Nested - Lists A2 - - - - - - Nested Lists A2a - - - - - Nested Lists A2b - - - - - - - Nested Lists A3 - - - - - - - Nested Lists B - - - - - Nested Lists B1 - - - - - Nested Lists B1a - - - - - + Lists A2 + + + + + + Nested Lists A2a + + + + + Nested Lists A2b + + + + + + + Nested Lists A3 + + + + + + + Nested Lists B + + + + + Nested Lists B1 + + + + + Nested Lists B1a + + + + + Nested Lists - B1b - - - - - - - - Nested Lists B2 - - - - - - - Nested Lists C - - - - , - ); + B1b + + + + + + + + Nested Lists B2 + + + + + + + Nested Lists C + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - Nested Lists A - - - - - Nested Lists A1 - - - - - Nested Lists A1a - - - - - Nested Lists A1b - - - - - + + + + + Nested Lists A + + + + + Nested Lists A1 + + + + + Nested Lists A1a + + + + + Nested Lists A1b + + + + + Nested - Lists A2 - - - - - - Nested Lists A2a - - - - - Nested Lists A2b - - - - - - - Nested Lists A3 - - - - - - - Nested Lists B - - - - - Nested Lists B1 - - - - - Nested Lists B1a - - - - - + Lists A2 + + + + + + Nested Lists A2a + + + + + Nested Lists A2b + + + + + + + Nested Lists A3 + + + + + + + Nested Lists B + + + + + Nested Lists B1 + + + + + Nested Lists B1a + + + + + Nested Lists - B1b - - - - - - - - Nested Lists B2 - - - - - - - - - Nested Lists C - - - - - ) as unknown as Editor; - - lists.increaseDepth(editor); + B1b + + + + + + + + Nested Lists B2 + + + + + + + + + Nested Lists C + + + + + ) as unknown as ListsEditor; + + increaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -498,81 +510,81 @@ describe('increaseDepth - multiple items selected', () => { describe('increaseDepth - multiple items and paragraphs selected', () => { it('Converts paragraphs into lists items and merges them together', () => { - const editor = createListsEditor( - - - - + const editor = ( + + + + aaa - - - - - - bbb - - - - - ccc - - - - - ddd - - - - - + + + + + + bbb + + + + + ccc + + + + + ddd + + + + + eee - - - - , - ); + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - - + + + + + + aaa - - - - - - bbb - - - - - ccc - - - - - ddd - - - - - - - + + + + + + bbb + + + + + ccc + + + + + ddd + + + + + + + eee - - - - - - - ) as unknown as Editor; - - lists.increaseDepth(editor); + + + + + + + ) as unknown as ListsEditor; + + increaseDepth(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); diff --git a/packages/slate-lists/src/lib/increaseDepth.ts b/packages/slate-lists/src/lib/increaseDepth.ts index a35355e63..87057626c 100644 --- a/packages/slate-lists/src/lib/increaseDepth.ts +++ b/packages/slate-lists/src/lib/increaseDepth.ts @@ -1,7 +1,7 @@ import { EditorCommands, nodeIdManager } from '@prezly/slate-commons'; -import type { Editor } from 'slate'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; +import { ListType } from '../types'; import { getListItemsInRange } from './getListItemsInRange'; import { increaseListItemDepth } from './increaseListItemDepth'; @@ -11,12 +11,12 @@ import { wrapInList } from './wrapInList'; * Increases nesting depth of all "list-items" in the current selection. * All nodes matching options.wrappableTypes in the selection will be converted to "list-items" and wrapped in a "list". */ -export function increaseDepth(options: ListsOptions, editor: Editor): void { +export function increaseDepth(editor: ListsEditor): void { if (!editor.selection) { return; } - const listItemsInRange = getListItemsInRange(options, editor, editor.selection); + const listItemsInRange = getListItemsInRange(editor, editor.selection); const indentableListItemsInRange = listItemsInRange.filter(([, listItemPath]) => { const previousListItem = EditorCommands.getPreviousSibling(editor, listItemPath); return previousListItem !== null; @@ -29,7 +29,7 @@ export function increaseDepth(options: ListsOptions, editor: Editor): void { }); // Before we indent "list-items", we want to convert every non list-related block in selection to a "list". - wrapInList(options, editor, options.listTypes[0]); + wrapInList(editor, ListType.UNORDERED); unreachableListItemsIds.forEach((id) => { const listItemEntry = nodeIdManager.get(editor, id); @@ -41,6 +41,6 @@ export function increaseDepth(options: ListsOptions, editor: Editor): void { } const [, listItemEntryPath] = listItemEntry; - increaseListItemDepth(options, editor, listItemEntryPath); + increaseListItemDepth(editor, listItemEntryPath); }); } diff --git a/packages/slate-lists/src/lib/increaseListItemDepth.ts b/packages/slate-lists/src/lib/increaseListItemDepth.ts index 56ab2dd6b..e25da91db 100644 --- a/packages/slate-lists/src/lib/increaseListItemDepth.ts +++ b/packages/slate-lists/src/lib/increaseListItemDepth.ts @@ -1,22 +1,15 @@ import { EditorCommands } from '@prezly/slate-commons'; -import { Editor, Node, Path, Transforms } from 'slate'; +import { Editor, Element, Node, Path, Transforms } from 'slate'; import { NESTED_LIST_PATH_INDEX } from '../constants'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; -import { createList } from './createList'; import { getListType } from './getListType'; -import { isList } from './isList'; -import { isListItem } from './isListItem'; /** * Increases nesting depth of "list-item" at a given Path. */ -export function increaseListItemDepth( - options: ListsOptions, - editor: Editor, - listItemPath: Path, -): void { +export function increaseListItemDepth(editor: ListsEditor, listItemPath: Path): void { const previousListItem = EditorCommands.getPreviousSibling(editor, listItemPath); if (!previousListItem) { @@ -27,7 +20,7 @@ export function increaseListItemDepth( const [previousListItemNode, previousListItemPath] = previousListItem; - if (!isListItem(options, previousListItemNode)) { + if (!editor.isListItemNode(previousListItemNode)) { // Sanity check. return; } @@ -40,13 +33,16 @@ export function increaseListItemDepth( if (!previousListItemHasChildList) { const listNodePath = Path.ancestors(listItemPath, { reverse: true })[0]; const listNode = Node.get(editor, listNodePath); - const newList = createList(getListType(options, listNode)); + const newList = editor.createListNode(getListType(editor, listNode)); Transforms.insertNodes(editor, newList, { at: previousListItemChildListPath }); } const previousListItemChildList = Node.get(editor, previousListItemChildListPath); - if (isList(options, previousListItemChildList)) { + if ( + Element.isElement(previousListItemChildList) && + editor.isListNode(previousListItemChildList) + ) { const index = previousListItemHasChildList ? previousListItemChildList.children.length : 0; diff --git a/packages/slate-lists/src/lib/index.ts b/packages/slate-lists/src/lib/index.ts index 230904757..f6fb1602d 100644 --- a/packages/slate-lists/src/lib/index.ts +++ b/packages/slate-lists/src/lib/index.ts @@ -1,9 +1,4 @@ export { canDeleteBackward } from './canDeleteBackward'; -export { cloneContentsMonkeyPatch } from './cloneContentsMonkeyPatch'; -export { createDefaultNode } from './createDefaultNode'; -export { createList } from './createList'; -export { createListItem } from './createListItem'; -export { createListItemText } from './createListItemText'; export { decreaseDepth } from './decreaseDepth'; export { decreaseListItemDepth } from './decreaseListItemDepth'; export { getListItemsInRange } from './getListItemsInRange'; @@ -16,21 +11,10 @@ export { increaseDepth } from './increaseDepth'; export { increaseListItemDepth } from './increaseListItemDepth'; export { isCursorAtStartOfListItem } from './isCursorAtStartOfListItem'; export { isCursorInEmptyListItem } from './isCursorInEmptyListItem'; -export { isList } from './isList'; -export { isListItem } from './isListItem'; -export { isListItemText } from './isListItemText'; export { listItemContainsText } from './listItemContainsText'; export { mergeListWithPreviousSiblingList } from './mergeListWithPreviousSiblingList'; export { moveListItemsToAnotherList } from './moveListItemsToAnotherList'; export { moveListToListItem } from './moveListToListItem'; -export { normalizeList } from './normalizeList'; -export { normalizeListChildren } from './normalizeListChildren'; -export { normalizeListItemChildren } from './normalizeListItemChildren'; -export { normalizeListItemTextChildren } from './normalizeListItemTextChildren'; -export { normalizeOrphanListItem } from './normalizeOrphanListItem'; -export { normalizeOrphanListItemText } from './normalizeOrphanListItemText'; -export { normalizeOrphanNestedList } from './normalizeOrphanNestedList'; -export { normalizeSiblingLists } from './normalizeSiblingLists'; export { setListType } from './setListType'; export { splitListItem } from './splitListItem'; export { unwrapList } from './unwrapList'; diff --git a/packages/slate-lists/src/lib/isCursorAtStartOfListItem.ts b/packages/slate-lists/src/lib/isCursorAtStartOfListItem.ts index a6e61c763..53515fcb0 100644 --- a/packages/slate-lists/src/lib/isCursorAtStartOfListItem.ts +++ b/packages/slate-lists/src/lib/isCursorAtStartOfListItem.ts @@ -1,20 +1,19 @@ import { EditorCommands } from '@prezly/slate-commons'; -import type { Editor } from 'slate'; import { Range } from 'slate'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; import { getListItemsInRange } from './getListItemsInRange'; /** * Returns true when editor has collapsed selection and the cursor is at the beginning of a "list-item". */ -export function isCursorAtStartOfListItem(options: ListsOptions, editor: Editor): boolean { +export function isCursorAtStartOfListItem(editor: ListsEditor): boolean { if (!editor.selection || Range.isExpanded(editor.selection)) { return false; } - const listItemsInSelection = getListItemsInRange(options, editor, editor.selection); + const listItemsInSelection = getListItemsInRange(editor, editor.selection); if (listItemsInSelection.length !== 1) { return false; diff --git a/packages/slate-lists/src/lib/isCursorInEmptyListItem.ts b/packages/slate-lists/src/lib/isCursorInEmptyListItem.ts index c95c6aabd..370a708dd 100644 --- a/packages/slate-lists/src/lib/isCursorInEmptyListItem.ts +++ b/packages/slate-lists/src/lib/isCursorInEmptyListItem.ts @@ -1,7 +1,6 @@ -import type { Editor } from 'slate'; import { Range } from 'slate'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; import { getListItemsInRange } from './getListItemsInRange'; import { listItemContainsText } from './listItemContainsText'; @@ -9,12 +8,12 @@ import { listItemContainsText } from './listItemContainsText'; /** * Returns true when editor has collapsed selection and the cursor is in an empty "list-item". */ -export function isCursorInEmptyListItem(options: ListsOptions, editor: Editor): boolean { +export function isCursorInEmptyListItem(editor: ListsEditor): boolean { if (!editor.selection || Range.isExpanded(editor.selection)) { return false; } - const listItemsInSelection = getListItemsInRange(options, editor, editor.selection); + const listItemsInSelection = getListItemsInRange(editor, editor.selection); if (listItemsInSelection.length !== 1) { return false; @@ -22,5 +21,5 @@ export function isCursorInEmptyListItem(options: ListsOptions, editor: Editor): const [[listItemNode]] = listItemsInSelection; - return !listItemContainsText(options, editor, listItemNode); + return !listItemContainsText(editor, listItemNode); } diff --git a/packages/slate-lists/src/lib/isList.ts b/packages/slate-lists/src/lib/isList.ts deleted file mode 100644 index 8d3bc619f..000000000 --- a/packages/slate-lists/src/lib/isList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ElementNode } from '@prezly/slate-types'; -import { isElementNode } from '@prezly/slate-types'; - -import type { ListsOptions } from '../types'; - -/** - * Checks whether node.type is an Element matching any of options.listTypes. - */ -export function isList(options: ListsOptions, node: unknown): node is ElementNode { - return isElementNode(node, options.listTypes); -} diff --git a/packages/slate-lists/src/lib/isListItem.ts b/packages/slate-lists/src/lib/isListItem.ts deleted file mode 100644 index 294144b24..000000000 --- a/packages/slate-lists/src/lib/isListItem.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ElementNode } from '@prezly/slate-types'; -import { isElementNode } from '@prezly/slate-types'; - -import type { ListsOptions } from '../types'; - -/** - * Checks whether node.type is an Element matching options.listItemType. - */ -export function isListItem(options: ListsOptions, node: unknown): node is ElementNode { - return isElementNode(node, options.listItemType); -} diff --git a/packages/slate-lists/src/lib/isListItemText.ts b/packages/slate-lists/src/lib/isListItemText.ts deleted file mode 100644 index bba1db80a..000000000 --- a/packages/slate-lists/src/lib/isListItemText.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ElementNode } from '@prezly/slate-types'; -import { isElementNode } from '@prezly/slate-types'; - -import type { ListsOptions } from '../types'; - -/** - * Checks whether node.type is an Element matching options.listItemTextType. - */ -export function isListItemText(options: ListsOptions, node: unknown): node is ElementNode { - return isElementNode(node, options.listItemTextType); -} diff --git a/packages/slate-lists/src/lib/listItemContainsText.ts b/packages/slate-lists/src/lib/listItemContainsText.ts index 63b98f430..9061ebe4c 100644 --- a/packages/slate-lists/src/lib/listItemContainsText.ts +++ b/packages/slate-lists/src/lib/listItemContainsText.ts @@ -1,27 +1,18 @@ -import { Editor } from 'slate'; +import type { Node } from 'slate'; +import { Editor, Element } from 'slate'; -import type { ListsOptions } from '../types'; - -import { isListItem } from './isListItem'; -import { isListItemText } from './isListItemText'; +import type { ListsEditor } from '../types'; /** * Returns true if given "list-item" node contains a non-empty "list-item-text" node. */ -export function listItemContainsText( - options: ListsOptions, - editor: Editor, - node: unknown, -): boolean { - if (!isListItem(options, node)) { - return false; - } +export function listItemContainsText(editor: ListsEditor, node: Node): boolean { + if (Element.isElement(node) && editor.isListItemNode(node)) { + const [listItemText] = node.children; - const [listItemText] = node.children; - - if (!isListItemText(options, listItemText)) { - return false; + if (Element.isElement(listItemText) && editor.isListItemTextNode(listItemText)) { + return !Editor.isEmpty(editor, listItemText); + } } - - return !Editor.isEmpty(editor, listItemText); + return false; } diff --git a/packages/slate-lists/src/lib/mergeListWithPreviousSiblingList.ts b/packages/slate-lists/src/lib/mergeListWithPreviousSiblingList.ts index 03ff47031..5029e0c1b 100644 --- a/packages/slate-lists/src/lib/mergeListWithPreviousSiblingList.ts +++ b/packages/slate-lists/src/lib/mergeListWithPreviousSiblingList.ts @@ -1,18 +1,17 @@ import { EditorCommands } from '@prezly/slate-commons'; -import type { Editor, Node, NodeEntry } from 'slate'; +import type { Node, NodeEntry } from 'slate'; import { Transforms } from 'slate'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; +import { getListType } from './getListType'; import { getParentListItem } from './getParentListItem'; -import { isList } from './isList'; export function mergeListWithPreviousSiblingList( - options: ListsOptions, - editor: Editor, + editor: ListsEditor, [node, path]: NodeEntry, ): boolean { - if (!isList(options, node)) { + if (!editor.isListNode(node)) { // This function does not know how to normalize other nodes. return false; } @@ -26,12 +25,13 @@ export function mergeListWithPreviousSiblingList( const [previousSiblingNode] = previousSibling; - if (!isList(options, previousSiblingNode)) { + if (!editor.isListNode(previousSiblingNode)) { return false; } - const isNestedList = Boolean(getParentListItem(options, editor, path)); - const isPreviousSiblingSameListType = previousSiblingNode.type === node.type; + const isNestedList = Boolean(getParentListItem(editor, path)); + const isPreviousSiblingSameListType = + getListType(editor, previousSiblingNode) === getListType(editor, node); if (!isPreviousSiblingSameListType && !isNestedList) { // If previous sibling "list" is of a different type, then this fix does not apply diff --git a/packages/slate-lists/src/lib/moveListItemsToAnotherList.ts b/packages/slate-lists/src/lib/moveListItemsToAnotherList.ts index 5995748aa..9fafef283 100644 --- a/packages/slate-lists/src/lib/moveListItemsToAnotherList.ts +++ b/packages/slate-lists/src/lib/moveListItemsToAnotherList.ts @@ -1,16 +1,13 @@ -import type { Editor, Node, NodeEntry } from 'slate'; -import { Transforms } from 'slate'; +import type { Node, NodeEntry } from 'slate'; +import { Element, Transforms } from 'slate'; -import type { ListsOptions } from '../types'; - -import { isList } from './isList'; +import type { ListsEditor } from '../types'; /** * Moves all "list-items" from one "list" to the end of another "list". */ export function moveListItemsToAnotherList( - options: ListsOptions, - editor: Editor, + editor: ListsEditor, parameters: { at: NodeEntry; to: NodeEntry; @@ -19,15 +16,18 @@ export function moveListItemsToAnotherList( const [sourceListNode, sourceListPath] = parameters.at; const [targetListNode, targetListPath] = parameters.to; - if (!isList(options, sourceListNode) || !isList(options, targetListNode)) { + if ( + Element.isElement(sourceListNode) && + Element.isElement(targetListNode) && + editor.isListNode(sourceListNode) && + editor.isListNode(targetListNode) + ) { // Sanity check. - return; - } - - for (let i = 0; i < sourceListNode.children.length; ++i) { - Transforms.moveNodes(editor, { - at: [...sourceListPath, 0], - to: [...targetListPath, targetListNode.children.length + i], - }); + for (let i = 0; i < sourceListNode.children.length; ++i) { + Transforms.moveNodes(editor, { + at: [...sourceListPath, 0], + to: [...targetListPath, targetListNode.children.length + i], + }); + } } } diff --git a/packages/slate-lists/src/lib/moveListToListItem.ts b/packages/slate-lists/src/lib/moveListToListItem.ts index 627c2c4e9..7c783cd9d 100644 --- a/packages/slate-lists/src/lib/moveListToListItem.ts +++ b/packages/slate-lists/src/lib/moveListToListItem.ts @@ -1,18 +1,14 @@ -import type { Editor, Node, NodeEntry } from 'slate'; +import type { Node, NodeEntry } from 'slate'; import { Transforms } from 'slate'; import { NESTED_LIST_PATH_INDEX } from '../constants'; -import type { ListsOptions } from '../types'; - -import { isList } from './isList'; -import { isListItem } from './isListItem'; +import type { ListsEditor } from '../types'; /** * Nests (moves) given "list" in a given "list-item". */ export function moveListToListItem( - options: ListsOptions, - editor: Editor, + editor: ListsEditor, parameters: { at: NodeEntry; to: NodeEntry; @@ -21,7 +17,7 @@ export function moveListToListItem( const [sourceListNode, sourceListPath] = parameters.at; const [targetListNode, targetListPath] = parameters.to; - if (!isList(options, sourceListNode) || !isListItem(options, targetListNode)) { + if (!editor.isListNode(sourceListNode) || !editor.isListItemNode(targetListNode)) { // Sanity check. return; } diff --git a/packages/slate-lists/src/lib/normalizeListItemChildren.ts b/packages/slate-lists/src/lib/normalizeListItemChildren.ts deleted file mode 100644 index c8bc74636..000000000 --- a/packages/slate-lists/src/lib/normalizeListItemChildren.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { Editor, NodeEntry } from 'slate'; -import { Element, Node, Text, Transforms } from 'slate'; - -import type { ListsOptions } from '../types'; - -import { createListItem } from './createListItem'; -import { createListItemText } from './createListItemText'; -import { isList } from './isList'; -import { isListItem } from './isListItem'; -import { isListItemText } from './isListItemText'; - -/** - * A "list-item" can have a single "list-item-text" and optionally an extra "list" as a child. - */ -export function normalizeListItemChildren( - options: ListsOptions, - editor: Editor, - [node, path]: NodeEntry, -): boolean { - if (!isListItem(options, node)) { - // This function does not know how to normalize other nodes. - return false; - } - - const children = Array.from(Node.children(editor, path)); - - for (let childIndex = 0; childIndex < children.length; ++childIndex) { - const [childNode, childPath] = children[childIndex]; - - if (Text.isText(childNode) || editor.isInline(childNode)) { - const listItemText = createListItemText(options, [childNode]); - Transforms.wrapNodes(editor, listItemText, { at: childPath }); - - if (childIndex > 0) { - const [previousChildNode] = children[childIndex - 1]; - - if (isListItemText(options, previousChildNode)) { - Transforms.mergeNodes(editor, { at: childPath }); - } - } - - return true; - } - - // Casting `as Element` here, because of TypeScript incorrectly complaining that `childNode` - // is of type `never`, even though we just checked if it's an `Element`. - if (Element.isElement(childNode) && typeof (childNode as Element).type === 'undefined') { - // It can happen during pasting that the `type` attribute will be missing. - Transforms.setNodes( - editor, - { type: options.listItemTextType as Element['type'] }, - { at: childPath }, - ); - return true; - } - - if (isListItem(options, childNode)) { - Transforms.liftNodes(editor, { at: childPath }); - return true; - } - - if (isListItemText(options, childNode) && childIndex !== 0) { - Transforms.wrapNodes(editor, createListItem(options), { at: childPath }); - return true; - } - - if (!isListItemText(options, childNode) && !isList(options, childNode)) { - Transforms.setNodes( - editor, - { type: options.listItemTextType as Element['type'] }, - { at: childPath }, - ); - return true; - } - } - - return false; -} diff --git a/packages/slate-lists/src/lib/normalizeSiblingLists.ts b/packages/slate-lists/src/lib/normalizeSiblingLists.ts deleted file mode 100644 index 37de3f408..000000000 --- a/packages/slate-lists/src/lib/normalizeSiblingLists.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { EditorCommands } from '@prezly/slate-commons'; -import type { Editor, Node, NodeEntry } from 'slate'; - -import type { ListsOptions } from '../types'; - -import { mergeListWithPreviousSiblingList } from './mergeListWithPreviousSiblingList'; - -/** - * If there are 2 "lists" of the same type next to each other, merge them together. - * If there are 2 nested "lists" next to each other, merge them together. - */ -export function normalizeSiblingLists( - options: ListsOptions, - editor: Editor, - entry: NodeEntry, -): boolean { - const normalized = mergeListWithPreviousSiblingList(options, editor, entry); - - if (normalized) { - return true; - } - - const [, path] = entry; - const nextSibling = EditorCommands.getNextSibling(editor, path); - - if (!nextSibling) { - return false; - } - - return mergeListWithPreviousSiblingList(options, editor, nextSibling); -} diff --git a/packages/slate-lists/src/lib/setListType.test.tsx b/packages/slate-lists/src/lib/setListType.test.tsx index 12cb2e9ad..0cfc58cce 100644 --- a/packages/slate-lists/src/lib/setListType.test.tsx +++ b/packages/slate-lists/src/lib/setListType.test.tsx @@ -1,38 +1,48 @@ /** @jsx jsx */ -import { NUMBERED_LIST_NODE_TYPE } from '@prezly/slate-types'; -import type { Editor } from 'slate'; +import { + jsx, + Editor, + OrderedList, + UnorderedList, + ListItem, + ListItemText, + Text, + Anchor, + Focus, +} from '../jsx'; +import type { ListsEditor } from '../types'; +import { ListType } from '../types'; -import { jsx } from '../jsx'; -import { createListsEditor, lists } from '../test-utils'; +import { setListType } from './setListType'; describe('setListType - no selection', () => { it('Does nothing when there is no selection', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - ) as unknown as Editor; + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; - lists.setListType(editor, NUMBERED_LIST_NODE_TYPE); + setListType(editor, ListType.ORDERED); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -41,135 +51,135 @@ describe('setListType - no selection', () => { describe('setListType - selection with paragraphs and lists of multiple types', () => { it('Changes lists types in selection', () => { - const editor = createListsEditor( - - - - - Nested Lists A - - - - - Nested Lists A1 - - - - - Nested Lists A1a - - - - - Nested Lists A1b - - - - - - - + const editor = ( + + + + + Nested Lists A + + + + + Nested Lists A1 + + + + + Nested Lists A1a + + + + + Nested Lists A1b + + + + + + + Nested - Lists A2 - - - - - - Nested Lists A2a - - - - - Nested Lists A2b - - - - - - - + Lists A2 + + + + + + Nested Lists A2a + + + + + Nested Lists A2b + + + + + + + Nested - Lists A3 - - - - - - - - Nested Lists B - - - - , - ); + Lists A3 + + + + + + + + Nested Lists B + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - Nested Lists A - - - - - Nested Lists A1 - - - - - Nested Lists A1a - - - - - Nested Lists A1b - - - - - - - + + + + + Nested Lists A + + + + + Nested Lists A1 + + + + + Nested Lists A1a + + + + + Nested Lists A1b + + + + + + + Nested - Lists A2 - - - - - - Nested Lists A2a - - - - - Nested Lists A2b - - - - - - - + Lists A2 + + + + + + Nested Lists A2a + + + + + Nested Lists A2b + + + + + + + Nested - Lists A3 - - - - - - - - Nested Lists B - - - - - ) as unknown as Editor; + Lists A3 + + + + + + + + Nested Lists B + + + + + ) as unknown as ListsEditor; - lists.setListType(editor, NUMBERED_LIST_NODE_TYPE); + setListType(editor, ListType.ORDERED); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); diff --git a/packages/slate-lists/src/lib/setListType.ts b/packages/slate-lists/src/lib/setListType.ts index c3b3bcd66..56e0df634 100644 --- a/packages/slate-lists/src/lib/setListType.ts +++ b/packages/slate-lists/src/lib/setListType.ts @@ -1,20 +1,19 @@ import { nodeIdManager } from '@prezly/slate-commons'; -import type { Editor, Element } from 'slate'; import { Transforms } from 'slate'; -import type { ListsOptions } from '../types'; +import type { ListType, ListsEditor } from '../types'; import { getListsInRange } from './getListsInRange'; /** * Sets "type" of all "list" nodes in the current selection. */ -export function setListType(options: ListsOptions, editor: Editor, listType: string): void { +export function setListType(editor: ListsEditor, listType: ListType): void { if (!editor.selection) { return; } - const lists = getListsInRange(options, editor, editor.selection); + const lists = getListsInRange(editor, editor.selection); const listsIds = lists.map((list) => nodeIdManager.assign(editor, list)); listsIds.forEach((id) => { @@ -27,6 +26,6 @@ export function setListType(options: ListsOptions, editor: Editor, listType: str } const [, listPath] = listEntry; - Transforms.setNodes(editor, { type: listType as Element['type'] }, { at: listPath }); + Transforms.setNodes(editor, editor.createListNode(listType), { at: listPath }); }); } diff --git a/packages/slate-lists/src/lib/splitListItem.test.tsx b/packages/slate-lists/src/lib/splitListItem.test.tsx index 818ae1f47..3c784f0b6 100644 --- a/packages/slate-lists/src/lib/splitListItem.test.tsx +++ b/packages/slate-lists/src/lib/splitListItem.test.tsx @@ -1,80 +1,92 @@ /** @jsx jsx */ -import type { Editor } from 'slate'; - -import { jsx } from '../jsx'; -import { createListsEditor, lists } from '../test-utils'; +import { + jsx, + Editor, + UnorderedList, + OrderedList, + ListItem, + ListItemText, + Text, + Paragraph, + Cursor, + Anchor, + Focus, +} from '../jsx'; +import type { ListsEditor } from '../types'; + +import { splitListItem } from './splitListItem'; describe('splitListItem - no selected items', () => { it('Does nothing when there is no selection', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - ) as unknown as Editor; - - lists.splitListItem(editor); + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; + + splitListItem(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Does nothing when there are no list items in selection', () => { - const editor = createListsEditor( - - - + const editor = ( + + + lorem - - - - - - - lorem ipsum - - - - , - ); + + + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; const expected = ( - - - + + + lorem - - - - - - - lorem ipsum - - - - - ) as unknown as Editor; - - lists.splitListItem(editor); + + + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; + + splitListItem(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -83,126 +95,126 @@ describe('splitListItem - no selected items', () => { describe('splitListItem - collapsed selection', () => { it('Creates new empty sibling list item when cursor is at the end of an item', () => { - const editor = createListsEditor( - - - - - + const editor = ( + + + + + lorem ipsum - - - - - - , - ); + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - - - - - - - - ) as unknown as Editor; - - lists.splitListItem(editor); + + + + + lorem ipsum + + + + + + + + + + + + ) as unknown as ListsEditor; + + splitListItem(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Creates new empty sibling list item when cursor is at the beginning of an item', () => { - const editor = createListsEditor( - - - - - - + const editor = ( + + + + + + lorem ipsum - - - - - , - ); + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - - - - - - - + + + + + + + + + + + lorem ipsum - - - - - - ) as unknown as Editor; + + + + + + ) as unknown as ListsEditor; - lists.splitListItem(editor); + splitListItem(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Creates a new sibling list item when cursor is in the middle of an item', () => { - const editor = createListsEditor( - - - - - + const editor = ( + + + + + lorem - + ipsum - - - - - , - ); + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem - - - - - - + + + + + lorem + + + + + + ipsum - - - - - - ) as unknown as Editor; + + + + + + ) as unknown as ListsEditor; - lists.splitListItem(editor); + splitListItem(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -211,198 +223,198 @@ describe('splitListItem - collapsed selection', () => { describe('splitListItem - collapsed selection - nested lists', () => { it('Creates new sibling list item in nested list when cursor is at the end of an item', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - + const editor = ( + + + + + lorem ipsum + + + + + lorem ipsum - - - - - - - - - dolor sit - - - - , - ); + + + + + + + + + dolor sit + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - lorem ipsum - - - - - - - - - - - - - - dolor sit - - - - - ) as unknown as Editor; - - lists.splitListItem(editor); + + + + + lorem ipsum + + + + + lorem ipsum + + + + + + + + + + + + + + dolor sit + + + + + ) as unknown as ListsEditor; + + splitListItem(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Creates new sibling list item in nested list when cursor is at the beginning of an item', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - - + const editor = ( + + + + + lorem ipsum + + + + + + lorem ipsum - - - - - - - - dolor sit - - - - , - ); + + + + + + + + dolor sit + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - - - - - - - + + + + + lorem ipsum + + + + + + + + + + + lorem ipsum - - - - - - - - dolor sit - - - - - ) as unknown as Editor; - - lists.splitListItem(editor); + + + + + + + + dolor sit + + + + + ) as unknown as ListsEditor; + + splitListItem(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Creates new sibling list item in nested list when cursor is in the middle of an item', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - + const editor = ( + + + + + lorem ipsum + + + + + lorem - + ipsum - - - - - - - - dolor sit - - - - , - ); + + + + + + + + dolor sit + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - lorem - - - - - - + + + + + lorem ipsum + + + + + lorem + + + + + + ipsum - - - - - - - - dolor sit - - - - - ) as unknown as Editor; - - lists.splitListItem(editor); + + + + + + + + dolor sit + + + + + ) as unknown as ListsEditor; + + splitListItem(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -411,79 +423,79 @@ describe('splitListItem - collapsed selection - nested lists', () => { describe('splitListItem - collapsed selection - deeply nested lists', () => { it('Creates new sibling list item in nested list when cursor is at the end of an item with nested list', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - + const editor = ( + + + + + lorem ipsum + + + + + lorem ipsum - - - - - - - lorem - - - - - ipsum - - - - - - - - , - ); + + + + + + + lorem + + + + + ipsum + + + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - lorem ipsum - - - - - - - - - - - - lorem - - - - - ipsum - - - - - - - - - ) as unknown as Editor; - - lists.splitListItem(editor); + + + + + lorem ipsum + + + + + lorem ipsum + + + + + + + + + + + + lorem + + + + + ipsum + + + + + + + + + ) as unknown as ListsEditor; + + splitListItem(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -492,89 +504,89 @@ describe('splitListItem - collapsed selection - deeply nested lists', () => { describe('splitListItem - expanded selection - deeply nested lists', () => { it('Removes selected text, creates new sibling list item in nested list when cursor is at the end of an item with nested list', () => { - // it's an interesting case because Transforms.delete will break in half, - // leaving as its only child which will be normalized by withLists - const editor = createListsEditor( - - - - - lorem ipsum - - - - - - + // it's an interesting case because Transforms.delete will break in half, + // leaving as its only child which will be normalized by withLists + const editor = ( + + + + + lorem ipsum + + + + + + lorem ipsum - - - - - - + + + + + + lorem ipsum - - - - - - - lorem - - - - - ipsum - - - - - - - - , - ); + + + + + + + lorem + + + + + ipsum + + + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - - - - - - - - - - - - - lorem - - - - - ipsum - - - - - - - - - ) as unknown as Editor; - - lists.splitListItem(editor); + + + + + lorem ipsum + + + + + + + + + + + + + + + + + lorem + + + + + ipsum + + + + + + + + + ) as unknown as ListsEditor; + + splitListItem(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); diff --git a/packages/slate-lists/src/lib/splitListItem.ts b/packages/slate-lists/src/lib/splitListItem.ts index fba6ed95f..f8526108c 100644 --- a/packages/slate-lists/src/lib/splitListItem.ts +++ b/packages/slate-lists/src/lib/splitListItem.ts @@ -2,10 +2,8 @@ import { EditorCommands } from '@prezly/slate-commons'; import { Editor, Node, Path, Range, Transforms } from 'slate'; import { NESTED_LIST_PATH_INDEX, TEXT_PATH_INDEX } from '../constants'; -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; -import { createListItem } from './createListItem'; -import { createListItemText } from './createListItemText'; import { getListItemsInRange } from './getListItemsInRange'; /** @@ -13,7 +11,7 @@ import { getListItemsInRange } from './getListItemsInRange'; * ends up in a "list-item" node, it will break that "list-item" into 2 nodes, splitting * the text at the cursor location. */ -export function splitListItem(options: ListsOptions, editor: Editor): void { +export function splitListItem(editor: ListsEditor): void { if (!editor.selection) { return; } @@ -23,7 +21,7 @@ export function splitListItem(options: ListsOptions, editor: Editor): void { Transforms.delete(editor); } - const listItemsInSelection = getListItemsInRange(options, editor, editor.selection); + const listItemsInSelection = getListItemsInRange(editor, editor.selection); if (listItemsInSelection.length !== 1) { // Selection is collapsed, so there should be either 0 or 1 "list-item" in selection. @@ -42,7 +40,9 @@ export function splitListItem(options: ListsOptions, editor: Editor): void { ); if (isStart) { - const newListItem = createListItem(options, [createListItemText(options)]); + const newListItem = editor.createListItemNode({ + children: [editor.createListItemTextNode()], + }); Transforms.insertNodes(editor, newListItem, { at: listItemPath }); return; } @@ -53,7 +53,9 @@ export function splitListItem(options: ListsOptions, editor: Editor): void { Editor.withoutNormalizing(editor, () => { if (isEnd) { - const newListItem = createListItem(options, [createListItemText(options)]); + const newListItem = editor.createListItemNode({ + children: [editor.createListItemTextNode()], + }); Transforms.insertNodes(editor, newListItem, { at: newListItemPath }); // Move the cursor to the new "list-item". Transforms.select(editor, newListItemPath); @@ -62,7 +64,7 @@ export function splitListItem(options: ListsOptions, editor: Editor): void { Transforms.splitNodes(editor); // The current "list-item-text" has a parent "list-item", the new one needs its own. - Transforms.wrapNodes(editor, createListItem(options), { at: newListItemTextPath }); + Transforms.wrapNodes(editor, editor.createListItemNode(), { at: newListItemTextPath }); // Move the new "list-item" up to be a sibling of the original "list-item". Transforms.moveNodes(editor, { diff --git a/packages/slate-lists/src/lib/unwrapList.test.tsx b/packages/slate-lists/src/lib/unwrapList.test.tsx index 338e27b12..f4ff8af3f 100644 --- a/packages/slate-lists/src/lib/unwrapList.test.tsx +++ b/packages/slate-lists/src/lib/unwrapList.test.tsx @@ -1,37 +1,47 @@ /** @jsx jsx */ -import type { Editor } from 'slate'; - -import { jsx } from '../jsx'; -import { createListsEditor, lists } from '../test-utils'; +import { + jsx, + Editor, + OrderedList, + UnorderedList, + ListItem, + ListItemText, + Text, + Paragraph, + Cursor, +} from '../jsx'; +import type { ListsEditor } from '../types'; + +import { unwrapList } from './unwrapList'; describe('unwrapList - no selection', () => { it('Does nothing when there is no selection', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - ) as unknown as Editor; - - lists.unwrapList(editor); + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; + + unwrapList(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -40,181 +50,181 @@ describe('unwrapList - no selection', () => { describe('unwrapList - selection within item', () => { it('Converts only list item into a paragraph', () => { - const editor = createListsEditor( - - - - - + const editor = ( + + + + + lorem - ipsum - - - - - , - ); + ipsum + + + + + + ) as unknown as ListsEditor; const expected = ( - - - + + + lorem - ipsum - - - - ) as unknown as Editor; + ipsum + + + + ) as unknown as ListsEditor; - lists.unwrapList(editor); + unwrapList(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Converts middle list item into a paragraph', () => { - const editor = createListsEditor( - - - - - dolor - - - - - + const editor = ( + + + + + dolor + + + + + lorem - ipsum - - - - - - sit - - - - , - ); + ipsum + + + + + + sit + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - dolor - - - - - + + + + + dolor + + + + + lorem - ipsum - - - - - - sit - - - - - ) as unknown as Editor; - - lists.unwrapList(editor); + ipsum + + + + + + sit + + + + + ) as unknown as ListsEditor; + + unwrapList(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); }); it('Converts nested middle list item into a paragraph', () => { - const editor = createListsEditor( - - - - - lorem - - - - - ipsum - - - - - lorem - - - - - + const editor = ( + + + + + lorem + + + + + ipsum + + + + + lorem + + + + + ipsum - - - - - - - dolor - - - - - - - dolor - - - - , - ); + + + + + + + dolor + + + + + + + dolor + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem - - - - - ipsum - - - - - lorem - - - - - - - + + + + + lorem + + + + + ipsum + + + + + lorem + + + + + + + ipsum - - - - - - - dolor - - - - - dolor - - - - - ) as unknown as Editor; - - lists.unwrapList(editor); + + + + + + + dolor + + + + + dolor + + + + + ) as unknown as ListsEditor; + + unwrapList(editor); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); diff --git a/packages/slate-lists/src/lib/unwrapList.ts b/packages/slate-lists/src/lib/unwrapList.ts index e82416f88..51c17e272 100644 --- a/packages/slate-lists/src/lib/unwrapList.ts +++ b/packages/slate-lists/src/lib/unwrapList.ts @@ -1,6 +1,4 @@ -import type { Editor } from 'slate'; - -import type { ListsOptions } from '../types'; +import type { ListsEditor } from '../types'; import { decreaseDepth } from './decreaseDepth'; import { getListItemsInRange } from './getListItemsInRange'; @@ -9,8 +7,8 @@ import { getListItemsInRange } from './getListItemsInRange'; * Unwraps all "list-items" in the current selection. * No list be left in the current selection. */ -export function unwrapList(options: ListsOptions, editor: Editor): void { - while (getListItemsInRange(options, editor, editor.selection).length > 0) { - decreaseDepth(options, editor); +export function unwrapList(editor: ListsEditor): void { + while (getListItemsInRange(editor, editor.selection).length > 0) { + decreaseDepth(editor); } } diff --git a/packages/slate-lists/src/lib/wrapInList.test.tsx b/packages/slate-lists/src/lib/wrapInList.test.tsx index 0f10cba7b..eb477dafb 100644 --- a/packages/slate-lists/src/lib/wrapInList.test.tsx +++ b/packages/slate-lists/src/lib/wrapInList.test.tsx @@ -1,50 +1,61 @@ /** @jsx jsx */ -import { BULLETED_LIST_NODE_TYPE } from '@prezly/slate-types'; -import type { Editor } from 'slate'; - -import { jsx } from '../jsx'; -import { createListsEditor, lists } from '../test-utils'; +import { + Anchor, + Divider, + Editor, + Focus, + jsx, + ListItem, + ListItemText, + Paragraph, + Text, + UnorderedList, +} from '../jsx'; +import type { ListsEditor } from '../types'; +import { ListType } from '../types'; + +import { wrapInList } from './wrapInList'; describe('wrapInList - no selection', () => { it('Does nothing when there is no selection', () => { - const editor = createListsEditor( - - - aaa - - - - - lorem ipsum - - - - - bbb - - , - ); + const editor = ( + + + aaa + + + + + lorem ipsum + + + + + bbb + + + ) as unknown as ListsEditor; const expected = ( - - - aaa - - - - - lorem ipsum - - - - - bbb - - - ) as unknown as Editor; - - lists.wrapInList(editor, BULLETED_LIST_NODE_TYPE); + + + aaa + + + + + lorem ipsum + + + + + bbb + + + ) as unknown as ListsEditor; + + wrapInList(editor, ListType.UNORDERED); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -53,35 +64,35 @@ describe('wrapInList - no selection', () => { describe('wrapInList - selection with wrappable nodes', () => { it('Converts wrappable node into list', () => { - const editor = createListsEditor( - - - - + const editor = ( + + + + aaa - - - - , - ); + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - - + + + + + + aaa - - - - - - - ) as unknown as Editor; + + + + + + + ) as unknown as ListsEditor; - lists.wrapInList(editor, BULLETED_LIST_NODE_TYPE); + wrapInList(editor, ListType.UNORDERED); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -90,59 +101,59 @@ describe('wrapInList - selection with wrappable nodes', () => { describe('wrapInList - selection with lists and wrappable nodes', () => { it('Converts wrappable nodes into lists items and merges them together', () => { - const editor = createListsEditor( - - - - + const editor = ( + + + + aaa - - - - - - lorem ipsum - - - - - + + + + + + lorem ipsum + + + + + bbb - - - - , - ); + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - - + + + + + + aaa - - - - - - lorem ipsum - - - - - + + + + + + lorem ipsum + + + + + bbb - - - - - - - ) as unknown as Editor; + + + + + + + ) as unknown as ListsEditor; - lists.wrapInList(editor, BULLETED_LIST_NODE_TYPE); + wrapInList(editor, ListType.UNORDERED); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); @@ -151,53 +162,53 @@ describe('wrapInList - selection with lists and wrappable nodes', () => { describe('wrapInList - selection with lists, wrappable & unwrappable nodes', () => { it('Converts wrappable nodes into lists items and merges them together, but leaves out unwrappable nodes', () => { - const editor = createListsEditor( - - - - + const editor = ( + + + + aaa - - - - bbb - - - + + + + bbb + + + ccc - - - - , - ); + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - - + + + + + + aaa - - - - - - bbb - - - - - + + + + + + bbb + + + + + ccc - - - - - ) as unknown as Editor; + + + + + ) as unknown as ListsEditor; - lists.wrapInList(editor, BULLETED_LIST_NODE_TYPE); + wrapInList(editor, ListType.UNORDERED); expect(editor.children).toEqual(expected.children); expect(editor.selection).toEqual(expected.selection); diff --git a/packages/slate-lists/src/lib/wrapInList.ts b/packages/slate-lists/src/lib/wrapInList.ts index 807693985..5dd9d9860 100644 --- a/packages/slate-lists/src/lib/wrapInList.ts +++ b/packages/slate-lists/src/lib/wrapInList.ts @@ -1,34 +1,31 @@ import { nodeIdManager } from '@prezly/slate-commons'; import { Editor, Element, Transforms } from 'slate'; -import type { ListsOptions } from '../types'; - -import { createList } from './createList'; -import { createListItem } from './createListItem'; +import type { ListsEditor } from '../types'; +import type { ListType } from '../types'; /** - * All nodes matching options.wrappableTypes in the current selection - * will be converted to "list-items" and wrapped in "lists". + * All nodes matching `isConvertibleToListTextNode()` in the current selection + * will be converted to list items and then wrapped in lists. + * + * @see ListsEditor.isConvertibleToListTextNode() */ -export function wrapInList(options: ListsOptions, editor: Editor, listType: string): void { +export function wrapInList(editor: ListsEditor, listType: ListType): void { if (!editor.selection) { return; } - const listNodeTypes = [...options.listTypes, options.listItemType, options.listItemTextType]; const nonListEntries = Array.from( Editor.nodes(editor, { at: editor.selection, match: (node) => { - if (!Element.isElement(node)) { - return false; - } - - if (listNodeTypes.includes(node.type)) { - return false; - } - - return options.wrappableTypes.includes(node.type); + return ( + Element.isElement(node) && + !editor.isListNode(node) && + !editor.isListItemNode(node) && + !editor.isListItemTextNode(node) && + editor.isConvertibleToListTextNode(node) + ); }, }), ); @@ -48,13 +45,9 @@ export function wrapInList(options: ListsOptions, editor: Editor, listType: stri const [, nonListEntryPath] = nonListEntry; Editor.withoutNormalizing(editor, () => { - Transforms.setNodes( - editor, - { type: options.listItemTextType as Element['type'] }, - { at: nonListEntryPath }, - ); - Transforms.wrapNodes(editor, createListItem(options), { at: nonListEntryPath }); - Transforms.wrapNodes(editor, createList(listType), { at: nonListEntryPath }); + Transforms.setNodes(editor, editor.createListItemTextNode(), { at: nonListEntryPath }); + Transforms.wrapNodes(editor, editor.createListItemNode(), { at: nonListEntryPath }); + Transforms.wrapNodes(editor, editor.createListNode(listType), { at: nonListEntryPath }); }); }); } diff --git a/packages/slate-lists/src/normalizations/index.ts b/packages/slate-lists/src/normalizations/index.ts new file mode 100644 index 000000000..d1907b68f --- /dev/null +++ b/packages/slate-lists/src/normalizations/index.ts @@ -0,0 +1,8 @@ +export { normalizeList } from './normalizeList'; +export { normalizeListChildren } from './normalizeListChildren'; +export { normalizeListItemChildren } from './normalizeListItemChildren'; +export { normalizeListItemTextChildren } from './normalizeListItemTextChildren'; +export { normalizeOrphanListItem } from './normalizeOrphanListItem'; +export { normalizeOrphanListItemText } from './normalizeOrphanListItemText'; +export { normalizeOrphanNestedList } from './normalizeOrphanNestedList'; +export { normalizeSiblingLists } from './normalizeSiblingLists'; diff --git a/packages/slate-lists/src/lib/normalizeList.ts b/packages/slate-lists/src/normalizations/normalizeList.ts similarity index 68% rename from packages/slate-lists/src/lib/normalizeList.ts rename to packages/slate-lists/src/normalizations/normalizeList.ts index bd688ca40..01fae8b55 100644 --- a/packages/slate-lists/src/lib/normalizeList.ts +++ b/packages/slate-lists/src/normalizations/normalizeList.ts @@ -1,22 +1,15 @@ import { EditorCommands } from '@prezly/slate-commons'; -import { isElementNode } from '@prezly/slate-types'; import type { Node, NodeEntry } from 'slate'; import { Editor, Element, Transforms } from 'slate'; -import type { ListsOptions } from '../types'; - -import { isList } from './isList'; +import type { ListsEditor } from '../types'; /** * A "list" can have no parent (be at the root) or have a "list-item" parent (nested list). * In any other case we will try to unwrap it, or lift it up. */ -export function normalizeList( - options: ListsOptions, - editor: Editor, - [node, path]: NodeEntry, -): boolean { - if (!isList(options, node)) { +export function normalizeList(editor: ListsEditor, [node, path]: NodeEntry): boolean { + if (!editor.isListNode(node)) { // This function does not know how to normalize other nodes. return false; } @@ -33,10 +26,7 @@ export function normalizeList( return false; } - if ( - isElementNode(ancestorNode) && - [...options.listTypes, options.listItemType].includes(ancestorNode.type) - ) { + if (editor.isListNode(ancestorNode) || editor.isListItemNode(ancestorNode)) { return false; } diff --git a/packages/slate-lists/src/lib/normalizeListChildren.ts b/packages/slate-lists/src/normalizations/normalizeListChildren.ts similarity index 63% rename from packages/slate-lists/src/lib/normalizeListChildren.ts rename to packages/slate-lists/src/normalizations/normalizeListChildren.ts index 0a35b8f19..0b41ef13e 100644 --- a/packages/slate-lists/src/lib/normalizeListChildren.ts +++ b/packages/slate-lists/src/normalizations/normalizeListChildren.ts @@ -1,25 +1,15 @@ -import type { Editor, NodeEntry } from 'slate'; +import type { NodeEntry } from 'slate'; import { Element, Node, Text, Transforms } from 'slate'; -import type { ListsOptions } from '../types'; - -import { createListItem } from './createListItem'; -import { createListItemText } from './createListItemText'; -import { isList } from './isList'; -import { isListItem } from './isListItem'; -import { isListItemText } from './isListItemText'; +import type { ListsEditor } from '../types'; /** * All children of a "list" have to be "list-items". It can happen (e.g. during pasting) that * this will not be true, so we have to convert all non-"list-item" children of a "list" * into "list-items". */ -export function normalizeListChildren( - options: ListsOptions, - editor: Editor, - [node, path]: NodeEntry, -): boolean { - if (!isList(options, node)) { +export function normalizeListChildren(editor: ListsEditor, [node, path]: NodeEntry): boolean { + if (!editor.isListNode(node)) { // This function does not know how to normalize other nodes. return false; } @@ -53,7 +43,9 @@ export function normalizeListChildren( Transforms.wrapNodes( editor, - createListItem(options, [createListItemText(options, [childNode])]), + editor.createListItemNode({ + children: [editor.createListItemTextNode({ children: [childNode] })], + }), { at: childPath }, ); normalized = true; @@ -64,26 +56,22 @@ export function normalizeListChildren( return; } - if (isListItemText(options, childNode)) { - Transforms.wrapNodes(editor, createListItem(options), { at: childPath }); + if (editor.isListItemTextNode(childNode)) { + Transforms.wrapNodes(editor, editor.createListItemNode(), { at: childPath }); normalized = true; return; } - if (isList(options, childNode)) { + if (editor.isListNode(childNode)) { // Wrap it into a list item so that `normalizeOrphanNestedList` can take care of it. - Transforms.wrapNodes(editor, createListItem(options), { at: childPath }); + Transforms.wrapNodes(editor, editor.createListItemNode(), { at: childPath }); normalized = true; return; } - if (!isListItem(options, childNode)) { - Transforms.setNodes( - editor, - { type: options.listItemTextType as Element['type'] }, - { at: childPath }, - ); - Transforms.wrapNodes(editor, createListItem(options), { at: childPath }); + if (!editor.isListItemNode(childNode)) { + Transforms.setNodes(editor, editor.createListItemTextNode(), { at: childPath }); + Transforms.wrapNodes(editor, editor.createListItemNode(), { at: childPath }); normalized = true; } }); diff --git a/packages/slate-lists/src/normalizations/normalizeListItemChildren.ts b/packages/slate-lists/src/normalizations/normalizeListItemChildren.ts new file mode 100644 index 000000000..81ab15a19 --- /dev/null +++ b/packages/slate-lists/src/normalizations/normalizeListItemChildren.ts @@ -0,0 +1,57 @@ +import type { NodeEntry } from 'slate'; +import { Node, Text, Transforms } from 'slate'; + +import type { ListsEditor } from '../types'; + +/** + * A "list-item" can have a single "list-item-text" and optionally an extra "list" as a child. + */ +export function normalizeListItemChildren( + editor: ListsEditor, + [node, path]: NodeEntry, +): boolean { + if (!editor.isListItemNode(node)) { + // This function does not know how to normalize other nodes. + return false; + } + + const children = Array.from(Node.children(editor, path)); + + for (let childIndex = 0; childIndex < children.length; ++childIndex) { + const [childNode, childPath] = children[childIndex]; + + if (Text.isText(childNode) || editor.isInline(childNode)) { + const listItemText = editor.createListItemTextNode({ + children: [childNode], + }); + Transforms.wrapNodes(editor, listItemText, { at: childPath }); + + if (childIndex > 0) { + const [previousChildNode] = children[childIndex - 1]; + + if (editor.isListItemTextNode(previousChildNode)) { + Transforms.mergeNodes(editor, { at: childPath }); + } + } + + return true; + } + + if (editor.isListItemNode(childNode)) { + Transforms.liftNodes(editor, { at: childPath }); + return true; + } + + if (editor.isListItemTextNode(childNode) && childIndex !== 0) { + Transforms.wrapNodes(editor, editor.createListItemNode(), { at: childPath }); + return true; + } + + if (!editor.isListItemTextNode(childNode) && !editor.isListNode(childNode)) { + Transforms.setNodes(editor, editor.createListItemTextNode(), { at: childPath }); + return true; + } + } + + return false; +} diff --git a/packages/slate-lists/src/lib/normalizeListItemTextChildren.ts b/packages/slate-lists/src/normalizations/normalizeListItemTextChildren.ts similarity index 77% rename from packages/slate-lists/src/lib/normalizeListItemTextChildren.ts rename to packages/slate-lists/src/normalizations/normalizeListItemTextChildren.ts index 01062f0b3..9befe46e5 100644 --- a/packages/slate-lists/src/lib/normalizeListItemTextChildren.ts +++ b/packages/slate-lists/src/normalizations/normalizeListItemTextChildren.ts @@ -1,19 +1,16 @@ import type { NodeEntry } from 'slate'; import { Editor, Element, Node, Transforms } from 'slate'; -import type { ListsOptions } from '../types'; - -import { isListItemText } from './isListItemText'; +import type { ListsEditor } from '../types'; /** * A "list-item-text" can have only inline nodes in it. */ export function normalizeListItemTextChildren( - options: ListsOptions, - editor: Editor, + editor: ListsEditor, [node, path]: NodeEntry, ): boolean { - if (!isListItemText(options, node)) { + if (!editor.isListItemTextNode(node)) { // This function does not know how to normalize other nodes. return false; } diff --git a/packages/slate-lists/src/lib/normalizeOrphanListItem.ts b/packages/slate-lists/src/normalizations/normalizeOrphanListItem.ts similarity index 63% rename from packages/slate-lists/src/lib/normalizeOrphanListItem.ts rename to packages/slate-lists/src/normalizations/normalizeOrphanListItem.ts index c21594dac..a762f277a 100644 --- a/packages/slate-lists/src/lib/normalizeOrphanListItem.ts +++ b/packages/slate-lists/src/normalizations/normalizeOrphanListItem.ts @@ -1,10 +1,8 @@ -import type { Element, Node, NodeEntry } from 'slate'; +import type { Node, NodeEntry } from 'slate'; import { Editor, Transforms } from 'slate'; -import type { ListsOptions } from '../types'; - -import { getParentList } from './getParentList'; -import { isListItem } from './isListItem'; +import { getParentList } from '../lib'; +import type { ListsEditor } from '../types'; /** * If "list-item" somehow (e.g. by deleting everything around it) ends up @@ -15,16 +13,15 @@ import { isListItem } from './isListItem'; * pasting, so we have a separate rule for that in `deserializeHtml`. */ export function normalizeOrphanListItem( - options: ListsOptions, - editor: Editor, + editor: ListsEditor, [node, path]: NodeEntry, ): boolean { - if (!isListItem(options, node)) { + if (!editor.isListItemNode(node)) { // This function does not know how to normalize other nodes. return false; } - const parentList = getParentList(options, editor, path); + const parentList = getParentList(editor, path); if (parentList) { // If there is a parent "list", then the fix does not apply. @@ -33,11 +30,7 @@ export function normalizeOrphanListItem( Editor.withoutNormalizing(editor, () => { Transforms.unwrapNodes(editor, { at: path }); - Transforms.setNodes( - editor, - { type: options.defaultBlockType as Element['type'] }, - { at: path }, - ); + Transforms.setNodes(editor, editor.createDefaultTextNode(), { at: path }); }); return true; diff --git a/packages/slate-lists/src/lib/normalizeOrphanListItemText.ts b/packages/slate-lists/src/normalizations/normalizeOrphanListItemText.ts similarity index 60% rename from packages/slate-lists/src/lib/normalizeOrphanListItemText.ts rename to packages/slate-lists/src/normalizations/normalizeOrphanListItemText.ts index a9432692f..88f8b3aa7 100644 --- a/packages/slate-lists/src/lib/normalizeOrphanListItemText.ts +++ b/packages/slate-lists/src/normalizations/normalizeOrphanListItemText.ts @@ -1,10 +1,8 @@ -import type { Editor, Element, Node, NodeEntry } from 'slate'; +import type { Node, NodeEntry } from 'slate'; import { Transforms } from 'slate'; -import type { ListsOptions } from '../types'; - -import { getParentListItem } from './getParentListItem'; -import { isListItemText } from './isListItemText'; +import { getParentListItem } from '../lib'; +import type { ListsEditor } from '../types'; /** * If "list-item-text" somehow (e.g. by deleting everything around it) ends up @@ -15,27 +13,22 @@ import { isListItemText } from './isListItemText'; * pasting, so we have a separate rule for that in `deserializeHtml`. */ export function normalizeOrphanListItemText( - options: ListsOptions, - editor: Editor, + editor: ListsEditor, [node, path]: NodeEntry, ): boolean { - if (!isListItemText(options, node)) { + if (!editor.isListItemTextNode(node)) { // This function does not know how to normalize other nodes. return false; } - const parentListItem = getParentListItem(options, editor, path); + const parentListItem = getParentListItem(editor, path); if (parentListItem) { // If there is a parent "list-item", then the fix does not apply. return false; } - Transforms.setNodes( - editor, - { type: options.defaultBlockType as Element['type'] }, - { at: path }, - ); + Transforms.setNodes(editor, editor.createDefaultTextNode(), { at: path }); return true; } diff --git a/packages/slate-lists/src/lib/normalizeOrphanNestedList.ts b/packages/slate-lists/src/normalizations/normalizeOrphanNestedList.ts similarity index 68% rename from packages/slate-lists/src/lib/normalizeOrphanNestedList.ts rename to packages/slate-lists/src/normalizations/normalizeOrphanNestedList.ts index ac3cf8b8f..8fcfa699b 100644 --- a/packages/slate-lists/src/lib/normalizeOrphanNestedList.ts +++ b/packages/slate-lists/src/normalizations/normalizeOrphanNestedList.ts @@ -1,25 +1,19 @@ import { EditorCommands } from '@prezly/slate-commons'; -import type { Editor, NodeEntry } from 'slate'; +import type { NodeEntry } from 'slate'; import { Node, Transforms } from 'slate'; -import type { ListsOptions } from '../types'; - -import { getNestedList } from './getNestedList'; -import { isList } from './isList'; -import { isListItem } from './isListItem'; -import { moveListItemsToAnotherList } from './moveListItemsToAnotherList'; -import { moveListToListItem } from './moveListToListItem'; +import { getNestedList, moveListItemsToAnotherList, moveListToListItem } from '../lib'; +import type { ListsEditor } from '../types'; /** * If there is a nested "list" inside a "list-item" without a "list-item-text" * unwrap that nested "list" and try to nest it in previous sibling "list-item". */ export function normalizeOrphanNestedList( - options: ListsOptions, - editor: Editor, + editor: ListsEditor, [node, path]: NodeEntry, ): boolean { - if (!isListItem(options, node)) { + if (!editor.isListItemNode(node)) { // This function does not know how to normalize other nodes. return false; } @@ -33,7 +27,7 @@ export function normalizeOrphanNestedList( const [list] = children; const [listNode, listPath] = list; - if (!isList(options, listNode)) { + if (!editor.isListNode(listNode)) { // If the first child is not a "list", then this fix does not apply. return false; } @@ -42,15 +36,15 @@ export function normalizeOrphanNestedList( if (previousListItem) { const [, previousListItemPath] = previousListItem; - const previousListItemNestedList = getNestedList(options, editor, previousListItemPath); + const previousListItemNestedList = getNestedList(editor, previousListItemPath); if (previousListItemNestedList) { - moveListItemsToAnotherList(options, editor, { + moveListItemsToAnotherList(editor, { at: list, to: previousListItemNestedList, }); } else { - moveListToListItem(options, editor, { + moveListToListItem(editor, { at: list, to: previousListItem, }); diff --git a/packages/slate-lists/src/normalizations/normalizeSiblingLists.ts b/packages/slate-lists/src/normalizations/normalizeSiblingLists.ts new file mode 100644 index 000000000..a010b0f0b --- /dev/null +++ b/packages/slate-lists/src/normalizations/normalizeSiblingLists.ts @@ -0,0 +1,26 @@ +import { EditorCommands } from '@prezly/slate-commons'; +import type { Node, NodeEntry } from 'slate'; + +import { mergeListWithPreviousSiblingList } from '../lib'; +import type { ListsEditor } from '../types'; + +/** + * If there are 2 "lists" of the same type next to each other, merge them together. + * If there are 2 nested "lists" next to each other, merge them together. + */ +export function normalizeSiblingLists(editor: ListsEditor, entry: NodeEntry): boolean { + const normalized = mergeListWithPreviousSiblingList(editor, entry); + + if (normalized) { + return true; + } + + const [, path] = entry; + const nextSibling = EditorCommands.getNextSibling(editor, path); + + if (!nextSibling) { + return false; + } + + return mergeListWithPreviousSiblingList(editor, nextSibling); +} diff --git a/packages/slate-lists/src/onKeyDown.ts b/packages/slate-lists/src/onKeyDown.ts new file mode 100644 index 000000000..0006fcd2d --- /dev/null +++ b/packages/slate-lists/src/onKeyDown.ts @@ -0,0 +1,57 @@ +import { isHotkey } from 'is-hotkey'; +import type { KeyboardEvent } from 'react'; +import { Editor } from 'slate'; +import { ReactEditor } from 'slate-react'; + +import { + canDeleteBackward, + decreaseDepth, + getListItemsInRange, + increaseDepth, + isCursorInEmptyListItem, + splitListItem, +} from './lib'; +import type { ListsEditor } from './types'; + +export function onKeyDown(editor: ListsEditor & ReactEditor, event: KeyboardEvent) { + const listItemsInSelection = getListItemsInRange(editor, editor.selection); + + // Since we're overriding the default Tab key behavior + // we need to bring back the possibility to blur the editor + // with keyboard. + if (isHotkey('esc', event.nativeEvent)) { + event.preventDefault(); + ReactEditor.blur(editor); + } + + if (isHotkey('tab', event.nativeEvent)) { + event.preventDefault(); + increaseDepth(editor); + } + + if (isHotkey('shift+tab', event.nativeEvent)) { + event.preventDefault(); + decreaseDepth(editor); + } + + if (isHotkey('backspace', event.nativeEvent) && !canDeleteBackward(editor)) { + event.preventDefault(); + decreaseDepth(editor); + } + + if (isHotkey('enter', event.nativeEvent)) { + if (isCursorInEmptyListItem(editor)) { + event.preventDefault(); + decreaseDepth(editor); + } else if (listItemsInSelection.length > 0) { + event.preventDefault(); + splitListItem(editor); + } + } + + // Slate does not always trigger normalization when one would expect it to. + // So we want to force it after we perform lists operations, as it fixes + // many unexpected behaviors. + // https://github.com/ianstormtaylor/slate/issues/3758 + Editor.normalize(editor, { force: true }); +} diff --git a/packages/slate-lists/src/test-utils.ts b/packages/slate-lists/src/test-utils.ts deleted file mode 100644 index f71b48faf..000000000 --- a/packages/slate-lists/src/test-utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable no-param-reassign */ - -import { - BULLETED_LIST_NODE_TYPE, - DIVIDER_NODE_TYPE, - LINK_NODE_TYPE, - LIST_ITEM_NODE_TYPE, - LIST_ITEM_TEXT_NODE_TYPE, - NUMBERED_LIST_NODE_TYPE, - PARAGRAPH_NODE_TYPE, - isElementNode, -} from '@prezly/slate-types'; -import type { Editor } from 'slate'; - -import { Lists } from './Lists'; -import type { ListsOptions } from './types'; -import { withLists } from './withLists'; - -export const INLINE_ELEMENT = LINK_NODE_TYPE; - -export const UNWRAPPABLE_ELEMENT = DIVIDER_NODE_TYPE; - -export const options: ListsOptions = { - defaultBlockType: PARAGRAPH_NODE_TYPE, - listItemTextType: LIST_ITEM_TEXT_NODE_TYPE, - listItemType: LIST_ITEM_NODE_TYPE, - listTypes: [BULLETED_LIST_NODE_TYPE, NUMBERED_LIST_NODE_TYPE], - wrappableTypes: [PARAGRAPH_NODE_TYPE], -}; - -export const lists = Lists(options); - -function withInlineElement(editor: T): T { - const { isInline } = editor; - - editor.isInline = (element) => { - if (isElementNode(element, INLINE_ELEMENT)) { - return true; - } - return isInline(element); - }; - - return editor; -} - -export function createListsEditor(input: JSX.Element) { - return withInlineElement(withLists(options)(input as unknown as Editor)); -} diff --git a/packages/slate-lists/src/types.ts b/packages/slate-lists/src/types.ts index 02de2bda1..df1d3a275 100644 --- a/packages/slate-lists/src/types.ts +++ b/packages/slate-lists/src/types.ts @@ -1,28 +1,28 @@ -import type { ElementNode } from '@prezly/slate-types'; - -export interface ListsOptions { - /** - * Type of the node that "listItemTextType" will become when it is unwrapped or normalized. - */ - defaultBlockType: ElementNode['type']; - - /** - * Type of the node representing list item text. - */ - listItemTextType: ElementNode['type']; - - /** - * Type of the node representing list item. - */ - listItemType: ElementNode['type']; - - /** - * Types of nodes representing lists. The first type will be the default type (e.g. when wrapping with lists). - */ - listTypes: ElementNode['type'][]; - - /** - * Types of nodes that can be converted into a node representing list item text. - */ - wrappableTypes: ElementNode['type'][]; +import type { BaseEditor, Descendant, Element, Node } from 'slate'; + +export enum ListType { + ORDERED = 'ol', + UNORDERED = 'ul', +} + +export interface ListsSchema { + isConvertibleToListTextNode(node: Node): boolean; + + isDefaultTextNode(node: Node): boolean; + + isListNode(node: Node, type?: ListType): boolean; + + isListItemNode(node: Node): boolean; + + isListItemTextNode(node: Node): boolean; + + createDefaultTextNode(props?: { children?: Descendant[] }): Element; + + createListNode(type?: ListType, props?: { children?: Descendant[] }): Element; + + createListItemNode(props?: { children?: Descendant[] }): Element; + + createListItemTextNode(props?: { children?: Descendant[] }): Element; } + +export interface ListsEditor extends ListsSchema, BaseEditor {} diff --git a/packages/slate-lists/src/util/index.ts b/packages/slate-lists/src/util/index.ts new file mode 100644 index 000000000..eb9ac0bbd --- /dev/null +++ b/packages/slate-lists/src/util/index.ts @@ -0,0 +1,2 @@ +export { patchRangeCloneContents } from './patchRangeCloneContents'; +export { withRangeCloneContentsPatched } from './withRangeCloneContentsPatched'; diff --git a/packages/slate-lists/src/util/patchRangeCloneContents.ts b/packages/slate-lists/src/util/patchRangeCloneContents.ts new file mode 100644 index 000000000..2f56a3fdc --- /dev/null +++ b/packages/slate-lists/src/util/patchRangeCloneContents.ts @@ -0,0 +1,62 @@ +function wrapInFragment(nodes: (string | Node)[]): DocumentFragment { + const fragment = document.createDocumentFragment(); + fragment.append(...nodes); + return fragment; +} + +function wrapInList(nodes: (string | Node)[], nodeName: 'OL' | 'UL'): HTMLElement { + const listElement = document.createElement(nodeName); + listElement.append(...nodes); + return listElement; +} + +function wrapInLi(nodes: (string | Node)[]): HTMLElement { + const listItemElement = document.createElement('li'); + listItemElement.append(...nodes); + return listItemElement; +} + +/** + * Activates `Range.prototype.cloneContents` override that ensures in the cloned contents: + * - there are no
      • children elements without parent
      • element + * - there are no
      • elements without parent
          or
            elements + */ +export function patchRangeCloneContents() { + const originalCloneContents = Range.prototype.cloneContents; + + Range.prototype.cloneContents = function cloneContents(): DocumentFragment { + const contents = originalCloneContents.apply(this); + + if ( + this.commonAncestorContainer.nodeName === 'OL' || + this.commonAncestorContainer.nodeName === 'UL' + ) { + return wrapInFragment([ + wrapInList([...contents.childNodes], this.commonAncestorContainer.nodeName), + ]); + } + + if ( + this.commonAncestorContainer.nodeName === 'LI' && + this.commonAncestorContainer.parentElement && + (this.commonAncestorContainer.parentElement.nodeName === 'OL' || + this.commonAncestorContainer.parentElement.nodeName === 'UL') + ) { + return wrapInFragment([ + wrapInList( + [wrapInLi([...contents.childNodes])], + this.commonAncestorContainer.parentElement.nodeName, + ), + ]); + } + + return contents; + }; + + /** + * Brings back the original `Range.prototype.cloneContents`. + */ + return function undo() { + Range.prototype.cloneContents = originalCloneContents; + }; +} diff --git a/packages/slate-lists/src/util/withRangeCloneContentsPatched.ts b/packages/slate-lists/src/util/withRangeCloneContentsPatched.ts new file mode 100644 index 000000000..f5bcc1a5d --- /dev/null +++ b/packages/slate-lists/src/util/withRangeCloneContentsPatched.ts @@ -0,0 +1,10 @@ +import { patchRangeCloneContents } from './patchRangeCloneContents'; + +export function withRangeCloneContentsPatched(callback: () => void) { + const undo = patchRangeCloneContents(); + try { + callback(); + } finally { + undo(); + } +} diff --git a/packages/slate-lists/src/withLists.test.tsx b/packages/slate-lists/src/withLists.test.tsx index 0162840a9..f7dafdd64 100644 --- a/packages/slate-lists/src/withLists.test.tsx +++ b/packages/slate-lists/src/withLists.test.tsx @@ -1,89 +1,100 @@ /** @jsx jsx */ -import { Editor } from 'slate'; - -import { jsx } from './jsx'; -import { createListsEditor } from './test-utils'; +import { Editor as Slate } from 'slate'; + +import { + jsx, + Editor, + OrderedList, + UnorderedList, + ListItem, + ListItemText, + Text, + Paragraph, + Link, + Untyped, +} from './jsx'; +import type { ListsEditor } from './types'; describe('withLists - normalizeListChildren', () => { it('Converts paragraph into list-item when it is a child of a list', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - dolor - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + dolor + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - dolor - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem ipsum + + + + + dolor + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); it('Wraps list in list-item when it is a child of a list', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - - lorem ipsum - - - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + + + lorem ipsum + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - lorem ipsum - - - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem ipsum + + + + + lorem ipsum + + + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); @@ -91,247 +102,247 @@ describe('withLists - normalizeListChildren', () => { describe('withLists - normalizeListItemChildren', () => { it('Lifts up list-items when they are children of list-item', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - dolor - - - - - sit - - - - - amet - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + dolor + + + + + sit + + + + + amet + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - dolor - - - - - sit - - - - - amet - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem ipsum + + + + + dolor + + + + + sit + + + + + amet + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); it('Normalizes paragraph children of list items', () => { - const editor = createListsEditor( - - - - - - lorem - - - - - - ipsum - - - - , - ); + const editor = ( + + + + + + lorem + + + + + + ipsum + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem - - - - - ipsum - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem + + + + + ipsum + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); it('Wraps extra list-item-text in list-item and lifts it up when it is a child of list-item', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - dolor sit - - - - amet - - - , - ); + const editor = ( + + + + + lorem ipsum + + + dolor sit + + + + amet + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - dolor sit - - - - - amet - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem ipsum + + + + + dolor sit + + + + + amet + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); it('Wraps inline list-item children in list-item-text', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - - - lorem ipsum - - - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + + + lorem ipsum + + + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); it('Wraps inline list-item children and sibling texts in list-item-text', () => { - const editor = createListsEditor( - - - - lorem - - ipsum - - dolor - - - , - ); + const editor = ( + + + + lorem + + ipsum + + dolor + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem - - ipsum - - dolor - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem + + ipsum + + dolor + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); it('Adds missing type attribute to block list-item children', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); @@ -339,47 +350,47 @@ describe('withLists - normalizeListItemChildren', () => { describe('withLists - normalizeListItemTextChildren', () => { it('Unwraps block children of list-item-text elements', () => { - const editor = createListsEditor( - - - - - - lorem ipsum - - - - - - - - dolor sit amet - - - - - - , - ); + const editor = ( + + + + + + lorem ipsum + + + + + + + + dolor sit amet + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - dolor sit amet - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem ipsum + + + + + dolor sit amet + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); @@ -387,33 +398,33 @@ describe('withLists - normalizeListItemTextChildren', () => { describe('withLists - normalizeOrphanListItem', () => { it('Converts orphan list-item into paragraph', () => { - const editor = createListsEditor( - - - - lorem ipsum - - - - - dolor sit - - - , - ); + const editor = ( + + + + lorem ipsum + + + + + dolor sit + + + + ) as unknown as ListsEditor; const expected = ( - - - lorem ipsum - - - dolor sit - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + lorem ipsum + + + dolor sit + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); @@ -421,29 +432,29 @@ describe('withLists - normalizeOrphanListItem', () => { describe('withLists - normalizeOrphanListItemText', () => { it('Converts orphan list-item-text into paragraph', () => { - const editor = createListsEditor( - - - lorem ipsum - - - dolor sit - - , - ); + const editor = ( + + + lorem ipsum + + + dolor sit + + + ) as unknown as ListsEditor; const expected = ( - - - lorem ipsum - - - dolor sit - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + lorem ipsum + + + dolor sit + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); @@ -451,149 +462,149 @@ describe('withLists - normalizeOrphanListItemText', () => { describe('withLists - normalizeOrphanNestedList', () => { it('Unwraps the nested list when it does not have sibling list-item-text', () => { - const editor = createListsEditor( - - - - - - - lorem ipsum - - - - - - , - ); + const editor = ( + + + + + + + lorem ipsum + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem ipsum + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); it("Moves items from nested list to previous list-item's nested list when it does not have sibling list-item-text", () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - aaa - - - - - bbb - - - - - - - - - lorem ipsum - - - - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + + aaa + + + + + bbb + + + + + + + + + lorem ipsum + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - aaa - - - - - bbb - - - - - lorem ipsum - - - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem ipsum + + + + + aaa + + + + + bbb + + + + + lorem ipsum + + + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); it('Moves nested list to previous list item when it does not have sibling list-item-text', () => { - const editor = createListsEditor( - - - - - lorem ipsum - - - - - - - lorem ipsum - - - - - - , - ); + const editor = ( + + + + + lorem ipsum + + + + + + + lorem ipsum + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem ipsum - - - - - lorem ipsum - - - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem ipsum + + + + + lorem ipsum + + + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); @@ -601,105 +612,105 @@ describe('withLists - normalizeOrphanNestedList', () => { describe('withLists - normalizeSiblingLists', () => { it('Merges sibling lists of same type', () => { - const editor = createListsEditor( - - - lorem - - - - - ipsum - - - - - - - - - - - , - ); + const editor = ( + + + lorem + + + + + ipsum + + + + + + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - lorem - - - - - ipsum - - - - - - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + lorem + + + + + ipsum + + + + + + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); it('Merges sibling lists of different types when they are nested lists', () => { - const editor = createListsEditor( - - - - - lorem - - - - - ipsum - - - - - - - dolor - - - - - - , - ); + const editor = ( + + + + + lorem + + + + + ipsum + + + + + + + dolor + + + + + + + ) as unknown as ListsEditor; const expected = ( - - - - - lorem - - - - - ipsum - - - - - dolor - - - - - - - ) as unknown as Editor; - - Editor.normalize(editor, { force: true }); + + + + + lorem + + + + + ipsum + + + + + dolor + + + + + + + ) as unknown as ListsEditor; + + Slate.normalize(editor, { force: true }); expect(editor.children).toEqual(expected.children); }); diff --git a/packages/slate-lists/src/withLists.ts b/packages/slate-lists/src/withLists.ts index d444539e2..1261918ff 100644 --- a/packages/slate-lists/src/withLists.ts +++ b/packages/slate-lists/src/withLists.ts @@ -1,6 +1,4 @@ -/* eslint-disable no-param-reassign */ - -import type { Editor } from 'slate'; +import type { Editor, NodeEntry } from 'slate'; import { normalizeList, @@ -11,10 +9,12 @@ import { normalizeOrphanListItemText, normalizeOrphanNestedList, normalizeSiblingLists, -} from './lib'; -import type { ListsOptions } from './types'; +} from './normalizations'; +import type { ListsEditor, ListsSchema } from './types'; + +type Normalizer = (editor: ListsEditor, entry: NodeEntry) => boolean; -const normalizers = [ +const LIST_NORMALIZERS: Normalizer[] = [ normalizeList, normalizeListChildren, normalizeListItemChildren, @@ -28,22 +28,36 @@ const normalizers = [ /** * Enables normalizations that enforce schema constraints and recover from unsupported cases. */ -export function withLists(options: ListsOptions) { - return function (editor: T): T { - const { normalizeNode } = editor; +export function withLists(schema: ListsSchema) { + return function (editor: T): T & ListsEditor { + const listsEditor: T & ListsEditor = Object.assign(editor, { + isConvertibleToListTextNode: schema.isConvertibleToListTextNode, + isDefaultTextNode: schema.isDefaultTextNode, + isListNode: schema.isListNode, + isListItemNode: schema.isListItemNode, + isListItemTextNode: schema.isListItemTextNode, + createDefaultTextNode: schema.createDefaultTextNode, + createListNode: schema.createListNode, + createListItemNode: schema.createListItemNode, + createListItemTextNode: schema.createListItemTextNode, + }); + + return withNormalizations(listsEditor, LIST_NORMALIZERS); + }; +} - editor.normalizeNode = (entry) => { - for (const normalizer of normalizers) { - const normalized = normalizer(options, editor, entry); +function withNormalizations(editor: T, normalizers: Normalizer[]): T { + const { normalizeNode } = editor; - if (normalized) { - return; - } + editor.normalizeNode = (entry) => { + for (const normalize of normalizers) { + const changed = normalize(editor, entry); + if (changed) { + return; } + } - normalizeNode(entry); - }; - - return editor; + normalizeNode(entry); }; + return editor; } diff --git a/packages/slate-lists/src/withListsReact.ts b/packages/slate-lists/src/withListsReact.ts index 7bb2e3e39..3c5f9f051 100644 --- a/packages/slate-lists/src/withListsReact.ts +++ b/packages/slate-lists/src/withListsReact.ts @@ -1,20 +1,21 @@ /* eslint-disable no-param-reassign */ +import type { Editor } from 'slate'; import type { ReactEditor } from 'slate-react'; -import { cloneContentsMonkeyPatch } from './lib'; +import { withRangeCloneContentsPatched } from './util'; /** * Enables Range.prototype.cloneContents monkey patch to improve pasting behavior * in few edge cases. */ -export function withListsReact(editor: T): T { +export function withListsReact(editor: T): T { const { setFragmentData } = editor; editor.setFragmentData = (data: DataTransfer) => { - cloneContentsMonkeyPatch.activate(); - setFragmentData(data); - cloneContentsMonkeyPatch.deactivate(); + withRangeCloneContentsPatched(function () { + setFragmentData(data); + }); }; return editor; diff --git a/packages/slate-lists/tsconfig.build.json b/packages/slate-lists/tsconfig.build.json index 98945424b..05b0b78c3 100644 --- a/packages/slate-lists/tsconfig.build.json +++ b/packages/slate-lists/tsconfig.build.json @@ -4,7 +4,7 @@ "module": "es6", "target": "es6" }, - "exclude": ["node_modules", "**/*.test.*", "**/jsx.ts", "**/test-utils.ts"], + "exclude": ["node_modules", "**/*.test.*", "**/jsx.ts"], "references": [ { "path": "../slate-types/tsconfig.json" }, { "path": "../slate-commons/tsconfig.build.json" }, diff --git a/packages/slate-lists/tsconfig.json b/packages/slate-lists/tsconfig.json index 0ca67b57e..552557b1f 100644 --- a/packages/slate-lists/tsconfig.json +++ b/packages/slate-lists/tsconfig.json @@ -5,7 +5,6 @@ "emitDeclarationOnly": true, "declarationDir": "./build/types", "rootDir": "./src", - "typeRoots": ["./src/@types"] }, "include": ["./src"], "exclude": ["node_modules"], diff --git a/packages/slate-types/src/nodes/ListNode.ts b/packages/slate-types/src/nodes/ListNode.ts index 42b5d51da..e1029f339 100644 --- a/packages/slate-types/src/nodes/ListNode.ts +++ b/packages/slate-types/src/nodes/ListNode.ts @@ -3,15 +3,14 @@ import { isElementNode } from './ElementNode'; import type { Alignable } from './interfaces'; export const BULLETED_LIST_NODE_TYPE = 'bulleted-list'; - export const NUMBERED_LIST_NODE_TYPE = 'numbered-list'; - export const LIST_ITEM_NODE_TYPE = 'list-item'; - export const LIST_ITEM_TEXT_NODE_TYPE = 'list-item-text'; -export interface ListNode extends ElementNode, Alignable { - type: typeof BULLETED_LIST_NODE_TYPE | typeof NUMBERED_LIST_NODE_TYPE; +type ListType = typeof BULLETED_LIST_NODE_TYPE | typeof NUMBERED_LIST_NODE_TYPE; + +export interface ListNode extends ElementNode, Alignable { + type: T; children: ListItemNode[]; } @@ -24,8 +23,12 @@ export interface ListItemTextNode extends ElementNode { type: typeof LIST_ITEM_TEXT_NODE_TYPE; } -export function isListNode(value: any): value is ListNode { - return isElementNode(value, [BULLETED_LIST_NODE_TYPE, NUMBERED_LIST_NODE_TYPE]); +export function isListNode(value: any): value is ListNode; +export function isListNode(value: any, type: T): value is ListNode; +export function isListNode(value: any, type?: ListType): boolean { + return type + ? isElementNode(value, type) + : isElementNode(value, [BULLETED_LIST_NODE_TYPE, NUMBERED_LIST_NODE_TYPE]); } export function isListItemNode(value: any): value is ListItemNode {