Skip to content

Commit 07b8313

Browse files
committed
feat(text editor): handle list changes with no selection
- instead of trying to convert nested list nodes and creating extra transactions we limit functionality
1 parent fba00f3 commit 07b8313

File tree

2 files changed

+74
-39
lines changed

2 files changed

+74
-39
lines changed

src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
toggleList,
1919
Dispatch,
2020
} from './utils/list-utils';
21-
import { adjustSelectionToFullBlocks } from './utils/selection-utils';
2221
import { copyPasteLinkCommand } from './utils/link-utils';
2322
import { findAncestorDepthOfType } from './utils/node-utils';
2423

@@ -217,42 +216,42 @@ const createWrapInCommand = (
217216
};
218217

219218
/**
220-
* Handles list operations when there is no selection.
219+
* Handles list operations when there is no selection (cursor only).
220+
* If the cursor is within a list item, only that list item is affected.
221221
*
222-
* @param state - The current editor state.
223-
* @param type - The type of list to toggle.
224-
* @param schema - The ProseMirror schema.
225-
* @param otherType - The other type of list to convert to.
226-
* @param dispatch - The dispatch function.
227-
* @returns A command for handling list operations when there is no selection.
222+
* @param EditorState - state - The current editor state.
223+
* @param NodeType - type - The type of list to toggle.
224+
* @param Schema - schema - The ProseMirror schema.
225+
* @param Function - dispatch - The dispatch function.
226+
* @returns boolean - True if the command was executed.
228227
*/
229-
const handleListNoSelection = (
230-
state: EditorState,
231-
type: NodeType,
232-
schema: Schema,
233-
otherType: NodeType,
234-
dispatch: Dispatch,
235-
) => {
228+
const handleListNoSelection = (state, type, schema, dispatch) => {
236229
const { $from } = state.selection;
237-
const blockFrom = $from.start();
238-
const blockTo = $from.end();
239-
const adjustedTr = state.tr.setSelection(
240-
new TextSelection(
241-
state.doc.resolve(blockFrom),
242-
state.doc.resolve(blockTo),
243-
),
230+
// Find the nearest list_item ancestor.
231+
const listItemDepth = findAncestorDepthOfType(
232+
$from,
233+
schema.nodes.list_item,
244234
);
245-
const newState = state.apply(adjustedTr);
246235

247-
if (isInListOfType(newState, type)) {
248-
return removeListNodes(newState, type, schema, dispatch);
236+
if (listItemDepth === null) {
237+
// Not inside a list item; fallback to toggling list on the current block.
238+
return toggleList(type)(state, dispatch);
249239
}
250240

251-
if (isInListOfType(newState, otherType)) {
252-
return convertAllListNodes(newState, otherType, type, dispatch);
253-
}
241+
// Get the content positions within the list item
242+
const listItemStart = $from.start(listItemDepth);
243+
const listItemEnd = $from.end(listItemDepth);
244+
245+
// Set selection to the current list item.
246+
const tr = state.tr.setSelection(
247+
new TextSelection(
248+
state.doc.resolve(listItemStart),
249+
state.doc.resolve(listItemEnd),
250+
),
251+
);
252+
const newState = state.apply(tr);
254253

255-
return toggleList(type)(newState, dispatch);
254+
return sinkListItem(schema.nodes.list_item)(newState, dispatch);
256255
};
257256

258257
/**
@@ -272,7 +271,7 @@ const handleListWithSelection = (
272271
otherType: NodeType,
273272
dispatch: Dispatch,
274273
) => {
275-
const { $from } = state.selection;
274+
const { $from, $to } = state.selection;
276275
const listItemType = schema.nodes.list_item;
277276
const ancestorDepth = findAncestorDepthOfType($from, listItemType);
278277

@@ -291,14 +290,7 @@ const handleListWithSelection = (
291290
return convertAllListNodes(state, otherType, type, dispatch);
292291
}
293292

294-
const { from, to } = adjustSelectionToFullBlocks(state);
295-
if (from >= to) {
296-
return false;
297-
}
298-
299-
const modifiedTr = state.tr.setSelection(
300-
new TextSelection(state.doc.resolve(from), state.doc.resolve(to)),
301-
);
293+
const modifiedTr = state.tr.setSelection(new TextSelection($from, $to));
302294
const updatedState = state.apply(modifiedTr);
303295

304296
return wrapInList(type)(updatedState, dispatch);
@@ -329,7 +321,7 @@ export const createListCommand = (
329321
const otherType = getOtherListType(schema, listTypeName);
330322

331323
return noSelection
332-
? handleListNoSelection(state, type, schema, otherType, dispatch)
324+
? handleListNoSelection(state, type, schema, dispatch)
333325
: handleListWithSelection(state, type, schema, otherType, dispatch);
334326
};
335327

src/components/text-editor/prosemirror-adapter/menu/utils/list-utils.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ export const isInListOfType = (
2424
return false;
2525
};
2626

27+
/**
28+
* Get the other list type from the current list type.
29+
* @param schema - The schema to use.
30+
* @param currentType - The current list type.
31+
* @returns The other list type.
32+
*/
2733
export const getOtherListType = (
2834
schema: Schema,
2935
currentType: string,
@@ -198,3 +204,40 @@ export const toggleList = (listType: NodeType) => {
198204
}
199205
};
200206
};
207+
208+
/**
209+
* Converts a single list node from one type to another.
210+
*/
211+
export const convertSingleListNode = (
212+
state: EditorState,
213+
fromType: NodeType,
214+
toType: NodeType,
215+
dispatch: Dispatch,
216+
): boolean => {
217+
const { $from } = state.selection;
218+
const tr = state.tr;
219+
220+
// Find the nearest parent list of fromType
221+
for (let depth = $from.depth; depth > 0; depth--) {
222+
const node = $from.node(depth);
223+
if (node.type === fromType) {
224+
const pos = $from.before(depth);
225+
const newNode = toType.create(
226+
convertListAttributes(fromType, toType, node.attrs),
227+
node.content,
228+
node.marks,
229+
);
230+
if (dispatch) {
231+
dispatch(
232+
tr
233+
.replaceWith(pos, pos + node.nodeSize, newNode)
234+
.scrollIntoView(),
235+
);
236+
}
237+
238+
return true;
239+
}
240+
}
241+
242+
return false;
243+
};

0 commit comments

Comments
 (0)