@@ -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+
280284function 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+
308330function 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+
433523function 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