Skip to content

Commit 8cb292a

Browse files
author
ComputelessComputer
committed
Fix loose task node movement
Allow Option+Arrow to move single-item task lists across adjacent loose task blocks and preserve selection while reordering.
1 parent 4114a82 commit 8cb292a

1 file changed

Lines changed: 100 additions & 16 deletions

File tree

apps/desktop/src/components/journal/EditableNote.tsx

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ function isListContainerNode(node: ProseMirrorNode,) {
277277
return node.type.name === "bulletList" || node.type.name === "orderedList" || node.type.name === "taskList";
278278
}
279279

280+
function isEmptyParagraphNode(node: ProseMirrorNode,) {
281+
return node.type.name === "paragraph" && node.content.size === 0;
282+
}
283+
280284
function findNestedListChildIndex(node: ProseMirrorNode, listTypeName: string,) {
281285
for (let index = 0; index < node.childCount; index += 1) {
282286
if (node.child(index,).type.name === listTypeName) {
@@ -292,7 +296,7 @@ function getDescendantNodePos(ancestorPos: number, ancestorNode: ProseMirrorNode
292296
let currentNode = ancestorNode;
293297

294298
for (const index of path) {
295-
let childPos = currentPos + 1;
299+
let childPos = currentNode.type.name === "doc" ? currentPos : currentPos + 1;
296300

297301
for (let childIndex = 0; childIndex < index; childIndex += 1) {
298302
childPos += currentNode.child(childIndex,).nodeSize;
@@ -305,6 +309,24 @@ function getDescendantNodePos(ancestorPos: number, ancestorNode: ProseMirrorNode
305309
return currentPos;
306310
}
307311

312+
function restoreMovedNodeSelection(
313+
tr: import("@tiptap/pm/state").Transaction,
314+
selection: Selection,
315+
nodePos: number,
316+
newNodePos: number,
317+
) {
318+
if (selection instanceof NodeSelection) {
319+
tr.setSelection(NodeSelection.create(tr.doc, newNodePos,),);
320+
return;
321+
}
322+
323+
tr.setSelection(TextSelection.create(
324+
tr.doc,
325+
newNodePos + (selection.anchor - nodePos),
326+
newNodePos + (selection.head - nodePos),
327+
),);
328+
}
329+
308330
function getMovableNodeContext(state: import("@tiptap/pm/state").EditorState,) {
309331
const { doc, selection, } = state;
310332

@@ -430,6 +452,74 @@ function tryMoveListItemIntoNextSibling(
430452
return true;
431453
}
432454

455+
function tryMoveSeparatedSingleListItem(
456+
view: import("@tiptap/pm/view").EditorView,
457+
context: ReturnType<typeof getMovableNodeContext>,
458+
direction: "up" | "down",
459+
): boolean {
460+
if (!context) return false;
461+
462+
const { state, } = view;
463+
const { selection, } = state;
464+
const {
465+
node,
466+
nodePos,
467+
parent,
468+
parentDepth,
469+
} = context;
470+
471+
if (!isListItemNode(node,) || !isListContainerNode(parent,) || parent.childCount !== 1 || parentDepth <= 0) {
472+
return false;
473+
}
474+
475+
const $nodePos = state.doc.resolve(nodePos,);
476+
const grandparentDepth = parentDepth - 1;
477+
const grandparent = $nodePos.node(grandparentDepth,);
478+
const listIndex = $nodePos.index(grandparentDepth,);
479+
const step = direction === "up" ? -1 : 1;
480+
let targetListIndex = -1;
481+
482+
for (
483+
let siblingIndex = listIndex + step;
484+
siblingIndex >= 0 && siblingIndex < grandparent.childCount;
485+
siblingIndex += step
486+
) {
487+
const sibling = grandparent.child(siblingIndex,);
488+
if (isEmptyParagraphNode(sibling,)) {
489+
continue;
490+
}
491+
492+
if (sibling.type === parent.type && sibling.childCount === 1 && isListItemNode(sibling.child(0,),)) {
493+
targetListIndex = siblingIndex;
494+
}
495+
break;
496+
}
497+
498+
if (targetListIndex < 0) {
499+
return false;
500+
}
501+
502+
const children = Array.from({ length: grandparent.childCount, }, (_, index,) => grandparent.child(index,),);
503+
[children[listIndex], children[targetListIndex],] = [children[targetListIndex], children[listIndex],];
504+
505+
const tr = state.tr;
506+
const grandparentPos = grandparentDepth === 0 ? 0 : $nodePos.before(grandparentDepth,);
507+
const grandparentStart = grandparentDepth === 0 ? 0 : $nodePos.start(grandparentDepth,);
508+
const grandparentEnd = grandparentDepth === 0 ? state.doc.content.size : $nodePos.end(grandparentDepth,);
509+
tr.replaceWith(grandparentStart, grandparentEnd, Fragment.fromArray(children,),);
510+
511+
const updatedGrandparent = grandparentDepth === 0 ? tr.doc : tr.doc.nodeAt(grandparentPos,);
512+
if (!updatedGrandparent) {
513+
view.dispatch(tr.scrollIntoView(),);
514+
return true;
515+
}
516+
517+
const newNodePos = getDescendantNodePos(grandparentPos, updatedGrandparent, [targetListIndex, 0,],);
518+
restoreMovedNodeSelection(tr, selection, nodePos, newNodePos,);
519+
view.dispatch(tr.scrollIntoView(),);
520+
return true;
521+
}
522+
433523
function moveSelectedNode(view: import("@tiptap/pm/view").EditorView, direction: "up" | "down",): boolean {
434524
const { state, } = view;
435525
const { selection, } = state;
@@ -447,6 +537,10 @@ function moveSelectedNode(view: import("@tiptap/pm/view").EditorView, direction:
447537
return true;
448538
}
449539

540+
if (tryMoveSeparatedSingleListItem(view, context, direction,)) {
541+
return true;
542+
}
543+
450544
const targetIndex = direction === "up" ? index - 1 : index + 1;
451545

452546
if (targetIndex < 0 || targetIndex >= parent.childCount) {
@@ -463,21 +557,11 @@ function moveSelectedNode(view: import("@tiptap/pm/view").EditorView, direction:
463557
const parentEnd = parentDepth === 0 ? state.doc.content.size : $nodePos.end(parentDepth,);
464558
tr.replaceWith(parentStart, parentEnd, Fragment.fromArray(children,),);
465559

466-
const adjacentNodeSize = direction === "up"
467-
? parent.child(index - 1,).nodeSize
468-
: parent.child(index + 1,).nodeSize;
469-
const positionDelta = direction === "up" ? -adjacentNodeSize : adjacentNodeSize;
470-
const newNodePos = nodePos + positionDelta;
471-
472-
if (selection instanceof NodeSelection) {
473-
tr.setSelection(NodeSelection.create(tr.doc, newNodePos,),);
474-
} else {
475-
const movedSelection = TextSelection.create(
476-
tr.doc,
477-
selection.anchor + positionDelta,
478-
selection.head + positionDelta,
479-
);
480-
tr.setSelection(movedSelection,);
560+
const parentPos = parentDepth === 0 ? 0 : $nodePos.before(parentDepth,);
561+
const updatedParent = parentDepth === 0 ? tr.doc : tr.doc.nodeAt(parentPos,);
562+
if (updatedParent) {
563+
const newNodePos = getDescendantNodePos(parentPos, updatedParent, [targetIndex,],);
564+
restoreMovedNodeSelection(tr, selection, nodePos, newNodePos,);
481565
}
482566

483567
view.dispatch(tr.scrollIntoView(),);

0 commit comments

Comments
 (0)