Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(extensions)!: Overwrite built-in List/Ordered toggle functions with a smartToggle option #620

Merged
merged 1 commit into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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