Skip to content

Commit fcc0d12

Browse files
committed
Added fixColumnList function
1 parent a521b8f commit fcc0d12

File tree

1 file changed

+160
-68
lines changed

1 file changed

+160
-68
lines changed

packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts

Lines changed: 160 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Fragment, ResolvedPos, Slice, type Node } from "prosemirror-model";
1+
import { ResolvedPos, type Node } from "prosemirror-model";
22
import { TextSelection, type Transaction } from "prosemirror-state";
33
import { ReplaceAroundStep } from "prosemirror-transform";
44
import type { Block, PartialBlock } from "../../../../blocks/defaultBlocks.js";
@@ -161,6 +161,144 @@ export function moveFirstBlockInColumn(
161161
}
162162
}
163163

164+
/**
165+
* Checks if a `column` node is empty, i.e. if it has only a single empty
166+
* block.
167+
* @param column The column to check.
168+
* @returns Whether the column is empty.
169+
*/
170+
function isEmptyColumn(column: Node) {
171+
if (!column || column.type.name !== "column") {
172+
throw new Error("Invalid columnPos: does not point to column node.");
173+
}
174+
175+
const blockContainer = column.firstChild;
176+
if (!blockContainer) {
177+
throw new Error("Invalid column: does not have child node.");
178+
}
179+
180+
const blockContent = blockContainer.firstChild;
181+
if (!blockContent) {
182+
throw new Error("Invalid blockContainer: does not have child node.");
183+
}
184+
185+
return (
186+
column.childCount === 1 &&
187+
blockContainer.childCount === 1 &&
188+
blockContent.type.spec.content === "inline*" &&
189+
blockContent.content.content.length === 0
190+
);
191+
}
192+
193+
/**
194+
* Removes all empty `column` nodes in a `columnList`. A `column` node is empty
195+
* if it has only a single empty block. If, however, removing the `column`s
196+
* leaves the `columnList` that has fewer than two, ProseMirror will re-add
197+
* empty columns.
198+
* @param tr The `Transaction` to add the changes to.
199+
* @param columnListPos The position just before the `columnList` node.
200+
*/
201+
function removeEmptyColumns(tr: Transaction, columnListPos: number) {
202+
const $columnListPos = tr.doc.resolve(columnListPos);
203+
const columnList = $columnListPos.nodeAfter;
204+
if (!columnList || columnList.type.name !== "columnList") {
205+
throw new Error(
206+
"Invalid columnListPos: does not point to columnList node.",
207+
);
208+
}
209+
210+
for (
211+
let columnIndex = columnList.childCount - 1;
212+
columnIndex >= 0;
213+
columnIndex--
214+
) {
215+
const columnPos = tr.doc
216+
.resolve($columnListPos.pos + 1)
217+
.posAtIndex(columnIndex);
218+
const $columnPos = tr.doc.resolve(columnPos);
219+
const column = $columnPos.nodeAfter;
220+
if (!column || column.type.name !== "column") {
221+
throw new Error("Invalid columnPos: does not point to column node.");
222+
}
223+
224+
if (isEmptyColumn(column)) {
225+
tr.delete(columnPos, columnPos + column?.nodeSize);
226+
}
227+
}
228+
}
229+
230+
/**
231+
* Fixes potential issues in a `columnList` node after a
232+
* `blockContainer`/`column` node is (re)moved from it:
233+
*
234+
* - Removes all empty `column` nodes. A `column` node is empty if it has only
235+
* a single empty block.
236+
* - If all but one `column` nodes are empty, replaces the `columnList` with
237+
* the content of the non-empty `column`.
238+
* - If all `column` nodes are empty, removes the `columnList` entirely.
239+
* @param tr The `Transaction` to add the changes to.
240+
* @param columnListPos
241+
* @returns The position just before the `columnList` node.
242+
*/
243+
export function fixColumnList(tr: Transaction, columnListPos: number) {
244+
removeEmptyColumns(tr, columnListPos);
245+
246+
const $columnListPos = tr.doc.resolve(columnListPos);
247+
const columnList = $columnListPos.nodeAfter;
248+
if (!columnList || columnList.type.name !== "columnList") {
249+
throw new Error(
250+
"Invalid columnListPos: does not point to columnList node.",
251+
);
252+
}
253+
254+
if (columnList.childCount > 2) {
255+
return;
256+
}
257+
258+
const firstColumnBeforePos = columnListPos + 1;
259+
const $firstColumnBeforePos = tr.doc.resolve(firstColumnBeforePos);
260+
const firstColumn = $firstColumnBeforePos.nodeAfter;
261+
const lastColumnAfterPos = columnListPos + columnList.nodeSize - 1;
262+
const $lastColumnAfterPos = tr.doc.resolve(lastColumnAfterPos);
263+
const lastColumn = $lastColumnAfterPos.nodeBefore;
264+
if (!firstColumn || !lastColumn) {
265+
throw new Error("Invalid columnList: does not have child node.");
266+
}
267+
268+
const firstColumnEmpty = isEmptyColumn(firstColumn);
269+
const lastColumnEmpty = isEmptyColumn(lastColumn);
270+
271+
if (firstColumnEmpty && lastColumnEmpty) {
272+
tr.delete(columnListPos, columnListPos + columnList.nodeSize);
273+
274+
return;
275+
}
276+
277+
if (firstColumnEmpty) {
278+
const lastColumnContent = tr.doc.slice(
279+
lastColumnAfterPos - lastColumn.nodeSize + 1,
280+
lastColumnAfterPos - 1,
281+
);
282+
283+
tr.delete(columnListPos, columnListPos + columnList.nodeSize);
284+
tr.insert(columnListPos, lastColumnContent.content);
285+
286+
return;
287+
}
288+
289+
if (lastColumnEmpty) {
290+
const firstColumnContent = tr.doc.slice(
291+
firstColumnBeforePos + 1,
292+
firstColumnBeforePos + firstColumn.nodeSize - 1,
293+
);
294+
295+
tr.delete(columnListPos, columnListPos + columnList.nodeSize);
296+
tr.insert(columnListPos, firstColumnContent.content);
297+
298+
return;
299+
}
300+
}
301+
164302
export function removeAndInsertBlocks<
165303
BSchema extends BlockSchema,
166304
I extends InlineContentSchema,
@@ -222,85 +360,32 @@ export function removeAndInsertBlocks<
222360
const oldDocSize = tr.doc.nodeSize;
223361

224362
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 (
363+
if (
279364
node.type.name === "column" &&
280365
node.attrs.id !== $pos.nodeAfter?.attrs.id
281366
) {
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:
367+
// This is a hacky work around to handle removing all columns in a
368+
// columnList. This is what happens when removing the last 2 columns:
286369
//
287370
// 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.
371+
// 2. `fixColumnList` runs, removing the `columnList` and inserting the
372+
// contents of the last column in its place.
373+
// 3. `removedSize` increases not just by the size of the second-to-last
374+
// `column`, but also by the positions removed due to running
375+
// `fixColumnList`. Some of these positions are after the contents of the
376+
// last `column`, namely just after the `column` and `columnList`.
292377
// 3. `tr.doc.descendants` traverses to the last `column`.
293378
// 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.
379+
// last `column`. This causes `pos - removedSize` to point to an
380+
// incorrect position, as it expects that the difference in document size
381+
// accounted for by `removedSize` comes before the block being removed.
297382
// 5. The deletion is offset by 3, because of those removed positions
298383
// included in `removedSize` that occur after the last `column`.
299384
//
300385
// Hence why we have to shift the start of the deletion range back by 3.
301386
// 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.
387+
// the size of the second `column`. Since it's been removed, we actually
388+
// care about the size of its children - a difference of 2 positions.
304389
tr.delete(pos - removedSize + 3, pos - removedSize + node.nodeSize + 1);
305390
} else if (
306391
$pos.node().type.name === "blockGroup" &&
@@ -314,6 +399,13 @@ export function removeAndInsertBlocks<
314399
} else {
315400
tr.delete(pos - removedSize, pos - removedSize + node.nodeSize);
316401
}
402+
403+
if ($pos.node().type.name === "column") {
404+
fixColumnList(tr, $pos.before(-1));
405+
} else if ($pos.node().type.name === "columnList") {
406+
fixColumnList(tr, $pos.before());
407+
}
408+
317409
const newDocSize = tr.doc.nodeSize;
318410
removedSize += oldDocSize - newDocSize;
319411

0 commit comments

Comments
 (0)