Skip to content

Commit 8eb4802

Browse files
committed
feat(text editor): handle list changes with no selection
- instead of converting list nodes and creating extra transactions we limit functionality - transactions can quickly become out of sync and preserving the content of the text editor can become tricky - instead our approach will be to limit what users can do by implementing conditional access to commands - this conditional access will be in a separate PR
1 parent fba00f3 commit 8eb4802

File tree

2 files changed

+75
-39
lines changed

2 files changed

+75
-39
lines changed

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

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import {
1717
convertAllListNodes,
1818
toggleList,
1919
Dispatch,
20+
convertSingleListNode,
2021
} from './utils/list-utils';
21-
import { adjustSelectionToFullBlocks } from './utils/selection-utils';
2222
import { copyPasteLinkCommand } from './utils/link-utils';
2323
import { findAncestorDepthOfType } from './utils/node-utils';
2424

@@ -217,42 +217,42 @@ const createWrapInCommand = (
217217
};
218218

219219
/**
220-
* Handles list operations when there is no selection.
220+
* Handles list operations when there is no selection (cursor only).
221+
* If the cursor is within a list item, only that list item is affected.
221222
*
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.
223+
* @param EditorState - state - The current editor state.
224+
* @param NodeType - type - The type of list to toggle.
225+
* @param Schema - schema - The ProseMirror schema.
226+
* @param Function - dispatch - The dispatch function.
227+
* @returns boolean - True if the command was executed.
228228
*/
229-
const handleListNoSelection = (
230-
state: EditorState,
231-
type: NodeType,
232-
schema: Schema,
233-
otherType: NodeType,
234-
dispatch: Dispatch,
235-
) => {
229+
const handleListNoSelection = (state, type, schema, dispatch) => {
236230
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-
),
231+
// Find the nearest list_item ancestor.
232+
const listItemDepth = findAncestorDepthOfType(
233+
$from,
234+
schema.nodes.list_item,
244235
);
245-
const newState = state.apply(adjustedTr);
246236

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

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

255-
return toggleList(type)(newState, dispatch);
255+
return sinkListItem(schema.nodes.list_item)(newState, dispatch);
256256
};
257257

258258
/**
@@ -272,7 +272,7 @@ const handleListWithSelection = (
272272
otherType: NodeType,
273273
dispatch: Dispatch,
274274
) => {
275-
const { $from } = state.selection;
275+
const { $from, $to } = state.selection;
276276
const listItemType = schema.nodes.list_item;
277277
const ancestorDepth = findAncestorDepthOfType($from, listItemType);
278278

@@ -291,14 +291,7 @@ const handleListWithSelection = (
291291
return convertAllListNodes(state, otherType, type, dispatch);
292292
}
293293

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-
);
294+
const modifiedTr = state.tr.setSelection(new TextSelection($from, $to));
302295
const updatedState = state.apply(modifiedTr);
303296

304297
return wrapInList(type)(updatedState, dispatch);
@@ -329,7 +322,7 @@ export const createListCommand = (
329322
const otherType = getOtherListType(schema, listTypeName);
330323

331324
return noSelection
332-
? handleListNoSelection(state, type, schema, otherType, dispatch)
325+
? handleListNoSelection(state, type, schema, dispatch)
333326
: handleListWithSelection(state, type, schema, otherType, dispatch);
334327
};
335328

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)