Skip to content

Commit

Permalink
feat: #3540 Ability to preserve marks on lists (#3541)
Browse files Browse the repository at this point in the history
* feat: #3540 Ability to preserve marks on lists

* feat: preserveAttrs in list items

* `keepMarks` is working, but need help with `keepAttrs`

* fix: conflict

* avoid casting
  • Loading branch information
gethari committed Feb 22, 2023
1 parent 10f9069 commit 36bb1e1
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 28 deletions.
22 changes: 21 additions & 1 deletion 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'
Expand Down Expand Up @@ -165,14 +168,31 @@ const MenuBar = ({ editor }) => {
>
redo
</button>
<button
onClick={() => editor.chain().focus().setColor('#958DF1').run()}
className={editor.isActive('textStyle', { color: '#958DF1' }) ? 'is-active' : ''}
>
purple
</button>
</>
)
}

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: `
<h2>
Expand Down
21 changes: 21 additions & 0 deletions docs/api/nodes/bullet-list.md
Expand Up @@ -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

Expand Down
21 changes: 21 additions & 0 deletions docs/api/nodes/ordered-list.md
Expand Up @@ -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()
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/commands/splitListItem.ts
Expand Up @@ -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
Expand Down
35 changes: 28 additions & 7 deletions packages/core/src/commands/toggleList.ts
Expand Up @@ -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
}
Expand Down Expand Up @@ -110,13 +109,35 @@ 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()
// try to convert node to default node if needed
.command(() => {
const canWrapInList = can().wrapInList(listType)

const filteredMarks = marks.filter(mark => splittableMarks.includes(mark.type.name))

tr.ensureMarks(filteredMarks)

if (canWrapInList) {
return true
}
Expand Down
41 changes: 33 additions & 8 deletions 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'
Expand All @@ -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<string, any>
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
| false
| null
joinPredicate?: (match: ExtendedRegExpMatchArray, node: ProseMirrorNode) => boolean
| Record<string, any>
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
| 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)
Expand All @@ -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 (
Expand Down
32 changes: 27 additions & 5 deletions 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<string, any>,
keepMarks: boolean,
keepAttributes: boolean,
}

declare module '@tiptap/core' {
Expand All @@ -25,6 +30,8 @@ export const BulletList = Node.create<BulletListOptions>({
return {
itemTypeName: 'listItem',
HTMLAttributes: {},
keepMarks: false,
keepAttributes: false,
}
},

Expand All @@ -46,8 +53,11 @@ export const BulletList = Node.create<BulletListOptions>({

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)
},
}
},
Expand All @@ -59,11 +69,23 @@ export const BulletList = Node.create<BulletListOptions>({
},

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,
]
},
})
34 changes: 27 additions & 7 deletions 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<string, any>,
keepMarks: boolean,
keepAttributes: boolean,
}

declare module '@tiptap/core' {
Expand All @@ -25,6 +30,8 @@ export const OrderedList = Node.create<OrderedListOptions>({
return {
itemTypeName: 'listItem',
HTMLAttributes: {},
keepMarks: false,
keepAttributes: false,
}
},

Expand Down Expand Up @@ -65,8 +72,11 @@ export const OrderedList = Node.create<OrderedListOptions>({

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)
},
}
},
Expand All @@ -78,13 +88,23 @@ export const OrderedList = Node.create<OrderedListOptions>({
},

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

0 comments on commit 36bb1e1

Please sign in to comment.