Skip to content

Commit e42dfc1

Browse files
committed
feat: added getListItemsToTransform to detect affected items, updated sink to prevent deep nesting
1 parent 4c1a0df commit e42dfc1

File tree

2 files changed

+144
-47
lines changed

2 files changed

+144
-47
lines changed

src/extensions/markdown/Lists/commands.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,4 +267,25 @@ describe('sinkOnlySelectedListItem', () => {
267267
),
268268
),
269269
));
270+
271+
it('removes selection markers without changing list structure for first item', () =>
272+
apply(
273+
doc(ul(li(p('1<a><b>1')), li(p('22')), li(p('33')))),
274+
sink,
275+
doc(ul(li(p('11')), li(p('22')), li(p('33')))),
276+
));
277+
278+
it('indents the second item into a sublist when selected', () =>
279+
apply(
280+
doc(ul(li(p('11')), li(p('2<a><b>2')), li(p('33')))),
281+
sink,
282+
doc(ul(li(p('11'), ul(li(p('22')))), li(p('33')))),
283+
));
284+
285+
it('indents only the selected item when selection spans two items', () =>
286+
apply(
287+
doc(ul(li(p('11')), li(p('2<a>2')), li(p('3<b>3')))),
288+
sink,
289+
doc(ul(li(p('11'), ul(li(p('22')))), li(p('33')))),
290+
));
270291
});
Lines changed: 123 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Fragment, type NodeRange, type NodeType, Slice} from 'prosemirror-model';
1+
import {Fragment, type Node, type NodeRange, type NodeType, Slice} from 'prosemirror-model';
22
import {wrapInList} from 'prosemirror-schema-list';
33
import type {Command, Transaction} from 'prosemirror-state';
44
import {ReplaceAroundStep, liftTarget} from 'prosemirror-transform';
@@ -30,11 +30,17 @@ export const joinPrevList = joinPreviousBlock({
3030
sinks list items deeper.
3131
*/
3232
const sink = (tr: Transaction, range: NodeRange, itemType: NodeType) => {
33-
const before = tr.mapping.map(range.start);
34-
const after = tr.mapping.map(range.end);
35-
const startIndex = tr.mapping.map(range.startIndex);
36-
33+
console.warn('sink', '=========>>>');
34+
const before = range.start;
35+
const after = range.end;
36+
const startIndex = range.startIndex;
3737
const parent = range.parent;
38+
39+
console.log('before', before);
40+
console.log('after', after);
41+
console.log('startIndex', startIndex);
42+
console.log('parent', parent);
43+
3844
const nodeBefore = parent.child(startIndex - 1);
3945

4046
const nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type === parent.type;
@@ -56,65 +62,135 @@ const sink = (tr: Transaction, range: NodeRange, itemType: NodeType) => {
5662
true,
5763
),
5864
);
65+
66+
// After sinking, lift any nested <li> children back out
67+
const from = range.start;
68+
const $movedPos = tr.doc.resolve(from);
69+
const movedItem = $movedPos.nodeAfter;
70+
71+
if (movedItem) {
72+
movedItem.forEach((child, offset) => {
73+
if (child.type === parent.type) {
74+
const nestedStart = from + offset + 1;
75+
const nestedEnd = nestedStart + child.nodeSize;
76+
const $liStart = tr.doc.resolve(nestedStart + 1);
77+
const $liEnd = tr.doc.resolve(nestedEnd - 1);
78+
const liftRange = $liStart.blockRange($liEnd, (node) => node.type === itemType);
79+
80+
if (liftRange) {
81+
const targetDepth = liftTarget(liftRange);
82+
if (targetDepth !== null) {
83+
tr.lift(liftRange, targetDepth);
84+
}
85+
}
86+
}
87+
});
88+
}
89+
5990
return true;
6091
};
6192

93+
const isListItemNode = (node: Node, itemType: NodeType) =>
94+
node.childCount > 0 && node.firstChild!.type === itemType;
95+
96+
/**
97+
* Returns a map of list item positions that should be transformed (e.g., sink or lift).
98+
*/
99+
function getListItemsToTransform(
100+
tr: Transaction,
101+
itemType: NodeType,
102+
{
103+
start,
104+
end,
105+
from,
106+
to,
107+
}: {
108+
start: number;
109+
end: number;
110+
from: number;
111+
to: number;
112+
},
113+
): Map<number, number> {
114+
// console.warn('getListItemsToTransform', start, end, from, to);
115+
const listItemsPoses = new Map<number, number>();
116+
let pos = start;
117+
118+
while (pos <= end) {
119+
const node = tr.doc.nodeAt(pos);
120+
121+
// console.log('pos', pos);
122+
// console.log('node', node?.type.name);
123+
124+
if (node?.type === itemType) {
125+
// console.log('list pos ----->: ', pos, pos + node.nodeSize);
126+
const isBeetwwen =
127+
(pos <= from && pos + node.nodeSize >= from) ||
128+
(pos <= to && pos + node.nodeSize >= to);
129+
if (isBeetwwen) {
130+
// console.warn(isBeetwwen);
131+
listItemsPoses.set(pos, pos + node.nodeSize);
132+
} else {
133+
// console.log(isBeetwwen, pos, pos + node.nodeSize, 'from:to', from, to);
134+
}
135+
}
136+
137+
pos++;
138+
}
139+
140+
return listItemsPoses;
141+
}
142+
62143
export function sinkOnlySelectedListItem(itemType: NodeType): Command {
63144
return ({tr, selection}, dispatch) => {
64-
const {$from, $to} = selection;
65-
const selectionRange = $from.blockRange(
66-
$to,
67-
(node) => node.childCount > 0 && node.firstChild!.type === itemType,
145+
const {$from, $to, from, to} = selection;
146+
const listItemSelectionRange = $from.blockRange($to, (node) =>
147+
isListItemNode(node, itemType),
68148
);
69-
if (!selectionRange) {
70-
return false;
71-
}
72-
73-
const {startIndex, parent, start, end} = selectionRange;
74-
if (startIndex === 0) {
75-
return false;
76-
}
77149

78-
const nodeBefore = parent.child(startIndex - 1);
79-
if (nodeBefore.type !== itemType) {
150+
if (!listItemSelectionRange) {
80151
return false;
81152
}
82153

83154
if (dispatch) {
84-
// lifts following list items sequentially to prepare correct nesting structure
85-
let currentEnd = end - 1;
86-
while (currentEnd > start) {
87-
const selectionEnd = tr.mapping.map($to.pos);
88-
89-
const $candidateBlockEnd = tr.doc.resolve(currentEnd);
90-
const candidateBlockStartPos = $candidateBlockEnd.before($candidateBlockEnd.depth);
91-
const $candidateBlockStart = tr.doc.resolve(candidateBlockStartPos);
92-
const candidateBlockRange = $candidateBlockStart.blockRange($candidateBlockEnd);
93-
94-
if (candidateBlockRange?.start) {
95-
const $rangeStart = tr.doc.resolve(candidateBlockRange.start);
96-
const shouldLift =
97-
candidateBlockRange.start > selectionEnd && isListNode($rangeStart.parent);
98-
99-
if (shouldLift) {
100-
currentEnd = candidateBlockRange.start;
101-
102-
const targetDepth = liftTarget(candidateBlockRange);
103-
if (targetDepth !== null) {
104-
tr.lift(candidateBlockRange, targetDepth);
105-
}
106-
}
107-
}
155+
const {start, end} = listItemSelectionRange;
156+
const listItemsPoses = getListItemsToTransform(tr, itemType, {
157+
start,
158+
end,
159+
from,
160+
to,
161+
});
108162

109-
currentEnd--;
110-
}
163+
console.warn(listItemsPoses, 'start: end', start, end);
164+
165+
for (const [startPos, endPos] of listItemsPoses) {
166+
const mappedStart = tr.mapping.map(startPos);
167+
const nodeStart = tr.doc.nodeAt(mappedStart);
111168

112-
// sinks the selected list item deeper into the list hierarchy
113-
sink(tr, selectionRange, itemType);
169+
const mappedEnd = tr.mapping.map(endPos);
170+
// const nodeEnd = tr.doc.nodeAt(mappedEnd);
114171

172+
console.log('startPos ---->', startPos);
173+
console.log('endPos ---->', endPos);
174+
175+
console.log('mapped startPos ---->', mappedStart);
176+
console.log('mapped endPos ---->', mappedEnd);
177+
178+
console.log('nodeStart ---->', nodeStart?.type.name);
179+
180+
const $mappedStart = tr.doc.resolve(mappedStart);
181+
const $mappedEnd = tr.doc.resolve(mappedEnd);
182+
const range = $mappedStart.blockRange($mappedEnd);
183+
184+
if (range) {
185+
console.log('sink ---->', range.start, range.end, range);
186+
sink(tr, range, itemType);
187+
}
188+
}
115189
dispatch(tr.scrollIntoView());
190+
116191
return true;
117192
}
193+
118194
return true;
119195
};
120196
}

0 commit comments

Comments
 (0)