Skip to content

Commit

Permalink
feat(commands): Add smartToggleBulletList and `smartToggleOrderedLi…
Browse files Browse the repository at this point in the history
…st` (#612)
  • Loading branch information
rfgamaral authored Jan 17, 2024
1 parent fa64f59 commit e5dcc8b
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 11 deletions.
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"@tiptap/extension-task-item": "2.1.13",
"@tiptap/extension-task-list": "2.1.13",
"@tiptap/extension-text": "2.1.13",
"@tiptap/extension-text-style": "2.1.13",
"@tiptap/extension-typography": "2.1.13",
"@tiptap/pm": "2.1.13",
"@tiptap/react": "2.1.13",
Expand Down
87 changes: 87 additions & 0 deletions src/extensions/rich-text/rich-text-bullet-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { BulletList } from '@tiptap/extension-bullet-list'
import { ListItem } from '@tiptap/extension-list-item'
import { TextStyle } from '@tiptap/extension-text-style'
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.
*/
declare module '@tiptap/core' {
interface Commands<ReturnType> {
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
}
}
}

/**
* 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.
*/
const RichTextBulletList = BulletList.extend({
addCommands() {
const { editor, name, options } = this

return {
smartToggleBulletList() {
return ({ commands, state, tr, chain }) => {
const { schema } = state
const { selection } = tr
const { $from, $to } = selection

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)
}
})

// 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)

if (options.keepAttributes) {
return chain()
.toggleList(name, options.itemTypeName, options.keepMarks)
.updateAttributes(ListItem.name, editor.getAttributes(TextStyle.name))
.run()
}

return commands.toggleList(name, options.itemTypeName, options.keepMarks)
}
},
}
},
})

export { RichTextBulletList }

export type { BulletListOptions as RichTextBulletListOptions }
19 changes: 10 additions & 9 deletions src/extensions/rich-text/rich-text-kit.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Extension } from '@tiptap/core'
import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold'
import { BulletList } from '@tiptap/extension-bullet-list'
import { CodeBlock } from '@tiptap/extension-code-block'
import { Dropcursor } from '@tiptap/extension-dropcursor'
import { Gapcursor } from '@tiptap/extension-gapcursor'
Expand All @@ -12,7 +11,6 @@ import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
import { Italic } from '@tiptap/extension-italic'
import { ListItem } from '@tiptap/extension-list-item'
import { ListKeymap } from '@tiptap/extension-list-keymap'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Text } from '@tiptap/extension-text'
import { Typography } from '@tiptap/extension-typography'
Expand All @@ -26,16 +24,17 @@ import { BoldAndItalics } from './bold-and-italics'
import { CurvenoteCodemark } from './curvenote-codemark'
import { PasteEmojis } from './paste-emojis'
import { PasteMarkdown } from './paste-markdown'
import { RichTextBulletList } from './rich-text-bullet-list'
import { RichTextCode } from './rich-text-code'
import { RichTextDocument } from './rich-text-document'
import { RichTextImage } from './rich-text-image'
import { RichTextLink } from './rich-text-link'
import { RichTextStrikethrough, RichTextStrikethroughOptions } from './rich-text-strikethrough'
import { RichTextOrderedList } from './rich-text-ordered-list'
import { RichTextStrikethrough } from './rich-text-strikethrough'

import type { Extensions } from '@tiptap/core'
import type { BlockquoteOptions } from '@tiptap/extension-blockquote'
import type { BoldOptions } from '@tiptap/extension-bold'
import type { BulletListOptions } from '@tiptap/extension-bullet-list'
import type { CodeOptions } from '@tiptap/extension-code'
import type { CodeBlockOptions } from '@tiptap/extension-code-block'
import type { DropcursorOptions } from '@tiptap/extension-dropcursor'
Expand All @@ -46,11 +45,13 @@ import type { HorizontalRuleOptions } from '@tiptap/extension-horizontal-rule'
import type { ItalicOptions } from '@tiptap/extension-italic'
import type { ListItemOptions } from '@tiptap/extension-list-item'
import type { ListKeymapOptions } from '@tiptap/extension-list-keymap'
import type { OrderedListOptions } from '@tiptap/extension-ordered-list'
import type { ParagraphOptions } from '@tiptap/extension-paragraph'
import type { RichTextBulletListOptions } from './rich-text-bullet-list'
import type { RichTextDocumentOptions } from './rich-text-document'
import type { RichTextImageOptions } from './rich-text-image'
import type { RichTextLinkOptions } from './rich-text-link'
import type { RichTextOrderedListOptions } from './rich-text-ordered-list'
import type { RichTextStrikethroughOptions } from './rich-text-strikethrough'

/**
* The options available to customize the `RichTextKit` extension.
Expand All @@ -69,7 +70,7 @@ type RichTextKitOptions = {
/**
* Set options for the `BulletList` extension, or `false` to disable.
*/
bulletList: Partial<BulletListOptions> | false
bulletList: Partial<RichTextBulletListOptions> | false

/**
* Set options for the `Code` extension, or `false` to disable.
Expand Down Expand Up @@ -144,7 +145,7 @@ type RichTextKitOptions = {
/**
* Set options for the `OrderedList` extension, or `false` to disable.
*/
orderedList: Partial<OrderedListOptions> | false
orderedList: Partial<RichTextOrderedListOptions> | false

/**
* Set options for the `Paragraph` extension, or `false` to disable.
Expand Down Expand Up @@ -210,7 +211,7 @@ const RichTextKit = Extension.create<RichTextKitOptions>({
}

if (this.options.bulletList !== false) {
extensions.push(BulletList.configure(this.options?.bulletList))
extensions.push(RichTextBulletList.configure(this.options?.bulletList))
}

if (this.options.code !== false) {
Expand Down Expand Up @@ -310,7 +311,7 @@ const RichTextKit = Extension.create<RichTextKitOptions>({
}

if (this.options.orderedList !== false) {
extensions.push(OrderedList.configure(this.options?.orderedList))
extensions.push(RichTextOrderedList.configure(this.options?.orderedList))
}

if (this.options.paragraph !== false) {
Expand Down
87 changes: 87 additions & 0 deletions src/extensions/rich-text/rich-text-ordered-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ListItem } from '@tiptap/extension-list-item'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { TextStyle } from '@tiptap/extension-text-style'
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.
*/
declare module '@tiptap/core' {
interface Commands<ReturnType> {
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
}
}
}

/**
* 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.
*/
const RichTextOrderedList = OrderedList.extend({
addCommands() {
const { editor, name, options } = this

return {
smartToggleOrderedList() {
return ({ commands, state, tr, chain }) => {
const { schema } = state
const { selection } = tr
const { $from, $to } = selection

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)
}
})

// 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)

if (options.keepAttributes) {
return chain()
.toggleList(name, options.itemTypeName, options.keepMarks)
.updateAttributes(ListItem.name, editor.getAttributes(TextStyle.name))
.run()
}

return commands.toggleList(name, options.itemTypeName, options.keepMarks)
}
},
}
},
})

export { RichTextOrderedList }

export type { OrderedListOptions as RichTextOrderedListOptions }
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,15 @@ function TypistEditorToolbar({ editor }: TypistEditorToolbarProps) {
disabled={false}
icon={<RiListUnordered />}
variant="quaternary"
onClick={() => editor.chain().focus().toggleBulletList().run()}
onClick={() => editor.chain().focus().smartToggleBulletList().run()}
/>
<Button
aria-label="Ordered List"
aria-pressed={editor.isActive('orderedList')}
disabled={false}
icon={<RiListOrdered />}
variant="quaternary"
onClick={() => editor.chain().focus().toggleOrderedList().run()}
onClick={() => editor.chain().focus().smartToggleOrderedList().run()}
/>
<Button
aria-label="Code Block"
Expand Down

0 comments on commit e5dcc8b

Please sign in to comment.