1- import { Fragment , ResolvedPos , Slice , type Node } from "prosemirror-model" ;
2- import { TextSelection , type Transaction } from "prosemirror-state" ;
3- import { ReplaceAroundStep } from "prosemirror-transform" ;
1+ import { type Node } from "prosemirror-model" ;
2+ import { type Transaction } from "prosemirror-state" ;
43import type { Block , PartialBlock } from "../../../../blocks/defaultBlocks.js" ;
54import type {
65 BlockIdentifier ,
76 BlockSchema ,
87 InlineContentSchema ,
98 StyleSchema ,
109} from "../../../../schema/index.js" ;
11- import { getBlockInfoFromResolvedPos } from "../../../getBlockInfoFromPos.js" ;
1210import { blockToNode } from "../../../nodeConversions/blockToNode.js" ;
1311import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js" ;
1412import { 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: 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: 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: 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: 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 ( columnList . 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- }
13+ import { fixColumnList } from "./util/fixColumnList.js" ;
16314
16415export function removeAndInsertBlocks <
16516 BSchema extends BlockSchema ,
@@ -222,85 +73,32 @@ export function removeAndInsertBlocks<
22273 const oldDocSize = tr . doc . nodeSize ;
22374
22475 const $pos = tr . doc . resolve ( pos - removedSize ) ;
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 (
239- $pos . node ( ) . type . name === "columnList" &&
240- $pos . node ( ) . childCount === 2
241- ) {
242- // Checks whether removing the entire column would leave only a single
243- // remaining `column` node in the columnList. In this case, we need to
244- // collapse the column list.
245- const column = getBlockInfoFromResolvedPos ( $pos ) ;
246- if ( column . blockNoteType !== "column" ) {
247- throw new Error (
248- `Invalid block: ${ column . blockNoteType } was found as child of columnList.` ,
249- ) ;
250- }
251- const columnList = getParentBlockInfo ( tr . doc , column . bnBlock . beforePos ) ;
252- if ( ! columnList ) {
253- throw new Error (
254- `Invalid block: column was found without a parent columnList.` ,
255- ) ;
256- }
257- if ( columnList ?. blockNoteType !== "columnList" ) {
258- throw new Error (
259- `Invalid block: ${ columnList . blockNoteType } was found as a parent of column.` ,
260- ) ;
261- }
262-
263- if ( $pos . node ( ) . childCount === 1 ) {
264- tr . replaceWith (
265- columnList . bnBlock . beforePos ,
266- columnList . bnBlock . afterPos ,
267- Fragment . empty ,
268- ) ;
269- }
270-
271- tr . replaceWith (
272- columnList . bnBlock . beforePos ,
273- columnList . bnBlock . afterPos ,
274- $pos . index ( ) === 0
275- ? columnList . bnBlock . node . lastChild ! . content
276- : columnList . bnBlock . node . firstChild ! . content ,
277- ) ;
278- } else if (
76+ if (
27977 node . type . name === "column" &&
28078 node . attrs . id !== $pos . nodeAfter ?. attrs . id
28179 ) {
282- // This is a hacky work around to handle an edge case with the previous
283- // `if else` block. When each `column` of a `columnList` is in the
284- // `blocksToRemove` array, this is what happens once all but the last 2
285- // columns are removed:
80+ // This is a hacky work around to handle removing all columns in a
81+ // columnList. This is what happens when removing the last 2 columns:
28682 //
28783 // 1. The second-to-last `column` is removed.
288- // 2. The last `column` and wrapping `columnList` are collapsed.
289- // 3. `removedSize` increases by the size of the removed column, and more
290- // due to positions at the starts/ends of the last `column` and wrapping
291- // `columnList` also getting removed.
84+ // 2. `fixColumnList` runs, removing the `columnList` and inserting the
85+ // contents of the last column in its place.
86+ // 3. `removedSize` increases not just by the size of the second-to-last
87+ // `column`, but also by the positions removed due to running
88+ // `fixColumnList`. Some of these positions are after the contents of the
89+ // last `column`, namely just after the `column` and `columnList`.
29290 // 3. `tr.doc.descendants` traverses to the last `column`.
29391 // 4. `removedSize` now includes positions that were removed after the
294- // last `column`. In order for `pos - removedSize` to correctly point to
295- // the start of the nodes that were previously wrapped by the last
296- // `column`, `removedPos` must only include positions removed before it .
92+ // last `column`. This causes `pos - removedSize` to point to an
93+ // incorrect position, as it expects that the difference in document size
94+ // accounted for by `removedSize` comes before the block being removed .
29795 // 5. The deletion is offset by 3, because of those removed positions
29896 // included in `removedSize` that occur after the last `column`.
29997 //
30098 // Hence why we have to shift the start of the deletion range back by 3.
30199 // The offset for the end of the range is smaller as `node.nodeSize` is
302- // the size of the whole second `column`, whereas now we are left with
303- // just its children since it's collapsed - a difference of 2 positions.
100+ // the size of the second `column`. Since it's been removed, we actually
101+ // care about the size of its children - a difference of 2 positions.
304102 tr . delete ( pos - removedSize + 3 , pos - removedSize + node . nodeSize + 1 ) ;
305103 } else if (
306104 $pos . node ( ) . type . name === "blockGroup" &&
@@ -314,6 +112,13 @@ export function removeAndInsertBlocks<
314112 } else {
315113 tr . delete ( pos - removedSize , pos - removedSize + node . nodeSize ) ;
316114 }
115+
116+ if ( $pos . node ( ) . type . name === "column" ) {
117+ fixColumnList ( tr , $pos . before ( - 1 ) ) ;
118+ } else if ( $pos . node ( ) . type . name === "columnList" ) {
119+ fixColumnList ( tr , $pos . before ( ) ) ;
120+ }
121+
317122 const newDocSize = tr . doc . nodeSize ;
318123 removedSize += oldDocSize - newDocSize ;
319124
0 commit comments