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
+ editor.chain().focus().setColor('#958DF1').run()}
+ className={editor.isActive('textStyle', { color: '#958DF1' }) ? 'is-active' : ''}
+ >
+ purple
+
>
)
}
@@ -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,
]
},
})