@atlaskit/editor-plugin-block-menu
Version:
BlockMenu plugin for @atlaskit/editor-core
212 lines (211 loc) • 8.63 kB
JavaScript
import { createBlockTaskItem } from '@atlaskit/editor-common/transforms';
import { Fragment } from '@atlaskit/editor-prosemirror/model';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { isListWithIndentation } from '../nodeChecks';
/**
* Recursively converts nested lists to the target list type.
* This function handles the conversion of both the list container and its items,
* including any nested lists within those items.
*
* Important: taskList has a different nesting structure than bulletList/orderedList:
* - taskList: nested taskLists are SIBLINGS of taskItems in the parent taskList
* - bulletList/orderedList: nested lists are CHILDREN of listItems
*/
const transformList = (node, targetListType, targetItemType, unsupportedContent) => {
const schema = node.type.schema;
const taskListType = schema.nodes.taskList;
const isSourceTaskList = node.type === taskListType;
const isTargetTaskList = targetListType === taskListType.name;
const convertFromTaskListStructure = (node, targetListType, targetItemType) => {
const schema = node.type.schema;
const targetListNodeType = schema.nodes[targetListType];
const transformedContent = [];
node.forEach(child => {
if (isListWithIndentation(child.type.name, schema)) {
// This is a nested list - it should become a child of the previous item
if (transformedContent.length > 0) {
const previousItem = transformedContent[transformedContent.length - 1];
// Convert the nested list and add it to the previous item's content
const transformedNestedList = transformList(child, targetListType, targetItemType, unsupportedContent);
const newContent = previousItem.content.append(Fragment.from([transformedNestedList]));
const updatedItem = previousItem.type.create(previousItem.attrs, newContent);
transformedContent[transformedContent.length - 1] = updatedItem;
}
// If there's no previous item, skip this nested list (orphaned)
} else {
const transformedItem = transformListItem(child, targetListType, targetItemType);
if (transformedItem) {
transformedContent.push(transformedItem);
}
}
});
return targetListNodeType.create(node.attrs, transformedContent);
};
const convertToTaskListStructure = (node, targetListType, targetItemType) => {
const schema = node.type.schema;
const targetListNodeType = schema.nodes[targetListType];
const transformedContent = [];
node.forEach(itemNode => {
const transformedItem = transformListItem(itemNode, targetListType, targetItemType, true);
if (transformedItem) {
transformedContent.push(transformedItem);
}
itemNode.forEach(child => {
if (isListWithIndentation(child.type.name, schema)) {
transformedContent.push(transformList(child, targetListType, targetItemType, unsupportedContent));
}
});
});
return targetListNodeType.create(node.attrs, transformedContent);
};
const transformListItem = (itemNode, targetListType, targetItemType, excludeNestedLists = false) => {
const schema = itemNode.type.schema;
const targetItemNodeType = schema.nodes[targetItemType];
const isTargetTaskItem = targetItemType === 'taskItem';
const isSourceTaskItem = itemNode.type.name === 'taskItem';
const paragraphType = schema.nodes.paragraph;
if (isTargetTaskItem) {
const inlineContent = [];
let blockMarks = itemNode.marks;
itemNode.forEach(child => {
if (child.type === paragraphType) {
inlineContent.push(...child.children);
if (child.marks.length > 0) {
blockMarks = child.marks;
}
} else if (child.isInline) {
inlineContent.push(child);
// Nested lists will be extracted and placed as siblings in the taskList
} else if (!isListWithIndentation(child.type.name, schema)) {
unsupportedContent.push(child);
}
});
const {
blockTaskItem
} = schema.nodes;
if (blockTaskItem && blockMarks.length > 0 && expValEquals('platform_editor_small_font_size', 'isEnabled', true)) {
return createBlockTaskItem({
content: inlineContent,
marks: blockMarks,
schema
});
}
return targetItemNodeType.create({}, inlineContent);
}
const transformedContent = [];
if (isSourceTaskItem) {
transformedContent.push(paragraphType.create(null, itemNode.content));
} else {
itemNode.forEach(child => {
if (isListWithIndentation(child.type.name, schema)) {
if (excludeNestedLists) {
// Skip nested lists - they will be handled separately as siblings
return;
}
transformedContent.push(transformList(child, targetListType, targetItemType, unsupportedContent));
} else {
transformedContent.push(child);
}
});
}
if (transformedContent.length === 0) {
transformedContent.push(paragraphType.create());
}
return targetItemNodeType.create({}, transformedContent);
};
const convertList = (node, schema, targetListType, targetItemType) => {
const targetListNodeType = schema.nodes[targetListType];
const transformedContent = [];
node.forEach(childNode => {
const transformedItem = isListWithIndentation(childNode.type.name, schema) ? transformList(childNode, targetListType, targetItemType, unsupportedContent) : transformListItem(childNode, targetListType, targetItemType);
if (transformedItem) {
transformedContent.push(transformedItem);
}
});
return targetListNodeType.create(node.attrs, transformedContent);
};
if (isSourceTaskList && !isTargetTaskList) {
return convertFromTaskListStructure(node, targetListType, targetItemType);
} else if (!isSourceTaskList && isTargetTaskList) {
return convertToTaskListStructure(node, targetListType, targetItemType);
}
return convertList(node, schema, targetListType, targetItemType);
};
/**
* Transform step that converts between bulletList, orderedList, and taskList types.
* This step maintains the order and indentation of the list by recursively
* converting all nested lists while preserving the structure. It also handles
* conversion between listItem and taskItem types.
*
* When converting to taskList/taskItem, unsupported content (images, codeBlocks) is filtered out.
*
* @example
* Input (bulletList with nested bulletList):
* - bulletList
* - listItem "1"
* - bulletList
* - listItem "1.1"
* - bulletList
* - listItem "1.1.1"
* - listItem "1.2"
* - listItem "2"
*
* Output (orderedList with nested orderedList):
* 1. orderedList
* 1. listItem "1"
* 1. orderedList
* 1. listItem "1.1"
* 1. orderedList
* 1. listItem "1.1.1"
* 2. listItem "1.2"
* 2. listItem "2"
*
* @example
* Input (bulletList with nested taskList):
* - bulletList
* - listItem "Regular item"
* - taskList
* - taskItem "Task 1" (checked)
* - taskItem "Task 2" (unchecked)
*
* Output (orderedList with nested orderedList, taskItems converted to listItems):
* 1. orderedList
* 1. listItem "Regular item"
* 1. orderedList
* 1. listItem "Task 1"
* 2. listItem "Task 2"
*
* @example
* Input (bulletList to taskList, with paragraph extraction):
* - bulletList
* - listItem
* - paragraph "Text content"
* - listItem
* - paragraph "Text"
* - codeBlock "code"
* - mediaSingle (image)
*
* Output (taskList with text extracted from paragraphs, unsupported content filtered):
* - taskList
* - taskItem "Text content" (text extracted from paragraph)
* - taskItem "Text" (text extracted, codeBlock and image filtered out)
*
* @param nodes - The nodes to transform
* @param context - The transformation context containing schema and target node type
* @returns The transformed nodes
*/
export const listToListStep = (nodes, context) => {
const {
schema,
targetNodeTypeName
} = context;
const unsupportedContent = [];
const transformedNodes = nodes.map(node => {
if (isListWithIndentation(node.type.name, schema)) {
const targetItemType = targetNodeTypeName === 'taskList' ? 'taskItem' : 'listItem';
return transformList(node, targetNodeTypeName, targetItemType, unsupportedContent);
}
return node;
});
return [...transformedNodes, ...unsupportedContent];
};