@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
466 lines (444 loc) • 21.5 kB
JavaScript
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
import { findChildrenByType, findParentNodeOfType, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { mapSlice } from '../utils/slice';
import { getSupportedListTypesSet, isBulletOrOrderedList, isTaskList, convertBlockToInlineContent } from './list-utils';
var getContentSupportChecker = function getContentSupportChecker(targetNodeType) {
return function (node) {
try {
return targetNodeType.validContent(Fragment.from(node));
} catch (_unused) {
return false;
}
};
};
export var createBlockTaskItem = function createBlockTaskItem(_ref) {
var attrs = _ref.attrs,
content = _ref.content,
marks = _ref.marks,
schema = _ref.schema;
var _schema$nodes = schema.nodes,
blockTaskItem = _schema$nodes.blockTaskItem,
paragraph = _schema$nodes.paragraph;
var newParagraph = paragraph.createChecked(null, content, marks === null || marks === void 0 ? void 0 : marks.filter(function (mark) {
return blockTaskItem.allowsMarkType(mark.type);
}));
return blockTaskItem.create(attrs !== null && attrs !== void 0 ? attrs : null, newParagraph);
};
var _transformListRecursively = function transformListRecursively(props, onhandleUnsupportedContent) {
var transformedItems = [];
var listNode = props.listNode,
isSourceBulletOrOrdered = props.isSourceBulletOrOrdered,
isTargetBulletOrOrdered = props.isTargetBulletOrOrdered,
isSourceTask = props.isSourceTask,
isTargetTask = props.isTargetTask,
supportedListTypes = props.supportedListTypes,
schema = props.schema,
targetNodeType = props.targetNodeType;
var _schema$nodes2 = schema.nodes,
taskList = _schema$nodes2.taskList,
listItem = _schema$nodes2.listItem,
taskItem = _schema$nodes2.taskItem,
paragraph = _schema$nodes2.paragraph,
blockTaskItem = _schema$nodes2.blockTaskItem;
// gating behind platform_editor_small_font_size to support task lists with font size applied,
// but keep this solution general
var isBlockTaskEnabled = !!blockTaskItem && expValEquals('platform_editor_small_font_size', 'isEnabled', true);
/**
* Extracts paragraph children from a blockTaskItem, preserving their marks.
*/
var extractParagraphsFromBlockTaskItem = function extractParagraphsFromBlockTaskItem(node) {
var paragraphs = [];
node.forEach(function (child) {
if (child.type === paragraph) {
paragraphs.push(child);
}
});
return paragraphs;
};
listNode.forEach(function (child) {
if (isSourceBulletOrOrdered && isTargetTask) {
// Convert bullet/ordered => task
if (child.type === listItem) {
var inlineContent = [];
var nestedTaskLists = [];
var blockMarks = [];
child.forEach(function (grandChild) {
if (supportedListTypes.has(grandChild.type) && grandChild.type !== taskList) {
nestedTaskLists.push(_transformListRecursively(_objectSpread(_objectSpread({}, props), {}, {
listNode: grandChild
}), onhandleUnsupportedContent));
} else if (!getContentSupportChecker(taskItem)(grandChild) && !grandChild.isTextblock) {
onhandleUnsupportedContent === null || onhandleUnsupportedContent === void 0 || onhandleUnsupportedContent(grandChild);
} else {
if (isBlockTaskEnabled && grandChild.type === paragraph && grandChild.marks.length > 0) {
blockMarks = grandChild.marks;
}
inlineContent.push.apply(inlineContent, _toConsumableArray(convertBlockToInlineContent(grandChild, schema)));
}
});
if (isBlockTaskEnabled && blockMarks.length > 0) {
transformedItems.push(createBlockTaskItem({
content: inlineContent,
marks: blockMarks,
schema: schema
}));
} else {
transformedItems.push(taskItem.create(null, inlineContent.length > 0 ? inlineContent : null));
}
transformedItems.push.apply(transformedItems, nestedTaskLists);
}
} else if (isSourceTask && isTargetBulletOrOrdered) {
// Convert task => bullet/ordered
if (child.type === taskItem) {
var _inlineContent = _toConsumableArray(child.content.content);
// Transfer taskItem's block marks to the paragraph.
// Use listItem.allowsMarkType since the paragraph will be inside a listItem
// (which uses ParagraphWithFontSizeStage0 that allows fontSize).
var paragraphMarks = isBlockTaskEnabled && child.marks.length > 0 ? child.marks.filter(function (mark) {
return listItem.allowsMarkType(mark.type);
}) : undefined;
var paragraphNode = paragraph.create(null, _inlineContent.length > 0 ? _inlineContent : null, paragraphMarks);
transformedItems.push(listItem.create(null, [paragraphNode]));
} else if (isBlockTaskEnabled && child.type === blockTaskItem) {
// blockTaskItem wraps content in paragraphs — extract them directly,
// preserving their fontSize marks
var paragraphs = extractParagraphsFromBlockTaskItem(child);
if (paragraphs.length > 0) {
transformedItems.push(listItem.create(null, paragraphs));
}
} else if (child.type === taskList) {
var transformedNestedList = _transformListRecursively(_objectSpread(_objectSpread({}, props), {}, {
listNode: child
}), onhandleUnsupportedContent);
var lastItem = transformedItems[transformedItems.length - 1];
if ((lastItem === null || lastItem === void 0 ? void 0 : lastItem.type) === listItem) {
// Attach nested list to previous item
var updatedContent = [].concat(_toConsumableArray(lastItem.content.content), [transformedNestedList]);
transformedItems[transformedItems.length - 1] = listItem.create(lastItem.attrs, updatedContent);
} else {
// No previous item, flatten nested items
transformedItems.push.apply(transformedItems, _toConsumableArray(transformedNestedList.content.content));
}
}
} else if (isSourceBulletOrOrdered && isTargetBulletOrOrdered) {
if (child.type === listItem) {
var convertedNestedLists = [];
child.forEach(function (grandChild) {
if (supportedListTypes.has(grandChild.type) && grandChild.type !== targetNodeType) {
var convertedNode = _transformListRecursively(_objectSpread(_objectSpread({}, props), {}, {
listNode: grandChild
}), onhandleUnsupportedContent);
convertedNestedLists.push(convertedNode);
} else {
convertedNestedLists.push(grandChild);
}
});
transformedItems.push(listItem.create(null, convertedNestedLists));
}
}
});
return targetNodeType.create(null, transformedItems);
};
/**
* Transform list structure between different list types
*/
export { _transformListRecursively as transformListRecursively };
export var transformListStructure = function transformListStructure(context) {
var tr = context.tr,
sourceNode = context.sourceNode,
sourcePos = context.sourcePos,
targetNodeType = context.targetNodeType;
var nodes = tr.doc.type.schema.nodes;
var unsupportedContent = [];
var onhandleUnsupportedContent = function onhandleUnsupportedContent(content) {
unsupportedContent.push(content);
};
try {
var listNode = {
node: sourceNode,
pos: sourcePos
};
var sourceList = listNode.node,
listPos = listNode.pos;
// const { taskList, listItem, taskItem, paragraph } = nodes;
var isSourceBulletOrOrdered = isBulletOrOrderedList(sourceList.type);
var isTargetTask = isTaskList(targetNodeType);
var isSourceTask = isTaskList(sourceList.type);
var isTargetBulletOrOrdered = isBulletOrOrderedList(targetNodeType);
var supportedListTypes = getSupportedListTypesSet(nodes);
var newList = _transformListRecursively({
isSourceBulletOrOrdered: isSourceBulletOrOrdered,
isSourceTask: isSourceTask,
isTargetBulletOrOrdered: isTargetBulletOrOrdered,
isTargetTask: isTargetTask,
listNode: sourceList,
schema: tr.doc.type.schema,
supportedListTypes: supportedListTypes,
targetNodeType: targetNodeType
}, onhandleUnsupportedContent);
tr.replaceWith(listPos, listPos + sourceList.nodeSize, [newList].concat(unsupportedContent));
return tr;
} catch (_unused2) {
return tr;
}
};
/**
* Transform between different list types
*/
export var transformBetweenListTypes = function transformBetweenListTypes(context) {
var tr = context.tr,
sourceNode = context.sourceNode,
sourcePos = context.sourcePos,
targetNodeType = context.targetNodeType;
var nodes = tr.doc.type.schema.nodes;
var sourceListType = sourceNode.type;
var isSourceBulletOrOrdered = isBulletOrOrderedList(sourceListType);
var isTargetTask = isTaskList(targetNodeType);
var isSourceTask = isTaskList(sourceListType);
var isTargetBulletOrOrdered = isBulletOrOrderedList(targetNodeType);
// Check if we need structure transformation
var needsStructureTransform = isSourceBulletOrOrdered && isTargetTask || isSourceTask && isTargetBulletOrOrdered;
try {
if (!needsStructureTransform) {
// Simple type change for same structure lists (bullet <-> ordered)
// Apply to the main list
tr.setNodeMarkup(sourcePos, targetNodeType);
// Apply to nested lists
var listStart = sourcePos;
var listEnd = sourcePos + sourceNode.nodeSize;
var supportedListTypesSet = getSupportedListTypesSet(nodes);
tr.doc.nodesBetween(listStart, listEnd, function (node, pos, parent) {
// Only process nested lists (not the root list we already handled)
if (supportedListTypesSet.has(node.type) && pos !== sourcePos) {
var isNestedList = parent && (supportedListTypesSet.has(parent.type) || parent.type === nodes.listItem);
if (isNestedList) {
var shouldTransformNode = node.type === sourceListType || isBulletOrOrderedList(node.type) && isTargetBulletOrOrdered;
if (shouldTransformNode) {
tr.setNodeMarkup(pos, targetNodeType);
}
}
}
return true; // Continue traversing
});
return tr;
} else {
return transformListStructure(context);
}
} catch (_unused3) {
return null;
}
};
/**
* Transform selection to task list
* Handles the special structure where taskItem contains text directly (no paragraph wrapper)
*/
export var transformToTaskList = function transformToTaskList(tr, range, targetNodeType, targetAttrs, nodes) {
try {
var taskItem = nodes.taskItem,
paragraph = nodes.paragraph,
blockTaskItem = nodes.blockTaskItem;
// gating behind platform_editor_small_font_size to support task lists with font size applied,
// but keep this solution general
var isBlockTaskItemEnabled = !!blockTaskItem && expValEquals('platform_editor_small_font_size', 'isEnabled', true);
var listItems = [];
// Process each block in the range
tr.doc.nodesBetween(range.start, range.end, function (node) {
if (node.isBlock) {
var inlineContent = _toConsumableArray(node.content.content);
if (inlineContent.length > 0) {
if (isBlockTaskItemEnabled && node.type === paragraph && node.marks.length > 0) {
listItems.push(createBlockTaskItem({
attrs: targetAttrs,
content: inlineContent,
marks: node.marks,
schema: tr.doc.type.schema
}));
} else {
listItems.push(taskItem.create(targetAttrs, inlineContent));
}
}
}
return false;
});
if (listItems.length === 0) {
return null;
}
// Create the new task list
var newList = targetNodeType.create(targetAttrs, listItems);
// Replace the range with the new list
tr.replaceWith(range.start, range.end, newList);
return tr;
} catch (_unused4) {
return null;
}
};
export var transformTaskListToBlockNodes = function transformTaskListToBlockNodes(context) {
var tr = context.tr,
targetNodeType = context.targetNodeType,
targetAttrs = context.targetAttrs,
sourceNode = context.sourceNode,
sourcePos = context.sourcePos;
var selection = tr.selection;
var schema = selection.$from.doc.type.schema;
var blockTaskItem = schema.nodes.blockTaskItem;
// gating behind platform_editor_small_font_size to support task lists with font size applied,
// but keep this solution general
var isBlockTaskItemEnabled = !!blockTaskItem && expValEquals('platform_editor_small_font_size', 'isEnabled', true);
if (isBlockTaskItemEnabled) {
var blockTaskItemsResult = findChildrenByType(sourceNode, blockTaskItem);
if (blockTaskItemsResult.length > 0 && targetNodeType === schema.nodes.paragraph) {
// blockTaskItem content is (paragraph | extension)+
// Extract paragraph children directly — they may carry block marks (e.g. fontSize)
var _targetNodes = [];
var _iterator = _createForOfIteratorHelper(blockTaskItemsResult),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var blockItem = _step.value.node;
blockItem.forEach(function (child) {
if (child.type === schema.nodes.paragraph) {
_targetNodes.push(child);
}
});
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
if (_targetNodes.length === 0) {
return null;
}
var _slice = new Slice(Fragment.fromArray(_targetNodes), 0, 0);
var _rangeStart = sourcePos !== null ? sourcePos : selection.from;
tr.replaceRange(_rangeStart, _rangeStart + sourceNode.nodeSize, _slice);
return tr;
}
}
// Original logic for regular taskItem children
var taskItemsResult = findChildrenByType(sourceNode, schema.nodes.taskItem);
var taskItems = taskItemsResult.map(function (item) {
return item.node;
});
var taskItemFragments = taskItems.map(function (taskItem) {
return taskItem.content;
});
var targetNodes = [];
// Convert fragments to headings if target is heading
if (targetNodeType === schema.nodes.heading && targetAttrs) {
// convert the fragments to headings
var targetHeadingLevel = targetAttrs.level;
targetNodes = taskItemFragments.map(function (fragment) {
return schema.nodes.heading.createChecked({
level: targetHeadingLevel
}, fragment.content);
});
}
// Convert fragments to paragraphs if target is paragraphs
if (targetNodeType === schema.nodes.paragraph) {
// convert the fragments to paragraphs
targetNodes = taskItemFragments.map(function (fragment) {
return schema.nodes.paragraph.createChecked({}, fragment.content);
});
}
// Convert fragments to code block if target is code block
if (targetNodeType === schema.nodes.codeBlock) {
// convert the fragments to one code block
var codeBlockContent = taskItemFragments.map(function (fragment) {
return fragment.textBetween(0, fragment.size, '\n');
}).join('\n');
targetNodes = [schema.nodes.codeBlock.createChecked({}, schema.text(codeBlockContent))];
}
// Replace the task list node with the new content in the transaction
var slice = new Slice(Fragment.fromArray(targetNodes), 0, 0);
var rangeStart = sourcePos !== null ? sourcePos : selection.from;
tr.replaceRange(rangeStart, rangeStart + sourceNode.nodeSize, slice);
return tr;
};
export var getFormattedNode = function getFormattedNode(tr) {
var selection = tr.selection;
var nodes = tr.doc.type.schema.nodes;
// gating behind platform_editor_small_font_size to support task lists with font size applied,
// but keep this solution general
var isBlockTaskItemEnabled = !!nodes.blockTaskItem && expValEquals('platform_editor_small_font_size', 'isEnabled', true);
// Find the node to format from the current selection
var nodeToFormat;
var nodePos = selection.from;
// Try to find the current node from selection
var selectedNode = findSelectedNodeOfType([nodes.paragraph, nodes.heading, nodes.blockquote, nodes.panel, nodes.expand, nodes.codeBlock, nodes.bulletList, nodes.orderedList, nodes.taskList, nodes.layoutSection])(selection);
if (selectedNode) {
nodeToFormat = selectedNode.node;
nodePos = selectedNode.pos;
} else {
// Try to find parent node (including list parents)
var parentNodeTypes = [nodes.blockquote, nodes.panel, nodes.expand, nodes.codeBlock, nodes.listItem, nodes.taskItem, nodes.layoutSection];
if (isBlockTaskItemEnabled) {
parentNodeTypes.push(nodes.blockTaskItem);
}
var parentNode = findParentNodeOfType(parentNodeTypes)(selection);
if (parentNode) {
nodeToFormat = parentNode.node;
nodePos = parentNode.pos;
var paragraphOrHeadingNode = findParentNodeOfType([nodes.paragraph, nodes.heading])(selection);
// Special case: if we found a listItem/taskItem/blockTaskItem, check if we need the parent list instead
if (parentNode.node.type === nodes.listItem || parentNode.node.type === nodes.taskItem || isBlockTaskItemEnabled && parentNode.node.type === nodes.blockTaskItem) {
var listParent = findParentNodeOfType([nodes.bulletList, nodes.orderedList, nodes.taskList])(selection);
if (listParent) {
// For list transformations, we want the list parent, not the listItem
nodeToFormat = listParent.node;
nodePos = listParent.pos;
}
} else if (parentNode.node.type !== nodes.blockquote && paragraphOrHeadingNode) {
nodeToFormat = paragraphOrHeadingNode.node;
nodePos = paragraphOrHeadingNode.pos;
}
}
}
if (!nodeToFormat) {
nodeToFormat = selection.$from.node();
nodePos = selection.$from.pos;
}
return {
node: nodeToFormat,
pos: nodePos
};
};
/**
* Ensures every `listItem` in a slice starts with a paragraph.
*
* @param slice - The slice to transform
* @param schema - The editor schema, used to create new paragraph nodes
* @returns A new slice with the transformation applied
*/
export var transformSliceEnsureListItemParagraphFirst = function transformSliceEnsureListItemParagraphFirst(slice, schema) {
var _schema$nodes3 = schema.nodes,
listItem = _schema$nodes3.listItem,
paragraph = _schema$nodes3.paragraph;
if (!listItem || !paragraph) {
return slice;
}
return mapSlice(slice, function (node) {
if (node.type === listItem) {
var firstChild = node.firstChild;
if (firstChild && firstChild.type !== paragraph) {
var emptyParagraph = paragraph.createAndFill();
if (emptyParagraph) {
var children = [emptyParagraph];
for (var i = 0; i < node.childCount; i++) {
children.push(node.child(i));
}
return node.copy(Fragment.from(children));
}
}
}
return node;
});
};