mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: list autojoining added
This commit is contained in:
parent
9249e6d5b9
commit
4b56cc5565
@ -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 { 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 { 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[]) => {
|
export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => {
|
||||||
// this is required to still handle the undo handling
|
// this is required to still handle the undo handling
|
||||||
if (editor.commands.undoInputRule()) {
|
if (editor.commands.undoInputRule()) {
|
||||||
return true;
|
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 &
|
// if the current item is NOT inside a list item &
|
||||||
// the previous item is a list (orderedList or bulletList)
|
// the previous item is a list (orderedList or bulletList)
|
||||||
// move the cursor into the list and delete the current item
|
// 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;
|
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);
|
const listItemPos = findListItemPos(name, editor.state);
|
||||||
|
|
||||||
if (!listItemPos) {
|
if (!listItemPos) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if current node is a list item and cursor it at start of a list node,
|
const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2);
|
||||||
// simply lift the list item i.e. remove it as a list item (task/bullet/ordered)
|
const prevNode = $prev.node(listItemPos.depth);
|
||||||
// irrespective of above node being a list or not
|
|
||||||
|
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();
|
return editor.chain().liftListItem(name).run();
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -41,12 +41,12 @@ export const CoreEditorExtensions = (
|
|||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
bulletList: {
|
bulletList: {
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "list-disc list-outside leading-3 -mt-2",
|
class: "list-disc list-outside leading-3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderedList: {
|
orderedList: {
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "list-decimal list-outside leading-3 -mt-2",
|
class: "list-decimal list-outside leading-3 -mt-2 -mb-2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
listItem: {
|
listItem: {
|
||||||
@ -98,7 +98,7 @@ export const CoreEditorExtensions = (
|
|||||||
}),
|
}),
|
||||||
TaskItem.configure({
|
TaskItem.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "flex items-start my-4",
|
class: "flex items-start mt-4",
|
||||||
},
|
},
|
||||||
nested: true,
|
nested: true,
|
||||||
}),
|
}),
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { Extension } from "@tiptap/core";
|
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" {
|
declare module "@tiptap/core" {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// 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<number> = [];
|
||||||
|
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({
|
export const CustomKeymap = Extension.create({
|
||||||
name: "CustomKeymap",
|
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() {
|
addKeyboardShortcuts() {
|
||||||
return {
|
return {
|
||||||
"Mod-a": ({ editor }) => {
|
"Mod-a": ({ editor }) => {
|
||||||
|
@ -29,12 +29,12 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
|||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
bulletList: {
|
bulletList: {
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "list-disc list-outside leading-3 -mt-2",
|
class: "list-disc list-outside leading-3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderedList: {
|
orderedList: {
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "list-decimal list-outside leading-3 -mt-2",
|
class: "list-decimal list-outside leading-3 -mt-2 -mb-2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
listItem: {
|
listItem: {
|
||||||
@ -82,7 +82,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
|||||||
}),
|
}),
|
||||||
TaskItem.configure({
|
TaskItem.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "flex items-start my-4",
|
class: "flex items-start mt-4",
|
||||||
},
|
},
|
||||||
nested: true,
|
nested: true,
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user