Skip to content

Commit

Permalink
feat(extensions)!: Overwrite built-in List/Ordered toggle functions w…
Browse files Browse the repository at this point in the history
…ith 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.
  • Loading branch information
rfgamaral committed Jan 18, 2024
1 parent b91dbb0 commit 059da61
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 98 deletions.
99 changes: 51 additions & 48 deletions src/extensions/rich-text/rich-text-bullet-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
}
}
}
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<RichTextBulletListOptions>({
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()
Expand All @@ -85,4 +88,4 @@ const RichTextBulletList = BulletList.extend({

export { RichTextBulletList }

export type { BulletListOptions as RichTextBulletListOptions }
export type { RichTextBulletListOptions }
99 changes: 51 additions & 48 deletions src/extensions/rich-text/rich-text-ordered-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
}
}
}
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<RichTextOrderedListOptions>({
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()
Expand All @@ -85,4 +88,4 @@ const RichTextOrderedList = OrderedList.extend({

export { RichTextOrderedList }

export type { OrderedListOptions as RichTextOrderedListOptions }
export type { RichTextOrderedListOptions }
6 changes: 6 additions & 0 deletions stories/typist-editor/constants/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,18 @@ const DEFAULT_STORY_ARGS: Partial<TypistEditorProps> = {
}

const DEFAULT_RICH_TEXT_KIT_OPTIONS: Partial<RichTextKitOptions> = {
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 }
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().smartToggleBulletList().run()}
onClick={() => editor.chain().focus().toggleBulletList().run()}
/>
<Button
aria-label="Ordered List"
aria-pressed={editor.isActive('orderedList')}
disabled={false}
icon={<RiListOrdered />}
variant="quaternary"
onClick={() => editor.chain().focus().smartToggleOrderedList().run()}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
/>
<Button
aria-label="Code Block"
Expand Down

0 comments on commit 059da61

Please sign in to comment.