Yes, thanks, i missed that api. I implemented the recursive solution using Fragment.childCount
/ Fragment.replaceChild
.
function findCommonListNode(
state: EditorState,
listTypes: Array<NodeType>
): { node: Node; from: number; to: number } | null {
const range = state.selection.$from.blockRange(state.selection.$to);
if (!range) {
return null;
}
const node = range.$from.node(-2);
if (!node || !listTypes.find((item) => item === node.type)) {
return null;
}
const from = range.$from.posAtIndex(0, -2);
return { node, from, to: from + node.nodeSize - 1 };
}
function updateContent(
content: Fragment,
targetListType: NodeType,
targetListItemType: NodeType,
listTypes: Array<NodeType>,
listItemTypes: Array<NodeType>
): Fragment {
let newContent = content;
for (var i = 0; i < content.childCount; i++) {
newContent = newContent.replaceChild(
i,
updateNode(
newContent.child(i),
targetListType,
targetListItemType,
listTypes,
listItemTypes
)
);
}
return newContent;
}
function getReplacementType(
node: Node,
target: NodeType,
options: Array<NodeType>
): NodeType | null {
return options.find((item) => item === node.type) ? target : null;
}
function updateNode(
node: Node,
targetListType: NodeType,
targetListItemType: NodeType,
listTypes: Array<NodeType>,
listItemTypes: Array<NodeType>
): Node {
const newContent = updateContent(
node.content,
targetListType,
targetListItemType,
listTypes,
listItemTypes
);
const replacementType =
getReplacementType(node, targetListType, listTypes) ||
getReplacementType(node, targetListItemType, listItemTypes);
if (replacementType) {
return replacementType.create(node.attrs, newContent, node.marks);
} else {
return node.copy(newContent);
}
}
export function buildWrapInListItem(
schema: Schema,
targetListType: NodeType,
targetListItemType: NodeType,
id: string,
attrs?: { [key: string]: string | number | boolean }
): MenuItemSpec {
const listTypes = getListTypes(schema);
const listItemTypes = getListItemTypes(schema);
const command = wrapInList(targetListType, attrs);
const commandAdapter: Command = (state, dispatch) => {
const result = command(state);
if (result) {
return command(state, dispatch);
}
const commonListNode = findCommonListNode(state, listTypes);
if (!commonListNode) {
return false;
}
if (dispatch) {
const updatedNode = updateNode(
commonListNode.node,
targetListType,
targetListItemType,
listTypes,
listItemTypes
);
let tr = state.tr;
tr = tr.replaceRangeWith(
commonListNode.from,
commonListNode.to,
updatedNode
);
tr = tr.setSelection(
new TextSelection(
tr.doc.resolve(state.selection.from),
tr.doc.resolve(state.selection.to)
)
);
dispatch(tr);
}
return true;
};
return {
id,
run: commandAdapter,
};
}
buildWrapInListItem
produces a command which runs the default wrapInList
command if it can, and if the selected range already is a list, runs custom logic to switch the list type.