Skip to content

Commit

Permalink
feat(text editor): handle list changes with no selection
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
john-traas committed Feb 21, 2025
1 parent fba00f3 commit 8eb4802
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import {
convertAllListNodes,
toggleList,
Dispatch,
convertSingleListNode,

Check failure on line 20 in src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts

View workflow job for this annotation

GitHub Actions / Lint

Remove this unused import of 'convertSingleListNode'

Check failure on line 20 in src/components/text-editor/prosemirror-adapter/menu/menu-commands.ts

View workflow job for this annotation

GitHub Actions / Lint

'convertSingleListNode' is defined but never used. Allowed unused vars must match /^h$/u
} from './utils/list-utils';
import { adjustSelectionToFullBlocks } from './utils/selection-utils';
import { copyPasteLinkCommand } from './utils/link-utils';
import { findAncestorDepthOfType } from './utils/node-utils';

Expand Down Expand Up @@ -217,42 +217,42 @@ const createWrapInCommand = (
};

/**
* Handles list operations when there is no selection.
* Handles list operations when there is no selection (cursor only).
* If the cursor is within a list item, only that list item is affected.
*
* @param state - The current editor state.
* @param type - The type of list to toggle.
* @param schema - The ProseMirror schema.
* @param otherType - The other type of list to convert to.
* @param dispatch - The dispatch function.
* @returns A command for handling list operations when there is no selection.
* @param EditorState - state - The current editor state.
* @param NodeType - type - The type of list to toggle.
* @param Schema - schema - The ProseMirror schema.
* @param Function - dispatch - The dispatch function.
* @returns boolean - True if the command was executed.
*/
const handleListNoSelection = (
state: EditorState,
type: NodeType,
schema: Schema,
otherType: NodeType,
dispatch: Dispatch,
) => {
const handleListNoSelection = (state, type, schema, dispatch) => {
const { $from } = state.selection;
const blockFrom = $from.start();
const blockTo = $from.end();
const adjustedTr = state.tr.setSelection(
new TextSelection(
state.doc.resolve(blockFrom),
state.doc.resolve(blockTo),
),
// Find the nearest list_item ancestor.
const listItemDepth = findAncestorDepthOfType(
$from,
schema.nodes.list_item,
);
const newState = state.apply(adjustedTr);

if (isInListOfType(newState, type)) {
return removeListNodes(newState, type, schema, dispatch);
if (listItemDepth === null) {
// Not inside a list item; fallback to toggling list on the current block.
return toggleList(type)(state, dispatch);
}

if (isInListOfType(newState, otherType)) {
return convertAllListNodes(newState, otherType, type, dispatch);
}
// Get the content positions within the list item
const listItemStart = $from.start(listItemDepth);
const listItemEnd = $from.end(listItemDepth);

// Set selection to the current list item.
const tr = state.tr.setSelection(
new TextSelection(
state.doc.resolve(listItemStart),
state.doc.resolve(listItemEnd),
),
);
const newState = state.apply(tr);

return toggleList(type)(newState, dispatch);
return sinkListItem(schema.nodes.list_item)(newState, dispatch);
};

/**
Expand All @@ -272,7 +272,7 @@ const handleListWithSelection = (
otherType: NodeType,
dispatch: Dispatch,
) => {
const { $from } = state.selection;
const { $from, $to } = state.selection;
const listItemType = schema.nodes.list_item;
const ancestorDepth = findAncestorDepthOfType($from, listItemType);

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

const { from, to } = adjustSelectionToFullBlocks(state);
if (from >= to) {
return false;
}

const modifiedTr = state.tr.setSelection(
new TextSelection(state.doc.resolve(from), state.doc.resolve(to)),
);
const modifiedTr = state.tr.setSelection(new TextSelection($from, $to));
const updatedState = state.apply(modifiedTr);

return wrapInList(type)(updatedState, dispatch);
Expand Down Expand Up @@ -329,7 +322,7 @@ export const createListCommand = (
const otherType = getOtherListType(schema, listTypeName);

return noSelection
? handleListNoSelection(state, type, schema, otherType, dispatch)
? handleListNoSelection(state, type, schema, dispatch)
: handleListWithSelection(state, type, schema, otherType, dispatch);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export const isInListOfType = (
return false;
};

/**
* Get the other list type from the current list type.
* @param schema - The schema to use.
* @param currentType - The current list type.
* @returns The other list type.
*/
export const getOtherListType = (
schema: Schema,
currentType: string,
Expand Down Expand Up @@ -198,3 +204,40 @@ export const toggleList = (listType: NodeType) => {
}
};
};

/**
* Converts a single list node from one type to another.
*/
export const convertSingleListNode = (
state: EditorState,
fromType: NodeType,
toType: NodeType,
dispatch: Dispatch,
): boolean => {
const { $from } = state.selection;
const tr = state.tr;

// Find the nearest parent list of fromType
for (let depth = $from.depth; depth > 0; depth--) {
const node = $from.node(depth);
if (node.type === fromType) {
const pos = $from.before(depth);
const newNode = toType.create(
convertListAttributes(fromType, toType, node.attrs),
node.content,
node.marks,
);
if (dispatch) {
dispatch(
tr
.replaceWith(pos, pos + node.nodeSize, newNode)
.scrollIntoView(),
);
}

return true;
}
}

return false;
};

0 comments on commit 8eb4802

Please sign in to comment.