[WEB-1526] feat: add auto merge behaviour to task lists and fix infinite backspace case (#4703)

* feat: add auto merge behaviour to task lists

* fix: unhandled cases for taskItem and taskList

* fix: css task list such that toggling task list doesn't shift things

* fix: task list jumps around while trying create/delete things in between two task lists

* fix: remove filtering for generic transactions i.e. transactions with some meta data while tying to join things
This commit is contained in:
M. Palanikannan 2024-06-07 12:36:19 +05:30 committed by GitHub
parent f5656111ee
commit b1c7e6ae20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 30 additions and 25 deletions

View File

@ -110,6 +110,11 @@ ul[data-type="taskList"] li > label input[type="checkbox"]:checked:hover {
} }
} }
/* the p tag just after the ul tag */
ul[data-type="taskList"] + p {
margin-top: 0.4rem !important;
}
ul[data-type="taskList"] li > label input[type="checkbox"] { ul[data-type="taskList"] li > label input[type="checkbox"] {
position: relative; position: relative;
-webkit-appearance: none; -webkit-appearance: none;
@ -152,6 +157,10 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
} }
} }
ul[data-type="taskList"] li > div > p {
margin-top: 10px;
}
ul[data-type="taskList"] li[data-checked="true"] > div > p { ul[data-type="taskList"] li[data-checked="true"] > div > p {
color: rgb(var(--color-text-400)); color: rgb(var(--color-text-400));
text-decoration: line-through; text-decoration: line-through;

View File

@ -72,7 +72,7 @@ const getPrevListDepth = (typeOrName: string, state: EditorState) => {
// Traverse up the document structure from the adjusted position // Traverse up the document structure from the adjusted position
for (let d = resolvedPos.depth; d > 0; d--) { for (let d = resolvedPos.depth; d > 0; d--) {
const node = resolvedPos.node(d); const node = resolvedPos.node(d);
if (node.type.name === "bulletList" || node.type.name === "orderedList") { if (node.type.name === "bulletList" || node.type.name === "orderedList" || node.type.name === "taskList") {
// Increment depth for each list ancestor found // Increment depth for each list ancestor found
depth++; depth++;
} }
@ -146,6 +146,8 @@ export const handleBackspace = (editor: Editor, name: string, parentListTypes: s
if (!isAtStartOfNode(editor.state)) { if (!isAtStartOfNode(editor.state)) {
return false; return false;
} }
// is the paragraph node inside of the current list item (maybe with a hard break)
const isParaSibling = isCurrentParagraphASibling(editor.state); const isParaSibling = isCurrentParagraphASibling(editor.state);
const isCurrentListItemSublist = prevListIsHigher(name, editor.state); const isCurrentListItemSublist = prevListIsHigher(name, editor.state);
const listItemPos = findListItemPos(name, editor.state); const listItemPos = findListItemPos(name, editor.state);
@ -306,7 +308,10 @@ const isCurrentParagraphASibling = (state: EditorState): boolean => {
const currentParagraphNode = $from.parent; // Get the current node where the selection is. const currentParagraphNode = $from.parent; // Get the current node where the selection is.
// Ensure we're in a paragraph and the parent is a list item. // Ensure we're in a paragraph and the parent is a list item.
if (currentParagraphNode.type.name === "paragraph" && listItemNode.type.name === "listItem") { if (
currentParagraphNode.type.name === "paragraph" &&
(listItemNode.type.name === "listItem" || listItemNode.type.name === "taskItem")
) {
let paragraphNodesCount = 0; let paragraphNodesCount = 0;
listItemNode.forEach((child) => { listItemNode.forEach((child) => {
if (child.type.name === "paragraph") { if (child.type.name === "paragraph") {
@ -327,16 +332,19 @@ export function isCursorInSubList(editor: Editor) {
// Check if the current node is a list item // Check if the current node is a list item
const listItem = editor.schema.nodes.listItem; const listItem = editor.schema.nodes.listItem;
const taskItem = editor.schema.nodes.taskItem;
// Traverse up the document tree from the current position // Traverse up the document tree from the current position
for (let depth = $from.depth; depth > 0; depth--) { for (let depth = $from.depth; depth > 0; depth--) {
const node = $from.node(depth); const node = $from.node(depth);
if (node.type === listItem) { if (node.type === listItem || node.type === taskItem) {
// If the parent of the list item is also a list, it's a sub-list // If the parent of the list item is also a list, it's a sub-list
const parent = $from.node(depth - 1); const parent = $from.node(depth - 1);
if ( if (
parent && parent &&
(parent.type === editor.schema.nodes.bulletList || parent.type === editor.schema.nodes.orderedList) (parent.type === editor.schema.nodes.bulletList ||
parent.type === editor.schema.nodes.orderedList ||
parent.type === editor.schema.nodes.taskList)
) { ) {
return true; return true;
} }

View File

@ -15,9 +15,7 @@ declare module "@tiptap/core" {
} }
} }
function autoJoin(tr: Transaction, newTr: Transaction, nodeType: NodeType) { function autoJoin(tr: Transaction, newTr: Transaction, nodeTypes: NodeType[]) {
if (!tr.isGeneric) return false;
// Find all ranges where we might want to join. // Find all ranges where we might want to join.
const ranges: Array<number> = []; const ranges: Array<number> = [];
for (let i = 0; i < tr.mapping.maps.length; i++) { for (let i = 0; i < tr.mapping.maps.length; i++) {
@ -28,7 +26,7 @@ function autoJoin(tr: Transaction, newTr: Transaction, nodeType: NodeType) {
// Figure out which joinable points exist inside those ranges, // Figure out which joinable points exist inside those ranges,
// by checking all node boundaries in their parent nodes. // by checking all node boundaries in their parent nodes.
const joinable = []; const joinable: number[] = [];
for (let i = 0; i < ranges.length; i += 2) { for (let i = 0; i < ranges.length; i += 2) {
const from = ranges[i], const from = ranges[i],
to = ranges[i + 1]; to = ranges[i + 1];
@ -40,7 +38,7 @@ function autoJoin(tr: Transaction, newTr: Transaction, nodeType: NodeType) {
if (!after) break; if (!after) break;
if (index && joinable.indexOf(pos) == -1) { if (index && joinable.indexOf(pos) == -1) {
const before = parent.child(index - 1); const before = parent.child(index - 1);
if (before.type == after.type && before.type === nodeType) joinable.push(pos); if (before.type == after.type && nodeTypes.includes(before.type)) joinable.push(pos);
} }
pos += after.nodeSize; pos += after.nodeSize;
} }
@ -88,25 +86,15 @@ export const CustomKeymap = Extension.create({
// Create a new transaction. // Create a new transaction.
const newTr = newState.tr; const newTr = newState.tr;
let joined = false; const joinableNodes = [
for (const transaction of transactions) { newState.schema.nodes["orderedList"],
const anotherJoin = autoJoin(transaction, newTr, newState.schema.nodes["orderedList"]); newState.schema.nodes["taskList"],
joined = anotherJoin || joined; newState.schema.nodes["bulletList"],
} ];
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; let joined = false;
for (const transaction of transactions) { for (const transaction of transactions) {
const anotherJoin = autoJoin(transaction, newTr, newState.schema.nodes["bulletList"]); const anotherJoin = autoJoin(transaction, newTr, joinableNodes);
joined = anotherJoin || joined; joined = anotherJoin || joined;
} }
if (joined) { if (joined) {