1- import type { Node } from "prosemirror-model" ;
2- import type { Transaction } from "prosemirror-state" ;
1+ import { ResolvedPos , Slice , type Node } from "prosemirror-model" ;
2+ import { TextSelection , type Transaction } from "prosemirror-state" ;
3+ import { ReplaceAroundStep } from "prosemirror-transform" ;
34import type { Block , PartialBlock } from "../../../../blocks/defaultBlocks.js" ;
45import type {
56 BlockIdentifier ,
67 BlockSchema ,
78 InlineContentSchema ,
89 StyleSchema ,
910} from "../../../../schema/index.js" ;
11+ import { getBlockInfoFromResolvedPos } from "../../../getBlockInfoFromPos.js" ;
1012import { blockToNode } from "../../../nodeConversions/blockToNode.js" ;
1113import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js" ;
1214import { getPmSchema } from "../../../pmUtil.js" ;
15+ import {
16+ getParentBlockInfo ,
17+ getPrevBlockInfo ,
18+ } from "../mergeBlocks/mergeBlocks.js" ;
19+
20+ // TODO: Where should this function go?
21+ /**
22+ * Moves the first block in a column to the previous/next column and handles
23+ * all necessary collapsing of `column`/`columnList` nodes. Only moves the
24+ * block to the start of the next column if it's in the first column.
25+ * Otherwise, moves the block to the end of the previous column.
26+ * @param tr The transaction to apply changes to.
27+ * @param blockBeforePos The position just before the first block in the column.
28+ * @returns The position just before the block, after it's moved.
29+ */
30+ export function moveFirstBlockInColumn (
31+ tr : Transaction ,
32+ blockBeforePos : ResolvedPos ,
33+ ) : ResolvedPos {
34+ const blockInfo = getBlockInfoFromResolvedPos ( blockBeforePos ) ;
35+ if ( ! blockInfo . isBlockContainer ) {
36+ throw new Error (
37+ "Invalid blockBeforePos passed to moveFirstBlockInColumn: does not point to blockContainer node." ,
38+ ) ;
39+ }
40+
41+ const prevBlockInfo = getPrevBlockInfo ( tr . doc , blockInfo . bnBlock . beforePos ) ;
42+ if ( prevBlockInfo ) {
43+ throw new Error (
44+ "Invalid blockBeforePos passed to moveFirstBlockInColumn: does not point to first blockContainer node in column." ,
45+ ) ;
46+ }
47+
48+ const parentBlockInfo = getParentBlockInfo (
49+ tr . doc ,
50+ blockInfo . bnBlock . beforePos ,
51+ ) ;
52+ if ( parentBlockInfo ?. blockNoteType !== "column" ) {
53+ throw new Error (
54+ "Invalid blockBeforePos passed to moveFirstBlockInColumn: blockContainer node is not child of column." ,
55+ ) ;
56+ }
57+
58+ const column = parentBlockInfo ;
59+ const columnList = getParentBlockInfo ( tr . doc , column . bnBlock . beforePos ) ;
60+ if ( columnList ?. blockNoteType !== "columnList" ) {
61+ throw new Error (
62+ "Invalid blockBeforePos passed to moveFirstBlockInColumn: blockContainer node is child of column, but column is not child of columnList node." ,
63+ ) ;
64+ }
65+
66+ const shouldRemoveColumn = column . childContainer ! . node . childCount === 1 ;
67+
68+ const shouldRemoveColumnList =
69+ shouldRemoveColumn && columnList . childContainer ! . node . childCount === 2 ;
70+
71+ const isFirstColumn =
72+ columnList . childContainer ! . node . firstChild === column . bnBlock . node ;
73+
74+ const blockToMove = tr . doc . slice (
75+ blockInfo . bnBlock . beforePos ,
76+ blockInfo . bnBlock . afterPos ,
77+ false ,
78+ ) ;
79+
80+ /*
81+ There are 3 different cases:
82+ a) remove entire column list (if no columns would be remaining)
83+ b) remove just a column (if no blocks inside a column would be remaining)
84+ c) keep columns (if there are blocks remaining inside a column)
85+
86+ Each of these 3 cases has 2 sub-cases, depending on whether the backspace happens at the start of the first (most-left) column,
87+ or at the start of a non-first column.
88+ */
89+ if ( shouldRemoveColumnList ) {
90+ if ( isFirstColumn ) {
91+ tr . step (
92+ new ReplaceAroundStep (
93+ // replace entire column list
94+ columnList . bnBlock . beforePos ,
95+ columnList . bnBlock . afterPos ,
96+ // select content of remaining column:
97+ column . bnBlock . afterPos + 1 ,
98+ columnList . bnBlock . afterPos - 2 ,
99+ blockToMove ,
100+ blockToMove . size , // append existing content to blockToMove
101+ false ,
102+ ) ,
103+ ) ;
104+ const pos = tr . doc . resolve ( column . bnBlock . beforePos ) ;
105+ tr . setSelection ( TextSelection . between ( pos , pos ) ) ;
106+
107+ return pos ;
108+ } else {
109+ // replaces the column list with the blockToMove slice, prepended with the content of the remaining column
110+ tr . step (
111+ new ReplaceAroundStep (
112+ // replace entire column list
113+ columnList . bnBlock . beforePos ,
114+ columnList . bnBlock . afterPos ,
115+ // select content of existing column:
116+ columnList . bnBlock . beforePos + 2 ,
117+ column . bnBlock . beforePos - 1 ,
118+ blockToMove ,
119+ 0 , // prepend existing content to blockToMove
120+ false ,
121+ ) ,
122+ ) ;
123+ const pos = tr . doc . resolve ( tr . mapping . map ( column . bnBlock . beforePos - 1 ) ) ;
124+ tr . setSelection ( TextSelection . between ( pos , pos ) ) ;
125+
126+ return pos ;
127+ }
128+ } else if ( shouldRemoveColumn ) {
129+ if ( isFirstColumn ) {
130+ // delete column
131+ tr . delete ( column . bnBlock . beforePos , column . bnBlock . afterPos ) ;
132+
133+ // move before columnlist
134+ tr . insert ( columnList . bnBlock . beforePos , blockToMove . content ) ;
135+
136+ const pos = tr . doc . resolve ( columnList . bnBlock . beforePos ) ;
137+ tr . setSelection ( TextSelection . between ( pos , pos ) ) ;
138+
139+ return pos ;
140+ } else {
141+ // just delete the </column><column> closing and opening tags to merge the columns
142+ tr . delete ( column . bnBlock . beforePos - 1 , column . bnBlock . beforePos + 1 ) ;
143+ const pos = tr . doc . resolve ( column . bnBlock . beforePos - 1 ) ;
144+
145+ return pos ;
146+ }
147+ } else {
148+ // delete block
149+ tr . delete ( blockInfo . bnBlock . beforePos , blockInfo . bnBlock . afterPos ) ;
150+ if ( isFirstColumn ) {
151+ // move before columnlist
152+ tr . insert ( columnList . bnBlock . beforePos - 1 , blockToMove . content ) ;
153+ } else {
154+ // append block to previous column
155+ tr . insert ( column . bnBlock . beforePos - 1 , blockToMove . content ) ;
156+ }
157+ const pos = tr . doc . resolve ( column . bnBlock . beforePos - 1 ) ;
158+ tr . setSelection ( TextSelection . between ( pos , pos ) ) ;
159+
160+ return pos ;
161+ }
162+ }
13163
14164export function removeAndInsertBlocks <
15165 BSchema extends BlockSchema ,
@@ -70,15 +220,29 @@ export function removeAndInsertBlocks<
70220 }
71221
72222 const oldDocSize = tr . doc . nodeSize ;
73- // Checks if the block is the only child of its parent. In this case, we
74- // need to delete the parent `blockGroup` node instead of just the
75- // `blockContainer`.
223+
76224 const $pos = tr . doc . resolve ( pos - removedSize ) ;
77- if (
225+ if ( $pos . node ( ) . type . name === "column" && $pos . node ( ) . childCount === 1 ) {
226+ // Checks if the block is the only child of a parent `column` node. In
227+ // this case, we need to collapse the `column` or parent `columnList`,
228+ // depending on if the `columnList` has more than 2 children. This is
229+ // handled by `moveFirstBlockInColumn`.
230+ const $newPos = moveFirstBlockInColumn ( tr , $pos ) ;
231+ // Instead of deleting it, `moveFirstBlockInColumn` moves the block in
232+ // order to handle the columns after, so we have to delete it manually.
233+ tr . replace (
234+ $newPos . pos ,
235+ $newPos . pos + $newPos . nodeAfter ! . nodeSize ,
236+ Slice . empty ,
237+ ) ;
238+ } else if (
78239 $pos . node ( ) . type . name === "blockGroup" &&
79240 $pos . node ( $pos . depth - 1 ) . type . name !== "doc" &&
80241 $pos . node ( ) . childCount === 1
81242 ) {
243+ // Checks if the block is the only child of a parent `blockGroup` node.
244+ // In this case, we need to delete the parent `blockGroup` node instead
245+ // of just the `blockContainer`.
82246 tr . delete ( $pos . before ( ) , $pos . after ( ) ) ;
83247 } else {
84248 tr . delete ( pos - removedSize , pos - removedSize + node . nodeSize ) ;
@@ -89,7 +253,7 @@ export function removeAndInsertBlocks<
89253 return false ;
90254 } ) ;
91255
92- // Throws an error if now all blocks could be found.
256+ // Throws an error if not all blocks could be found.
93257 if ( idsOfBlocksToRemove . size > 0 ) {
94258 const notFoundIds = [ ...idsOfBlocksToRemove ] . join ( "\n" ) ;
95259
0 commit comments