Skip to content
Open
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
4 changes: 2 additions & 2 deletions cypress/e2e/nodes/ListItem.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import { ListItem, OrderedList } from '@tiptap/extension-list'
/* eslint-enable import/no-named-as-default */
import Markdown from './../../../src/extensions/Markdown.js'
import BulletList from './../../../src/nodes/BulletList.js'
import BulletList from './../../../src/nodes/BulletList.ts'
import TaskItem from './../../../src/nodes/TaskItem.js'
import TaskList from './../../../src/nodes/TaskList.js'
import TaskList from './../../../src/nodes/TaskList.ts'
import { createCustomEditor } from './../../support/components.js'
import { expectMarkdown, loadMarkdown, runCommands } from './helpers.js'

Expand Down
61 changes: 27 additions & 34 deletions cypress/fixtures/ListItem.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,26 @@
## bullet list to task list

* toggleTaskList
* keep
* two
* three

---

- [ ] did toggleTaskList
* [ ] did toggleTaskList
* [ ] two
* [ ] three

* keep
## numbered list to task list

1. toggleTaskList
2. two
3. three

---

- [ ] did toggleTaskList
- [ ] two
- [ ] three

## bullet list to ordered list

Expand All @@ -61,6 +74,17 @@
1. did toggleOrderedList
2. keep

## task list to bullet list

* [ ] one
* [ ] toggleBulletList
* [ ] three

---

* one
* did toggleBulletList
* three

## removes the list when toggling task off

Expand All @@ -78,31 +102,6 @@ toggleBulletList

- did toggleBulletList

## Splits bullet list when turning one item into task

* toggleTaskList
* not todo

---

- [ ] did toggleTaskList

* not todo

## toggles two list items separately

* toggleTaskList
* not todo
* toggleTaskList

---

- [ ] did toggleTaskList

* not todo

- [ ] did toggleTaskList

## toggle off task list item should turn it into normal list item

* not todo
Expand All @@ -113,9 +112,3 @@ toggleBulletList
* not todo

did toggleTaskList

---

* not todo
* toggleTaskList

92 changes: 92 additions & 0 deletions src/commands/convertList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { CommandProps } from '@tiptap/core'
import type { Node, NodeType } from '@tiptap/pm/model'
import type { Transaction } from '@tiptap/pm/state'

import { findParentNode, isList } from '@tiptap/core'
import { TextSelection } from '@tiptap/pm/state'

type ParentList = {
pos: number
node: Node
}

/**
* Rebuild a list node with a new list type and item type, preserving all item content.
*
* Used for cross-silo list conversion (bulletList/orderedList ↔ taskList) where a simple
* setNodeMarkup is not sufficient because the child item types also differ
* (listItem vs taskItem).
*
* Only the direct children of the list are retyped; nested sub-lists are preserved as-is.
* When converting to taskItem the item receives checked: false.
* When converting away from taskItem the checked attribute is simply dropped.
*
* @param parentList - the parent list node
* @param targetListType - the target list type
* @param targetItemType - the target item type
* @param tr - the ProseMirror transaction
*/
function convertListType(
parentList: ParentList,
targetListType: NodeType,
targetItemType: NodeType,
tr: Transaction,
): void {
const newItems: Node[] = []
parentList.node.forEach((item) => {
const attrs = targetItemType.name === 'taskItem' ? { checked: false } : {}
newItems.push(targetItemType.create(attrs, item.content))
})
const newList = targetListType.create(parentList.node.attrs, newItems)
tr.replaceWith(
parentList.pos,
parentList.pos + parentList.node.nodeSize,
newList,
)
}

/**
* Returns a Tiptap command that toggles a given list type.
* To be used by BulletList, OrderedList and TaskList nodes.
*
* Handles cross-type conversion (e.g. bulletList → taskList) that
* `toggleList` cannot handle due to incompatible item types.
*
* @param listTypeName - the target list type
* @param itemTypeName - the target item type
*/
export const toggleListCommand =
(listTypeName: string, itemTypeName: string) =>
() =>
({ editor, state, tr, dispatch, commands }: CommandProps): boolean => {
const { extensions } = editor.extensionManager

const parentList = findParentNode((node) =>
isList(node.type.name, extensions),
)(state.selection)

const listType = state.schema.nodes[listTypeName]!
const itemType = state.schema.nodes[itemTypeName]!

if (
parentList
&& parentList.node.type !== listType
&& !listType.validContent(parentList.node.content)
) {
if (!dispatch) {
return true
}
const { from, to } = state.selection
convertListType(parentList, listType, itemType, tr)
tr.setSelection(TextSelection.create(tr.doc, from, to))
dispatch(tr)
return true
}

return commands.toggleList(listType, itemType)
}
3 changes: 2 additions & 1 deletion src/commands/index.js → src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { toggleListCommand } from './convertList'
import listInputRule from './listInputRule.js'

export { listInputRule }
export { listInputRule, toggleListCommand }
2 changes: 1 addition & 1 deletion src/commands/listInputRule.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { InputRule, wrappingInputRule } from '@tiptap/core'
* @param {object} type Node Type object
* @param {*} getAttributes handler to get the attributes
*/
export default function (find, type, getAttributes) {
export default function (find, type, getAttributes = null) {
const handler = ({ state, range, match }) => {
const wrap = wrappingInputRule({ find, type, getAttributes })
wrap.handler({ state, range, match })
Expand Down
6 changes: 3 additions & 3 deletions src/extensions/RichText.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
Strong,
Underline,
} from './../marks/index.js'
import BulletList from './../nodes/BulletList.js'
import BulletList from './../nodes/BulletList.ts'
import Callouts from './../nodes/Callouts.js'
import CodeBlock from './../nodes/CodeBlock.js'
import Details from './../nodes/Details.js'
Expand All @@ -44,12 +44,12 @@ import HardBreak from './../nodes/HardBreak.js'
import Image from './../nodes/Image.js'
import ImageInline from './../nodes/ImageInline.js'
import { MathBlock, MathInline } from './../nodes/Mathematics.js'
import OrderedList from './../nodes/OrderedList.js'
import OrderedList from './../nodes/OrderedList.ts'
import Paragraph from './../nodes/Paragraph.js'
import Preview from './../nodes/Preview.js'
import Table from './../nodes/Table.js'
import TaskItem from './../nodes/TaskItem.js'
import TaskList from './../nodes/TaskList.js'
import TaskList from './../nodes/TaskList.ts'
import TrailingNode from './../nodes/TrailingNode.js'
import Emoji from './Emoji.js'
import KeepSyntax from './KeepSyntax.js'
Expand Down
11 changes: 9 additions & 2 deletions src/nodes/BulletList.js → src/nodes/BulletList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { BulletList as TiptapBulletList } from '@tiptap/extension-list'
import { listInputRule } from '../commands/index.js'
import { listInputRule, toggleListCommand } from '../commands'

/* We want to allow for `* [ ]` as an input rule for bullet lists.
* Therefore the list input rules need to check the input
Expand All @@ -13,7 +13,7 @@ import { listInputRule } from '../commands/index.js'
*/
const BulletList = TiptapBulletList.extend({
parseHTML() {
return this.parent().map((rule) =>
return this.parent?.()?.map((rule) =>
Object.assign(rule, { preserveWhitespace: true }),
)
},
Expand All @@ -36,6 +36,13 @@ const BulletList = TiptapBulletList.extend({
addInputRules() {
return [listInputRule(/^\s*([-+*])\s([^\s[]+)$/, this.type)]
},

addCommands() {
return {
...this.parent?.(),
toggleBulletList: toggleListCommand('bulletList', 'listItem'),
}
},
})

export default BulletList
8 changes: 8 additions & 0 deletions src/nodes/OrderedList.js → src/nodes/OrderedList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { OrderedList as TiptapOrderedList } from '@tiptap/extension-list'
import { toggleListCommand } from '../commands'

const OrderedList = TiptapOrderedList.extend({
addAttributes() {
Expand All @@ -15,6 +16,13 @@ const OrderedList = TiptapOrderedList.extend({
},
}
},

addCommands() {
return {
...this.parent?.(),
toggleOrderedList: toggleListCommand('orderedList', 'listItem'),
}
},
})

export default OrderedList
28 changes: 21 additions & 7 deletions src/nodes/TaskList.js → src/nodes/TaskList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Node } from '@tiptap/pm/model'
import type { MarkdownSerializerState } from 'prosemirror-markdown'

import { mergeAttributes } from '@tiptap/core'
import { TaskList as TiptapTaskList } from '@tiptap/extension-list'
import { toggleListCommand } from '../commands'

const TaskList = TiptapTaskList.extend({
parseHTML: [
{
priority: 100,
tag: 'ul.contains-task-list',
},
],
parseHTML() {
return [
{
priority: 100,
tag: 'ul.contains-task-list',
},
]
},

renderHTML({ HTMLAttributes }) {
return [
Expand All @@ -39,9 +45,17 @@ const TaskList = TiptapTaskList.extend({
}
},

toMarkdown: (state, node) => {
// @ts-expect-error - toMarkdown is a custom field not part of the official Tiptap API
toMarkdown: (state: MarkdownSerializerState, node: Node) => {
state.renderList(node, ' ', () => `${node.attrs.bullet} `)
},

addCommands() {
return {
...this.parent?.(),
toggleTaskList: toggleListCommand('taskList', 'taskItem'),
}
},
})

export default TaskList
4 changes: 2 additions & 2 deletions src/tests/extensions/Markdown.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { Markdown } from '../../extensions/index.js'
import { createMarkdownSerializer } from '../../extensions/Markdown.js'
import { Italic, Link, Strong, Underline } from '../../marks/index.js'
import Image from '../../nodes/Image.js'
import OrderedList from '../../nodes/OrderedList.js'
import OrderedList from '../../nodes/OrderedList.ts'
import Table from '../../nodes/Table.js'
import TaskItem from '../../nodes/TaskItem.js'
import TaskList from '../../nodes/TaskList.js'
import TaskList from '../../nodes/TaskList.ts'
import createCustomEditor from '../testHelpers/createCustomEditor.ts'
import ImageInline from './../../nodes/ImageInline.js'

Expand Down
Loading
Loading