diff --git a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/handle-backspace.ts b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/handle-backspace.ts index a4f2d5db9..885169aab 100644 --- a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/handle-backspace.ts +++ b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/handle-backspace.ts @@ -4,18 +4,15 @@ import { Node } from "@tiptap/pm/model"; import { findListItemPos } from "src/ui/extensions/custom-list-keymap/list-helpers/find-list-item-pos"; import { hasListBefore } from "src/ui/extensions/custom-list-keymap/list-helpers/has-list-before"; +import { hasListItemBefore } from "src/ui/extensions/custom-list-keymap/list-helpers/has-list-item-before"; +import { listItemHasSubList } from "src/ui/extensions/custom-list-keymap/list-helpers/list-item-has-sub-list"; + export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => { // this is required to still handle the undo handling if (editor.commands.undoInputRule()) { return true; } - // if the cursor is not at the start of a node - // do nothing and proceed - if (!isAtStartOfNode(editor.state)) { - return false; - } - // if the current item is NOT inside a list item & // the previous item is a list (orderedList or bulletList) // move the cursor into the list and delete the current item @@ -53,14 +50,31 @@ export const handleBackspace = (editor: Editor, name: string, parentListTypes: s return false; } + // if the cursor is not at the start of a node + // do nothing and proceed + if (!isAtStartOfNode(editor.state)) { + return false; + } + const listItemPos = findListItemPos(name, editor.state); if (!listItemPos) { return false; } - // if current node is a list item and cursor it at start of a list node, - // simply lift the list item i.e. remove it as a list item (task/bullet/ordered) - // irrespective of above node being a list or not + const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2); + const prevNode = $prev.node(listItemPos.depth); + + const previousListItemHasSubList = listItemHasSubList(name, editor.state, prevNode); + + // if the previous item is a list item and doesn't have a sublist, join the list items + if (hasListItemBefore(name, editor.state) && previousListItemHasSubList) { + return editor.chain().liftListItem(name).run(); + // return editor.commands.joinItemBackward(); + } + + // otherwise in the end, a backspace should + // always just lift the list item if + // joining / merging is not possible return editor.chain().liftListItem(name).run(); }; diff --git a/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/list-item-has-sub-list.ts b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/list-item-has-sub-list.ts new file mode 100644 index 000000000..5c15a2b63 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/custom-list-keymap/list-helpers/list-item-has-sub-list.ts @@ -0,0 +1,21 @@ +import { getNodeType } from "@tiptap/core"; +import { Node } from "@tiptap/pm/model"; +import { EditorState } from "@tiptap/pm/state"; + +export const listItemHasSubList = (typeOrName: string, state: EditorState, node?: Node) => { + if (!node) { + return false; + } + + const nodeType = getNodeType(typeOrName, state.schema); + + let hasSubList = false; + + node.descendants((child) => { + if (child.type === nodeType) { + hasSubList = true; + } + }); + + return hasSubList; +}; diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index 1a932d6d5..9b43875f9 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -41,12 +41,12 @@ export const CoreEditorExtensions = ( StarterKit.configure({ bulletList: { HTMLAttributes: { - class: "list-disc list-outside leading-3 -mt-2", + class: "list-disc list-outside leading-3", }, }, orderedList: { HTMLAttributes: { - class: "list-decimal list-outside leading-3 -mt-2", + class: "list-decimal list-outside leading-3 -mt-2 -mb-2", }, }, listItem: { @@ -98,7 +98,7 @@ export const CoreEditorExtensions = ( }), TaskItem.configure({ HTMLAttributes: { - class: "flex items-start my-4", + class: "flex items-start mt-4", }, nested: true, }), diff --git a/packages/editor/core/src/ui/extensions/keymap.tsx b/packages/editor/core/src/ui/extensions/keymap.tsx index 0caa194cd..2e0bdd1fe 100644 --- a/packages/editor/core/src/ui/extensions/keymap.tsx +++ b/packages/editor/core/src/ui/extensions/keymap.tsx @@ -1,4 +1,7 @@ import { Extension } from "@tiptap/core"; +import { Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; +import { canJoin } from "@tiptap/pm/transform"; +import { NodeType } from "@tiptap/pm/model"; declare module "@tiptap/core" { // eslint-disable-next-line no-unused-vars @@ -12,6 +15,51 @@ declare module "@tiptap/core" { } } +function autoJoin(tr: Transaction, newTr: Transaction, nodeType: NodeType) { + if (!tr.isGeneric) return false; + + // Find all ranges where we might want to join. + const ranges: Array = []; + for (let i = 0; i < tr.mapping.maps.length; i++) { + const map = tr.mapping.maps[i]; + for (let j = 0; j < ranges.length; j++) ranges[j] = map.map(ranges[j]); + map.forEach((_s, _e, from, to) => ranges.push(from, to)); + } + + // Figure out which joinable points exist inside those ranges, + // by checking all node boundaries in their parent nodes. + const joinable = []; + for (let i = 0; i < ranges.length; i += 2) { + const from = ranges[i], + to = ranges[i + 1]; + const $from = tr.doc.resolve(from), + depth = $from.sharedDepth(to), + parent = $from.node(depth); + for (let index = $from.indexAfter(depth), pos = $from.after(depth + 1); pos <= to; ++index) { + const after = parent.maybeChild(index); + if (!after) break; + if (index && joinable.indexOf(pos) == -1) { + const before = parent.child(index - 1); + if (before.type == after.type && before.type === nodeType) joinable.push(pos); + } + pos += after.nodeSize; + } + } + + let joined = false; + + // Join the joinable points + joinable.sort((a, b) => a - b); + for (let i = joinable.length - 1; i >= 0; i--) { + if (canJoin(tr.doc, joinable[i])) { + newTr.join(joinable[i]); + joined = true; + } + } + + return joined; +} + export const CustomKeymap = Extension.create({ name: "CustomKeymap", @@ -32,6 +80,42 @@ export const CustomKeymap = Extension.create({ }; }, + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey("ordered-list-merging"), + appendTransaction(transactions, oldState, newState) { + // Create a new transaction. + const newTr = newState.tr; + + let joined = false; + for (const transaction of transactions) { + const anotherJoin = autoJoin(transaction, newTr, newState.schema.nodes["orderedList"]); + joined = anotherJoin || joined; + } + if (joined) { + return newTr; + } + }, + }), + new Plugin({ + key: new PluginKey("unordered-list-merging"), + appendTransaction(transactions, oldState, newState) { + // Create a new transaction. + const newTr = newState.tr; + + let joined = false; + for (const transaction of transactions) { + const anotherJoin = autoJoin(transaction, newTr, newState.schema.nodes["bulletList"]); + joined = anotherJoin || joined; + } + if (joined) { + return newTr; + } + }, + }), + ]; + }, addKeyboardShortcuts() { return { "Mod-a": ({ editor }) => { diff --git a/packages/editor/core/src/ui/read-only/extensions.tsx b/packages/editor/core/src/ui/read-only/extensions.tsx index 93e1b3887..81af5c523 100644 --- a/packages/editor/core/src/ui/read-only/extensions.tsx +++ b/packages/editor/core/src/ui/read-only/extensions.tsx @@ -29,12 +29,12 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { StarterKit.configure({ bulletList: { HTMLAttributes: { - class: "list-disc list-outside leading-3 -mt-2", + class: "list-disc list-outside leading-3", }, }, orderedList: { HTMLAttributes: { - class: "list-decimal list-outside leading-3 -mt-2", + class: "list-decimal list-outside leading-3 -mt-2 -mb-2", }, }, listItem: { @@ -82,7 +82,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { }), TaskItem.configure({ HTMLAttributes: { - class: "flex items-start my-4", + class: "flex items-start mt-4", }, nested: true, }),