From 059da61605370f8a3534d3c7669acbc952ea40d3 Mon Sep 17 00:00:00 2001 From: Ricardo Amaral Date: Thu, 18 Jan 2024 16:02:37 +0000 Subject: [PATCH] feat(extensions)!: Overwrite built-in List/Ordered toggle functions with a `smartToggle` option (#620) BREAKING CHANGE: The `smartToggleBulletList` and `smartToggleOrderedList` commands were renamed to have the same name as the built-in toggle functions so that they can easily be used by the default keyboard shortcuts without having to change the `addKeyboardShortcuts` function. The `BulletList` and `OrderedList` extensions now take an additional option, `smartToggle` (default: `false`), that indicates whether hard breaks should be replaced by paragraphs before toggling the selection into a bullet/ordered list, or not. --- .../rich-text/rich-text-bullet-list.ts | 99 ++++++++++--------- .../rich-text/rich-text-ordered-list.ts | 99 ++++++++++--------- stories/typist-editor/constants/defaults.ts | 6 ++ .../typist-editor-toolbar.tsx | 4 +- 4 files changed, 110 insertions(+), 98 deletions(-) diff --git a/src/extensions/rich-text/rich-text-bullet-list.ts b/src/extensions/rich-text/rich-text-bullet-list.ts index d6833139..0b9049b3 100644 --- a/src/extensions/rich-text/rich-text-bullet-list.ts +++ b/src/extensions/rich-text/rich-text-bullet-list.ts @@ -6,68 +6,71 @@ import { Fragment, Slice } from '@tiptap/pm/model' import type { BulletListOptions } from '@tiptap/extension-bullet-list' /** - * Augment the official `@tiptap/core` module with extra commands, relevant for this extension, so - * that the compiler knows about them. + * The options available to customize the `RichTextBulletList` extension. */ -declare module '@tiptap/core' { - interface Commands { - richTextBulletList: { - /** - * Smartly toggles the selection into a bullet list, converting any hard breaks into - * paragraphs before doing so. - * - * @see https://discuss.prosemirror.net/t/how-to-convert-a-selection-of-text-lines-into-paragraphs/6099 - */ - smartToggleBulletList: () => ReturnType - } - } -} +type RichTextBulletListOptions = { + /** + * Replace hard breaks in the selection with paragraphs before toggling the selection into a + * bullet list. By default, hard breaks are not replaced. + */ + smartToggle: boolean +} & BulletListOptions /** - * Custom extension that extends the built-in `BulletList` extension to add a smart toggle command - * with support for hard breaks, which are automatically converted into paragraphs before toggling - * the selection into a bullet list. + * Custom extension that extends the built-in `BulletList` extension to add an option for smart + * toggling, which takes into account hard breaks in the selection, and converts them into + * paragraphs before toggling the selection into a bullet list. */ -const RichTextBulletList = BulletList.extend({ +const RichTextBulletList = BulletList.extend({ + addOptions() { + return { + ...this.parent?.(), + smartToggle: false, + } + }, + addCommands() { const { editor, name, options } = this return { ...this.parent?.(), - smartToggleBulletList() { + toggleBulletList() { return ({ commands, state, tr, chain }) => { - const { schema } = state - const { selection } = tr - const { $from, $to } = selection + // Replace hard breaks in the selection with paragraphs before toggling? + if (options.smartToggle) { + const { schema } = state + const { selection } = tr + const { $from, $to } = selection - const hardBreakPositions: number[] = [] + const hardBreakPositions: number[] = [] - // Find and store the positions of all hard breaks in the selection - tr.doc.nodesBetween($from.pos, $to.pos, (node, pos) => { - if (node.type.name === 'hardBreak') { - hardBreakPositions.push(pos) - } - }) + // Find and store the positions of all hard breaks in the selection + tr.doc.nodesBetween($from.pos, $to.pos, (node, pos) => { + if (node.type.name === 'hardBreak') { + hardBreakPositions.push(pos) + } + }) - // Replace each hard break with a slice that closes and re-opens a paragraph, - // effectively inserting a "paragraph break" in place of a "hard break" - // (this is performed in reverse order to compensate for content shifting that - // occurs with each replacement, ensuring accurate insertion points) - hardBreakPositions.reverse().forEach((pos) => { - tr.replace( - pos, - pos + 1, - Slice.maxOpen( - Fragment.fromArray([ - schema.nodes.paragraph.create(), - schema.nodes.paragraph.create(), - ]), - ), - ) - }) + // Replace each hard break with a slice that closes and re-opens a paragraph, + // effectively inserting a "paragraph break" in place of a "hard break" + // (this is performed in reverse order to compensate for content shifting that + // occurs with each replacement, ensuring accurate insertion points) + hardBreakPositions.reverse().forEach((pos) => { + tr.replace( + pos, + pos + 1, + Slice.maxOpen( + Fragment.fromArray([ + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create(), + ]), + ), + ) + }) + } // Toggle the selection into a bullet list, optionally keeping attributes - // (this is a verbatim copy of the built-in`toggleBulletList` command) + // (this is a verbatim copy of the built-in `toggleBulletList` command) if (options.keepAttributes) { return chain() @@ -85,4 +88,4 @@ const RichTextBulletList = BulletList.extend({ export { RichTextBulletList } -export type { BulletListOptions as RichTextBulletListOptions } +export type { RichTextBulletListOptions } diff --git a/src/extensions/rich-text/rich-text-ordered-list.ts b/src/extensions/rich-text/rich-text-ordered-list.ts index 47e12ccc..fcca2df5 100644 --- a/src/extensions/rich-text/rich-text-ordered-list.ts +++ b/src/extensions/rich-text/rich-text-ordered-list.ts @@ -6,68 +6,71 @@ import { Fragment, Slice } from '@tiptap/pm/model' import type { OrderedListOptions } from '@tiptap/extension-ordered-list' /** - * Augment the official `@tiptap/core` module with extra commands, relevant for this extension, so - * that the compiler knows about them. + * The options available to customize the `RichTextOrderedList` extension. */ -declare module '@tiptap/core' { - interface Commands { - richTextOrderedList: { - /** - * Smartly toggles the selection into an oredered list, converting any hard breaks into - * paragraphs before doing so. - * - * @see https://discuss.prosemirror.net/t/how-to-convert-a-selection-of-text-lines-into-paragraphs/6099 - */ - smartToggleOrderedList: () => ReturnType - } - } -} +type RichTextOrderedListOptions = { + /** + * Replace hard breaks in the selection with paragraphs before toggling the selection into a + * bullet list. By default, hard breaks are not replaced. + */ + smartToggle: boolean +} & OrderedListOptions /** - * Custom extension that extends the built-in `OrderedList` extension to add a smart toggle command - * with support for hard breaks, which are automatically converted into paragraphs before toggling - * the selection into an ordered list. + * Custom extension that extends the built-in `OrderedList` extension to add an option for smart + * toggling, which takes into account hard breaks in the selection, and converts them into + * paragraphs before toggling the selection into a bullet list. */ -const RichTextOrderedList = OrderedList.extend({ +const RichTextOrderedList = OrderedList.extend({ + addOptions() { + return { + ...this.parent?.(), + smartToggle: false, + } + }, + addCommands() { const { editor, name, options } = this return { ...this.parent?.(), - smartToggleOrderedList() { + toggleOrderedList() { return ({ commands, state, tr, chain }) => { - const { schema } = state - const { selection } = tr - const { $from, $to } = selection + // Replace hard breaks in the selection with paragraphs before toggling? + if (options.smartToggle) { + const { schema } = state + const { selection } = tr + const { $from, $to } = selection - const hardBreakPositions: number[] = [] + const hardBreakPositions: number[] = [] - // Find and store the positions of all hard breaks in the selection - tr.doc.nodesBetween($from.pos, $to.pos, (node, pos) => { - if (node.type.name === 'hardBreak') { - hardBreakPositions.push(pos) - } - }) + // Find and store the positions of all hard breaks in the selection + tr.doc.nodesBetween($from.pos, $to.pos, (node, pos) => { + if (node.type.name === 'hardBreak') { + hardBreakPositions.push(pos) + } + }) - // Replace each hard break with a slice that closes and re-opens a paragraph, - // effectively inserting a "paragraph break" in place of a "hard break" - // (this is performed in reverse order to compensate for content shifting that - // occurs with each replacement, ensuring accurate insertion points) - hardBreakPositions.reverse().forEach((pos) => { - tr.replace( - pos, - pos + 1, - Slice.maxOpen( - Fragment.fromArray([ - schema.nodes.paragraph.create(), - schema.nodes.paragraph.create(), - ]), - ), - ) - }) + // Replace each hard break with a slice that closes and re-opens a paragraph, + // effectively inserting a "paragraph break" in place of a "hard break" + // (this is performed in reverse order to compensate for content shifting that + // occurs with each replacement, ensuring accurate insertion points) + hardBreakPositions.reverse().forEach((pos) => { + tr.replace( + pos, + pos + 1, + Slice.maxOpen( + Fragment.fromArray([ + schema.nodes.paragraph.create(), + schema.nodes.paragraph.create(), + ]), + ), + ) + }) + } // Toggle the selection into a bullet list, optionally keeping attributes - // (this is a verbatim copy of the built-in`toggleBulletList` command) + // (this is a verbatim copy of the built-in `toggleBulletList` command) if (options.keepAttributes) { return chain() @@ -85,4 +88,4 @@ const RichTextOrderedList = OrderedList.extend({ export { RichTextOrderedList } -export type { OrderedListOptions as RichTextOrderedListOptions } +export type { RichTextOrderedListOptions } diff --git a/stories/typist-editor/constants/defaults.ts b/stories/typist-editor/constants/defaults.ts index 77cf7eb1..db04c960 100644 --- a/stories/typist-editor/constants/defaults.ts +++ b/stories/typist-editor/constants/defaults.ts @@ -116,12 +116,18 @@ const DEFAULT_STORY_ARGS: Partial = { } const DEFAULT_RICH_TEXT_KIT_OPTIONS: Partial = { + bulletList: { + smartToggle: true, + }, dropCursor: { class: 'ProseMirror-dropcursor', }, link: { openOnClick: false, }, + orderedList: { + smartToggle: true, + }, } export { DEFAULT_ARG_TYPES, DEFAULT_RICH_TEXT_KIT_OPTIONS, DEFAULT_STORY_ARGS } diff --git a/stories/typist-editor/decorators/typist-editor-decorator/typist-editor-toolbar.tsx b/stories/typist-editor/decorators/typist-editor-decorator/typist-editor-toolbar.tsx index 8d200f2a..297c9b88 100644 --- a/stories/typist-editor/decorators/typist-editor-decorator/typist-editor-toolbar.tsx +++ b/stories/typist-editor/decorators/typist-editor-decorator/typist-editor-toolbar.tsx @@ -187,7 +187,7 @@ function TypistEditorToolbar({ editor }: TypistEditorToolbarProps) { disabled={false} icon={} variant="quaternary" - onClick={() => editor.chain().focus().smartToggleBulletList().run()} + onClick={() => editor.chain().focus().toggleBulletList().run()} />