diff --git a/demos/src/Examples/Default/React/index.jsx b/demos/src/Examples/Default/React/index.jsx index 42c168adb72..ef74b7b0d69 100644 --- a/demos/src/Examples/Default/React/index.jsx +++ b/demos/src/Examples/Default/React/index.jsx @@ -1,5 +1,8 @@ import './styles.scss' +import { Color } from '@tiptap/extension-color' +import ListItem from '@tiptap/extension-list-item' +import TextStyle from '@tiptap/extension-text-style' import { EditorContent, useEditor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import React from 'react' @@ -165,6 +168,12 @@ const MenuBar = ({ editor }) => { > redo + ) } @@ -172,7 +181,18 @@ const MenuBar = ({ editor }) => { export default () => { const editor = useEditor({ extensions: [ - StarterKit, + Color.configure({ types: [TextStyle.name, ListItem.name] }), + TextStyle.configure({ types: [ListItem.name] }), + StarterKit.configure({ + bulletList: { + keepMarks: true, + keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help + }, + orderedList: { + keepMarks: true, + keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help + }, + }), ], content: `

diff --git a/docs/api/nodes/bullet-list.md b/docs/api/nodes/bullet-list.md index 652bd27c917..b829f3dc1d0 100644 --- a/docs/api/nodes/bullet-list.md +++ b/docs/api/nodes/bullet-list.md @@ -41,6 +41,27 @@ BulletList.configure({ itemTypeName: 'listItem', }) ``` +### keepMarks +Decides whether to keep the marks from a previous line after toggling the list either using `inputRule` or using the button + +Default: `false` + +```js +BulletList.configure({ + keepMarks: true, +}) +``` + +### keepAttributes +Decides whether to keep the attributes from a previous line after toggling the list either using `inputRule` or using the button + +Default: `false` + +```js +BulletList.configure({ + keepAttributes: true, +}) +``` ## Commands diff --git a/docs/api/nodes/ordered-list.md b/docs/api/nodes/ordered-list.md index 7fab529f152..b665813f4b0 100644 --- a/docs/api/nodes/ordered-list.md +++ b/docs/api/nodes/ordered-list.md @@ -42,6 +42,27 @@ OrderedList.configure({ }) ``` +### keepMarks +Decides whether to keep the marks from a previous line after toggling the list either using `inputRule` or using the button + +Default: `false` + +```js +OrderedList.configure({ + keepMarks: true, +}) +``` +### keepAttributes +Decides whether to keep the attributes from a previous line after toggling the list either using `inputRule` or using the button + +Default: `false` + +```js +OrderedList.configure({ + keepAttributes: true, +}) +``` + ## Commands ### toggleOrderedList() diff --git a/packages/core/src/commands/splitListItem.ts b/packages/core/src/commands/splitListItem.ts index 00148942c77..8952bc4df86 100644 --- a/packages/core/src/commands/splitListItem.ts +++ b/packages/core/src/commands/splitListItem.ts @@ -130,7 +130,19 @@ export const splitListItem: RawCommands['splitListItem'] = typeOrName => ({ } if (dispatch) { + const { selection, storedMarks } = state + const { splittableMarks } = editor.extensionManager + const marks = storedMarks || (selection.$to.parentOffset && selection.$from.marks()) + tr.split($from.pos, 2, types).scrollIntoView() + + if (!marks || !dispatch) { + return true + } + + const filteredMarks = marks.filter(mark => splittableMarks.includes(mark.type.name)) + + tr.ensureMarks(filteredMarks) } return true diff --git a/packages/core/src/commands/toggleList.ts b/packages/core/src/commands/toggleList.ts index 23da8576021..4aad12c081e 100644 --- a/packages/core/src/commands/toggleList.ts +++ b/packages/core/src/commands/toggleList.ts @@ -63,24 +63,23 @@ declare module '@tiptap/core' { /** * Toggle between different list types. */ - toggleList: ( - listTypeOrName: string | NodeType, - itemTypeOrName: string | NodeType, - ) => ReturnType + toggleList: (listTypeOrName: string | NodeType, itemTypeOrName: string | NodeType, keepMarks?: boolean) => ReturnType; } } } -export const toggleList: RawCommands['toggleList'] = (listTypeOrName, itemTypeOrName) => ({ +export const toggleList: RawCommands['toggleList'] = (listTypeOrName, itemTypeOrName, keepMarks) => ({ editor, tr, state, dispatch, chain, commands, can, }) => { - const { extensions } = editor.extensionManager + const { extensions, splittableMarks } = editor.extensionManager const listType = getNodeType(listTypeOrName, state.schema) const itemType = getNodeType(itemTypeOrName, state.schema) - const { selection } = state + const { selection, storedMarks } = state const { $from, $to } = selection const range = $from.blockRange($to) + const marks = storedMarks || (selection.$to.parentOffset && selection.$from.marks()) + if (!range) { return false } @@ -110,6 +109,24 @@ export const toggleList: RawCommands['toggleList'] = (listTypeOrName, itemTypeOr .run() } } + if (!keepMarks || !marks || !dispatch) { + + return chain() + // try to convert node to default node if needed + .command(() => { + const canWrapInList = can().wrapInList(listType) + + if (canWrapInList) { + return true + } + + return commands.clearNodes() + }) + .wrapInList(listType) + .command(() => joinListBackwards(tr, listType)) + .command(() => joinListForwards(tr, listType)) + .run() + } return ( chain() @@ -117,6 +134,10 @@ export const toggleList: RawCommands['toggleList'] = (listTypeOrName, itemTypeOr .command(() => { const canWrapInList = can().wrapInList(listType) + const filteredMarks = marks.filter(mark => splittableMarks.includes(mark.type.name)) + + tr.ensureMarks(filteredMarks) + if (canWrapInList) { return true } diff --git a/packages/core/src/inputRules/wrappingInputRule.ts b/packages/core/src/inputRules/wrappingInputRule.ts index c845a6dbca7..c41504236c2 100644 --- a/packages/core/src/inputRules/wrappingInputRule.ts +++ b/packages/core/src/inputRules/wrappingInputRule.ts @@ -1,6 +1,7 @@ import { Node as ProseMirrorNode, NodeType } from '@tiptap/pm/model' import { canJoin, findWrapping } from '@tiptap/pm/transform' +import { Editor } from '../Editor' import { InputRule, InputRuleFinder } from '../InputRule' import { ExtendedRegExpMatchArray } from '../types' import { callOrReturn } from '../utilities/callOrReturn' @@ -20,18 +21,24 @@ import { callOrReturn } from '../utilities/callOrReturn' * return a boolean to indicate whether a join should happen. */ export function wrappingInputRule(config: { - find: InputRuleFinder - type: NodeType + find: InputRuleFinder, + type: NodeType, + keepMarks?: boolean, + keepAttributes?: boolean, + editor?: Editor getAttributes?: - | Record - | ((match: ExtendedRegExpMatchArray) => Record) - | false - | null - joinPredicate?: (match: ExtendedRegExpMatchArray, node: ProseMirrorNode) => boolean + | Record + | ((match: ExtendedRegExpMatchArray) => Record) + | false + | null + , + joinPredicate?: (match: ExtendedRegExpMatchArray, node: ProseMirrorNode) => boolean, }) { return new InputRule({ find: config.find, - handler: ({ state, range, match }) => { + handler: ({ + state, range, match, chain, + }) => { const attributes = callOrReturn(config.getAttributes, undefined, match) || {} const tr = state.tr.delete(range.from, range.to) const $start = tr.doc.resolve(range.from) @@ -44,6 +51,24 @@ export function wrappingInputRule(config: { tr.wrap(blockRange, wrapping) + if (config.keepMarks && config.editor) { + const { selection, storedMarks } = state + const { splittableMarks } = config.editor.extensionManager + const marks = storedMarks || (selection.$to.parentOffset && selection.$from.marks()) + + if (marks) { + const filteredMarks = marks.filter(mark => splittableMarks.includes(mark.type.name)) + + tr.ensureMarks(filteredMarks) + } + } + if (config.keepAttributes) { + /** If the nodeType is `bulletList` or `orderedList` set the `nodeType` as `listItem` */ + const nodeType = config.type.name === 'bulletList' || config.type.name === 'orderedList' ? 'listItem' : 'taskList' + + chain().updateAttributes(nodeType, attributes).run() + } + const before = tr.doc.resolve(range.from - 1).nodeBefore if ( diff --git a/packages/extension-bullet-list/src/bullet-list.ts b/packages/extension-bullet-list/src/bullet-list.ts index 7c6ab14a988..77c0fa6a371 100644 --- a/packages/extension-bullet-list/src/bullet-list.ts +++ b/packages/extension-bullet-list/src/bullet-list.ts @@ -1,8 +1,13 @@ import { mergeAttributes, Node, wrappingInputRule } from '@tiptap/core' +import ListItem from '../../extension-list-item/src' +import TextStyle from '../../extension-text-style/src' + export interface BulletListOptions { itemTypeName: string, HTMLAttributes: Record, + keepMarks: boolean, + keepAttributes: boolean, } declare module '@tiptap/core' { @@ -25,6 +30,8 @@ export const BulletList = Node.create({ return { itemTypeName: 'listItem', HTMLAttributes: {}, + keepMarks: false, + keepAttributes: false, } }, @@ -46,8 +53,11 @@ export const BulletList = Node.create({ addCommands() { return { - toggleBulletList: () => ({ commands }) => { - return commands.toggleList(this.name, this.options.itemTypeName) + toggleBulletList: () => ({ commands, chain }) => { + if (this.options.keepAttributes) { + return chain().toggleList(this.name, this.options.itemTypeName, this.options.keepMarks).updateAttributes(ListItem.name, this.editor.getAttributes(TextStyle.name)).run() + } + return commands.toggleList(this.name, this.options.itemTypeName, this.options.keepMarks) }, } }, @@ -59,11 +69,23 @@ export const BulletList = Node.create({ }, addInputRules() { - return [ - wrappingInputRule({ + let inputRule = wrappingInputRule({ + find: inputRegex, + type: this.type, + }) + + if (this.options.keepMarks || this.options.keepAttributes) { + inputRule = wrappingInputRule({ find: inputRegex, type: this.type, - }), + keepMarks: this.options.keepMarks, + keepAttributes: this.options.keepAttributes, + getAttributes: () => { return this.editor.getAttributes(TextStyle.name) }, + editor: this.editor, + }) + } + return [ + inputRule, ] }, }) diff --git a/packages/extension-ordered-list/src/ordered-list.ts b/packages/extension-ordered-list/src/ordered-list.ts index 6dd181fa2a1..52533bee3fc 100644 --- a/packages/extension-ordered-list/src/ordered-list.ts +++ b/packages/extension-ordered-list/src/ordered-list.ts @@ -1,8 +1,13 @@ import { mergeAttributes, Node, wrappingInputRule } from '@tiptap/core' +import ListItem from '../../extension-list-item/src' +import TextStyle from '../../extension-text-style/src' + export interface OrderedListOptions { itemTypeName: string, HTMLAttributes: Record, + keepMarks: boolean, + keepAttributes: boolean, } declare module '@tiptap/core' { @@ -25,6 +30,8 @@ export const OrderedList = Node.create({ return { itemTypeName: 'listItem', HTMLAttributes: {}, + keepMarks: false, + keepAttributes: false, } }, @@ -65,8 +72,11 @@ export const OrderedList = Node.create({ addCommands() { return { - toggleOrderedList: () => ({ commands }) => { - return commands.toggleList(this.name, this.options.itemTypeName) + toggleOrderedList: () => ({ commands, chain }) => { + if (this.options.keepAttributes) { + return chain().toggleList(this.name, this.options.itemTypeName, this.options.keepMarks).updateAttributes(ListItem.name, this.editor.getAttributes(TextStyle.name)).run() + } + return commands.toggleList(this.name, this.options.itemTypeName, this.options.keepMarks) }, } }, @@ -78,13 +88,23 @@ export const OrderedList = Node.create({ }, addInputRules() { - return [ - wrappingInputRule({ + let inputRule = wrappingInputRule({ + find: inputRegex, + type: this.type, + }) + + if (this.options.keepMarks || this.options.keepAttributes) { + inputRule = wrappingInputRule({ find: inputRegex, type: this.type, - getAttributes: match => ({ start: +match[1] }), - joinPredicate: (match, node) => node.childCount + node.attrs.start === +match[1], - }), + keepMarks: this.options.keepMarks, + keepAttributes: this.options.keepAttributes, + getAttributes: () => { return this.editor.getAttributes(TextStyle.name) }, + editor: this.editor, + }) + } + return [ + inputRule, ] }, })